diff options
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/webui/components/portfolios')
7 files changed, 722 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js new file mode 100644 index 00000000..fd9a72d2 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenario.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import PropTypes from 'prop-types' +import { PlusIcon } from '@patternfly/react-icons' +import { Button } from '@patternfly/react-core' +import { useState } from 'react' +import { useNewScenario } from '../../data/project' +import NewScenarioModal from './NewScenarioModal' + +function NewScenario({ projectId, portfolioId }) { + const [isVisible, setVisible] = useState(false) + const { mutate: addScenario } = useNewScenario() + + const onSubmit = (projectId, portfolioNumber, data) => { + addScenario({ projectId, portfolioNumber, data }) + setVisible(false) + } + + return ( + <> + <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}> + New Scenario + </Button> + <NewScenarioModal + projectId={projectId} + portfolioId={portfolioId} + isOpen={isVisible} + onSubmit={onSubmit} + onCancel={() => setVisible(false)} + /> + </> + ) +} + +NewScenario.propTypes = { + projectId: PropTypes.number, + portfolioId: PropTypes.number, +} + +export default NewScenario diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js new file mode 100644 index 00000000..ed35c163 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/NewScenarioModal.js @@ -0,0 +1,157 @@ +import PropTypes from 'prop-types' +import React, { useRef, useState } from 'react' +import Modal from '../util/modals/Modal' +import { + Checkbox, + Form, + FormGroup, + FormSection, + FormSelect, + FormSelectOption, + NumberInput, + TextInput, +} from '@patternfly/react-core' +import { useSchedulers, useTraces } from '../../data/experiments' +import { useTopologies } from '../../data/topology' +import { usePortfolio } from '../../data/project' + +function NewScenarioModal({ projectId, portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) { + const { data: portfolio } = usePortfolio(projectId, portfolioId) + const { data: topologies = [] } = useTopologies(projectId, { enabled: isOpen }) + const { data: traces = [] } = useTraces({ enabled: isOpen }) + const { data: schedulers = [] } = useSchedulers({ enabled: isOpen }) + + // eslint-disable-next-line no-unused-vars + const [isSubmitted, setSubmitted] = useState(false) + const [traceLoad, setTraceLoad] = useState(100) + const [trace, setTrace] = useState(undefined) + const [topology, setTopology] = useState(undefined) + const [scheduler, setScheduler] = useState(undefined) + const [failuresEnabled, setFailuresEnabled] = useState(false) + const [opPhenEnabled, setOpPhenEnabled] = useState(false) + const nameInput = useRef(null) + + const resetState = () => { + setSubmitted(false) + setTraceLoad(100) + setTrace(undefined) + setTopology(undefined) + setScheduler(undefined) + setFailuresEnabled(false) + setOpPhenEnabled(false) + nameInput.current.value = '' + } + + const onSubmit = (event) => { + setSubmitted(true) + + if (event) { + event.preventDefault() + } + + const name = nameInput.current.value + + onSubmitUpstream(portfolio.project.id, portfolio.number, { + name, + workload: { + trace: trace || traces[0].id, + samplingFraction: traceLoad / 100, + }, + topology: topology || topologies[0].number, + phenomena: { + failures: failuresEnabled, + interference: opPhenEnabled, + }, + schedulerName: scheduler || schedulers[0], + }) + + resetState() + return true + } + const onCancel = () => { + onCancelUpstream() + resetState() + } + + return ( + <Modal title="New Scenario" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}> + <Form onSubmit={onSubmit}> + <FormGroup label="Name" fieldId="name" isRequired> + <TextInput + id="name" + name="name" + type="text" + isDisabled={portfolio?.scenarios?.length === 0} + defaultValue={portfolio?.scenarios?.length === 0 ? 'Base scenario' : ''} + ref={nameInput} + /> + </FormGroup> + <FormSection title="Workload"> + <FormGroup label="Trace" fieldId="trace" isRequired> + <FormSelect id="trace" name="trace" value={trace} onChange={setTrace}> + {traces.map((trace) => ( + <FormSelectOption value={trace.id} key={trace.id} label={trace.name} /> + ))} + </FormSelect> + </FormGroup> + <FormGroup label="Load Sampling Fraction" fieldId="trace-load" isRequired> + <NumberInput + name="trace-load" + type="number" + min={0} + max={100} + value={traceLoad} + onMinus={() => setTraceLoad((load) => load - 1)} + onPlus={() => setTraceLoad((load) => load + 1)} + onChange={(e) => setTraceLoad(Number(e.target.value))} + unit="%" + /> + </FormGroup> + </FormSection> + <FormSection title="Topology"> + <FormGroup label="Topology" fieldId="topology" isRequired> + <FormSelect id="topology" name="topology" value={topology} onChange={setTopology}> + {topologies.map((topology) => ( + <FormSelectOption value={topology.number} key={topology.number} label={topology.name} /> + ))} + </FormSelect> + </FormGroup> + + <FormGroup label="Scheduler" fieldId="scheduler" isRequired> + <FormSelect id="scheduler" name="scheduler" value={scheduler} onChange={setScheduler}> + {schedulers.map((scheduler) => ( + <FormSelectOption value={scheduler} key={scheduler} label={scheduler} /> + ))} + </FormSelect> + </FormGroup> + </FormSection> + <FormSection title="Operational Phenomena"> + <Checkbox + label="Failures" + id="failures" + name="failures" + isChecked={failuresEnabled} + onChange={() => setFailuresEnabled((e) => !e)} + /> + <Checkbox + label="Performance Interference" + id="perf-interference" + name="perf-interference" + isChecked={opPhenEnabled} + onChange={() => setOpPhenEnabled((e) => !e)} + /> + </FormSection> + </Form> + </Modal> + ) +} + +NewScenarioModal.propTypes = { + projectId: PropTypes.number, + portfolioId: PropTypes.number, + isOpen: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +} + +export default NewScenarioModal diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js new file mode 100644 index 00000000..e561b655 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioOverview.js @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import PropTypes from 'prop-types' +import { + Card, + CardActions, + CardBody, + CardHeader, + CardTitle, + Chip, + ChipGroup, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, + Skeleton, +} from '@patternfly/react-core' +import React from 'react' +import { usePortfolio } from '../../data/project' +import { METRIC_NAMES } from '../../util/available-metrics' +import NewScenario from './NewScenario' +import ScenarioTable from './ScenarioTable' + +function PortfolioOverview({ projectId, portfolioId }) { + const { status, data: portfolio } = usePortfolio(projectId, portfolioId) + + return ( + <Grid hasGutter> + <GridItem md={2}> + <Card> + <CardTitle>Details</CardTitle> + <CardBody> + <DescriptionList> + <DescriptionListGroup> + <DescriptionListTerm>Name</DescriptionListTerm> + <DescriptionListDescription> + {portfolio?.name ?? <Skeleton screenreaderText="Loading portfolio" />} + </DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Scenarios</DescriptionListTerm> + <DescriptionListDescription> + {portfolio?.scenarios?.length ?? <Skeleton screenreaderText="Loading portfolio" />} + </DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Metrics</DescriptionListTerm> + <DescriptionListDescription> + {portfolio ? ( + portfolio.targets.metrics.length > 0 ? ( + <ChipGroup> + {portfolio.targets.metrics.map((metric) => ( + <Chip isReadOnly key={metric}> + {METRIC_NAMES[metric]} + </Chip> + ))} + </ChipGroup> + ) : ( + 'No metrics enabled' + ) + ) : ( + <Skeleton screenreaderText="Loading portfolio" /> + )} + </DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Repeats per Scenario</DescriptionListTerm> + <DescriptionListDescription> + {portfolio?.targets?.repeats ?? <Skeleton screenreaderText="Loading portfolio" />} + </DescriptionListDescription> + </DescriptionListGroup> + </DescriptionList> + </CardBody> + </Card> + </GridItem> + <GridItem md={6}> + <Card> + <CardHeader> + <CardActions> + <NewScenario projectId={projectId} portfolioId={portfolioId} /> + </CardActions> + <CardTitle>Scenarios</CardTitle> + </CardHeader> + <CardBody> + <ScenarioTable portfolio={portfolio} status={status} /> + </CardBody> + </Card> + </GridItem> + </Grid> + ) +} + +PortfolioOverview.propTypes = { + projectId: PropTypes.number, + portfolioId: PropTypes.number, +} + +export default PortfolioOverview diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js new file mode 100644 index 00000000..dbfa928f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResultInfo.js @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import PropTypes from 'prop-types' +import { Tooltip } from '@patternfly/react-core' +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons' +import { METRIC_DESCRIPTIONS } from '../../util/available-metrics' + +function PortfolioResultInfo({ metric }) { + return ( + <Tooltip position="top" content={<div>{METRIC_DESCRIPTIONS[metric]}</div>}> + <OutlinedQuestionCircleIcon title="Metric information" /> + </Tooltip> + ) +} + +PortfolioResultInfo.propTypes = { + metric: PropTypes.string.isRequired, +} + +export default PortfolioResultInfo diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js new file mode 100644 index 00000000..62150fa7 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/PortfolioResults.js @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { mean, std } from 'mathjs' +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, + CardActions, + CardBody, + CardHeader, + CardTitle, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Grid, + GridItem, + 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' + +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.jobs && scenario.jobs[scenario.jobs.length - 1].results) + .map((scenario) => { + const job = scenario.jobs[scenario.jobs.length - 1] + return { + metric, + x: scenario.name, + y: mean(job.results[metric]), + errorY: std(job.results[metric]), + label, + } + }) + }) + return dataPerMetric + }, [scenarios]) + + const categories = useMemo(() => ({ x: scenarios.map((s) => s.name).reverse() }), [scenarios]) + + if (status === 'loading') { + return ( + <Bullseye> + <EmptyState> + <EmptyStateIcon variant="container" component={Spinner} /> + <Title size="lg" headingLevel="h4"> + Loading Results + </Title> + </EmptyState> + </Bullseye> + ) + } else if (status === 'error') { + return ( + <Bullseye> + <EmptyState> + <EmptyStateIcon variant="container" component={ErrorCircleOIcon} /> + <Title size="lg" headingLevel="h4"> + Unable to connect + </Title> + <EmptyStateBody> + There was an error retrieving data. Check your connection and try again. + </EmptyStateBody> + </EmptyState> + </Bullseye> + ) + } else if (scenarios.length === 0) { + return ( + <Bullseye> + <EmptyState> + <EmptyStateIcon variant="container" component={CubesIcon} /> + <Title size="lg" headingLevel="h4"> + No results + </Title> + <EmptyStateBody> + No results are currently available for this portfolio. Run a scenario to obtain simulation + results. + </EmptyStateBody> + <NewScenario projectId={projectId} portfolioId={portfolioId} /> + </EmptyState> + </Bullseye> + ) + } + + return ( + <Grid hasGutter> + {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 + /> + </Chart> + </CardBody> + </Card> + </GridItem> + ) + )} + </Grid> + ) +} + +PortfolioResults.propTypes = { + projectId: PropTypes.number, + portfolioId: PropTypes.number, +} + +export default PortfolioResults diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js new file mode 100644 index 00000000..99d83f64 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioState.js @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { ClockIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons' +import { JobState } from '../../shapes' + +function ScenarioState({ state }) { + switch (state) { + case 'PENDING': + case 'CLAIMED': + return ( + <span> + <ClockIcon color="blue" /> Queued + </span> + ) + case 'RUNNING': + return ( + <span> + <ClockIcon color="green" /> Running + </span> + ) + case 'FINISHED': + return ( + <span> + <CheckCircleIcon color="green" /> Finished + </span> + ) + case 'FAILED': + return ( + <span> + <ErrorCircleOIcon color="red" /> Failed + </span> + ) + } + + return 'Unknown' +} + +ScenarioState.propTypes = { + state: JobState.isRequired, +} + +export default ScenarioState diff --git a/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js new file mode 100644 index 00000000..b068d045 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/portfolios/ScenarioTable.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Bullseye } from '@patternfly/react-core' +import Link from 'next/link' +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' +import ScenarioState from './ScenarioState' +import { useDeleteScenario } from '../../data/project' + +function ScenarioTable({ portfolio, status }) { + const { mutate: deleteScenario } = useDeleteScenario() + const projectId = portfolio?.project?.id + const scenarios = portfolio?.scenarios ?? [] + + const actions = ({ number }) => [ + { + title: 'Delete Scenario', + onClick: () => deleteScenario({ projectId: projectId, number }), + isDisabled: number === 0, + }, + ] + + return ( + <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}`}> + {scenario.topology.name} + </Link> + ) : ( + 'Unknown Topology' + )} + </Td> + <Td dataLabel="Workload">{`${scenario.workload.trace.name} (${ + scenario.workload.samplingFraction * 100 + }%)`}</Td> + <Td dataLabel="State"> + <ScenarioState state={scenario.jobs[scenario.jobs.length - 1].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> + ) +} + +ScenarioTable.propTypes = { + portfolio: Portfolio, + status: Status.isRequired, +} + +export default ScenarioTable |
