diff --git a/package-lock.json b/package-lock.json index 840dbe9..fbb7f6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1179,6 +1179,14 @@ "minimist": "^1.2.0" } }, + "@createnl/grouped-checkboxes": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@createnl/grouped-checkboxes/-/grouped-checkboxes-1.1.2.tgz", + "integrity": "sha512-F4SoFF7UwktrpVeJPs/cQFP5Nn+1wpUm5v1/vzUcgseDGxvjXjLrs+F99ivH/TA3VDJbeFdBPi7gymmw2cessQ==", + "requires": { + "lodash.debounce": "^4.0.8" + } + }, "@csstools/convert-colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", diff --git a/package.json b/package.json index 2910036..af5f894 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-solid-svg-icons": "^5.15.1", + "@createnl/grouped-checkboxes": "^1.1.2", "@fortawesome/react-fontawesome": "^0.1.13", "babel-runtime": "^6.26.0", "bootstrap": "^4.5.3", diff --git a/src/common/data-managers/notifications-factory.js b/src/common/data-managers/notifications-factory.js index ece1e8b..a032db0 100644 --- a/src/common/data-managers/notifications-factory.js +++ b/src/common/data-managers/notifications-factory.js @@ -138,6 +138,14 @@ class NotificationsFactory { }; } + static ACTION_INFO() { + return { + title: 'Action successfully requested', + level: 'info' + }; + } + + } export default NotificationsFactory; diff --git a/src/common/dialogs/delete-dialog.js b/src/common/dialogs/delete-dialog.js index 50d85a7..a4499e5 100644 --- a/src/common/dialogs/delete-dialog.js +++ b/src/common/dialogs/delete-dialog.js @@ -42,7 +42,9 @@ class DeleteDialog extends React.Component { - + + + ; diff --git a/src/common/dialogs/dialog.js b/src/common/dialogs/dialog.js index 4cc15af..9c5c2ce 100644 --- a/src/common/dialogs/dialog.js +++ b/src/common/dialogs/dialog.js @@ -45,6 +45,11 @@ class Dialog extends React.Component { } render() { + + const buttonStyle = { + marginLeft: '10px' + }; + return ( @@ -56,8 +61,10 @@ class Dialog extends React.Component { - {this.props.blendOutCancel?
: } - + + {this.props.blendOutCancel?
: } + + ); diff --git a/src/common/home.js b/src/common/home.js index 0fc874d..5932022 100644 --- a/src/common/home.js +++ b/src/common/home.js @@ -43,17 +43,15 @@ class Home extends React.Component { Logo VILLASweb

Home

- Welcome to {config.instance} hosted by {config.admin.name}!
+ {/*Welcome to {config.instance} hosted by {config.admin.name}!
*/} + Welcome to {config.instance}!

You are logged in as user {currentUser.username} with ID {currentUser.id} and role {currentUser.role}.

-

- An interactive documentation of the VILLASweb API is available here. -

-

Data Model

+ {/*

Data Model

Datamodel VILLASweb

Terminology

@@ -82,32 +80,47 @@ class Home extends React.Component {
  • A collection of component configurations and dashboards for a specific experiment
  • Users can have access to multiple scenarios
  • -
+ */}

Credits

-

VILLASweb is developed by the Institute for Automation of Complex Power Systems at the RWTH Aachen University.

+

VILLASweb is an open source project developed by the Institute for Automation of Complex Power Systems at RWTH Aachen University.

+ Logo ACS

Links

Funding

-

The development of VILLASframework projects have received funding from

+

The development of VILLASframework projects has received funding from

    -
  • Urban Energy Lab 4.0 a project funded by OP EFRE NRW (European Regional Development Fund) for the setup of a novel energy research infrastructure.
  • -
  • RESERVE a European Union’s Horizon 2020 research and innovation programme under grant agreement No 727481
  • -
  • JARA-ENERGY. Jülich-Aachen Research Alliance (JARA) is an initiative of RWTH Aachen University and Forschungszentrum Jülich.
  • +

    SLEW: Second Life for Energiewende, an Exploratory Teaching Space project funded by RWTH Aachen University

    +

    ERIgrid 2.0: An EU Horizon 2020 research and innovation action project for connecting European Smart Grid Infrastructures (grant agreement No 870620)

    +

    + Logo EU + Logo ERIgrid 2.0 +

    +

    Urban Energy Lab 4.0: A project funded by EFRE.NRW (European Regional Development Fund) for the setup of a novel energy research infrastructure.

    +

    + Logo UEL OP EFRE NRW + Logo UEL +

    +

    RESERVE: An EU Horizon 2020 research and innovation project (grant agreement No 727481)

    +

    + Logo EU + Logo RESERVE +

    +

    JARA-ENERGY: Jülich-Aachen Research Alliance (JARA) is an initiative of RWTH Aachen University and Forschungszentrum Jülich.

- Logo EU - Logo EU - Logo UEL OP EFRE NRW - Logo UEL - Logo ACS + + + { //Logo JARA } diff --git a/src/common/icon.js b/src/common/icon.js index 1ad1701..25155b4 100644 --- a/src/common/icon.js +++ b/src/common/icon.js @@ -28,7 +28,7 @@ library.add(fas); class Icon extends React.Component { render() { - return + return } } diff --git a/src/common/table-column.js b/src/common/table-column.js index 9d77606..460eb4b 100644 --- a/src/common/table-column.js +++ b/src/common/table-column.js @@ -23,7 +23,9 @@ class TableColumn extends Component { modifier: null, width: null, editButton: false, + showEditButton: null, deleteButton: false, + showDeleteButton: null, exportButton: false, duplicateButton: false, link: '/', diff --git a/src/common/table.js b/src/common/table.js index 400bffe..f54335e 100644 --- a/src/common/table.js +++ b/src/common/table.js @@ -34,7 +34,8 @@ class CustomTable extends Component { } static defaultProps = { - width: null + width: null, + checked: true }; onClick(event, row, column) { @@ -78,9 +79,19 @@ class CustomTable extends Component { cell.push(); } else if (linkKey === 'filebuttons') { content.forEach((contentvalue, contentkey) => { - cell.push(Download {contentvalue}} > - ); + cell.push( + Download {contentvalue}} > + + ); }); } else { cell.push(content); @@ -113,17 +124,34 @@ class CustomTable extends Component { } // add buttons - if (child.props.editButton) { - let disable = (typeof data.managedexternally !== "undefined" && data.managedexternally); - cell.push({disable ? "Externally managed ICs cannot be edited" : "edit"} } > - ); + let showEditButton = true + if (child.props.showEditButton !== null) + { + showEditButton = child.props.showEditButton(index) } + if(showEditButton){ + if (child.props.editButton) { + cell.push( + Edit }> + + ); + } + } + if (child.props.checkbox) { const checkboxKey = child.props.checkboxKey; let isDisabled = false; if (child.props.checkboxDisabled != null){ - isDisabled = !child.props.checkboxDisabled(index) + isDisabled = child.props.checkboxDisabled(index) } cell.push( Export } > - ); + cell.push( + Export } > + + ); } if (child.props.duplicateButton) { - cell.push( Duplicate } > - ); + cell.push( + Duplicate } > + + ); } if (child.props.addRemoveFilesButton) { - cell.push(Add/remove File(s)} > - ); + cell.push( + Add/remove File(s)} > + + ); } if (child.props.downloadAllButton) { - cell.push(Download All Files} > - ); + cell.push( + Download All Files} > + + ); } - if (child.props.deleteButton) { - cell.push( Delete } > - ); + let showDeleteButton = true; + if (child.props.showDeleteButton !== null){ + showDeleteButton = child.props.showDeleteButton(index) } + if (showDeleteButton){ + if (child.props.deleteButton) { + cell.push( + Delete } > + + ); + } + } + + + return cell; } // addCell @@ -243,9 +330,19 @@ class CustomTable extends Component { onCellBlur: () => { } }; - return ( + return ( {(this.state.editCell[0] === cellIndex && this.state.editCell[1] === rowIndex) ? ( - children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} ref={ref => { this.activeInput = ref; }} /> + children[cellIndex].props.onInlineChange(event, rowIndex, cellIndex)} + ref={ref => { this.activeInput = ref; }} /> ) : ( {cell.map((element, elementIndex) => ( diff --git a/src/dashboard/dashboard-button-group.js b/src/dashboard/dashboard-button-group.js index b2df4de..20a8994 100644 --- a/src/dashboard/dashboard-button-group.js +++ b/src/dashboard/dashboard-button-group.js @@ -24,13 +24,12 @@ class DashboardButtonGroup extends React.Component { render() { const buttonStyle = { marginLeft: '12px', - height: '44px', - width : '35px' + height: '44px', + width : '35px', }; const iconStyle = { - color: '#007bff', - height: '25px', + height: '25px', width : '25px' } @@ -45,21 +44,22 @@ class DashboardButtonGroup extends React.Component { buttons.push( Save changes } > , Discard changes } > ); } else { if (this.props.fullscreen !== true) { buttons.push( + Change to fullscreen view } > ); @@ -67,7 +67,7 @@ class DashboardButtonGroup extends React.Component { buttons.push( Back to normal view } > ); @@ -77,7 +77,7 @@ class DashboardButtonGroup extends React.Component { buttons.push( Continue simulation } > ); @@ -85,7 +85,7 @@ class DashboardButtonGroup extends React.Component { buttons.push( Pause simulation } > ); @@ -96,16 +96,15 @@ class DashboardButtonGroup extends React.Component { Add, edit or delete files of scenario }> ); buttons.push( - Add, edit or delete input signals }> + Add, edit or delete input signals }> ); @@ -114,7 +113,7 @@ class DashboardButtonGroup extends React.Component { Add, edit or delete output signals }> ); @@ -123,7 +122,7 @@ class DashboardButtonGroup extends React.Component { Add widgets and edit layout }> ); diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index 56b9880..b8a627b 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -478,7 +478,7 @@ class Dashboard extends Component { return
- {this.state.dashboard.name} +

{this.state.dashboard.name}

this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}>
- + Name this.handleChange(e)} /> diff --git a/src/dashboard/new-dashboard.js b/src/dashboard/new-dashboard.js index 6f4b9da..a023ba4 100644 --- a/src/dashboard/new-dashboard.js +++ b/src/dashboard/new-dashboard.js @@ -69,7 +69,7 @@ class NewDashboardDialog extends React.Component { return ( this.onClose(c)} onReset={() => this.resetState()} valid={this.valid}> - + Name this.handleChange(e)} /> diff --git a/src/file/edit-files.js b/src/file/edit-files.js index af5d251..d15102b 100644 --- a/src/file/edit-files.js +++ b/src/file/edit-files.js @@ -149,11 +149,14 @@ class EditFilesDialog extends React.Component { + + diff --git a/src/ic/ic-action.js b/src/ic/ic-action.js index 93634e5..2d08af4 100644 --- a/src/ic/ic-action.js +++ b/src/ic/ic-action.js @@ -16,7 +16,10 @@ ******************************************************************************/ import React from 'react'; -import { Button, ButtonToolbar, DropdownButton, Dropdown, InputGroup, FormControl } from 'react-bootstrap'; +import { Button, DropdownButton, Dropdown, InputGroup, FormControl } from 'react-bootstrap'; +import AppDispatcher from "../common/app-dispatcher"; +import NotificationsFactory from "../common/data-managers/notifications-factory"; +import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; class ICAction extends React.Component { constructor(props) { @@ -47,9 +50,187 @@ class ICAction extends React.Component { }; } } + return null } + runAction(action, when) { + + if (action.data.action === 'none') { + console.warn("No command selected. Nothing was sent."); + return; + } + + if (!this.props.hasConfigs){ + let newAction = {}; + newAction["action"] = action.data.action + newAction["when"] = when + + for (let index of this.props.selectedICs) { + let ic = this.props.ics[index]; + let icID = ic.id; + + /* VILLAScontroller protocol + see: https://villas.fein-aachen.org/doc/controller-protocol.html + + RESET SHUTDOWN + { + "action": "reset/shutdown/stop/pause/resume" + "when": "1234567" + } + + DELETE + { + "action": "delete" + "parameters":{ + "uuid": "uuid-of-the-manager-for-this-IC" + } + "when": "1234567" + } + + CREATE is not possible within ICAction (see add IC) + */ + + if (newAction.action === "delete"){ + // prepare parameters for delete incl. correct IC id + newAction["parameters"] = {}; + newAction.parameters["uuid"] = ic.uuid; + // get the ID of the manager IC + let managerIC = null; + for (let i of this.props.ics){ + if (i.uuid === ic.manager){ + managerIC = i; + } + } + if (managerIC == null){ + NotificationsDataManager.addNotification(NotificationsFactory.DELETE_ERROR("Could not find manager IC with UUID " + ic.manager)); + continue; + } + + icID = managerIC.id; // send delete action to manager of IC + } + + AppDispatcher.dispatch({ + type: 'ics/start-action', + icid: icID, + action: newAction, + result: null, + token: this.props.token + }); + + } // end for loop over selected ICs + } else { + + /*VILLAScontoller protocol + see: https://villas.fein-aachen.org/doc/controller-protocol.html + * + * STOP PAUSE RESUME + { + "action": "reset/shutdown/stop/pause/resume" + "when": "1234567" + } + * + * START + { + "action": "start" + "when": 1234567 + "parameters": { + Start parameters for this IC as configured in the component config + } + "model": { + "type": "url" + "url": "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/files/{fileID}" where fileID is the model file configured in the component config + "token": "asessiontoken" + } + "results":{ + "type": "url" + "url" : "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/results/{resultID}/file" where resultID is the ID of the result created for this run + "token": "asessiontoken" + } + } + * + * + * */ + + + let newActions = []; + for (let config of this.props.selectedConfigs) { + let newAction = {} + newAction["action"] = action.data.action + newAction["when"] = when + + // get IC for component config + let ic = null; + for (let component of this.props.ics) { + if (component.id === config.icID) { + ic = component; + } + } + + if (ic == null) { + continue; + } + + // the following is not required by the protocol; it is an internal help + newAction["icid"] = ic.id + + if (newAction.action === 'start') { + newAction["parameters"] = config.startParameters; + + + if (config.fileIDs.length > 0){ + newAction["model"] = {} + newAction.model["type"] = "url" + newAction.model["token"] = this.props.token + + let fileURLs = [] + for (let fileID of config.fileIDs){ + fileURLs.push("/files/" + fileID.toString()) + } + newAction.model["url"] = fileURLs + } + + newAction["results"] = {} + newAction.results["type"] = "url" + newAction.results["token"] = this.props.token + newAction.results["url"] = "/results/RESULTID/file" // RESULTID serves as placeholder and is replaced later + + } + + // add the new action + newActions.push(newAction); + + } // end for loop over selected configs + + + let newResult = {} + newResult["result"] = {} + if (action.data.action === 'start') { + + let configSnapshots = []; + // create config snapshots in case action is start + for (let config of this.props.selectedConfigs) { + let index = this.props.configs.indexOf(config) + configSnapshots.push(this.props.snapshotConfig(index)); + } + + // create new result for new run + newResult.result["description"] = "Start at " + when; + newResult.result["scenarioID"] = this.props.selectedConfigs[0].scenarioID + newResult.result["configSnapshots"] = configSnapshots + } + + + console.log("Dispatching actions for configs", newActions, newResult) + AppDispatcher.dispatch({ + type: 'ics/start-action', + action: newActions, + result: newResult, + token: this.props.token + }); + } + } + setAction = id => { // search action for (let action of this.props.actions) { @@ -65,7 +246,13 @@ class ICAction extends React.Component { render() { - let sendCommandDisabled = this.props.runDisabled || this.state.selectedAction == null || this.state.selectedAction.id === "-1" + let sendCommandDisabled = false; + if (!this.props.hasConfigs && this.props.selectedICs.length === 0 || this.state.selectedAction == null || this.state.selectedAction.id === "-1"){ + sendCommandDisabled = true; + } + if (this.props.hasConfigs && this.props.selectedConfigs.length === 0|| this.state.selectedAction == null || this.state.selectedAction.id === "-1"){ + sendCommandDisabled = true; + } let time = this.state.time.getFullYear().pad(4) + '-' + this.state.time.getMonth().pad(2) + '-' + @@ -79,11 +266,11 @@ class ICAction extends React.Component { )); - return
+ return
@@ -96,9 +283,9 @@ class ICAction extends React.Component { onChange={this.setTimeForAction} /> + onClick={() => this.runAction(this.state.selectedAction, this.state.time)}>Run Select time for synced command execution
; diff --git a/src/ic/ic-dialog.js b/src/ic/ic-dialog.js index 32a21d9..cba6291 100644 --- a/src/ic/ic-dialog.js +++ b/src/ic/ic-dialog.js @@ -113,10 +113,10 @@ class ICDialog extends React.Component { {this.props.userRole === "Admin" ? (
Controls:
-
- - diff --git a/src/ic/ic-store.js b/src/ic/ic-store.js index 7cf314a..36abb91 100644 --- a/src/ic/ic-store.js +++ b/src/ic/ic-store.js @@ -66,16 +66,48 @@ class InfrastructureComponentStore extends ArrayStore { return state; case 'ics/start-action': - if (!Array.isArray(action.data)) - action.data = [ action.data ] + if (!Array.isArray(action.action)) + action.action = [ action.action ] - ICsDataManager.doActions(action.ic, action.data, action.token); + ICsDataManager.doActions(action.icid, action.action, action.token, action.result); + return state; + + case 'ics/action-started': + NotificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO()); return state; case 'ics/action-error': console.log(action.error); return state; + case 'ics/action-result-added': + + for (let a of action.actions){ + + if (a.results !== undefined && a.results != null){ + // adapt URL for newly created result ID + a.results.url = a.results.url.replace("RESULTID", action.data.result.id); + a.results.url = ICsDataManager.makeURL(a.results.url); + a.results.url = window.location.host + a.results.url; + } + if (a.model !== undefined && a.model != null && JSON.stringify(a.model) !== JSON.stringify({})) { + // adapt URL(s) for model file + let modelURLs = [] + for (let url of a.model.url){ + let modifiedURL = ICsDataManager.makeURL(url); + modifiedURL = window.location.host + modifiedURL; + modelURLs.push(modifiedURL) + } + a.model.url = modelURLs + } + ICsDataManager.doActions(a.icid, [a], action.token) + } + return state; + + case 'ics/action-result-add-error': + console.log(action.error); + return state + case 'ics/get-status': ICsDataManager.getStatus(action.url, action.token, action.ic); return super.reduce(state, action); diff --git a/src/ic/ics-data-manager.js b/src/ic/ics-data-manager.js index 02d480f..a2382fc 100644 --- a/src/ic/ics-data-manager.js +++ b/src/ic/ics-data-manager.js @@ -24,24 +24,86 @@ class IcsDataManager extends RestDataManager { super('ic', '/ic'); } - doActions(ic, actions, token = null) { - for (let action of actions) { - if (action.when) - // Send timestamp as Unix Timestamp - action.when = Math.round(action.when.getTime() / 1000); - } + doActions(icid, actions, token = null, result=null) { - RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), actions, token).then(response => { + + if (icid !== undefined && icid != null && JSON.stringify(icid) !== JSON.stringify({})) { + + for (let action of actions) { + if (action.when) { + // Send timestamp as Unix Timestamp + action.when = Math.round(action.when.getTime() / 1000); + } + } + // sending action to a specific IC via IC list + + RestAPI.post(this.makeURL(this.url + '/' + icid + '/action'), actions, token).then(response => { AppDispatcher.dispatch({ - type: 'ics/action-started', - data: response + type: 'ics/action-started', + data: response }); - }).catch(error => { + }).catch(error => { AppDispatcher.dispatch({ - type: 'ics/action-error', + type: 'ics/action-error', + error + }); + }); + } else { + // sending the same action to multiple ICs via scenario controls + + // distinguish between "start" action and any other + + if (actions[0].action !== "start"){ + for (let a of actions){ + + // sending action to a specific IC via IC list + if (a.when) { + // Send timestamp as Unix Timestamp + a.when = Math.round(a.when.getTime() / 1000); + } + + RestAPI.post(this.makeURL(this.url + '/' + a.icid + '/action'), [a], token).then(response => { + AppDispatcher.dispatch({ + type: 'ics/action-started', + data: response + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'ics/action-error', + error + }); + }); + + } + } else{ + // for start actions procedure is different + // first a result needs to be created, then the start actions can be sent + + RestAPI.post(this.makeURL( '/results'), result, token).then(response => { + AppDispatcher.dispatch({ + type: 'ics/action-result-added', + data: response, + actions: actions, + token: token, + }); + + AppDispatcher.dispatch({ + type: "results/added", + data: response.result, + }); + }).catch(error => { + AppDispatcher.dispatch({ + type: 'ics/action-result-add-error', error + }); }); - }); + + + } + + + + } } getStatus(url,token,ic){ diff --git a/src/ic/ics.js b/src/ic/ics.js index 2347050..bb97ef1 100644 --- a/src/ic/ics.js +++ b/src/ic/ics.js @@ -36,6 +36,8 @@ import ICDialog from './ic-dialog'; import ICAction from './ic-action'; import DeleteDialog from '../common/dialogs/delete-dialog'; +import NotificationsDataManager from "../common/data-managers/notifications-data-manager"; +import NotificationsFactory from "../common/data-managers/notifications-factory"; class InfrastructureComponents extends Component { static getStores() { @@ -149,11 +151,35 @@ class InfrastructureComponents extends Component { this.setState({ newModal : false }); if (data) { - AppDispatcher.dispatch({ - type: 'ics/start-add', - data, - token: this.state.sessionToken, - }); + if (!data.managedexternally) { + AppDispatcher.dispatch({ + type: 'ics/start-add', + data, + token: this.state.sessionToken, + }); + } else { + // externally managed IC: dispatch create action to selected manager + let newAction = {}; + newAction["action"] = "create"; + newAction["parameters"] = data; + newAction["when"] = new Date() + + // find the manager IC + let managerIC = this.state.ics.find(ic => ic.uuid === data.manager) + if (managerIC === null || managerIC === undefined){ + NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Could not find manager IC with UUID " + data.manager)); + return; + } + + AppDispatcher.dispatch({ + type: 'ics/start-action', + icid: managerIC.id, + action: newAction, + result: null, + token: this.state.sessionToken + }); + + } } } @@ -240,18 +266,7 @@ class InfrastructureComponents extends Component { this.setState({ selectedICs: selectedICs }); } - runAction(action, when) { - for (let index of this.state.selectedICs) { - action.when = when; - AppDispatcher.dispatch({ - type: 'ics/start-action', - ic: this.state.ics[index], - data: action.data, - token: this.state.sessionToken, - }); - } - } static isICOutdated(component) { if (!component.stateUpdateAt) @@ -354,7 +369,7 @@ class InfrastructureComponents extends Component { modifyNameColumn(name, component){ let index = this.state.ics.indexOf(component); - return + return } openICStatus(ic){ @@ -378,9 +393,9 @@ class InfrastructureComponents extends Component { } } - isExternalIC(index){ - let ic = this.state.ics[index] - return ic.managedexternally + isLocalIC(index, ics){ + let ic = ics[index] + return !ic.managedexternally } getICCategoryTable(ics, editable, title){ @@ -390,7 +405,7 @@ class InfrastructureComponents extends Component { this.isExternalIC(index)} + checkboxDisabled={(index) => this.isLocalIC(index, ics) === true} onChecked={(ic, event) => this.onICChecked(ic, event)} width='30' /> @@ -420,19 +435,21 @@ class InfrastructureComponents extends Component { modifier={(stateUpdateAt, component) => this.stateUpdateModifier(stateUpdateAt, component)} /> - {this.state.currentUser.role === "Admin" && editable ? + {this.state.currentUser.role === "Admin" ? this.isLocalIC(index, ics)} exportButton deleteButton + showDeleteButton = {(index) => this.isLocalIC(index, ics)} onEdit={index => this.setState({editModal: true, modalIC: ics[index], modalIndex: index})} onExport={index => this.exportIC(index)} onDelete={index => this.setState({deleteModal: true, modalIC: ics[index], modalIndex: index})} /> : this.exportIC(index)} /> @@ -451,6 +468,11 @@ class InfrastructureComponents extends Component { marginLeft: '10px' }; + const iconStyle = { + height: '30px', + width: '30px' + } + let managerTable = this.getICCategoryTable(this.state.managers, false, "IC Managers") let simulatorTable = this.getICCategoryTable(this.state.simulators, true, "Simulators") let gatewayTable = this.getICCategoryTable(this.state.gateways, true, "Gateways") @@ -461,19 +483,19 @@ class InfrastructureComponents extends Component {

Infrastructure Components {this.state.currentUser.role === "Admin" ? - ( + ( Add Infrastructure Component } > - Import Infrastructure Component } > - ) @@ -491,12 +513,15 @@ class InfrastructureComponents extends Component { {this.state.currentUser.role === "Admin" && this.state.numberOfExternalICs > 0 ?
this.runAction(action, when)} + hasConfigs = {false} + ics={this.state.ics} + selectedICs={this.state.selectedICs} + token={this.state.sessionToken} actions={[ {id: '-1', title: 'Action', data: {action: 'none'}}, {id: '0', title: 'Reset', data: {action: 'reset'}}, {id: '1', title: 'Shutdown', data: {action: 'shutdown'}}, + {id: '2', title: 'Delete', data: {action: 'delete'}} ]} />
diff --git a/src/ic/import-ic.js b/src/ic/import-ic.js index bde1d9a..a165af6 100644 --- a/src/ic/import-ic.js +++ b/src/ic/import-ic.js @@ -118,7 +118,7 @@ class ImportICDialog extends React.Component { this.loadFile(e.target.files)} /> - + Name this.handleChange(e)} /> @@ -128,7 +128,7 @@ class ImportICDialog extends React.Component { this.handleChange(e)} /> - + UUID this.handleChange(e)} /> diff --git a/src/ic/new-ic.js b/src/ic/new-ic.js index 2e81887..39aeebd 100644 --- a/src/ic/new-ic.js +++ b/src/ic/new-ic.js @@ -170,7 +170,7 @@ class NewICDialog extends React.Component { {this.props.managers.length > 0 ? <> - An externally managed component is created and managed by an IC manager via AMQP} > + An externally managed component is created and managed by an IC manager via AMQP} > this.handleChange(e)}> diff --git a/src/img/erigrid2.png b/src/img/erigrid2.png new file mode 100644 index 0000000..0c19b4c Binary files /dev/null and b/src/img/erigrid2.png differ diff --git a/src/result/result-configs-dialog.js b/src/result/result-configs-dialog.js new file mode 100644 index 0000000..8dfc220 --- /dev/null +++ b/src/result/result-configs-dialog.js @@ -0,0 +1,64 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ +import React from 'react'; +import Dialog from '../common/dialogs/dialog'; +import ReactJson from 'react-json-view'; + + +class ResultConfigDialog extends React.Component { + valid = true; + + constructor(props) { + super(props); + + this.state = { + confirmCommand: false, + command: '', + }; + } + + onClose(canceled) { + this.props.onClose(); + } + + render() { + return ( + this.onClose(c)} + valid={true} + size="lg" + blendOutCancel={true} + > + + + + + ); + } +} + +export default ResultConfigDialog; diff --git a/src/scenario/scenario.js b/src/scenario/scenario.js index 698f1d6..1f8dee2 100644 --- a/src/scenario/scenario.js +++ b/src/scenario/scenario.js @@ -37,7 +37,8 @@ import NewDashboardDialog from "../dashboard/new-dashboard"; import EditDashboardDialog from '../dashboard/edit-dashboard'; import EditFiles from '../file/edit-files' import NewResultDialog from '../result/new-result'; -import EditResultDialog from '../result/edit-result' +import EditResultDialog from '../result/edit-result'; +import ResultConfigDialog from '../result/result-configs-dialog'; import ICAction from '../ic/ic-action'; @@ -119,6 +120,9 @@ class Scenario extends React.Component { filesToDownload: prevState.filesToDownload, zipfiles: prevState.zipfiles || false, resultNodl: prevState.resultNodl, + resultConfigsModal: false, + modalResultConfigs: {}, + modalResultConfigsIndex: 0, editOutputSignalsModal: prevState.editOutputSignalsModal || false, editInputSignalsModal: prevState.editInputSignalsModal || false, @@ -161,12 +165,12 @@ class Scenario extends React.Component { componentDidUpdate(prevProps, prevState) { // check whether file data has been loaded - if (this.state.filesToDownload && this.state.filesToDownload.length > 0 ) { + if (this.state.filesToDownload && this.state.filesToDownload.length > 0) { if (this.state.files != prevState.files) { if (!this.state.zipfiles) { let fileToDownload = FileStore.getState().filter(file => file.id === this.state.filesToDownload[0]) if (fileToDownload.length === 1 && fileToDownload[0].data) { - const blob = new Blob([fileToDownload[0].data], {type: fileToDownload[0].type}); + const blob = new Blob([fileToDownload[0].data], { type: fileToDownload[0].type }); FileSaver.saveAs(blob, fileToDownload[0].name); this.setState({ filesToDownload: [] }); } @@ -178,7 +182,7 @@ class Scenario extends React.Component { zip.file(file.name, file.data); }); let zipname = "result_" + this.state.resultNodl + "_" + (new Date()).toISOString(); - zip.generateAsync({type: "blob"}).then(function(content) { + zip.generateAsync({ type: "blob" }).then(function (content) { saveAs(content, zipname); }); this.setState({ filesToDownload: [] }); @@ -367,7 +371,7 @@ class Scenario extends React.Component { this.setState({ selectedConfigs: selectedConfigs }); } - usesExternalIC(index){ + usesExternalIC(index) { let icID = this.state.configs[index].icID; let ic = null; @@ -381,8 +385,8 @@ class Scenario extends React.Component { return false; } - if (ic.managedexternally === true){ - this.setState({ExternalICInUse: true}) + if (ic.managedexternally === true) { + this.setState({ ExternalICInUse: true }) return true } @@ -390,42 +394,6 @@ class Scenario extends React.Component { } - runAction(action, when) { - if (action.data.action === 'none') { - console.warn("No command selected. Nothing was sent."); - return; - } - - for (let index of this.state.selectedConfigs) { - // get IC for component config - let ic = null; - for (let component of this.state.ics) { - if (component.id === this.state.configs[index].icID) { - ic = component; - } - } - - if (ic == null) { - continue; - } - - if (action.data.action === 'start') { - action.data.parameters = this.state.configs[index].startParameters; - } - - action.data.when = when; - - console.log("Sending action: ", action.data) - - AppDispatcher.dispatch({ - type: 'ics/start-action', - ic: ic, - data: action.data, - token: this.state.sessionToken - }); - } - }; - getICName(icID) { for (let ic of this.state.ics) { if (ic.id === icID) { @@ -539,11 +507,21 @@ class Scenario extends React.Component { ############################################## */ closeEditSignalsModal(direction) { + + // reload the config + AppDispatcher.dispatch({ + type: 'configs/start-load', + data: this.state.modalConfigData.id, + token: this.state.sessionToken + }); + if (direction === "in") { this.setState({ editInputSignalsModal: false }); } else if (direction === "out") { this.setState({ editOutputSignalsModal: false }); } + + } onEditFiles() { @@ -645,13 +623,13 @@ class Scenario extends React.Component { let toDownload = []; let zip = false; - if (typeof(param) === 'object') { // download all files + if (typeof (param) === 'object') { // download all files toDownload = param.resultFileIDs; zip = true; this.setState({ filesToDownload: toDownload, zipfiles: zip, resultNodl: param.id }); } else { // download one file toDownload.push(param); - this.setState({ filesToDownload: toDownload, zipfiles: zip}); + this.setState({ filesToDownload: toDownload, zipfiles: zip }); } toDownload.forEach(fileid => { @@ -677,6 +655,30 @@ class Scenario extends React.Component { }); } + openResultConfigSnaphots(result) { + if (result.configSnapshots === null || result.configSnapshots === undefined) { + this.setState({ + modalResultConfigs: {"configs": []}, + modalResultConfigsIndex: result.id, + resultConfigsModal: true + }); + } else { + this.setState({ + modalResultConfigs: result.configSnapshots, + modalResultConfigsIndex: result.id, + resultConfigsModal: true + }); + } + } + + closeResultConfigSnapshots() { + this.setState({ resultConfigsModal: false }); + } + + modifyResultNoColumn(id, result) { + return + } + startPintura(configIndex) { let config = this.state.configs[configIndex]; @@ -719,18 +721,17 @@ class Scenario extends React.Component { return (); } - const buttonStyle = { - marginLeft: '10px' - }; + const altButtonStyle = { + marginLeft: '10px', + } const tableHeadingStyle = { paddingTop: '30px' } const iconStyle = { - color: '#007bff', - height: '25px', - width: '25px' + height: '30px', + width: '30px' } if (this.state.scenario === undefined) { @@ -740,48 +741,58 @@ class Scenario extends React.Component { let resulttable; if (this.state.results && this.state.results.length > 0) { resulttable =
-

- - - - - this.downloadResultData(index)} - /> - this.setState({ editResultsModal: true, modalResultsIndex: index })} - onDownloadAll={(index) => this.downloadResultData(this.state.results[index])} - onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.state.results[index], modalResultsIndex: index })} - /> -
+ + this.modifyResultNoColumn(id, result)} + /> + + + + this.downloadResultData(index)} + /> + this.setState({ editResultsModal: true, modalResultsIndex: index })} + onDownloadAll={(index) => this.downloadResultData(this.state.results[index])} + onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.state.results[index], modalResultsIndex: index })} + /> +
- - this.closeDeleteResultsModal(e)} /> -
+ + this.closeDeleteResultsModal(e)} /> + +
} return
Add, edit or delete files of scenario } > -
@@ -798,18 +809,20 @@ class Scenario extends React.Component { {/*Component Configurations table*/}

Component Configurations + Add Component Configuration } > - + Import Component Configuration } > - + +

