summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/components/portfolios
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2022-09-20 22:10:01 +0200
committerGitHub <noreply@github.com>2022-09-20 22:10:01 +0200
commitf7ba5cd9bbf1f4d145c3d3d171c2632d44b5f94a (patch)
tree855256f27ded3cf0ec662119dbf26c3b138a8f5b /opendc-web/opendc-web-ui/src/components/portfolios
parent48d43a83f675db8f5f13755081e56b3cde1a7207 (diff)
parent86bc9e74630374853d11bc1c8f7ba5ffafbaa868 (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.js136
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js109
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>
)
}