diff options
Diffstat (limited to 'opendc-web/opendc-web-ui')
64 files changed, 537 insertions, 552 deletions
diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js index 680d49ce..1a9877d0 100644 --- a/opendc-web/opendc-web-ui/src/api/index.js +++ b/opendc-web/opendc-web-ui/src/api/index.js @@ -47,5 +47,5 @@ export async function request(auth, path, method = 'GET', body) { throw response.message } - return json.data + return json } diff --git a/opendc-web/opendc-web-ui/src/api/portfolios.js b/opendc-web/opendc-web-ui/src/api/portfolios.js index 82ac0ced..d818876f 100644 --- a/opendc-web/opendc-web-ui/src/api/portfolios.js +++ b/opendc-web/opendc-web-ui/src/api/portfolios.js @@ -22,22 +22,18 @@ import { request } from './index' -export function fetchPortfolio(auth, portfolioId) { - return request(auth, `portfolios/${portfolioId}`) +export function fetchPortfolio(auth, projectId, number) { + return request(auth, `projects/${projectId}/portfolios/${number}`) } -export function fetchPortfoliosOfProject(auth, projectId) { +export function fetchPortfolios(auth, projectId) { return request(auth, `projects/${projectId}/portfolios`) } -export function addPortfolio(auth, portfolio) { - return request(auth, `projects/${portfolio.projectId}/portfolios`, 'POST', { portfolio }) +export function addPortfolio(auth, projectId, portfolio) { + return request(auth, `projects/${projectId}/portfolios`, 'POST', portfolio) } -export function updatePortfolio(auth, portfolioId, portfolio) { - return request(auth, `portfolios/${portfolioId}`, 'PUT', { portfolio }) -} - -export function deletePortfolio(auth, portfolioId) { - return request(auth, `portfolios/${portfolioId}`, 'DELETE') +export function deletePortfolio(auth, projectId, number) { + return request(auth, `projects/${projectId}/portfolios/${number}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/prefabs.js b/opendc-web/opendc-web-ui/src/api/prefabs.js deleted file mode 100644 index eb9aa23c..00000000 --- a/opendc-web/opendc-web-ui/src/api/prefabs.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { request } from './index' - -export function getPrefab(auth, prefabId) { - return request(auth, `prefabs/${prefabId}`) -} - -export function addPrefab(auth, prefab) { - return request(auth, 'prefabs/', 'POST', { prefab }) -} - -export function updatePrefab(auth, prefab) { - return request(auth, `prefabs/${prefab._id}`, 'PUT', { prefab }) -} - -export function deletePrefab(auth, prefabId) { - return request(auth, `prefabs/${prefabId}`, 'DELETE') -} diff --git a/opendc-web/opendc-web-ui/src/api/projects.js b/opendc-web/opendc-web-ui/src/api/projects.js index 4123b371..e7e095da 100644 --- a/opendc-web/opendc-web-ui/src/api/projects.js +++ b/opendc-web/opendc-web-ui/src/api/projects.js @@ -31,11 +31,7 @@ export function fetchProject(auth, projectId) { } export function addProject(auth, project) { - return request(auth, 'projects/', 'POST', { project }) -} - -export function updateProject(auth, project) { - return request(auth, `projects/${project._id}`, 'PUT', { project }) + return request(auth, 'projects/', 'POST', project) } export function deleteProject(auth, projectId) { diff --git a/opendc-web/opendc-web-ui/src/api/scenarios.js b/opendc-web/opendc-web-ui/src/api/scenarios.js index 88516caa..7eeb8f28 100644 --- a/opendc-web/opendc-web-ui/src/api/scenarios.js +++ b/opendc-web/opendc-web-ui/src/api/scenarios.js @@ -22,22 +22,18 @@ import { request } from './index' -export function fetchScenario(auth, scenarioId) { - return request(auth, `scenarios/${scenarioId}`) +export function fetchScenario(auth, projectId, scenarioId) { + return request(auth, `projects/${projectId}/scenarios/${scenarioId}`) } -export function fetchScenariosOfPortfolio(auth, portfolioId) { - return request(auth, `portfolios/${portfolioId}/scenarios`) +export function fetchScenariosOfPortfolio(auth, projectId, portfolioId) { + return request(auth, `projects/${projectId}/portfolios/${portfolioId}/scenarios`) } -export function addScenario(auth, scenario) { - return request(auth, `portfolios/${scenario.portfolioId}/scenarios`, 'POST', { scenario }) +export function addScenario(auth, projectId, portfolioId, scenario) { + return request(auth, `projects/${projectId}/portfolios/${portfolioId}/scenarios`, 'POST', scenario) } -export function updateScenario(auth, scenarioId, scenario) { - return request(auth, `scenarios/${scenarioId}`, 'PUT', { scenario }) -} - -export function deleteScenario(auth, scenarioId) { - return request(auth, `scenarios/${scenarioId}`, 'DELETE') +export function deleteScenario(auth, projectId, scenarioId) { + return request(auth, `projects/${projectId}/scenarios/${scenarioId}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/topologies.js b/opendc-web/opendc-web-ui/src/api/topologies.js index bd4e3bc4..0509c6d0 100644 --- a/opendc-web/opendc-web-ui/src/api/topologies.js +++ b/opendc-web/opendc-web-ui/src/api/topologies.js @@ -22,24 +22,23 @@ import { request } from './index' -export function fetchTopology(auth, topologyId) { - return request(auth, `topologies/${topologyId}`) +export function fetchTopology(auth, projectId, number) { + return request(auth, `projects/${projectId}/topologies/${number}`) } -export function fetchTopologiesOfProject(auth, projectId) { +export function fetchTopologies(auth, projectId) { return request(auth, `projects/${projectId}/topologies`) } -export function addTopology(auth, topology) { - return request(auth, `projects/${topology.projectId}/topologies`, 'POST', { topology }) +export function addTopology(auth, projectId, topology) { + return request(auth, `projects/${projectId}/topologies`, 'POST', topology) } export function updateTopology(auth, topology) { - // eslint-disable-next-line no-unused-vars - const { _id, ...data } = topology - return request(auth, `topologies/${topology._id}`, 'PUT', { topology: data }) + const { project, number, rooms } = topology + return request(auth, `projects/${project.id}/topologies/${number}`, 'PUT', { rooms }) } -export function deleteTopology(auth, topologyId) { - return request(auth, `topologies/${topologyId}`, 'DELETE') +export function deleteTopology(auth, projectId, number) { + return request(auth, `projects/${projectId}/topologies/${number}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/components/AppNavigation.js b/opendc-web/opendc-web-ui/src/components/AppNavigation.js index 178c3ec0..77c683a2 100644 --- a/opendc-web/opendc-web-ui/src/components/AppNavigation.js +++ b/opendc-web/opendc-web-ui/src/components/AppNavigation.js @@ -23,15 +23,9 @@ import { Nav, NavItem, NavList } from '@patternfly/react-core' import { useRouter } from 'next/router' import NavItemLink from './util/NavItemLink' -import { useProject } from '../data/project' export function AppNavigation() { - const { pathname, query } = useRouter() - const { project: projectId } = query - const { data: project } = useProject(projectId) - - const nextTopologyId = project?.topologyIds?.[0] - const nextPortfolioId = project?.portfolioIds?.[0] + const { pathname } = useRouter() return ( <Nav variant="horizontal"> @@ -45,28 +39,6 @@ export function AppNavigation() { > Projects </NavItem> - {pathname.startsWith('/projects/[project]') && ( - <> - <NavItem - id="topologies" - to={nextTopologyId ? `/projects/${projectId}/topologies/${nextTopologyId}` : '/projects'} - itemId={1} - component={NavItemLink} - isActive={pathname === '/projects/[project]/topologies/[topology]'} - > - Topologies - </NavItem> - <NavItem - id="portfolios" - to={nextPortfolioId ? `/projects/${projectId}/portfolios/${nextPortfolioId}` : '/projects'} - itemId={2} - component={NavItemLink} - isActive={pathname === '/projects/[project]/portfolios/[portfolio]'} - > - Portfolios - </NavItem> - </> - )} </NavList> </Nav> ) diff --git a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js index 3712cfa0..a99b60c0 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js @@ -22,13 +22,11 @@ import PropTypes from 'prop-types' import { ContextSelector as PFContextSelector, ContextSelectorItem } from '@patternfly/react-core' -import { useMemo, useState, useReducer } from 'react' +import { useMemo, useState } from 'react' import { contextSelector } from './ContextSelector.module.scss' -function ContextSelector({ activeItem, items, onSelect, label }) { - const [isOpen, toggle] = useReducer((isOpen) => !isOpen, false) +function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label }) { const [searchValue, setSearchValue] = useState('') - const filteredItems = useMemo( () => items.filter(({ name }) => name.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1) || items, [items, searchValue] @@ -36,23 +34,22 @@ function ContextSelector({ activeItem, items, onSelect, label }) { return ( <PFContextSelector - menuAppendTo={global.document?.body} className={contextSelector} toggleText={activeItem ? `${label}: ${activeItem.name}` : label} onSearchInputChange={(value) => setSearchValue(value)} searchInputValue={searchValue} isOpen={isOpen} - onToggle={toggle} + onToggle={(_, isOpen) => onToggle(isOpen)} onSelect={(event) => { - const targetId = event.target.value - const target = items.find((item) => item._id === targetId) + const targetId = +event.target.value + const target = items.find((item) => item.id === targetId) - toggle() onSelect(target) + onToggle(!isOpen) }} > {filteredItems.map((item) => ( - <ContextSelectorItem key={item._id} value={item._id}> + <ContextSelectorItem key={item.id} value={item.id}> {item.name} </ContextSelectorItem> ))} @@ -61,7 +58,7 @@ function ContextSelector({ activeItem, items, onSelect, label }) { } const Item = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.any.isRequired, name: PropTypes.string.isRequired, }) @@ -69,6 +66,8 @@ ContextSelector.propTypes = { activeItem: Item, items: PropTypes.arrayOf(Item).isRequired, onSelect: PropTypes.func.isRequired, + onToggle: PropTypes.func.isRequired, + isOpen: PropTypes.bool, label: PropTypes.string, } diff --git a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss index fefba41f..0aa63ee6 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss +++ b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.module.scss @@ -21,7 +21,6 @@ */ .contextSelector { - width: auto; margin-right: 20px; --pf-c-context-selector__toggle--PaddingTop: var(--pf-global--spacer--sm); diff --git a/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js b/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js index 694681ac..c4f2d50e 100644 --- a/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js @@ -21,27 +21,31 @@ */ import { useRouter } from 'next/router' -import { useMemo } from 'react' -import { useProjectPortfolios } from '../../data/project' +import { useState } from 'react' +import { usePortfolios } from '../../data/project' +import { Portfolio } from '../../shapes' import ContextSelector from './ContextSelector' -function PortfolioSelector() { +function PortfolioSelector({ activePortfolio }) { const router = useRouter() - const { project, portfolio: activePortfolioId } = router.query - const { data: portfolios = [] } = useProjectPortfolios(project) - const activePortfolio = useMemo(() => portfolios.find((portfolio) => portfolio._id === activePortfolioId), [ - activePortfolioId, - portfolios, - ]) + + const [isOpen, setOpen] = useState(false) + const { data: portfolios = [] } = usePortfolios(activePortfolio?.project?.id, { enabled: isOpen }) return ( <ContextSelector label="Portfolio" activeItem={activePortfolio} items={portfolios} - onSelect={(portfolio) => router.push(`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`)} + onSelect={(portfolio) => router.push(`/projects/${portfolio.project.id}/portfolios/${portfolio.number}`)} + onToggle={setOpen} + isOpen={isOpen} /> ) } +PortfolioSelector.propTypes = { + activePortfolio: Portfolio, +} + export default PortfolioSelector diff --git a/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js b/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js index 753632ab..7721e04c 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js @@ -20,29 +20,32 @@ * SOFTWARE. */ -import PropTypes from 'prop-types' import { useRouter } from 'next/router' -import { useMemo } from 'react' +import { useState } from 'react' import { useProjects } from '../../data/project' +import { Project } from '../../shapes' import ContextSelector from './ContextSelector' -function ProjectSelector({ projectId }) { +function ProjectSelector({ activeProject }) { const router = useRouter() - const { data: projects = [] } = useProjects() - const activeProject = useMemo(() => projects.find((project) => project._id === projectId), [projectId, projects]) + + const [isOpen, setOpen] = useState(false) + const { data: projects = [] } = useProjects({ enabled: isOpen }) return ( <ContextSelector label="Project" activeItem={activeProject} items={projects} - onSelect={(project) => router.push(`/projects/${project._id}`)} + onSelect={(project) => router.push(`/projects/${project.id}`)} + onToggle={setOpen} + isOpen={isOpen} /> ) } ProjectSelector.propTypes = { - projectId: PropTypes.string, + activeProject: Project, } export default ProjectSelector diff --git a/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js b/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js index d5e51c6c..9cae4cbf 100644 --- a/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js @@ -20,33 +20,32 @@ * SOFTWARE. */ -import PropTypes from 'prop-types' import { useRouter } from 'next/router' -import { useMemo } from 'react' -import { useProjectTopologies } from '../../data/topology' +import { useState } from 'react' +import { useTopologies } from '../../data/topology' +import { Topology } from '../../shapes' import ContextSelector from './ContextSelector' -function TopologySelector({ projectId, topologyId }) { +function TopologySelector({ activeTopology }) { const router = useRouter() - const { data: topologies = [] } = useProjectTopologies(projectId) - const activeTopology = useMemo(() => topologies.find((topology) => topology._id === topologyId), [ - topologyId, - topologies, - ]) + + const [isOpen, setOpen] = useState(false) + const { data: topologies = [] } = useTopologies(activeTopology?.project?.id, { enabled: isOpen }) return ( <ContextSelector label="Topology" activeItem={activeTopology} items={topologies} - onSelect={(topology) => router.push(`/projects/${topology.projectId}/topologies/${topology._id}`)} + onSelect={(topology) => router.push(`/projects/${topology.project.id}/topologies/${topology.number}`)} + onToggle={setOpen} + isOpen={isOpen} /> ) } TopologySelector.propTypes = { - projectId: PropTypes.string, - topologyId: PropTypes.string, + activeTopology: Topology, } export default TopologySelector diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js index 856282a7..fd9a72d2 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenario.js @@ -24,21 +24,15 @@ import PropTypes from 'prop-types' import { PlusIcon } from '@patternfly/react-icons' import { Button } from '@patternfly/react-core' import { useState } from 'react' -import { useMutation } from 'react-query' +import { useNewScenario } from '../../data/project' import NewScenarioModal from './NewScenarioModal' -function NewScenario({ portfolioId }) { +function NewScenario({ projectId, portfolioId }) { const [isVisible, setVisible] = useState(false) - const { mutate: addScenario } = useMutation('addScenario') + const { mutate: addScenario } = useNewScenario() - const onSubmit = (name, portfolioId, trace, topology, operational) => { - addScenario({ - portfolioId, - name, - trace, - topology, - operational, - }) + const onSubmit = (projectId, portfolioNumber, data) => { + addScenario({ projectId, portfolioNumber, data }) setVisible(false) } @@ -48,6 +42,7 @@ function NewScenario({ portfolioId }) { New Scenario </Button> <NewScenarioModal + projectId={projectId} portfolioId={portfolioId} isOpen={isVisible} onSubmit={onSubmit} @@ -58,7 +53,8 @@ function NewScenario({ portfolioId }) { } NewScenario.propTypes = { - portfolioId: PropTypes.string, + projectId: PropTypes.number, + portfolioId: PropTypes.number, } export default NewScenario diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js index 7f620c8c..ed35c163 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/NewScenarioModal.js @@ -12,14 +12,14 @@ import { TextInput, } from '@patternfly/react-core' import { useSchedulers, useTraces } from '../../data/experiments' -import { useProjectTopologies } from '../../data/topology' +import { useTopologies } from '../../data/topology' import { usePortfolio } from '../../data/project' -const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => { - const { data: portfolio } = usePortfolio(portfolioId) - const { data: topologies = [] } = useProjectTopologies(portfolio?.projectId) - const { data: traces = [] } = useTraces() - const { data: schedulers = [] } = useSchedulers() +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) @@ -51,22 +51,19 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC const name = nameInput.current.value - onSubmitUpstream( + onSubmitUpstream(portfolio.project.id, portfolio.number, { name, - portfolio._id, - { - traceId: trace || traces[0]._id, - loadSamplingFraction: traceLoad / 100, + workload: { + trace: trace || traces[0].id, + samplingFraction: traceLoad / 100, }, - { - topologyId: topology || topologies[0]._id, + topology: topology || topologies[0].number, + phenomena: { + failures: failuresEnabled, + interference: opPhenEnabled, }, - { - failuresEnabled, - performanceInterferenceEnabled: opPhenEnabled, - schedulerName: scheduler || schedulers[0].name, - } - ) + schedulerName: scheduler || schedulers[0], + }) resetState() return true @@ -84,8 +81,8 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC id="name" name="name" type="text" - isDisabled={portfolio?.scenarioIds?.length === 0} - defaultValue={portfolio?.scenarioIds?.length === 0 ? 'Base scenario' : ''} + isDisabled={portfolio?.scenarios?.length === 0} + defaultValue={portfolio?.scenarios?.length === 0 ? 'Base scenario' : ''} ref={nameInput} /> </FormGroup> @@ -93,7 +90,7 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC <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} /> + <FormSelectOption value={trace.id} key={trace.id} label={trace.name} /> ))} </FormSelect> </FormGroup> @@ -115,7 +112,7 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC <FormGroup label="Topology" fieldId="topology" isRequired> <FormSelect id="topology" name="topology" value={topology} onChange={setTopology}> {topologies.map((topology) => ( - <FormSelectOption value={topology._id} key={topology._id} label={topology.name} /> + <FormSelectOption value={topology.number} key={topology.number} label={topology.name} /> ))} </FormSelect> </FormGroup> @@ -123,7 +120,7 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC <FormGroup label="Scheduler" fieldId="scheduler" isRequired> <FormSelect id="scheduler" name="scheduler" value={scheduler} onChange={setScheduler}> {schedulers.map((scheduler) => ( - <FormSelectOption value={scheduler.name} key={scheduler.name} label={scheduler.name} /> + <FormSelectOption value={scheduler} key={scheduler} label={scheduler} /> ))} </FormSelect> </FormGroup> @@ -150,7 +147,8 @@ const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onC } NewScenarioModal.propTypes = { - portfolioId: PropTypes.string, + projectId: PropTypes.number, + portfolioId: PropTypes.number, isOpen: PropTypes.bool.isRequired, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js index 580b0a29..e561b655 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioOverview.js @@ -43,8 +43,8 @@ import { METRIC_NAMES } from '../../util/available-metrics' import NewScenario from './NewScenario' import ScenarioTable from './ScenarioTable' -function PortfolioOverview({ portfolioId }) { - const { data: portfolio } = usePortfolio(portfolioId) +function PortfolioOverview({ projectId, portfolioId }) { + const { status, data: portfolio } = usePortfolio(projectId, portfolioId) return ( <Grid hasGutter> @@ -62,16 +62,16 @@ function PortfolioOverview({ portfolioId }) { <DescriptionListGroup> <DescriptionListTerm>Scenarios</DescriptionListTerm> <DescriptionListDescription> - {portfolio?.scenarioIds.length ?? <Skeleton screenreaderText="Loading portfolio" />} + {portfolio?.scenarios?.length ?? <Skeleton screenreaderText="Loading portfolio" />} </DescriptionListDescription> </DescriptionListGroup> <DescriptionListGroup> <DescriptionListTerm>Metrics</DescriptionListTerm> <DescriptionListDescription> - {portfolio?.targets?.enabledMetrics ? ( - portfolio.targets.enabledMetrics.length > 0 ? ( + {portfolio ? ( + portfolio.targets.metrics.length > 0 ? ( <ChipGroup> - {portfolio.targets.enabledMetrics.map((metric) => ( + {portfolio.targets.metrics.map((metric) => ( <Chip isReadOnly key={metric}> {METRIC_NAMES[metric]} </Chip> @@ -88,9 +88,7 @@ function PortfolioOverview({ portfolioId }) { <DescriptionListGroup> <DescriptionListTerm>Repeats per Scenario</DescriptionListTerm> <DescriptionListDescription> - {portfolio?.targets?.repeatsPerScenario ?? ( - <Skeleton screenreaderText="Loading portfolio" /> - )} + {portfolio?.targets?.repeats ?? <Skeleton screenreaderText="Loading portfolio" />} </DescriptionListDescription> </DescriptionListGroup> </DescriptionList> @@ -101,12 +99,12 @@ function PortfolioOverview({ portfolioId }) { <Card> <CardHeader> <CardActions> - <NewScenario portfolioId={portfolioId} /> + <NewScenario projectId={projectId} portfolioId={portfolioId} /> </CardActions> <CardTitle>Scenarios</CardTitle> </CardHeader> <CardBody> - <ScenarioTable portfolioId={portfolioId} /> + <ScenarioTable portfolio={portfolio} status={status} /> </CardBody> </Card> </GridItem> @@ -115,7 +113,8 @@ function PortfolioOverview({ portfolioId }) { } PortfolioOverview.propTypes = { - portfolioId: PropTypes.string, + projectId: PropTypes.number, + portfolioId: PropTypes.number, } export default PortfolioOverview 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 00023d9e..f63f0c7f 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -42,12 +42,14 @@ import { Title, } from '@patternfly/react-core' import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons' -import { usePortfolioScenarios } from '../../data/project' +import { usePortfolio } from '../../data/project' import PortfolioResultInfo from './PortfolioResultInfo' import NewScenario from './NewScenario' -const PortfolioResults = ({ portfolioId }) => { - const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId) +const PortfolioResults = ({ projectId, portfolioId }) => { + const { status, data: scenarios = [] } = usePortfolio(projectId, portfolioId, { + select: (portfolio) => portfolio.scenarios, + }) if (status === 'loading') { return ( @@ -86,7 +88,7 @@ const PortfolioResults = ({ portfolioId }) => { No results are currently available for this portfolio. Run a scenario to obtain simulation results. </EmptyStateBody> - <NewScenario portfolioId={portfolioId} /> + <NewScenario projectId={projectId} portfolioId={portfolioId} /> </EmptyState> </Bullseye> ) @@ -96,11 +98,11 @@ const PortfolioResults = ({ portfolioId }) => { AVAILABLE_METRICS.forEach((metric) => { dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.results) + .filter((scenario) => scenario.job?.results) .map((scenario) => ({ name: scenario.name, - value: mean(scenario.results[metric]), - errorX: std(scenario.results[metric]), + value: mean(scenario.job.results[metric]), + errorX: std(scenario.job.results[metric]), })) }) @@ -150,7 +152,8 @@ const PortfolioResults = ({ portfolioId }) => { } PortfolioResults.propTypes = { - portfolioId: PropTypes.string, + projectId: PropTypes.number, + portfolioId: PropTypes.number, } export default PortfolioResults diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js index 66691580..99d83f64 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioState.js @@ -20,13 +20,13 @@ * SOFTWARE. */ -import PropTypes from 'prop-types' import { ClockIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons' +import { JobState } from '../../shapes' function ScenarioState({ state }) { switch (state) { + case 'PENDING': case 'CLAIMED': - case 'QUEUED': return ( <span> <ClockIcon color="blue" /> Queued @@ -56,7 +56,7 @@ function ScenarioState({ state }) { } ScenarioState.propTypes = { - state: PropTypes.oneOf(['QUEUED', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED']), + state: JobState.isRequired, } export default ScenarioState 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 9966e3ba..68647957 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js @@ -20,44 +20,38 @@ * SOFTWARE. */ -import PropTypes from 'prop-types' import Link from 'next/link' import { Table, TableBody, TableHeader } from '@patternfly/react-table' import React from 'react' +import { Portfolio, Status } from '../../shapes' import TableEmptyState from '../util/TableEmptyState' import ScenarioState from './ScenarioState' -import { usePortfolio, usePortfolioScenarios } from '../../data/project' -import { useProjectTopologies } from '../../data/topology' -import { useMutation } from 'react-query' +import { useDeleteScenario } from '../../data/project' -const ScenarioTable = ({ portfolioId }) => { - const { data: portfolio } = usePortfolio(portfolioId) - const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId) - const { data: topologies } = useProjectTopologies(portfolio?.projectId, { - select: (topologies) => new Map(topologies.map((topology) => [topology._id, topology])), - }) - - const { mutate: deleteScenario } = useMutation('deleteScenario') +function ScenarioTable({ portfolio, status }) { + const { mutate: deleteScenario } = useDeleteScenario() + 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 = topologies?.get(scenario.topology.topologyId) + const topology = scenario.topology return [ scenario.name, { title: topology ? ( - <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}> + <Link href={`/projects/${projectId}/topologies/${topology.number}`}> <a>{topology.name}</a> </Link> ) : ( 'Unknown Topology' ), }, - scenario.trace.traceId, - { title: <ScenarioState state={scenario.simulation.state} /> }, + `${scenario.workload.trace.name} (${scenario.workload.samplingFraction * 100}%)`, + { title: <ScenarioState state={scenario.job.state} /> }, ] }) : [ @@ -82,7 +76,7 @@ const ScenarioTable = ({ portfolioId }) => { const actionResolver = (_, { rowIndex }) => [ { title: 'Delete Scenario', - onClick: (_, rowId) => deleteScenario(scenarios[rowId]._id), + onClick: (_, rowId) => deleteScenario({ projectId: projectId, number: scenarios[rowId].number }), isDisabled: rowIndex === 0, }, ] @@ -102,7 +96,8 @@ const ScenarioTable = ({ portfolioId }) => { } ScenarioTable.propTypes = { - portfolioId: PropTypes.string, + portfolio: Portfolio, + status: Status.isRequired, } export default ScenarioTable diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js index 87ea059d..aebcc3c9 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js +++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js @@ -24,12 +24,12 @@ import PropTypes from 'prop-types' import { PlusIcon } from '@patternfly/react-icons' import { Button } from '@patternfly/react-core' import { useState } from 'react' -import { useMutation } from 'react-query' +import { useNewPortfolio } from '../../data/project' import NewPortfolioModal from './NewPortfolioModal' function NewPortfolio({ projectId }) { const [isVisible, setVisible] = useState(false) - const { mutate: addPortfolio } = useMutation('addPortfolio') + const { mutate: addPortfolio } = useNewPortfolio() const onSubmit = (name, targets) => { addPortfolio({ projectId, name, targets }) @@ -47,7 +47,7 @@ function NewPortfolio({ projectId }) { } NewPortfolio.propTypes = { - projectId: PropTypes.string, + projectId: PropTypes.number, } export default NewPortfolio diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js index 4276d7d4..ba4bc819 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js +++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolioModal.js @@ -67,7 +67,7 @@ const NewPortfolioModal = ({ isOpen, onSubmit: onSubmitUpstream, onCancel: onUps setErrors({ name: true }) return false } else { - onSubmitUpstream(name, { enabledMetrics: selectedMetrics, repeatsPerScenario: repeats }) + onSubmitUpstream(name, { metrics: selectedMetrics, repeats }) } clearState() diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js index 984264dc..bfa7c01a 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js +++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { Button } from '@patternfly/react-core' -import { useMutation } from 'react-query' import { PlusIcon } from '@patternfly/react-icons' +import { useNewProject } from '../../data/project' import { buttonContainer } from './NewProject.module.scss' import TextInputModal from '../util/modals/TextInputModal' @@ -10,7 +10,7 @@ import TextInputModal from '../util/modals/TextInputModal' */ const NewProject = () => { const [isVisible, setVisible] = useState(false) - const { mutate: addProject } = useMutation('addProject') + const { mutate: addProject } = useNewProject() const onSubmit = (name) => { if (name) { diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js index 77c57d26..4c569c56 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js +++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js @@ -21,25 +21,17 @@ */ import PropTypes from 'prop-types' -import produce from 'immer' import { PlusIcon } from '@patternfly/react-icons' import { Button } from '@patternfly/react-core' import { useState } from 'react' -import { useMutation } from "react-query"; -import { useProjectTopologies } from "../../data/topology"; +import { useNewTopology } from '../../data/topology' import NewTopologyModal from './NewTopologyModal' function NewTopology({ projectId }) { const [isVisible, setVisible] = useState(false) - const { data: topologies = [] } = useProjectTopologies(projectId) - const { mutate: addTopology } = useMutation('addTopology') + const { mutate: addTopology } = useNewTopology() - const onSubmit = (name, duplicateId) => { - const candidate = topologies.find((topology) => topology._id === duplicateId) || { projectId, rooms: [] } - const topology = produce(candidate, (draft) => { - delete draft._id - draft.name = name - }) + const onSubmit = (topology) => { addTopology(topology) setVisible(false) } @@ -59,7 +51,7 @@ function NewTopology({ projectId }) { } NewTopology.propTypes = { - projectId: PropTypes.string, + projectId: PropTypes.number, } export default NewTopology diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js index a495f73e..be4256e3 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js +++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js @@ -20,10 +20,11 @@ * SOFTWARE. */ +import produce from 'immer' import PropTypes from 'prop-types' import React, { useRef, useState } from 'react' import { Form, FormGroup, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core' -import { useProjectTopologies } from '../../data/topology' +import { useTopologies } from '../../data/topology' import Modal from '../util/modals/Modal' const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => { @@ -32,10 +33,12 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan const [originTopology, setOriginTopology] = useState(-1) const [errors, setErrors] = useState({}) - const { data: topologies = [] } = useProjectTopologies(projectId) + const { data: topologies = [] } = useTopologies(projectId, { enabled: isOpen }) const clearState = () => { - nameInput.current.value = '' + if (nameInput.current) { + nameInput.current.value = '' + } setSubmitted(false) setOriginTopology(-1) setErrors({}) @@ -53,10 +56,13 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan if (!name) { setErrors({ name: true }) return false - } else if (originTopology === -1) { - onSubmitUpstream(name) } else { - onSubmitUpstream(name, originTopology) + const candidate = topologies.find((topology) => topology.id === originTopology) || { projectId, rooms: [] } + const topology = produce(candidate, (draft) => { + delete draft.id + draft.name = name + }) + onSubmitUpstream(topology) } clearState() @@ -84,7 +90,7 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan <FormSelect id="origin" name="origin" value={originTopology} onChange={setOriginTopology}> <FormSelectOption value={-1} key={-1} label="None - start from scratch" /> {topologies.map((topology) => ( - <FormSelectOption value={topology._id} key={topology._id} label={topology.name} /> + <FormSelectOption value={topology.id} key={topology.id} label={topology.name} /> ))} </FormSelect> </FormGroup> @@ -94,7 +100,7 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan } NewTopologyModal.propTypes = { - projectId: PropTypes.string, + projectId: PropTypes.number, isOpen: PropTypes.bool.isRequired, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, diff --git a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js index 45e399ed..aa679843 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js +++ b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js @@ -25,12 +25,11 @@ import Link from 'next/link' import { Table, TableBody, TableHeader } from '@patternfly/react-table' import React from 'react' import TableEmptyState from '../util/TableEmptyState' -import { useProjectPortfolios } from '../../data/project' -import { useMutation } from 'react-query' +import { usePortfolios, useDeletePortfolio } from '../../data/project' const PortfolioTable = ({ projectId }) => { - const { status, data: portfolios = [] } = useProjectPortfolios(projectId) - const { mutate: deletePortfolio } = useMutation('deletePortfolio') + const { status, data: portfolios = [] } = usePortfolios(projectId) + const { mutate: deletePortfolio } = useDeletePortfolio() const columns = ['Name', 'Scenarios', 'Metrics', 'Repeats'] const rows = @@ -38,20 +37,12 @@ const PortfolioTable = ({ projectId }) => { ? portfolios.map((portfolio) => [ { title: ( - <Link href={`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`}> - {portfolio.name} - </Link> + <Link href={`/projects/${projectId}/portfolios/${portfolio.number}`}>{portfolio.name}</Link> ), }, - - portfolio.scenarioIds.length === 1 ? '1 scenario' : `${portfolio.scenarioIds.length} scenarios`, - - portfolio.targets.enabledMetrics.length === 1 - ? '1 metric' - : `${portfolio.targets.enabledMetrics.length} metrics`, - portfolio.targets.repeatsPerScenario === 1 - ? '1 repeat' - : `${portfolio.targets.repeatsPerScenario} repeats`, + portfolio.scenarios.length === 1 ? '1 scenario' : `${portfolio.scenarios.length} scenarios`, + portfolio.targets.metrics.length === 1 ? '1 metric' : `${portfolio.targets.metrics.length} metrics`, + portfolio.targets.repeats === 1 ? '1 repeat' : `${portfolio.targets.repeats} repeats`, ]) : [ { @@ -77,7 +68,7 @@ const PortfolioTable = ({ projectId }) => { ? [ { title: 'Delete Portfolio', - onClick: (_, rowId) => deletePortfolio(portfolios[rowId]._id), + onClick: (_, rowId) => deletePortfolio({ projectId, number: portfolios[rowId].number }), }, ] : [] @@ -91,7 +82,7 @@ const PortfolioTable = ({ projectId }) => { } PortfolioTable.propTypes = { - projectId: PropTypes.string, + projectId: PropTypes.number, } export default PortfolioTable diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js index 65b8f5a0..3e1656f6 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectOverview.js @@ -92,7 +92,7 @@ function ProjectOverview({ projectId }) { } ProjectOverview.propTypes = { - projectId: PropTypes.string, + projectId: PropTypes.number, } export default ProjectOverview diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js index a7290259..6921578c 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js @@ -5,26 +5,23 @@ import { Project, Status } from '../../shapes' import { Table, TableBody, TableHeader } from '@patternfly/react-table' import { parseAndFormatDateTime } from '../../util/date-time' import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations' -import { useAuth } from '../../auth' import TableEmptyState from '../util/TableEmptyState' const ProjectTable = ({ status, projects, onDelete, isFiltering }) => { - const { user } = useAuth() const columns = ['Project name', 'Last edited', 'Access Rights'] const rows = projects.length > 0 ? projects.map((project) => { - const { level } = project.authorizations.find((auth) => auth.userId === user.sub) - const Icon = AUTH_ICON_MAP[level] + const Icon = AUTH_ICON_MAP[project.role] return [ { - title: <Link href={`/projects/${project._id}`}>{project.name}</Link>, + title: <Link href={`/projects/${project.id}`}>{project.name}</Link>, }, - parseAndFormatDateTime(project.datetimeLastEdited), + parseAndFormatDateTime(project.updatedAt), { title: ( <> - <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[level]} + <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[project.role]} </> ), }, diff --git a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js index 80099ece..ced5304a 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js +++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js @@ -26,26 +26,21 @@ import { Table, TableBody, TableHeader } from '@patternfly/react-table' import React from 'react' import TableEmptyState from '../util/TableEmptyState' import { parseAndFormatDateTime } from '../../util/date-time' -import { useMutation } from 'react-query' -import { useProjectTopologies } from '../../data/topology' +import { useTopologies, useDeleteTopology } from '../../data/topology' const TopologyTable = ({ projectId }) => { - const { status, data: topologies = [] } = useProjectTopologies(projectId) - const { mutate: deleteTopology } = useMutation('deleteTopology') + const { status, data: topologies = [] } = useTopologies(projectId) + const { mutate: deleteTopology } = useDeleteTopology() const columns = ['Name', 'Rooms', 'Last Edited'] const rows = topologies.length > 0 ? topologies.map((topology) => [ { - title: ( - <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}> - {topology.name} - </Link> - ), + title: <Link href={`/projects/${projectId}/topologies/${topology.number}`}>{topology.name}</Link>, }, topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`, - parseAndFormatDateTime(topology.datetimeLastEdited), + parseAndFormatDateTime(topology.updatedAt), ]) : [ { @@ -69,7 +64,7 @@ const TopologyTable = ({ projectId }) => { const actionResolver = (_, { rowIndex }) => [ { title: 'Delete Topology', - onClick: (_, rowId) => deleteTopology(topologies[rowId]._id), + onClick: (_, rowId) => deleteTopology({ projectId, number: topologies[rowId].number }), isDisabled: rowIndex === 0, }, ] @@ -89,7 +84,7 @@ const TopologyTable = ({ projectId }) => { } TopologyTable.propTypes = { - projectId: PropTypes.string, + projectId: PropTypes.number, } export default TopologyTable diff --git a/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js b/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js index 9bf369e9..49e5f095 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js @@ -7,11 +7,11 @@ import { Table, TableBody, TableHeader } from '@patternfly/react-table' import { deleteRoom } from '../../redux/actions/topology/room' import TableEmptyState from '../util/TableEmptyState' -function RoomTable({ topologyId, onSelect }) { +function RoomTable({ projectId, topologyId, onSelect }) { const dispatch = useDispatch() - const { status, data: topology } = useTopology(topologyId) + const { status, data: topology } = useTopology(projectId, topologyId) - const onDelete = (room) => dispatch(deleteRoom(room._id)) + const onDelete = (room) => dispatch(deleteRoom(room.id)) const columns = ['Name', 'Tiles', 'Racks'] const rows = @@ -62,7 +62,8 @@ function RoomTable({ topologyId, onSelect }) { } RoomTable.propTypes = { - topologyId: PropTypes.string, + projectId: PropTypes.number, + topologyId: PropTypes.number, onSelect: PropTypes.func, } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js b/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js index 213a4868..f8ee4990 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/TopologyOverview.js @@ -38,8 +38,8 @@ import { useTopology } from '../../data/topology' import { parseAndFormatDateTime } from '../../util/date-time' import RoomTable from './RoomTable' -function TopologyOverview({ topologyId, onSelect }) { - const { data: topology } = useTopology(topologyId) +function TopologyOverview({ projectId, topologyNumber, onSelect }) { + const { data: topology } = useTopology(projectId, topologyNumber) return ( <Grid hasGutter> <GridItem md={2}> @@ -57,7 +57,7 @@ function TopologyOverview({ topologyId, onSelect }) { <DescriptionListTerm>Last edited</DescriptionListTerm> <DescriptionListDescription> {topology ? ( - parseAndFormatDateTime(topology.datetimeLastEdited) + parseAndFormatDateTime(topology.updatedAt) ) : ( <Skeleton screenreaderText="Loading topology" /> )} @@ -71,7 +71,11 @@ function TopologyOverview({ topologyId, onSelect }) { <Card> <CardTitle>Rooms</CardTitle> <CardBody> - <RoomTable topologyId={topologyId} onSelect={(room) => onSelect('room', room)} /> + <RoomTable + projectId={projectId} + topologyId={topologyNumber} + onSelect={(room) => onSelect('room', room)} + /> </CardBody> </Card> </GridItem> @@ -80,7 +84,8 @@ function TopologyOverview({ topologyId, onSelect }) { } TopologyOverview.propTypes = { - topologyId: PropTypes.string, + projectId: PropTypes.number, + topologyNumber: PropTypes.number, onSelect: PropTypes.func, } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js index 411a5ca7..21be3c79 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js @@ -33,7 +33,7 @@ function TileContainer({ tileId, ...props }) { const dispatch = useDispatch() const onClick = (tile) => { if (tile.rack) { - dispatch(goFromRoomToRack(tile._id)) + dispatch(goFromRoomToRack(tile.id)) } } return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} /> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js index 7d304b6b..fdae53f2 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js @@ -21,6 +21,7 @@ function ImageComponent({ src, x, y, width, height, opacity }) { } }, [src]) + // eslint-disable-next-line jsx-a11y/alt-text return <Image image={image} x={x} y={y} width={width} height={height} opacity={opacity} /> } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js index 46030135..dad2d62d 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js @@ -11,8 +11,8 @@ function RackGroup({ tile }) { <Group> <TileObject positionX={tile.positionX} positionY={tile.positionY} color={RACK_BACKGROUND_COLOR} /> <Group> - <RackSpaceFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} /> - <RackEnergyFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} /> + <RackSpaceFillContainer tileId={tile.id} positionX={tile.positionX} positionY={tile.positionY} /> + <RackEnergyFillContainer tileId={tile.id} positionX={tile.positionX} positionY={tile.positionY} /> </Group> </Group> ) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js index a42e7bb7..3f8b3089 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js @@ -7,7 +7,7 @@ import TileContainer from '../TileContainer' import WallContainer from '../WallContainer' function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick }) { - if (currentRoomInConstruction === room._id) { + if (currentRoomInConstruction === room.id) { return ( <Group onClick={onClick}> {room.tiles.map((tileId) => ( @@ -22,7 +22,7 @@ function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick {(() => { if ( (interactionLevel.mode === 'RACK' || interactionLevel.mode === 'MACHINE') && - interactionLevel.roomId === room._id + interactionLevel.roomId === room.id ) { return [ room.tiles @@ -37,7 +37,7 @@ function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick return room.tiles.map((tileId) => <TileContainer key={tileId} tileId={tileId} />) } })()} - <WallContainer roomId={room._id} /> + <WallContainer roomId={room.id} /> </Group> ) } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js index 5e351691..727f4e25 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js @@ -40,8 +40,8 @@ function RoomHoverLayer() { .map((id) => ({ ...state.topology.rooms[id] })) .filter( (room) => - state.topology.root.rooms.indexOf(room._id) !== -1 && - room._id !== state.construction.currentRoomInConstruction + state.topology.root.rooms.indexOf(room.id) !== -1 && + room.id !== state.construction.currentRoomInConstruction ) ;[...oldRooms, newRoom].forEach((room) => { diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js index 9268f615..6f89e10b 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js @@ -17,7 +17,7 @@ function MachineSidebar({ tileId, position }) { const rack = topology.racks[topology.tiles[tileId].rack] return topology.machines[rack.machines[position - 1]] }) - const machineId = machine._id + const machineId = machine.id return ( <div> <TextContent> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js index 88591208..4507b409 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js @@ -22,7 +22,7 @@ function UnitAddComponent({ units, onAdd }) { selections={selected} > {units.map((unit) => ( - <SelectOption value={unit._id} key={unit._id}> + <SelectOption value={unit.id} key={unit.id}> {unit.name} </SelectOption> ))} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js index 6dcc414f..25e750c4 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js @@ -33,7 +33,7 @@ function UnitListContainer({ machineId, unitType }) { return machine[unitType].map((id) => state.topology[unitType][id]) }) - const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit._id)) + const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit.id)) return <UnitListComponent units={units} unitType={unitType} onDelete={onDelete} /> } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js index e944c2e8..6a0c3ff3 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/AddPrefab.js @@ -22,14 +22,11 @@ import PropTypes from 'prop-types' import React from 'react' -import { useDispatch } from 'react-redux' import { Button } from '@patternfly/react-core' import { SaveIcon } from '@patternfly/react-icons' -import { addPrefab } from '../../../../api/prefabs' -function AddPrefab({ tileId }) { - const dispatch = useDispatch() - const onClick = () => dispatch(addPrefab('name', tileId)) +function AddPrefab() { + const onClick = () => {} // TODO return ( <Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm"> Save this rack to a prefab diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js index 619bb4e2..e1914730 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js @@ -43,7 +43,7 @@ function MachineListContainer({ tileId, ...props }) { <MachineListComponent {...props} machines={machinesNull} - onAdd={(index) => dispatch(addMachine(rack._id, index))} + onAdd={(index) => dispatch(addMachine(rack.id, index))} onSelect={(index) => dispatch(goFromRackToMachine(index))} /> ) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js index 30f38cce..c3422318 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js @@ -5,11 +5,11 @@ import NameComponent from '../NameComponent' import { editRackName } from '../../../../redux/actions/topology/rack' const RackNameContainer = ({ tileId }) => { - const { name: rackName, _id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) + const { name: rackName, id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) const dispatch = useDispatch() const callback = (name) => { if (name) { - dispatch(editRackName(_id, name)) + dispatch(editRackName(id, name)) } } return <NameComponent name={rackName} onEdit={callback} /> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js index fb52d826..72d45bea 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js @@ -27,11 +27,11 @@ import NameComponent from '../NameComponent' import { editRoomName } from '../../../../redux/actions/topology/room' function RoomName({ roomId }) { - const { name: roomName, _id } = useSelector((state) => state.topology.rooms[roomId]) + const { name: roomName, id } = useSelector((state) => state.topology.rooms[roomId]) const dispatch = useDispatch() const callback = (name) => { if (name) { - dispatch(editRoomName(_id, name)) + dispatch(editRoomName(id, name)) } } return <NameComponent name={roomName} onEdit={callback} /> diff --git a/opendc-web/opendc-web-ui/src/data/experiments.js b/opendc-web/opendc-web-ui/src/data/experiments.js index a76ea53f..ca8912a2 100644 --- a/opendc-web/opendc-web-ui/src/data/experiments.js +++ b/opendc-web/opendc-web-ui/src/data/experiments.js @@ -35,13 +35,13 @@ export function configureExperimentClient(queryClient, auth) { /** * Return the available traces to experiment with. */ -export function useTraces() { - return useQuery('traces') +export function useTraces(options) { + return useQuery('traces', options) } /** * Return the available schedulers to experiment with. */ -export function useSchedulers() { - return useQuery('schedulers') +export function useSchedulers(options) { + return useQuery('schedulers', options) } diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 9dcd8532..b1db3da5 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -20,9 +20,9 @@ * SOFTWARE. */ -import { useQuery } from 'react-query' +import { useQuery, useMutation } from 'react-query' import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' -import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfoliosOfProject } from '../api/portfolios' +import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfolios } from '../api/portfolios' import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios' /** @@ -37,79 +37,60 @@ export function configureProjectClient(queryClient, auth) { mutationFn: (data) => addProject(auth, data), onSuccess: async (result) => { queryClient.setQueryData('projects', (old = []) => [...old, result]) - queryClient.setQueryData(['projects', result._id], result) + queryClient.setQueryData(['projects', result.id], result) }, }) queryClient.setMutationDefaults('deleteProject', { mutationFn: (id) => deleteProject(auth, id), onSuccess: async (result) => { - queryClient.setQueryData('projects', (old = []) => old.filter((project) => project._id !== result._id)) - queryClient.removeQueries(['projects', result._id]) + queryClient.setQueryData('projects', (old = []) => old.filter((project) => project.id !== result.id)) + queryClient.removeQueries(['projects', result.id]) }, }) queryClient.setQueryDefaults('portfolios', { - queryFn: ({ queryKey }) => fetchPortfolio(auth, queryKey[1]), - }) - queryClient.setQueryDefaults('project-portfolios', { - queryFn: ({ queryKey }) => fetchPortfoliosOfProject(auth, queryKey[1]), + queryFn: ({ queryKey }) => + queryKey.length === 2 ? fetchPortfolios(auth, queryKey[1]) : fetchPortfolio(auth, queryKey[1], queryKey[2]), }) queryClient.setMutationDefaults('addPortfolio', { - mutationFn: (data) => addPortfolio(auth, data), + mutationFn: ({ projectId, ...data }) => addPortfolio(auth, projectId, data), onSuccess: async (result) => { - queryClient.setQueryData(['projects', result.projectId], (old) => ({ - ...old, - portfolioIds: [...old.portfolioIds, result._id], - })) - queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) => [...old, result]) - queryClient.setQueryData(['portfolios', result._id], result) + queryClient.setQueryData(['portfolios', result.project.id], (old = []) => [...old, result]) + queryClient.setQueryData(['portfolios', result.project.id, result.number], result) }, }) queryClient.setMutationDefaults('deletePortfolio', { - mutationFn: (id) => deletePortfolio(auth, id), + mutationFn: ({ projectId, number }) => deletePortfolio(auth, projectId, number), onSuccess: async (result) => { - queryClient.setQueryData(['projects', result.projectId], (old) => ({ - ...old, - portfolioIds: old.portfolioIds.filter((id) => id !== result._id), - })) - queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) => - old.filter((portfolio) => portfolio._id !== result._id) + queryClient.setQueryData(['portfolios', result.project.id], (old = []) => + old.filter((portfolio) => portfolio.id !== result.id) ) - queryClient.removeQueries(['portfolios', result._id]) + queryClient.removeQueries(['portfolios', result.project.id, result.number]) }, }) queryClient.setQueryDefaults('scenarios', { - queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1]), - }) - queryClient.setQueryDefaults('portfolio-scenarios', { - queryFn: ({ queryKey }) => fetchScenariosOfPortfolio(auth, queryKey[1]), + queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1], queryKey[2]), }) queryClient.setMutationDefaults('addScenario', { - mutationFn: (data) => addScenario(auth, data), + mutationFn: ({ projectId, portfolioNumber, data }) => addScenario(auth, projectId, portfolioNumber, data), onSuccess: async (result) => { // Register updated scenario in cache - queryClient.setQueryData(['scenarios', result._id], result) - queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) => [...old, result]) - - // Add scenario id to portfolio - queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({ + queryClient.setQueryData(['scenarios', result.project.id, result.id], result) + queryClient.setQueryData(['portfolios', result.project.id, result.portfolio.number], (old) => ({ ...old, - scenarioIds: [...old.scenarioIds, result._id], + scenarios: [...old.scenarios, result], })) }, }) queryClient.setMutationDefaults('deleteScenario', { - mutationFn: (id) => deleteScenario(auth, id), + mutationFn: ({ projectId, number }) => deleteScenario(auth, projectId, number), onSuccess: async (result) => { - queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({ + queryClient.removeQueries(['scenarios', result.project.id, result.id]) + queryClient.setQueryData(['portfolios', result.project.id, result.portfolio.number], (old) => ({ ...old, - scenarioIds: old.scenarioIds.filter((id) => id !== result._id), + scenarios: old?.scenarios?.filter((scenario) => scenario.id !== result.id), })) - queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) => - old.filter((scenario) => scenario._id !== result._id) - ) - queryClient.removeQueries(['scenarios', result._id]) }, }) } @@ -129,22 +110,57 @@ export function useProject(projectId, options = {}) { } /** + * Create a mutation for a new project. + */ +export function useNewProject() { + return useMutation('addProject') +} + +/** + * Create a mutation for deleting a project. + */ +export function useDeleteProject() { + return useMutation('deleteProject') +} + +/** * Return the portfolio with the specified identifier. */ -export function usePortfolio(portfolioId, options = {}) { - return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId, ...options }) +export function usePortfolio(projectId, portfolioId, options = {}) { + return useQuery(['portfolios', projectId, portfolioId], { enabled: !!(projectId && portfolioId), ...options }) } /** * Return the portfolios of the specified project. */ -export function useProjectPortfolios(projectId, options = {}) { - return useQuery(['project-portfolios', projectId], { enabled: !!projectId, ...options }) +export function usePortfolios(projectId, options = {}) { + return useQuery(['portfolios', projectId], { enabled: !!projectId, ...options }) +} + +/** + * Create a mutation for a new portfolio. + */ +export function useNewPortfolio() { + return useMutation('addPortfolio') +} + +/** + * Create a mutation for deleting a portfolio. + */ +export function useDeletePortfolio() { + return useMutation('deletePortfolio') +} + +/** + * Create a mutation for a new scenario. + */ +export function useNewScenario() { + return useMutation('addScenario') } /** - * Return the scenarios of the specified portfolio. + * Create a mutation for deleting a scenario. */ -export function usePortfolioScenarios(portfolioId, options = {}) { - return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId, ...options }) +export function useDeleteScenario() { + return useMutation('deleteScenario') } diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js index e068ed8e..cf098c56 100644 --- a/opendc-web/opendc-web-ui/src/data/topology.js +++ b/opendc-web/opendc-web-ui/src/data/topology.js @@ -20,58 +20,69 @@ * SOFTWARE. */ -import { useQuery } from 'react-query' -import { addTopology, deleteTopology, fetchTopologiesOfProject, fetchTopology, updateTopology } from '../api/topologies' +import { useQuery, useMutation } from 'react-query' +import { addTopology, deleteTopology, fetchTopologies, fetchTopology, updateTopology } from '../api/topologies' /** * Configure the query defaults for the topology endpoints. */ export function configureTopologyClient(queryClient, auth) { - queryClient.setQueryDefaults('topologies', { queryFn: ({ queryKey }) => fetchTopology(auth, queryKey[1]) }) - queryClient.setQueryDefaults('project-topologies', { - queryFn: ({ queryKey }) => fetchTopologiesOfProject(auth, queryKey[1]), + queryClient.setQueryDefaults('topologies', { + queryFn: ({ queryKey }) => + queryKey.length === 2 ? fetchTopologies(auth, queryKey[1]) : fetchTopology(auth, queryKey[1], queryKey[2]), }) queryClient.setMutationDefaults('addTopology', { - mutationFn: (data) => addTopology(auth, data), - onSuccess: async (result) => { - queryClient.setQueryData(['projects', result.projectId], (old) => ({ - ...old, - topologyIds: [...old.topologyIds, result._id], - })) - queryClient.setQueryData(['project-topologies', result.projectId], (old = []) => [...old, result]) - queryClient.setQueryData(['topologies', result._id], result) + mutationFn: ({ projectId, ...data }) => addTopology(auth, projectId, data), + onSuccess: (result) => { + queryClient.setQueryData(['topologies', result.project.id], (old = []) => [...old, result]) + queryClient.setQueryData(['topologies', result.project.id, result.number], result) }, }) queryClient.setMutationDefaults('updateTopology', { mutationFn: (data) => updateTopology(auth, data), - onSuccess: (result) => queryClient.setQueryData(['topologies', result._id], result), + onSuccess: (result) => { + queryClient.setQueryData(['topologies', result.project.id], (old = []) => + old.map((topology) => (topology.id === result.id ? result : topology)) + ) + queryClient.setQueryData(['topologies', result.project.id, result.number], result) + }, }) queryClient.setMutationDefaults('deleteTopology', { - mutationFn: (id) => deleteTopology(auth, id), - onSuccess: async (result) => { - queryClient.setQueryData(['projects', result.projectId], (old) => ({ - ...old, - topologyIds: old.topologyIds.filter((id) => id !== result._id), - })) - queryClient.setQueryData(['project-topologies', result.projectId], (old = []) => - old.filter((topology) => topology._id !== result._id) + mutationFn: ({ projectId, id }) => deleteTopology(auth, projectId, id), + onSuccess: (result) => { + queryClient.setQueryData(['topologies', result.project.id], (old = []) => + old.filter((topology) => topology.id !== result.id) ) - queryClient.removeQueries(['topologies', result._id]) + queryClient.removeQueries(['topologies', result.project.id, result.number]) }, }) } /** - * Return the current active topology. + * Fetch the topology with the specified identifier for the specified project. + */ +export function useTopology(projectId, topologyId, options = {}) { + return useQuery(['topologies', projectId, topologyId], { enabled: !!(projectId && topologyId), ...options }) +} + +/** + * Fetch all topologies of the specified project. + */ +export function useTopologies(projectId, options = {}) { + return useQuery(['topologies', projectId], { enabled: !!projectId, ...options }) +} + +/** + * Create a mutation for a new topology. */ -export function useTopology(topologyId, options = {}) { - return useQuery(['topologies', topologyId], { enabled: !!topologyId, ...options }) +export function useNewTopology() { + return useMutation('addTopology') } /** - * Return the topologies of the specified project. + * Create a mutation for deleting a topology. */ -export function useProjectTopologies(projectId, options = {}) { - return useQuery(['project-topologies', projectId], { enabled: !!projectId, ...options }) +export function useDeleteTopology() { + return useMutation('deleteTopology') } diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js index 900ff405..4861f5c1 100644 --- a/opendc-web/opendc-web-ui/src/pages/_app.js +++ b/opendc-web/opendc-web-ui/src/pages/_app.js @@ -22,6 +22,7 @@ import PropTypes from 'prop-types' import Head from 'next/head' +import Script from 'next/script' import { Provider } from 'react-redux' import { useNewQueryClient } from '../data/query' import { useStore } from '../redux' @@ -91,6 +92,19 @@ export default function App(props) { <Inner {...props} /> </AuthProvider> </Sentry.ErrorBoundary> + {/* Google Analytics */} + <Script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" /> + <Script + id="gtag" + dangerouslySetInnerHTML={{ + __html: ` + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'UA-84285092-3'); + `, + }} + /> </> ) } diff --git a/opendc-web/opendc-web-ui/src/pages/_document.js b/opendc-web/opendc-web-ui/src/pages/_document.js index 51d8d3e0..011bf4da 100644 --- a/opendc-web/opendc-web-ui/src/pages/_document.js +++ b/opendc-web/opendc-web-ui/src/pages/_document.js @@ -69,19 +69,6 @@ class OpenDCDocument extends Document { href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet" /> - - {/* Google Analytics */} - <script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" /> - <script - dangerouslySetInnerHTML={{ - __html: ` - window.dataLayer = window.dataLayer || []; - function gtag(){dataLayer.push(arguments);} - gtag('js', new Date()); - gtag('config', 'UA-84285092-3'); - `, - }} - /> </Head> <body> <Main /> diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js index c07a2c31..39fcb4f3 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js +++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js @@ -40,9 +40,9 @@ import BreadcrumbLink from '../../../components/util/BreadcrumbLink' function Project() { const router = useRouter() - const { project: projectId } = router.query + const projectId = +router.query['project'] - const { data: project } = useProject(projectId) + const { data: project } = useProject(+projectId) const breadcrumb = ( <Breadcrumb> @@ -57,7 +57,7 @@ function Project() { const contextSelectors = ( <ContextSelectionSection> - <ProjectSelector projectId={projectId} /> + <ProjectSelector activeProject={project} /> </ContextSelectionSection> ) diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js index d1533d98..68345d0b 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js +++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js @@ -20,6 +20,7 @@ * SOFTWARE. */ +import dynamic from 'next/dynamic' import { useRouter } from 'next/router' import Head from 'next/head' import React, { useRef } from 'react' @@ -42,18 +43,24 @@ import PortfolioSelector from '../../../../components/context/PortfolioSelector' import ProjectSelector from '../../../../components/context/ProjectSelector' import BreadcrumbLink from '../../../../components/util/BreadcrumbLink' import PortfolioOverview from '../../../../components/portfolios/PortfolioOverview' -import PortfolioResults from '../../../../components/portfolios/PortfolioResults' +import { usePortfolio } from '../../../../data/project' + +const PortfolioResults = dynamic(() => import('../../../../components/portfolios/PortfolioResults')) /** * Page that displays the results in a portfolio. */ function Portfolio() { const router = useRouter() - const { project: projectId, portfolio: portfolioId } = router.query + const projectId = +router.query['project'] + const portfolioNumber = +router.query['portfolio'] const overviewRef = useRef(null) const resultsRef = useRef(null) + const { data: portfolio } = usePortfolio(projectId, portfolioNumber) + const project = portfolio?.project + const breadcrumb = ( <Breadcrumb> <BreadcrumbItem to="/projects" component={BreadcrumbLink}> @@ -62,7 +69,11 @@ function Portfolio() { <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}> Project details </BreadcrumbItem> - <BreadcrumbItem to={`/projects/${projectId}/portfolios/${portfolioId}`} component={BreadcrumbLink} isActive> + <BreadcrumbItem + to={`/projects/${projectId}/portfolios/${portfolioNumber}`} + component={BreadcrumbLink} + isActive + > Portfolio </BreadcrumbItem> </Breadcrumb> @@ -70,8 +81,8 @@ function Portfolio() { const contextSelectors = ( <ContextSelectionSection> - <ProjectSelector projectId={projectId} /> - <PortfolioSelector projectId={projectId} portfolioId={portfolioId} /> + <ProjectSelector activeProject={project} /> + <PortfolioSelector activePortfolio={portfolio} /> </ContextSelectionSection> ) @@ -104,10 +115,10 @@ function Portfolio() { </PageSection> <PageSection isFilled> <TabContent eventKey={0} id="overview" ref={overviewRef} aria-label="Overview tab"> - <PortfolioOverview portfolioId={portfolioId} /> + <PortfolioOverview projectId={projectId} portfolioId={portfolioNumber} /> </TabContent> <TabContent eventKey={1} id="results" ref={resultsRef} aria-label="Results tab" hidden> - <PortfolioResults portfolioId={portfolioId} /> + <PortfolioResults projectId={projectId} portfolioId={portfolioNumber} /> </TabContent> </PageSection> </AppPage> diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js index f7188d9f..6297b8c3 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js +++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js @@ -26,7 +26,6 @@ import ContextSelectionSection from '../../../../components/context/ContextSelec import ProjectSelector from '../../../../components/context/ProjectSelector' import TopologySelector from '../../../../components/context/TopologySelector' import TopologyOverview from '../../../../components/topologies/TopologyOverview' -import { useProject } from '../../../../data/project' import { useDispatch } from 'react-redux' import React, { useEffect, useState } from 'react' import Head from 'next/head' @@ -45,6 +44,7 @@ import { TextContent, } from '@patternfly/react-core' import BreadcrumbLink from '../../../../components/util/BreadcrumbLink' +import { useTopology } from '../../../../data/topology' import { goToRoom } from '../../../../redux/actions/interaction-level' import { openTopology } from '../../../../redux/actions/topology' @@ -55,16 +55,18 @@ const TopologyMap = dynamic(() => import('../../../../components/topologies/Topo */ function Topology() { const router = useRouter() - const { project: projectId, topology: topologyId } = router.query + const projectId = +router.query['project'] + const topologyNumber = +router.query['topology'] - const { data: project } = useProject(projectId) + const { data: topology } = useTopology(projectId, topologyNumber) + const project = topology?.project const dispatch = useDispatch() useEffect(() => { - if (topologyId) { - dispatch(openTopology(topologyId)) + if (topologyNumber) { + dispatch(openTopology(projectId, topologyNumber)) } - }, [topologyId, dispatch]) + }, [projectId, topologyNumber, dispatch]) const [activeTab, setActiveTab] = useState('overview') @@ -76,7 +78,11 @@ function Topology() { <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}> Project details </BreadcrumbItem> - <BreadcrumbItem to={`/projects/${projectId}/topologies/${topologyId}`} component={BreadcrumbLink} isActive> + <BreadcrumbItem + to={`/projects/${projectId}/topologies/${topologyNumber}`} + component={BreadcrumbLink} + isActive + > Topology </BreadcrumbItem> </Breadcrumb> @@ -84,8 +90,8 @@ function Topology() { const contextSelectors = ( <ContextSelectionSection> - <ProjectSelector projectId={projectId} /> - <TopologySelector projectId={projectId} topologyId={topologyId} /> + <ProjectSelector activeProject={project} /> + <TopologySelector activeTopology={topology} /> </ContextSelectionSection> ) @@ -117,16 +123,22 @@ function Topology() { <PageSection padding={activeTab === 'floor-plan' && { default: 'noPadding' }} isFilled> <TabContent id="overview" aria-label="Overview tab" hidden={activeTab !== 'overview'}> <TopologyOverview - topologyId={topologyId} + projectId={projectId} + topologyNumber={topologyNumber} onSelect={(type, obj) => { if (type === 'room') { - dispatch(goToRoom(obj._id)) + dispatch(goToRoom(obj.id)) setActiveTab('floor-plan') } }} /> </TabContent> - <TabContent id="floor-plan" aria-label="Floor Plan tab" className="pf-u-h-100" hidden={activeTab !== 'floor-plan'}> + <TabContent + id="floor-plan" + aria-label="Floor Plan tab" + className="pf-u-h-100" + hidden={activeTab !== 'floor-plan'} + > <TopologyMap /> </TabContent> </PageSection> diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js index eb77701e..bb1fbd69 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/index.js +++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js @@ -26,9 +26,8 @@ import ProjectFilterPanel from '../../components/projects/FilterPanel' import { useAuth } from '../../auth' import { AppPage } from '../../components/AppPage' import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core' -import { useProjects } from '../../data/project' +import { useProjects, useDeleteProject } from '../../data/project' import ProjectTable from '../../components/projects/ProjectTable' -import { useMutation } from 'react-query' import NewProject from '../../components/projects/NewProject' const getVisibleProjects = (projects, filter, userId) => { @@ -52,13 +51,12 @@ function Projects() { const { user } = useAuth() const { status, data: projects } = useProjects() const [filter, setFilter] = useState('SHOW_ALL') - const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter, user?.sub), [ - projects, - filter, - user?.sub, - ]) + const visibleProjects = useMemo( + () => getVisibleProjects(projects ?? [], filter, user?.sub), + [projects, filter, user?.sub] + ) - const { mutate: deleteProject } = useMutation('deleteProject') + const { mutate: deleteProject } = useDeleteProject() return ( <AppPage> @@ -76,7 +74,7 @@ function Projects() { status={status} isFiltering={filter !== 'SHOW_ALL'} projects={visibleProjects} - onDelete={(project) => deleteProject(project._id)} + onDelete={(project) => deleteProject(project.id)} /> <NewProject /> </PageSection> diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js index 939c24a4..e430da2e 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js @@ -14,16 +14,16 @@ export const DELETE_TILE = 'DELETE_TILE' export function startNewRoomConstruction() { return (dispatch, getState) => { const { topology } = getState() - const topologyId = topology.root._id + const topologyId = topology.root.id const room = { - _id: uuid(), + id: uuid(), name: 'Room', topologyId, tiles: [], } dispatch(addRoom(topologyId, room)) - dispatch(startNewRoomConstructionSucceeded(room._id)) + dispatch(startNewRoomConstructionSucceeded(room.id)) } } @@ -97,7 +97,7 @@ export function addTile(roomId, positionX, positionY) { return { type: ADD_TILE, tile: { - _id: uuid(), + id: uuid(), roomId, positionX, positionY, diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js index 94a712c4..d48af37a 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/index.js @@ -23,9 +23,10 @@ export const OPEN_TOPOLOGY = 'OPEN_TOPOLOGY' export const STORE_TOPOLOGY = 'STORE_TOPOLOGY' -export function openTopology(id) { +export function openTopology(projectId, id) { return { type: OPEN_TOPOLOGY, + projectId, id, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js index c319d966..308acaa6 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js @@ -24,7 +24,7 @@ export function addMachine(rackId, position) { return { type: ADD_MACHINE, machine: { - _id: uuid(), + id: uuid(), rackId, position, cpus: [], diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js index bd447db5..fd2d8cdc 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js @@ -16,7 +16,7 @@ export function addRoom(topologyId, room) { return { type: ADD_ROOM, room: { - _id: uuid(), + id: uuid(), topologyId, ...room, }, @@ -54,9 +54,9 @@ export function addRackToTile(positionX, positionY) { dispatch({ type: ADD_RACK_TO_TILE, rack: { - _id: uuid(), + id: uuid(), name: 'Rack', - tileId: tile._id, + tileId: tile.id, capacity: DEFAULT_RACK_SLOT_CAPACITY, powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, machines: [], diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js index 47af53cf..1789257b 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js @@ -10,7 +10,7 @@ function machine(state = {}, action, { racks }) { case ADD_MACHINE: return produce(state, (draft) => { const { machine } = action - draft[machine._id] = machine + draft[machine.id] = machine }) case DELETE_MACHINE: return produce(state, (draft) => { diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js index 155837cb..ca79348a 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js @@ -33,7 +33,7 @@ function rack(state = {}, action, { machines }) { case ADD_RACK_TO_TILE: return produce(state, (draft) => { const { rack } = action - draft[rack._id] = rack + draft[rack.id] = rack }) case EDIT_RACK_NAME: return produce(state, (draft) => { @@ -48,7 +48,7 @@ function rack(state = {}, action, { machines }) { case ADD_MACHINE: return produce(state, (draft) => { const { machine } = action - draft[machine.rackId].machines.push(machine._id) + draft[machine.rackId].machines.push(machine.id) }) case DELETE_MACHINE: return produce(state, (draft) => { diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js index d6cc51c1..c05c8bfa 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js @@ -32,7 +32,7 @@ function room(state = {}, action, { tiles }) { case ADD_ROOM: return produce(state, (draft) => { const { room } = action - draft[room._id] = room + draft[room.id] = room }) case DELETE_ROOM: return produce(state, (draft) => { @@ -47,7 +47,7 @@ function room(state = {}, action, { tiles }) { case ADD_TILE: return produce(state, (draft) => { const { tile } = action - draft[tile.roomId].tiles.push(tile._id) + draft[tile.roomId].tiles.push(tile.id) }) case DELETE_TILE: return produce(state, (draft) => { diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js index 6dbccb66..8e5ecd6e 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js @@ -33,7 +33,7 @@ function tile(state = {}, action, { racks }) { case ADD_TILE: return produce(state, (draft) => { const { tile } = action - draft[tile._id] = tile + draft[tile.id] = tile }) case DELETE_TILE: return produce(state, (draft) => { @@ -43,7 +43,7 @@ function tile(state = {}, action, { racks }) { case ADD_RACK_TO_TILE: return produce(state, (draft) => { const { rack } = action - draft[rack.tileId].rack = rack._id + draft[rack.tileId].rack = rack.id }) case DELETE_RACK: return produce(state, (draft) => { diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js index cd9b5efd..dff0a69e 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js @@ -21,7 +21,7 @@ */ import produce from 'immer' -import { STORE_TOPOLOGY } from "../../actions/topology"; +import { STORE_TOPOLOGY } from '../../actions/topology' import { ADD_ROOM, DELETE_ROOM } from '../../actions/topology/room' function topology(state = undefined, action) { @@ -31,7 +31,7 @@ function topology(state = undefined, action) { case ADD_ROOM: return produce(state, (draft) => { const { room } = action - draft.rooms.push(room._id) + draft.rooms.push(room.id) }) case DELETE_ROOM: return produce(state, (draft) => { diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js index 4c8ff5da..15147bcf 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -32,14 +32,15 @@ export function* updateServer() { * Watch the topology on the server for changes. */ export function* watchServer() { - let { id } = yield take(OPEN_TOPOLOGY) + let { projectId, id } = yield take(OPEN_TOPOLOGY) while (true) { - const channel = yield queryObserver(id) + const channel = yield queryObserver(projectId, id) while (true) { const [action, response] = yield race([take(OPEN_TOPOLOGY), take(channel)]) if (action) { + projectId = action.projectId id = action.id break } @@ -57,9 +58,9 @@ export function* watchServer() { /** * Observe changes for the topology with the specified identifier. */ -function* queryObserver(id) { +function* queryObserver(projectId, id) { const queryClient = yield getContext('queryClient') - const observer = new QueryObserver(queryClient, { queryKey: ['topologies', id] }) + const observer = new QueryObserver(queryClient, { queryKey: ['topologies', projectId, id] }) return eventChannel((emitter) => { const unsubscribe = observer.subscribe((result) => { diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js index abdf146e..6c93f458 100644 --- a/opendc-web/opendc-web-ui/src/shapes.js +++ b/opendc-web/opendc-web-ui/src/shapes.js @@ -22,26 +22,18 @@ import PropTypes from 'prop-types' -export const User = PropTypes.shape({ - _id: PropTypes.string.isRequired, - googleId: PropTypes.string.isRequired, - email: PropTypes.string.isRequired, - givenName: PropTypes.string.isRequired, - familyName: PropTypes.string.isRequired, - authorizations: PropTypes.array.isRequired, -}) +export const ProjectRole = PropTypes.oneOf(['VIEWER', 'EDITOR', 'OWNER']) export const Project = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, - datetimeCreated: PropTypes.string.isRequired, - datetimeLastEdited: PropTypes.string.isRequired, - topologyIds: PropTypes.array.isRequired, - portfolioIds: PropTypes.array.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + role: ProjectRole, }) export const ProcessingUnit = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, clockRateMhz: PropTypes.number.isRequired, numberOfCores: PropTypes.number.isRequired, @@ -49,7 +41,7 @@ export const ProcessingUnit = PropTypes.shape({ }) export const StorageUnit = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, speedMbPerS: PropTypes.number.isRequired, sizeMb: PropTypes.number.isRequired, @@ -57,38 +49,45 @@ export const StorageUnit = PropTypes.shape({ }) export const Machine = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, position: PropTypes.number.isRequired, - cpus: PropTypes.arrayOf(PropTypes.string), - gpus: PropTypes.arrayOf(PropTypes.string), - memories: PropTypes.arrayOf(PropTypes.string), - storages: PropTypes.arrayOf(PropTypes.string), + cpus: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, PropTypes.string])), + gpus: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, PropTypes.string])), + memories: PropTypes.arrayOf(PropTypes.oneOfType([StorageUnit, PropTypes.string])), + storages: PropTypes.arrayOf(PropTypes.oneOfType([StorageUnit, PropTypes.string])), }) export const Rack = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, capacity: PropTypes.number.isRequired, powerCapacityW: PropTypes.number.isRequired, - machines: PropTypes.arrayOf(PropTypes.string), + machines: PropTypes.arrayOf(PropTypes.oneOfType([Machine, PropTypes.string])), }) export const Tile = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, positionX: PropTypes.number.isRequired, positionY: PropTypes.number.isRequired, - rack: PropTypes.string, + rack: PropTypes.oneOfType([Rack, PropTypes.string]), }) export const Room = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - tiles: PropTypes.arrayOf(PropTypes.string), + tiles: PropTypes.arrayOf(PropTypes.oneOfType([Tile, PropTypes.string])), }) export const Topology = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, + number: PropTypes.number.isRequired, + project: Project.isRequired, name: PropTypes.string.isRequired, - rooms: PropTypes.arrayOf(PropTypes.string), + rooms: PropTypes.arrayOf(PropTypes.oneOfType([Room, PropTypes.string])), +}) + +export const Phenomena = PropTypes.shape({ + failures: PropTypes.bool.isRequired, + interference: PropTypes.bool.isRequired, }) export const Scheduler = PropTypes.shape({ @@ -96,47 +95,82 @@ export const Scheduler = PropTypes.shape({ }) export const Trace = PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, type: PropTypes.string.isRequired, }) -export const Portfolio = PropTypes.shape({ - _id: PropTypes.string.isRequired, - projectId: PropTypes.string.isRequired, +export const Workload = PropTypes.shape({ + trace: Trace.isRequired, + samplingFraction: PropTypes.number.isRequired, +}) + +export const Targets = PropTypes.shape({ + repeats: PropTypes.number.isRequired, + metrics: PropTypes.arrayOf(PropTypes.string).isRequired, +}) + +export const TopologySummary = PropTypes.shape({ + id: PropTypes.number.isRequired, + number: PropTypes.number.isRequired, + project: Project.isRequired, + name: PropTypes.string.isRequired, +}) + +export const PortfolioSummary = PropTypes.shape({ + id: PropTypes.number.isRequired, + number: PropTypes.number.isRequired, + project: Project.isRequired, name: PropTypes.string.isRequired, - scenarioIds: PropTypes.arrayOf(PropTypes.string).isRequired, targets: PropTypes.shape({ - enabledMetrics: PropTypes.arrayOf(PropTypes.string).isRequired, - repeatsPerScenario: PropTypes.number.isRequired, + repeats: PropTypes.number.isRequired, + metrics: PropTypes.arrayOf(PropTypes.string).isRequired, }).isRequired, }) -export const Scenario = PropTypes.shape({ - _id: PropTypes.string.isRequired, - portfolioId: PropTypes.string.isRequired, +export const ScenarioSummary = PropTypes.shape({ + id: PropTypes.number.isRequired, + number: PropTypes.number.isRequired, name: PropTypes.string.isRequired, - simulation: PropTypes.shape({ - state: PropTypes.string.isRequired, - }).isRequired, - trace: PropTypes.shape({ - traceId: PropTypes.string.isRequired, - trace: Trace, - loadSamplingFraction: PropTypes.number.isRequired, - }).isRequired, - topology: PropTypes.shape({ - topologyId: PropTypes.string.isRequired, - topology: Topology, - }).isRequired, - operational: PropTypes.shape({ - failuresEnabled: PropTypes.bool.isRequired, - performanceInterferenceEnabled: PropTypes.bool.isRequired, - schedulerName: PropTypes.string.isRequired, - scheduler: Scheduler, - }).isRequired, + workload: Workload.isRequired, + topology: TopologySummary.isRequired, + phenomena: Phenomena.isRequired, + schedulerName: PropTypes.string.isRequired, results: PropTypes.object, }) +export const JobState = PropTypes.oneOf(['PENDING', 'CLAIMED', 'RUNNING', 'FAILED', 'FINISHED']) + +export const Job = PropTypes.shape({ + id: PropTypes.number.isRequired, + state: JobState.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + results: PropTypes.object, +}) + +export const Scenario = PropTypes.shape({ + id: PropTypes.number.isRequired, + number: PropTypes.number.isRequired, + project: Project.isRequired, + portfolio: PortfolioSummary.isRequired, + name: PropTypes.string.isRequired, + workload: Workload.isRequired, + topology: TopologySummary.isRequired, + phenomena: Phenomena.isRequired, + schedulerName: PropTypes.string.isRequired, + job: Job.isRequired, +}) + +export const Portfolio = PropTypes.shape({ + id: PropTypes.number.isRequired, + number: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + project: Project.isRequired, + targets: Targets.isRequired, + scenarios: PropTypes.arrayOf(ScenarioSummary).isRequired, +}) + export const WallSegment = PropTypes.shape({ startPosX: PropTypes.number.isRequired, startPosY: PropTypes.number.isRequired, diff --git a/opendc-web/opendc-web-ui/src/util/authorizations.js b/opendc-web/opendc-web-ui/src/util/authorizations.js index ce5d34b6..fffcefeb 100644 --- a/opendc-web/opendc-web-ui/src/util/authorizations.js +++ b/opendc-web/opendc-web-ui/src/util/authorizations.js @@ -3,13 +3,13 @@ import EditIcon from '@patternfly/react-icons/dist/js/icons/edit-icon' import EyeIcon from '@patternfly/react-icons/dist/js/icons/eye-icon' export const AUTH_ICON_MAP = { - OWN: HomeIcon, - EDIT: EditIcon, - VIEW: EyeIcon, + OWNER: HomeIcon, + EDITOR: EditIcon, + VIEWER: EyeIcon, } export const AUTH_DESCRIPTION_MAP = { - OWN: 'Own', - EDIT: 'Can Edit', - VIEW: 'Can View', + OWNER: 'Own', + EDITOR: 'Can Edit', + VIEWER: 'Can View', } diff --git a/opendc-web/opendc-web-ui/src/util/topology-schema.js b/opendc-web/opendc-web-ui/src/util/topology-schema.js index 7779ccfe..ff672dd6 100644 --- a/opendc-web/opendc-web-ui/src/util/topology-schema.js +++ b/opendc-web/opendc-web-ui/src/util/topology-schema.js @@ -22,10 +22,10 @@ import { schema } from 'normalizr' -const Cpu = new schema.Entity('cpus', {}, { idAttribute: '_id' }) -const Gpu = new schema.Entity('gpus', {}, { idAttribute: '_id' }) -const Memory = new schema.Entity('memories', {}, { idAttribute: '_id' }) -const Storage = new schema.Entity('storages', {}, { idAttribute: '_id' }) +const Cpu = new schema.Entity('cpus', {}, { idAttribute: 'id' }) +const Gpu = new schema.Entity('gpus', {}, { idAttribute: 'id' }) +const Memory = new schema.Entity('memories', {}, { idAttribute: 'id' }) +const Storage = new schema.Entity('storages', {}, { idAttribute: 'id' }) export const Machine = new schema.Entity( 'machines', @@ -35,13 +35,13 @@ export const Machine = new schema.Entity( memories: [Memory], storages: [Storage], }, - { idAttribute: '_id' } + { idAttribute: 'id' } ) -export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: '_id' }) +export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: 'id' }) -export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: '_id' }) +export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: 'id' }) -export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: '_id' }) +export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: 'id' }) -export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: '_id' }) +export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: 'id' }) diff --git a/opendc-web/opendc-web-ui/src/util/unit-specifications.js b/opendc-web/opendc-web-ui/src/util/unit-specifications.js index 28479edd..3e3671cd 100644 --- a/opendc-web/opendc-web-ui/src/util/unit-specifications.js +++ b/opendc-web/opendc-web-ui/src/util/unit-specifications.js @@ -1,34 +1,34 @@ export const CPU_UNITS = { 'cpu-1': { - _id: 'cpu-1', + id: 'cpu-1', name: 'Intel i7 v6 6700k', clockRateMhz: 4100, numberOfCores: 4, energyConsumptionW: 70, }, 'cpu-2': { - _id: 'cpu-2', + id: 'cpu-2', name: 'Intel i5 v6 6700k', clockRateMhz: 3500, numberOfCores: 2, energyConsumptionW: 50, }, 'cpu-3': { - _id: 'cpu-3', + id: 'cpu-3', name: 'Intel® Xeon® E-2224G', clockRateMhz: 3500, numberOfCores: 4, energyConsumptionW: 71, }, 'cpu-4': { - _id: 'cpu-4', + id: 'cpu-4', name: 'Intel® Xeon® E-2244G', clockRateMhz: 3800, numberOfCores: 8, energyConsumptionW: 71, }, 'cpu-5': { - _id: 'cpu-5', + id: 'cpu-5', name: 'Intel® Xeon® E-2246G', clockRateMhz: 3600, numberOfCores: 12, @@ -38,14 +38,14 @@ export const CPU_UNITS = { export const GPU_UNITS = { 'gpu-1': { - _id: 'gpu-1', + id: 'gpu-1', name: 'NVIDIA GTX 4 1080', clockRateMhz: 1200, numberOfCores: 200, energyConsumptionW: 250, }, 'gpu-2': { - _id: 'gpu-2', + id: 'gpu-2', name: 'NVIDIA Tesla V100', clockRateMhz: 1200, numberOfCores: 5120, @@ -55,28 +55,28 @@ export const GPU_UNITS = { export const MEMORY_UNITS = { 'memory-1': { - _id: 'memory-1', + id: 'memory-1', name: 'Samsung PC DRAM K4A4G045WD', speedMbPerS: 16000, sizeMb: 4000, energyConsumptionW: 10, }, 'memory-2': { - _id: 'memory-2', + id: 'memory-2', name: 'Samsung PC DRAM M393A2K43BB1-CRC', speedMbPerS: 2400, sizeMb: 16000, energyConsumptionW: 10, }, 'memory-3': { - _id: 'memory-3', + id: 'memory-3', name: 'Crucial MTA18ASF4G72PDZ-3G2E1', speedMbPerS: 3200, sizeMb: 32000, energyConsumptionW: 10, }, 'memory-4': { - _id: 'memory-4', + id: 'memory-4', name: 'Crucial MTA9ASF2G72PZ-3G2E1', speedMbPerS: 3200, sizeMb: 16000, @@ -86,14 +86,14 @@ export const MEMORY_UNITS = { export const STORAGE_UNITS = { 'storage-1': { - _id: 'storage-1', + id: 'storage-1', name: 'Samsung EVO 2016 SATA III', speedMbPerS: 6000, sizeMb: 250000, energyConsumptionW: 10, }, 'storage-2': { - _id: 'storage-2', + id: 'storage-2', name: 'Western Digital MTA9ASF2G72PZ-3G2E1', speedMbPerS: 6000, sizeMb: 4000000, |
