diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-09-20 22:10:01 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-09-20 22:10:01 +0200 |
| commit | f7ba5cd9bbf1f4d145c3d3d171c2632d44b5f94a (patch) | |
| tree | 855256f27ded3cf0ec662119dbf26c3b138a8f5b /opendc-web/opendc-web-ui/src/components/portfolios | |
| parent | 48d43a83f675db8f5f13755081e56b3cde1a7207 (diff) | |
| parent | 86bc9e74630374853d11bc1c8f7ba5ffafbaa868 (diff) | |
merge: Improve web interface (#100)
This pull request addresses several issues with the current web interface.
## Implementation Notes :hammer_and_pick:
* Update dependencies of web UI where possible
* Fix deletion of topology
* Fix duplication of topology
* Only display selected metrics
* Use correct color for login button
* Fix z-index of context selector
* Move project selector into masthead
* Reduce height of application header
* Redesign projects page
* Use PatternFly Charts for plots
* Do not fail on stale Redux state
* Fix overflow of topology sidebar
* Fix deletion of portfolios
* Migrate to composable table
## External Dependencies :four_leaf_clover:
* `classnames` has been replaced by `clsx`
* PatternFly Charts have replaced the use of `recharts`
Diffstat (limited to 'opendc-web/opendc-web-ui/src/components/portfolios')
| -rw-r--r-- | opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js | 136 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js | 109 |
2 files changed, 132 insertions, 113 deletions
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..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 React 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 { mean, std } from 'mathjs' -import approx from 'approximate-number' +import React, { useMemo } from 'react' +import PropTypes from 'prop-types' +import { VictoryErrorBar } from 'victory-errorbar' +import { METRIC_NAMES, METRIC_UNITS, AVAILABLE_METRICS } from '../../util/available-metrics' import { Bullseye, Card, @@ -41,15 +40,36 @@ 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' 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 = 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 ( @@ -94,59 +114,57 @@ 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]), - })) - }) - return ( <Grid hasGutter> - {AVAILABLE_METRICS.map((metric) => ( - <GridItem xl={6} lg={12} key={metric}> - <Card> - <CardHeader> - <CardActions> - <PortfolioResultInfo metric={metric} /> - </CardActions> - <CardTitle>{METRIC_NAMES[metric]}</CardTitle> - </CardHeader> - <CardBody> - <ResponsiveContainer aspect={16 / 9} width="100%"> - <ComposedChart - data={dataPerMetric[metric]} - margin={{ left: 35, bottom: 15 }} - layout="vertical" - > - <CartesianGrid strokeDasharray="3 3" /> - <XAxis - tickFormatter={(tick) => approx(tick)} - label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} - type="number" - /> - <YAxis dataKey="name" type="category" /> - <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} /> - <Scatter dataKey="value" opacity={0} isAnimationActive={false}> - <ErrorBar - dataKey="errorX" - width={10} - strokeWidth={3} - stroke="#FF6600" - direction="x" + {AVAILABLE_METRICS.map( + (metric) => + selectedMetrics.has(metric) && ( + <GridItem xl={6} lg={12} key={metric}> + <Card> + <CardHeader> + <CardActions> + <PortfolioResultInfo metric={metric} /> + </CardActions> + <CardTitle>{METRIC_NAMES[metric]}</CardTitle> + </CardHeader> + <CardBody> + <Chart + width={650} + height={250} + padding={{ + top: 10, + bottom: 60, + left: 130, + }} + domainPadding={25} + > + <ChartAxis /> + <ChartAxis + dependentAxis + showGrid + label={METRIC_UNITS[metric]} + fixLabelOverlap + /> + <ChartBar + categories={categories} + data={dataPerMetric[metric]} + labelComponent={<ChartTooltip constrainToVisibleArea />} + barWidth={25} + horizontal + /> + <VictoryErrorBar + categories={categories} + data={dataPerMetric[metric]} + errorY={(d) => d.errorY} + labelComponent={<></>} + horizontal /> - </Scatter> - </ComposedChart> - </ResponsiveContainer> - </CardBody> - </Card> - </GridItem> - ))} + </Chart> + </CardBody> + </Card> + </GridItem> + ) + )} </Grid> ) } 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 ? ( - <Link href={`/projects/${projectId}/topologies/${topology.number}`}> - <a>{topology.name}</a> - </Link> - ) : ( - 'Unknown Topology' - ), - }, - `${scenario.workload.trace.name} (${scenario.workload.samplingFraction * 100}%)`, - { title: <ScenarioState state={scenario.job.state} /> }, - ] - }) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 4 }, - title: ( - <TableEmptyState - status={status} - loadingTitle="Loading Scenarios" - emptyTitle="No scenarios" - emptyText="You have not created any scenario for this portfolio yet. Click the New Scenario button to create one." - /> - ), - }, - ], - }, - ] - - 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 ( - <Table - aria-label="Scenario List" - variant="compact" - cells={columns} - rows={rows} - actionResolver={scenarios.length > 0 ? actionResolver : undefined} - > - <TableHeader /> - <TableBody /> - </Table> + <TableComposable aria-label="Scenario List" variant="compact"> + <Thead> + <Tr> + <Th>Name</Th> + <Th>Topology</Th> + <Th>Trace</Th> + <Th>State</Th> + </Tr> + </Thead> + <Tbody> + {scenarios.map((scenario) => ( + <Tr key={scenario.id}> + <Td dataLabel="Name">{scenario.name}</Td> + <Td dataLabel="Topology"> + {scenario.topology ? ( + <Link href={`/projects/${projectId}/topologies/${scenario.topology.number}`}> + <a>{scenario.topology.name}</a> + </Link> + ) : ( + 'Unknown Topology' + )} + , + </Td> + <Td dataLabel="Workload">{`${scenario.workload.trace.name} (${ + scenario.workload.samplingFraction * 100 + }%)`}</Td> + <Td dataLabel="State"> + <ScenarioState state={scenario.job.state} /> + </Td> + <Td isActionCell> + <ActionsColumn items={actions(scenario)} /> + </Td> + </Tr> + ))} + {scenarios.length === 0 && ( + <Tr> + <Td colSpan={4}> + <Bullseye> + <TableEmptyState + status={status} + loadingTitle="Loading Scenarios" + emptyTitle="No scenarios" + emptyText="You have not created any scenario for this portfolio yet. Click the New Scenario button to create one." + /> + </Bullseye> + </Td> + </Tr> + )} + </Tbody> + </TableComposable> ) } |
