From 419297128ce2ab2c8085c81fafdb843c1135661d Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 14 Sep 2022 13:49:58 +0200 Subject: fix(web/ui): Fix deletion of topology This change fixes an issue with the OpenDC web interface where the user cannot remove an existing topology from the topology table due to a programming mistake. --- opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'opendc-web/opendc-web-ui/src/components') 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 21be3c79..ef5af263 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 @@ -28,9 +28,9 @@ import TileGroup from './groups/TileGroup' function TileContainer({ tileId, ...props }) { const interactionLevel = useSelector((state) => state.interactionLevel) + const dispatch = useDispatch() const tile = useSelector((state) => state.topology.tiles[tileId]) - const dispatch = useDispatch() const onClick = (tile) => { if (tile.rack) { dispatch(goFromRoomToRack(tile.id)) -- cgit v1.2.3 From 7da6543fb9c58736c2fa8c000b25290be4f78de4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 14 Sep 2022 14:12:41 +0200 Subject: fix(web/ui): Fix duplication of topology This change addresses an issue where a new topology did not correctly clone an existing topology. --- .../src/components/projects/NewTopologyModal.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components') 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 be4256e3..780ec034 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js +++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopologyModal.js @@ -57,9 +57,10 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan setErrors({ name: true }) return false } else { - const candidate = topologies.find((topology) => topology.id === originTopology) || { projectId, rooms: [] } + const candidate = topologies.find((topology) => topology.id === originTopology) || { rooms: [] } const topology = produce(candidate, (draft) => { - delete draft.id + delete draft.project + draft.projectId = projectId draft.name = name }) onSubmitUpstream(topology) @@ -87,7 +88,12 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan - + setOriginTopology(+v)} + > {topologies.map((topology) => ( -- cgit v1.2.3 From b8f726cd84ee5b7b0816d04d802f53f0815f6d82 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 14 Sep 2022 14:23:02 +0200 Subject: fix(web/ui): Only display selected metrics This change fixes an issue with the web interface where all available metrics were shown to the user, instead of the metrics belonging to the portfolio. --- .../src/components/portfolios/PortfolioResults.js | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js index f63f0c7f..33604896 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -20,10 +20,10 @@ * SOFTWARE. */ -import React from 'react' +import React, { useMemo } from 'react' import PropTypes from 'prop-types' import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' -import { AVAILABLE_METRICS, METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics' +import { METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics' import { mean, std } from 'mathjs' import approx from 'approximate-number' import { @@ -46,10 +46,9 @@ import { usePortfolio } from '../../data/project' import PortfolioResultInfo from './PortfolioResultInfo' import NewScenario from './NewScenario' -const PortfolioResults = ({ projectId, portfolioId }) => { - const { status, data: scenarios = [] } = usePortfolio(projectId, portfolioId, { - select: (portfolio) => portfolio.scenarios, - }) +function PortfolioResults({ projectId, portfolioId }) { + const { status, data: portfolio } = usePortfolio(projectId, portfolioId) + const scenarios = portfolio?.scenarios ?? [] if (status === 'loading') { return ( @@ -94,21 +93,24 @@ const PortfolioResults = ({ projectId, portfolioId }) => { ) } - const dataPerMetric = {} - - AVAILABLE_METRICS.forEach((metric) => { - dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.job?.results) - .map((scenario) => ({ - name: scenario.name, - value: mean(scenario.job.results[metric]), - errorX: std(scenario.job.results[metric]), - })) - }) + const metrics = portfolio?.targets?.metrics ?? [] + const dataPerMetric = useMemo(() => { + const dataPerMetric = {} + metrics.forEach((metric) => { + dataPerMetric[metric] = scenarios + .filter((scenario) => scenario.job?.results) + .map((scenario) => ({ + name: scenario.name, + value: mean(scenario.job.results[metric]), + errorX: std(scenario.job.results[metric]), + })) + }) + return dataPerMetric + }, [scenarios, metrics]) return ( - {AVAILABLE_METRICS.map((metric) => ( + {metrics.map((metric) => ( -- cgit v1.2.3 From d2c0b9c038f5cbcb2b1528d4cb22b862309bd99a Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 14 Sep 2022 16:56:37 +0200 Subject: fix(web/ui): Fix z-index of context selector component This change addresses an issue with the context selector component where the dropdown would fall behind a sticky tab bar in the main content. --- .../opendc-web-ui/src/components/context/ContextSelector.module.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'opendc-web/opendc-web-ui/src/components') 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 07b7b1d0..c4b89503 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 @@ -24,6 +24,7 @@ // Ensure this selector has precedence over the default one margin-right: 20px; + --pf-c-context-selector__menu--ZIndex: var(--pf-global--ZIndex--lg); --pf-c-context-selector__toggle--PaddingTop: var(--pf-global--spacer--sm); --pf-c-context-selector__toggle--PaddingRight: 0; --pf-c-context-selector__toggle--PaddingBottom: var(--pf-global--spacer--sm); -- cgit v1.2.3 From 9dd75a9a40f7f2aebbc617980c99085f9dc688f8 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 14 Sep 2022 23:02:46 +0200 Subject: refactor(web/ui): Move project selector into masthead This change moves the project selector into the masthead since it affects the whole application. This follows the PatternFly guidelines. --- .../opendc-web-ui/src/components/AppHeader.js | 46 ++++++++--- .../src/components/AppHeader.module.scss | 35 ++++++++ .../opendc-web-ui/src/components/AppHeaderTools.js | 96 ++++++---------------- .../opendc-web-ui/src/components/AppHeaderUser.js | 81 ++++++++++++++++++ opendc-web/opendc-web-ui/src/components/AppLogo.js | 46 ----------- .../src/components/AppLogo.module.scss | 33 -------- .../src/components/context/ContextSelector.js | 9 +- .../components/context/ContextSelector.module.scss | 2 +- .../src/components/context/ProjectSelector.js | 8 +- 9 files changed, 187 insertions(+), 169 deletions(-) create mode 100644 opendc-web/opendc-web-ui/src/components/AppHeader.module.scss create mode 100644 opendc-web/opendc-web-ui/src/components/AppHeaderUser.js delete mode 100644 opendc-web/opendc-web-ui/src/components/AppLogo.js delete mode 100644 opendc-web/opendc-web-ui/src/components/AppLogo.module.scss (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.js b/opendc-web/opendc-web-ui/src/components/AppHeader.js index fd54b3ad..54f3bbf3 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeader.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeader.js @@ -20,24 +20,44 @@ * SOFTWARE. */ -import { PageHeader } from '@patternfly/react-core' import React from 'react' +import { + Masthead, + MastheadMain, + MastheadBrand, + MastheadContent, + Toolbar, + ToolbarContent, + ToolbarItem, +} from '@patternfly/react-core' +import Link from "next/link"; import AppHeaderTools from './AppHeaderTools' -import { AppNavigation } from './AppNavigation' -import AppLogo from './AppLogo' +import AppHeaderUser from './AppHeaderUser' +import ProjectSelector from './context/ProjectSelector' -export function AppHeader() { - // eslint-disable-next-line @next/next/no-img-element - const logo = OpenDC +import styles from './AppHeader.module.scss' +export function AppHeader() { return ( - } - topNav={} - /> + + + }> + OpenDC logo + OpenDC + + + + + + + + + + + + + + ) } diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss b/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss new file mode 100644 index 00000000..a7a6e325 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss @@ -0,0 +1,35 @@ +/*! + * 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. + */ + +.logo { + span { + margin-left: 8px; + color: #fff; + align-self: center; + font-weight: 500; + } + + &:hover, + &:focus { + text-decoration: none; + } +} diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js index 3e58b209..499bceef 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js @@ -21,29 +21,19 @@ */ import { - Avatar, Button, ButtonVariant, Dropdown, - DropdownGroup, DropdownItem, - DropdownToggle, KebabToggle, - PageHeaderTools, - PageHeaderToolsGroup, - PageHeaderToolsItem, - Skeleton, + ToolbarGroup, + ToolbarItem, } from '@patternfly/react-core' -import { useState } from 'react' -import { useAuth } from '../auth' +import { useReducer } from 'react' import { GithubIcon, HelpIcon } from '@patternfly/react-icons' function AppHeaderTools() { - const { logout, user, isAuthenticated, isLoading } = useAuth() - const username = isAuthenticated || isLoading ? user?.name : 'Anonymous' - const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg' - - const [isKebabDropdownOpen, setKebabDropdownOpen] = useState(false) + const [isKebabDropdownOpen, toggleKebabDropdown] = useReducer((t) => !t, false) const kebabDropdownItems = [ , ] - const [isDropdownOpen, setDropdownOpen] = useState(false) - const userDropdownItems = [ - - logout({ returnTo: window.location.origin })} - > - Logout - - , - ] - return ( - - - + + + - - + + - - - - - setKebabDropdownOpen(!isKebabDropdownOpen)} />} - isOpen={isKebabDropdownOpen} - dropdownItems={kebabDropdownItems} - /> - - - setDropdownOpen(!isDropdownOpen)}> - {username ?? ( - - )} - - } - dropdownItems={userDropdownItems} - /> - - - {avatar ? ( - - ) : ( - - )} - + + + + } + isOpen={isKebabDropdownOpen} + dropdownItems={kebabDropdownItems} + /> + + ) } diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js b/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js new file mode 100644 index 00000000..809f3ac3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 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 { + Dropdown, + DropdownToggle, + Skeleton, + ToolbarItem, + DropdownItem, + DropdownGroup, + Avatar, +} from '@patternfly/react-core' +import { useReducer } from 'react' +import { useAuth } from '../auth' + +export default function AppHeaderUser() { + const { logout, user, isAuthenticated, isLoading } = useAuth() + const username = isAuthenticated || isLoading ? user?.name : 'Anonymous' + const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg' + + const [isDropdownOpen, toggleDropdown] = useReducer((t) => !t, false) + const userDropdownItems = [ + + logout({ returnTo: window.location.origin })} + > + Logout + + , + ] + + const avatarComponent = avatar ? ( + + ) : ( + + ) + + return ( + + + {username ?? ( + + )} + + } + dropdownItems={userDropdownItems} + /> + + ) +} diff --git a/opendc-web/opendc-web-ui/src/components/AppLogo.js b/opendc-web/opendc-web-ui/src/components/AppLogo.js deleted file mode 100644 index 92663295..00000000 --- a/opendc-web/opendc-web-ui/src/components/AppLogo.js +++ /dev/null @@ -1,46 +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 PropTypes from 'prop-types' -import Link from 'next/link' -import { appLogo } from './AppLogo.module.scss' - -function AppLogo({ href, children, className, ...props }) { - return ( - <> - - - {children} - OpenDC - - - - ) -} - -AppLogo.propTypes = { - href: PropTypes.string.isRequired, - children: PropTypes.node, - className: PropTypes.string, -} - -export default AppLogo diff --git a/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss b/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss deleted file mode 100644 index 3d228cb6..00000000 --- a/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss +++ /dev/null @@ -1,33 +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. - */ - -.appLogo { - span { - margin-left: 4px; - color: #fff; - } - - &:hover, - &:focus { - text-decoration: none; - } -} 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 a99b60c0..436c179b 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js @@ -23,9 +23,9 @@ import PropTypes from 'prop-types' import { ContextSelector as PFContextSelector, ContextSelectorItem } from '@patternfly/react-core' import { useMemo, useState } from 'react' -import { contextSelector } from './ContextSelector.module.scss' +import styles from './ContextSelector.module.scss' -function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label }) { +function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label, isFullHeight, type = 'page' }) { const [searchValue, setSearchValue] = useState('') const filteredItems = useMemo( () => items.filter(({ name }) => name.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1) || items, @@ -34,7 +34,7 @@ function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label return ( setSearchValue(value)} searchInputValue={searchValue} @@ -47,6 +47,7 @@ function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label onSelect(target) onToggle(!isOpen) }} + isFullHeight={isFullHeight} > {filteredItems.map((item) => ( @@ -69,6 +70,8 @@ ContextSelector.propTypes = { onToggle: PropTypes.func.isRequired, isOpen: PropTypes.bool, label: PropTypes.string, + isFullHeight: PropTypes.bool, + type: PropTypes.oneOf(['app', 'page']), } export default ContextSelector 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 c4b89503..4f86ac64 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 @@ -20,7 +20,7 @@ * SOFTWARE. */ -.contextSelector.contextSelector { +.pageSelector.pageSelector { // Ensure this selector has precedence over the default one margin-right: 20px; 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 7721e04c..5f47c798 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js @@ -22,14 +22,16 @@ import { useRouter } from 'next/router' import { useState } from 'react' -import { useProjects } from '../../data/project' +import { useProjects, useProject } from '../../data/project' import { Project } from '../../shapes' import ContextSelector from './ContextSelector' -function ProjectSelector({ activeProject }) { +function ProjectSelector() { const router = useRouter() + const projectId = +router.query['project'] const [isOpen, setOpen] = useState(false) + const { data: activeProject } = useProject(+projectId) const { data: projects = [] } = useProjects({ enabled: isOpen }) return ( @@ -40,6 +42,8 @@ function ProjectSelector({ activeProject }) { onSelect={(project) => router.push(`/projects/${project.id}`)} onToggle={setOpen} isOpen={isOpen} + isFullHeight + type="app" /> ) } -- cgit v1.2.3 From 24b857ae580fcbea441e7cb91bc7aba681fc6c8b Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 15 Sep 2022 10:17:44 +0200 Subject: feat(web/ui): Reduce height of application header This change reduces the height of the application header to 3.5rem to increase the screen real-estate that we can use for the application content. --- .../opendc-web-ui/src/components/AppHeader.js | 24 ++++++++--- .../src/components/AppHeader.module.scss | 11 ++++- .../opendc-web-ui/src/components/AppHeaderUser.js | 4 +- .../opendc-web-ui/src/components/AppNavigation.js | 47 ---------------------- opendc-web/opendc-web-ui/src/components/AppPage.js | 2 +- .../src/components/context/ContextSelector.js | 14 ++++--- .../src/components/context/PortfolioSelector.js | 3 +- .../src/components/context/ProjectSelector.js | 6 +-- .../src/components/context/TopologySelector.js | 3 +- 9 files changed, 46 insertions(+), 68 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/components/AppNavigation.js (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.js b/opendc-web/opendc-web-ui/src/components/AppHeader.js index 54f3bbf3..f9ef00aa 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeader.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeader.js @@ -20,6 +20,8 @@ * SOFTWARE. */ +import Image from 'next/image' +import PropTypes from 'prop-types' import React from 'react' import { Masthead, @@ -30,19 +32,26 @@ import { ToolbarContent, ToolbarItem, } from '@patternfly/react-core' -import Link from "next/link"; +import Link from 'next/link' import AppHeaderTools from './AppHeaderTools' import AppHeaderUser from './AppHeaderUser' import ProjectSelector from './context/ProjectSelector' import styles from './AppHeader.module.scss' -export function AppHeader() { +export default function AppHeader({ nav }) { return ( - + - }> - OpenDC logo + ( + + + + )} + > + OpenDC logo OpenDC @@ -52,6 +61,7 @@ export function AppHeader() { + {nav && {nav}} @@ -61,4 +71,6 @@ export function AppHeader() { ) } -AppHeader.propTypes = {} +AppHeader.propTypes = { + nav: PropTypes.node, +} diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss b/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss index a7a6e325..73ef553c 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss +++ b/opendc-web/opendc-web-ui/src/components/AppHeader.module.scss @@ -20,12 +20,21 @@ * SOFTWARE. */ +.header.header { + /* Increase precedence */ + --pf-c-masthead--m-display-inline__content--MinHeight: 3rem; + --pf-c-masthead--m-display-inline__main--MinHeight: 3rem; + + --pf-c-masthead--c-context-selector--Width: 200px; +} + .logo { span { margin-left: 8px; color: #fff; align-self: center; - font-weight: 500; + font-weight: 600; + font-size: 0.9rem; } &:hover, diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js b/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js index 809f3ac3..e271accb 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js @@ -51,9 +51,9 @@ export default function AppHeaderUser() { ] const avatarComponent = avatar ? ( - + ) : ( - + ) return ( diff --git a/opendc-web/opendc-web-ui/src/components/AppNavigation.js b/opendc-web/opendc-web-ui/src/components/AppNavigation.js deleted file mode 100644 index 77c683a2..00000000 --- a/opendc-web/opendc-web-ui/src/components/AppNavigation.js +++ /dev/null @@ -1,47 +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 { Nav, NavItem, NavList } from '@patternfly/react-core' -import { useRouter } from 'next/router' -import NavItemLink from './util/NavItemLink' - -export function AppNavigation() { - const { pathname } = useRouter() - - return ( - - ) -} - -AppNavigation.propTypes = {} diff --git a/opendc-web/opendc-web-ui/src/components/AppPage.js b/opendc-web/opendc-web-ui/src/components/AppPage.js index 25afaf9a..2893146e 100644 --- a/opendc-web/opendc-web-ui/src/components/AppPage.js +++ b/opendc-web/opendc-web-ui/src/components/AppPage.js @@ -21,7 +21,7 @@ */ import PropTypes from 'prop-types' -import { AppHeader } from './AppHeader' +import AppHeader from './AppHeader' import React from 'react' import { Page, PageGroup, PageBreadcrumb } from '@patternfly/react-core' 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 436c179b..059cfea8 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/ContextSelector.js @@ -23,9 +23,10 @@ import PropTypes from 'prop-types' import { ContextSelector as PFContextSelector, ContextSelectorItem } from '@patternfly/react-core' import { useMemo, useState } from 'react' + import styles from './ContextSelector.module.scss' -function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label, isFullHeight, type = 'page' }) { +function ContextSelector({ id, type = 'page', toggleText, items, onSelect, onToggle, isOpen, isFullHeight }) { const [searchValue, setSearchValue] = useState('') const filteredItems = useMemo( () => items.filter(({ name }) => name.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1) || items, @@ -34,11 +35,11 @@ function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label, return ( setSearchValue(value)} searchInputValue={searchValue} - isOpen={isOpen} onToggle={(_, isOpen) => onToggle(isOpen)} onSelect={(event) => { const targetId = +event.target.value @@ -47,6 +48,7 @@ function ContextSelector({ activeItem, items, onSelect, onToggle, isOpen, label, onSelect(target) onToggle(!isOpen) }} + isOpen={isOpen} isFullHeight={isFullHeight} > {filteredItems.map((item) => ( @@ -64,14 +66,14 @@ const Item = PropTypes.shape({ }) ContextSelector.propTypes = { - activeItem: Item, + id: PropTypes.string, + type: PropTypes.oneOf(['app', 'page']), items: PropTypes.arrayOf(Item).isRequired, + toggleText: PropTypes.string, onSelect: PropTypes.func.isRequired, onToggle: PropTypes.func.isRequired, isOpen: PropTypes.bool, - label: PropTypes.string, isFullHeight: PropTypes.bool, - type: PropTypes.oneOf(['app', 'page']), } export default ContextSelector 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 c4f2d50e..e401e6fc 100644 --- a/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/PortfolioSelector.js @@ -34,7 +34,8 @@ function PortfolioSelector({ activePortfolio }) { return ( router.push(`/projects/${portfolio.project.id}/portfolios/${portfolio.number}`)} 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 5f47c798..f2791b38 100644 --- a/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/ProjectSelector.js @@ -36,14 +36,14 @@ function ProjectSelector() { return ( router.push(`/projects/${project.id}`)} onToggle={setOpen} isOpen={isOpen} isFullHeight - type="app" /> ) } 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 9cae4cbf..355d9f4b 100644 --- a/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js +++ b/opendc-web/opendc-web-ui/src/components/context/TopologySelector.js @@ -34,7 +34,8 @@ function TopologySelector({ activeTopology }) { return ( router.push(`/projects/${topology.project.id}/topologies/${topology.number}`)} -- cgit v1.2.3 From 7199e2c15838d78fedd3c6127beddf1656dbeae2 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 15 Sep 2022 15:38:06 +0200 Subject: feat(web/ui): Redesign projects page This change updates the design of the projects page to use a gallery overview. --- .../src/components/projects/FilterPanel.js | 2 +- .../src/components/projects/NewProject.js | 39 ------ .../src/components/projects/NewProject.module.scss | 26 ---- .../src/components/projects/ProjectCollection.js | 137 +++++++++++++++++++++ .../src/components/projects/ProjectTable.js | 73 ----------- .../src/components/util/NavItemLink.js | 12 +- 6 files changed, 145 insertions(+), 144 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/components/projects/NewProject.js delete mode 100644 opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss create mode 100644 opendc-web/opendc-web-ui/src/components/projects/ProjectCollection.js delete mode 100644 opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js index 285217e9..7c6d129c 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js +++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js @@ -6,7 +6,7 @@ import { filterPanel } from './FilterPanel.module.scss' export const FILTERS = { SHOW_ALL: 'All Projects', SHOW_OWN: 'My Projects', SHOW_SHARED: 'Shared with me' } const FilterPanel = ({ onSelect, activeFilter = 'SHOW_ALL' }) => ( - + {Object.keys(FILTERS).map((filter) => ( { - const [isVisible, setVisible] = useState(false) - const { mutate: addProject } = useNewProject() - - const onSubmit = (name) => { - if (name) { - addProject({ name }) - } - setVisible(false) - } - - return ( - <> -
- -
- - - ) -} - -export default NewProject diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss b/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss deleted file mode 100644 index 5a0e74fc..00000000 --- a/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss +++ /dev/null @@ -1,26 +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. - */ - -.buttonContainer { - flex: 0 1 auto; - padding: 20px 0; -} diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectCollection.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectCollection.js new file mode 100644 index 00000000..70f02812 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectCollection.js @@ -0,0 +1,137 @@ +import { + Gallery, + Bullseye, + EmptyState, + EmptyStateIcon, + Card, + CardTitle, + CardActions, + DropdownItem, + CardHeader, + Dropdown, + KebabToggle, + CardBody, + CardHeaderMain, + TextVariants, + Text, + TextContent, + Tooltip, + Button, + Label, +} from '@patternfly/react-core' +import { PlusIcon, FolderIcon, TrashIcon } from '@patternfly/react-icons' +import PropTypes from 'prop-types' +import React, { useReducer, useMemo } from 'react' +import { Project, Status } from '../../shapes' +import { parseAndFormatDateTime } from '../../util/date-time' +import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP, AUTH_NAME_MAP } from '../../util/authorizations' +import NavItemLink from '../util/NavItemLink' +import TableEmptyState from '../util/TableEmptyState' + +function ProjectCard({ project, onDelete }) { + const [isKebabOpen, toggleKebab] = useReducer((t) => !t, false) + const { id, role, name, updatedAt } = project + const Icon = AUTH_ICON_MAP[role] + + return ( + + + + + + + + + + } + isOpen={isKebabOpen} + dropdownItems={[ + { + onDelete() + toggleKebab() + }} + position="right" + icon={} + > + Delete + , + ]} + /> + + + + {name} + + + + Last modified {parseAndFormatDateTime(updatedAt)} + + + + ) +} + +function ProjectCollection({ status, projects, onDelete, onCreate, isFiltering }) { + const sortedProjects = useMemo(() => { + const res = [...projects] + res.sort((a, b) => (new Date(a.updatedAt) < new Date(b.updatedAt) ? 1 : -1)) + return res + }, [projects]) + + if (sortedProjects.length === 0) { + return ( + } onClick={onCreate}> + Create Project + + } + /> + ) + } + + return ( + + {sortedProjects.map((project) => ( + onDelete(project)} /> + ))} + + + + + + + + + ) +} + +ProjectCollection.propTypes = { + status: Status.isRequired, + isFiltering: PropTypes.bool, + projects: PropTypes.arrayOf(Project).isRequired, + onDelete: PropTypes.func, + onCreate: PropTypes.func, +} + +export default ProjectCollection diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js deleted file mode 100644 index 6921578c..00000000 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import Link from 'next/link' -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 TableEmptyState from '../util/TableEmptyState' - -const ProjectTable = ({ status, projects, onDelete, isFiltering }) => { - const columns = ['Project name', 'Last edited', 'Access Rights'] - const rows = - projects.length > 0 - ? projects.map((project) => { - const Icon = AUTH_ICON_MAP[project.role] - return [ - { - title: {project.name}, - }, - parseAndFormatDateTime(project.updatedAt), - { - title: ( - <> - {AUTH_DESCRIPTION_MAP[project.role]} - - ), - }, - ] - }) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 3 }, - title: ( - - ), - }, - ], - }, - ] - - const actions = - projects.length > 0 - ? [ - { - title: 'Delete Project', - onClick: (_, rowId) => onDelete(projects[rowId]), - }, - ] - : [] - - return ( - - - -
- ) -} - -ProjectTable.propTypes = { - status: Status.isRequired, - isFiltering: PropTypes.bool, - projects: PropTypes.arrayOf(Project).isRequired, - onDelete: PropTypes.func, -} - -export default ProjectTable diff --git a/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js b/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js index c0d109bd..83301361 100644 --- a/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js +++ b/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js @@ -23,11 +23,13 @@ import Link from 'next/link' import PropTypes from 'prop-types' -const NavItemLink = ({ children, href, ...props }) => ( - -
{children} - -) +function NavItemLink({ children, href, ...props }) { + return ( + + {children} + + ) +} NavItemLink.propTypes = { children: PropTypes.node, -- cgit v1.2.3 From 98bc4c3e9458aea98890b770493f14327a7bc7c4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 15 Sep 2022 22:52:00 +0200 Subject: refactor(web/ui): Use PatternFly Charts for plots This change updates the OpenDC web interface to use the PatternFly Charts package to render the results of a portfolio. Previously, we used Recharts, but this package does not support SSR, whereas the PatternFly Charts package matches our design framework. --- .../src/components/portfolios/PortfolioResults.js | 132 ++++++++++++--------- 1 file changed, 74 insertions(+), 58 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components') 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 33604896..f50105ed 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -20,12 +20,11 @@ * SOFTWARE. */ +import { mean, std } from 'mathjs' import React, { useMemo } from 'react' import PropTypes from 'prop-types' -import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' -import { METRIC_NAMES, METRIC_UNITS } from '../../util/available-metrics' -import { mean, std } from 'mathjs' -import approx from 'approximate-number' +import { VictoryErrorBar } from 'victory-errorbar' +import { METRIC_NAMES, METRIC_UNITS, AVAILABLE_METRICS } from '../../util/available-metrics' import { Bullseye, Card, @@ -41,6 +40,7 @@ import { Spinner, Title, } from '@patternfly/react-core' +import { Chart, ChartAxis, ChartBar, ChartTooltip } from '@patternfly/react-charts' import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons' import { usePortfolio } from '../../data/project' import PortfolioResultInfo from './PortfolioResultInfo' @@ -48,7 +48,28 @@ import NewScenario from './NewScenario' function PortfolioResults({ projectId, portfolioId }) { const { status, data: portfolio } = usePortfolio(projectId, portfolioId) - const scenarios = portfolio?.scenarios ?? [] + const scenarios = useMemo(() => portfolio?.scenarios ?? [], [portfolio]) + + const label = ({ datum }) => + `${datum.x}: ${datum.y.toLocaleString()} ± ${datum.errorY.toLocaleString()} ${METRIC_UNITS[datum.metric]}` + const selectedMetrics = new Set(portfolio?.targets?.metrics ?? []) + const dataPerMetric = useMemo(() => { + const dataPerMetric = {} + AVAILABLE_METRICS.forEach((metric) => { + dataPerMetric[metric] = scenarios + .filter((scenario) => scenario.job?.results) + .map((scenario) => ({ + metric, + x: scenario.name, + y: mean(scenario.job.results[metric]), + errorY: std(scenario.job.results[metric]), + label, + })) + }) + return dataPerMetric + }, [scenarios]) + + const categories = useMemo(() => ({ x: scenarios.map((s) => s.name).reverse() }), [scenarios]) if (status === 'loading') { return ( @@ -93,62 +114,57 @@ function PortfolioResults({ projectId, portfolioId }) { ) } - const metrics = portfolio?.targets?.metrics ?? [] - const dataPerMetric = useMemo(() => { - const dataPerMetric = {} - metrics.forEach((metric) => { - dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.job?.results) - .map((scenario) => ({ - name: scenario.name, - value: mean(scenario.job.results[metric]), - errorX: std(scenario.job.results[metric]), - })) - }) - return dataPerMetric - }, [scenarios, metrics]) - return ( - {metrics.map((metric) => ( - - - - - - - {METRIC_NAMES[metric]} - - - - - - approx(tick)} - label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} - type="number" - /> - - - - + selectedMetrics.has(metric) && ( + + + + + + + {METRIC_NAMES[metric]} + + + + + + } + barWidth={25} + horizontal + /> + d.errorY} + labelComponent={<>} + horizontal /> - - - - - - - ))} + + +
+
+ ) + )}
) } -- cgit v1.2.3 From 0b3d0ba3193ebcdeadc6a4b0a192eeb06e9add29 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Sep 2022 11:04:34 +0200 Subject: fix(web/ui): Do not fail on stale Redux state This change fixes an issue where switching between different topologies would fail due to stale Redux state. We have updated the components to take into account that ids may not exist in the Redux store. --- .../topologies/map/RackEnergyFillContainer.js | 34 ++++++++++++---------- .../topologies/map/RackSpaceFillContainer.js | 11 +++++-- .../src/components/topologies/map/RoomContainer.js | 17 ++++++----- .../src/components/topologies/map/TileContainer.js | 4 +++ .../src/components/topologies/map/WallContainer.js | 2 +- .../components/topologies/map/groups/RackGroup.js | 4 +-- 6 files changed, 42 insertions(+), 30 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js index be1f3e45..a1ca7426 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js @@ -3,24 +3,26 @@ import PropTypes from 'prop-types' import { useSelector } from 'react-redux' import RackFillBar from './elements/RackFillBar' -function RackSpaceFillContainer({ tileId, ...props }) { +function RackSpaceFillContainer({ rackId, ...props }) { const fillFraction = useSelector((state) => { + const rack = state.topology.racks[rackId] + if (!rack) { + return 0 + } + + const { machines, cpus, gpus, memories, storages } = state.topology let energyConsumptionTotal = 0 - const rack = state.topology.racks[state.topology.tiles[tileId].rack] - const machineIds = rack.machines - machineIds.forEach((machineId) => { - if (machineId !== null) { - const machine = state.topology.machines[machineId] - machine.cpus.forEach((id) => (energyConsumptionTotal += state.topology.cpus[id].energyConsumptionW)) - machine.gpus.forEach((id) => (energyConsumptionTotal += state.topology.gpus[id].energyConsumptionW)) - machine.memories.forEach( - (id) => (energyConsumptionTotal += state.topology.memories[id].energyConsumptionW) - ) - machine.storages.forEach( - (id) => (energyConsumptionTotal += state.topology.storages[id].energyConsumptionW) - ) + + for (const machineId of rack.machines) { + if (!machineId) { + continue } - }) + const machine = machines[machineId] + machine.cpus.forEach((id) => (energyConsumptionTotal += cpus[id].energyConsumptionW)) + machine.gpus.forEach((id) => (energyConsumptionTotal += gpus[id].energyConsumptionW)) + machine.memories.forEach((id) => (energyConsumptionTotal += memories[id].energyConsumptionW)) + machine.storages.forEach((id) => (energyConsumptionTotal += storages[id].energyConsumptionW)) + } return Math.min(1, energyConsumptionTotal / rack.powerCapacityW) }) @@ -28,7 +30,7 @@ function RackSpaceFillContainer({ tileId, ...props }) { } RackSpaceFillContainer.propTypes = { - tileId: PropTypes.string.isRequired, + rackId: PropTypes.string.isRequired, } export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js index 0c15d54b..2039a9d3 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js @@ -25,13 +25,18 @@ import PropTypes from 'prop-types' import { useSelector } from 'react-redux' import RackFillBar from './elements/RackFillBar' -function RackSpaceFillContainer({ tileId, ...props }) { - const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) +function RackSpaceFillContainer({ rackId, ...props }) { + const rack = useSelector((state) => state.topology.racks[rackId]) + + if (!rack) { + return null + } + return } RackSpaceFillContainer.propTypes = { - tileId: PropTypes.string.isRequired, + rackId: PropTypes.string.isRequired, } export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js index 65189891..191318ee 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js @@ -27,15 +27,16 @@ import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level' import RoomGroup from './groups/RoomGroup' function RoomContainer({ roomId, ...props }) { - const state = useSelector((state) => { - return { - interactionLevel: state.interactionLevel, - currentRoomInConstruction: state.construction.currentRoomInConstruction, - room: state.topology.rooms[roomId], - } - }) + const interactionLevel = useSelector((state) => state.interactionLevel) + const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction) + const room = useSelector((state) => state.topology.rooms[roomId]) const dispatch = useDispatch() - return dispatch(goFromBuildingToRoom(roomId))} /> + + if (!room) { + return null + } + + return dispatch(goFromBuildingToRoom(roomId))} /> } RoomContainer.propTypes = { 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 ef5af263..0788b894 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 @@ -31,6 +31,10 @@ function TileContainer({ tileId, ...props }) { const dispatch = useDispatch() const tile = useSelector((state) => state.topology.tiles[tileId]) + if (!tile) { + return null + } + const onClick = (tile) => { if (tile.rack) { dispatch(goFromRoomToRack(tile.id)) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js index 143f70c2..106d8d3d 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js @@ -27,7 +27,7 @@ import WallGroup from './groups/WallGroup' function WallContainer({ roomId, ...props }) { const tiles = useSelector((state) => { - return state.topology.rooms[roomId].tiles.map((tileId) => state.topology.tiles[tileId]) + return state.topology.rooms[roomId]?.tiles.map((tileId) => state.topology.tiles[tileId]) ?? [] }) return } 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 dad2d62d..ed942661 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 }) { - - + + ) -- cgit v1.2.3 From 78255fc6a1ef18759670682c1d90cee685315493 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Sep 2022 12:17:34 +0200 Subject: fix(web/ui): Fix overflow of topology sidebar This change fixes an issue with the web interface where the sidebar would overflow due to the large number of rack slots that are displayed in the sidebar. --- .../opendc-web-ui/src/components/topologies/TopologyMap.js | 4 ++-- .../opendc-web-ui/src/components/topologies/map/MapStage.js | 2 +- .../src/components/topologies/map/MapStage.module.scss | 2 -- .../src/components/topologies/map/RoomContainer.js | 10 +++++++++- .../components/topologies/sidebar/rack/RackSidebar.module.scss | 10 ++++++---- 5 files changed, 18 insertions(+), 10 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js b/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js index 47235c7e..ff583750 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js @@ -55,9 +55,9 @@ function TopologyMap() { ) : ( - + - + setExpanded(true)} /> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js index 7b96f548..8bf529b2 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js @@ -15,7 +15,7 @@ import Toolbar from './controls/Toolbar' function MapStage({ hotkeysRef }) { const reduxContext = useContext(ReactReduxContext) const stageRef = useRef(null) - const { width = 100, height = 100 } = useResizeObserver({ ref: stageRef.current?.attrs?.container }) + const { width = 500, height = 500 } = useResizeObserver({ ref: stageRef.current?.attrs?.container }) const [[x, y], setPos] = useState([0, 0]) const [scale, setScale] = useState(1) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss index d879b4c8..47c3dde2 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss @@ -24,8 +24,6 @@ background-color: var(--pf-global--Color--light-200); position: relative; display: flex; - justify-content: center; - align-items: center; width: 100%; height: 100%; } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js index 191318ee..76785bea 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js @@ -36,7 +36,15 @@ function RoomContainer({ roomId, ...props }) { return null } - return dispatch(goFromBuildingToRoom(roomId))} /> + return ( + dispatch(goFromBuildingToRoom(roomId))} + /> + ) } RoomContainer.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.module.scss b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.module.scss index 6f258aec..f4c8829f 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.module.scss +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.module.scss @@ -1,12 +1,14 @@ .sidebarContainer { display: flex; - height: 100%; - max-height: 100%; flex-direction: column; + + height: 100%; } .machineListContainer { - flex: 1; - overflow-y: scroll; + overflow-y: auto; + + flex: 1 0 300px; + margin-top: 10px; } -- cgit v1.2.3 From 86bc9e74630374853d11bc1c8f7ba5ffafbaa868 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Sep 2022 14:28:40 +0200 Subject: refactor(web/ui): Migrate to composable table This change updates the web interface to use the composable table API offered by PatternFly 4. This has replaced the legacy table API which will be removed in the next major version of PatternFly. --- .../src/components/portfolios/ScenarioTable.js | 109 +++++++++++---------- .../src/components/projects/PortfolioTable.js | 105 +++++++++++--------- .../src/components/projects/TopologyTable.js | 89 ++++++++--------- .../src/components/topologies/RoomTable.js | 98 +++++++++--------- 4 files changed, 209 insertions(+), 192 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/components') diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js index 68647957..8dc52f7a 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js @@ -20,8 +20,9 @@ * SOFTWARE. */ +import { Bullseye } from '@patternfly/react-core' import Link from 'next/link' -import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn } from '@patternfly/react-table' import React from 'react' import { Portfolio, Status } from '../../shapes' import TableEmptyState from '../util/TableEmptyState' @@ -33,65 +34,65 @@ function ScenarioTable({ portfolio, status }) { const projectId = portfolio?.project?.id const scenarios = portfolio?.scenarios ?? [] - const columns = ['Name', 'Topology', 'Trace', 'State'] - const rows = - scenarios.length > 0 - ? scenarios.map((scenario) => { - const topology = scenario.topology - - return [ - scenario.name, - { - title: topology ? ( - - {topology.name} - - ) : ( - 'Unknown Topology' - ), - }, - `${scenario.workload.trace.name} (${scenario.workload.samplingFraction * 100}%)`, - { title: }, - ] - }) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 4 }, - title: ( - - ), - }, - ], - }, - ] - - const actionResolver = (_, { rowIndex }) => [ + const actions = ({ number }) => [ { title: 'Delete Scenario', - onClick: (_, rowId) => deleteScenario({ projectId: projectId, number: scenarios[rowId].number }), - isDisabled: rowIndex === 0, + onClick: () => deleteScenario({ projectId: projectId, number }), + isDisabled: number === 0, }, ] return ( - 0 ? actionResolver : undefined} - > - - -
+ + + + Name + Topology + Trace + State + + + + {scenarios.map((scenario) => ( + + {scenario.name} + + {scenario.topology ? ( + + {scenario.topology.name} + + ) : ( + 'Unknown Topology' + )} + , + + {`${scenario.workload.trace.name} (${ + scenario.workload.samplingFraction * 100 + }%)`} + + + + + + + + ))} + {scenarios.length === 0 && ( + + + + + + + + )} + + ) } 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 aa679843..0afeaeaf 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js +++ b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js @@ -20,64 +20,75 @@ * SOFTWARE. */ +import { Bullseye } from '@patternfly/react-core' import PropTypes from 'prop-types' import Link from 'next/link' -import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import { TableComposable, Thead, Tbody, Tr, Th, Td, ActionsColumn } from '@patternfly/react-table' import React from 'react' import TableEmptyState from '../util/TableEmptyState' import { usePortfolios, useDeletePortfolio } from '../../data/project' -const PortfolioTable = ({ projectId }) => { +function PortfolioTable({ projectId }) { const { status, data: portfolios = [] } = usePortfolios(projectId) const { mutate: deletePortfolio } = useDeletePortfolio() - const columns = ['Name', 'Scenarios', 'Metrics', 'Repeats'] - const rows = - portfolios.length > 0 - ? portfolios.map((portfolio) => [ - { - title: ( - {portfolio.name} - ), - }, - 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`, - ]) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 4 }, - title: ( - - ), - }, - ], - }, - ] - - const actions = - portfolios.length > 0 - ? [ - { - title: 'Delete Portfolio', - onClick: (_, rowId) => deletePortfolio({ projectId, number: portfolios[rowId].number }), - }, - ] - : [] + const actions = (portfolio) => [ + { + title: 'Delete Portfolio', + onClick: () => deletePortfolio({ projectId, number: portfolio.number }), + }, + ] return ( - - - -
+ + + + Name + Scenarios + Metrics + Repeats + + + + {portfolios.map((portfolio) => ( + + + {portfolio.name} + + + {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`} + + + + + + ))} + {portfolios.length === 0 && ( + + + + + + + + )} + + ) } 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 ced5304a..62deace0 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js +++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js @@ -20,66 +20,67 @@ * SOFTWARE. */ +import { Bullseye } from '@patternfly/react-core' import PropTypes from 'prop-types' import Link from 'next/link' -import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import { Tr, Th, Thead, Td, ActionsColumn, Tbody, TableComposable } from '@patternfly/react-table' import React from 'react' import TableEmptyState from '../util/TableEmptyState' import { parseAndFormatDateTime } from '../../util/date-time' import { useTopologies, useDeleteTopology } from '../../data/topology' -const TopologyTable = ({ projectId }) => { +function TopologyTable({ projectId }) { 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: {topology.name}, - }, - topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`, - parseAndFormatDateTime(topology.updatedAt), - ]) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 3 }, - title: ( - - ), - }, - ], - }, - ] - - const actionResolver = (_, { rowIndex }) => [ + const actions = ({ number }) => [ { title: 'Delete Topology', - onClick: (_, rowId) => deleteTopology({ projectId, number: topologies[rowId].number }), - isDisabled: rowIndex === 0, + onClick: () => deleteTopology({ projectId, number }), + isDisabled: number === 0, }, ] return ( - 0 ? actionResolver : () => []} - > - - -
+ + + + Name + Rooms + Last Edited + + + + {topologies.map((topology) => ( + + + {topology.name} + + + {topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`} + + {parseAndFormatDateTime(topology.updatedAt)} + + + + + ))} + {topologies.length === 0 && ( + + + + + + + + )} + + ) } 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 49e5f095..7f7b4171 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/RoomTable.js @@ -1,63 +1,67 @@ -import { Button } from '@patternfly/react-core' +import { Button, Bullseye } from '@patternfly/react-core' import PropTypes from 'prop-types' import React from 'react' import { useDispatch } from 'react-redux' import { useTopology } from '../../data/topology' -import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import { Tr, Th, Thead, TableComposable, Td, ActionsColumn, Tbody } from '@patternfly/react-table' import { deleteRoom } from '../../redux/actions/topology/room' import TableEmptyState from '../util/TableEmptyState' function RoomTable({ projectId, topologyId, onSelect }) { const dispatch = useDispatch() const { status, data: topology } = useTopology(projectId, topologyId) - const onDelete = (room) => dispatch(deleteRoom(room.id)) - - const columns = ['Name', 'Tiles', 'Racks'] - const rows = - topology?.rooms.length > 0 - ? topology.rooms.map((room) => { - const tileCount = room.tiles.length - const rackCount = room.tiles.filter((tile) => tile.rack).length - return [ - { - title: ( - - ), - }, - tileCount === 1 ? '1 tile' : `${tileCount} tiles`, - rackCount === 1 ? '1 rack' : `${rackCount} racks`, - ] - }) - : [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 3 }, - title: , - }, - ], - }, - ] - - const actions = - topology?.rooms.length > 0 - ? [ - { - title: 'Delete room', - onClick: (_, rowId) => onDelete(topology.rooms[rowId]), - }, - ] - : [] + const actions = (room) => [ + { + title: 'Delete room', + onClick: () => onDelete(room), + }, + ] return ( - - - -
+ + + + Name + Tiles + Racks + + + + {topology?.rooms.map((room) => { + const tileCount = room.tiles.length + const rackCount = room.tiles.filter((tile) => tile.rack).length + return ( + + + + + {tileCount === 1 ? '1 tile' : `${tileCount} tiles`} + {rackCount === 1 ? '1 rack' : `${rackCount} racks`} + + + + + ) + })} + {topology?.rooms.length === 0 && ( + + + + + + + + )} + + ) } -- cgit v1.2.3