webgme-dss
Version:
Design Studio for Dynamic Systems with Modelica as backend
407 lines (351 loc) • 16.2 kB
JSX
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import update from 'immutability-helper';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ErrorIcon from '@material-ui/icons/Error';
import CheckIcon from '@material-ui/icons/Check';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import Typography from '@material-ui/core/Typography';
import CircularProgress from '@material-ui/core/CircularProgress';
import LinearProgress from '@material-ui/core/LinearProgress';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import Edit from '@material-ui/icons/Edit';
import CloudDownload from '@material-ui/icons/CloudDownload';
import Delete from '@material-ui/icons/Delete';
import Description from '@material-ui/icons/Description';
import ChromeReaderMode from '@material-ui/icons/ChromeReaderMode';
import Territory from 'webgme-react-components/src/components/Territory';
import ConfirmDialog from 'webgme-react-components/src/components/ConfirmDialog';
import {downloadBlobArtifact} from '../../gme/utils/saveUrlToDisk';
import PlotVariableSelector from './PlotVariableSelector';
import RenameInput from './RenameInput';
import ConsoleDialog from '../../Dialogs/ConsoleDialog';
import getMetaNodeByName from '../../gme/utils/getMetaNodeByName';
import {setPlotNode, setSimResData, setResultNode} from '../../actions';
const mapStateToProps = state => ({
plotNode: state.plotData.nodeId,
resultNode: state.resultNode,
simRes: state.plotData.simRes,
});
const mapDispatchToProps = dispatch => ({
setPlotNode: (nodeId) => {
dispatch(setPlotNode(nodeId));
},
setSimResData: (simRes) => {
dispatch(setSimResData(simRes));
},
setResultNode: (resultNode) => {
dispatch(setResultNode(resultNode));
},
});
class ResultList extends Component {
static propTypes = {
gmeClient: PropTypes.object.isRequired,
minimized: PropTypes.bool.isRequired,
setPlotNode: PropTypes.func.isRequired,
setSimResData: PropTypes.func.isRequired,
setResultNode: PropTypes.func.isRequired,
resultNode: PropTypes.string,
// plotNode: PropTypes.string,
simRes: PropTypes.object,
};
static defaultProps = {
// plotNode: null,
resultNode: null,
simRes: null,
};
constructor(props) {
super(props);
const {gmeClient} = this.props;
const simResNode = getMetaNodeByName(gmeClient, 'SimulationResults');
if (simResNode) {
this.state.containerId = simResNode.getId();
this.state.territory = {
[this.state.containerId]: {children: 1},
};
} else {
console.error(new Error('Could not find SimulationResults in meta...'));
}
}
state = {
containerId: null,
expandedResId: null,
territory: null,
results: {},
editResultName: false,
showConfirmDelete: false,
showConsoleDialog: false,
};
onDeleteConfirmed = (doDelete) => {
const {gmeClient} = this.props;
const {expandedResId} = this.state;
if (doDelete && expandedResId) {
gmeClient.deleteNode(expandedResId, `Result was removed ${expandedResId}`);
}
this.setState({showConfirmDelete: false});
};
handleEvents = (hash, loads, updates, unloads) => {
const {gmeClient, resultNode} = this.props;
const {containerId} = this.state;
const updateDesc = {};
const attrNames = ['name', 'simRes', 'simPackage', 'csvFile', 'timeStamp'];
loads.forEach((nodeId) => {
if (nodeId !== containerId) {
const nodeObj = gmeClient.getNode(nodeId);
const modelId = nodeObj.getChildrenIds()[0]; // FIXME: This is assuming one and only one model
const isRunning = !nodeObj.getAttribute('stdout');
updateDesc[nodeId] = {
$set: {
isRunning,
modelId,
},
};
attrNames.forEach((attrName) => {
updateDesc[nodeId].$set[attrName] = nodeObj.getAttribute(attrName);
});
if (nodeId === resultNode && isRunning === false) {
// FIXME
setTimeout(() => this.switchPlotNode(nodeId));
}
}
});
updates.forEach((nodeId) => {
if (nodeId !== containerId) {
const nodeObj = gmeClient.getNode(nodeId);
const modelId = nodeObj.getChildrenIds()[0]; // FIXME: This is assuming one and only one model
const isRunning = !nodeObj.getAttribute('stdout');
updateDesc[nodeId] = {
isRunning: {$set: isRunning},
modelId: {$set: modelId},
};
attrNames.forEach((attrName) => {
updateDesc[nodeId][attrName] = {$set: nodeObj.getAttribute(attrName)};
});
if (nodeId === resultNode && isRunning === false) {
// FIXME
setTimeout(() => this.switchPlotNode(nodeId));
}
}
});
updateDesc.$unset = unloads.filter(nodeId => nodeId !== containerId);
if (unloads.includes(resultNode)) {
this.props.setResultNode(null);
this.switchPlotNode(null);
}
this.setState({
results: update(this.state.results, updateDesc),
});
};
handleExpand = resId => (event, expanded) => {
// extract attribute simRes and add it to the state
if (expanded) {
this.props.setResultNode(resId);
this.switchPlotNode(resId);
this.setState({expandedResId: resId});
} else {
this.setState({expandedResId: null});
}
};
updateResultTitle = (resId, canceled, newName) => {
const {gmeClient} = this.props;
const {results} = this.state;
if (results[resId] && canceled === false && results[resId].name !== newName) {
gmeClient.setAttribute(resId, 'name', newName);
}
this.setState({editResultName: false});
};
downloadArtifact = (entirePackage) => {
const {results, expandedResId} = this.state;
if (expandedResId && results[expandedResId]) {
if (entirePackage && results[expandedResId].simPackage) {
downloadBlobArtifact(results[expandedResId].simPackage);
}
if (!entirePackage && results[expandedResId].csvFile) {
downloadBlobArtifact(results[expandedResId].csvFile);
}
}
};
switchPlotNode = (resId) => {
const {results} = this.state;
if (resId === null) {
this.props.setPlotNode(null);
this.props.setSimResData(null);
} else {
this.props.setPlotNode(results[resId].modelId);
this.props.setSimResData(results[resId].simRes ? JSON.parse(results[resId].simRes) : null);
}
};
render() {
const {
minimized, gmeClient, simRes,
} = this.props;
const {
territory,
results,
expandedResId,
editResultName,
showConfirmDelete,
showConsoleDialog,
} = this.state;
const resultIds = Object.keys(results).sort((a, b) => {
const tA = results[a].timeStamp || 0;
const tB = results[b].timeStamp || 0;
if (tA > tB) {
return -1;
} else if (tB > tA) {
return 1;
}
return 0;
});
return (
<div style={{display: minimized ? 'none' : undefined}}>
<Territory
gmeClient={gmeClient}
territory={territory}
onUpdate={this.handleEvents}
onlyActualEvents
reuseTerritory={false}
/>
{resultIds.map((resId) => {
const resInfo = results[resId];
const hasResults = simRes !== null;
const isExpanded = resId === expandedResId;
const failed = resInfo.isRunning === false && !resInfo.simRes;
const actionButtons = [
{
id: 'modeEdit',
toolTip: 'Rename result',
iconClass: <Edit style={{color: 'rgba(0, 0, 0, 0.54)', fontSize: 18}}/>,
onClick: () => {
this.setState({editResultName: true});
},
},
{
id: 'delete',
toolTip: 'Delete result',
iconClass: <Delete style={{color: 'rgba(0, 0, 0, 0.54)', fontSize: 18}}/>,
onClick: () => {
this.setState({showConfirmDelete: true});
},
},
{
id: 'viewConsole',
toolTip: 'View console output',
iconClass: <ChromeReaderMode style={{color: 'rgba(0, 0, 0, 0.54)', fontSize: 18}}/>,
onClick: () => {
this.setState({showConsoleDialog: true});
},
},
];
let statusIcon;
if (resInfo.isRunning) {
statusIcon = <CircularProgress size={18}/>;
} else if (failed) {
statusIcon = <ErrorIcon style={{color: 'pink', width: 18, height: 18}}/>;
} else {
statusIcon = <CheckIcon style={{color: 'lightgreen', width: 18, height: 18}}/>;
actionButtons.push({
id: 'downloadCSV',
toolTip: 'Download CSV result file',
iconClass: <Description style={{color: 'rgba(0, 0, 0, 0.54)', fontSize: 18}}/>,
onClick: () => {
this.downloadArtifact(false);
},
});
actionButtons.push({
id: 'downloadPackage',
toolTip: 'Download simulation package',
iconClass: <CloudDownload style={{color: 'rgba(0, 0, 0, 0.54)', fontSize: 18}}/>,
onClick: () => {
this.downloadArtifact(true);
},
});
}
return (
<ExpansionPanel
key={resId}
expanded={isExpanded}
onChange={this.handleExpand(resId)}
>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>}>
<div style={{marginRight: 6, marginLeft: -18}}>
{statusIcon}
</div>
{editResultName && expandedResId === resId && results[expandedResId] ?
(
<RenameInput
initialValue={results[expandedResId].name}
onDone={(canceled, newValue) => {
this.updateResultTitle(resId, canceled, newValue);
}}
/>) :
(
<Typography
type="subheading"
style={{
textOverflow: 'ellipsis',
maxWidth: 160,
overflow: 'hidden',
}}
>{resInfo.name}
</Typography>)
}
</ExpansionPanelSummary>
<ExpansionPanelDetails style={{display: 'block', padding: 0, paddingBottom: 10}}>
<span>
{actionButtons.map(desc => (
<Tooltip
key={desc.id}
id={desc.id}
title={desc.toolTip}
placement="top"
>
<IconButton
key={desc.id}
onClick={desc.onClick}
>
{desc.iconClass}
</IconButton>
</Tooltip>
))}
</span>
{(() => {
if (hasResults) {
return <PlotVariableSelector gmeClient={gmeClient} nodeId={resId}/>;
} else if (failed) {
return null;
}
return (
<div>
<LinearProgress/>
<br/>
<LinearProgress color="secondary"/>
</div>);
})()}
</ExpansionPanelDetails>
</ExpansionPanel>);
})}
{showConfirmDelete ?
<ConfirmDialog
title="Delete Result"
message="Do you want to delete the result? If the simulation is running it will be aborted.."
onClose={this.onDeleteConfirmed}
/> : null
}
{showConsoleDialog && expandedResId ?
<ConsoleDialog
gmeClient={gmeClient}
attributeName="stdout"
title="Simulation ouput"
nodeId={expandedResId}
onClose={() => this.setState({showConsoleDialog: false})}
/> : null
}
</div>);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ResultList);