diff options
| author | jc0b <j@jc0b.computer> | 2020-07-10 15:18:49 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2020-08-24 19:48:02 +0200 |
| commit | d8479e7e3744b8d1d31ac4d9f972e560eacd2cf8 (patch) | |
| tree | 79b7dfccec6e3cc1fce189b4605a37b354d676a2 | |
| parent | 4befa57993831274ad7e6ca62f96aa582f81cc5d (diff) | |
| parent | 3b4e27320c479bd6ef48998f448ed070e8bd7511 (diff) | |
Merge branch 'master' of github.com:atlarge-research/opendc-dev
56 files changed, 1057 insertions, 544 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()) + }) +}) diff --git a/opendc-api-spec.yml b/opendc-api-spec.yml index aa78a84a..39cb4f1c 100644 --- a/opendc-api-spec.yml +++ b/opendc-api-spec.yml @@ -842,8 +842,6 @@ definitions: type: array items: type: string - baseScenarioId: - type: string targets: type: object properties: diff --git a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py index ab32aae2..1c5e0ab6 100644 --- a/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py +++ b/web-server/opendc/api/v2/portfolios/portfolioId/scenarios/endpoint.py @@ -1,4 +1,5 @@ from opendc.models.portfolio import Portfolio +from opendc.models.scenario import Scenario from opendc.util.rest import Response @@ -29,14 +30,14 @@ def POST(request): portfolio.check_exists() portfolio.check_user_access(request.google_id, True) - scenario = Portfolio(request.params_body['scenario']) + scenario = Scenario(request.params_body['scenario']) scenario.set_property('portfolioId', request.params_path['portfolioId']) scenario.set_property('simulationState', 'QUEUED') scenario.insert() - portfolio.obj['portfolioIds'].append(portfolio.get_id()) + portfolio.obj['scenarioIds'].append(scenario.get_id()) portfolio.update() - return Response(200, 'Successfully added Portfolio.', portfolio.obj) + return Response(200, 'Successfully added Scenario.', scenario.obj) diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py index c51dee14..0bc65565 100644 --- a/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/endpoint.py @@ -26,7 +26,6 @@ def POST(request): portfolio.set_property('projectId', request.params_path['projectId']) portfolio.set_property('scenarioIds', []) - portfolio.set_property('baseScenarioId', '-1') portfolio.insert() diff --git a/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py index 5b4d9043..24416cc3 100644 --- a/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py +++ b/web-server/opendc/api/v2/projects/projectId/portfolios/test_endpoint.py @@ -65,7 +65,6 @@ def test_add_portfolio(client, mocker): }, 'projectId': '1', 'scenarioIds': [], - 'baseScenarioId': '-1', }) mocker.patch.object(DB, 'update', return_value=None) res = client.post( @@ -81,5 +80,4 @@ def test_add_portfolio(client, mocker): }) assert 'projectId' in res.json['content'] assert 'scenarioIds' in res.json['content'] - assert 'baseScenarioId' in res.json['content'] assert '200' in res.status diff --git a/web-server/opendc/models/scenario.py b/web-server/opendc/models/scenario.py index d7d959ca..8d53e408 100644 --- a/web-server/opendc/models/scenario.py +++ b/web-server/opendc/models/scenario.py @@ -21,6 +21,6 @@ class Scenario(Model): portfolio = Portfolio.from_id(self.obj['portfolioId']) user = User.from_google_id(google_id) authorizations = list( - filter(lambda x: str(x['projectId']) == str(portfolio.get_id()), user.obj['authorizations'])) + filter(lambda x: str(x['projectId']) == str(portfolio.obj['projectId']), user.obj['authorizations'])) if len(authorizations) == 0 or (edit_access and authorizations[0]['authorizationLevel'] == 'VIEW'): raise ClientError(Response(403, 'Forbidden from retrieving/editing scenario.')) diff --git a/web-server/opendc/util/parameter_checker.py b/web-server/opendc/util/parameter_checker.py index d1256009..14dd1dc0 100644 --- a/web-server/opendc/util/parameter_checker.py +++ b/web-server/opendc/util/parameter_checker.py @@ -48,7 +48,7 @@ def _incorrect_parameter(params_required, params_actual, parent=''): type_pairs = [ ('int', (int,)), - ('float', (float,)), + ('float', (float, int)), ('bool', (bool,)), ('string', (str, int)), ('list', (list,)), |
