From 0ed336fb37f2e5f1245f284659557dc64bcf67ff Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 17 Mar 2023 00:51:26 +0100 Subject: [PATCH] Added workflow forms \o/ --- frontend/src/views/RunWorkflow.jsx | 777 +++++++++++++++++++++++++++++ 1 file changed, 777 insertions(+) create mode 100644 frontend/src/views/RunWorkflow.jsx diff --git a/frontend/src/views/RunWorkflow.jsx b/frontend/src/views/RunWorkflow.jsx new file mode 100644 index 00000000..aaf5f800 --- /dev/null +++ b/frontend/src/views/RunWorkflow.jsx @@ -0,0 +1,777 @@ +/* eslint-disable react/no-multi-comp */ +import React, {useState, useEffect} from 'react'; +import ReactDOM from "react-dom" + +import { useInterval } from "react-powerhooks"; +import { makeStyles } from '@material-ui/styles'; +import { useNavigate, Link, useParams } from "react-router-dom"; +import {isMobile} from "react-device-detect"; +import theme from '../theme'; +import { validateJson, GetIconInfo } from "./Workflows.jsx"; +import { green, yellow } from "./AngularWorkflow.jsx"; + +import { + Tooltip, + IconButton, + CircularProgress, + TextField, + Button, + ButtonGroup, + Paper, + Typography, + Divider, +} from '@material-ui/core'; + +import { + Preview as PreviewIcon, + ContentCopy as ContentCopyIcon, +} from '@mui/icons-material'; + +const hrefStyle = { + color: "white", + textDecoration: "none" +} + +const bodyDivStyle = { + margin: "auto", + marginTop: 100, + width: isMobile? "100%":"500px", + position: "relative", +} + +const boxStyle = { + color: "white", + paddingLeft: "30px", + paddingRight: "30px", + paddingBottom: "30px", + paddingTop: "30px", + backgroundColor: theme.palette.surfaceColor, + marginBottom: 150, +} + +const RunWorkflow = (defaultprops) => { + const { globalUrl, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, register, serverside } = defaultprops; + + let navigate = useNavigate(); + const [message, setMessage] = useState(""); + const [workflow, setWorkflow] = React.useState({}); + const [executionRequest, setExecutionRequest] = React.useState({}); + const [executionArgument, setExecutionArgument] = useState(""); + const [executionLoading, setExecutionLoading] = useState(false); + const [executionData, setExecutionData] = React.useState({}); + const [executionRunning, setExecutionRunning] = useState(false); + const [workflowQuestion, setWorkflowQuestion] = useState(""); + const [selectedOrganization, setSelectedOrganization] = React.useState(undefined); + const [apps, setApps] = React.useState([]); + const [buttonClicked, setButtonClicked] = React.useState(""); + + const params = useParams(); + var props = JSON.parse(JSON.stringify(defaultprops)) + props.match = {} + props.match.params = params + + + const parsedsearch = serverside === true ? "" : window.location.search + if (serverside !== true) { + const tmpMessage = new URLSearchParams(window.location.search).get("message") + if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { + setMessage(tmpMessage) + } + } + + // Used to swap from login to register. True = login, false = register + + // Error messages etc + const [executionInfo, setExecutionInfo] = useState(""); + + const handleValidateForm = (executionArgument) => { + return true + } + + + const getApps = () => { + fetch(globalUrl + "/api/v1/apps", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + setApps(responseJson) + }) + .catch(error => { + console.log("App error: ", error); + }) + } + + const ShowExecutionResults = (props) => { + const { executionData } = props; + + if (executionData === undefined || executionData === null || executionData === {}) { + return null + } + + const executionMargin = 20 + const defaultReturn = +
+ + No results yet + +
+ + if (executionData.results === undefined || executionData.results === null) { + return defaultReturn + } + + return ( +
+ + Results + + + {executionData.results.map((data, index) => { + if (executionData.results.length !== 1 && (data.status === "SKIPPED")) { + return null; + } + + // FIXME: The latter replace doens't really work if ' is used in a string + var showResult = data.result.trim(); + const validate = validateJson(showResult); + + const curapp = apps.find((a) => a.name === data.action.app_name && a.app_version === data.action.app_version); + const imgsize = 50; + const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? green : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : yellow; + + var imgSrc = curapp === undefined ? "" : curapp.large_image; + if ( + imgSrc.length === 0 && + workflow.actions !== undefined && + workflow.actions !== null + ) { + // Look for the node in the workflow + const action = workflow.actions.find( + (action) => action.id === data.action.id + ); + if (action !== undefined && action !== null) { + imgSrc = action.large_image; + } + } + + var actionimg = + curapp === null ? null : ( + {data.action.app_name} + ); + + if (data.action.app_name === "shuffle-subflow") { + //const parsedImage = triggers[2].large_image; + //actionimg = ( + // {"Shuffle + //); + } else if (data.action.app_name === "User Input") { + //actionimg = ( + // {"Shuffle + //); + } + + if (validate.valid && typeof validate.result === "string") { + validate.result = JSON.parse(validate.result); + } + + if (validate.valid && typeof validate.result === "object") { + if ( + validate.result.result !== undefined && + validate.result.result !== null + ) { + try { + validate.result.result = JSON.parse(validate.result.result); + } catch (e) { + //console.log("ERROR PARSING: ", e) + } + } + } + + + var similarActionsView = null + if (data.similar_actions !== undefined && data.similar_actions !== null) { + var minimumMatch = 85 + var matching_executions = [] + if (data.similar_actions !== undefined && data.similar_actions !== null) { + for (let [k,kval] in Object.entries(data.similar_actions)){ + if (data.similar_actions.hasOwnProperty(k)) { + if (data.similar_actions[k].similarity > minimumMatch) { + matching_executions.push(data.similar_actions[k].execution_id) + } + } + } + } + + if (matching_executions.length !== 0) { + var parsed_url = matching_executions.join(",") + + similarActionsView = + + { + navigate(`?execution_highlight=${parsed_url}`) + }} + > + + + + } + } + + return ( +
{ + }} + onMouseOut={() => { + }} + > +
+ + Status  + + + {data.status} + +
+
+ ) + })} +
+ ) + } + + const onSubmit = (execution_id, authorization, answer) => { + stop() + setMessage("") + setExecutionLoading(true) + setExecutionData({}) + setExecutionInfo("") + + var data = { + "execution_argument": executionArgument + } + + var url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/execute` + var fetchBody = { + mode: 'cors', + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + } + + if (answer !== undefined && execution_id !== undefined && authorization !== undefined) { + url += `?reference_execution=${execution_id}&authorization=${authorization}&answer=${answer}` + data = {} + fetchBody.method = "GET" + } else { + fetchBody.method = "POST" + fetchBody.body = JSON.stringify(data) + } + + fetch(url, fetchBody) + .then((response) => { + if (response.status !== 200 && response.status !== 201) { + + if (answer !== undefined && execution_id !== undefined && authorization !== undefined) { + setExecutionLoading(false) + setExecutionRunning(true); + setExecutionRequest({ + "execution_id": execution_id, + "authorization": authorization, + }) + + start(); + return + } + } + + return response.json(); + }) + .then(responseJson => { + setExecutionLoading(false) + if (responseJson["success"] === false) { + console.log("Failed sending execution request") + } else { + console.log("Started execution") + + if (answer !== undefined && answer !== null) { + console.log("Skipping start") + } else { + setExecutionRunning(true); + setExecutionRequest(responseJson) + start(); + } + } + }) + .catch(error => { + //setExecutionInfo("Error in workflow startup: " + error) + setExecutionLoading(false) + }) + } + + const getWorkflow = (workflow_id) => { + fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + // Not sure why this is necessary. + if (responseJson.isValid === undefined) { + responseJson.isValid = true; + } + + if (responseJson.errors === undefined) { + responseJson.errors = []; + } + + if (responseJson.actions === undefined || responseJson.actions === null) { + responseJson.actions = []; + } + + if (responseJson.triggers === undefined || responseJson.triggers === null) { + responseJson.triggers = []; + } + + handleGetOrg(responseJson.org_id) + setWorkflow(responseJson); + }) + .catch((error) => { + console.log("Get workflow error: ", error.toString()); + }); + }; + + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: true, + callback: () => { + fetchUpdates(executionRequest.execution_id, executionRequest.authorization) + }, + }); + + const handleUpdateResults = (responseJson, executionRequest) => { + if (responseJson === undefined || responseJson === null || responseJson.success === false) { + return + } + + console.log("Got response: ", responseJson) + + ReactDOM.unstable_batchedUpdates(() => { + if (JSON.stringify(responseJson) !== JSON.stringify(executionData)) { + // FIXME: If another is selected, don't edit.. + // Doesn't work because this is some async garbage + if (executionData.execution_id === undefined || (responseJson.execution_id === executionData.execution_id && responseJson.results !== undefined && responseJson.results !== null)) { + if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || (executionData.results !== undefined && responseJson.results !== null && executionData.results.length !== responseJson.results.length)) { + console.log("Updating data!") + setExecutionData(responseJson) + + for (var key in responseJson.results) { + if (responseJson.results[key].status === "WAITING") { + console.log("Found: ", responseJson.results[key]) + + const validate = validateJson(responseJson.results[key].result) + console.log("Validate: ", validate) + if (validate.valid && typeof validate.result === "string") { + validate.result = JSON.parse(validate.result) + } + + console.log("Newresult: ", validate.result) + if (validate.result["information"] !== undefined && validate.result["information"] !== null) { + setWorkflowQuestion(validate.result["information"]) + } + + break + } + } + } else { + console.log("NOT updating executiondata state."); + } + } + } + + if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING") { + stop(); + + if (executionRunning) { + setExecutionRunning(false); + } + + //getWorkflowExecution(props.match.params.key, ""); + } else if (responseJson.status === "FINISHED") { + setExecutionRunning(false) + stop(); + //getWorkflowExecution(props.match.params.key, ""); + } + }) + } + + const handleGetOrg = (orgId, execution_id, authorization) => { + if (orgId.length === 0) { + return; + } + + // Just use this one? + var url = execution_id !== undefined && authorization !== undefined ? `${globalUrl}/api/v1/orgs/${orgId}?reference_execution=${execution_id}&authorization=${authorization}` : `${globalUrl}/api/v1/orgs/${orgId}`; + fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 401) { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + } else { + if (responseJson.sync_features === undefined || responseJson.sync_features === null) { + } + + setSelectedOrganization(responseJson) + } + }) + .catch((error) => { + console.log("Error getting org: ", error); + }); + }; + + const fetchUpdates = (execution_id, authorization, getorg) => { + const innerRequest = { + "execution_id": execution_id, + "authorization": authorization + } + + if (executionRequest.execution_id !== innerRequest.execution_id) { + setExecutionRequest(innerRequest) + } + + if (execution_id === "" || authorization === "") { + setExecutionLoading(false) + setExecutionRunning(false) + stop() + return + } + + fetch(globalUrl + "/api/v1/streams/results", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(innerRequest), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (getorg === true) { + handleGetOrg(responseJson.org_id, execution_id, authorization) + } + + handleUpdateResults(responseJson, executionRequest); + }) + .catch((error) => { + console.log("Execution result Error: ", error); + }); + }; + + const answer = new URLSearchParams(window.location.search).get("answer") + const execution_id = new URLSearchParams(window.location.search).get("reference_execution") + const authorization = new URLSearchParams(window.location.search).get("authorization") + useEffect(() => { + getWorkflow(props.match.params.key) + if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) { + console.log("Get execution: ", execution_id) + fetchUpdates(execution_id, authorization, true) + } + + if (answer !== undefined && answer !== null) { + console.log("Got answer: ", answer) + } + }, []) + + + + const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)" + const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"} + + const disabledButtons = message.length > 0 || executionData.status === "FINISHED" || executionData.status === "ABORTED" + + const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : "Unknown" + const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io" + //const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io" + const image = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.image : theme.palette.defaultImage + + // + +  or  + + + + + : +
+ +
+ } + + {buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ? + finalize workflow animation { + console.log("Img loaded.") + setTimeout(() => { + console.log("Img closing.") + setButtonClicked("finished") + + }, 1250) + + }} + /> + : null} + +
+ {executionInfo} +
+ + {answer !== undefined && answer !== null ? null : + + } + + + + + const loadedCheck = isLoaded ? +
+ {basedata} +
+ : +
+
+ + return ( +
+ {loadedCheck} +
+ ) +} + +export default RunWorkflow;