summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/actions/experiments.js33
-rw-r--r--frontend/src/actions/modals/experiments.js14
-rw-r--r--frontend/src/actions/modals/portfolios.js14
-rw-r--r--frontend/src/actions/modals/scenarios.js14
-rw-r--r--frontend/src/actions/portfolios.js41
-rw-r--r--frontend/src/actions/scenarios.js43
-rw-r--r--frontend/src/actions/topology/building.js7
-rw-r--r--frontend/src/api/routes/experiments.js26
-rw-r--r--frontend/src/api/routes/portfolios.js42
-rw-r--r--frontend/src/api/routes/projects.js16
-rw-r--r--frontend/src/api/routes/scenarios.js42
-rw-r--r--frontend/src/components/app/map/MapStageComponent.js4
-rw-r--r--frontend/src/components/app/sidebars/Sidebar.js12
-rw-r--r--frontend/src/components/app/sidebars/project/PortfolioListComponent.js59
-rw-r--r--frontend/src/components/app/sidebars/project/ProjectSidebarComponent.js7
-rw-r--r--frontend/src/components/app/sidebars/project/ScenarioListComponent.js54
-rw-r--r--frontend/src/components/app/sidebars/project/TopologyListComponent.js8
-rw-r--r--frontend/src/components/experiments/ExperimentListComponent.js59
-rw-r--r--frontend/src/components/experiments/ExperimentRowComponent.js36
-rw-r--r--frontend/src/components/experiments/NewExperimentButtonComponent.js17
-rw-r--r--frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js99
-rw-r--r--frontend/src/components/modals/custom-components/NewScenarioModalComponent.js (renamed from frontend/src/components/modals/custom-components/NewExperimentModalComponent.js)86
-rw-r--r--frontend/src/containers/app/sidebars/project/PortfolioListContainer.js47
-rw-r--r--frontend/src/containers/app/sidebars/project/ProjectSidebarContainer.js7
-rw-r--r--frontend/src/containers/app/sidebars/project/ScenarioListContainer.js45
-rw-r--r--frontend/src/containers/app/sidebars/project/TopologyListContainer.js29
-rw-r--r--frontend/src/containers/experiments/ExperimentListContainer.js28
-rw-r--r--frontend/src/containers/experiments/ExperimentRowContainer.js30
-rw-r--r--frontend/src/containers/experiments/NewExperimentButtonContainer.js15
-rw-r--r--frontend/src/containers/modals/NewExperimentModal.js39
-rw-r--r--frontend/src/containers/modals/NewPortfolioModal.js32
-rw-r--r--frontend/src/containers/modals/NewScenarioModal.js46
-rw-r--r--frontend/src/pages/App.js78
-rw-r--r--frontend/src/reducers/construction-mode.js9
-rw-r--r--frontend/src/reducers/current-ids.js39
-rw-r--r--frontend/src/reducers/index.js6
-rw-r--r--frontend/src/reducers/interaction-level.js6
-rw-r--r--frontend/src/reducers/modals.js8
-rw-r--r--frontend/src/reducers/objects.js3
-rw-r--r--frontend/src/routes/index.js10
-rw-r--r--frontend/src/sagas/experiments.js91
-rw-r--r--frontend/src/sagas/index.js34
-rw-r--r--frontend/src/sagas/objects.js2
-rw-r--r--frontend/src/sagas/portfolios.js108
-rw-r--r--frontend/src/sagas/projects.js7
-rw-r--r--frontend/src/sagas/scenarios.js72
-rw-r--r--frontend/src/sagas/topology.js15
-rw-r--r--frontend/src/shapes/index.js37
-rw-r--r--frontend/src/util/available-metrics.js4
-rw-r--r--frontend/src/util/state-utils.js5
50 files changed, 1051 insertions, 534 deletions
diff --git a/frontend/src/actions/experiments.js b/frontend/src/actions/experiments.js
deleted file mode 100644
index dce48a09..00000000
--- a/frontend/src/actions/experiments.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export const FETCH_EXPERIMENTS_OF_PROJECT = 'FETCH_EXPERIMENTS_OF_PROJECT'
-export const ADD_EXPERIMENT = 'ADD_EXPERIMENT'
-export const DELETE_EXPERIMENT = 'DELETE_EXPERIMENT'
-export const OPEN_EXPERIMENT_SUCCEEDED = 'OPEN_EXPERIMENT_SUCCEEDED'
-
-export function fetchExperimentsOfProject(projectId) {
- return {
- type: FETCH_EXPERIMENTS_OF_PROJECT,
- projectId,
- }
-}
-
-export function addExperiment(experiment) {
- return {
- type: ADD_EXPERIMENT,
- experiment,
- }
-}
-
-export function deleteExperiment(id) {
- return {
- type: DELETE_EXPERIMENT,
- id,
- }
-}
-
-export function openExperimentSucceeded(projectId, experimentId) {
- return {
- type: OPEN_EXPERIMENT_SUCCEEDED,
- projectId,
- experimentId,
- }
-}
diff --git a/frontend/src/actions/modals/experiments.js b/frontend/src/actions/modals/experiments.js
deleted file mode 100644
index 37f1922f..00000000
--- a/frontend/src/actions/modals/experiments.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export const OPEN_NEW_EXPERIMENT_MODAL = 'OPEN_NEW_EXPERIMENT_MODAL'
-export const CLOSE_NEW_EXPERIMENT_MODAL = 'CLOSE_EXPERIMENT_MODAL'
-
-export function openNewExperimentModal() {
- return {
- type: OPEN_NEW_EXPERIMENT_MODAL,
- }
-}
-
-export function closeNewExperimentModal() {
- return {
- type: CLOSE_NEW_EXPERIMENT_MODAL,
- }
-}
diff --git a/frontend/src/actions/modals/portfolios.js b/frontend/src/actions/modals/portfolios.js
new file mode 100644
index 00000000..f6dce2e3
--- /dev/null
+++ b/frontend/src/actions/modals/portfolios.js
@@ -0,0 +1,14 @@
+export const OPEN_NEW_PORTFOLIO_MODAL = 'OPEN_NEW_PORTFOLIO_MODAL'
+export const CLOSE_NEW_PORTFOLIO_MODAL = 'CLOSE_PORTFOLIO_MODAL'
+
+export function openNewPortfolioModal() {
+ return {
+ type: OPEN_NEW_PORTFOLIO_MODAL,
+ }
+}
+
+export function closeNewPortfolioModal() {
+ return {
+ type: CLOSE_NEW_PORTFOLIO_MODAL,
+ }
+}
diff --git a/frontend/src/actions/modals/scenarios.js b/frontend/src/actions/modals/scenarios.js
new file mode 100644
index 00000000..b71cb27b
--- /dev/null
+++ b/frontend/src/actions/modals/scenarios.js
@@ -0,0 +1,14 @@
+export const OPEN_NEW_SCENARIO_MODAL = 'OPEN_NEW_SCENARIO_MODAL'
+export const CLOSE_NEW_SCENARIO_MODAL = 'CLOSE_SCENARIO_MODAL'
+
+export function openNewScenarioModal() {
+ return {
+ type: OPEN_NEW_SCENARIO_MODAL,
+ }
+}
+
+export function closeNewScenarioModal() {
+ return {
+ type: CLOSE_NEW_SCENARIO_MODAL,
+ }
+}
diff --git a/frontend/src/actions/portfolios.js b/frontend/src/actions/portfolios.js
new file mode 100644
index 00000000..d37886d8
--- /dev/null
+++ b/frontend/src/actions/portfolios.js
@@ -0,0 +1,41 @@
+export const ADD_PORTFOLIO = 'ADD_PORTFOLIO'
+export const UPDATE_PORTFOLIO = 'UPDATE_PORTFOLIO'
+export const DELETE_PORTFOLIO = 'DELETE_PORTFOLIO'
+export const OPEN_PORTFOLIO_SUCCEEDED = 'OPEN_PORTFOLIO_SUCCEEDED'
+export const SET_CURRENT_PORTFOLIO = 'SET_CURRENT_PORTFOLIO'
+
+export function addPortfolio(portfolio) {
+ return {
+ type: ADD_PORTFOLIO,
+ portfolio,
+ }
+}
+
+export function updatePortfolio(portfolio) {
+ return {
+ type: UPDATE_PORTFOLIO,
+ portfolio,
+ }
+}
+
+export function deletePortfolio(id) {
+ return {
+ type: DELETE_PORTFOLIO,
+ id,
+ }
+}
+
+export function openPortfolioSucceeded(projectId, portfolioId) {
+ return {
+ type: OPEN_PORTFOLIO_SUCCEEDED,
+ projectId,
+ portfolioId,
+ }
+}
+
+export function setCurrentPortfolio(portfolioId) {
+ return {
+ type: SET_CURRENT_PORTFOLIO,
+ portfolioId,
+ }
+}
diff --git a/frontend/src/actions/scenarios.js b/frontend/src/actions/scenarios.js
new file mode 100644
index 00000000..c8a90762
--- /dev/null
+++ b/frontend/src/actions/scenarios.js
@@ -0,0 +1,43 @@
+export const ADD_SCENARIO = 'ADD_SCENARIO'
+export const UPDATE_SCENARIO = 'UPDATE_SCENARIO'
+export const DELETE_SCENARIO = 'DELETE_SCENARIO'
+export const OPEN_SCENARIO_SUCCEEDED = 'OPEN_SCENARIO_SUCCEEDED'
+export const SET_CURRENT_SCENARIO = 'SET_CURRENT_SCENARIO'
+
+export function addScenario(scenario) {
+ return {
+ type: ADD_SCENARIO,
+ scenario,
+ }
+}
+
+export function updateScenario(scenario) {
+ return {
+ type: UPDATE_SCENARIO,
+ scenario,
+ }
+}
+
+export function deleteScenario(id) {
+ return {
+ type: DELETE_SCENARIO,
+ id,
+ }
+}
+
+export function openScenarioSucceeded(projectId, portfolioId, scenarioId) {
+ return {
+ type: OPEN_SCENARIO_SUCCEEDED,
+ projectId,
+ portfolioId,
+ scenarioId,
+ }
+}
+
+export function setCurrentScenario(portfolioId, scenarioId) {
+ return {
+ type: SET_CURRENT_SCENARIO,
+ portfolioId,
+ scenarioId,
+ }
+}
diff --git a/frontend/src/actions/topology/building.js b/frontend/src/actions/topology/building.js
index da2dc311..d6c53af9 100644
--- a/frontend/src/actions/topology/building.js
+++ b/frontend/src/actions/topology/building.js
@@ -1,5 +1,4 @@
export const SET_CURRENT_TOPOLOGY = 'SET_CURRENT_TOPOLOGY'
-export const RESET_CURRENT_TOPOLOGY = 'RESET_CURRENT_TOPOLOGY'
export const START_NEW_ROOM_CONSTRUCTION = 'START_NEW_ROOM_CONSTRUCTION'
export const START_NEW_ROOM_CONSTRUCTION_SUCCEEDED =
'START_NEW_ROOM_CONSTRUCTION_SUCCEEDED'
@@ -19,12 +18,6 @@ export function setCurrentTopology(topologyId) {
}
}
-export function resetCurrentTopology() {
- return {
- type: RESET_CURRENT_TOPOLOGY,
- }
-}
-
export function startNewRoomConstruction() {
return {
type: START_NEW_ROOM_CONSTRUCTION,
diff --git a/frontend/src/api/routes/experiments.js b/frontend/src/api/routes/experiments.js
deleted file mode 100644
index acc72f34..00000000
--- a/frontend/src/api/routes/experiments.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { deleteById, getById } from './util'
-import { sendRequest } from '../index'
-
-export function addExperiment(projectId, experiment) {
- return sendRequest({
- path: '/projects/{projectId}/experiments',
- method: 'POST',
- parameters: {
- body: {
- experiment,
- },
- path: {
- projectId,
- },
- query: {},
- },
- })
-}
-
-export function getExperiment(experimentId) {
- return getById('/experiments/{experimentId}', { experimentId })
-}
-
-export function deleteExperiment(experimentId) {
- return deleteById('/experiments/{experimentId}', { experimentId })
-}
diff --git a/frontend/src/api/routes/portfolios.js b/frontend/src/api/routes/portfolios.js
new file mode 100644
index 00000000..7c9ea02a
--- /dev/null
+++ b/frontend/src/api/routes/portfolios.js
@@ -0,0 +1,42 @@
+import { deleteById, getById } from './util'
+import { sendRequest } from '../index'
+
+export function addPortfolio(projectId, portfolio) {
+ return sendRequest({
+ path: '/projects/{projectId}/portfolios',
+ method: 'POST',
+ parameters: {
+ body: {
+ portfolio,
+ },
+ path: {
+ projectId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function getPortfolio(portfolioId) {
+ return getById('/portfolios/{portfolioId}', { portfolioId })
+}
+
+export function updatePortfolio(portfolioId, portfolio) {
+ return sendRequest({
+ path: '/portfolios/{projectId}',
+ method: 'POST',
+ parameters: {
+ body: {
+ portfolio,
+ },
+ path: {
+ portfolioId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deletePortfolio(portfolioId) {
+ return deleteById('/portfolios/{portfolioId}', { portfolioId })
+}
diff --git a/frontend/src/api/routes/projects.js b/frontend/src/api/routes/projects.js
index a261adb8..4109079c 100644
--- a/frontend/src/api/routes/projects.js
+++ b/frontend/src/api/routes/projects.js
@@ -38,19 +38,3 @@ export function updateProject(project) {
export function deleteProject(projectId) {
return deleteById('/projects/{projectId}', { projectId })
}
-
-export function addExperiment(projectId, experiment) {
- return sendRequest({
- path: '/projects/{projectId}/experiments',
- method: 'POST',
- parameters: {
- body: {
- experiment,
- },
- path: {
- projectId,
- },
- query: {},
- },
- })
-}
diff --git a/frontend/src/api/routes/scenarios.js b/frontend/src/api/routes/scenarios.js
new file mode 100644
index 00000000..ab2e8b86
--- /dev/null
+++ b/frontend/src/api/routes/scenarios.js
@@ -0,0 +1,42 @@
+import { deleteById, getById } from './util'
+import { sendRequest } from '../index'
+
+export function addScenario(portfolioId, scenario) {
+ return sendRequest({
+ path: '/portfolios/{portfolioId}/scenarios',
+ method: 'POST',
+ parameters: {
+ body: {
+ scenario,
+ },
+ path: {
+ portfolioId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function getScenario(scenarioId) {
+ return getById('/scenarios/{scenarioId}', { scenarioId })
+}
+
+export function updateScenario(scenarioId, scenario) {
+ return sendRequest({
+ path: '/scenarios/{projectId}',
+ method: 'POST',
+ parameters: {
+ body: {
+ scenario,
+ },
+ path: {
+ scenarioId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deleteScenario(scenarioId) {
+ return deleteById('/scenarios/{scenarioId}', { scenarioId })
+}
diff --git a/frontend/src/components/app/map/MapStageComponent.js b/frontend/src/components/app/map/MapStageComponent.js
index 455604e4..f1c2b211 100644
--- a/frontend/src/components/app/map/MapStageComponent.js
+++ b/frontend/src/components/app/map/MapStageComponent.js
@@ -23,11 +23,9 @@ class MapStageComponent extends React.Component {
this.updateScale = this.updateScale.bind(this)
}
- componentWillMount() {
+ componentDidMount() {
this.updateDimensions()
- }
- componentDidMount() {
window.addEventListener('resize', this.updateDimensions)
window.addEventListener('wheel', this.updateScale)
diff --git a/frontend/src/components/app/sidebars/Sidebar.js b/frontend/src/components/app/sidebars/Sidebar.js
index 7ba8639a..a47a67c0 100644
--- a/frontend/src/components/app/sidebars/Sidebar.js
+++ b/frontend/src/components/app/sidebars/Sidebar.js
@@ -1,8 +1,18 @@
+import PropTypes from 'prop-types'
import classNames from 'classnames'
import React from 'react'
import './Sidebar.css'
class Sidebar extends React.Component {
+ static propTypes = {
+ isRight: PropTypes.bool.isRequired,
+ collapsible: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ collapsible: true
+ }
+
state = {
collapsed: false,
}
@@ -41,7 +51,7 @@ class Sidebar extends React.Component {
onWheel={e => e.stopPropagation()}
>
{this.props.children}
- {collapseButton}
+ {this.props.collapsible && collapseButton}
</div>
)
}
diff --git a/frontend/src/components/app/sidebars/project/PortfolioListComponent.js b/frontend/src/components/app/sidebars/project/PortfolioListComponent.js
new file mode 100644
index 00000000..a31f11cf
--- /dev/null
+++ b/frontend/src/components/app/sidebars/project/PortfolioListComponent.js
@@ -0,0 +1,59 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shapes from '../../../../shapes'
+import { Link } from 'react-router-dom'
+import FontAwesome from 'react-fontawesome'
+import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer'
+
+class PortfolioListComponent extends React.Component {
+ static propTypes = {
+ portfolios: PropTypes.arrayOf(Shapes.Portfolio),
+ currentProjectId: PropTypes.string.isRequired,
+ currentPortfolioId: PropTypes.string,
+ onNewPortfolio: PropTypes.func.isRequired,
+ onChoosePortfolio: PropTypes.func.isRequired,
+ onDeletePortfolio: PropTypes.func.isRequired,
+ }
+
+ onDelete(id) {
+ this.props.onDeletePortfolio(id)
+ }
+
+ render() {
+ return (
+ <div className="pb-3">
+ <h2>
+ Portfolios
+ <button className="btn btn-outline-primary float-right" onClick={this.props.onNewPortfolio.bind(this)}>
+ <FontAwesome name="plus"/>
+ </button>
+ </h2>
+
+ {this.props.portfolios.map((portfolio, idx) => (
+ <div key={portfolio._id}>
+ <div className="row mb-1">
+ <div
+ className={'col-8 align-self-center ' + (portfolio._id === this.props.currentPortfolioId ? 'font-weight-bold' : '')}>
+ {portfolio.name}
+ </div>
+ <div className="col-4 text-right">
+ <Link
+ className="btn btn-outline-primary mr-1 fa fa-play"
+ to={`/projects/${this.props.currentProjectId}/portfolios/${portfolio._id}`}
+ onClick={() => this.props.onChoosePortfolio(portfolio._id)}
+ />
+ <span
+ className="btn btn-outline-danger fa fa-trash"
+ onClick={() => this.onDelete(portfolio._id)}
+ />
+ </div>
+ </div>
+ <ScenarioListContainer portfolioId={portfolio._id}/>
+ </div>
+ ))}
+ </div>
+ )
+ }
+}
+
+export default PortfolioListComponent
diff --git a/frontend/src/components/app/sidebars/project/ProjectSidebarComponent.js b/frontend/src/components/app/sidebars/project/ProjectSidebarComponent.js
index d6e39ff6..b21b012b 100644
--- a/frontend/src/components/app/sidebars/project/ProjectSidebarComponent.js
+++ b/frontend/src/components/app/sidebars/project/ProjectSidebarComponent.js
@@ -1,11 +1,12 @@
import React from 'react'
import Sidebar from '../Sidebar'
import TopologyListContainer from '../../../../containers/app/sidebars/project/TopologyListContainer'
+import PortfolioListContainer from '../../../../containers/app/sidebars/project/PortfolioListContainer'
-const ProjectSidebarComponent = () => (
- <Sidebar isRight={false}>
+const ProjectSidebarComponent = ({collapsible}) => (
+ <Sidebar isRight={false} collapsible={collapsible}>
<TopologyListContainer/>
- <h2>Portfolios</h2>
+ <PortfolioListContainer/>
</Sidebar>
)
diff --git a/frontend/src/components/app/sidebars/project/ScenarioListComponent.js b/frontend/src/components/app/sidebars/project/ScenarioListComponent.js
new file mode 100644
index 00000000..9d2e261e
--- /dev/null
+++ b/frontend/src/components/app/sidebars/project/ScenarioListComponent.js
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shapes from '../../../../shapes'
+import { Link } from 'react-router-dom'
+import FontAwesome from 'react-fontawesome'
+
+class ScenarioListComponent extends React.Component {
+ static propTypes = {
+ scenarios: PropTypes.arrayOf(Shapes.Scenario),
+ portfolioId: PropTypes.string,
+ currentProjectId: PropTypes.string.isRequired,
+ currentScenarioId: PropTypes.string,
+ onNewScenario: PropTypes.func.isRequired,
+ onChooseScenario: PropTypes.func.isRequired,
+ onDeleteScenario: PropTypes.func.isRequired,
+ }
+
+ onDelete(id) {
+ this.props.onDeleteScenario(id)
+ }
+
+ render() {
+ return (
+ <>
+ {this.props.scenarios.map((scenario, idx) => (
+ <div key={scenario._id} className="row mb-1">
+ <div className={'col-8 pl-5 align-self-center ' + (scenario._id === this.props.currentScenarioId ? 'font-weight-bold' : '')}>
+ {scenario.name}
+ </div>
+ <div className="col-4 text-right">
+ <Link
+ className="btn btn-outline-primary mr-1 fa fa-play"
+ to={`/projects/${this.props.currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`}
+ onClick={() => this.props.onChooseScenario(scenario.portfolioId, scenario._id)}
+ />
+ <span
+ className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')}
+ onClick={() => idx !== 0 ? this.onDelete(scenario._id) : undefined}
+ />
+ </div>
+ </div>
+ ))}
+ <div className="pl-4 mb-2">
+ <div className="btn btn-outline-primary" onClick={() => this.props.onNewScenario(this.props.portfolioId)}>
+ <FontAwesome name="plus" className="mr-1"/>
+ New scenario
+ </div>
+ </div>
+ </>
+ )
+ }
+}
+
+export default ScenarioListComponent
diff --git a/frontend/src/components/app/sidebars/project/TopologyListComponent.js b/frontend/src/components/app/sidebars/project/TopologyListComponent.js
index 98615711..b8b41200 100644
--- a/frontend/src/components/app/sidebars/project/TopologyListComponent.js
+++ b/frontend/src/components/app/sidebars/project/TopologyListComponent.js
@@ -5,7 +5,6 @@ import FontAwesome from 'react-fontawesome'
class TopologyListComponent extends React.Component {
static propTypes = {
- show: PropTypes.bool.isRequired,
topologies: PropTypes.arrayOf(Shapes.Topology),
currentTopologyId: PropTypes.string,
onChooseTopology: PropTypes.func.isRequired,
@@ -17,13 +16,6 @@ class TopologyListComponent extends React.Component {
this.props.onChooseTopology(id)
}
- onDuplicate() {
- this.props.onNewTopology(
- this.textInput.value,
- this.originTopology.value,
- )
- }
-
onDelete(id) {
this.props.onDeleteTopology(id)
}
diff --git a/frontend/src/components/experiments/ExperimentListComponent.js b/frontend/src/components/experiments/ExperimentListComponent.js
deleted file mode 100644
index 3c53fc94..00000000
--- a/frontend/src/components/experiments/ExperimentListComponent.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import ExperimentRowContainer from '../../containers/experiments/ExperimentRowContainer'
-
-const ExperimentListComponent = ({ experimentIds, loading }) => {
- let alert
-
- if (loading) {
- alert = (
- <div className="alert alert-success">
- <span className="fa fa-refresh fa-spin mr-2"/>
- <strong>Loading Experiments...</strong>
- </div>
- )
- } else if (experimentIds.length === 0 && !loading) {
- alert = (
- <div className="alert alert-info">
- <span className="fa fa-question-circle mr-2"/>
- <strong>No experiments here yet...</strong> Add some with the button
- below!
- </div>
- )
- }
-
- return (
- <div className="vertically-expanding-container">
- {alert ? (
- alert
- ) : (
- <table className="table table-striped">
- <thead>
- <tr>
- <th>Name</th>
- <th>Topology</th>
- <th>Trace</th>
- <th>Scheduler</th>
- <th/>
- </tr>
- </thead>
- <tbody>
- {experimentIds.map(experimentId => (
- <ExperimentRowContainer
- experimentId={experimentId}
- key={experimentId}
- />
- ))}
- </tbody>
- </table>
- )}
- </div>
- )
-}
-
-ExperimentListComponent.propTypes = {
- experimentIds: PropTypes.arrayOf(PropTypes.string).isRequired,
- loading: PropTypes.bool,
-}
-
-export default ExperimentListComponent
diff --git a/frontend/src/components/experiments/ExperimentRowComponent.js b/frontend/src/components/experiments/ExperimentRowComponent.js
deleted file mode 100644
index c6ae1ba4..00000000
--- a/frontend/src/components/experiments/ExperimentRowComponent.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Link } from 'react-router-dom'
-import Shapes from '../../shapes/index'
-
-const ExperimentRowComponent = ({ experiment, projectId, onDelete }) => (
- <tr>
- <td className="pt-3">{experiment.name}</td>
- <td className="pt-3">{experiment.topology.name}</td>
- <td className="pt-3">{experiment.trace.name}</td>
- <td className="pt-3">{experiment.scheduler.name}</td>
- <td className="text-right">
- <Link
- to={'/projects/' + projectId + '/experiments/' + experiment._id}
- className="btn btn-outline-primary btn-sm mr-2"
- title="Open this experiment"
- >
- <span className="fa fa-play"/>
- </Link>
- <div
- className="btn btn-outline-danger btn-sm"
- title="Delete this experiment"
- onClick={() => onDelete(experiment._id)}
- >
- <span className="fa fa-trash"/>
- </div>
- </td>
- </tr>
-)
-
-ExperimentRowComponent.propTypes = {
- experiment: Shapes.Experiment.isRequired,
- projectId: PropTypes.string.isRequired,
-}
-
-export default ExperimentRowComponent
diff --git a/frontend/src/components/experiments/NewExperimentButtonComponent.js b/frontend/src/components/experiments/NewExperimentButtonComponent.js
deleted file mode 100644
index 2902825d..00000000
--- a/frontend/src/components/experiments/NewExperimentButtonComponent.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-
-const NewExperimentButtonComponent = ({ onClick }) => (
- <div className="bottom-btn-container">
- <div className="btn btn-primary float-right" onClick={onClick}>
- <span className="fa fa-plus mr-2"/>
- New Experiment
- </div>
- </div>
-)
-
-NewExperimentButtonComponent.propTypes = {
- onClick: PropTypes.func.isRequired,
-}
-
-export default NewExperimentButtonComponent
diff --git a/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js b/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js
new file mode 100644
index 00000000..ace2d751
--- /dev/null
+++ b/frontend/src/components/modals/custom-components/NewPortfolioModalComponent.js
@@ -0,0 +1,99 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Modal from '../Modal'
+import { AVAILABLE_METRICS } from '../../../util/available-metrics'
+
+class NewPortfolioModalComponent extends React.Component {
+ static propTypes = {
+ show: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
+ }
+
+ constructor(props) {
+ super(props)
+ this.metricCheckboxes = {}
+ }
+
+ componentDidMount() {
+ this.reset()
+ }
+
+ reset() {
+ this.textInput.value = ''
+ AVAILABLE_METRICS.forEach(metric => {
+ this.metricCheckboxes[metric].checked = true
+ })
+ this.repeatsInput.value = 1
+ }
+
+ onSubmit() {
+ this.props.callback(
+ this.textInput.value,
+ {
+ enabledMetrics: AVAILABLE_METRICS.filter(metric => this.metricCheckboxes[metric].checked),
+ repeatsPerScenario: parseInt(this.repeatsInput.value),
+ },
+ )
+ this.reset()
+ }
+
+ onCancel() {
+ this.props.callback(undefined)
+ this.reset()
+ }
+
+ render() {
+ return (
+ <Modal
+ title="New Portfolio"
+ show={this.props.show}
+ onSubmit={this.onSubmit.bind(this)}
+ onCancel={this.onCancel.bind(this)}
+ >
+ <form
+ onSubmit={e => {
+ e.preventDefault()
+ this.onSubmit()
+ }}
+ >
+ <div className="form-group">
+ <label className="form-control-label">Name</label>
+ <input
+ type="text"
+ className="form-control"
+ required
+ ref={textInput => (this.textInput = textInput)}
+ />
+ </div>
+ <h4>Targets</h4>
+ <h5>Metrics</h5>
+ <div className="form-group">
+ {AVAILABLE_METRICS.map(metric => (
+ <div className="form-check" key={metric}>
+ <label className="form-check-label">
+ <input
+ type="checkbox"
+ className="form-check-input"
+ ref={checkbox => (this.metricCheckboxes[metric] = checkbox)}
+ />
+ <code>{metric}</code>
+ </label>
+ </div>
+ ))}
+ </div>
+ <div className="form-group">
+ <label className="form-control-label">Repeats per scenario</label>
+ <input
+ type="number"
+ className="form-control"
+ required
+ ref={repeatsInput => (this.repeatsInput = repeatsInput)}
+ />
+ </div>
+ </form>
+ </Modal>
+ )
+ }
+}
+
+export default NewPortfolioModalComponent
diff --git a/frontend/src/components/modals/custom-components/NewExperimentModalComponent.js b/frontend/src/components/modals/custom-components/NewScenarioModalComponent.js
index ce685837..4c2df2f6 100644
--- a/frontend/src/components/modals/custom-components/NewExperimentModalComponent.js
+++ b/frontend/src/components/modals/custom-components/NewScenarioModalComponent.js
@@ -3,28 +3,46 @@ import React from 'react'
import Shapes from '../../../shapes'
import Modal from '../Modal'
-class NewExperimentModalComponent extends React.Component {
+class NewScenarioModalComponent extends React.Component {
static propTypes = {
show: PropTypes.bool.isRequired,
+ currentPortfolioId: PropTypes.string.isRequired,
+ traces: PropTypes.arrayOf(Shapes.Trace),
topologies: PropTypes.arrayOf(Shapes.Topology),
schedulers: PropTypes.arrayOf(Shapes.Scheduler),
- traces: PropTypes.arrayOf(Shapes.Trace),
callback: PropTypes.func.isRequired,
}
+ componentDidMount() {
+ this.reset()
+ }
+
reset() {
this.textInput.value = ''
- this.topologySelect.selectedIndex = 0
this.traceSelect.selectedIndex = 0
+ this.traceLoadInput.value = 1.0
+ this.topologySelect.selectedIndex = 0
+ this.failuresCheckbox.checked = false
+ this.performanceInterferenceCheckbox.checked = false
this.schedulerSelect.selectedIndex = 0
}
onSubmit() {
this.props.callback(
this.textInput.value,
- this.topologySelect.value,
- this.traceSelect.value,
- this.schedulerSelect.value,
+ this.props.currentPortfolioId,
+ {
+ traceId: this.traceSelect.value,
+ loadSamplingFraction: parseFloat(this.traceLoadInput.value),
+ },
+ {
+ topologyId: this.topologySelect.value
+ },
+ {
+ failuresEnabled: this.failuresCheckbox.checked,
+ performanceInterferenceEnabled: this.performanceInterferenceCheckbox.checked,
+ schedulerName: this.schedulerSelect.value,
+ },
)
this.reset()
}
@@ -37,7 +55,7 @@ class NewExperimentModalComponent extends React.Component {
render() {
return (
<Modal
- title="New Experiment"
+ title="New Scenario"
show={this.props.show}
onSubmit={this.onSubmit.bind(this)}
onCancel={this.onCancel.bind(this)}
@@ -57,32 +75,64 @@ class NewExperimentModalComponent extends React.Component {
ref={textInput => (this.textInput = textInput)}
/>
</div>
+ <h4>Trace</h4>
<div className="form-group">
- <label className="form-control-label">Topology</label>
+ <label className="form-control-label">Trace</label>
<select
className="form-control"
- ref={topologySelect => (this.topologySelect = topologySelect)}
+ ref={traceSelect => (this.traceSelect = traceSelect)}
>
- {this.props.topologies.map(topology => (
- <option value={topology._id} key={topology._id}>
- {topology.name}
+ {this.props.traces.map(trace => (
+ <option value={trace._id} key={trace._id}>
+ {trace.name}
</option>
))}
</select>
</div>
<div className="form-group">
- <label className="form-control-label">Trace</label>
+ <label className="form-control-label">Load sampling fraction</label>
+ <input
+ type="number"
+ className="form-control"
+ required
+ ref={traceLoadInput => (this.traceLoadInput = traceLoadInput)}
+ />
+ </div>
+ <h4>Topology</h4>
+ <div className="form-group">
+ <label className="form-control-label">Topology</label>
<select
className="form-control"
- ref={traceSelect => (this.traceSelect = traceSelect)}
+ ref={topologySelect => (this.topologySelect = topologySelect)}
>
- {this.props.traces.map(trace => (
- <option value={trace._id} key={trace._id}>
- {trace.name}
+ {this.props.topologies.map(topology => (
+ <option value={topology._id} key={topology._id}>
+ {topology.name}
</option>
))}
</select>
</div>
+ <h4>Operational Phenomena</h4>
+ <div className="form-check">
+ <label className="form-check-label">
+ <input
+ type="checkbox"
+ className="form-check-input"
+ ref={failuresCheckbox => (this.failuresCheckbox = failuresCheckbox)}
+ />
+ <span className="ml-2">Enable failures</span>
+ </label>
+ </div>
+ <div className="form-check">
+ <label className="form-check-label">
+ <input
+ type="checkbox"
+ className="form-check-input"
+ ref={performanceInterferenceCheckbox => (this.performanceInterferenceCheckbox = performanceInterferenceCheckbox)}
+ />
+ <span className="ml-2">Enable performance interference</span>
+ </label>
+ </div>
<div className="form-group">
<label className="form-control-label">Scheduler</label>
<select
@@ -102,4 +152,4 @@ class NewExperimentModalComponent extends React.Component {
}
}
-export default NewExperimentModalComponent
+export default NewScenarioModalComponent
diff --git a/frontend/src/containers/app/sidebars/project/PortfolioListContainer.js b/frontend/src/containers/app/sidebars/project/PortfolioListContainer.js
new file mode 100644
index 00000000..d32a5c60
--- /dev/null
+++ b/frontend/src/containers/app/sidebars/project/PortfolioListContainer.js
@@ -0,0 +1,47 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent'
+import { deletePortfolio, setCurrentPortfolio } from '../../../../actions/portfolios'
+import { openNewPortfolioModal } from '../../../../actions/modals/portfolios'
+import { getState } from '../../../../util/state-utils'
+import { setCurrentTopology } from '../../../../actions/topology/building'
+
+const mapStateToProps = state => {
+ let portfolios = state.objects.project[state.currentProjectId] ? state.objects.project[state.currentProjectId].portfolioIds.map(t => (
+ state.objects.portfolio[t]
+ )) : []
+ if (portfolios.filter(t => !t).length > 0) {
+ portfolios = []
+ }
+
+ return {
+ currentProjectId: state.currentProjectId,
+ currentPortfolioId: state.currentPortfolioId,
+ portfolios,
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onNewPortfolio: () => {
+ dispatch(openNewPortfolioModal())
+ },
+ onChoosePortfolio: (portfolioId) => {
+ dispatch(setCurrentPortfolio(portfolioId))
+ },
+ onDeletePortfolio: async (id) => {
+ if (id) {
+ const state = await getState(dispatch)
+ dispatch(deletePortfolio(id))
+ dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
+ ownProps.history.push(`/projects/${state.currentProjectId}`)
+ }
+ },
+ }
+}
+
+const PortfolioListContainer = withRouter(connect(mapStateToProps, mapDispatchToProps)(
+ PortfolioListComponent,
+))
+
+export default PortfolioListContainer
diff --git a/frontend/src/containers/app/sidebars/project/ProjectSidebarContainer.js b/frontend/src/containers/app/sidebars/project/ProjectSidebarContainer.js
index ced0b18b..3951c24a 100644
--- a/frontend/src/containers/app/sidebars/project/ProjectSidebarContainer.js
+++ b/frontend/src/containers/app/sidebars/project/ProjectSidebarContainer.js
@@ -1,5 +1,10 @@
+import React from 'react'
+import { withRouter } from 'react-router-dom'
import ProjectSidebarComponent from '../../../../components/app/sidebars/project/ProjectSidebarComponent'
-const ProjectSidebarContainer = ProjectSidebarComponent
+const ProjectSidebarContainer = withRouter(({ location, ...props }) => (
+ <ProjectSidebarComponent
+ collapsible={location.pathname.indexOf('portfolios') === -1 && location.pathname.indexOf('scenarios') === -1} {...props}/>
+))
export default ProjectSidebarContainer
diff --git a/frontend/src/containers/app/sidebars/project/ScenarioListContainer.js b/frontend/src/containers/app/sidebars/project/ScenarioListContainer.js
new file mode 100644
index 00000000..2fd56d2b
--- /dev/null
+++ b/frontend/src/containers/app/sidebars/project/ScenarioListContainer.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux'
+import ScenarioListComponent from '../../../../components/app/sidebars/project/ScenarioListComponent'
+import { openNewScenarioModal } from '../../../../actions/modals/scenarios'
+import { deleteScenario, setCurrentScenario } from '../../../../actions/scenarios'
+import { setCurrentPortfolio } from '../../../../actions/portfolios'
+
+const mapStateToProps = (state, ownProps) => {
+ let scenarios = state.objects.portfolio[ownProps.portfolioId] ? state.objects.portfolio[ownProps.portfolioId].scenarioIds.map(t => (
+ state.objects.scenario[t]
+ )) : []
+ if (scenarios.filter(t => !t).length > 0) {
+ scenarios = []
+ }
+
+ return {
+ currentProjectId: state.currentProjectId,
+ currentScenarioId: state.currentScenarioId,
+ scenarios,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onNewScenario: (currentPortfolioId) => {
+ dispatch(setCurrentPortfolio(currentPortfolioId))
+ dispatch(openNewScenarioModal())
+ },
+ onChooseScenario: (portfolioId, scenarioId) => {
+ dispatch(setCurrentScenario(portfolioId, scenarioId))
+ },
+ onDeleteScenario: (id) => {
+ if (id) {
+ dispatch(
+ deleteScenario(id),
+ )
+ }
+ },
+ }
+}
+
+const ScenarioListContainer = connect(mapStateToProps, mapDispatchToProps)(
+ ScenarioListComponent,
+)
+
+export default ScenarioListContainer
diff --git a/frontend/src/containers/app/sidebars/project/TopologyListContainer.js b/frontend/src/containers/app/sidebars/project/TopologyListContainer.js
index cab47c8d..6905c7c5 100644
--- a/frontend/src/containers/app/sidebars/project/TopologyListContainer.js
+++ b/frontend/src/containers/app/sidebars/project/TopologyListContainer.js
@@ -2,7 +2,9 @@ import { connect } from 'react-redux'
import TopologyListComponent from '../../../../components/app/sidebars/project/TopologyListComponent'
import { setCurrentTopology } from '../../../../actions/topology/building'
import { openNewTopologyModal } from '../../../../actions/modals/topology'
-import { deleteTopology } from '../../../../actions/topologies'
+import { withRouter } from 'react-router-dom'
+import { getState } from '../../../../util/state-utils'
+import { deleteScenario } from '../../../../actions/scenarios'
const mapStateToProps = state => {
let topologies = state.objects.project[state.currentProjectId] ? state.objects.project[state.currentProjectId].topologyIds.map(t => (
@@ -13,34 +15,35 @@ const mapStateToProps = state => {
}
return {
- show: state.modals.changeTopologyModalVisible,
currentTopologyId: state.currentTopologyId,
topologies,
+
}
}
-const mapDispatchToProps = dispatch => {
+const mapDispatchToProps = (dispatch, ownProps) => {
return {
- onChooseTopology: (id) => {
- dispatch(
- setCurrentTopology(id),
- )
+ onChooseTopology: async (id) => {
+ dispatch(setCurrentTopology(id))
+ const state = await getState(dispatch)
+ ownProps.history.push(`/projects/${state.currentProjectId}`)
},
onNewTopology: () => {
dispatch(openNewTopologyModal())
},
- onDeleteTopology: (id) => {
+ onDeleteTopology: async (id) => {
if (id) {
- dispatch(
- deleteTopology(id),
- )
+ const state = await getState(dispatch)
+ dispatch(deleteScenario(id))
+ dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
+ ownProps.history.push(`/projects/${state.currentProjectId}`)
}
},
}
}
-const TopologyListContainer = connect(mapStateToProps, mapDispatchToProps)(
+const TopologyListContainer = withRouter(connect(mapStateToProps, mapDispatchToProps)(
TopologyListComponent,
-)
+))
export default TopologyListContainer
diff --git a/frontend/src/containers/experiments/ExperimentListContainer.js b/frontend/src/containers/experiments/ExperimentListContainer.js
deleted file mode 100644
index 0b3b70ca..00000000
--- a/frontend/src/containers/experiments/ExperimentListContainer.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { connect } from 'react-redux'
-import ExperimentListComponent from '../../components/experiments/ExperimentListComponent'
-
-const mapStateToProps = state => {
- if (
- state.currentProjectId === '-1' ||
- !('experimentIds' in state.objects.project[state.currentProjectId])
- ) {
- return {
- loading: true,
- experimentIds: [],
- }
- }
-
- const experimentIds =
- state.objects.project[state.currentProjectId].experimentIds
- if (experimentIds) {
- return {
- experimentIds,
- }
- }
-}
-
-const ExperimentListContainer = connect(mapStateToProps)(
- ExperimentListComponent,
-)
-
-export default ExperimentListContainer
diff --git a/frontend/src/containers/experiments/ExperimentRowContainer.js b/frontend/src/containers/experiments/ExperimentRowContainer.js
deleted file mode 100644
index 87d8af67..00000000
--- a/frontend/src/containers/experiments/ExperimentRowContainer.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { connect } from 'react-redux'
-import { deleteExperiment } from '../../actions/experiments'
-import ExperimentRowComponent from '../../components/experiments/ExperimentRowComponent'
-
-const mapStateToProps = (state, ownProps) => {
- const experiment = Object.assign(
- {},
- state.objects.experiment[ownProps.experimentId],
- )
- experiment.trace = state.objects.trace[experiment.traceId]
- experiment.scheduler = state.objects.scheduler[experiment.schedulerName]
- experiment.topology = state.objects.topology[experiment.topologyId]
-
- return {
- experiment,
- projectId: state.currentProjectId,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- onDelete: id => dispatch(deleteExperiment(id)),
- }
-}
-
-const ExperimentRowContainer = connect(mapStateToProps, mapDispatchToProps)(
- ExperimentRowComponent,
-)
-
-export default ExperimentRowContainer
diff --git a/frontend/src/containers/experiments/NewExperimentButtonContainer.js b/frontend/src/containers/experiments/NewExperimentButtonContainer.js
deleted file mode 100644
index 41895d8a..00000000
--- a/frontend/src/containers/experiments/NewExperimentButtonContainer.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { connect } from 'react-redux'
-import { openNewExperimentModal } from '../../actions/modals/experiments'
-import NewExperimentButtonComponent from '../../components/experiments/NewExperimentButtonComponent'
-
-const mapDispatchToProps = dispatch => {
- return {
- onClick: () => dispatch(openNewExperimentModal()),
- }
-}
-
-const NewExperimentButtonContainer = connect(undefined, mapDispatchToProps)(
- NewExperimentButtonComponent,
-)
-
-export default NewExperimentButtonContainer
diff --git a/frontend/src/containers/modals/NewExperimentModal.js b/frontend/src/containers/modals/NewExperimentModal.js
deleted file mode 100644
index f07b53e6..00000000
--- a/frontend/src/containers/modals/NewExperimentModal.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { connect } from 'react-redux'
-import { addExperiment } from '../../actions/experiments'
-import { closeNewExperimentModal } from '../../actions/modals/experiments'
-import NewExperimentModalComponent from '../../components/modals/custom-components/NewExperimentModalComponent'
-
-const mapStateToProps = state => {
- return {
- show: state.modals.newExperimentModalVisible,
- topologies: state.objects.project[state.currentProjectId].topologyIds.map(t => (
- state.objects.topology[t]
- )),
- traces: Object.values(state.objects.trace),
- schedulers: Object.values(state.objects.scheduler),
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- callback: (name, topologyId, traceId, schedulerName) => {
- if (name) {
- dispatch(
- addExperiment({
- name,
- topologyId,
- traceId,
- schedulerName,
- }),
- )
- }
- dispatch(closeNewExperimentModal())
- },
- }
-}
-
-const NewExperimentModal = connect(mapStateToProps, mapDispatchToProps)(
- NewExperimentModalComponent,
-)
-
-export default NewExperimentModal
diff --git a/frontend/src/containers/modals/NewPortfolioModal.js b/frontend/src/containers/modals/NewPortfolioModal.js
new file mode 100644
index 00000000..5c4644d5
--- /dev/null
+++ b/frontend/src/containers/modals/NewPortfolioModal.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux'
+import NewPortfolioModalComponent from '../../components/modals/custom-components/NewPortfolioModalComponent'
+import { addPortfolio } from '../../actions/portfolios'
+import { closeNewPortfolioModal } from '../../actions/modals/portfolios'
+
+const mapStateToProps = state => {
+ return {
+ show: state.modals.newPortfolioModalVisible,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ callback: (name, targets) => {
+ if (name) {
+ dispatch(
+ addPortfolio({
+ name,
+ targets
+ }),
+ )
+ }
+ dispatch(closeNewPortfolioModal())
+ },
+ }
+}
+
+const NewPortfolioModal = connect(mapStateToProps, mapDispatchToProps)(
+ NewPortfolioModalComponent,
+)
+
+export default NewPortfolioModal
diff --git a/frontend/src/containers/modals/NewScenarioModal.js b/frontend/src/containers/modals/NewScenarioModal.js
new file mode 100644
index 00000000..0b273ed0
--- /dev/null
+++ b/frontend/src/containers/modals/NewScenarioModal.js
@@ -0,0 +1,46 @@
+import { connect } from 'react-redux'
+import NewScenarioModalComponent from '../../components/modals/custom-components/NewScenarioModalComponent'
+import { addScenario } from '../../actions/scenarios'
+import { closeNewScenarioModal } from '../../actions/modals/scenarios'
+
+const mapStateToProps = state => {
+ let topologies = state.currentProjectId !== '-1' ? state.objects.project[state.currentProjectId].topologyIds.map(t => (
+ state.objects.topology[t]
+ )) : []
+ if (topologies.filter(t => !t).length > 0) {
+ topologies = []
+ }
+
+ return {
+ show: state.modals.newScenarioModalVisible,
+ currentPortfolioId: state.currentPortfolioId,
+ traces: Object.values(state.objects.trace),
+ topologies,
+ schedulers: Object.values(state.objects.scheduler),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ callback: (name, portfolioId, trace, topology, operational) => {
+ if (name) {
+ dispatch(
+ addScenario({
+ portfolioId,
+ name,
+ trace,
+ topology,
+ operational,
+ }),
+ )
+ }
+ dispatch(closeNewScenarioModal())
+ },
+ }
+}
+
+const NewScenarioModal = connect(mapStateToProps, mapDispatchToProps)(
+ NewScenarioModalComponent,
+)
+
+export default NewScenarioModal
diff --git a/frontend/src/pages/App.js b/frontend/src/pages/App.js
index 8f99d1bd..3ccae29d 100644
--- a/frontend/src/pages/App.js
+++ b/frontend/src/pages/App.js
@@ -3,9 +3,8 @@ import React from 'react'
import DocumentTitle from 'react-document-title'
import { connect } from 'react-redux'
import { ShortcutManager } from 'react-shortcuts'
-import { openExperimentSucceeded } from '../actions/experiments'
+import { openPortfolioSucceeded } from '../actions/portfolios'
import { openProjectSucceeded } from '../actions/projects'
-import { resetCurrentTopology } from '../actions/topology/building'
import ToolPanelComponent from '../components/app/map/controls/ToolPanelComponent'
import LoadingScreen from '../components/app/map/LoadingScreen'
import ScaleIndicatorContainer from '../containers/app/map/controls/ScaleIndicatorContainer'
@@ -18,25 +17,33 @@ import EditRackNameModal from '../containers/modals/EditRackNameModal'
import EditRoomNameModal from '../containers/modals/EditRoomNameModal'
import KeymapConfiguration from '../shortcuts/keymap'
import NewTopologyModal from '../containers/modals/NewTopologyModal'
-import { openNewTopologyModal } from '../actions/modals/topology'
import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
import ProjectSidebarContainer from '../containers/app/sidebars/project/ProjectSidebarContainer'
+import { openScenarioSucceeded } from '../actions/scenarios'
+import NewPortfolioModal from '../containers/modals/NewPortfolioModal'
+import NewScenarioModal from '../containers/modals/NewScenarioModal'
const shortcutManager = new ShortcutManager(KeymapConfiguration)
class AppComponent extends React.Component {
static propTypes = {
projectId: PropTypes.string.isRequired,
- experimentId: PropTypes.number,
+ portfolioId: PropTypes.string,
+ scenarioId: PropTypes.string,
projectName: PropTypes.string,
- onViewTopologies: PropTypes.func,
}
static childContextTypes = {
shortcuts: PropTypes.object.isRequired,
}
componentDidMount() {
- this.props.openProjectSucceeded(this.props.projectId)
+ if (this.props.scenarioId) {
+ this.props.openScenarioSucceeded(this.props.projectId, this.props.portfolioId, this.props.scenarioId)
+ } else if (this.props.portfolioId) {
+ this.props.openPortfolioSucceeded(this.props.projectId, this.props.portfolioId)
+ } else {
+ this.props.openProjectSucceeded(this.props.projectId)
+ }
}
getChildContext() {
@@ -46,26 +53,51 @@ class AppComponent extends React.Component {
}
render() {
+ const constructionElements = this.props.topologyIsLoading ? (
+ <div className="full-height d-flex align-items-center justify-content-center">
+ <LoadingScreen/>
+ </div>
+ ) : (
+ <div className="full-height">
+ <MapStage/>
+ <ScaleIndicatorContainer/>
+ <ToolPanelComponent/>
+ <ProjectSidebarContainer/>
+ <TopologySidebarContainer/>
+ </div>
+ )
+
+ const portfolioElements = (
+ <div className="full-height">
+ <ProjectSidebarContainer/>
+ <h2>Portfolio loading</h2>
+ </div>
+ )
+
+ const scenarioElements = (
+ <div className="full-height">
+ <ProjectSidebarContainer/>
+ <h2>Scenario loading</h2>
+ </div>
+ )
+
return (
<DocumentTitle
title={this.props.projectName ? this.props.projectName + ' - OpenDC' : 'Simulation - OpenDC'}
>
<div className="page-container full-height">
- <AppNavbarContainer fullWidth={true} />
- {this.props.topologyIsLoading ? (
- <div className="full-height d-flex align-items-center justify-content-center">
- <LoadingScreen/>
- </div>
- ) : (
- <div className="full-height">
- <MapStage/>
- <ScaleIndicatorContainer/>
- <ToolPanelComponent/>
- <ProjectSidebarContainer/>
- <TopologySidebarContainer/>
- </div>
+ <AppNavbarContainer fullWidth={true}/>
+ {this.props.scenarioId ? (
+ scenarioElements
+ ) : (this.props.portfolioId ? (
+ portfolioElements
+ ) : (
+ constructionElements
+ )
)}
<NewTopologyModal/>
+ <NewPortfolioModal/>
+ <NewScenarioModal/>
<EditRoomNameModal/>
<DeleteRoomModal/>
<EditRackNameModal/>
@@ -91,11 +123,9 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
- resetCurrentTopology: () => dispatch(resetCurrentTopology()),
- openProjectSucceeded: (id) => dispatch(openProjectSucceeded(id)),
- onViewTopologies: () => dispatch(openNewTopologyModal()),
- openExperimentSucceeded: (projectId, experimentId) =>
- dispatch(openExperimentSucceeded(projectId, experimentId)),
+ openProjectSucceeded: (projectId) => dispatch(openProjectSucceeded(projectId)),
+ openPortfolioSucceeded: (projectId, portfolioId) => dispatch(openPortfolioSucceeded(projectId, portfolioId)),
+ openScenarioSucceeded: (projectId, portfolioId, scenarioId) => dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId)),
}
}
diff --git a/frontend/src/reducers/construction-mode.js b/frontend/src/reducers/construction-mode.js
index b15ac834..257dddd2 100644
--- a/frontend/src/reducers/construction-mode.js
+++ b/frontend/src/reducers/construction-mode.js
@@ -1,5 +1,4 @@
import { combineReducers } from 'redux'
-import { OPEN_EXPERIMENT_SUCCEEDED } from '../actions/experiments'
import { GO_DOWN_ONE_INTERACTION_LEVEL } from '../actions/interaction-level'
import {
CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
@@ -10,6 +9,8 @@ import {
START_ROOM_EDIT,
} from '../actions/topology/building'
import { DELETE_ROOM, START_RACK_CONSTRUCTION, STOP_RACK_CONSTRUCTION } from '../actions/topology/room'
+import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios'
+import { OPEN_SCENARIO_SUCCEEDED } from '../actions/scenarios'
export function currentRoomInConstruction(state = '-1', action) {
switch (action.type) {
@@ -19,7 +20,8 @@ export function currentRoomInConstruction(state = '-1', action) {
return action.roomId
case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED:
case FINISH_NEW_ROOM_CONSTRUCTION:
- case OPEN_EXPERIMENT_SUCCEEDED:
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
case FINISH_ROOM_EDIT:
case SET_CURRENT_TOPOLOGY:
case DELETE_ROOM:
@@ -34,7 +36,8 @@ export function inRackConstructionMode(state = false, action) {
case START_RACK_CONSTRUCTION:
return true
case STOP_RACK_CONSTRUCTION:
- case OPEN_EXPERIMENT_SUCCEEDED:
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
case SET_CURRENT_TOPOLOGY:
case GO_DOWN_ONE_INTERACTION_LEVEL:
return false
diff --git a/frontend/src/reducers/current-ids.js b/frontend/src/reducers/current-ids.js
index 0726da6d..9b46aa60 100644
--- a/frontend/src/reducers/current-ids.js
+++ b/frontend/src/reducers/current-ids.js
@@ -1,13 +1,12 @@
-import { OPEN_EXPERIMENT_SUCCEEDED } from '../actions/experiments'
+import { OPEN_PORTFOLIO_SUCCEEDED, SET_CURRENT_PORTFOLIO } from '../actions/portfolios'
import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
-import { RESET_CURRENT_TOPOLOGY, SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
+import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
+import { OPEN_SCENARIO_SUCCEEDED, SET_CURRENT_SCENARIO } from '../actions/scenarios'
export function currentTopologyId(state = '-1', action) {
switch (action.type) {
case SET_CURRENT_TOPOLOGY:
return action.topologyId
- case RESET_CURRENT_TOPOLOGY:
- return '-1'
default:
return state
}
@@ -17,9 +16,39 @@ export function currentProjectId(state = '-1', action) {
switch (action.type) {
case OPEN_PROJECT_SUCCEEDED:
return action.id
- case OPEN_EXPERIMENT_SUCCEEDED:
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
return action.projectId
default:
return state
}
}
+
+export function currentPortfolioId(state = '-1', action) {
+ switch (action.type) {
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case SET_CURRENT_PORTFOLIO:
+ case SET_CURRENT_SCENARIO:
+ return action.portfolioId
+ case OPEN_SCENARIO_SUCCEEDED:
+ return action.portfolioId
+ case OPEN_PROJECT_SUCCEEDED:
+ case SET_CURRENT_TOPOLOGY:
+ return '-1'
+ default:
+ return state
+ }
+}
+export function currentScenarioId(state = '-1', action) {
+ switch (action.type) {
+ case OPEN_SCENARIO_SUCCEEDED:
+ case SET_CURRENT_SCENARIO:
+ return action.scenarioId
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case SET_CURRENT_TOPOLOGY:
+ case OPEN_PROJECT_SUCCEEDED:
+ return '-1'
+ default:
+ return state
+ }
+}
diff --git a/frontend/src/reducers/index.js b/frontend/src/reducers/index.js
index 6ca95ec6..787d5a74 100644
--- a/frontend/src/reducers/index.js
+++ b/frontend/src/reducers/index.js
@@ -1,7 +1,7 @@
import { combineReducers } from 'redux'
import { auth } from './auth'
import { construction } from './construction-mode'
-import { currentProjectId, currentTopologyId } from './current-ids'
+import { currentPortfolioId, currentProjectId, currentScenarioId, currentTopologyId } from './current-ids'
import { interactionLevel } from './interaction-level'
import { map } from './map'
import { modals } from './modals'
@@ -11,11 +11,13 @@ import { projectList } from './project-list'
const rootReducer = combineReducers({
objects,
modals,
- projectList: projectList,
+ projectList,
construction,
map,
currentProjectId,
currentTopologyId,
+ currentPortfolioId,
+ currentScenarioId,
interactionLevel,
auth,
})
diff --git a/frontend/src/reducers/interaction-level.js b/frontend/src/reducers/interaction-level.js
index 21aba715..eafcb269 100644
--- a/frontend/src/reducers/interaction-level.js
+++ b/frontend/src/reducers/interaction-level.js
@@ -1,4 +1,4 @@
-import { OPEN_EXPERIMENT_SUCCEEDED } from '../actions/experiments'
+import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios'
import {
GO_DOWN_ONE_INTERACTION_LEVEL,
GO_FROM_BUILDING_TO_ROOM,
@@ -7,10 +7,12 @@ import {
} from '../actions/interaction-level'
import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
+import { OPEN_SCENARIO_SUCCEEDED } from '../actions/scenarios'
export function interactionLevel(state = { mode: 'BUILDING' }, action) {
switch (action.type) {
- case OPEN_EXPERIMENT_SUCCEEDED:
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
case OPEN_PROJECT_SUCCEEDED:
case SET_CURRENT_TOPOLOGY:
return {
diff --git a/frontend/src/reducers/modals.js b/frontend/src/reducers/modals.js
index 77927cff..2fd71a5b 100644
--- a/frontend/src/reducers/modals.js
+++ b/frontend/src/reducers/modals.js
@@ -1,6 +1,4 @@
import { combineReducers } from 'redux'
-import { OPEN_EXPERIMENT_SUCCEEDED } from '../actions/experiments'
-import { CLOSE_NEW_EXPERIMENT_MODAL, OPEN_NEW_EXPERIMENT_MODAL } from '../actions/modals/experiments'
import { CLOSE_DELETE_PROFILE_MODAL, OPEN_DELETE_PROFILE_MODAL } from '../actions/modals/profile'
import { CLOSE_NEW_PROJECT_MODAL, OPEN_NEW_PROJECT_MODAL } from '../actions/modals/projects'
import {
@@ -17,6 +15,8 @@ import {
OPEN_EDIT_RACK_NAME_MODAL,
OPEN_EDIT_ROOM_NAME_MODAL,
} from '../actions/modals/topology'
+import { CLOSE_NEW_PORTFOLIO_MODAL, OPEN_NEW_PORTFOLIO_MODAL } from '../actions/modals/portfolios'
+import { CLOSE_NEW_SCENARIO_MODAL, OPEN_NEW_SCENARIO_MODAL } from '../actions/modals/scenarios'
function modal(openAction, closeAction) {
return function(state = false, action) {
@@ -24,7 +24,6 @@ function modal(openAction, closeAction) {
case openAction:
return true
case closeAction:
- case OPEN_EXPERIMENT_SUCCEEDED:
return false
default:
return state
@@ -41,5 +40,6 @@ export const modals = combineReducers({
editRackNameModalVisible: modal(OPEN_EDIT_RACK_NAME_MODAL, CLOSE_EDIT_RACK_NAME_MODAL),
deleteRackModalVisible: modal(OPEN_DELETE_RACK_MODAL, CLOSE_DELETE_RACK_MODAL),
deleteMachineModalVisible: modal(OPEN_DELETE_MACHINE_MODAL, CLOSE_DELETE_MACHINE_MODAL),
- newExperimentModalVisible: modal(OPEN_NEW_EXPERIMENT_MODAL, CLOSE_NEW_EXPERIMENT_MODAL),
+ newPortfolioModalVisible: modal(OPEN_NEW_PORTFOLIO_MODAL, CLOSE_NEW_PORTFOLIO_MODAL),
+ newScenarioModalVisible: modal(OPEN_NEW_SCENARIO_MODAL, CLOSE_NEW_SCENARIO_MODAL),
})
diff --git a/frontend/src/reducers/objects.js b/frontend/src/reducers/objects.js
index d25eb136..b4db0a6b 100644
--- a/frontend/src/reducers/objects.js
+++ b/frontend/src/reducers/objects.js
@@ -22,7 +22,8 @@ export const objects = combineReducers({
topology: object('topology'),
trace: object('trace'),
scheduler: object('scheduler'),
- experiment: object('experiment'),
+ portfolio: object('portfolio'),
+ scenario: object('scenario'),
})
function object(type, defaultState = {}) {
diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js
index d3f50be5..96f615b1 100644
--- a/frontend/src/routes/index.js
+++ b/frontend/src/routes/index.js
@@ -9,7 +9,13 @@ import Projects from '../pages/Projects'
const ProtectedComponent = (component) => () => (userIsLoggedIn() ? component : <Redirect to="/"/>)
const AppComponent = ({ match }) =>
- userIsLoggedIn() ? <App projectId={match.params.projectId}/> : <Redirect to="/"/>
+ userIsLoggedIn() ? (
+ <App
+ projectId={match.params.projectId}
+ portfolioId={match.params.portfolioId}
+ scenarioId={match.params.scenarioId}
+ />
+ ) : <Redirect to="/"/>
const Routes = () => (
<BrowserRouter>
@@ -17,6 +23,8 @@ const Routes = () => (
<Route exact path="/" component={Home}/>
<Route exact path="/projects" render={ProtectedComponent(<Projects/>)}/>
<Route exact path="/projects/:projectId" component={AppComponent}/>
+ <Route exact path="/projects/:projectId/portfolios/:portfolioId" component={AppComponent}/>
+ <Route exact path="/projects/:projectId/portfolios/:portfolioId/scenarios/:scenarioId" component={AppComponent}/>
<Route exact path="/profile" render={ProtectedComponent(<Profile/>)}/>
<Route path="/*" component={NotFound}/>
</Switch>
diff --git a/frontend/src/sagas/experiments.js b/frontend/src/sagas/experiments.js
deleted file mode 100644
index f2b23017..00000000
--- a/frontend/src/sagas/experiments.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { call, put, select } from 'redux-saga/effects'
-import { addPropToStoreObject, addToStore } from '../actions/objects'
-import { deleteExperiment, getExperiment } from '../api/routes/experiments'
-import { addExperiment, getProject } from '../api/routes/projects'
-import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
-import { fetchAndStoreAllTopologiesOfProject, fetchTopologyOfExperiment } from './topology'
-
-export function* onOpenExperimentSucceeded(action) {
- try {
- const project = yield call(getProject, action.projectId)
- yield put(addToStore('project', project))
-
- const experiment = yield call(getExperiment, action.experimentId)
- yield put(addToStore('experiment', experiment))
-
- yield fetchExperimentSpecifications()
-
- yield fetchTopologyOfExperiment(experiment)
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onFetchExperimentsOfProject() {
- try {
- const currentProjectId = yield select((state) => state.currentProjectId)
- const currentProject = yield select((state) => state.object.project[currentProjectId])
-
- yield fetchExperimentSpecifications()
-
- for (let i in currentProject.experimentIds) {
- const experiment = yield call(getExperiment, currentProject.experimentIds[i])
- yield put(addToStore('experiment', experiment))
- }
- } catch (error) {
- console.error(error)
- }
-}
-
-function* fetchExperimentSpecifications() {
- try {
- const currentProjectId = yield select((state) => state.currentProjectId)
- yield fetchAndStoreAllTopologiesOfProject(currentProjectId)
- yield fetchAndStoreAllTraces()
- yield fetchAndStoreAllSchedulers()
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onAddExperiment(action) {
- try {
- const currentProjectId = yield select((state) => state.currentProjectId)
-
- const experiment = yield call(
- addExperiment,
- currentProjectId,
- Object.assign({}, action.experiment, {
- id: '-1',
- projectId: currentProjectId,
- }),
- )
- yield put(addToStore('experiment', experiment))
-
- const experimentIds = yield select((state) => state.objects.project[currentProjectId].experimentIds)
- yield put(
- addPropToStoreObject('project', currentProjectId, {
- experimentIds: experimentIds.concat([experiment._id]),
- }),
- )
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onDeleteExperiment(action) {
- try {
- yield call(deleteExperiment, action.id)
-
- const currentProjectId = yield select((state) => state.currentProjectId)
- const experimentIds = yield select((state) => state.objects.project[currentProjectId].experimentIds)
-
- yield put(
- addPropToStoreObject('project', currentProjectId, {
- experimentIds: experimentIds.filter((id) => id !== action.id),
- }),
- )
- } catch (error) {
- console.error(error)
- }
-}
diff --git a/frontend/src/sagas/index.js b/frontend/src/sagas/index.js
index 26d19d58..daae8d8c 100644
--- a/frontend/src/sagas/index.js
+++ b/frontend/src/sagas/index.js
@@ -1,11 +1,10 @@
import { takeEvery } from 'redux-saga/effects'
import { LOG_IN } from '../actions/auth'
import {
- ADD_EXPERIMENT,
- DELETE_EXPERIMENT,
- FETCH_EXPERIMENTS_OF_PROJECT,
- OPEN_EXPERIMENT_SUCCEEDED,
-} from '../actions/experiments'
+ ADD_PORTFOLIO,
+ DELETE_PORTFOLIO,
+ OPEN_PORTFOLIO_SUCCEEDED, UPDATE_PORTFOLIO,
+} from '../actions/portfolios'
import { ADD_PROJECT, DELETE_PROJECT, OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
import {
ADD_TILE,
@@ -18,11 +17,11 @@ import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/ra
import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room'
import { DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER } from '../actions/users'
import {
- onAddExperiment,
- onDeleteExperiment,
- onFetchExperimentsOfProject,
- onOpenExperimentSucceeded,
-} from './experiments'
+ onAddPortfolio,
+ onDeletePortfolio,
+ onOpenPortfolioSucceeded,
+ onUpdatePortfolio,
+} from './portfolios'
import { onDeleteCurrentUser } from './profile'
import { onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects'
import {
@@ -44,6 +43,8 @@ import {
} from './topology'
import { onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser } from './users'
import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies'
+import { ADD_SCENARIO, DELETE_SCENARIO, OPEN_SCENARIO_SUCCEEDED, UPDATE_SCENARIO } from '../actions/scenarios'
+import { onAddScenario, onDeleteScenario, onOpenScenarioSucceeded, onUpdateScenario } from './scenarios'
export default function* rootSaga() {
yield takeEvery(LOG_IN, onFetchLoggedInUser)
@@ -55,7 +56,8 @@ export default function* rootSaga() {
yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser)
yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded)
- yield takeEvery(OPEN_EXPERIMENT_SUCCEEDED, onOpenExperimentSucceeded)
+ yield takeEvery(OPEN_PORTFOLIO_SUCCEEDED, onOpenPortfolioSucceeded)
+ yield takeEvery(OPEN_SCENARIO_SUCCEEDED, onOpenScenarioSucceeded)
yield takeEvery(ADD_TOPOLOGY, onAddTopology)
yield takeEvery(DELETE_TOPOLOGY, onDeleteTopology)
@@ -73,7 +75,11 @@ export default function* rootSaga() {
yield takeEvery(ADD_UNIT, onAddUnit)
yield takeEvery(DELETE_UNIT, onDeleteUnit)
- yield takeEvery(FETCH_EXPERIMENTS_OF_PROJECT, onFetchExperimentsOfProject)
- yield takeEvery(ADD_EXPERIMENT, onAddExperiment)
- yield takeEvery(DELETE_EXPERIMENT, onDeleteExperiment)
+ yield takeEvery(ADD_PORTFOLIO, onAddPortfolio)
+ yield takeEvery(UPDATE_PORTFOLIO, onUpdatePortfolio)
+ yield takeEvery(DELETE_PORTFOLIO, onDeletePortfolio)
+
+ yield takeEvery(ADD_SCENARIO, onAddScenario)
+ yield takeEvery(UPDATE_SCENARIO, onUpdateScenario)
+ yield takeEvery(DELETE_SCENARIO, onDeleteScenario)
}
diff --git a/frontend/src/sagas/objects.js b/frontend/src/sagas/objects.js
index 8a12bd13..17b28d02 100644
--- a/frontend/src/sagas/objects.js
+++ b/frontend/src/sagas/objects.js
@@ -11,6 +11,8 @@ export const OBJECT_SELECTORS = {
project: (state) => state.objects.project,
user: (state) => state.objects.user,
authorization: (state) => state.objects.authorization,
+ portfolio: (state) => state.objects.portfolio,
+ scenario: (state) => state.objects.scenario,
cpu: (state) => state.objects.cpu,
gpu: (state) => state.objects.gpu,
memory: (state) => state.objects.memory,
diff --git a/frontend/src/sagas/portfolios.js b/frontend/src/sagas/portfolios.js
new file mode 100644
index 00000000..cda1be9b
--- /dev/null
+++ b/frontend/src/sagas/portfolios.js
@@ -0,0 +1,108 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addPropToStoreObject, addToStore } from '../actions/objects'
+import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../api/routes/portfolios'
+import { getProject } from '../api/routes/projects'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { getScenario } from '../api/routes/scenarios'
+
+export function* onOpenPortfolioSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.projectId)
+ yield put(addToStore('project', project))
+ yield fetchAndStoreAllTopologiesOfProject(project._id)
+ yield fetchPortfoliosOfProject()
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+
+ // TODO Fetch portfolio-specific metrics
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* fetchPortfoliosOfProject() {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const currentProject = yield select((state) => state.objects.project[currentProjectId])
+
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+
+ for (let i in currentProject.portfolioIds) {
+ yield fetchPortfolioWithScenarios(currentProject.portfolioIds[i])
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* fetchPortfolioWithScenarios(portfolioId) {
+ try {
+ const portfolio = yield call(getPortfolio, portfolioId)
+ yield put(addToStore('portfolio', portfolio))
+
+ for (let i in portfolio.scenarioIds) {
+ const scenario = yield call(getScenario, portfolio.scenarioIds[i])
+ yield put(addToStore('scenario', scenario))
+ }
+ return portfolio
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddPortfolio(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+
+ const portfolio = yield call(
+ addPortfolio,
+ currentProjectId,
+ Object.assign({}, action.portfolio, {
+ projectId: currentProjectId,
+ scenarioIds: [],
+ }),
+ )
+ yield put(addToStore('portfolio', portfolio))
+
+ const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ portfolioIds: portfolioIds.concat([portfolio._id]),
+ }),
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onUpdatePortfolio(action) {
+ try {
+ const portfolio = yield call(
+ updatePortfolio,
+ action.portfolio._id,
+ action.portfolio,
+ )
+ yield put(addToStore('portfolio', portfolio))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeletePortfolio(action) {
+ try {
+ yield call(deletePortfolio, action.id)
+
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
+
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ portfolioIds: portfolioIds.filter((id) => id !== action.id),
+ }),
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/frontend/src/sagas/projects.js b/frontend/src/sagas/projects.js
index d1f5e7f7..fdeea132 100644
--- a/frontend/src/sagas/projects.js
+++ b/frontend/src/sagas/projects.js
@@ -3,13 +3,18 @@ import { addToStore } from '../actions/objects'
import { addProjectSucceeded, deleteProjectSucceeded } from '../actions/projects'
import { addProject, deleteProject, getProject } from '../api/routes/projects'
import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchPortfoliosOfProject } from './portfolios'
export function* onOpenProjectSucceeded(action) {
try {
const project = yield call(getProject, action.id)
yield put(addToStore('project', project))
- yield fetchAndStoreAllTopologiesOfProject(action.id)
+ yield fetchAndStoreAllTopologiesOfProject(action.id, true)
+ yield fetchPortfoliosOfProject()
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
} catch (error) {
console.error(error)
}
diff --git a/frontend/src/sagas/scenarios.js b/frontend/src/sagas/scenarios.js
new file mode 100644
index 00000000..48b1e9be
--- /dev/null
+++ b/frontend/src/sagas/scenarios.js
@@ -0,0 +1,72 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addPropToStoreObject, addToStore } from '../actions/objects'
+import { getProject } from '../api/routes/projects'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { addScenario, deleteScenario, updateScenario } from '../api/routes/scenarios'
+import { fetchPortfolioWithScenarios } from './portfolios'
+
+export function* onOpenScenarioSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.projectId)
+ yield put(addToStore('project', project))
+ yield fetchAndStoreAllTopologiesOfProject(project._id)
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+ yield fetchPortfolioWithScenarios(action.portfolioId)
+
+ // TODO Fetch scenario-specific metrics
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddScenario(action) {
+ try {
+ const scenario = yield call(
+ addScenario,
+ action.scenario.portfolioId,
+ action.scenario,
+ )
+ yield put(addToStore('scenario', scenario))
+
+ const scenarioIds = yield select((state) => state.objects.portfolio[action.scenario.portfolioId].scenarioIds)
+ yield put(
+ addPropToStoreObject('portfolio', action.scenario.portfolioId, {
+ scenarioIds: scenarioIds.concat([scenario._id]),
+ }),
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onUpdateScenario(action) {
+ try {
+ const scenario = yield call(
+ updateScenario,
+ action.scenario._id,
+ action.scenario,
+ )
+ yield put(addToStore('scenario', scenario))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteScenario(action) {
+ try {
+ yield call(deleteScenario, action.id)
+
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ const scenarioIds = yield select((state) => state.objects.project[currentPortfolioId].scenarioIds)
+
+ yield put(
+ addPropToStoreObject('scenario', currentPortfolioId, {
+ scenarioIds: scenarioIds.filter((id) => id !== action.id),
+ }),
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/frontend/src/sagas/topology.js b/frontend/src/sagas/topology.js
index 008c7b63..e915f9ff 100644
--- a/frontend/src/sagas/topology.js
+++ b/frontend/src/sagas/topology.js
@@ -20,16 +20,7 @@ import { fetchAndStoreTopology, updateTopologyOnServer } from './objects'
import { uuid } from 'uuidv4'
import { addTopology, deleteTopology } from '../api/routes/topologies'
-export function* fetchTopologyOfExperiment(experiment) {
- try {
- yield fetchAndStoreTopology(experiment.topologyId)
- yield put(setCurrentTopology(experiment.topologyId))
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* fetchAndStoreAllTopologiesOfProject(projectId) {
+export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) {
try {
const project = yield select((state) => state.objects.project[projectId])
@@ -37,7 +28,9 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId) {
yield fetchAndStoreTopology(project.topologyIds[i])
}
- yield put(setCurrentTopology(project.topologyIds[0]))
+ if (setTopology) {
+ yield put(setCurrentTopology(project.topologyIds[0]))
+ }
} catch (error) {
console.error(error)
}
diff --git a/frontend/src/shapes/index.js b/frontend/src/shapes/index.js
index b3889243..32914f25 100644
--- a/frontend/src/shapes/index.js
+++ b/frontend/src/shapes/index.js
@@ -17,7 +17,7 @@ Shapes.Project = PropTypes.shape({
datetimeCreated: PropTypes.string.isRequired,
datetimeLastEdited: PropTypes.string.isRequired,
topologyIds: PropTypes.array.isRequired,
- experimentIds: PropTypes.array.isRequired,
+ portfolioIds: PropTypes.array.isRequired,
})
Shapes.Authorization = PropTypes.shape({
@@ -96,16 +96,37 @@ Shapes.Trace = PropTypes.shape({
type: PropTypes.string.isRequired,
})
-Shapes.Experiment = PropTypes.shape({
+Shapes.Portfolio = PropTypes.shape({
_id: PropTypes.string.isRequired,
projectId: PropTypes.string.isRequired,
- topologyId: PropTypes.string.isRequired,
- topology: Shapes.Topology,
- traceId: PropTypes.string.isRequired,
- trace: Shapes.Trace,
- schedulerName: PropTypes.string.isRequired,
- scheduler: Shapes.Scheduler,
name: PropTypes.string.isRequired,
+ scenarioIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ targets: PropTypes.shape({
+ enabledMetrics: PropTypes.arrayOf(PropTypes.string).isRequired,
+ repeatsPerScenario: PropTypes.number.isRequired,
+ }).isRequired,
+})
+
+Shapes.Scenario = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ portfolioId: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ simulationState: PropTypes.string.isRequired,
+ trace: PropTypes.shape({
+ traceId: PropTypes.string.isRequired,
+ trace: Shapes.Trace,
+ loadSamplingFraction: PropTypes.number.isRequired,
+ }).isRequired,
+ topology: PropTypes.shape({
+ topologyId: PropTypes.string.isRequired,
+ topology: Shapes.Topology,
+ }).isRequired,
+ operational: PropTypes.shape({
+ failuresEnabled: PropTypes.bool.isRequired,
+ performanceInterferenceEnabled: PropTypes.bool.isRequired,
+ schedulerName: PropTypes.string.isRequired,
+ scheduler: Shapes.Scheduler,
+ }).isRequired,
})
Shapes.WallSegment = PropTypes.shape({
diff --git a/frontend/src/util/available-metrics.js b/frontend/src/util/available-metrics.js
new file mode 100644
index 00000000..c8035ddd
--- /dev/null
+++ b/frontend/src/util/available-metrics.js
@@ -0,0 +1,4 @@
+export const AVAILABLE_METRICS = [
+ 'granted-cpu',
+ 'overcommitted-cpu',
+]
diff --git a/frontend/src/util/state-utils.js b/frontend/src/util/state-utils.js
new file mode 100644
index 00000000..ba248c60
--- /dev/null
+++ b/frontend/src/util/state-utils.js
@@ -0,0 +1,5 @@
+export const getState = (dispatch) => new Promise((resolve) => {
+ dispatch((dispatch, getState) => {
+ resolve(getState())
+ })
+})