summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/components/projects
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-ui/src/components/projects')
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js53
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewProject.js39
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss26
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewScenario.js64
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewTopology.js58
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js97
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js38
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectList.js41
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js29
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js76
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js62
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js108
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js95
14 files changed, 686 insertions, 118 deletions
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 5129c013..285217e9 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
@@ -1,23 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
-import { Button, ButtonGroup } from 'reactstrap'
+import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'
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' }) => (
- <ButtonGroup className={`${filterPanel} mb-2`}>
+ <ToggleGroup className={`${filterPanel} mb-2`}>
{Object.keys(FILTERS).map((filter) => (
- <Button
- color="secondary"
+ <ToggleGroupItem
key={filter}
- onClick={() => activeFilter === filter || onSelect(filter)}
- active={activeFilter === filter}
- >
- {FILTERS[filter]}
- </Button>
+ onChange={() => activeFilter === filter || onSelect(filter)}
+ isSelected={activeFilter === filter}
+ text={FILTERS[filter]}
+ />
))}
- </ButtonGroup>
+ </ToggleGroup>
)
FilterPanel.propTypes = {
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
new file mode 100644
index 00000000..ae4cb9cd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import NewPortfolioModal from '../modals/custom-components/NewPortfolioModal'
+import { useMutation } from 'react-query'
+
+function NewPortfolio({ projectId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addPortfolio } = useMutation('addPortfolio')
+
+ const onSubmit = (name, targets) => {
+ addPortfolio({ projectId, name, targets })
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Portfolio
+ </Button>
+ <NewPortfolioModal isOpen={isVisible} onSubmit={onSubmit} onCancel={() => setVisible(false)} />
+ </>
+ )
+}
+
+NewPortfolio.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default NewPortfolio
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
new file mode 100644
index 00000000..4f5d79cf
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
@@ -0,0 +1,39 @@
+import React, { useState } from 'react'
+import TextInputModal from '../../components/modals/TextInputModal'
+import { Button } from '@patternfly/react-core'
+import { useMutation } from 'react-query'
+import { PlusIcon } from '@patternfly/react-icons'
+import { buttonContainer } from './NewProject.module.scss'
+
+/**
+ * A container for creating a new project.
+ */
+const NewProject = () => {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addProject } = useMutation('addProject')
+
+ const onSubmit = (name) => {
+ if (name) {
+ addProject({ name })
+ }
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <div className={buttonContainer}>
+ <Button
+ icon={<PlusIcon />}
+ color="primary"
+ className="pf-u-float-right"
+ onClick={() => setVisible(true)}
+ >
+ New Project
+ </Button>
+ </div>
+ <TextInputModal title="New Project" label="Project name" isOpen={isVisible} callback={onSubmit} />
+ </>
+ )
+}
+
+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
new file mode 100644
index 00000000..5a0e74fc
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss
@@ -0,0 +1,26 @@
+/*!
+ * 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/NewScenario.js b/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js
new file mode 100644
index 00000000..6d4f48c1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import NewScenarioModal from '../modals/custom-components/NewScenarioModal'
+import { useMutation } from 'react-query'
+
+function NewScenario({ portfolioId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addScenario } = useMutation('addScenario')
+
+ const onSubmit = (name, portfolioId, trace, topology, operational) => {
+ addScenario({
+ portfolioId,
+ name,
+ trace,
+ topology,
+ operational,
+ })
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Scenario
+ </Button>
+ <NewScenarioModal
+ portfolioId={portfolioId}
+ isOpen={isVisible}
+ onSubmit={onSubmit}
+ onCancel={() => setVisible(false)}
+ />
+ </>
+ )
+}
+
+NewScenario.propTypes = {
+ portfolioId: PropTypes.string,
+}
+
+export default NewScenario
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
new file mode 100644
index 00000000..c6c4171b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import NewTopologyModal from '../modals/custom-components/NewTopologyModal'
+import { useDispatch } from 'react-redux'
+import { addTopology } from '../../redux/actions/topologies'
+
+function NewTopology({ projectId }) {
+ const [isVisible, setVisible] = useState(false)
+ const dispatch = useDispatch()
+
+ const onSubmit = (name, duplicateId) => {
+ dispatch(addTopology(projectId, name, duplicateId))
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Topology
+ </Button>
+ <NewTopologyModal
+ projectId={projectId}
+ isOpen={isVisible}
+ onSubmit={onSubmit}
+ onCancel={() => setVisible(false)}
+ />
+ </>
+ )
+}
+
+NewTopology.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default NewTopology
diff --git a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
new file mode 100644
index 00000000..45e399ed
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
@@ -0,0 +1,97 @@
+/*
+ * 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 { 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'
+
+const PortfolioTable = ({ projectId }) => {
+ const { status, data: portfolios = [] } = useProjectPortfolios(projectId)
+ const { mutate: deletePortfolio } = useMutation('deletePortfolio')
+
+ const columns = ['Name', 'Scenarios', 'Metrics', 'Repeats']
+ const rows =
+ portfolios.length > 0
+ ? portfolios.map((portfolio) => [
+ {
+ title: (
+ <Link href={`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`}>
+ {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`,
+ ])
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 4 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading portfolios"
+ emptyTitle="No portfolios"
+ emptyText="You have not created any portfolio for this project yet. Click the New Portfolio button to create one."
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actions =
+ portfolios.length > 0
+ ? [
+ {
+ title: 'Delete Portfolio',
+ onClick: (_, rowId) => deletePortfolio(portfolios[rowId]._id),
+ },
+ ]
+ : []
+
+ return (
+ <Table aria-label="Portfolio List" variant="compact" cells={columns} rows={rows} actions={actions}>
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+PortfolioTable.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default PortfolioTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
deleted file mode 100644
index 4adf3205..00000000
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Link from 'next/link'
-import { Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlay, faUsers, faTrash } from '@fortawesome/free-solid-svg-icons'
-
-const ProjectActionButtons = ({ projectId, onViewUsers, onDelete }) => (
- <td className="text-right">
- <Link href={`/projects/${projectId}`} passHref>
- <Button color="primary" outline size="sm" className="mr-2" title="Open this project">
- <FontAwesomeIcon icon={faPlay} />
- </Button>
- </Link>
- <Button
- color="success"
- outline
- size="sm"
- disabled
- className="mr-2"
- title="View and edit collaborators (not supported currently)"
- onClick={() => onViewUsers(projectId)}
- >
- <FontAwesomeIcon icon={faUsers} />
- </Button>
- <Button color="danger" outline size="sm" title="Delete this project" onClick={() => onDelete(projectId)}>
- <FontAwesomeIcon icon={faTrash} />
- </Button>
- </td>
-)
-
-ProjectActionButtons.propTypes = {
- projectId: PropTypes.string.isRequired,
- onViewUsers: PropTypes.func,
- onDelete: PropTypes.func,
-}
-
-export default ProjectActionButtons
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js
deleted file mode 100644
index 46ef4691..00000000
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Project } from '../../shapes'
-import ProjectRow from './ProjectRow'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'
-
-const ProjectList = ({ projects }) => {
- return (
- <div className="vertically-expanding-container">
- {projects.length === 0 ? (
- <div className="alert alert-info">
- <FontAwesomeIcon icon={faQuestionCircle} className="info-icon mr-2" />
- <strong>No projects here yet...</strong> Add some with the &lsquo;New Project&rsquo; button!
- </div>
- ) : (
- <table className="table table-striped">
- <thead>
- <tr>
- <th>Project name</th>
- <th>Last edited</th>
- <th>Access rights</th>
- <th />
- </tr>
- </thead>
- <tbody>
- {projects.map((project) => (
- <ProjectRow project={project} key={project._id} />
- ))}
- </tbody>
- </table>
- )}
- </div>
- )
-}
-
-ProjectList.propTypes = {
- projects: PropTypes.arrayOf(Project).isRequired,
-}
-
-export default ProjectList
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js
deleted file mode 100644
index 91368de8..00000000
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react'
-import ProjectActions from '../../containers/projects/ProjectActions'
-import { Project } from '../../shapes'
-import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations'
-import { parseAndFormatDateTime } from '../../util/date-time'
-import { useAuth } from '../../auth'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-
-const ProjectRow = ({ project }) => {
- const { user } = useAuth()
- const { level } = project.authorizations.find((auth) => auth.userId === user.sub)
- return (
- <tr>
- <td className="pt-3">{project.name}</td>
- <td className="pt-3">{parseAndFormatDateTime(project.datetimeLastEdited)}</td>
- <td className="pt-3">
- <FontAwesomeIcon icon={AUTH_ICON_MAP[level]} className="mr-2" />
- {AUTH_DESCRIPTION_MAP[level]}
- </td>
- <ProjectActions projectId={project._id} />
- </tr>
- )
-}
-
-ProjectRow.propTypes = {
- project: Project.isRequired,
-}
-
-export default ProjectRow
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
new file mode 100644
index 00000000..a7290259
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
@@ -0,0 +1,76 @@
+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 { 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]
+ return [
+ {
+ title: <Link href={`/projects/${project._id}`}>{project.name}</Link>,
+ },
+ parseAndFormatDateTime(project.datetimeLastEdited),
+ {
+ title: (
+ <>
+ <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[level]}
+ </>
+ ),
+ },
+ ]
+ })
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 3 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading Projects"
+ isFiltering={isFiltering}
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actions =
+ projects.length > 0
+ ? [
+ {
+ title: 'Delete Project',
+ onClick: (_, rowId) => onDelete(projects[rowId]),
+ },
+ ]
+ : []
+
+ return (
+ <Table aria-label="Project List" variant="compact" cells={columns} rows={rows} actions={actions}>
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+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/projects/ScenarioState.js b/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js
new file mode 100644
index 00000000..285345e7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { ClockIcon, InfoIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons'
+
+function ScenarioState({ state }) {
+ switch (state) {
+ case 'CLAIMED':
+ case 'QUEUED':
+ return (
+ <span>
+ <ClockIcon color="blue" /> Queued
+ </span>
+ )
+ case 'RUNNING':
+ return (
+ <span>
+ <ClockIcon color="green" /> Running
+ </span>
+ )
+ case 'FINISHED':
+ return (
+ <span>
+ <CheckCircleIcon color="green" /> Finished
+ </span>
+ )
+ case 'FAILED':
+ return (
+ <span>
+ <ErrorCircleOIcon color="red" /> Failed
+ </span>
+ )
+ }
+
+ return 'Unknown'
+}
+
+ScenarioState.propTypes = {
+ state: PropTypes.oneOf(['QUEUED', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED']),
+}
+
+export default ScenarioState
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js
new file mode 100644
index 00000000..9966e3ba
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js
@@ -0,0 +1,108 @@
+/*
+ * 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 { Table, TableBody, TableHeader } from '@patternfly/react-table'
+import React from 'react'
+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'
+
+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')
+
+ const columns = ['Name', 'Topology', 'Trace', 'State']
+ const rows =
+ scenarios.length > 0
+ ? scenarios.map((scenario) => {
+ const topology = topologies?.get(scenario.topology.topologyId)
+
+ return [
+ scenario.name,
+ {
+ title: topology ? (
+ <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}>
+ <a>{topology.name}</a>
+ </Link>
+ ) : (
+ 'Unknown Topology'
+ ),
+ },
+ scenario.trace.traceId,
+ { title: <ScenarioState state={scenario.simulation.state} /> },
+ ]
+ })
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 4 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading Scenarios"
+ emptyTitle="No scenarios"
+ emptyText="You have not created any scenario for this portfolio yet. Click the New Scenario button to create one."
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actionResolver = (_, { rowIndex }) => [
+ {
+ title: 'Delete Scenario',
+ onClick: (_, rowId) => deleteScenario(scenarios[rowId]._id),
+ isDisabled: rowIndex === 0,
+ },
+ ]
+
+ return (
+ <Table
+ aria-label="Scenario List"
+ variant="compact"
+ cells={columns}
+ rows={rows}
+ actionResolver={scenarios.length > 0 ? actionResolver : undefined}
+ >
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+ScenarioTable.propTypes = {
+ portfolioId: PropTypes.string,
+}
+
+export default ScenarioTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
new file mode 100644
index 00000000..80099ece
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
@@ -0,0 +1,95 @@
+/*
+ * 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 { 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'
+
+const TopologyTable = ({ projectId }) => {
+ const { status, data: topologies = [] } = useProjectTopologies(projectId)
+ const { mutate: deleteTopology } = useMutation('deleteTopology')
+
+ 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>
+ ),
+ },
+ topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`,
+ parseAndFormatDateTime(topology.datetimeLastEdited),
+ ])
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 3 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading topologies"
+ emptyTitle="No topologies"
+ emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actionResolver = (_, { rowIndex }) => [
+ {
+ title: 'Delete Topology',
+ onClick: (_, rowId) => deleteTopology(topologies[rowId]._id),
+ isDisabled: rowIndex === 0,
+ },
+ ]
+
+ return (
+ <Table
+ aria-label="Topology List"
+ variant="compact"
+ cells={columns}
+ rows={rows}
+ actionResolver={topologies.length > 0 ? actionResolver : () => []}
+ >
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+TopologyTable.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default TopologyTable