diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-03-07 18:19:21 +0100 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-04-04 12:48:05 +0200 |
| commit | 3d1c02e50ee619598bcd7fad4368be8b4a039e84 (patch) | |
| tree | 89baaf3250eb0495295616a9945c681f5e1ccdb8 | |
| parent | d12efc754a1611a624d170b4d1fa6085e6bb177b (diff) | |
refactor(web/ui): Fix compatibility with new API
This change updates the web interface in React to be compatible with the
new API written in Kotlin. Several changes have been made in the new API
to ensure consistency.
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, |