- { this.state.ExternalICInUse ? ( -
+ {this.state.ExternalICInUse ? ( +
this.runAction(action, when)} + hasConfigs={true} + ics={this.state.ics} + configs={this.state.configs} + selectedConfigs = {this.state.selectedConfigs} + snapshotConfig = {(index) => this.copyConfig(index)} + token = {this.state.sessionToken} actions={[ - {id: '-1', title: 'Action', data: {action: 'none'}}, - {id: '0', title: 'Start', data: {action: 'start'}}, - {id: '1', title: 'Stop', data: {action: 'stop'}}, - {id: '2', title: 'Pause', data: {action: 'pause'}}, - {id: '3', title: 'Resume', data: {action: 'resume'}} - ]}/> + { id: '-1', title: 'Action', data: { action: 'none' } }, + { id: '0', title: 'Start', data: { action: 'start' } }, + { id: '1', title: 'Stop', data: { action: 'stop' } }, + { id: '2', title: 'Pause', data: { action: 'pause' } }, + { id: '3', title: 'Resume', data: { action: 'resume' } } + ]} />
- ) : (
) + ) : (
) } -
+ + < div style={{ clear: 'both' }} /> Dashboards + Add Dashboard } > - + Import Dashboard } > - + + @@ -944,12 +964,14 @@ class Scenario extends React.Component { {/*Result table*/}

Results + Add Result } > - + +

