diff --git a/src/pages/dashboards/widget/widgets/player.jsx b/src/pages/dashboards/widget/widgets/player.jsx new file mode 100644 index 0000000..b50b3e3 --- /dev/null +++ b/src/pages/dashboards/widget/widgets/player.jsx @@ -0,0 +1,299 @@ +/** + * 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, { useState, useEffect } from 'react'; +import { Container, Row, Col } from 'react-bootstrap'; +import JSZip from 'jszip'; +import IconButton from '../../../../common/buttons/icon-button'; +import IconTextButton from '../../../../common/buttons/icon-text-button'; +import ParametersEditor from '../../../../common/parameters-editor'; +import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog'; +import { playerMachine } from '../widget-player/player-machine'; +import { interpret } from 'xstate'; +import { useSendActionMutation, useLazyDownloadFileQuery, useGetResultsQuery, useGetFilesQuery } from '../../../../store/apiSlice'; +import notificationsDataManager from '../../../../common/data-managers/notifications-data-manager'; +import NotificationsFactory from '../../../../common/data-managers/notifications-factory'; +import { start } from 'xstate/lib/actions'; + +const WidgetPlayer = ( + {widget, editing, configs, onStarted, ics, results, files, scenarioID}) => { + + const [sendAction] = useSendActionMutation(); + const [triggerDownloadFile] = useLazyDownloadFileQuery(); + const {refetch: refetchResults} = useGetResultsQuery(scenarioID); + const {refetch: refetchFiles} = useGetFilesQuery(scenarioID); + + + const [playerState, setPlayerState] = useState(playerMachine.initialState); + const [configID, setConfigID] = useState(-1); + const [config, setConfig] = useState({}); + const [ic, setIC] = useState(null); + const [icState, setICState] = useState("unknown"); + const [startParameters, setStartParameters] = useState({}); + const [playerIC, setPlayerIC] = useState({name: ""}); + const [showPythonModal, setShowPythonModal] = useState(false); + const [showConfig, setShowConfig] = useState(false); + const [isUploadResultsChecked, setIsUploadResultsChecked] = useState(false); + const [resultArrayId, setResultArrayId] = useState(0); + const [filesToDownload, setFilesToDownload] = useState([]); + + const [showWarning, setShowWarning] = useState(false); + const [warningText, setWarningText] = useState(""); + const [configBtnText, setConfigBtnText] = useState("Component Configuration"); + + const playerService = interpret(playerMachine); + playerService.start(); + + useEffect(() => { + if (typeof widget.customProperties.configID !== "undefined" + && configID !== widget.customProperties.configID) { + let configID = widget.customProperties.configID; + let config = configs.find(cfg => cfg.id === parseInt(configID, 10)); + if (config) { + let playeric = ics.find(ic => ic.id === parseInt(config.icID, 10)); + if (playeric) { + var afterCreateState = ''; + if (playeric.state === 'idle') { + afterCreateState = transitionState(playerState, 'ICIDLE'); + } else { + afterCreateState = transitionState(playerState, 'ICBUSY'); + } + + setPlayerIC(playeric); + setConfigID(configID); + setPlayerState(afterCreateState); + setConfig(config); + setStartParameters(config.startParameters); + } + } + } + }, [configs]); + + useEffect(() => { + if (results && results.length != resultArrayId) { + setResultArrayId(results.length - 1); + } + }, [results]); + + useEffect(() => { + if (ic && ic?.state != icState){ + var newState = ""; + switch (ic.state) { + case 'stopping': // if configured, show results + if (isUploadResultsChecked) { + refetchResults(); + refetchFiles(); + } + newState = transitionState(playerState, 'FINISH') + return { playerState: newState, icState: ic.state } + case 'idle': + newState = transitionState(playerState, 'ICIDLE') + return { playerState: newState, icState: ic.state } + default: + if (ic.state === 'running') { + onStarted() + } + newState = transitionState(playerState, 'ICBUSY') + return { playerState: newState, icState: ic.state } + } + } + }, [icState]); + + const transitionState = (currentState, playerEvent) => { + return playerMachine.transition(currentState, { type: playerEvent }) + } + + const clickStart = async () => { + const startConfig = { ...config }; + startConfig.startParameters = startParameters; + + try { + sendAction({ icid: startConfig.icID, action: "start", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + } catch(error) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); + } + + setPlayerState(transitionState(playerState, 'START')); + } + + const clickReset = async () => { + try { + sendAction({ icid: ic.id, action: "reset", when: Math.round((new Date()).getTime() / 1000), parameters: {...startParameters } }).unwrap(); + } catch(error) { + notificationsDataManager.addNotification(NotificationsFactory.LOAD_ERROR(error?.data?.message)); + console.log(error); + } + } + + const downloadResultFiles = () => { + if (results.length <= resultArrayId) { + setShowWarning(true); + setWarningText('no new result'); + return; + } + + const result = results[resultArrayId]; + const toDownload = result.resultFileIDs; + + if(toDownload.length <= 0){ + setShowWarning(true); + setWarningText('no result files'); + } else { + toDownload.forEach(fileID => handleDownloadFile(fileID)) + } + + setFilesToDownload(toDownload); + + } + + const handleDownloadFile = async (fileID) => { + try { + const res = await triggerDownloadFile(fileID); + const file = files.find(f => f.id === fileID); + const blob = new Blob([res], { type: 'application/octet-stream' }); + zip.file(file.name, blob); + } catch (error) { + notificationsDataManager.addNotification(NotificationsFactory.UPDATE_ERROR(error?.data?.message)); + } + } + + const openPythonDialog = () => { + if (results.length <= resultArrayId) { + setShowWarning(true); + setWarningText('no new result'); + return; + } + + setShowPythonModal(true); + } + + const iconStyle = { + height: '20px', + width: '20px' + } + + let configButton = { + height: '70px', + width: '120px', + fontSize: '13px' + } + + return ( +
+
+ + + + + clickStart()} + icon='play' + disabled={!(playerState && playerState.matches('startable'))} + iconStyle={iconStyle} + tooltip='Start Component' + /> + + + + + clickReset()} + icon='undo' + iconStyle={iconStyle} + tooltip='Reset Component' + /> + + + + + + + + setShowConfig(prevState => (!prevState))} + icon='cogs' + text={configBtnText + ' '} + buttonStyle={configButton} + disabled={false} + tooltip='Open/Close Component Configuration' + /> + + + + + {isUploadResultsChecked ? +
+

Results

+ + + + openPythonDialog()} + icon={['fab', 'python']} + disabled={(playerState && playerState.matches('finished')) ? false : true} + iconStyle={iconStyle} + /> + + + + + downloadResultFiles()} + icon='file-download' + disabled={(playerState && playerState.matches('finished')) ? false : true} + iconStyle={iconStyle} + /> + + + +
+ : <> + } +
+ + +
+ {showConfig && config ? +
+ setStartParameters(data)} + />
+ : <> + } + {showWarning ? +

{warningText}

: <> + } + {isUploadResultsChecked ? + setShowPythonModal(false)} + /> : <> + } +
+ ); +} + +export default WidgetPlayer;