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 : (
+

+ );
+
+ if (data.action.app_name === "shuffle-subflow") {
+ //const parsedImage = triggers[2].large_image;
+ //actionimg = (
+ //

+ //);
+ } else if (data.action.app_name === "User Input") {
+ //actionimg = (
+ //

+ //);
+ }
+
+ 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
+
+ //