{resulttable} this.closeNewResultModal(data)} /> @@ -976,11 +998,15 @@ class Scenario extends React.Component { type="text" /> + +

diff --git a/src/scenario/scenarios.js b/src/scenario/scenarios.js index 8158af8..aef0740 100644 --- a/src/scenario/scenarios.js +++ b/src/scenario/scenarios.js @@ -236,24 +236,31 @@ class Scenarios extends Component { render() { const buttonStyle = { - marginLeft: '10px' + marginLeft: '10px', }; + const iconStyle = { + height: '30px', + width: '30px' + } + return (

Scenarios + Add Scenario } > - + Import Scenario } > - + +

diff --git a/src/signal/edit-signal-mapping.js b/src/signal/edit-signal-mapping.js index 82a8442..3b75386 100644 --- a/src/signal/edit-signal-mapping.js +++ b/src/signal/edit-signal-mapping.js @@ -17,7 +17,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Button, FormGroup, FormLabel, FormText} from 'react-bootstrap'; +import {Button, FormGroup, FormLabel, FormText, OverlayTrigger, Tooltip} from 'react-bootstrap'; import {Collapse} from 'react-collapse'; import Table from '../common/table'; import TableColumn from '../common/table-column'; @@ -64,6 +64,11 @@ class EditSignalMapping extends React.Component { } } + signals.forEach(signal => { + if(signal.checked === undefined) signal.checked = false + }); + + return { signals: signals, }; @@ -92,23 +97,24 @@ class EditSignalMapping extends React.Component { let signals = this.state.signals; let modifiedSignals = this.state.modifiedSignalIDs; - - if (column === 1) { // Name change + console.log("HandleMappingChange", row, column) + if (column === 2) { // Name change signals[row].name = event.target.value; if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); } - } else if (column === 2) { // unit change + } else if (column === 3) { // unit change signals[row].unit = event.target.value; if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); } - } else if (column === 3) { // scaling factor change + } else if (column === 4) { // scaling factor change signals[row].scalingFactor = parseFloat(event.target.value); if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); } - } else if (column === 0) { //index change + } else if (column === 1) { //index change + console.log("Index change") signals[row].index =parseInt(event.target.value, 10); if (modifiedSignals.find(id => id === signals[row].id) === undefined){ modifiedSignals.push(signals[row].id); @@ -134,6 +140,21 @@ class EditSignalMapping extends React.Component { }; + handleRemove = () => { + + let checkedSignals = this.state.signals.filter(signal => signal.checked === true); + + checkedSignals.forEach(signal => { + AppDispatcher.dispatch({ + type: 'signals/start-remove', + data: signal, + token: this.props.sessionToken + }); + + }) + + } + handleAdd = (configID = null) => { if(typeof this.props.configs !== "undefined"){ @@ -173,10 +194,38 @@ class EditSignalMapping extends React.Component { this.setState({signals: signals}) } + onSignalChecked(signal) { + let tempSignals = this.state.signals; + const index = tempSignals.indexOf(signal); + + tempSignals[index].checked = !tempSignals[index].checked; + + this.setState({signals: tempSignals}); + + } + + checkAll(){ + let tempSignals = this.state.signals; + let allChecked = true; + + tempSignals.forEach(signal => + { + if(signal.checked === false){ + signal.checked = true; + allChecked = false; + } + }); + + if(allChecked){ + tempSignals.forEach(signal => signal.checked = false); + } + this.setState({signals: tempSignals}); + } + render() { const buttonStyle = { - marginLeft: '10px' + marginLeft: '10px', }; return( @@ -185,7 +234,7 @@ class EditSignalMapping extends React.Component { show={this.props.show} title="Edit Signal Mapping" buttonTitle="Save" - blendOutCancel = {true} + blendOutCancel = {false} onClose={(c) => this.onClose(c)} onReset={() => this.resetState()} valid={true} @@ -194,7 +243,8 @@ class EditSignalMapping extends React.Component { {this.props.direction} Mapping Click Index, Name or Unit cell to edit -
+
this.onSignalChecked(signal)} data={this.state.signals}> + this.onSignalChecked(index, event)} checkboxKey='checked' width='30' /> this.handleMappingChange(e, row, column)} /> this.handleMappingChange(e, row, column)} /> this.handleMappingChange(e, row, column)} /> @@ -202,20 +252,24 @@ class EditSignalMapping extends React.Component { this.handleDelete(index)} />
-
- +
+ Check/Uncheck All } > + + + +
Choose a Component Configuration to add the signal to:
-
+
{typeof this.props.configs !== "undefined" && this.props.configs.map(config => ( - + ))}
- +
diff --git a/src/signal/signal-store.js b/src/signal/signal-store.js index 175467c..40eae35 100644 --- a/src/signal/signal-store.js +++ b/src/signal/signal-store.js @@ -27,11 +27,9 @@ class SignalStore extends ArrayStore{ reduce(state, action) { switch (action.type) { + case 'signals/added': - this.dataManager.reloadConfig(action.token, action.data); - return super.reduce(state, action); - case 'signals/removed': - this.dataManager.reloadConfig(action.token, action.data); + this.dataManager.reloadConfig(action.token, action.data.configID); return super.reduce(state, action); case 'signals/start-autoconfig': @@ -41,7 +39,6 @@ class SignalStore extends ArrayStore{ case 'signals/autoconfig-loaded': console.log("AutoConfig Loaded: ", action.data) this.dataManager.saveSignals(action.data, action.token, action.configID, action.socketname); - return super.reduce(state, action); case 'signals/autoconfig-error': diff --git a/src/signal/signals-data-manager.js b/src/signal/signals-data-manager.js index 3cc6493..1a5ee0c 100644 --- a/src/signal/signals-data-manager.js +++ b/src/signal/signals-data-manager.js @@ -27,9 +27,9 @@ class SignalsDataManager extends RestDataManager{ super('signal', '/signals'); } - reloadConfig(token, data){ + reloadConfig(token, id){ // request in signals - RestAPI.get(this.makeURL('/configs/' + data.configID), token).then(response => { + RestAPI.get(this.makeURL('/configs/' + id), token).then(response => { AppDispatcher.dispatch({ type: 'configs/edited', data: response.config diff --git a/src/styles/app.css b/src/styles/app.css index 69894a4..4d588c2 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -403,6 +403,18 @@ hr { height: auto !important; padding: 5px; float: right; + border-color: #ffffff; + background-color: #ffffff; +} + +.section-buttons-group-right .btn{ + border-color: #ffffff; + background-color: #ffffff; +} + +.section-buttons-group-right .btn:hover{ + border-color: #e3e3e3; + background-color: #e3e3e3; } .section-buttons-group-left { @@ -411,10 +423,60 @@ hr { float: left; } +.section-buttons-group-left .btn{ + background-color: #527984; + border-color: #527984; +} + +.section-buttons-group-left .btn:hover{ + background-color: #31484f; + border-color: #31484f; +} + +.drag-and-drop .btn{ + color: #527984; + border-color: #527984; +} + +.drag-and-drop .btn:hover{ + color: #527984; + border-color: #527984; +} + + .section-buttons-group-right .rc-slider { margin-left: 12px; } +.solid-button .btn{ + background-color: #527984; + border-color: #527984; +} + +.solid-button .btn:hover{ + background-color: #31484f; + border-color: #31484f; +} + +.solid-button .btn:disabled{ + background-color: #527984; + border-color: #527984; +} + +.icon-button .btn{ + border-color: #ffffff; + background-color: #ffffff; +} + +.icon-button .btn:hover{ + border-color: #e3e3e3; + background-color: #e3e3e3; +} + +.icon-color { + color: #527984; +} + .form-horizontal .form-group { margin-left: 0 !important; margin-right: 0 !important; diff --git a/src/user/login-form.js b/src/user/login-form.js index 0131849..fedfa26 100644 --- a/src/user/login-form.js +++ b/src/user/login-form.js @@ -93,8 +93,10 @@ class LoginForm extends Component { - - + + + + diff --git a/src/user/user.js b/src/user/user.js index 2026771..e85f5ac 100644 --- a/src/user/user.js +++ b/src/user/user.js @@ -17,7 +17,7 @@ import React, { Component } from 'react'; import { Container } from 'flux/utils'; -import {Button, Col, Row} from 'react-bootstrap'; +import {Button, Col, Row, FormGroup} from 'react-bootstrap'; import AppDispatcher from '../common/app-dispatcher'; import UsersStore from './users-store'; @@ -112,6 +112,7 @@ class User extends Component { render() { + return (

Your User Account

@@ -119,27 +120,25 @@ class User extends Component { {this.state.currentUser !== undefined && this.state.currentUser !== null ?
+ - Username: - {this.state.currentUser.username} + +
Username:
+
E-mail:
+
Role:
+
+ +
{this.state.currentUser.username}
+
{this.state.currentUser.mail}
+
{this.state.currentUser.role}
+ + + +
- - - E-mail: - {this.state.currentUser.mail} - - - - Role: - {this.state.currentUser.role} - - - - - this.closeEditModal(data)} - user={this.state.currentUser}/> + user={this.state.currentUser} /> : "Loading user data..." } @@ -148,8 +147,5 @@ class User extends Component { } } - - - let fluxContainerConverter = require('../common/FluxContainerConverter'); export default Container.create(fluxContainerConverter.convert(User)); diff --git a/src/user/users.js b/src/user/users.js index d577462..96b1e4c 100644 --- a/src/user/users.js +++ b/src/user/users.js @@ -131,20 +131,25 @@ class Users extends Component { render() { const buttonStyle = { - marginLeft: '10px' + marginLeft: '10px', }; + const iconStyle = { + height: '30px', + width: '30px' + } + return (

Users - + Add User } > - + - +

@@ -156,8 +161,6 @@ class Users extends Component { this.setState({ editModal: true, modalData: this.state.users[index] })} onDelete={index => this.setState({ deleteModal: true, modalData: this.state.users[index] })} />
- - this.closeNewModal(data)} /> this.closeEditModal(data)} user={this.state.modalData} /> diff --git a/src/widget/edit-widget/edit-widget-color-zones-control.js b/src/widget/edit-widget/edit-widget-color-zones-control.js index 415fbd0..6ef3fcd 100644 --- a/src/widget/edit-widget/edit-widget-color-zones-control.js +++ b/src/widget/edit-widget/edit-widget-color-zones-control.js @@ -175,6 +175,15 @@ class EditWidgetColorZonesControl extends React.Component { render() { + const buttonStyle = { + marginBottom: '10px', + marginLeft: '120px', + }; + + const iconStyle = { + height: '25px', + width : '25px' + } let tempColor = 'FFFFFF'; let collapse = false; @@ -191,9 +200,10 @@ class EditWidgetColorZonesControl extends React.Component { } return - Color Zones - - + Color zones + + +
{ this.state.widget.customProperties.zones.map((zone, idx) => { @@ -243,7 +253,9 @@ class EditWidgetColorZonesControl extends React.Component { - + + + this.closeEditModal(data)} widget={this.state.widget} zoneIndex={this.state.selectedIndex} controlId={'strokeStyle'} /> diff --git a/src/widget/toolbox-item.js b/src/widget/toolbox-item.js index f1ebb93..4c268c5 100644 --- a/src/widget/toolbox-item.js +++ b/src/widget/toolbox-item.js @@ -51,7 +51,7 @@ class ToolboxItem extends React.Component { if (this.props.disabled === false) { return this.props.connectDragSource(
- + {this.props.icon && } {this.props.name} diff --git a/src/widget/widget-toolbox.js b/src/widget/widget-toolbox.js index 033109c..ee48d37 100644 --- a/src/widget/widget-toolbox.js +++ b/src/widget/widget-toolbox.js @@ -96,11 +96,15 @@ class WidgetToolbox extends React.Component { const disableDecrease = this.disableDecrease(); // Only one topology widget at the time is supported const iconStyle = { - color: '#007bff', height: '25px', width : '25px' } + const buttonStyle = { + marginRight: '3px', + height: '40px', + } + const thereIsTopologyWidget = this.props.widgets != null && Object.values(this.props.widgets).filter(w => w.type === 'Topology').length > 0; const topologyItemMsg = thereIsTopologyWidget? 'Currently only one is supported' : ''; @@ -110,16 +114,16 @@ class WidgetToolbox extends React.Component {
Show/ hide available Cosmetic Widgets } > - + Show/ hide available Displaying Widgets } > - + Show/ hide available Manipulation Widgets } > - +
@@ -128,13 +132,13 @@ class WidgetToolbox extends React.Component { Grid: { this.props.grid > 1 ? this.props.grid : 'Disabled' } Increase dashboard height } > - Decrease dashboard height } > - @@ -143,7 +147,7 @@ class WidgetToolbox extends React.Component {





-
+