From 9086e9bb6b62c3b5c43370037482523f1ef95b78 Mon Sep 17 00:00:00 2001 From: Sandro Weber <webers@in.tum.de> Date: Wed, 10 Feb 2021 16:55:51 +0100 Subject: [PATCH] experiment rights managed by services --- .../experiment-list-element.js | 58 ++++++--------- .../experiment-list/experiment-list.js | 2 +- .../experiment-overview.js | 39 ++++++---- .../execution/experiment-execution-service.js | 2 +- ...rvice.js => running-simulation-service.js} | 0 .../experiments/experiment-constants.js | 17 ++++- .../files/experiment-storage-service.js | 41 ++++++++--- ...rvice.js => public-experiments-service.js} | 73 ++++++++++++++----- 8 files changed, 146 insertions(+), 86 deletions(-) rename src/services/experiments/execution/{simulation-service.js => running-simulation-service.js} (100%) rename src/services/experiments/files/{shared-experiments-service.js => public-experiments-service.js} (50%) diff --git a/src/components/experiment-list/experiment-list-element.js b/src/components/experiment-list/experiment-list-element.js index f6b3af6..d03645a 100644 --- a/src/components/experiment-list/experiment-list-element.js +++ b/src/components/experiment-list/experiment-list-element.js @@ -4,7 +4,7 @@ import { VscTriangleUp, VscTriangleDown } from 'react-icons/vsc'; import { GoFileSubmodule } from 'react-icons/go'; import timeDDHHMMSS from '../../utility/time-filter.js'; -import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service.js'; +//import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service.js'; import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js'; import SimulationDetails from './simulation-details'; @@ -24,23 +24,12 @@ export default class ExperimentListElement extends React.Component { showSimDetails: true }; - //TODO: put in service? - this.canLaunchExperiment = (this.props.experiment.private && this.props.experiment.owned) || - !this.props.experiment.private; this.launchButtonTitle = ''; this.wrapperRef = React.createRef(); } async componentDidMount() { - // retrieve the experiment thumbnail - let thumbnail = await ExperimentStorageService.instance.getThumbnail( - this.props.experiment.name, - this.props.experiment.configuration.thumbnail); - this.setState({ - thumbnail: URL.createObjectURL(thumbnail) - }); - this.handleClickOutside = this.handleClickOutside.bind(this); document.addEventListener('mousedown', this.handleClickOutside); } @@ -88,11 +77,11 @@ export default class ExperimentListElement extends React.Component { } isLaunchDisabled() { - let isDisabled = !this.canLaunchExperiment || + let isDisabled = !this.props.experiment.rights.launch || this.props.availableServers.length === 0 || this.props.startingExperiment === this.props.experiment; - if (!this.canLaunchExperiment) { + if (!this.props.experiment.rights.launch) { this.launchButtonTitle = 'Sorry, no permission to start experiment.'; } else if (this.props.availableServers.length === 0) { @@ -121,13 +110,13 @@ export default class ExperimentListElement extends React.Component { ref={this.wrapperRef}> <div className='list-entry-left' style={{ position: 'relative' }}> - <img className='entity-thumbnail' src={this.state.thumbnail} alt='' /> + <img className='entity-thumbnail' src={exp.thumbnailURL} alt='' /> </div> <div className='list-entry-middle flex-container up-down'> <div className='flex-container left-right title-line'> <div className='h4'> - {exp.configuration.name} + {config.name} </div> {exp.joinableServers.length > 0 ? <div className='exp-title-sim-info'> @@ -136,9 +125,9 @@ export default class ExperimentListElement extends React.Component { : null} </div> <div> - {!this.state.selected && exp.configuration.description.length > SHORT_DESCRIPTION_LENGTH ? - exp.configuration.description.substr(0, SHORT_DESCRIPTION_LENGTH) + ' ...' : - exp.configuration.description} + {!this.state.selected && config.description.length > SHORT_DESCRIPTION_LENGTH ? + config.description.substr(0, SHORT_DESCRIPTION_LENGTH) + ' ...' : + config.description} <br /> </div> @@ -146,12 +135,12 @@ export default class ExperimentListElement extends React.Component { <div className='experiment-details' > <i> Timeout: - {timeDDHHMMSS(exp.configuration.timeout)} - ({(exp.configuration.timeoutType === 'simulation' ? 'simulation' : 'real')} time) + {timeDDHHMMSS(config.timeout)} + ({(config.timeoutType === 'simulation' ? 'simulation' : 'real')} time) </i> <br /> <i> - Brain processes: {exp.configuration.brainProcesses} + Brain processes: {config.brainProcesses} </i> <br /> <div style={{ display: 'flex' }}> @@ -167,8 +156,7 @@ export default class ExperimentListElement extends React.Component { /*return exp.id === pageState.selected;*/ }}> <div className='btn-group' role='group' > - {this.canLaunchExperiment && - exp.configuration.experimentFile && exp.configuration.bibiConfSrc ? + {exp.rights.launch ? <button onClick={() => { ExperimentExecutionService.instance.startNewExperiment(exp, false); @@ -180,30 +168,27 @@ export default class ExperimentListElement extends React.Component { </button> : null} - {this.canLaunchExperiment && config.brainProcesses > 1 && - this.props.availableServers.length > 0 && - exp.configuration.experimentFile && exp.configuration.bibiConfSrc ? + {exp.rights.launch /*&& config.brainProcesses > 1*/ ? <button className='btn btn-default'> <FaPlay className='icon' />Launch in Single Process Mode </button> : null} - {this.canLaunchExperiment && this.props.availableServers.length > 1 && - exp.configuration.experimentFile && exp.configuration.bibiConfSrc ? + {exp.rights.launch /*&& this.props.availableServers.length > 1*/ ? <button className='btn btn-default' > <FaPlay className='icon' />Launch Multiple </button> : null} {/* isPrivateExperiment */} - {this.canLaunchExperiment ? + {exp.rights.delete ? <button className='btn btn-default'> <FaTrash className='icon' />Delete </button> : null} {/* Records button */} - {this.canLaunchExperiment ? + {exp.rights.launch ? <button className='btn btn-default'> {this.state.showRecordings ? <VscTriangleUp className='icon' /> : <VscTriangleDown className='icon' /> @@ -213,14 +198,14 @@ export default class ExperimentListElement extends React.Component { : null} {/* Export button */} - {this.canLaunchExperiment ? + {exp.rights.launch ? <button className='btn btn-default'> <FaFileExport className='icon' />Export </button> : null} {/* Simulations button */} - {this.canLaunchExperiment && exp.joinableServers.length > 0 ? + {exp.rights.launch && exp.joinableServers.length > 0 ? <button className='btn btn-default' onClick={() => { this.setState({ showSimDetails: !this.state.showSimDetails }); @@ -233,22 +218,21 @@ export default class ExperimentListElement extends React.Component { : null} {/* Clone button */} - {config.canCloneExperiments && (!exp.configuration.privateStorage || - (exp.configuration.experimentFile && exp.configuration.bibiConfSrc)) ? + {exp.rights.clone ? <button className='btn btn-default'> <FaClone className='icon' />Clone </button> : null} {/* Files button */} - {this.canLaunchExperiment ? + {exp.rights.launch ? <button className='btn btn-default' > <GoFileSubmodule className='icon' />Files </button> : null} {/* Shared button */} - {this.canLaunchExperiment ? + {exp.rights.launch ? <button className='btn btn-default'> <FaShareAlt className='icon' />Share </button> diff --git a/src/components/experiment-list/experiment-list.js b/src/components/experiment-list/experiment-list.js index 8ff9165..8a146aa 100644 --- a/src/components/experiment-list/experiment-list.js +++ b/src/components/experiment-list/experiment-list.js @@ -14,7 +14,7 @@ export default class ExperimentList extends React.Component { <ol> {this.props.experiments.map(experiment => { return ( - <li key={experiment.id} className='nostyle'> + <li key={experiment.id || experiment.configuration.id} className='nostyle'> <ExperimentListElement experiment={experiment} availableServers={this.props.availableServers} startingExperiment={this.props.startingExperiment} /> diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js index bc68bd8..34a4555 100644 --- a/src/components/experiment-overview/experiment-overview.js +++ b/src/components/experiment-overview/experiment-overview.js @@ -3,7 +3,7 @@ import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import 'react-tabs/style/react-tabs.css'; import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service.js'; -import SharedExperimentsService from '../../services/experiments/files/shared-experiments-service.js'; +import PublicExperimentsService from '../../services/experiments/files/public-experiments-service.js'; import ExperimentServerService from '../../services/experiments/execution/server-resources-service.js'; import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js'; @@ -17,7 +17,7 @@ export default class ExperimentOverview extends React.Component { super(props); this.state = { storageExperiments: [], - templateExperiments: [], + publicExperiments: [], joinableExperiments: [], availableServers: [], startingExperiment: undefined @@ -25,18 +25,6 @@ export default class ExperimentOverview extends React.Component { } async componentDidMount() { - try { - const storageExperiments = await ExperimentStorageService.instance.getExperiments(); - const templateExperiments = await SharedExperimentsService.instance.getExperiments(); - this.setState({ - storageExperiments: storageExperiments, - templateExperiments: templateExperiments - }); - } - catch (error) { - console.error(`Failed to fetch the list of experiments. Error: ${error}`); - } - this.onUpdateServerAvailability = this.onUpdateServerAvailability.bind(this); ExperimentServerService.instance.addListener( ExperimentServerService.EVENTS.UPDATE_SERVER_AVAILABILITY, @@ -49,11 +37,17 @@ export default class ExperimentOverview extends React.Component { this.onStartExperiment ); - this.onUpdateExperiments = this.onUpdateStorageExperiments.bind(this); + this.onUpdateStorageExperiments = this.onUpdateStorageExperiments.bind(this); ExperimentStorageService.instance.addListener( ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.onUpdateStorageExperiments ); + + this.onUpdatePublicExperiments = this.onUpdatePublicExperiments.bind(this); + PublicExperimentsService.instance.addListener( + PublicExperimentsService.EVENTS.UPDATE_EXPERIMENTS, + this.onUpdatePublicExperiments + ); } componentWillUnmount() { @@ -71,6 +65,11 @@ export default class ExperimentOverview extends React.Component { ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.onUpdateStorageExperiments ); + + PublicExperimentsService.instance.removeListener( + PublicExperimentsService.EVENTS.UPDATE_EXPERIMENTS, + this.onUpdatePublicExperiments + ); } onUpdateServerAvailability(availableServers) { @@ -82,6 +81,7 @@ export default class ExperimentOverview extends React.Component { }; onUpdateStorageExperiments(storageExperiments) { + //console.info(storageExperiments); let joinableExperiments = storageExperiments.filter( experiment => experiment.joinableServers && experiment.joinableServers.length > 0); this.setState({ @@ -90,6 +90,13 @@ export default class ExperimentOverview extends React.Component { }); } + onUpdatePublicExperiments(publicExperiments) { + //console.info(publicExperiments); + this.setState({ + publicExperiments: publicExperiments.filter(exp => exp.configuration.maturity === 'production') + }); + } + render() { return ( <div className='experiment-overview-wrapper'> @@ -122,7 +129,7 @@ export default class ExperimentOverview extends React.Component { <h2>"Experiment Files" tab coming soon ...</h2> </TabPanel> <TabPanel> - <ExperimentList experiments={this.state.templateExperiments} + <ExperimentList experiments={this.state.publicExperiments} availableServers={this.state.availableServers} startingExperiment={this.state.startingExperiment} /> </TabPanel> diff --git a/src/services/experiments/execution/experiment-execution-service.js b/src/services/experiments/execution/experiment-execution-service.js index 37ff851..924c608 100644 --- a/src/services/experiments/execution/experiment-execution-service.js +++ b/src/services/experiments/execution/experiment-execution-service.js @@ -2,7 +2,7 @@ import _ from 'lodash'; //import NrpAnalyticsService from '../../nrp-analytics-service.js'; import ServerResourcesService from './server-resources-service.js'; -import SimulationService from './simulation-service.js'; +import SimulationService from './running-simulation-service.js'; import { HttpService } from '../../http-service.js'; import { EXPERIMENT_STATE } from '../experiment-constants.js'; diff --git a/src/services/experiments/execution/simulation-service.js b/src/services/experiments/execution/running-simulation-service.js similarity index 100% rename from src/services/experiments/execution/simulation-service.js rename to src/services/experiments/execution/running-simulation-service.js diff --git a/src/services/experiments/experiment-constants.js b/src/services/experiments/experiment-constants.js index b09c414..078d95c 100644 --- a/src/services/experiments/experiment-constants.js +++ b/src/services/experiments/experiment-constants.js @@ -8,4 +8,19 @@ const EXPERIMENT_STATE = { STOPPED: 'stopped' }; -module.exports = {EXPERIMENT_STATE}; \ No newline at end of file +const EXPERIMENT_RIGHTS = { + PUBLICLY_SHARED: { + launch: false, + delete: false, + clone: true, + share: false + }, + OWNED: { + launch: true, + delete: true, + clone: true, + share: true + } +}; + +module.exports = { EXPERIMENT_STATE, EXPERIMENT_RIGHTS }; \ No newline at end of file diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js index 3097dcb..d140384 100644 --- a/src/services/experiments/files/experiment-storage-service.js +++ b/src/services/experiments/files/experiment-storage-service.js @@ -1,4 +1,5 @@ import { HttpService } from '../../http-service.js'; +import { EXPERIMENT_RIGHTS } from '../experiment-constants'; import endpoints from '../../proxy/data/endpoints.json'; import config from '../../../config.json'; @@ -61,12 +62,14 @@ class ExperimentStorageService extends HttpService { * @param {boolean} forceUpdate forces an update of the list * @return experiments - the list of template experiments */ + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? async getExperiments(forceUpdate = false) { if (!this.experiments || forceUpdate) { - let response = await this.httpRequestGET(storageExperimentsURL); - this.experiments = await response.json(); - this.sortExperiments(); - await this.fillExperimentDetails(); + let experimentList = await (await this.httpRequestGET(storageExperimentsURL)).json(); + this.sortExperiments(experimentList); + await this.fillExperimentDetails(experimentList); + this.experiments = experimentList; this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); } @@ -80,6 +83,8 @@ class ExperimentStorageService extends HttpService { * * @returns {Blob} image object */ + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? async getThumbnail(experimentName, thumbnailFilename) { return await this.getBlob(experimentName, thumbnailFilename, true); } @@ -87,8 +92,10 @@ class ExperimentStorageService extends HttpService { /** * Sort the local list of experiments alphabetically. */ - sortExperiments() { - this.experiments = this.experiments.sort( + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? + sortExperiments(experimentList) { + experimentList = experimentList.sort( (a, b) => { let nameA = a.configuration.name.toLowerCase(); let nameB = b.configuration.name.toLowerCase(); @@ -106,12 +113,26 @@ class ExperimentStorageService extends HttpService { /** * Fill in some details for the local experiment list that might be missing. */ - async fillExperimentDetails() { - this.experiments.forEach(exp => { - if (!exp.configuration.brainProcesses && exp.configuration.bibiConfSrc) { - exp.configuration.brainProcesses = 1; + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? + async fillExperimentDetails(experimentList) { + let experimentUpdates = []; + experimentList.forEach(experiment => { + if (!experiment.configuration.brainProcesses && experiment.configuration.bibiConfSrc) { + experiment.configuration.brainProcesses = 1; } + + // retrieve the experiment thumbnail + experimentUpdates.push(this.getThumbnail(experiment.name, experiment.configuration.thumbnail) + .then(thumbnail => { + experiment.thumbnailURL = URL.createObjectURL(thumbnail); + })); + + experiment.rights = EXPERIMENT_RIGHTS.OWNED; + experiment.rights.launch = (experiment.private && experiment.owned) || !experiment.private; }); + + return Promise.all(experimentUpdates); } /** diff --git a/src/services/experiments/files/shared-experiments-service.js b/src/services/experiments/files/public-experiments-service.js similarity index 50% rename from src/services/experiments/files/shared-experiments-service.js rename to src/services/experiments/files/public-experiments-service.js index d668ef1..306c22f 100644 --- a/src/services/experiments/files/shared-experiments-service.js +++ b/src/services/experiments/files/public-experiments-service.js @@ -1,8 +1,10 @@ import { HttpService } from '../../http-service.js'; +import { EXPERIMENT_RIGHTS } from '../experiment-constants'; import endpoints from '../../proxy/data/endpoints.json'; import config from '../../../config.json'; const experimentsURL = `${config.api.proxy.url}${endpoints.proxy.experiments.url}`; +const experimentImageURL = `${config.api.proxy.url}${endpoints.proxy.experimentImage.url}`; const cloneURL = `${config.api.proxy.url}${endpoints.proxy.storage.clone.url}`; let _instance = null; @@ -12,7 +14,7 @@ const SINGLETON_ENFORCER = Symbol(); * Service that handles storage experiment files and configurations given * that the user has authenticated successfully. */ -class SharedExperimentsService extends HttpService { +class PublicExperimentsService extends HttpService { constructor(enforcer) { super(); if (enforcer !== SINGLETON_ENFORCER) { @@ -28,7 +30,7 @@ class SharedExperimentsService extends HttpService { static get instance() { if (_instance == null) { - _instance = new SharedExperimentsService(SINGLETON_ENFORCER); + _instance = new PublicExperimentsService(SINGLETON_ENFORCER); } return _instance; @@ -43,7 +45,7 @@ class SharedExperimentsService extends HttpService { () => { this.getExperiments(true); }, - SharedExperimentsService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS + PublicExperimentsService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS ); } @@ -62,14 +64,15 @@ class SharedExperimentsService extends HttpService { * @param {boolean} forceUpdate forces an update of the list * @return experiments - the list of template experiments */ + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? async getExperiments(forceUpdate = false) { if (!this.experiments || forceUpdate) { - let response = await (await this.httpRequestGET(experimentsURL)).json(); - this.experiments = response.values(); - console.info(this.experiments); - this.sortExperiments(); - //await this.fillExperimentDetails(); - this.emit(SharedExperimentsService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); + let experimentList = Object.values(await (await this.httpRequestGET(experimentsURL)).json()); + this.sortExperiments(experimentList); + await this.fillExperimentDetails(experimentList); + this.experiments = experimentList; + this.emit(PublicExperimentsService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); } return this.experiments; @@ -78,8 +81,10 @@ class SharedExperimentsService extends HttpService { /** * Sort the local list of experiments alphabetically. */ - sortExperiments() { - this.experiments = this.experiments.sort( + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? + sortExperiments(experimentList) { + experimentList = experimentList.sort( (a, b) => { let nameA = a.configuration.name.toLowerCase(); let nameB = b.configuration.name.toLowerCase(); @@ -97,12 +102,40 @@ class SharedExperimentsService extends HttpService { /** * Fill in some details for the local experiment list that might be missing. */ - async fillExperimentDetails() { - this.experiments.forEach(exp => { - if (!exp.configuration.brainProcesses && exp.configuration.bibiConfSrc) { - exp.configuration.brainProcesses = 1; + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? + async fillExperimentDetails(experimentList) { + let experimentUpdates = []; + experimentList.forEach(experiment => { + if (!experiment.configuration.brainProcesses && experiment.configuration.bibiConfSrc) { + experiment.configuration.brainProcesses = 1; } + + // retrieve the experiment thumbnail + experimentUpdates.push(this.getThumbnailURL(experiment.configuration.id).then(thumbnailURL => { + if (thumbnailURL) { + experiment.thumbnailURL = thumbnailURL; //URL.createObjectURL(thumbnail); + } + })); + + experiment.rights = EXPERIMENT_RIGHTS.PUBLICLY_SHARED; }); + + return Promise.all(experimentUpdates); + } + + /** + * Retrieves the thumbnail image for a given experiment. + * @param {string} experimentName - name of the experiment + * @param {string} thumbnailFilename - name of the thumbnail file + * + * @returns {Blob} image object + */ + //TODO: between storage experiments and shared experiments, can this be unified? + // move to experiment-configuration-service? + async getThumbnailURL(experimentName) { + let url = experimentImageURL + '/' + experimentName; + return url; } /** @@ -110,18 +143,18 @@ class SharedExperimentsService extends HttpService { * @param {Object} experiment The Experiment configuration */ async cloneExperiment(experiment) { - let expPath = experiment.configuration.experimentConfiguration; - let response = await this.httpRequestPOST(cloneURL, { expPath }); + let experimentConfigFilepath = experiment.configuration.experimentConfiguration; + let response = await this.httpRequestPOST(cloneURL, { expPath: experimentConfigFilepath }); console.info(response); } } -SharedExperimentsService.EVENTS = Object.freeze({ +PublicExperimentsService.EVENTS = Object.freeze({ UPDATE_EXPERIMENTS: 'UPDATE_EXPERIMENTS' }); -SharedExperimentsService.CONSTANTS = Object.freeze({ +PublicExperimentsService.CONSTANTS = Object.freeze({ INTERVAL_POLL_EXPERIMENTS: 5000 }); -export default SharedExperimentsService; +export default PublicExperimentsService; -- GitLab