From b8f726cd84ee5b7b0816d04d802f53f0815f6d82 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 14 Sep 2022 14:23:02 +0200 Subject: fix(web/ui): Only display selected metrics This change fixes an issue with the web interface where all available metrics were shown to the user, instead of the metrics belonging to the portfolio. --- .../src/components/portfolios/PortfolioResults.js | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components/portfolios') diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js index f63f0c7f..33604896 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -20,10 +20,10 @@ * SOFTWARE. */ -import React from 'react' +import React, { useMemo } from 'react' import PropTypes from 'prop-types' import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' -import { AVAILABLE_METRICS, METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics' +import { METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics' import { mean, std } from 'mathjs' import approx from 'approximate-number' import { @@ -46,10 +46,9 @@ import { usePortfolio } from '../../data/project' import PortfolioResultInfo from './PortfolioResultInfo' import NewScenario from './NewScenario' -const PortfolioResults = ({ projectId, portfolioId }) => { - const { status, data: scenarios = [] } = usePortfolio(projectId, portfolioId, { - select: (portfolio) => portfolio.scenarios, - }) +function PortfolioResults({ projectId, portfolioId }) { + const { status, data: portfolio } = usePortfolio(projectId, portfolioId) + const scenarios = portfolio?.scenarios ?? [] if (status === 'loading') { return ( @@ -94,21 +93,24 @@ const PortfolioResults = ({ projectId, portfolioId }) => { ) } - const dataPerMetric = {} - - AVAILABLE_METRICS.forEach((metric) => { - dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.job?.results) - .map((scenario) => ({ - name: scenario.name, - value: mean(scenario.job.results[metric]), - errorX: std(scenario.job.results[metric]), - })) - }) + const metrics = portfolio?.targets?.metrics ?? [] + const dataPerMetric = useMemo(() => { + const dataPerMetric = {} + metrics.forEach((metric) => { + dataPerMetric[metric] = scenarios + .filter((scenario) => scenario.job?.results) + .map((scenario) => ({ + name: scenario.name, + value: mean(scenario.job.results[metric]), + errorX: std(scenario.job.results[metric]), + })) + }) + return dataPerMetric + }, [scenarios, metrics]) return ( - {AVAILABLE_METRICS.map((metric) => ( + {metrics.map((metric) => ( -- cgit v1.2.3 From 98bc4c3e9458aea98890b770493f14327a7bc7c4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 15 Sep 2022 22:52:00 +0200 Subject: refactor(web/ui): Use PatternFly Charts for plots This change updates the OpenDC web interface to use the PatternFly Charts package to render the results of a portfolio. Previously, we used Recharts, but this package does not support SSR, whereas the PatternFly Charts package matches our design framework. --- .../src/components/portfolios/PortfolioResults.js | 132 ++++++++++++--------- 1 file changed, 74 insertions(+), 58 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components/portfolios') diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js index 33604896..f50105ed 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -20,12 +20,11 @@ * SOFTWARE. */ +import { mean, std } from 'mathjs' import React, { useMemo } from 'react' import PropTypes from 'prop-types' -import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' -import { METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics' -import { mean, std } from 'mathjs' -import approx from 'approximate-number' +import { VictoryErrorBar } from 'victory-errorbar' +import { METRIC_NAMES, METRIC_UNITS, AVAILABLE_METRICS } from '../../util/available-metrics' import { Bullseye, Card, @@ -41,6 +40,7 @@ import { Spinner, Title, } from '@patternfly/react-core' +import { Chart, ChartAxis, ChartBar, ChartTooltip } from '@patternfly/react-charts' import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons' import { usePortfolio } from '../../data/project' import PortfolioResultInfo from './PortfolioResultInfo' @@ -48,7 +48,28 @@ import NewScenario from './NewScenario' function PortfolioResults({ projectId, portfolioId }) { const { status, data: portfolio } = usePortfolio(projectId, portfolioId) - const scenarios = portfolio?.scenarios ?? [] + const scenarios = useMemo(() => portfolio?.scenarios ?? [], [portfolio]) + + const label = ({ datum }) => + `${datum.x}: ${datum.y.toLocaleString()} ± ${datum.errorY.toLocaleString()} ${METRIC_UNITS[datum.metric]}` + const selectedMetrics = new Set(portfolio?.targets?.metrics ?? []) + const dataPerMetric = useMemo(() => { + const dataPerMetric = {} + AVAILABLE_METRICS.forEach((metric) => { + dataPerMetric[metric] = scenarios + .filter((scenario) => scenario.job?.results) + .map((scenario) => ({ + metric, + x: scenario.name, + y: mean(scenario.job.results[metric]), + errorY: std(scenario.job.results[metric]), + label, + })) + }) + return dataPerMetric + }, [scenarios]) + + const categories = useMemo(() => ({ x: scenarios.map((s) => s.name).reverse() }), [scenarios]) if (status === 'loading') { return ( @@ -93,62 +114,57 @@ function PortfolioResults({ projectId, portfolioId }) { ) } - const metrics = portfolio?.targets?.metrics ?? [] - const dataPerMetric = useMemo(() => { - const dataPerMetric = {} - metrics.forEach((metric) => { - dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.job?.results) - .map((scenario) => ({ - name: scenario.name, - value: mean(scenario.job.results[metric]), - errorX: std(scenario.job.results[metric]), - })) - }) - return dataPerMetric - }, [scenarios, metrics]) - return ( - {metrics.map((metric) => ( - - - - - - - {METRIC_NAMES[metric]} - - - - - - approx(tick)} - label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} - type="number" - /> - - - - + selectedMetrics.has(metric) && ( + + + + + + + {METRIC_NAMES[metric]} + + + + + + } + barWidth={25} + horizontal + /> + d.errorY} + labelComponent={<>} + horizontal /> - - - - - - - ))} + + + + + ) + )} ) } -- cgit v1.2.3 From 86bc9e74630374853d11bc1c8f7ba5ffafbaa868 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Sep 2022 14:28:40 +0200 Subject: refactor(web/ui): Migrate to composable table This change updates the web interface to use the composable table API offered by PatternFly 4. This has replaced the legacy table API which will be removed in the next major version of PatternFly. --- .../src/components/portfolios/ScenarioTable.js | 109 +++++++++++---------- 1 file changed, 55 insertions(+), 54 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components/portfolios') diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js index 68647957..8dc52f7a 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js @@ -20,8 +20,9 @@ * SOFTWARE. */ +import { Bullseye } from '@patternfly/react-core' import Link from 'next/link' -import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn } from '@patternfly/react-table' import React from 'react' import { Portfolio, Status } from '../../shapes' import TableEmptyState from '../util/TableEmptyState' @@ -33,65 +34,65 @@ function ScenarioTable({ portfolio, status }) { const projectId = portfolio?.project?.id const scenarios = portfolio?.scenarios ?? [] - const columns = ['Name', 'Topology', 'Trace', 'State'] - const rows = - scenarios.length > 0 - ? scenarios.map((scenario) => { - const topology = scenario.topology - - return [ - scenario.name, - { - title: topology ? ( - - {topology.name} - - ) : ( - 'Unknown Topology' - ), - }, - `${scenario.workload.trace.name} (${scenario.workload.samplingFraction * 100}%)`, - { title: }, - ] - }) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 4 }, - title: ( - - ), - }, - ], - }, - ] - - const actionResolver = (_, { rowIndex }) => [ + const actions = ({ number }) => [ { title: 'Delete Scenario', - onClick: (_, rowId) => deleteScenario({ projectId: projectId, number: scenarios[rowId].number }), - isDisabled: rowIndex === 0, + onClick: () => deleteScenario({ projectId: projectId, number }), + isDisabled: number === 0, }, ] return ( - 0 ? actionResolver : undefined} - > - - -
+ + + + Name + Topology + Trace + State + + + + {scenarios.map((scenario) => ( + + {scenario.name} + + {scenario.topology ? ( + + {scenario.topology.name} + + ) : ( + 'Unknown Topology' + )} + , + + {`${scenario.workload.trace.name} (${ + scenario.workload.samplingFraction * 100 + }%)`} + + + + + + + + ))} + {scenarios.length === 0 && ( + + + + + + + + )} + + ) } -- cgit v1.2.3