7471 lines
278 KiB
React
7471 lines
278 KiB
React
import React, {useState, useEffect, useLayoutEffect} from 'react';
|
||
import { useInterval } from 'react-powerhooks';
|
||
|
||
import uuid from "uuid";
|
||
import {Link} from 'react-router-dom';
|
||
import { Prompt } from 'react-router'
|
||
import ReactJson from 'react-json-view'
|
||
import { useBeforeunload } from 'react-beforeunload';
|
||
import NestedMenuItem from "material-ui-nested-menu-item";
|
||
|
||
import {TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade} from '@material-ui/core';
|
||
|
||
import {ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons';
|
||
|
||
import * as cytoscape from 'cytoscape';
|
||
import * as edgehandles from 'cytoscape-edgehandles';
|
||
import * as clipboard from 'cytoscape-clipboard';
|
||
import CytoscapeComponent from 'react-cytoscapejs';
|
||
import undoRedo from 'cytoscape-undo-redo';
|
||
import Draggable from 'react-draggable';
|
||
|
||
import cytoscapestyle from '../defaultCytoscapeStyle';
|
||
import cxtmenu from 'cytoscape-cxtmenu';
|
||
|
||
import { w3cwebsocket as W3CWebSocket } from "websocket";
|
||
import { useAlert } from "react-alert";
|
||
import { validateJson } from "./Workflows.jsx";
|
||
import { GetParsedPaths } from "./Apps.jsx";
|
||
|
||
const surfaceColor = "#27292D"
|
||
const inputColor = "#383B40"
|
||
|
||
// http://apps.cytoscape.org/apps/yfileslayoutalgorithms
|
||
cytoscape.use(edgehandles);
|
||
cytoscape.use(clipboard);
|
||
cytoscape.use(undoRedo);
|
||
cytoscape.use( cxtmenu );
|
||
|
||
// https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
|
||
function useWindowSize() {
|
||
const [size, setSize] = useState([0, 0]);
|
||
useLayoutEffect(() => {
|
||
function updateSize() {
|
||
setSize([window.innerWidth, window.innerHeight]);
|
||
}
|
||
window.addEventListener('resize', updateSize);
|
||
updateSize();
|
||
return () => window.removeEventListener('resize', updateSize);
|
||
}, []);
|
||
return size;
|
||
}
|
||
|
||
function removeParam(key, sourceURL) {
|
||
if (sourceURL === undefined) {
|
||
return
|
||
}
|
||
|
||
var rtn = sourceURL.split("?")[0],
|
||
param,
|
||
params_arr = [],
|
||
queryString = (sourceURL.indexOf("?") !== -1) ? sourceURL.split("?")[1] : "";
|
||
|
||
if (queryString !== "") {
|
||
params_arr = queryString.split("&");
|
||
for (var i = params_arr.length - 1; i >= 0; i -= 1) {
|
||
param = params_arr[i].split("=")[0];
|
||
if (param === key) {
|
||
params_arr.splice(i, 1);
|
||
}
|
||
}
|
||
rtn = rtn + "?" + params_arr.join("&");
|
||
}
|
||
|
||
if (rtn === "?") {
|
||
return ""
|
||
}
|
||
|
||
return rtn;
|
||
}
|
||
|
||
const splitter = "|~|"
|
||
//const referenceUrl = "https://shuffler.io/functions/webhooks/"
|
||
//const referenceUrl = window.location.origin+"/api/v1/hooks/"
|
||
|
||
const AngularWorkflow = (props) => {
|
||
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
|
||
const referenceUrl = globalUrl+"/api/v1/hooks/"
|
||
const alert = useAlert()
|
||
const borderRadius = 3
|
||
|
||
const [bodyWidth, bodyHeight] = useWindowSize();
|
||
const appBarSize = 74
|
||
const headerSize = 60
|
||
|
||
var to_be_copied = ""
|
||
const [cystyle, ] = useState(cytoscapestyle)
|
||
const [cy, setCy] = React.useState()
|
||
|
||
const [appSearch, setAppSearch] = React.useState("")
|
||
const [currentView, setCurrentView] = React.useState(0)
|
||
const [triggerAuthentication, setTriggerAuthentication] = React.useState({})
|
||
const [triggerFolders, setTriggerFolders] = React.useState([])
|
||
const [workflows, setWorkflows] = React.useState([])
|
||
const [showEnvironment, setShowEnvironment] = React.useState(false)
|
||
|
||
const [workflow, setWorkflow] = React.useState({});
|
||
const [userSettings, setUserSettings] = React.useState({});
|
||
const [subworkflow, setSubworkflow] = React.useState({});
|
||
const [subworkflowStartnode, setSubworkflowStartnode] = React.useState("");
|
||
const [leftViewOpen, setLeftViewOpen] = React.useState(true);
|
||
const [leftBarSize, setLeftBarSize] = React.useState(350)
|
||
const [executionText, setExecutionText] = React.useState("");
|
||
const [executionRequestStarted, setExecutionRequestStarted] = React.useState(false);
|
||
|
||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||
const [variablesModalOpen, setVariablesModalOpen] = React.useState(false);
|
||
const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = React.useState(false);
|
||
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
|
||
const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false);
|
||
const [newVariableName, setNewVariableName] = React.useState("");
|
||
const [newVariableDescription, setNewVariableDescription] = React.useState("");
|
||
const [newVariableValue, setNewVariableValue] = React.useState("");
|
||
const [workflowDone, setWorkflowDone] = React.useState(false)
|
||
const [localFirstrequest, setLocalFirstrequest] = React.useState(true)
|
||
const [requiresAuthentication, setRequiresAuthentication] = React.useState(false)
|
||
const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false)
|
||
const [showSkippedActions, setShowSkippedActions] = React.useState(false)
|
||
const [lastExecution, setLastExecution] = React.useState("")
|
||
const [curpath, setCurpath] = useState(typeof window === 'undefined' || window.location === undefined ? "" : window.location.pathname)
|
||
|
||
// 0 = normal, 1 = just done, 2 = normal
|
||
const [savingState, setSavingState] = React.useState(0)
|
||
|
||
const [selectedResult, setSelectedResult] = React.useState({})
|
||
const [codeModalOpen, setCodeModalOpen] = React.useState(false);
|
||
|
||
const [variableAnchorEl, setVariableAnchorEl] = React.useState(null)
|
||
|
||
const [sourceValue, setSourceValue] = React.useState({})
|
||
const [destinationValue, setDestinationValue] = React.useState({})
|
||
const [conditionValue, setConditionValue] = React.useState({})
|
||
const [dragging, setDragging] = React.useState(false)
|
||
const [dragPosition, setDragPosition] = React.useState({
|
||
x: 0,
|
||
y: 0,
|
||
})
|
||
|
||
// Trigger stuff
|
||
const [selectedTrigger, setSelectedTrigger] = React.useState({});
|
||
const [selectedTriggerIndex, setSelectedTriggerIndex] = React.useState({});
|
||
const [selectedEdge, setSelectedEdge] = React.useState({});
|
||
const [selectedEdgeIndex, setSelectedEdgeIndex] = React.useState({});
|
||
|
||
|
||
const [visited, setVisited] = React.useState([]);
|
||
//const [workflow, setWorkflow] = React.useState(workflowdata);
|
||
|
||
const [apps, setApps] = React.useState([]);
|
||
const [filteredApps, setFilteredApps] = React.useState([]);
|
||
const [prioritizedApps, setPrioritizedApps] = React.useState([]);
|
||
const [firstrequest, setFirstrequest] = React.useState(true)
|
||
//const [apps, setApps] = React.useState(appdata);
|
||
//const [filteredApps, setFilteredApps] = React.useState();
|
||
|
||
const [environments, setEnvironments] = React.useState([]);
|
||
const [established, setEstablished] = React.useState(false);
|
||
|
||
const [graphSetup, setGraphSetup] = React.useState(false);
|
||
|
||
const [selectedApp, setSelectedApp] = React.useState({});
|
||
const [selectedAction, setSelectedAction] = React.useState({});
|
||
const [selectedActionName, setSelectedActionName] = React.useState({});
|
||
const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({});
|
||
|
||
const [executionRequest, setExecutionRequest] = React.useState({})
|
||
|
||
const [, setExecutingNodes] = React.useState([])
|
||
const [executionRunning, setExecutionRunning] = React.useState(false)
|
||
const [executionModalOpen, setExecutionModalOpen] = React.useState(false)
|
||
const [executionModalView, setExecutionModalView] = React.useState(0)
|
||
const [executionData, setExecutionData] = React.useState({})
|
||
|
||
const [lastSaved, setLastSaved] = React.useState(true)
|
||
|
||
const [appAdded, setAppAdded] = useState(false)
|
||
const [update, setUpdate] = useState("");
|
||
const [workflowExecutions, setWorkflowExecutions] = React.useState([]);
|
||
const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0)
|
||
|
||
const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false
|
||
//const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments
|
||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
|
||
const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"]
|
||
|
||
const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?'
|
||
useBeforeunload(() => {
|
||
if (!lastSaved) {
|
||
return unloadText
|
||
}
|
||
})
|
||
|
||
const [elements, setElements] = useState([])
|
||
const { start, stop } = useInterval({
|
||
duration: 2500,
|
||
startImmediate: false,
|
||
callback: () => {
|
||
fetchUpdates()
|
||
}
|
||
})
|
||
|
||
const getAvailableWorkflows = (trigger_index) => {
|
||
fetch(globalUrl+"/api/v1/workflows", {
|
||
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
|
||
}
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson !== undefined) {
|
||
setWorkflows(responseJson)
|
||
|
||
if (trigger_index > -1) {
|
||
var outersub = {}
|
||
const trigger = workflow.triggers[trigger_index]
|
||
if (trigger.parameters.length >= 3) {
|
||
for (var key in trigger.parameters) {
|
||
const param = trigger.parameters[key]
|
||
if (param.name === "workflow") {
|
||
const sub = responseJson.find(data => data.id === param.value)
|
||
if (sub !== undefined && subworkflow.id !== sub.id) {
|
||
setSubworkflow(sub)
|
||
outersub = sub
|
||
}
|
||
}
|
||
|
||
if (param.name === "startnode" && outersub.id !== undefined) {
|
||
console.log("SHOULD SET STARTNODE: ", outersub)
|
||
const innernode = outersub.actions.find(action => action.id === param.value)
|
||
console.log("FOUND NODE: ", innernode)
|
||
if (innernode !== undefined && subworkflowStartnode.id !== innernode.id) {
|
||
setSubworkflowStartnode(innernode)
|
||
}
|
||
/*
|
||
const sub = responseJson.find(data => data.id === param.value)
|
||
setSubworkflow(sub)
|
||
}
|
||
*/
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const generateApikey = () => {
|
||
fetch(globalUrl+"/api/v1/generateapikey", {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
setUserSettings(responseJson)
|
||
})
|
||
.catch(error => {
|
||
console.log(error)
|
||
});
|
||
}
|
||
|
||
const getSettings = () => {
|
||
fetch(globalUrl+"/api/v1/getsettings", {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.apikey === undefined || responseJson.apikey.length === 0 || responseJson.apikey === null) {
|
||
generateApikey()
|
||
}
|
||
setUserSettings(responseJson)
|
||
})
|
||
.catch(error => {
|
||
console.log(error)
|
||
});
|
||
}
|
||
|
||
const setNewAppAuth = (appAuthData) => {
|
||
console.log("DAta: ", appAuthData)
|
||
fetch(globalUrl+"/api/v1/apps/authentication", {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(appAuthData),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for setting app auth :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
alert.error("Failed to set app auth: "+responseJson.reason)
|
||
} else {
|
||
getAppAuthentication(true)
|
||
setAuthenticationModalOpen(false)
|
||
|
||
// Needs a refresh with the new authentication..
|
||
alert.success("Successfully saved new app auth")
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
})
|
||
}
|
||
|
||
const getWorkflowExecution = (id, execution_id) => {
|
||
fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.length > 0) {
|
||
// FIXME: Sort this by time
|
||
setWorkflowExecutions(responseJson)
|
||
|
||
const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search
|
||
var tmpView = new URLSearchParams(cursearch).get("execution_id")
|
||
if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) {
|
||
tmpView = execution_id
|
||
}
|
||
|
||
if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) {
|
||
const execution = responseJson.find(data => data.execution_id === tmpView)
|
||
if (execution !== null && execution !== undefined) {
|
||
setExecutionData(execution)
|
||
setExecutionModalView(1)
|
||
|
||
const newitem = removeParam("execution_id", cursearch)
|
||
props.history.push(curpath+newitem)
|
||
}
|
||
}
|
||
}
|
||
|
||
//alert.info("Loaded executions")
|
||
//setWorkflowExecutions(responseJson)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const debugView = workflowExecutions.length > 0 ?
|
||
<Draggable>
|
||
<div style={{color: "white", position: "fixed", top: appBarSize+65, left: leftBarSize+20, zIndex: 5000, minHeight: 100, padding: 15, maxHeight: 100, maxWidth: 500, overflowX: "hidden",}}>
|
||
{workflowExecutions.slice(0,15).map((data, index) => {
|
||
return (
|
||
<div key={index}>
|
||
{new Date(data.started_at*1000).toISOString()}
|
||
, {data.status}
|
||
{data.result.length > 0 ? ", "+data.result : ", "}
|
||
{data.execution_argument.length > 0 ? ", "+data.execution_argument : ", "}
|
||
<Divider style={{backgroundColor: "white"}}/>
|
||
</div>
|
||
)
|
||
return
|
||
})}
|
||
</div>
|
||
</Draggable>
|
||
: null
|
||
|
||
const fetchUpdates = () => {
|
||
fetch(globalUrl+"/api/v1/streams/results", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(executionRequest),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
stop()
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
handleUpdateResults(responseJson)
|
||
})
|
||
.catch(error => {
|
||
console.log("Error: ", error)
|
||
//alert.error(error.toString())
|
||
stop()
|
||
});
|
||
}
|
||
|
||
const abortExecution = () => {
|
||
setExecutionRunning(false)
|
||
|
||
//alert.info("Aborting execution")
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/executions/"+executionRequest.execution_id+"/abort", {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
|
||
} else {
|
||
alert.success("Execution aborted")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
// Controls the colors and direction of execution results.
|
||
// Style is in defaultCytoscapeStyle.js
|
||
const handleUpdateResults = (responseJson) => {
|
||
//console.log(responseJson)
|
||
// Loop nodes and find results
|
||
// Update on every interval? idk
|
||
|
||
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) {
|
||
setExecutionData(responseJson)
|
||
}
|
||
}
|
||
if (responseJson.execution_id !== executionRequest.execution_id) {
|
||
cy.elements().removeClass('success-highlight failure-highlight executing-highlight')
|
||
return
|
||
}
|
||
|
||
if (responseJson.results !== null && responseJson.results !== []) {
|
||
for (var key in responseJson.results) {
|
||
var item = responseJson.results[key]
|
||
var currentnode = cy.getElementById(item.action.id)
|
||
if (currentnode.length === 0) {
|
||
continue
|
||
}
|
||
|
||
currentnode = currentnode[0]
|
||
const outgoingEdges = currentnode.outgoers('edge')
|
||
const incomingEdges = currentnode.incomers('edge')
|
||
|
||
//currentnode.removeClass('success-highlight failure-highlight executing-highlight')
|
||
switch (item.status) {
|
||
case "EXECUTING":
|
||
currentnode.removeClass('not-executing-highlight')
|
||
currentnode.removeClass('success-highlight')
|
||
currentnode.removeClass('failure-highlight')
|
||
currentnode.removeClass('shuffle-hover-highlight')
|
||
currentnode.removeClass('awaiting-data-highlight')
|
||
incomingEdges.addClass('success-highlight')
|
||
currentnode.addClass('executing-highlight')
|
||
break
|
||
case "SKIPPED":
|
||
currentnode.removeClass('not-executing-highlight')
|
||
currentnode.removeClass('success-highlight')
|
||
currentnode.removeClass('failure-highlight')
|
||
currentnode.removeClass('shuffle-hover-highlight')
|
||
currentnode.removeClass('awaiting-data-highlight')
|
||
currentnode.removeClass('executing-highlight')
|
||
currentnode.addClass('skipped-highlight')
|
||
break
|
||
case "WAITING":
|
||
currentnode.removeClass('not-executing-highlight')
|
||
currentnode.removeClass('success-highlight')
|
||
currentnode.removeClass('failure-highlight')
|
||
currentnode.removeClass('shuffle-hover-highlight')
|
||
currentnode.removeClass('awaiting-data-highlight')
|
||
currentnode.addClass('executing-highlight')
|
||
|
||
if (!visited.includes(item.action.label)) {
|
||
if (executionRunning) {
|
||
//alert.show("WAITING FOR "+item.action.label+" with result "+item.result)
|
||
visited.push(item.action.label)
|
||
setVisited(visited)
|
||
}
|
||
}
|
||
|
||
// FIXME - add outgoing nodes to executing
|
||
//const outgoingNodes = outgoingEdges.find().data().target
|
||
if (outgoingEdges.length > 0) {
|
||
outgoingEdges.addClass('success-highlight')
|
||
}
|
||
break
|
||
case "SUCCESS":
|
||
currentnode.removeClass('not-executing-highlight')
|
||
currentnode.removeClass('executing-highlight')
|
||
currentnode.removeClass('failure-highlight')
|
||
currentnode.removeClass('shuffle-hover-highlight')
|
||
currentnode.removeClass('awaiting-data-highlight')
|
||
currentnode.addClass('success-highlight')
|
||
|
||
if (visited !== undefined && visited !== null && !visited.includes(item.action.label)) {
|
||
if (executionRunning) {
|
||
//alert.show("Success in node "+item.action.label)
|
||
//+" with result "+item.result)
|
||
visited.push(item.action.label)
|
||
setVisited(visited)
|
||
}
|
||
}
|
||
|
||
// FIXME - add outgoing nodes to executing
|
||
//const outgoingNodes = outgoingEdges.find().data().target
|
||
if (outgoingEdges.length > 0) {
|
||
for (var i = 0; i < outgoingEdges.length; i++) {
|
||
const edge = outgoingEdges[i]
|
||
const targetnode = cy.getElementById(edge.data().target)
|
||
if (targetnode !== undefined && !targetnode.classes().includes("success-highlight") && !targetnode.classes().includes("failure-highlight")) {
|
||
targetnode.removeClass('not-executing-highlight')
|
||
targetnode.removeClass('success-highlight')
|
||
targetnode.removeClass('shuffle-hover-highlight')
|
||
targetnode.removeClass('failure-highlight')
|
||
targetnode.removeClass('awaiting-data-highlight')
|
||
targetnode.addClass('executing-highlight')
|
||
}
|
||
}
|
||
|
||
// const outgoingEdges = currentnode.outgoers('edge')
|
||
}
|
||
break
|
||
case "FAILURE":
|
||
//When status comes as failure, allow user to start workflow execution
|
||
setExecutionRunning(false)
|
||
|
||
currentnode.removeClass('not-executing-highlight')
|
||
currentnode.removeClass('executing-highlight')
|
||
currentnode.removeClass('success-highlight')
|
||
currentnode.removeClass('awaiting-data-highlight')
|
||
currentnode.removeClass('shuffle-hover-highlight')
|
||
currentnode.addClass('failure-highlight')
|
||
|
||
if (!visited.includes(item.action.label)) {
|
||
if (!item.action.result.includes("failed condition")) {
|
||
alert.error("Error for "+item.action.label+" with result "+item.result)
|
||
}
|
||
visited.push(item.action.label)
|
||
setVisited(visited)
|
||
}
|
||
break
|
||
case "AWAITING_DATA":
|
||
currentnode.removeClass('not-executing-highlight')
|
||
currentnode.removeClass('executing-highlight')
|
||
currentnode.removeClass('success-highlight')
|
||
currentnode.removeClass('failure-highlight')
|
||
currentnode.removeClass('shuffle-hover-highlight')
|
||
currentnode.addClass('awaiting-data-highlight')
|
||
break
|
||
default:
|
||
console.log("DEFAULT?")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status == "WAITING") {
|
||
stop()
|
||
|
||
setExecutionRunning(false)
|
||
var curelements = cy.elements()
|
||
for (var i = 0; i < curelements.length; i++) {
|
||
if (curelements[i].classes().includes("executing-highlight")) {
|
||
curelements[i].removeClass("executing-highlight")
|
||
curelements[i].addClass("failure-highlight")
|
||
} else {
|
||
//curelements[i].removeClass('not-executing-highlight')
|
||
//curelements[i].removeClass('executing-highlight')
|
||
//curelements[i].removeClass('success-highlight')
|
||
//curelements[i].removeClass('awaiting-data-highlight')
|
||
//curelements[i].removeClass('failure-highlight')
|
||
}
|
||
}
|
||
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
} else if (responseJson.status === "FINISHED") {
|
||
console.log("STOPPING BECAUSE ITS OVAH!")
|
||
setExecutionRunning(false)
|
||
stop()
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
setUpdate(Math.random())
|
||
}
|
||
}
|
||
|
||
const saveWorkflow = (curworkflow) => {
|
||
var success = false
|
||
setSavingState(2)
|
||
|
||
// This might not be the right course of action, but seems logical, as items could be running already
|
||
// Makes it possible to update with a version in current render
|
||
stop()
|
||
var useworkflow = workflow
|
||
if (curworkflow !== undefined) {
|
||
useworkflow = curworkflow
|
||
} else {
|
||
//alert.info("Saving workflow")
|
||
}
|
||
|
||
var cyelements = cy.elements()
|
||
var newActions = []
|
||
var newTriggers = []
|
||
var newBranches = []
|
||
for (var key in cyelements) {
|
||
if (cyelements[key].data === undefined) {
|
||
continue
|
||
}
|
||
|
||
var type = cyelements[key].data()["type"]
|
||
if (type === undefined) {
|
||
if (cyelements[key].data().source === undefined ||
|
||
cyelements[key].data().target === undefined) {
|
||
continue
|
||
}
|
||
|
||
var parsedElement = {
|
||
id: cyelements[key].data().id,
|
||
source_id: cyelements[key].data().source,
|
||
destination_id: cyelements[key].data().target,
|
||
conditions: cyelements[key].data().conditions,
|
||
}
|
||
|
||
newBranches.push(parsedElement)
|
||
} else {
|
||
if (type === "ACTION") {
|
||
// FIXME - check whether position is new to not fuck up params etc.
|
||
var curworkflowAction = useworkflow.actions.find(a => a.id === cyelements[key].data()["id"])
|
||
if (curworkflowAction === undefined) {
|
||
curworkflowAction = cyelements[key].data()
|
||
}
|
||
|
||
curworkflowAction.position = cyelements[key].position()
|
||
|
||
// workaround to fix some edgecases
|
||
if (curworkflowAction.parameters === "" || curworkflowAction.parameters === null) {
|
||
curworkflowAction.parameters = []
|
||
}
|
||
|
||
if (curworkflowAction.example === undefined || curworkflowAction.example === "" || curworkflowAction.example === null) {
|
||
if (cyelements[key].data().example !== undefined) {
|
||
curworkflowAction.example = cyelements[key].data().example
|
||
}
|
||
}
|
||
|
||
// Override just in this place
|
||
curworkflowAction.errors = []
|
||
curworkflowAction.isValid = true
|
||
|
||
// Cleans up OpenAPI items
|
||
var newparams = []
|
||
for (var key in curworkflowAction.parameters) {
|
||
const thisitem = curworkflowAction.parameters[key]
|
||
if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) {
|
||
continue
|
||
}
|
||
|
||
newparams.push(thisitem)
|
||
}
|
||
|
||
curworkflowAction.parameters = newparams
|
||
newActions.push(curworkflowAction)
|
||
} else if (type === "TRIGGER") {
|
||
//console.log("TRIGGER")
|
||
var curworkflowTrigger = useworkflow.triggers.find(a => a.id === cyelements[key].data()["id"])
|
||
if (curworkflowTrigger === undefined) {
|
||
curworkflowTrigger = cyelements[key].data()
|
||
}
|
||
|
||
curworkflowTrigger.position = cyelements[key].position()
|
||
//console.log(curworkflowTrigger)
|
||
|
||
newTriggers.push(curworkflowTrigger)
|
||
}
|
||
}
|
||
}
|
||
|
||
useworkflow.actions = newActions
|
||
useworkflow.triggers = newTriggers
|
||
useworkflow.branches = newBranches
|
||
|
||
// Errors are backend defined
|
||
useworkflow.errors = []
|
||
|
||
setLastSaved(true)
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(useworkflow),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
setSavingState(0)
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for setting workflows :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
console.log(responseJson)
|
||
alert.error("Failed to save: "+responseJson.reason)
|
||
} else {
|
||
success = true
|
||
if (responseJson.errors !== undefined) {
|
||
//console.log(responseJson)
|
||
workflow.errors = responseJson.errors
|
||
if (responseJson.errors.length === 0) {
|
||
workflow.isValid = true
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
}
|
||
//alert.success("Successfully saved workflow")
|
||
setSavingState(1)
|
||
setTimeout(() => {
|
||
setSavingState(0)
|
||
}, 1500);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
setSavingState(0)
|
||
alert.error(error.toString())
|
||
});
|
||
|
||
return success
|
||
}
|
||
|
||
const monitorUpdates = () => {
|
||
const firstnode = cy.getElementById(workflow.start)
|
||
if (firstnode.length === 0) {
|
||
return false
|
||
}
|
||
|
||
cy.elements().removeClass('success-highlight failure-highlight executing-highlight')
|
||
firstnode[0].addClass('executing-highlight')
|
||
|
||
return true
|
||
}
|
||
|
||
const executeWorkflow = (executionArgument, startNode) => {
|
||
if (!lastSaved) {
|
||
//alert.error("You might have forgotten to save before executing.")
|
||
console.log("FIXME: Might have forgotten to save before executing.")
|
||
}
|
||
|
||
var returncheck = monitorUpdates()
|
||
if (!returncheck) {
|
||
alert.error("No startnode set.")
|
||
return
|
||
}
|
||
|
||
setVisited([])
|
||
setExecutionRequest({})
|
||
setExecutionRequestStarted(true)
|
||
stop()
|
||
|
||
var curelements = cy.elements()
|
||
for (var i = 0; i < curelements.length; i++) {
|
||
curelements[i].addClass("not-executing-highlight")
|
||
}
|
||
|
||
if (executionArgument !== undefined && executionArgument !== null && executionArgument.length > 0) {
|
||
//alert.success("Starting execution WITH an execution argument")
|
||
} else {
|
||
//alert.success("Starting execution")
|
||
}
|
||
|
||
const data = {"execution_argument": executionArgument, "start": startNode}
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/execute", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
body: JSON.stringify(data),
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
alert.error("Failed to start: "+responseJson.reason)
|
||
setExecutionRunning(false)
|
||
setExecutionRequestStarted(false)
|
||
stop()
|
||
|
||
for (var i = 0; i < curelements.length; i++) {
|
||
curelements[i].removeClass("not-executing-highlight")
|
||
}
|
||
return
|
||
} else {
|
||
setExecutionRunning(true)
|
||
setExecutionRequestStarted(false)
|
||
}
|
||
|
||
setExecutionRequest({
|
||
"execution_id": responseJson.execution_id,
|
||
"authorization": responseJson.authorization,
|
||
})
|
||
setExecutingNodes([workflow.start])
|
||
setExecutionData({})
|
||
setExecutionModalOpen(true)
|
||
setExecutionModalView(1)
|
||
start()
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
//const handleAppVersioning = (apps) => {
|
||
// var newapps = []
|
||
// for (var key in apps) {
|
||
// var item = apps[key]
|
||
// const previtem = newapps.findIndex(data => data.name === item.name)
|
||
// if (previtem === -1) {
|
||
// item["versions"] = [item.app_version]
|
||
// newapps.push(item)
|
||
// continue
|
||
// }
|
||
|
||
// // THere might be duplicates for some reason..
|
||
// if (!newapps[previtem]["versions"].includes(item.app_version)) {
|
||
// newapps[previtem]["versions"].push(item.app_version)
|
||
// }
|
||
// }
|
||
//
|
||
// // FIXME - handle this, as we can't have more than one of each :)
|
||
// //setVersionedApps(newapps)
|
||
//}
|
||
|
||
// Builtin actions that should ran in Worker and not apps
|
||
const getExtraApps = () => {
|
||
const data = [{
|
||
name: "Filter",
|
||
is_valid: true,
|
||
id: "0ca8887e-b4af-4e3e-887c-87e9d3bc3d3e",
|
||
link: "https://shuffler.io",
|
||
app_version: "1.0.0",
|
||
generated: true,
|
||
downloaded: false,
|
||
sharing: false,
|
||
verified: false,
|
||
tested: false,
|
||
owner: "",
|
||
private_id: "",
|
||
description: "Filter",
|
||
environment: "Shuffle",
|
||
small_image: "",
|
||
large_image: "",
|
||
contact_info: {name: "", url: ""},
|
||
authentication: {required: false, parameters: [],},
|
||
actions: [{
|
||
description: "Filter cases",
|
||
id: "",
|
||
name: "filter_cases",
|
||
node_type: "action",
|
||
environment: "Shuffle",
|
||
sharing: false,
|
||
private_id: "",
|
||
app_id: "",
|
||
authentication: null,
|
||
tested: true,
|
||
parameters: [{
|
||
description: "",
|
||
id: "",
|
||
name: "Field to look for",
|
||
example: "$testing_1.#.id",
|
||
value: "",
|
||
multiline: true,
|
||
action_field: "",
|
||
variant: "",
|
||
required: true,
|
||
schema: {type: "string"},
|
||
}]
|
||
}],
|
||
}]
|
||
|
||
return data
|
||
}
|
||
|
||
// This can be used to only show prioritzed ones later
|
||
// Right now, it can prioritize authenticated ones
|
||
const internalIds = [
|
||
"Shuffle Tools",
|
||
"Testing",
|
||
"Http",
|
||
]
|
||
|
||
const getAppAuthentication = (reset) => {
|
||
fetch(globalUrl+"/api/v1/apps/authentication", {
|
||
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
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.success) {
|
||
var newauth = []
|
||
for (var key in responseJson.data) {
|
||
if (responseJson.data[key].defined === false) {
|
||
continue
|
||
}
|
||
|
||
newauth.push(responseJson.data[key])
|
||
}
|
||
|
||
if (reset === true) {
|
||
console.log("APP RESET = reset cy")
|
||
cy.on('select', 'node', (e) => onNodeSelect(e, newauth))
|
||
}
|
||
setAppAuthentication(newauth)
|
||
} else {
|
||
alert.error("Failed getting authentications")
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
|
||
const getApps = () => {
|
||
fetch(globalUrl+"/api/v1/workflows/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) => {
|
||
// FIXME - handle versions on left bar
|
||
//handleAppVersioning(responseJson)
|
||
//var tmpapps = []
|
||
//tmpapps = tmpapps.concat(getExtraApps())
|
||
//tmpapps = tmpapps.concat(responseJson)
|
||
setApps(responseJson)
|
||
//getAppAuthentication()
|
||
|
||
setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated)))
|
||
setPrioritizedApps(responseJson.filter(app => internalIds.includes(app.name)))
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const getWorkflow = () => {
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key, {
|
||
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!")
|
||
window.location.pathname = "/workflows"
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
// Not sure why this is necessary.
|
||
if (responseJson.isValid === undefined) {
|
||
responseJson.isValid = true
|
||
}
|
||
if (responseJson.errors === undefined) {
|
||
responseJson.errors = []
|
||
}
|
||
|
||
setWorkflow(responseJson)
|
||
setWorkflowDone(true)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const onUnselect = (event) => {
|
||
console.time("UNSELECT")
|
||
|
||
// Attempt at rewrite of name in other actions in following nodes.
|
||
// Should probably be done in the onBlur for the textfield instead
|
||
/*
|
||
if (event.target.data().type === "ACTION") {
|
||
const nodeaction = event.target.data()
|
||
const curaction = workflow.actions.find(a => a.id === nodeaction.id)
|
||
console.log("workflowaction: ", curaction)
|
||
console.log("nodeaction: ", nodeaction)
|
||
if (nodeaction.label !== curaction.label) {
|
||
console.log("BEACH!")
|
||
|
||
var params = []
|
||
const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_")
|
||
for (var actionkey in workflow.actions) {
|
||
if (workflow.actions[actionkey].id === curaction.id) {
|
||
continue
|
||
}
|
||
|
||
for (var paramkey in workflow.actions[actionkey].parameters) {
|
||
const param = workflow.actions[actionkey].parameters[paramkey]
|
||
if (param.value === null || param.value === undefined || !param.value.includes("$")) {
|
||
continue
|
||
}
|
||
|
||
const innername = param.value.toLowerCase().replace(" ", "_")
|
||
if (innername.includes(fixedName)) {
|
||
//workflow.actions[actionkey].parameters[paramkey].replace(
|
||
//console.log("FOUND!: ", innername)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
*/
|
||
|
||
// FIXME - check if they have value before overriding like this for no reason.
|
||
// Would save a lot of time (400~ ms -> 30ms)
|
||
//console.log("ACTION: ", selectedAction)
|
||
//console.log("APP: ", selectedApp)
|
||
setSelectedAction({})
|
||
setSelectedApp({})
|
||
setSelectedTrigger({})
|
||
//setSelectedEdge({})
|
||
|
||
// setSelectedTriggerIndex(-1)
|
||
//setSelectedActionEnvironment({})
|
||
setSelectedEdge({})
|
||
//setTriggerAuthentication({})
|
||
//setSelectedTriggerIndex(-1)
|
||
//setTriggerFolders([])
|
||
|
||
// Can be used for right side view
|
||
setRightSideBarOpen(false)
|
||
console.timeEnd("UNSELECT")
|
||
}
|
||
|
||
const onEdgeSelect = (event) => {
|
||
setRightSideBarOpen(true)
|
||
setLastSaved(false)
|
||
|
||
/*
|
||
// Used to not be able to edit trigger-based branches.
|
||
const triggercheck = workflow.triggers.find(trigger => trigger.id === event.target.data()["source"])
|
||
if (triggercheck === undefined) {
|
||
*/
|
||
setSelectedEdgeIndex(workflow.branches.findIndex(data => data.id === event.target.data()["id"]))
|
||
setSelectedEdge(event.target.data())
|
||
|
||
/*
|
||
} else {
|
||
//alert.info("Can't edit branches from triggers")
|
||
console.log("IN HERE: !", triggercheck)
|
||
}
|
||
*/
|
||
|
||
setSelectedAction({})
|
||
setSelectedTrigger({})
|
||
}
|
||
|
||
const onNodeSelect = (event, newAppAuth) => {
|
||
const data = event.target.data()
|
||
setLastSaved(false)
|
||
const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id)
|
||
console.log("NODE: ", data)
|
||
console.log("BRANCHES: ", branch)
|
||
|
||
if (data.type === "ACTION") {
|
||
// FIXME - unselect
|
||
//console.log(cy.elements('[_id!="${data._id}"]`))
|
||
// Does it choose the wrong action?
|
||
var curaction = workflow.actions.find(a => a.id === data.id)
|
||
if (!curaction || curaction === undefined) {
|
||
//alert.error("Action not found. Please remake it.")
|
||
return
|
||
}
|
||
|
||
const curapp = apps.find(a => a.name === curaction.app_name && a.app_version === curaction.app_version)
|
||
if (!curapp || curapp === undefined) {
|
||
alert.error("App "+curaction.app_name+" not found. Did someone delete it?")
|
||
//return
|
||
} else {
|
||
setRequiresAuthentication(curapp.authentication.required && curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null)
|
||
if (curapp.authentication.required) {
|
||
// Setup auth here :)
|
||
const authenticationOptions = []
|
||
var findAuthId = ""
|
||
if (curaction.authentication_id !== null && curaction.authentication_id !== undefined && curaction.authentication_id.length > 0) {
|
||
findAuthId = curaction.authentication_id
|
||
}
|
||
|
||
var tmpAuth = JSON.parse(JSON.stringify(newAppAuth))
|
||
console.log("Checking authentication: ", tmpAuth)
|
||
for (var key in tmpAuth) {
|
||
var item = tmpAuth[key]
|
||
|
||
const newfields = {}
|
||
for (var filterkey in item.fields) {
|
||
newfields[item.fields[filterkey].key] = item.fields[filterkey].value
|
||
}
|
||
|
||
item.fields = newfields
|
||
if (item.app.name === curapp.name) {
|
||
authenticationOptions.push(item)
|
||
if (item.id === findAuthId) {
|
||
curaction.selectedAuthentication = item
|
||
}
|
||
}
|
||
}
|
||
|
||
curaction.authentication = authenticationOptions
|
||
console.log("Authentication: ", authenticationOptions)
|
||
if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "") {
|
||
curaction.selectedAuthentication = {}
|
||
}
|
||
} else {
|
||
curaction.authentication = []
|
||
curaction.authentication_id = ""
|
||
curaction.selectedAuthentication = {}
|
||
}
|
||
|
||
setSelectedApp(curapp)
|
||
}
|
||
|
||
if (environments !== undefined && environments !== null) {
|
||
var env = environments.find(a => a.Name === curaction.environment)
|
||
if (!env || env === undefined) {
|
||
env = environments[defaultEnvironmentIndex]
|
||
}
|
||
|
||
setSelectedActionEnvironment(env)
|
||
}
|
||
|
||
setSelectedActionName(curaction.name)
|
||
setSelectedAction(curaction)
|
||
|
||
/*
|
||
var params = []
|
||
const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_")
|
||
for (var actionkey in workflow.actions) {
|
||
if (workflow.actions[actionkey].id === curaction.id) {
|
||
continue
|
||
}
|
||
|
||
for (var paramkey in workflow.actions[actionkey].parameters) {
|
||
const param = workflow.actions[actionkey].parameters[paramkey]
|
||
if (param.value === null || param.value === undefined || !param.value.includes("$")) {
|
||
continue
|
||
}
|
||
|
||
const innername = param.value.toLowerCase().replace(" ", "_")
|
||
if (innername.includes(fixedName)) {
|
||
console.log("FOUND!: ", innername)
|
||
}
|
||
}
|
||
}
|
||
*/
|
||
} else if (data.type === "TRIGGER") {
|
||
//console.log("Should handle trigger "+data.triggertype)
|
||
//console.log(data)
|
||
const trigger_index = workflow.triggers.findIndex(a => a.id === data.id)
|
||
setSelectedTriggerIndex(trigger_index)
|
||
setSelectedTrigger(data)
|
||
setSelectedActionName(data.name)
|
||
setSelectedActionEnvironment(data.env)
|
||
|
||
if (data.app_name === "Shuffle Workflow") {
|
||
getAvailableWorkflows(trigger_index)
|
||
getSettings()
|
||
}
|
||
} else {
|
||
alert.error("Can't handle "+data.type)
|
||
}
|
||
|
||
setRightSideBarOpen(true)
|
||
}
|
||
|
||
// Checks for errors in edges when they're added
|
||
const onEdgeAdded = (event) => {
|
||
setLastSaved(false)
|
||
const edge = event.target.data()
|
||
//console.log(workflow.branches)
|
||
|
||
// Check if:
|
||
// dest == source && source == dest
|
||
// dest == dest && source == source
|
||
// backend: check all children? to stop recursion
|
||
var found = false
|
||
for (var key in workflow.branches) {
|
||
if (workflow.branches[key].destination_id === edge.source && workflow.branches[key].source_id === edge.target) {
|
||
alert.error("A branch in the opposite direction already exists")
|
||
event.target.remove()
|
||
found = true
|
||
break
|
||
} else if (workflow.branches[key].destination_id === edge.target && workflow.branches[key].source_id === edge.source) {
|
||
console.log(edge.source)
|
||
alert.error("That branch already exists")
|
||
event.target.remove()
|
||
found = true
|
||
break
|
||
} else if (edge.target === workflow.start) {
|
||
var targetnode = workflow.triggers.findIndex(data => data.id === edge.source)
|
||
if (targetnode === -1) {
|
||
alert.error("Can't make arrow to starting node")
|
||
event.target.remove()
|
||
found = true
|
||
break
|
||
}
|
||
} else if (edge.source === workflow.branches[key].source_id) {
|
||
// FIXME: Verify multi-target for triggers
|
||
// 1. Check if destination exists
|
||
// 2. Check if source is a trigger
|
||
// targetnode = workflow.triggers.findIndex(data => data.id === edge.source)
|
||
// console.log("Destination: ", edge.target)
|
||
// console.log("CHECK SOURCE IF ITS A TRIGGER: ", targetnode)
|
||
// if (targetnode !== -1) {
|
||
// alert.error("Triggers can only target one target (startnode)")
|
||
// event.target.remove()
|
||
// found = true
|
||
// break
|
||
// }
|
||
} else {
|
||
// Find the targetnode and check if its a trigger
|
||
// FIXME - do this for both actions and other types?
|
||
targetnode = workflow.triggers.findIndex(data => data.id === edge.target)
|
||
if (targetnode !== -1) {
|
||
if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow") {
|
||
} else {
|
||
alert.error("Can't have triggers as target of branch")
|
||
event.target.remove()
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var newbranch = {
|
||
"source_id": edge.source,
|
||
"destination_id": edge.target,
|
||
"id_": edge.id,
|
||
"id": edge.id,
|
||
"hasErrors": false,
|
||
}
|
||
|
||
if (!found) {
|
||
newbranch["hasErrors"] = false
|
||
|
||
workflow.branches.push(newbranch)
|
||
setWorkflow(workflow)
|
||
}
|
||
}
|
||
|
||
const onNodeAdded = (event) => {
|
||
setLastSaved(false)
|
||
const node = event.target
|
||
|
||
if (node.isNode() && cy.nodes().size() === 1) {
|
||
//setStartNode(node.data('id'))
|
||
workflow.start = node.data('id')
|
||
setWorkflow(workflow)
|
||
} else {
|
||
if (workflow.actions === null) {
|
||
return
|
||
}
|
||
|
||
// Remove bad startnode
|
||
const startnode_exists = false
|
||
for (var key in workflow.actions) {
|
||
const action = workflow.actions[key]
|
||
if (action.isStartNode && workflow.start !== action.id) {
|
||
action.isStartNode = false
|
||
}
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
}
|
||
}
|
||
|
||
const onEdgeRemoved = (event) => {
|
||
setLastSaved(false)
|
||
const edge = event.target
|
||
|
||
workflow.branches = workflow.branches.filter(a => a.id !== edge.data().id)
|
||
setWorkflow(workflow)
|
||
|
||
// trigger as source check
|
||
const indexcheck = workflow.triggers.findIndex(data => edge.data()["source"] === data.id)
|
||
if (indexcheck !== -1) {
|
||
//alert.error("Can't remove edge from a trigger")
|
||
console.log("Shouldnt remove edge from trigger")
|
||
//const edgeToBeAdded = {
|
||
// group: "edges",
|
||
// data: newcybranch,
|
||
//}
|
||
}
|
||
}
|
||
|
||
const onNodeRemoved = (event) => {
|
||
const node = event.target
|
||
const data = node.data()
|
||
setLastSaved(false)
|
||
|
||
//var currentnode = cy.getElementById(data.id)
|
||
//if (currentnode.length === 0) {
|
||
//}
|
||
if (workflow.start === data.id && workflow.actions.length > 1) {
|
||
// FIXME - should check branches connected to startnode, as picking random
|
||
// might just be confusing
|
||
cy.nodes().some(function( ele ) {
|
||
if (ele.id() !== workflow.start && ele.data()["label"] !== undefined) {
|
||
alert.success("Changed startnode to "+ele.data()["label"])
|
||
ele.data("isStartNode", true)
|
||
workflow.start = ele.id()
|
||
//throw BreakException
|
||
return false
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
workflow.actions = workflow.actions.filter(a => a.id !== data.id)
|
||
workflow.triggers = workflow.triggers.filter(a => a.id !== data.id)
|
||
|
||
setWorkflow(workflow)
|
||
if (data.type === "TRIGGER") {
|
||
saveWorkflow(workflow)
|
||
}
|
||
}
|
||
|
||
var previouskey = 0
|
||
const handleKeyDown = (event) => {
|
||
// SHIFT = 16
|
||
// CTRL = 17
|
||
//console.log(event.keyCode)
|
||
switch( event.keyCode ) {
|
||
case 27:
|
||
console.log("ESCAPE")
|
||
break;
|
||
case 46:
|
||
//removeNode()
|
||
console.log("DELETE")
|
||
break;
|
||
case 38:
|
||
console.log("UP")
|
||
break;
|
||
case 37:
|
||
console.log("LEFT")
|
||
break;
|
||
case 40:
|
||
console.log("DOWN")
|
||
break;
|
||
case 39:
|
||
console.log("RIGHT")
|
||
break;
|
||
case 90:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+Z")
|
||
}
|
||
break;
|
||
case 67:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+C")
|
||
}
|
||
break;
|
||
case 86:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+V")
|
||
}
|
||
break;
|
||
case 88:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+V")
|
||
}
|
||
break;
|
||
case 83:
|
||
if (previouskey === 17) {
|
||
event.preventDefault()
|
||
saveWorkflow()
|
||
}
|
||
break;
|
||
case 70:
|
||
//if (previouskey === 17) {
|
||
// event.preventDefault()
|
||
// cy.fit(null, 50)
|
||
//}
|
||
//break;
|
||
case 65:
|
||
// As a poweruser myself, I found myself hitting this a few
|
||
// too many times to just edit text. Need a better bind
|
||
//
|
||
//if (previouskey === 17) {
|
||
// event.preventDefault()
|
||
// if (executionRunning || executionRequestStarted) {
|
||
// abortExecution()
|
||
// } else {
|
||
// executeWorkflow()
|
||
// }
|
||
// cy.fit(null, 50)
|
||
//}
|
||
break;
|
||
default:
|
||
//console.log(event.keyCode)
|
||
break;
|
||
}
|
||
|
||
previouskey = event.keyCode
|
||
}
|
||
|
||
const registerKeys = () => {
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
}
|
||
|
||
const getEnvironments = () => {
|
||
fetch(globalUrl+"/api/v1/getenvironments", {
|
||
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!")
|
||
if (isCloud) {
|
||
setEnvironments({"name": "Cloud", "type": "cloud"})
|
||
} else {
|
||
setEnvironments({"name": "Onprem", "type": "onprem"})
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
var found = false
|
||
var showEnvCnt = 0
|
||
for (var key in responseJson) {
|
||
if (responseJson[key].default) {
|
||
setDefaultEnvironmentIndex(key)
|
||
found = true
|
||
}
|
||
|
||
if (responseJson[key].archived === false) {
|
||
showEnvCnt += 1
|
||
}
|
||
}
|
||
|
||
if (showEnvCnt > 1) {
|
||
setShowEnvironment(true)
|
||
}
|
||
|
||
if (!found) {
|
||
for (var key in responseJson) {
|
||
if (!responseJson[key].archived) {
|
||
setDefaultEnvironmentIndex(key)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
setEnvironments(responseJson)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (firstrequest) {
|
||
setFirstrequest(false)
|
||
getWorkflow()
|
||
getApps()
|
||
getAppAuthentication()
|
||
getEnvironments()
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
getAvailableWorkflows(-1)
|
||
getSettings()
|
||
|
||
const cursearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search
|
||
const tmpView = new URLSearchParams(cursearch).get("view")
|
||
if (tmpView !== undefined && tmpView !== null && tmpView === "executions") {
|
||
setExecutionModalOpen(true)
|
||
|
||
const newitem = removeParam("view", cursearch)
|
||
props.history.push(curpath+newitem)
|
||
}
|
||
return
|
||
}
|
||
|
||
// App length necessary cus of cy initialization
|
||
if (elements.length === 0 && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) {
|
||
setGraphSetup(true)
|
||
setupGraph()
|
||
} else if (!established && cy !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0){
|
||
setEstablished(true)
|
||
cy.edgehandles({
|
||
handleNodes: (el) => el.isNode(),
|
||
preview: false,
|
||
toggleOffOnLeave: false,
|
||
loopAllowed: function( node ){
|
||
return false;
|
||
},
|
||
})
|
||
|
||
cy.fit(null, 200)
|
||
|
||
cy.on('select', 'node', (e) => onNodeSelect(e, appAuthentication))
|
||
cy.on('select', 'edge', (e) => onEdgeSelect(e))
|
||
cy.on('unselect', (e) => onUnselect(e))
|
||
|
||
cy.on('add', 'node', (e) => onNodeAdded(e))
|
||
cy.on('add', 'edge', (e) => onEdgeAdded(e))
|
||
cy.on('remove', 'node', (e) => onNodeRemoved(e))
|
||
cy.on('remove', 'edge', (e) => onEdgeRemoved(e))
|
||
|
||
cy.on('mouseover', 'edge', (e) => onEdgeHover(e))
|
||
cy.on('mouseout', 'edge', (e) => onEdgeHoverOut(e))
|
||
cy.on('mouseover', 'node', (e) => onNodeHover(e))
|
||
cy.on('mouseout', 'node', (e) => onNodeHoverOut(e))
|
||
|
||
//cy.on('mouseover', 'node', () => $(targetElement).addClass('mouseover'));
|
||
|
||
//cy.on('cxttapstart', 'node', (e) => edgeHandler.start(e.target))
|
||
//cy.on('cxttapend', 'node', (e) => edgeHandler.stop())
|
||
//cy.on('cxtdragover', 'node', (e) => edgeHandler.preview(e.target))
|
||
//cy.on('cxtdragout', 'node', (e) => edgeHandler.unpreview(e.target))
|
||
|
||
// RIGHT HERE..?
|
||
// This is wrong sometimes.. I'm mad
|
||
document.title = "Workflow - "+workflow.name
|
||
registerKeys()
|
||
//setStartNode(workflow.start)
|
||
} else if (established) {
|
||
//console.log("established - should fix colors of things")
|
||
//console.log(cy.elements())
|
||
}
|
||
})
|
||
|
||
var previousnodecolor = ""
|
||
//var previousedgecolor = ""
|
||
const animationDuration = 150
|
||
const onNodeHoverOut = (event) => {
|
||
event.target.animate({
|
||
style: {
|
||
"border-width": "1px",
|
||
}
|
||
}, {
|
||
duration: animationDuration,
|
||
})
|
||
}
|
||
|
||
const onNodeHover = (event) => {
|
||
event.target.animate({
|
||
style: {
|
||
"border-width": "5px",
|
||
"border-opacity": ".7",
|
||
}
|
||
}, {
|
||
duration: animationDuration,
|
||
})
|
||
|
||
previousnodecolor = event.target.style("border-color")
|
||
}
|
||
|
||
const onEdgeHoverOut = (event) => {
|
||
event.target.removeStyle()
|
||
}
|
||
|
||
// This is here to have a proper transition for lines
|
||
const onEdgeHover = (event) => {
|
||
const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color")
|
||
const targetcolor = cy.getElementById(event.target.data("target")).style("border-color")
|
||
event.target.animate({
|
||
style: {
|
||
"line-fill": "linear-gradient",
|
||
'target-arrow-color': targetcolor,
|
||
"line-gradient-stop-colors": [sourcecolor, targetcolor],
|
||
"line-gradient-stop-positions": [0, 1],
|
||
},
|
||
duration: 0,
|
||
})
|
||
}
|
||
|
||
|
||
const setupGraph = () => {
|
||
const actions = workflow.actions.map(action => {
|
||
const node = {}
|
||
node.position = action.position
|
||
node.data = action
|
||
|
||
node.data._id = action["id"]
|
||
node.data.type = "ACTION"
|
||
node.isStartNode = action["id"] === workflow.start
|
||
|
||
var example = ""
|
||
if (action.example !== undefined && action.example !== null && action.example.length > 0) {
|
||
example = action.example
|
||
}
|
||
|
||
node.data.example = example
|
||
|
||
return node;
|
||
})
|
||
|
||
const triggers = workflow.triggers.map(trigger => {
|
||
const node = {}
|
||
node.position = trigger.position
|
||
node.data = trigger
|
||
|
||
node.data._id = trigger["id"]
|
||
node.data.type = "TRIGGER"
|
||
|
||
return node;
|
||
})
|
||
|
||
// FIXME - tmp branch update
|
||
var insertedNodes = [].concat(actions, triggers)
|
||
const edges = workflow.branches.map((branch, index) => {
|
||
//workflow.branches[index].conditions = [{
|
||
|
||
const edge = { };
|
||
var conditions = workflow.branches[index].conditions
|
||
if (conditions === undefined || conditions === null) {
|
||
conditions = []
|
||
}
|
||
|
||
var label = ""
|
||
if (conditions.length === 1) {
|
||
label = conditions.length+" condition"
|
||
} else if (conditions.length > 1) {
|
||
label = conditions.length+" conditions"
|
||
}
|
||
|
||
edge.data = {
|
||
id: branch.id,
|
||
_id: branch.id,
|
||
source: branch.source_id,
|
||
target: branch.destination_id,
|
||
label: label,
|
||
conditions: conditions,
|
||
hasErrors: branch.has_errors
|
||
};
|
||
return edge;
|
||
})
|
||
|
||
setWorkflow(workflow)
|
||
|
||
// Verifies if a branch is valid and skips others
|
||
var newedges = []
|
||
for (var key in edges) {
|
||
var item = edges[key]
|
||
|
||
const sourcecheck = insertedNodes.find(data => data.data.id === item.data.source)
|
||
const destcheck = insertedNodes.find(data => data.data.id === item.data.target)
|
||
if (sourcecheck === undefined || destcheck === undefined) {
|
||
continue
|
||
}
|
||
|
||
newedges.push(item)
|
||
}
|
||
|
||
insertedNodes = insertedNodes.concat(newedges)
|
||
setElements(insertedNodes)
|
||
}
|
||
|
||
const removeNode = () => {
|
||
setSelectedApp({})
|
||
setSelectedAction({})
|
||
setSelectedActionName("")
|
||
|
||
const selectedNode = cy.$(':selected')
|
||
if (selectedNode.data() === undefined) {
|
||
return
|
||
}
|
||
|
||
if (selectedNode.data().type === "TRIGGER") {
|
||
console.log("Should remove trigger!")
|
||
console.log(selectedNode.data())
|
||
const triggerindex = workflow.triggers.findIndex(data => data.id === selectedNode.data().id)
|
||
setSelectedTriggerIndex(triggerindex)
|
||
if (selectedNode.data().trigger_type === "SCHEDULE") {
|
||
setSelectedTrigger(selectedNode.data())
|
||
stopSchedule(selectedNode.data(), triggerindex)
|
||
} else if (selectedNode.data().trigger_type === "WEBHOOK") {
|
||
setSelectedTrigger(selectedNode.data())
|
||
deleteWebhook(selectedNode.data(), triggerindex)
|
||
} else if (selectedNode.data().trigger_type === "EMAIL") {
|
||
setSelectedTrigger(selectedNode.data())
|
||
stopMailSub(selectedTrigger, triggerindex)
|
||
}
|
||
|
||
}
|
||
|
||
selectedNode.remove()
|
||
setSelectedTrigger({})
|
||
setSelectedTriggerIndex({})
|
||
}
|
||
|
||
const stopSchedule = (trigger, triggerindex) => {
|
||
alert.info("Stopping schedule")
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule/"+trigger.id, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
// No matter what, it's being stopped.
|
||
if (!responseJson.success) {
|
||
alert.WARNING("Failed to stop schedule: " + responseJson.reason)
|
||
} else {
|
||
alert.success("Successfully stopped schedule")
|
||
}
|
||
|
||
workflow.triggers[triggerindex].status = "stopped"
|
||
trigger.status = "stopped"
|
||
setSelectedTrigger(trigger)
|
||
setWorkflow(workflow)
|
||
saveWorkflow(workflow)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
//const submitSchedule = (id, name, frequency, executionArg) => {
|
||
const submitSchedule = (trigger, triggerindex) => {
|
||
//const cronSplit = workflow.triggers[triggerindex].parameters[0].value.split("*")
|
||
//if (cronSplit.length <= 5 || cronSplit.length > 6) {
|
||
// alert.error("Error: Bad cron, example run every 1 minute: */1 * * * *")
|
||
// return
|
||
//}
|
||
|
||
if (trigger.name.length <= 0) {
|
||
alert.error("Error: name can't be empty")
|
||
return
|
||
}
|
||
|
||
alert.info("Attempting to create schedule with name " + trigger.name)
|
||
const data = {
|
||
"name": trigger.name,
|
||
"frequency": workflow.triggers[triggerindex].parameters[0].value,
|
||
"execution_argument": workflow.triggers[triggerindex].parameters[1].value,
|
||
"environment": workflow.triggers[triggerindex].environment,
|
||
"id": trigger.id,
|
||
}
|
||
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(data),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
alert.error("Failed to set schedule: " + responseJson.reason)
|
||
} else {
|
||
alert.success("Successfully created schedule")
|
||
workflow.triggers[triggerindex].status = "running"
|
||
trigger.status = "running"
|
||
setSelectedTrigger(trigger)
|
||
setWorkflow(workflow)
|
||
console.log("Should set the status to running and save")
|
||
saveWorkflow(workflow)
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const appViewStyle = {
|
||
marginLeft: 5,
|
||
marginRight: 5,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
height: "100%",
|
||
}
|
||
|
||
|
||
const paperAppStyle = {
|
||
borderRadius: borderRadius,
|
||
minHeight: 100,
|
||
maxHeight: 100,
|
||
minWidth: "100%",
|
||
maxWidth: "100%",
|
||
marginTop: "5px",
|
||
color: "white",
|
||
backgroundColor: surfaceColor,
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
}
|
||
|
||
const paperVariableStyle = {
|
||
borderRadius: borderRadius,
|
||
minHeight: 50,
|
||
maxHeight: 50,
|
||
minWidth: "100%",
|
||
maxWidth: "100%",
|
||
marginTop: "5px",
|
||
color: "white",
|
||
backgroundColor: surfaceColor,
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
}
|
||
|
||
const VariablesView = () => {
|
||
const [open, setOpen] = React.useState(false);
|
||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||
|
||
const menuClick = (event) => {
|
||
setOpen(!open)
|
||
setAnchorEl(event.currentTarget);
|
||
}
|
||
|
||
const deleteVariable = (variableName) => {
|
||
workflow.workflow_variables = workflow.workflow_variables.filter(data => data.name !== variableName)
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
const deleteExecutionVariable = (variableName) => {
|
||
workflow.execution_variables = workflow.execution_variables.filter(data => data.name !== variableName)
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
const variableScrollStyle = {
|
||
margin: 15,
|
||
overflow: "scroll",
|
||
height: "66vh",
|
||
overflowX: "auto",
|
||
overflowY: "auto",
|
||
flex: "10",
|
||
}
|
||
|
||
return (
|
||
<div style={appViewStyle}>
|
||
<div style={variableScrollStyle}>
|
||
What are <a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#workflow_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>WORKFLOW variables?</a>
|
||
{workflow.workflow_variables === null ?
|
||
null : workflow.workflow_variables.map(variable=> {
|
||
return (
|
||
<div key={variable.name} >
|
||
<Paper square style={paperVariableStyle} onClick={() => {
|
||
}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}} />
|
||
<div style={{display: "flex", width: "100%"}}>
|
||
<div style={{flex: "10", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}} onClick={() => {
|
||
setNewVariableName(variable.name)
|
||
setNewVariableDescription(variable.description)
|
||
setNewVariableValue(variable.value)
|
||
setVariablesModalOpen(true)}}>
|
||
Name: {variable.name}
|
||
</div>
|
||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||
<IconButton
|
||
aria-label="more"
|
||
aria-controls="long-menu"
|
||
aria-haspopup="true"
|
||
onClick={menuClick}
|
||
style={{color: "white"}}
|
||
>
|
||
<MoreVertIcon />
|
||
</IconButton>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={anchorEl}
|
||
keepMounted
|
||
open={open}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setOpen(false)
|
||
setAnchorEl(null)
|
||
}}
|
||
>
|
||
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
setOpen(false)
|
||
setNewVariableName(variable.name)
|
||
setNewVariableDescription(variable.description)
|
||
setNewVariableValue(variable.value)
|
||
setVariablesModalOpen(true)
|
||
}} key={"Edit"}>{"Edit"}</MenuItem>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
deleteVariable(variable.name)
|
||
setOpen(false)
|
||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
</div>
|
||
</Paper>
|
||
</div>
|
||
)
|
||
})}
|
||
<div style={{flex: "1"}}>
|
||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
|
||
setVariablesModalOpen(true)
|
||
setLastSaved(false)
|
||
}}>New workflow variable</Button>
|
||
</div>
|
||
<Divider style={{marginBottom: 20, marginTop: 20, height: 1, width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
What are <a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>EXECUTION variables?</a>
|
||
{workflow.execution_variables === null || workflow.execution_variables === undefined ?
|
||
null : workflow.execution_variables.map(variable=> {
|
||
return (
|
||
<div>
|
||
<Paper square style={paperVariableStyle} onClick={() => {
|
||
}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}} />
|
||
<div style={{display: "flex", width: "100%"}}>
|
||
<div style={{flex: "10", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}} onClick={() => {
|
||
setNewVariableName(variable.name)
|
||
setExecutionVariablesModalOpen(true)}}>
|
||
Name: {variable.name}
|
||
</div>
|
||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||
<IconButton
|
||
aria-label="more"
|
||
aria-controls="long-menu"
|
||
aria-haspopup="true"
|
||
onClick={menuClick}
|
||
style={{color: "white"}}
|
||
>
|
||
<MoreVertIcon />
|
||
</IconButton>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={anchorEl}
|
||
keepMounted
|
||
open={open}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setOpen(false)
|
||
setAnchorEl(null)
|
||
}}
|
||
>
|
||
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
setOpen(false)
|
||
setNewVariableName(variable.name)
|
||
setExecutionVariablesModalOpen(true)
|
||
}} key={"Edit"}>{"Edit"}</MenuItem>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
deleteExecutionVariable(variable.name)
|
||
setOpen(false)
|
||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
</div>
|
||
</Paper>
|
||
</div>
|
||
)
|
||
})}
|
||
<div style={{flex: "1"}}>
|
||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
|
||
setExecutionVariablesModalOpen(true)
|
||
setLastSaved(false)
|
||
}}>New execution variable</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const curTab = 0
|
||
const handleSetTab = (event, newValue) => {
|
||
setCurrentView(newValue)
|
||
}
|
||
|
||
const HandleLeftView = () => {
|
||
// Defaults to apps.
|
||
var thisview = <AppView />
|
||
if (currentView === 1) {
|
||
thisview = <TriggersView />
|
||
} else if (currentView === 2) {
|
||
thisview = <VariablesView />
|
||
}
|
||
|
||
const tabStyle = {
|
||
maxWidth: leftBarSize/3,
|
||
minWidth: leftBarSize/3,
|
||
flex: 1,
|
||
textTransform: "none",
|
||
}
|
||
|
||
const iconStyle = {
|
||
marginTop: 3,
|
||
marginRight: 5,
|
||
}
|
||
|
||
return(
|
||
<div>
|
||
<div style={{minHeight: bodyHeight-appBarSize-50, maxHeight: bodyHeight-appBarSize-50}}>
|
||
{thisview}
|
||
</div>
|
||
<Divider style={{backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<Tabs
|
||
value={currentView}
|
||
indicatorColor="primary"
|
||
onChange={handleSetTab}
|
||
aria-label="Left sidebar tab"
|
||
>
|
||
<Tab label={
|
||
<Grid container direction="row" alignItems="center">
|
||
<Grid item>
|
||
<AppsIcon style={iconStyle} />
|
||
</Grid>
|
||
<Grid item>
|
||
Apps
|
||
</Grid>
|
||
</Grid>
|
||
} style={tabStyle} />
|
||
<Tab label={
|
||
<Grid container direction="row" alignItems="center">
|
||
<Grid item>
|
||
<ScheduleIcon style={iconStyle} />
|
||
</Grid>
|
||
<Grid item>
|
||
Triggers
|
||
</Grid>
|
||
</Grid>
|
||
} style={tabStyle} />
|
||
<Tab label={
|
||
<Grid container direction="row" alignItems="center">
|
||
<Grid item>
|
||
<FavoriteBorderIcon style={iconStyle} />
|
||
</Grid>
|
||
<Grid item>
|
||
Variables
|
||
</Grid>
|
||
</Grid>
|
||
}style={tabStyle} />
|
||
</Tabs>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const triggers = [{
|
||
"name": "Webhook",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"trigger_type": "WEBHOOK",
|
||
"errors": null,
|
||
"large_image": 'data:image/png;base64,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',
|
||
"is_valid": true,
|
||
"label": "Webhook",
|
||
"environment": "onprem",
|
||
"description": "Simple HTTP webhook",
|
||
"long_description": "Execute a workflow with an unauthicated POST request",
|
||
},
|
||
{
|
||
"name": "Shuffle Workflow",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"trigger_type": "SUBFLOW",
|
||
"errors": null,
|
||
"large_image": 'data:image/png;base64,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',
|
||
"is_valid": true,
|
||
"label": "Subflow",
|
||
"environment": "onprem",
|
||
"description": "Control another workflow",
|
||
"long_description": "Execute another workflow from this workflow",
|
||
},
|
||
{
|
||
"name": "User Input",
|
||
"type": "TRIGGER",
|
||
"status": "running",
|
||
"large_image": "data:image/png;base64,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",
|
||
"description": "Wait for user input",
|
||
"trigger_type": "USERINPUT",
|
||
"errors": null,
|
||
"is_valid": true,
|
||
"label": "User input",
|
||
"environment": "cloud",
|
||
"long_description": "Take user input to continue execution",
|
||
},
|
||
{
|
||
"name": "Schedule",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"description": "Schedule execution time",
|
||
"trigger_type": "SCHEDULE",
|
||
"errors": null,
|
||
"large_image": "data:image/png;base64,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",
|
||
"label": "Schedule",
|
||
"is_valid": true,
|
||
"environment": "onprem",
|
||
"long_description": "Create a schedule based on cron",
|
||
},
|
||
{
|
||
"name": "Email",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"description": "Add your email provider",
|
||
"trigger_type": "EMAIL",
|
||
"errors": null,
|
||
"is_valid": true ? false : cloudSyncEnabled,
|
||
"label": "Email",
|
||
"environment": "cloud",
|
||
"large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoM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',
|
||
"long_description": "Execute a workflow when you get an email",
|
||
}]
|
||
|
||
const TriggersView = () => {
|
||
const triggersViewStyle = {
|
||
marginLeft: "10px",
|
||
marginRight: "10px",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}
|
||
|
||
// Predefined hurr
|
||
|
||
return (
|
||
<div style={triggersViewStyle}>
|
||
<div style={appScrollStyle}>
|
||
{triggers.map((trigger, index) => {
|
||
var imageline = trigger.large_image.length === 0 ?
|
||
<img alt="" style={{width: "80px", pointerEvents: "none", }} />
|
||
:
|
||
<img alt="" src={trigger.large_image} style={{width: 80, height: 80, pointerEvents: "none", }} />
|
||
|
||
const color = trigger.is_valid ? "green" : "orange"
|
||
return(
|
||
<Draggable
|
||
key={index}
|
||
onDrag={(e) => {handleTriggerDrag(e, trigger)}}
|
||
onStop={(e) => {handleDragStop(e)}}
|
||
dragging={false}
|
||
position={{
|
||
x: 0,
|
||
y: 0,
|
||
}}
|
||
>
|
||
<Paper square style={paperAppStyle} onClick={() => {}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: color, marginRight: "5px"}}>
|
||
</div>
|
||
<Grid container style={{margin: "10px 10px 10px 10px", flex: "10"}}>
|
||
<Grid item>
|
||
<ButtonBase>
|
||
{imageline}
|
||
</ButtonBase>
|
||
</Grid>
|
||
<Grid style={{display: "flex", flexDirection: "column", marginLeft: "20px"}}>
|
||
<Grid item style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "0px", marginTop: "10px"}}>{trigger.name}</h3>
|
||
</Grid>
|
||
<Grid item style={{flex: "1"}}>
|
||
{trigger.description}
|
||
</Grid>
|
||
</Grid>
|
||
</Grid>
|
||
</Paper>
|
||
</Draggable>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
var newNodeId = ""
|
||
const handleTriggerDrag = (e, data) => {
|
||
const cycontainer = cy.container()
|
||
// Chrome lol
|
||
//if (e.srcElement !== undefined && e.srcElement.localName === "canvas") {
|
||
if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) {
|
||
if (newNodeId.length > 0) {
|
||
var currentnode = cy.getElementById(newNodeId)
|
||
if (currentnode.length === 0) {
|
||
return
|
||
}
|
||
|
||
currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft)
|
||
currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop)
|
||
} else{
|
||
console.log(workflow)
|
||
if (workflow.start === "" || workflow.start === undefined) {
|
||
alert.error("Define a starting action first.")
|
||
return
|
||
}
|
||
|
||
if (data.is_valid === false) {
|
||
alert.error(data.name+" is only available on https://shuffler.io so far")
|
||
return
|
||
}
|
||
|
||
const triggerLabel = getNextActionName(data.name)
|
||
|
||
newNodeId = uuid.v4()
|
||
const newposition = {
|
||
"x": e.pageX-cycontainer.offsetLeft,
|
||
"y": e.pageY-cycontainer.offsetTop,
|
||
}
|
||
|
||
console.log(data)
|
||
const newAppData = {
|
||
app_name: data.name,
|
||
app_version: "1.0.0",
|
||
environment: data.environment,
|
||
description: data.description,
|
||
long_description: data.long_description,
|
||
errors: [],
|
||
id_: newNodeId,
|
||
_id_: newNodeId,
|
||
id: newNodeId,
|
||
label: triggerLabel,
|
||
type: data.type,
|
||
is_valid: true,
|
||
trigger_type: data.trigger_type,
|
||
large_image: data.large_image,
|
||
status: "uninitialized",
|
||
name: data.name,
|
||
isStartNode: false,
|
||
position: newposition,
|
||
}
|
||
|
||
//if (data.trigger_type === "WEBHOOK") {
|
||
// newAppData.status = "running"
|
||
//}
|
||
|
||
// Can all the data be in here? hmm
|
||
const nodeToBeAdded = {
|
||
group: "nodes",
|
||
data: newAppData,
|
||
renderedPosition: newposition,
|
||
}
|
||
|
||
cy.add(nodeToBeAdded)
|
||
|
||
if (workflow.triggers === undefined) {
|
||
workflow.triggers = [newAppData]
|
||
} else {
|
||
workflow.triggers.push(newAppData)
|
||
}
|
||
|
||
const newEdgeUuid = uuid.v4()
|
||
const newbranch = {
|
||
"source_id": newNodeId,
|
||
"destination_id": workflow.start,
|
||
"id_": newEdgeUuid,
|
||
"id": newEdgeUuid,
|
||
"hasErrors": false,
|
||
}
|
||
|
||
const newcybranch = {
|
||
"source": newNodeId,
|
||
"target": workflow.start,
|
||
"_id": newEdgeUuid,
|
||
"id": newEdgeUuid,
|
||
"hasErrors": false,
|
||
}
|
||
|
||
const edgeToBeAdded = {
|
||
group: "edges",
|
||
data: newcybranch,
|
||
}
|
||
|
||
if (data.name !== "User Input" && data.name !== "Shuffle Workflow") {
|
||
//workflow.branches.push(newbranch)
|
||
cy.add(edgeToBeAdded)
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
//if (data.trigger_type === "WEBHOOK") {
|
||
// newWebhook(newAppData)
|
||
// saveWorkflow(workflow)
|
||
//}
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleAppDrag = (e, app) => {
|
||
const cycontainer = cy.container()
|
||
// Chrome lol
|
||
//if (e.srcElement !== undefined && e.srcElement.localName === "canvas") {
|
||
if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) {
|
||
if (newNodeId.length > 0) {
|
||
var currentnode = cy.getElementById(newNodeId)
|
||
if (currentnode.length === 0) {
|
||
return
|
||
}
|
||
|
||
currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft)
|
||
currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop)
|
||
} else{
|
||
if (app.actions === undefined || app.actions === null || app.actions.length === 0) {
|
||
alert.error("App "+app.name+" currently has no actions to perform. Please go to https://shuffler.io/apps to edit it.")
|
||
return
|
||
}
|
||
|
||
newNodeId = uuid.v4()
|
||
const actionType = "ACTION"
|
||
const actionLabel = getNextActionName(app.name)
|
||
var parameters = null
|
||
var example = ""
|
||
|
||
if (app.actions[0].parameters !== null && app.actions[0].parameters.length > 0) {
|
||
parameters = app.actions[0].parameters
|
||
}
|
||
if (app.actions[0].returns.example !== undefined && app.actions[0].returns.example !== null && app.actions[0].returns.example.length > 0) {
|
||
example = app.actions[0].returns.example
|
||
}
|
||
|
||
var newAppPopup = false
|
||
|
||
/*
|
||
FIXME: Add auth.
|
||
selectedAction.selectedAuthentication = e.target.value
|
||
selectedAction.authentication_id = e.target.value.id
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
*/
|
||
|
||
const newAppData = {
|
||
app_name: app.name,
|
||
app_version: app.app_version,
|
||
app_id: app.id,
|
||
sharing: app.sharing,
|
||
private_id: app.private_id,
|
||
environment: environments === null ? "cloud" : environments[defaultEnvironmentIndex].Name,
|
||
errors: [],
|
||
id_: newNodeId,
|
||
_id_: newNodeId,
|
||
id: newNodeId,
|
||
is_valid: true,
|
||
label: actionLabel,
|
||
type: actionType,
|
||
name: app.actions[0].name,
|
||
parameters: parameters,
|
||
isStartNode: false,
|
||
large_image: app.large_image,
|
||
authentication: [],
|
||
execution_variable: undefined,
|
||
example: example,
|
||
category: app.categories !== null && app.categories !== undefined && app.categories.length > 0 ? app.categories[0] : ""
|
||
}
|
||
|
||
// FIXME: overwrite category if the ACTION chosen has a different category
|
||
|
||
// const image = "url("+app.large_image+")"
|
||
|
||
// FIXME - find the cytoscape offset position
|
||
// Can this be done with zoom calculations?
|
||
const nodeToBeAdded = {
|
||
group: "nodes",
|
||
data: newAppData,
|
||
renderedPosition: {
|
||
x: e.layerX,
|
||
y: e.layerY,
|
||
}
|
||
}
|
||
|
||
cy.add(nodeToBeAdded)
|
||
|
||
if (workflow.actions === undefined || workflow.actions.length === 0) {
|
||
workflow.start = newAppData.id
|
||
workflow.actions = []
|
||
newAppData.isStartNode = true
|
||
//setStartNode(newAppData.id)
|
||
}
|
||
|
||
if (workflow.actions.length > 0 && elements.length === 0) {
|
||
const actions = workflow.actions.map(action => {
|
||
const node = {}
|
||
node.position = action.position
|
||
node.data = action
|
||
|
||
node.data._id = action["id"]
|
||
node.data.type = "ACTION"
|
||
node.isStartNode = action["id"] === workflow.start
|
||
|
||
return node
|
||
})
|
||
|
||
const tmpelements = [].concat(actions)
|
||
setElements(tmpelements)
|
||
}
|
||
|
||
if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) {
|
||
const newEdgeUuid = uuid.v4()
|
||
const newcybranch = {
|
||
"source": workflow.start,
|
||
"target": newNodeId,
|
||
"_id": newEdgeUuid,
|
||
"id": newEdgeUuid,
|
||
"hasErrors": false,
|
||
}
|
||
|
||
const edgeToBeAdded = {
|
||
group: "edges",
|
||
data: newcybranch,
|
||
}
|
||
console.log("SHOULD STITCH WITH STARTNODE")
|
||
cy.add(edgeToBeAdded)
|
||
}
|
||
|
||
workflow.actions.push(newAppData)
|
||
setWorkflow(workflow)
|
||
|
||
if (newAppPopup) {
|
||
//alert.error("SHOULD MAKE USER AUTHENTICATE THE APP OR SET hasError")
|
||
//alert.info("Remember: set the authentication for the user itself, not the app")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleDragStop = (e, app) => {
|
||
newNodeId = ""
|
||
console.log("STOP!: ", e)
|
||
console.log("APP!: ", app)
|
||
}
|
||
|
||
const appScrollStyle = {
|
||
overflow: "scroll",
|
||
maxHeight: bodyHeight-appBarSize-55,
|
||
minHeight: bodyHeight-appBarSize-55,
|
||
overflowY: "auto",
|
||
overflowX: "hidden",
|
||
}
|
||
|
||
const runAppSearch = (event) => {
|
||
setAppSearch(event.target.value)
|
||
setFilteredApps(apps.filter(app => app.name.toLowerCase().includes(event.target.value.trim().toLowerCase())))
|
||
}
|
||
|
||
const ParsedAppPaper = (props) => {
|
||
const app = props.app
|
||
const [hover, setHover] = React.useState(false)
|
||
|
||
// FIXME - add label to apps, as this might be slow with A LOT of apps
|
||
var newAppname = app.name
|
||
newAppname = newAppname.replace("_", " ").charAt(0).toUpperCase()+newAppname.substring(1)
|
||
const maxlen = 24
|
||
if (newAppname.length > maxlen) {
|
||
newAppname = newAppname.slice(0, maxlen)+".."
|
||
}
|
||
|
||
//const image = "url("+app.large_image+")"
|
||
const image = app.large_image
|
||
const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle))
|
||
const pixelSize = !hover ? "2px" : "4px"
|
||
newAppStyle.borderLeft = app.is_valid ? `${pixelSize} solid green` : `${pixelSize} solid orange`
|
||
|
||
|
||
return (
|
||
<Draggable
|
||
onDrag={(e) => {handleAppDrag(e, app)}}
|
||
onStop={(e) => {handleDragStop(e, app)}}
|
||
key={app.id}
|
||
dragging={false}
|
||
position={{
|
||
x: 0,
|
||
y: 0,
|
||
}}
|
||
>
|
||
<Paper square style={newAppStyle} onMouseOver={() => {setHover(true)}} onMouseOut={() => {setHover(false)}}>
|
||
<Grid container style={{margin: "10px 10px 10px 15px", flex: "10"}}>
|
||
<Grid item>
|
||
<img alt={newAppname} src={image} style={{pointerEvents: "none", userDrag: "none", userSelect: "none", borderRadius: borderRadius, height: 80, width: 80,}} />
|
||
</Grid>
|
||
<Grid style={{display: "flex", flexDirection: "column", marginLeft: "20px", minWidth: 185, maxWidth: 185, overflow: "hidden", maxHeight: 77, }}>
|
||
<Grid item style={{flex: 1}}>
|
||
<h4 style={{marginBottom: 0, marginTop: 5}}>{newAppname}</h4>
|
||
</Grid>
|
||
<Grid item style={{flex: 1}}>
|
||
Version: {app.app_version}
|
||
</Grid>
|
||
<Grid item style={{flex: 1, width: "100%", maxHeight: 27, overflow: "hidden",}}>
|
||
{app.description}
|
||
</Grid>
|
||
</Grid>
|
||
</Grid>
|
||
</Paper>
|
||
</Draggable>
|
||
)
|
||
}
|
||
|
||
const AppView = () => {
|
||
return(
|
||
<div style={appViewStyle}>
|
||
<div style={{flex: "1"}}>
|
||
<div style={appScrollStyle}>
|
||
{/*
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
style={{backgroundColor: inputColor}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"Search apps"}
|
||
onChange={(event) => {
|
||
runAppSearch(event)
|
||
}}
|
||
/>
|
||
*/}
|
||
{prioritizedApps.map((app, index) => {
|
||
return(
|
||
<ParsedAppPaper key={index} app={app} />
|
||
)
|
||
})}
|
||
{filteredApps.filter(innerapp => !internalIds.includes(innerapp.id)).map((app, index) => {
|
||
if (app.invalid) {
|
||
return null
|
||
}
|
||
|
||
return(
|
||
<ParsedAppPaper key={index} app={app} />
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const getNextActionName = (appName) => {
|
||
var highest = ""
|
||
//label = name + _number
|
||
const allitems = workflow.actions.concat(workflow.triggers)
|
||
for (var key in allitems) {
|
||
const item = allitems[key]
|
||
if (item.app_name === appName && item.label !== undefined && item.label !== null) {
|
||
var number = item.label.split("_")
|
||
if (isNaN(number[-1]) && parseInt(number[number.length-1]) > highest) {
|
||
highest = number[number.length-1]
|
||
}
|
||
}
|
||
}
|
||
|
||
if (highest) {
|
||
return appName+"_"+(parseInt(highest)+1)
|
||
} else {
|
||
return appName+"_"+1
|
||
}
|
||
}
|
||
|
||
const setNewSelectedAction = (e) => {
|
||
const newaction = selectedApp.actions.find(a => a.name === e.target.value)
|
||
|
||
// Does this one find the wrong one?
|
||
selectedAction.name = newaction.name
|
||
selectedAction.parameters = JSON.parse(JSON.stringify(newaction.parameters))
|
||
|
||
if (newaction.returns.example !== undefined && newaction.returns.example !== null && newaction.returns.example.length > 0) {
|
||
selectedAction.example = newaction.returns.example
|
||
}
|
||
|
||
// FIXME - this is broken sometimes lol
|
||
//var env = environments.find(a => a.Name === newaction.environment)
|
||
//if ((!env || env === undefined) && selectedAction.environment === undefined ) {
|
||
// env = environments[defaultEnvironmentIndex]
|
||
//}
|
||
//setSelectedActionEnvironment(env)
|
||
|
||
setSelectedAction(selectedAction)
|
||
setSelectedActionName(e.target.value)
|
||
}
|
||
|
||
// APPSELECT at top
|
||
// appname & version
|
||
// description
|
||
// ACTION select
|
||
//
|
||
const selectedNameChange = (event) => {
|
||
console.log("OLDNAME: ", selectedActionName)
|
||
event.target.value = event.target.value.replace("(", "")
|
||
event.target.value = event.target.value.replace(")", "")
|
||
event.target.value = event.target.value.replace("$", "")
|
||
event.target.value = event.target.value.replace("#", "")
|
||
event.target.value = event.target.value.replace(".", "")
|
||
event.target.value = event.target.value.replace(",", "")
|
||
event.target.value = event.target.value.replace(" ", "_")
|
||
selectedAction.label = event.target.value
|
||
setSelectedAction(selectedAction)
|
||
|
||
console.log("SHOULD CHANGE NAME EVERYWHERE ITS USED TOO BASED ON OLD NAME!")
|
||
|
||
/*
|
||
if (nodeaction.label !== curaction.label) {
|
||
console.log("BEACH!")
|
||
|
||
var params = []
|
||
const fixedName = "$"+curaction.label.toLowerCase().replace(" ", "_")
|
||
for (var actionkey in workflow.actions) {
|
||
if (workflow.actions[actionkey].id === curaction.id) {
|
||
continue
|
||
}
|
||
|
||
for (var paramkey in workflow.actions[actionkey].parameters) {
|
||
const param = workflow.actions[actionkey].parameters[paramkey]
|
||
if (param.value === null || param.value === undefined || !param.value.includes("$")) {
|
||
continue
|
||
}
|
||
|
||
const innername = param.value.toLowerCase().replace(" ", "_")
|
||
if (innername.includes(fixedName)) {
|
||
//workflow.actions[actionkey].parameters[paramkey].replace(
|
||
//console.log("FOUND!: ", innername)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
*/
|
||
}
|
||
|
||
const selectedTriggerChange = (event) => {
|
||
selectedTrigger.label = event.target.value
|
||
setSelectedTrigger(selectedTrigger)
|
||
}
|
||
|
||
const getParents = (action) => {
|
||
var allkeys = [action.id]
|
||
var handled = []
|
||
var results = []
|
||
|
||
while(true) {
|
||
for (var key in allkeys) {
|
||
var currentnode = cy.getElementById(allkeys[key])
|
||
if (handled.includes(currentnode.data().id)) {
|
||
continue
|
||
} else {
|
||
// Get the name / label here too?
|
||
handled.push(currentnode.data().id)
|
||
results.push(currentnode.data())
|
||
}
|
||
|
||
if (currentnode.length === 0) {
|
||
continue
|
||
}
|
||
|
||
const incomingEdges = currentnode.incomers('edge')
|
||
if (incomingEdges.length === 0) {
|
||
continue
|
||
}
|
||
|
||
for (var i = 0; i < incomingEdges.length; i++) {
|
||
var tmp = incomingEdges[i]
|
||
if (!allkeys.includes(tmp.data().source)) {
|
||
allkeys.push(tmp.data().source)
|
||
}
|
||
}
|
||
}
|
||
if (results.length === allkeys.length) {
|
||
break
|
||
}
|
||
}
|
||
|
||
// Remove self
|
||
results = results.filter(data => data.id !== action.id)
|
||
results = results.filter(data => data.type !== "TRIGGER")
|
||
results.push({"label": "Execution Argument", "type": "INTERNAL"})
|
||
return results
|
||
}
|
||
|
||
// BOLD name: type: required?
|
||
// FORM
|
||
// Dropdown -> static, action, local env, global env
|
||
// VALUE (JSON)
|
||
// {data.name}, {data.description}, {data.required}, {data.schema.type}
|
||
const AppActionArguments = (props) => {
|
||
const [selectedActionParameters, setSelectedActionParameters] = React.useState([])
|
||
const [selectedVariableParameter, setSelectedVariableParameter] = React.useState("")
|
||
const [showDropdown, setShowDropdown] = React.useState(false)
|
||
const [showDropdownNumber, setShowDropdownNumber] = React.useState(0)
|
||
const [actionlist, setActionlist] = React.useState([])
|
||
const [jsonList, setJsonList] = React.useState([])
|
||
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
|
||
const [menuPosition, setMenuPosition] = useState(null)
|
||
|
||
useEffect(() => {
|
||
if (selectedActionParameters !== null && selectedActionParameters.length === 0) {
|
||
if (selectedAction.parameters !== null && selectedAction.parameters.length > 0) {
|
||
setSelectedActionParameters(selectedAction.parameters)
|
||
}
|
||
}
|
||
|
||
if ((selectedVariableParameter === null || selectedVariableParameter === undefined) && (workflow.workflow_variables !== null && workflow.workflow_variables.length > 0)) {
|
||
// FIXME - this is the bad thing
|
||
setSelectedVariableParameter(workflow.workflow_variables[0].name)
|
||
}
|
||
|
||
if (actionlist.length === 0) {
|
||
// FIXME: Have previous execution values in here
|
||
actionlist.push({"type": "Execution Argument", "name": "Execution Argument", "value": "$exec", "highlight": "exec", "autocomplete": "exec", "example": "hello"})
|
||
if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) {
|
||
for (var key in workflow.workflow_variables) {
|
||
const item = workflow.workflow_variables[key]
|
||
actionlist.push({"type": "workflow_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": item.value})
|
||
}
|
||
}
|
||
|
||
// FIXME: Add values from previous executions if they exist
|
||
if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) {
|
||
for (var key in workflow.execution_variables) {
|
||
const item = workflow.execution_variables[key]
|
||
actionlist.push({"type": "execution_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": ""})
|
||
}
|
||
}
|
||
|
||
// Loops parent nodes' old results to fix autocomplete
|
||
var parents = getParents(selectedAction)
|
||
if (parents.length > 1) {
|
||
for (var key in parents) {
|
||
const item = parents[key]
|
||
if (item.label === "Execution Argument") {
|
||
continue
|
||
}
|
||
|
||
var exampledata = item.example === undefined ? "" : item.example
|
||
// Find previous execution and their variables
|
||
//exampledata === "" &&
|
||
if (workflowExecutions.length > 0) {
|
||
// Look for the ID
|
||
const found = false
|
||
for (var key in workflowExecutions) {
|
||
if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) {
|
||
continue
|
||
}
|
||
|
||
var foundResult = workflowExecutions[key].results.find(result => result.action.id === item.id)
|
||
if (foundResult === undefined) {
|
||
continue
|
||
}
|
||
|
||
foundResult.result = foundResult.result.trim()
|
||
foundResult.result = foundResult.result.split(" None").join(" \"None\"")
|
||
foundResult.result = foundResult.result.split(" False").join(" false")
|
||
foundResult.result = foundResult.result.split(" True").join(" true")
|
||
|
||
var jsonvalid = true
|
||
try {
|
||
const tmp = String(JSON.parse(foundResult.result))
|
||
if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) {
|
||
jsonvalid = false
|
||
}
|
||
} catch (e) {
|
||
try {
|
||
foundResult.result = foundResult.result.split("\'").join("\"")
|
||
const tmp = String(JSON.parse(foundResult.result))
|
||
if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) {
|
||
jsonvalid = false
|
||
}
|
||
} catch (e) {
|
||
jsonvalid = false
|
||
}
|
||
}
|
||
|
||
// Finds the FIRST json only
|
||
if (jsonvalid) {
|
||
exampledata = JSON.parse(foundResult.result)
|
||
break
|
||
}
|
||
//else {
|
||
// console.log("Invalid JSON: ", foundResult.result)
|
||
//}
|
||
}
|
||
}
|
||
|
||
// 1. Take
|
||
const actionvalue = {"type": "action", "id": item.id, "name": item.label, "autocomplete": `${item.label.split(" ").join("_")}`, "example": exampledata}
|
||
actionlist.push(actionvalue)
|
||
}
|
||
}
|
||
|
||
setActionlist(actionlist)
|
||
}
|
||
})
|
||
|
||
const changeActionParameter = (event, count, data) => {
|
||
if (data.name.startsWith("${") && data.name.endsWith("}")) {
|
||
// PARAM FIX - Gonna use the ID field, even though it's a hack
|
||
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
|
||
if (paramcheck !== undefined) {
|
||
if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) {
|
||
paramcheck["value_replace"] = [{
|
||
"key": data.name,
|
||
"value": event.target.value,
|
||
}]
|
||
|
||
} else {
|
||
const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name)
|
||
if (subparamindex === -1) {
|
||
paramcheck["value_replace"].push({
|
||
"key": data.name,
|
||
"value": event.target.value,
|
||
})
|
||
} else {
|
||
paramcheck["value_replace"][subparamindex]["value"] = event.target.value
|
||
}
|
||
}
|
||
//console.log("PARAM: ", paramcheck)
|
||
|
||
selectedActionParameters[count]["value_replace"] = paramcheck
|
||
selectedAction.parameters[count]["value_replace"] = paramcheck
|
||
setSelectedAction(selectedAction)
|
||
return
|
||
}
|
||
}
|
||
|
||
if (event.target.value[event.target.value.length-1] === "$") {
|
||
if (!showDropdown) {
|
||
setShowAutocomplete(false)
|
||
setShowDropdown(true)
|
||
setShowDropdownNumber(count)
|
||
}
|
||
} else {
|
||
if (showDropdown) {
|
||
setShowDropdown(false)
|
||
}
|
||
}
|
||
|
||
// bad detection mechanism probably
|
||
if (event.target.value[event.target.value.length-1] === "." && actionlist.length > 0) {
|
||
console.log("GET THE LAST ARGUMENT FOR NODE!")
|
||
// THIS IS AN EXAMPLE OF SHOWING IT
|
||
/*
|
||
const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"}
|
||
setJsonList(GetParsedPaths(inputdata, ""))
|
||
if (!showDropdown) {
|
||
setShowAutocomplete(false)
|
||
setShowDropdown(true)
|
||
setShowDropdownNumber(count)
|
||
}
|
||
console.log(jsonList)
|
||
*/
|
||
|
||
// Search for the item backwards
|
||
// 1. Reverse search backwards from . -> $
|
||
// 2. Search the actionlist for the item
|
||
// 3. Find the data for the specific item
|
||
|
||
var curstring = ""
|
||
var record = false
|
||
for (var key in selectedActionParameters[count].value) {
|
||
const item = selectedActionParameters[count].value[key]
|
||
if (record) {
|
||
curstring += item
|
||
}
|
||
|
||
if (item === "$") {
|
||
record = true
|
||
curstring = ""
|
||
}
|
||
}
|
||
|
||
//console.log("CURSTRING: ", curstring)
|
||
if (curstring.length > 0 && actionlist !== null) {
|
||
// Search back in the action list
|
||
curstring = curstring.split(" ").join("_").toLowerCase()
|
||
var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring)
|
||
if (actionItem !== undefined) {
|
||
console.log("Found item: ", actionItem)
|
||
|
||
//actionItem.example = actionItem.example.trim()
|
||
//actionItem.example = actionItem.example.split(" None").join(" \"None\"")
|
||
//actionItem.example = actionItem.example.split("\'").join("\"")
|
||
|
||
var jsonvalid = true
|
||
try {
|
||
const tmp = String(JSON.parse(actionItem.example))
|
||
if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) {
|
||
jsonvalid = false
|
||
}
|
||
} catch (e) {
|
||
jsonvalid = false
|
||
}
|
||
|
||
if (jsonvalid) {
|
||
setJsonList(GetParsedPaths(JSON.parse(actionItem.example), ""))
|
||
|
||
if (!showDropdown) {
|
||
setShowAutocomplete(false)
|
||
setShowDropdown(true)
|
||
setShowDropdownNumber(count)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if (jsonList.length > 0) {
|
||
setJsonList([])
|
||
}
|
||
}
|
||
|
||
selectedActionParameters[count].value = event.target.value
|
||
selectedAction.parameters[count].value = event.target.value
|
||
setSelectedAction(selectedAction)
|
||
//setUpdate(event.target.value)
|
||
}
|
||
|
||
const changeActionParameterVariable = (fieldvalue, count) => {
|
||
//console.log("CALLED THIS ONE WITH VALUE!", fieldvalue)
|
||
//if (selectedVariableParameter === fieldvalue) {
|
||
// return
|
||
//}
|
||
|
||
setSelectedVariableParameter(fieldvalue)
|
||
|
||
selectedActionParameters[count].action_field = fieldvalue
|
||
selectedAction.parameters = selectedActionParameters
|
||
|
||
setSelectedActionName(selectedActionName)
|
||
setSelectedApp(selectedApp)
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(fieldvalue)
|
||
}
|
||
|
||
// Sets ACTION_RESULT things
|
||
const changeActionParameterActionResult = (fieldvalue, count) => {
|
||
//cy.nodes().forEach(function( ele ) {
|
||
// if (ele.data()["label"] === fieldvalue) {
|
||
// selectedActionParameters[count].action_field = ele.id()
|
||
// return
|
||
// }
|
||
//});
|
||
|
||
selectedActionParameters[count].action_field = fieldvalue
|
||
selectedAction.parameters = selectedActionParameters
|
||
|
||
// FIXME - check if startnode
|
||
|
||
// Set value
|
||
setSelectedActionName(selectedActionName)
|
||
setSelectedApp(selectedApp)
|
||
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(fieldvalue)
|
||
}
|
||
|
||
const changeActionParameterVariant = (data, count) => {
|
||
selectedActionParameters[count].variant = data
|
||
selectedActionParameters[count].value = ""
|
||
|
||
if (data === "ACTION_RESULT") {
|
||
var parents = getParents(selectedAction)
|
||
if (parents.length > 0) {
|
||
selectedActionParameters[count].action_field = parents[0].label
|
||
} else {
|
||
selectedActionParameters[count].action_field = ""
|
||
}
|
||
} else if (data === "WORKFLOW_VARIABLE") {
|
||
if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) {
|
||
selectedActionParameters[count].action_field = workflow.workflow_variables[0].name
|
||
}
|
||
}
|
||
|
||
selectedAction.parameters = selectedActionParameters
|
||
|
||
// This is a stupid workaround to make it refresh rofl
|
||
setSelectedActionName({})
|
||
setSelectedAction({})
|
||
setSelectedTrigger({})
|
||
setSelectedApp({})
|
||
setSelectedEdge({})
|
||
// FIXME - check if startnode
|
||
|
||
// Set value
|
||
setSelectedActionName(selectedActionName)
|
||
setSelectedApp(selectedApp)
|
||
setSelectedAction(selectedAction)
|
||
}
|
||
|
||
// FIXME: Issue #40 - selectedActionParameters not reset
|
||
if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) {
|
||
return (
|
||
<div style={{marginTop: "30px"}}>
|
||
<b>Parameters</b>
|
||
{selectedActionParameters.map((data, count) => {
|
||
if (data.variant === "") {
|
||
data.variant = "STATIC_VALUE"
|
||
}
|
||
|
||
|
||
// selectedAction.selectedAuthentication = e.target.value
|
||
// selectedAction.authentication_id = e.target.value.id
|
||
if (!selectedAction.auth_not_required && selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) {
|
||
// This sets the placeholder in the frontend. (Replaced in backend)
|
||
selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name]
|
||
selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name]
|
||
setSelectedAction(selectedAction)
|
||
|
||
return null
|
||
}
|
||
|
||
var staticcolor = "inherit"
|
||
var actioncolor = "inherit"
|
||
var varcolor = "inherit"
|
||
var multiline
|
||
if (data.multiline !== undefined && data.multiline !== null && data.multiline === true) {
|
||
multiline = true
|
||
}
|
||
|
||
if (data.value !== undefined && data.value !== null && data.value.startsWith("{") && data.value.endsWith("}")) {
|
||
multiline = true
|
||
}
|
||
|
||
var placeholder = "Static value"
|
||
if (data.example !== undefined && data.example !== null && data.example.length > 0) {
|
||
placeholder = data.example
|
||
}
|
||
|
||
if (data.name.startsWith("${") && data.name.endsWith("}")) {
|
||
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
|
||
if (paramcheck !== undefined && paramcheck !== null) {
|
||
if (paramcheck["value_replace"] !== undefined && paramcheck["value_replace"] !== null) {
|
||
//console.log("IN THE VALUE REPLACE: ", paramcheck["value_replace"])
|
||
const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name)
|
||
if (subparamindex !== -1) {
|
||
data.value = paramcheck["value_replace"][subparamindex]["value"]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var disabled = false
|
||
var rows = "5"
|
||
var openApiHelperText = "This is an OpenAPI specific field"
|
||
if (selectedApp.generated && selectedApp.activated && data.name === "body") {
|
||
const regex = /\${(\w+)}/g
|
||
const found = placeholder.match(regex)
|
||
if (found === null) {
|
||
//setExtraBodyFields([])
|
||
} else {
|
||
rows = "1"
|
||
disabled = true
|
||
openApiHelperText = "OpenAPI spec: fill the following fields."
|
||
//console.log("SHOULD ADD TO selectedActionParameters!: ", found, selectedActionParameters)
|
||
var changed = false
|
||
for (var specKey in found) {
|
||
const tmpitem = found[specKey]
|
||
var skip = false
|
||
for (var innerkey in selectedActionParameters) {
|
||
if (selectedActionParameters[innerkey].name === tmpitem) {
|
||
skip = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if (skip) {
|
||
//console.log("SKIPPING ", tmpitem)
|
||
continue
|
||
}
|
||
|
||
changed = true
|
||
selectedActionParameters.push({
|
||
action_field: "",
|
||
configuration: false,
|
||
description: "Generated by OpenAPI body example",
|
||
example: "",
|
||
id: "",
|
||
multiline: false,
|
||
name: tmpitem,
|
||
options: null,
|
||
required: false,
|
||
schema: {type: "string"},
|
||
skip_multicheck: false,
|
||
tags: null,
|
||
value: "",
|
||
variant: "STATIC_VALUE",
|
||
})
|
||
}
|
||
|
||
if (changed) {
|
||
setSelectedActionParameters(selectedActionParameters)
|
||
}
|
||
|
||
return <Divider key={Math.random()} />
|
||
}
|
||
}
|
||
|
||
//console.log("Data: ", data)
|
||
var datafield =
|
||
<TextField
|
||
disabled={disabled}
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: 5,
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<Tooltip title="Autocomplete text" placement="top">
|
||
<AddCircleOutlineIcon style={{cursor: "pointer"}} onClick={(event) => {
|
||
setMenuPosition({
|
||
top: event.pageY+10,
|
||
left: event.pageX+10,
|
||
})
|
||
setShowDropdownNumber(count)
|
||
setShowDropdown(true)
|
||
setShowAutocomplete(true)
|
||
}}/>
|
||
</Tooltip>
|
||
</InputAdornment>
|
||
)
|
||
}}
|
||
fullWidth
|
||
multiline={multiline}
|
||
rows={rows}
|
||
color="primary"
|
||
defaultValue={data.value}
|
||
type={placeholder.includes("***") ? "password" : "text"}
|
||
placeholder={placeholder}
|
||
onChange={(event) => {
|
||
changeActionParameter(event, count, data)
|
||
}}
|
||
helperText={selectedApp.generated && selectedApp.activated && data.name === "body" ?
|
||
<span style={{color:"white", marginBottom: 5, marginleft: 5,}}>
|
||
{openApiHelperText}
|
||
</span>
|
||
:
|
||
data.name.startsWith("${") && data.name.endsWith("}") ?
|
||
<span style={{color:"white", marginBottom: 5, marginLeft: 5}}>
|
||
OpenAPI helperfield
|
||
</span>
|
||
:
|
||
null
|
||
}
|
||
onBlur={(event) => {
|
||
// Super basic check
|
||
//if (event.target.value.startsWith("{")) {
|
||
// console.log("VALIDATING JSON")
|
||
// try {
|
||
// JSON.parse(event.target.value)
|
||
// } catch (e) {
|
||
// alert.error("Failed to parse json: ", e)
|
||
// }
|
||
//}
|
||
}}
|
||
/>
|
||
|
||
if (selectedActionParameters[count].schema !== undefined && selectedActionParameters[count].schema !== null && selectedActionParameters[count].schema.type === "file") {
|
||
datafield =
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<Tooltip title="Autocomplete text" placement="top">
|
||
<AddCircleOutlineIcon style={{cursor: "pointer"}} onClick={(event) => {
|
||
setMenuPosition({
|
||
top: event.pageY+10,
|
||
left: event.pageX+10,
|
||
})
|
||
setShowDropdownNumber(count)
|
||
setShowDropdown(true)
|
||
setShowAutocomplete(true)
|
||
}}/>
|
||
</Tooltip>
|
||
</InputAdornment>
|
||
)
|
||
}}
|
||
fullWidth
|
||
multiline={multiline}
|
||
rows="5"
|
||
color="primary"
|
||
defaultValue={data.value}
|
||
type={"text"}
|
||
placeholder={"The file ID to get"}
|
||
onChange={(event) => {
|
||
changeActionParameter(event, count, data)
|
||
}}
|
||
onBlur={(event) => {
|
||
}}
|
||
/>
|
||
//const fileId = "6daabec1-892b-469c-b603-c902e47223a9"
|
||
//datafield = `SHOW FILES FROM OTHER NODES? Filename: ${selectedActionParameters[count].value}`
|
||
/*
|
||
if (selectedActionParameters[count].value != fileId) {
|
||
changeActionParameter(fileId, count, data)
|
||
setUpdate(Math.random())
|
||
|
||
}
|
||
*/
|
||
} else if (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0) {
|
||
if (selectedActionParameters[count].value === "" && selectedActionParameters[count].required) {
|
||
// Rofl, dirty workaround :)
|
||
const e = {
|
||
target: {
|
||
value: selectedActionParameters[count].options[0],
|
||
}
|
||
}
|
||
|
||
changeActionParameter(e, count, data)
|
||
}
|
||
|
||
datafield =
|
||
<Select
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
value={selectedActionParameters[count].value}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
console.log("VAL: ", e.target.value)
|
||
changeActionParameter(e, count, data)
|
||
setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: surfaceColor, color: "white", height: "50px"}}
|
||
>
|
||
{selectedActionParameters[count].options.map((data, index) => {
|
||
const split_data = data.split("||")
|
||
var viewed_data = data
|
||
if (split_data.length > 1) {
|
||
viewed_data = split_data[0]
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{viewed_data}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
|
||
} else if (data.variant === "STATIC_VALUE") {
|
||
staticcolor = "#f85a3e"
|
||
} else if (data.variant === "ACTION_RESULT") {
|
||
// Gets the parents of the current node
|
||
var parents = getParents(selectedAction)
|
||
actioncolor = "#f85a3e"
|
||
// set the datafield
|
||
//var datafieldvalue = "Error: No parents. Action not eligible"
|
||
//if (parents.length > 0) {
|
||
// datafieldvalue = parents[0].label
|
||
//}
|
||
const fixedActionText = selectedActionParameters[count].value
|
||
|
||
datafield =
|
||
<div>
|
||
<Select
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
value={selectedActionParameters[count].action_field}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
changeActionParameterActionResult(e.target.value, count)
|
||
}}
|
||
style={{backgroundColor: surfaceColor, color: "white", height: "50px"}}
|
||
>
|
||
{parents.map(data => (
|
||
<MenuItem key={data.label} style={{backgroundColor: inputColor, color: "white"}} value={data.label}>
|
||
{data.label}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
defaultValue={data.value}
|
||
helperText={<div style={{marginLeft: "5px", color:"white", marginBottom: "2px",}}>Example: $.body will get "data" from {'{"body": "data"}'}</div>}
|
||
placeholder="Action variable ($.)"
|
||
onChange={(event) => {
|
||
changeActionParameter(event, count, data)
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
} else if (data.variant === "WORKFLOW_VARIABLE") {
|
||
varcolor = "#f85a3e"
|
||
if ((workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) && (workflow.execution_variables === null || workflow.execution_variables === undefined || workflow.execution_variables.length === 0)) {
|
||
setCurrentView(2)
|
||
datafield =
|
||
<div>
|
||
<div>
|
||
Looks like you don't have any variables yet.
|
||
</div>
|
||
<div style={{width: "100%", margin: "auto"}}>
|
||
<Button style={{margin: "auto", marginTop: "10px"}} color="primary" variant="outlined" onClick={() => {
|
||
setVariablesModalOpen(true)
|
||
setLastSaved(false)
|
||
}}>New workflow variable</Button>
|
||
</div>
|
||
</div>
|
||
} else {
|
||
// FIXME - this is a shitty solution that needs re-renders all the time
|
||
datafield =
|
||
<Select
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
fullWidth
|
||
value={selectedAction.parameters[count].action_field}
|
||
onChange={(e) => {
|
||
console.log(e.target.value)
|
||
changeActionParameterVariable(e.target.value, count)
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{workflow.workflow_variables !== undefined && workflow.workflow_variables !== null ? workflow.workflow_variables.map(data => (
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||
{data.name}
|
||
</MenuItem>
|
||
)) : null}
|
||
<Divider />
|
||
{workflow.execution_variables !== undefined && workflow.execution_variables !== null ? workflow.execution_variables.map(data => (
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||
{data.name}
|
||
</MenuItem>
|
||
)) : null}
|
||
</Select>
|
||
}
|
||
}
|
||
|
||
// Shows nested list of nodes > their JSON lists
|
||
const ActionlistWrapper = (props) => {
|
||
|
||
const handleMenuClose = () => {
|
||
setShowAutocomplete(false)
|
||
|
||
if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === "$") {
|
||
setShowDropdown(false)
|
||
}
|
||
|
||
setUpdate(Math.random())
|
||
setMenuPosition(null)
|
||
}
|
||
|
||
const handleItemClick = (values) => {
|
||
if (values === undefined || values === null || values.length === 0) {
|
||
return
|
||
}
|
||
|
||
var toComplete = selectedActionParameters[count].value.trim().endsWith("$") ? values[0].autocomplete : "$"+values[0].autocomplete
|
||
for (var key in values) {
|
||
if (key == 0 || values[key].autocomplete.length === 0) {
|
||
continue
|
||
}
|
||
|
||
toComplete += values[key].autocomplete
|
||
}
|
||
|
||
// Handles the fields under OpenAPI body to be parsed.
|
||
if (data.name.startsWith("${") && data.name.endsWith("}")) {
|
||
console.log("INSIDE VALUE REPLACE: ", data.name, toComplete)
|
||
// PARAM FIX - Gonna use the ID field, even though it's a hack
|
||
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
|
||
if (paramcheck !== undefined) {
|
||
if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) {
|
||
paramcheck["value_replace"] = [{
|
||
"key": data.name,
|
||
"value": toComplete,
|
||
}]
|
||
|
||
} else {
|
||
const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name)
|
||
if (subparamindex === -1) {
|
||
paramcheck["value_replace"].push({
|
||
"key": data.name,
|
||
"value": toComplete,
|
||
})
|
||
} else {
|
||
paramcheck["value_replace"][subparamindex]["value"] += toComplete
|
||
}
|
||
}
|
||
|
||
selectedActionParameters[count]["value_replace"] = paramcheck
|
||
selectedAction.parameters[count]["value_replace"] = paramcheck
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
|
||
setShowDropdown(false)
|
||
setMenuPosition(null)
|
||
return
|
||
}
|
||
}
|
||
|
||
selectedActionParameters[count].value += toComplete
|
||
selectedAction.parameters[count].value = selectedActionParameters[count].value
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
|
||
setShowDropdown(false)
|
||
setMenuPosition(null)
|
||
}
|
||
|
||
const iconStyle = {
|
||
marginRight: 15,
|
||
}
|
||
|
||
return (
|
||
<Menu
|
||
anchorReference="anchorPosition"
|
||
anchorPosition={menuPosition}
|
||
onClose={() => {
|
||
handleMenuClose()
|
||
}}
|
||
open={!!menuPosition}
|
||
style={{
|
||
border: `2px solid #f85a3e`,
|
||
color: "white",
|
||
marginTop: 2,
|
||
}}
|
||
>
|
||
{actionlist.map(innerdata => {
|
||
const icon = innerdata.type === "action" ? <AppsIcon style={{marginRight: 10}} /> : innerdata.type === "workflow_variable" || innerdata.type === "execution_variable" ? <FavoriteBorderIcon style={{marginRight: 10}} /> : <ScheduleIcon style={{marginRight: 10}} />
|
||
|
||
const handleExecArgumentHover = (inside) => {
|
||
var exec_text_field = document.getElementById("execution_argument_input_field")
|
||
if (exec_text_field !== null) {
|
||
if (inside) {
|
||
exec_text_field.style.border = "2px solid #f85a3e"
|
||
} else {
|
||
exec_text_field.style.border = ""
|
||
}
|
||
}
|
||
|
||
// Also doing arguments
|
||
if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > 0) {
|
||
for (var key in workflow.triggers) {
|
||
const item = workflow.triggers[key]
|
||
|
||
var node = cy.getElementById(item.id)
|
||
if (node.length > 0) {
|
||
if (inside) {
|
||
node.addClass('shuffle-hover-highlight')
|
||
} else {
|
||
node.removeClass('shuffle-hover-highlight')
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleActionHover = (inside, actionId) => {
|
||
var node = cy.getElementById(actionId)
|
||
if (node.length > 0) {
|
||
if (inside) {
|
||
node.addClass('shuffle-hover-highlight')
|
||
} else {
|
||
node.removeClass('shuffle-hover-highlight')
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleMouseover = () => {
|
||
if (innerdata.type === "Execution Argument") {
|
||
handleExecArgumentHover(true)
|
||
} else if (innerdata.type === "action") {
|
||
handleActionHover(true, innerdata.id)
|
||
}
|
||
}
|
||
|
||
const handleMouseOut = () => {
|
||
if (innerdata.type === "Execution Argument") {
|
||
handleExecArgumentHover(false)
|
||
} else if (innerdata.type === "action") {
|
||
handleActionHover(false, innerdata.id)
|
||
}
|
||
}
|
||
|
||
var parsedPaths = []
|
||
if (typeof(innerdata.example) === "object") {
|
||
parsedPaths = GetParsedPaths(innerdata.example, "")
|
||
}
|
||
|
||
return (
|
||
parsedPaths.length > 0 ?
|
||
<NestedMenuItem
|
||
key={innerdata.name}
|
||
label={
|
||
<div style={{display: "flex"}}>
|
||
{icon} {innerdata.name}
|
||
</div>
|
||
}
|
||
parentMenuOpen={!!menuPosition}
|
||
style={{backgroundColor: inputColor, color: "white", minWidth: 250,}}
|
||
onClick={() => {
|
||
handleItemClick([innerdata])
|
||
}}
|
||
>
|
||
{parsedPaths.map((pathdata, index) => {
|
||
// FIXME: Should be recursive in here
|
||
const icon = pathdata.type === "value" ? <VpnKeyIcon style={iconStyle} /> : pathdata.type === "list" ? <FormatListNumberedIcon style={iconStyle} /> : <ExpandMoreIcon style={iconStyle} />
|
||
return (
|
||
<MenuItem key={pathdata.name} style={{backgroundColor: inputColor, color: "white", minWidth: 250, }} value={pathdata} onMouseOver={() => {}}
|
||
onClick={() => {
|
||
handleItemClick([innerdata, pathdata])
|
||
}}
|
||
>
|
||
<Tooltip color="primary" title={`Ex. value: ${pathdata.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {pathdata.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
)
|
||
|
||
})}
|
||
</NestedMenuItem>
|
||
:
|
||
<MenuItem key={innerdata.name} style={{backgroundColor: inputColor, color: "white"}} value={innerdata} onMouseOver={() => handleMouseover()} onMouseOut={() => {handleMouseOut()}}
|
||
onClick={() => {
|
||
handleItemClick([innerdata])
|
||
}}
|
||
>
|
||
<Tooltip color="primary" title={`Value: ${innerdata.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {innerdata.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
|
||
)
|
||
})}
|
||
</Menu>
|
||
)
|
||
}
|
||
|
||
var itemColor = "#f85a3e"
|
||
if (!data.required) {
|
||
itemColor = "#ffeb3b"
|
||
}
|
||
|
||
var tmpitem = data.name.valueOf()
|
||
if (data.name.startsWith("${") && data.name.endsWith("}")) {
|
||
tmpitem = tmpitem.slice(2, data.name.length-1)
|
||
}
|
||
|
||
tmpitem = tmpitem.charAt(0).toUpperCase()+tmpitem.substring(1)
|
||
const description = data.description === undefined ? "" : data.description
|
||
|
||
return (
|
||
<div key={data.name}>
|
||
<div style={{marginTop: 20, marginBottom: 7, display: "flex"}}>
|
||
|
||
|
||
{data.configuration === true ?
|
||
<Tooltip color="primary" title={`Authenticate ${selectedApp.name}`} placement="top">
|
||
<LockOpenIcon style={{cursor: "pointer", width: 24, height: 24, marginRight: 10, }} onClick={() => {
|
||
setAuthenticationModalOpen(true)
|
||
}}/>
|
||
</Tooltip>
|
||
:
|
||
<div style={{width: 17, height: 17, borderRadius: 17 / 2, backgroundColor: itemColor, marginRight: 10, marginTop: 2,}}/>
|
||
}
|
||
<div style={{flex: "10"}}>
|
||
<Tooltip title={description} placement="top">
|
||
<b>{tmpitem} </b>
|
||
</Tooltip>
|
||
</div>
|
||
|
||
{selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0 ? null :
|
||
<div style={{display: "flex"}}>
|
||
<Tooltip color="primary" title="Static data" placement="top">
|
||
<div style={{cursor: "pointer", color: staticcolor}} onClick={(e) => {
|
||
e.preventDefault()
|
||
changeActionParameterVariant("STATIC_VALUE", count)
|
||
}}>
|
||
<CreateIcon />
|
||
</div>
|
||
</Tooltip>
|
||
|
|
||
<Tooltip color="primary" title="Data from previous action" placement="top">
|
||
<div style={{cursor: "pointer", color: actioncolor}} onClick={(e) => {
|
||
e.preventDefault()
|
||
changeActionParameterVariant("ACTION_RESULT", count)
|
||
}}>
|
||
<AppsIcon />
|
||
</div>
|
||
</Tooltip>
|
||
|
|
||
<Tooltip color="primary" title="Use local variable" placement="top">
|
||
<div style={{cursor: "pointer", color: varcolor}} onClick={(e) => {
|
||
e.preventDefault()
|
||
changeActionParameterVariant("WORKFLOW_VARIABLE", count)
|
||
}}>
|
||
<FavoriteBorderIcon />
|
||
</div>
|
||
</Tooltip>
|
||
</div>
|
||
}
|
||
</div>
|
||
{datafield}
|
||
{showDropdown && showDropdownNumber === count && data.variant === "STATIC_VALUE" && jsonList.length > 0 ?
|
||
<FormControl fullWidth style={{marginTop: 0}}>
|
||
<InputLabel id="action-autocompleter" style={{marginLeft: 10, color: "white"}}>Autocomplete</InputLabel>
|
||
<Select
|
||
labelId="action-autocompleter"
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setShowAutocomplete(false)
|
||
|
||
if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === ".") {
|
||
setShowDropdown(false)
|
||
}
|
||
|
||
setUpdate(Math.random())
|
||
}}
|
||
onClick={() => setShowAutocomplete(true)}
|
||
fullWidth
|
||
open={showAutocomplete}
|
||
style={{border: `2px solid #f85a3e`, color: "white", height: 50, marginTop: 2,}}
|
||
onChange={(e) => {
|
||
if (selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === ".") {
|
||
e.target.value.autocomplete = e.target.value.autocomplete.slice(1, e.target.value.autocomplete.length)
|
||
}
|
||
|
||
selectedActionParameters[count].value += e.target.value.autocomplete
|
||
selectedAction.parameters[count].value = selectedActionParameters[count].value
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
|
||
setShowDropdown(false)
|
||
}}
|
||
>
|
||
{jsonList.map(data => {
|
||
const iconStyle = {
|
||
marginRight: 15,
|
||
}
|
||
|
||
const icon = data.type === "value" ? <VpnKeyIcon style={iconStyle} /> : data.type === "list" ? <FormatListNumberedIcon style={iconStyle} /> : <ExpandMoreIcon style={iconStyle} />
|
||
return (
|
||
<MenuItem key={data.name} style={{backgroundColor: inputColor, color: "white"}} value={data} onMouseOver={() => {}}>
|
||
<Tooltip color="primary" title={`Ex. value: ${data.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {data.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</FormControl>
|
||
: null}
|
||
{showDropdown && showDropdownNumber === count && data.variant === "STATIC_VALUE" && jsonList.length === 0 ?
|
||
<ActionlistWrapper actionlist={actionlist} />
|
||
: null}
|
||
|
||
|
||
</div>
|
||
)})}
|
||
</div>
|
||
)
|
||
}
|
||
return null
|
||
}
|
||
|
||
//height: "100%",
|
||
const appApiViewStyle = {
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
backgroundColor: "#1F2023",
|
||
color: "white",
|
||
paddingRight: 15,
|
||
paddingLeft: 15,
|
||
minHeight: "100%",
|
||
zIndex: 1000,
|
||
resize: "vertical",
|
||
overflow: "auto",
|
||
}
|
||
|
||
const defineStartnode = () => {
|
||
var oldstartnode = cy.getElementById(workflow.start)
|
||
if (oldstartnode.length > 0) {
|
||
oldstartnode[0].data("isStartNode", false)
|
||
var oldnodecnt = workflow.actions.findIndex(a => a.id === workflow.start)
|
||
if (workflow.actions[oldnodecnt] !== undefined) {
|
||
workflow.actions[oldnodecnt].isStartNode = false
|
||
}
|
||
}
|
||
|
||
var newstartnode = cy.getElementById(selectedAction.id)
|
||
if (newstartnode.length > 0) {
|
||
newstartnode[0].data("isStartNode", true)
|
||
var newnodecnt = workflow.actions.findIndex(a => a.id === selectedAction.id)
|
||
console.log("NEW NODE CNT: ", newnodecnt)
|
||
if (workflow.actions[newnodecnt] !== undefined) {
|
||
workflow.actions[newnodecnt].isStartNode = true
|
||
console.log(workflow.actions[newnodecnt])
|
||
}
|
||
}
|
||
|
||
// Find branches with triggers as source nodes
|
||
// Move these targets to be the new node
|
||
// Set arrows pointing to new startnode with errors
|
||
//for (var key in workflow.branches) {
|
||
// var item = workflow.branches[key]
|
||
// if (item.destination_id === oldstartnode[0].data()["id"]) {
|
||
// var curbranch = cy.getElementById(item.id)
|
||
// if (curbranch.length > 0) {
|
||
// //console.log(curbranch[0].data())
|
||
// //curbranch[0].data("target", selectedAction.id)
|
||
// //curbranch[0].data("hasErrors", true)
|
||
// //workflow.branches[key].destination_id = selectedAction.id
|
||
// //console.log(curbranch[0].data())
|
||
// }
|
||
// }
|
||
//}
|
||
|
||
setUpdate("start_node"+selectedAction.id)
|
||
workflow.start = selectedAction.id
|
||
setWorkflow(workflow)
|
||
//setStartNode(selectedAction.id)
|
||
}
|
||
|
||
function sortByKey(array, key) {
|
||
if (array === undefined) {
|
||
return []
|
||
}
|
||
|
||
return array.sort(function(a, b) {
|
||
var x = a[key]; var y = b[key]
|
||
return ((x < y) ? -1 : ((x > y) ? 1 : 0))
|
||
})
|
||
}
|
||
|
||
const rightsidebarStyle = {
|
||
position: "fixed",
|
||
right: 0,
|
||
top: headerSize+1,
|
||
height: "100%",
|
||
bottom: 0,
|
||
minWidth: 350,
|
||
maxWidth: 350,
|
||
borderLeft: "1px solid rgb(91, 96, 100)",
|
||
overflow: "scroll",
|
||
overflowX: "auto",
|
||
overflowY: "auto",
|
||
zIndex: 1000,
|
||
}
|
||
|
||
const textFieldStyle = {
|
||
backgroundColor: inputColor,
|
||
borderRadius: borderRadius,
|
||
}
|
||
|
||
|
||
const innerTextfieldStyle = {
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
borderRadius: borderRadius,
|
||
}
|
||
|
||
const appApiView = Object.getOwnPropertyNames(selectedAction).length > 0 ?
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", minHeight: 40, marginBottom: 30}}>
|
||
<div style={{flex: 1}}>
|
||
<h3 style={{marginBottom: 5}}>{selectedAction.app_name}</h3>
|
||
<div style={{display: "flex",}}>
|
||
<IconButton style={{marginTop: "auto", marginBottom: "auto", height: 30, paddingLeft: 0, paddingRight: 0}} onClick={() => {
|
||
console.log("FIND EXAMPLE RESULTS FOR ", selectedAction)
|
||
if (workflowExecutions.length > 0) {
|
||
// Look for the ID
|
||
const found = false
|
||
for (var key in workflowExecutions) {
|
||
if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) {
|
||
continue
|
||
}
|
||
|
||
var foundResult = workflowExecutions[key].results.find(result => result.action.id === selectedAction.id)
|
||
if (foundResult === undefined || foundResult === null) {
|
||
continue
|
||
}
|
||
|
||
setSelectedResult(foundResult)
|
||
setCodeModalOpen(true)
|
||
break
|
||
}
|
||
}
|
||
}}>
|
||
<Tooltip color="primary" title="See last result" placement="top">
|
||
<ArrowLeftIcon style={{color: "white"}}/>
|
||
</Tooltip>
|
||
</IconButton>
|
||
<span style={{}}>
|
||
<Typography style={{marginTop: 5, marginLeft: 10,}}><a rel="norefferer" href="https://shuffler.io/docs/workflows#nodes" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>What are actions?</a></Typography>
|
||
{selectedAction.errors !== null && selectedAction.errors.length > 0 ?
|
||
<div>
|
||
Errors: {selectedAction.errors.join("\n")}
|
||
</div>
|
||
: null
|
||
}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div style={{flex: "1"}}>
|
||
<Button disabled={selectedAction.id === workflow.start} style={{zIndex: 5000, marginTop: "15px",}} color="primary" variant="outlined" onClick={(e) => {
|
||
defineStartnode(e)
|
||
}}>Set startnode</Button>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<Typography>
|
||
Name
|
||
</Typography>
|
||
<TextField
|
||
style={textFieldStyle}
|
||
InputProps={{
|
||
style: innerTextfieldStyle,
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedAction.label}
|
||
onChange={selectedNameChange}
|
||
/>
|
||
{selectedApp.name !== undefined && selectedAction.authentication !== undefined && selectedAction.authentication.length === 0 && requiresAuthentication ?
|
||
<div style={{marginTop: 15}}>
|
||
Authenticate {selectedApp.name}:
|
||
<Tooltip color="primary" title={"Add authentication option"} placement="top">
|
||
<span>
|
||
<Button color="primary" style={{}} variant="text" onClick={() => {
|
||
setAuthenticationModalOpen(true)
|
||
}}>
|
||
<AddIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
: null}
|
||
{selectedAction.authentication !== undefined && selectedAction.authentication.length > 0 ?
|
||
<div style={{marginTop: 20, overflow: "hidden",}}>
|
||
Authentication
|
||
<div style={{display: "flex"}}>
|
||
<Select
|
||
labelId="select-app-auth"
|
||
value={selectedAction.selectedAuthentication}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
console.log("CHOSE AN AUTHENTICATION OPTION: ", e.target.value)
|
||
selectedAction.selectedAuthentication = e.target.value
|
||
selectedAction.authentication_id = e.target.value.id
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: 50, maxWidth: rightsidebarStyle.maxWidth-80,}}
|
||
>
|
||
{selectedAction.authentication.map(data => (
|
||
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data.label} - ({data.app.app_version})
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
|
||
{/*
|
||
|
||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="contained" onClick={() => setAuthenticationModalOpen(true)}>
|
||
AUTHENTICATE
|
||
</Button>
|
||
curaction.authentication = authenticationOptions
|
||
if (curaction.selectedAuthentication === null || curaction.selectedAuthentication === undefined || curaction.selectedAuthentication.length === "")
|
||
*/}
|
||
<Tooltip color="primary" title={"Add authentication option"} placement="top">
|
||
<IconButton color="primary" style={{}} onClick={() => {
|
||
setAuthenticationModalOpen(true)
|
||
}}>
|
||
<AddIcon />
|
||
</IconButton>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
: null}
|
||
{showEnvironment ?
|
||
<div style={{marginTop: "20px"}}>
|
||
<Typography>
|
||
Environment
|
||
</Typography>
|
||
<Select
|
||
value={selectedActionEnvironment === undefined || selectedActionEnvironment.Name === undefined ? "" : selectedActionEnvironment.Name}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
const env = environments.find(a => a.Name === e.target.value)
|
||
setSelectedActionEnvironment(env)
|
||
selectedAction.environment = env.Name
|
||
setSelectedAction(selectedAction)
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{environments.map(data => {
|
||
if (data.archived) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data.Name} style={{backgroundColor: inputColor, color: "white"}} value={data.Name}>
|
||
{data.Name}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</div>
|
||
: null}
|
||
{workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ?
|
||
<div style={{marginTop: "20px"}}>
|
||
Set execution variable (optional)
|
||
<Select
|
||
value={selectedAction.execution_variable !== undefined ? selectedAction.execution_variable.name : "No selection"}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
if (e.target.value === "No selection") {
|
||
selectedAction.execution_variable = {"name": "No selection"}
|
||
} else {
|
||
const value = workflow.execution_variables.find(a => a.name === e.target.value)
|
||
selectedAction.execution_variable = value
|
||
}
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value="No selection">
|
||
<em>No selection</em>
|
||
</MenuItem>
|
||
<Divider />
|
||
{workflow.execution_variables.map(data => (
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||
{data.name}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
: null}
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div style={{marginBottom: 5}}>
|
||
<b>Actions</b>
|
||
</div>
|
||
<Select
|
||
value={selectedActionName}
|
||
fullWidth
|
||
onChange={setNewSelectedAction}
|
||
style={{backgroundColor: inputColor, color: "white", height: 50, borderRadius: borderRadius,}}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
maxHeight: 200,
|
||
borderRadius: borderRadius,
|
||
}
|
||
}}
|
||
>
|
||
{sortByKey(selectedApp.actions, "label").map(data => {
|
||
var newActionname = data.name
|
||
if (data.label !== undefined && data.label !== null && data.label.length > 0) {
|
||
newActionname = data.label
|
||
}
|
||
// ROFL FIXME - loop
|
||
newActionname = newActionname.replace("_", " ")
|
||
newActionname = newActionname.replace("_", " ")
|
||
newActionname = newActionname.replace("_", " ")
|
||
newActionname = newActionname.replace("_", " ")
|
||
newActionname = newActionname.charAt(0).toUpperCase()+newActionname.substring(1)
|
||
return (
|
||
<MenuItem key={data.name} style={{maxWidth: 400, overflowX: "hidden", backgroundColor: inputColor, color: "white"}} value={data.name}>
|
||
{newActionname}
|
||
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
{selectedAction.description !== undefined && selectedAction.description.length > 0 ?
|
||
<div style={{marginTop: 10, marginBottom: 10, maxHeight: 60, overflow: "hidden"}}>
|
||
{selectedAction.description}
|
||
</div> : null}
|
||
<div style={{marginTop: "10px", borderColor: "white", borderWidth: "2px", marginBottom: 200}}>
|
||
<AppActionArguments key={selectedAction.id} selectedAction={selectedAction} />
|
||
|
||
</div>
|
||
<div style={{}}>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
: null
|
||
|
||
|
||
const setTriggerFolderWrapperMulti = event => {
|
||
const { options } = event.target
|
||
const value = []
|
||
for (let i = 0, l = options.length; i < l; i += 1) {
|
||
if (options[i].selected) {
|
||
value.push(options[i].value)
|
||
}
|
||
}
|
||
|
||
if (selectedTrigger.parameters === null) {
|
||
selectedTrigger.parameters = [[]]
|
||
workflow.triggers[selectedTriggerIndex].parameters = [[]]
|
||
}
|
||
|
||
// This is a dirty workaround for the static values in the go backend and datastore db
|
||
const fixedValue = value.join(splitter)
|
||
selectedTrigger.parameters[0] = {"value": fixedValue, "name": "outlookfolder"}
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": fixedValue, "name": "outlookfolder"}
|
||
|
||
// This resets state for some reason (:
|
||
setSelectedActionName({})
|
||
setSelectedAction({})
|
||
setSelectedTrigger({})
|
||
setSelectedApp({})
|
||
setSelectedEdge({})
|
||
|
||
// Set value
|
||
setSelectedTrigger(selectedTrigger)
|
||
setWorkflow(workflow)
|
||
};
|
||
|
||
//const setTriggerFolderWrapper = (event) => {
|
||
// if (selectedTrigger.parameters === null) {
|
||
// selectedTrigger.parameters = []
|
||
// workflow.triggers[selectedTriggerIndex].parameters = []
|
||
// }
|
||
|
||
// const folder = triggerFolders.find(a => a.displayName === event.target.value)
|
||
// console.log(event.target.value)
|
||
// console.log(folder)
|
||
//
|
||
// if (folder !== undefined) {
|
||
// workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": folder.displayName, "name": "outlookfolder", "id": folder.id}
|
||
// selectedTrigger.parameters[0] = {"value": folder.displayName, "name": "outlookfolder", "id": folder.id}
|
||
// setWorkflow(workflow)
|
||
|
||
// // This resets state for some reason (:
|
||
// setSelectedActionName({})
|
||
// setSelectedActionEnvironment({})
|
||
// setSelectedAction({})
|
||
// setSelectedTrigger({})
|
||
// setSelectedApp({})
|
||
// setSelectedEdge({})
|
||
|
||
// // Set value
|
||
// setSelectedTrigger(selectedTrigger)
|
||
|
||
// } else {
|
||
// alert.error("Some error occurred with folder "+event.target.value)
|
||
// }
|
||
//}
|
||
|
||
const setTriggerCronWrapper = (value) => {
|
||
if (selectedTrigger.parameters === null) {
|
||
selectedTrigger.parameters = []
|
||
}
|
||
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "cron"}
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
const setTriggerOptionsWrapper = (value) => {
|
||
if (selectedTrigger.parameters === null) {
|
||
selectedTrigger.parameters = []
|
||
}
|
||
|
||
const splitItems = workflow.triggers[selectedTriggerIndex].parameters[2].value.split(",")
|
||
console.log(splitItems)
|
||
if (splitItems.includes(value)) {
|
||
for( var i = 0; i < splitItems.length; i++){
|
||
if (splitItems[i] === value) {
|
||
splitItems.splice(i, 1)
|
||
}
|
||
}
|
||
|
||
} else {
|
||
splitItems.push(value)
|
||
}
|
||
|
||
for( var i = 0; i < splitItems.length; i++){
|
||
if (splitItems[i] === "") {
|
||
splitItems.splice(i, 1)
|
||
}
|
||
}
|
||
|
||
workflow.triggers[selectedTriggerIndex].parameters[2].value = splitItems.join(",")
|
||
|
||
console.log(splitItems)
|
||
setWorkflow(workflow)
|
||
setLocalFirstrequest(!localFirstrequest)
|
||
}
|
||
|
||
const setTriggerTextInformationWrapper = (value) => {
|
||
if (selectedTrigger.parameters === null) {
|
||
selectedTrigger.parameters = []
|
||
}
|
||
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "alertinfo"}
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
const setTriggerBodyWrapper = (value) => {
|
||
if (selectedTrigger.parameters === null) {
|
||
selectedTrigger.parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "cron"}
|
||
}
|
||
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"value": value, "name": "execution_argument"}
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
const AppConditionHandler = (props) => {
|
||
const { tmpdata, type } = props
|
||
const [data, ] = useState(tmpdata)
|
||
const [multiline, setMultiline] = useState(false)
|
||
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
|
||
const [actionlist, setActionlist] = React.useState([])
|
||
|
||
if (tmpdata === undefined) {
|
||
return tmpdata
|
||
}
|
||
|
||
if (data.variant === "") {
|
||
data.variant = "STATIC_VALUE"
|
||
}
|
||
|
||
|
||
// Set actions based on NEXT node, since it should be able to involve those two
|
||
//console.log("IN APPACTIONARG: ", selectedEdge)
|
||
if (actionlist.length === 0) {
|
||
// FIXME: Have previous execution values in here
|
||
actionlist.push({"type": "Execution Argument", "name": "Execution Argument", "value": "$exec", "highlight": "exec", "autocomplete": "exec", "example": "hello"})
|
||
|
||
if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) {
|
||
for (var key in workflow.workflow_variables) {
|
||
const item = workflow.workflow_variables[key]
|
||
actionlist.push({"type": "workflow_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": item.value})
|
||
}
|
||
}
|
||
|
||
// FIXME: Add values from previous executions if they exist
|
||
if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) {
|
||
for (var key in workflow.execution_variables) {
|
||
const item = workflow.execution_variables[key]
|
||
actionlist.push({"type": "execution_variable", "name": item.name, "value": item.value, "id": item.id, "autocomplete": `${item.name.split(" ").join("_")}`, "example": ""})
|
||
}
|
||
}
|
||
|
||
const destAction = cy.getElementById(selectedEdge.target)
|
||
//console.log(destAction.data())
|
||
var parents = getParents(destAction.data())
|
||
if (parents.length > 1) {
|
||
for (var key in parents) {
|
||
const item = parents[key]
|
||
if (item.label === "Execution Argument") {
|
||
continue
|
||
}
|
||
|
||
// 1. Take
|
||
const actionvalue = {"type": "action", "id": item.id, "name": item.label, "autocomplete": `${item.label.split(" ").join("_")}`, "example": item.example === undefined ? "" : item.example}
|
||
actionlist.push(actionvalue)
|
||
}
|
||
}
|
||
|
||
setActionlist(actionlist)
|
||
}
|
||
|
||
var staticcolor = "inherit"
|
||
var actioncolor = "inherit"
|
||
var varcolor = "inherit"
|
||
if (data.multiline !== undefined && data.multiline !== null && data.multiline === true) {
|
||
setMultiline(true)
|
||
}
|
||
|
||
var placeholder = "Static value"
|
||
if (data.example !== undefined && data.example !== null && data.example.length > 0) {
|
||
placeholder = data.example
|
||
}
|
||
|
||
var datafield =
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
multiline={multiline}
|
||
rows={5}
|
||
color="primary"
|
||
defaultValue={data.value}
|
||
placeholder={placeholder}
|
||
helperText={data.value !== undefined && data.value !== null && data.value.includes(".#") ?
|
||
<span style={{color:"white", marginBottom: 5, marginleft: 5,}}>
|
||
Use "Shuffle Tools" app with "Filter List" action to handle loops
|
||
</span>
|
||
: null
|
||
}
|
||
onClick={() => {
|
||
console.log("CHANGE FIELD")
|
||
}}
|
||
onBlur={(e) => {
|
||
changeActionVariable(data.action_field, e.target.value)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
|
||
const changeActionVariable = (variable, value) => {
|
||
// set the name
|
||
data.value = value
|
||
data.action_field = variable
|
||
|
||
//setConditionValue({})
|
||
|
||
if (type === "source") {
|
||
setSourceValue(data)
|
||
//setDestinationValue(destinationValue)
|
||
} else if (type === "destination") {
|
||
setDestinationValue(data)
|
||
//setSourceValue(sourceValue)
|
||
}
|
||
}
|
||
|
||
const changeActionParameterVariant = (variant) => {
|
||
if (data.variant === variant) {
|
||
return
|
||
}
|
||
|
||
data.variant = variant
|
||
data.value = ""
|
||
|
||
if (variant === "ACTION_RESULT") {
|
||
console.log("SHOULD FIND PARENTS OF EDGE")
|
||
|
||
// Uses the target's parents, as the target should be executing the checks (I think)
|
||
var parents = getParents(workflow.actions.find(a => a.id === selectedEdge["target"]))
|
||
if (parents.length > 0) {
|
||
data.action_field = parents[0].label
|
||
} else {
|
||
data.action_field = ""
|
||
}
|
||
} else if (variant === "WORKFLOW_VARIABLE") {
|
||
if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) {
|
||
data.action_field = workflow.workflow_variables[0].name
|
||
}
|
||
}
|
||
|
||
setSourceValue({})
|
||
setConditionValue({})
|
||
setDestinationValue({})
|
||
|
||
if (type === "source") {
|
||
setSourceValue(data)
|
||
setDestinationValue(destinationValue)
|
||
} else if (type === "destination") {
|
||
setDestinationValue(data)
|
||
setSourceValue(sourceValue)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>{data.name} </b>
|
||
</div>
|
||
</div>
|
||
{datafield}
|
||
{actionlist.length === 0 ? null :
|
||
<FormControl fullWidth>
|
||
<InputLabel id="action-autocompleter" style={{marginLeft: 10, color: "white"}}>Autocomplete</InputLabel>
|
||
<Select
|
||
labelId="action-autocompleter"
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setShowAutocomplete(false)
|
||
|
||
//if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === "$") {
|
||
// setShowDropdown(false)
|
||
//}
|
||
|
||
setUpdate(Math.random())
|
||
}}
|
||
onClick={() => {
|
||
setShowAutocomplete(true)
|
||
}}
|
||
open={showAutocomplete}
|
||
fullWidth
|
||
style={{borderBottom: `1px solid #f85a3e`, color: "white", height: 50, marginTop: 2,}}
|
||
onChange={(e) => {
|
||
const autocomplete = e.target.value.autocomplete
|
||
const newValue = autocomplete.startsWith("$") ? data.value+autocomplete : `${data.value}$${autocomplete}`
|
||
|
||
changeActionVariable(data.action_field, newValue)
|
||
}}
|
||
>
|
||
{actionlist.map(data => {
|
||
const icon = data.type === "action" ? <AppsIcon style={{marginRight: 10}} /> : data.type === "workflow_variable" || data.type === "execution_variable" ? <FavoriteBorderIcon style={{marginRight: 10}} /> : <ScheduleIcon style={{marginRight: 10}} />
|
||
|
||
const handleExecArgumentHover = (inside) => {
|
||
var exec_text_field = document.getElementById("execution_argument_input_field")
|
||
if (exec_text_field !== null) {
|
||
if (inside) {
|
||
exec_text_field.style.border = "2px solid #f85a3e"
|
||
} else {
|
||
exec_text_field.style.border = ""
|
||
}
|
||
}
|
||
|
||
// Also doing arguments
|
||
if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > 0) {
|
||
for (var key in workflow.triggers) {
|
||
const item = workflow.triggers[key]
|
||
|
||
var node = cy.getElementById(item.id)
|
||
if (node.length > 0) {
|
||
if (inside) {
|
||
node.addClass('shuffle-hover-highlight')
|
||
} else {
|
||
node.removeClass('shuffle-hover-highlight')
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleActionHover = (inside, actionId) => {
|
||
var node = cy.getElementById(actionId)
|
||
if (node.length > 0) {
|
||
if (inside) {
|
||
node.addClass('shuffle-hover-highlight')
|
||
} else {
|
||
node.removeClass('shuffle-hover-highlight')
|
||
}
|
||
}
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data.name} style={{backgroundColor: inputColor, color: "white"}} value={data} onMouseOver={() => {
|
||
if (data.type === "Execution Argument") {
|
||
handleExecArgumentHover(true)
|
||
} else if (data.type === "action") {
|
||
handleActionHover(true, data.id)
|
||
}
|
||
}} onMouseOut={() => {
|
||
if (data.type === "Execution Argument") {
|
||
handleExecArgumentHover(false)
|
||
} else if (data.type === "action") {
|
||
handleActionHover(false, data.id)
|
||
}
|
||
}}>
|
||
<Tooltip color="primary" title={`Value: ${data.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {data.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</FormControl>
|
||
}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
const menuItemStyle = {
|
||
color: "white",
|
||
backgroundColor: inputColor
|
||
}
|
||
|
||
const conditionsModal =
|
||
<Dialog modal
|
||
open={conditionsModalOpen}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
minWidth: 800,
|
||
},
|
||
}}
|
||
onClose={() => {
|
||
setConditionsModalOpen(false)
|
||
setSourceValue({})
|
||
setConditionValue({})
|
||
setDestinationValue({})
|
||
}}
|
||
>
|
||
<span style={{position: "absolute", bottom: 10, left: 10, color: "rgba(255,255,255,0.6)",}}>
|
||
Conditions can't be used for loops [ .# ] <a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#conditions" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a>
|
||
</span>
|
||
<FormControl>
|
||
<DialogTitle><span style={{color:"white"}}>Condition</span>
|
||
</DialogTitle>
|
||
<DialogContent style={{}}>
|
||
|
||
<div style={{display: "flex"}}>
|
||
<Tooltip color="primary" title={conditionValue.configuration ? "Negated" : "Default"} placement="top">
|
||
<span style={{margin: "auto", height: 50, marginBottom: "auto", marginTop: "auto", marginRight: 5}}>
|
||
<Button color="primary" variant={conditionValue.configuration ? "contained" : "outlined"} style={{margin: "auto", height: 50, marginBottom: "auto", marginTop: "auto", marginRight: 5}} onClick={(e) => {
|
||
conditionValue.configuration = !conditionValue.configuration
|
||
setConditionValue(conditionValue)
|
||
setUpdate(Math.random())
|
||
}}>
|
||
{conditionValue.configuration ? "!" : "="}
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<div style={{flex: "2"}}>
|
||
<AppConditionHandler tmpdata={sourceValue} setData={setSourceValue} type={"source"} />
|
||
</div>
|
||
<div style={{flex: "1", margin: "auto", marginBottom: 0, marginLeft: 5, marginRight: 5,}}>
|
||
<Button color="primary" variant="outlined" style={{margin: "auto", height: 50, marginBottom: 50,}} fullWidth aria-haspopup="true" onClick={(e) => {setVariableAnchorEl(e.currentTarget)}}>
|
||
{conditionValue.value}
|
||
</Button>
|
||
<Menu
|
||
id="simple-menu"
|
||
keepMounted
|
||
open={Boolean(variableAnchorEl)}
|
||
anchorEl={variableAnchorEl}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
|
||
onClose={() => {
|
||
setVariableAnchorEl(null)
|
||
}}
|
||
>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "equals"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"equals"}>equals</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "does not equal"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"does not equal"}>does not equal</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "startswith"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"starts with"}>starts with</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "endswith"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"ends with"}>ends with</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "contains"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"contains"}>contains</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "contains_any_of"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"contains_any_of"}>contains any of</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "matches regex"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"matches regex"}>matches regex</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "larger than"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"larger than"}>larger than</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "less than"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"less than"}>less than</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
<div style={{flex: "2"}}>
|
||
<AppConditionHandler tmpdata={destinationValue} setData={setDestinationValue} type={"destination"} />
|
||
</div>
|
||
|
||
</div>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setSelectedEdge({})
|
||
|
||
var data = {
|
||
condition: conditionValue,
|
||
source: sourceValue,
|
||
destination: destinationValue,
|
||
}
|
||
|
||
setConditionsModalOpen(false)
|
||
if (selectedEdge.conditions === undefined) {
|
||
selectedEdge.conditions = [data]
|
||
} else {
|
||
const curedgeindex = selectedEdge.conditions.findIndex(data => data.source.id === sourceValue.id)
|
||
if (curedgeindex < 0) {
|
||
selectedEdge.conditions.push(data)
|
||
} else {
|
||
selectedEdge.conditions[curedgeindex] = data
|
||
}
|
||
}
|
||
|
||
setSelectedEdge(selectedEdge)
|
||
workflow.branches[selectedEdgeIndex] = selectedEdge
|
||
setWorkflow(workflow)
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</FormControl>
|
||
</Dialog>
|
||
|
||
const EdgeSidebar = () => {
|
||
const ConditionHandler = (condition, index) => {
|
||
const [open, setOpen] = React.useState(false)
|
||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||
|
||
const duplicateCondition = (conditionIndex) => {
|
||
|
||
var newEdge = JSON.parse(JSON.stringify(selectedEdge.conditions[conditionIndex]))
|
||
const newUuid = uuid.v4()
|
||
newEdge.condition.id = newUuid
|
||
newEdge.source.id = newUuid
|
||
newEdge.destination.id = newUuid
|
||
selectedEdge.conditions.push(newEdge)
|
||
|
||
setUpdate(Math.random())
|
||
}
|
||
|
||
const deleteCondition = (conditionIndex) => {
|
||
console.log(selectedEdge)
|
||
if (selectedEdge.conditions.length === 1) {
|
||
selectedEdge.conditions = []
|
||
} else {
|
||
selectedEdge.conditions.splice(conditionIndex, 1)
|
||
}
|
||
|
||
setSelectedEdge(selectedEdge)
|
||
setOpen(false)
|
||
setUpdate(Math.random())
|
||
}
|
||
|
||
const paperVariableStyle = {
|
||
minHeight: 75,
|
||
maxHeight: 75,
|
||
minWidth: "100%",
|
||
maxWidth: "100%",
|
||
marginTop: "5px",
|
||
color: "white",
|
||
backgroundColor: surfaceColor,
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
}
|
||
|
||
const menuClick = (event) => {
|
||
console.log("MENU CLICK")
|
||
setOpen(!open)
|
||
setAnchorEl(event.currentTarget)
|
||
}
|
||
|
||
return (
|
||
<Paper key={condition.condition.id} square style={paperVariableStyle} onClick={() => {}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}} />
|
||
<div style={{display: "flex", width: "100%"}}>
|
||
<div style={{flex: "10", display: "flex"}} onClick={() => {
|
||
setSourceValue(condition.source)
|
||
setConditionValue(condition.condition)
|
||
setDestinationValue(condition.destination)
|
||
setConditionsModalOpen(true)
|
||
}}>
|
||
<div style={{flex: 1, textAlign: "left", marginTop: "15px", marginLeft: "10px", overflow: "hidden", maxWidth: 72, }}>
|
||
{condition.source.value}
|
||
</div>
|
||
<Divider style={{height: "100%", width: "1px", marginLeft: "5px", marginRight: "5px", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: 1, textAlign: "center", marginTop: "15px", overflow: "hidden", maxWidth: 72,}} onClick={() => {}}>
|
||
{condition.condition.value}
|
||
</div>
|
||
<Divider style={{height: "100%", width: "1px", marginLeft: "5px", marginRight: "5px", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: 1, textAlign: "left", marginTop: "auto", marginBottom: "auto", marginLeft: "10px", overflow: "hidden", maxWidth: 72, }}>
|
||
{condition.destination.value}
|
||
</div>
|
||
</div>
|
||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||
<IconButton
|
||
aria-label="more"
|
||
aria-controls="long-menu"
|
||
aria-haspopup="true"
|
||
onClick={menuClick}
|
||
style={{color: "white",}}
|
||
>
|
||
<MoreVertIcon />
|
||
</IconButton>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={anchorEl}
|
||
keepMounted
|
||
open={open}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setOpen(false)
|
||
setAnchorEl(null)
|
||
}}
|
||
>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
duplicateCondition(index)
|
||
}} key={"Duplicate"}>{"Duplicate"}</MenuItem>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
setOpen(false)
|
||
deleteCondition(index)
|
||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
</div>
|
||
</Paper>
|
||
)
|
||
}
|
||
|
||
var injectedData =
|
||
<div>
|
||
</div>
|
||
|
||
if (selectedEdge.conditions !== undefined && selectedEdge.conditions !== null && selectedEdge.conditions.length > 0) {
|
||
injectedData = selectedEdge.conditions.map((condition, index) => {
|
||
return ConditionHandler(condition, index)
|
||
})
|
||
}
|
||
|
||
// FIXME - remove index
|
||
const conditionId = uuid.v4()
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}} >Branch: Conditions - {selectedEdgeIndex}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Conditions
|
||
</div>
|
||
{injectedData}
|
||
|
||
<Button style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
|
||
setSourceValue({"name": "source", "value": "", "variant": "STATIC_VALUE", "action_field": "", "id": conditionId})
|
||
setConditionValue({"name": "condition", "value": "equals", "id": conditionId})
|
||
setDestinationValue({"name": "destination", "value": "", "variant": "STATIC_VALUE", "action_field": "", "id": conditionId})
|
||
setConditionsModalOpen(true)
|
||
}} fullWidth>New condition</Button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 1. GET the trigger authentication data
|
||
// 2. Parse the fields that are used (outlook & gmail)
|
||
// 3. Parse the folders that are selected
|
||
// 4. Start / stop
|
||
const EmailSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length === 0) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex] === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}]
|
||
selectedTrigger.parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}]
|
||
setWorkflow(workflow)
|
||
setSelectedTrigger(selectedTrigger)
|
||
}
|
||
|
||
const setFolders = () => {
|
||
fetch(globalUrl+"/functions/outlook/getFolders?trigger_id="+selectedTrigger.id, {
|
||
method: "GET",
|
||
headers: {"content-type": "application/json"},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
throw new Error("No folders :o!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
setTriggerFolders(responseJson)
|
||
if (workflow.triggers[selectedTriggerIndex].parameters.length === 0 && responseJson.length > 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}]
|
||
selectedTrigger.parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}]
|
||
setWorkflow(workflow)
|
||
setSelectedTrigger(selectedTrigger)
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.log(error.toString())
|
||
})
|
||
}
|
||
|
||
const getTriggerAuth = () => {
|
||
fetch(globalUrl+"/api/v1/triggers/"+selectedTrigger.id, {
|
||
method: "GET",
|
||
headers: {"content-type": "application/json"},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
throw new Error("No trigger info :o!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
setTriggerAuthentication(responseJson)
|
||
})
|
||
.catch(error => {
|
||
console.log(error.toString())
|
||
})
|
||
}
|
||
|
||
// Getting the triggers and the folders if they exist
|
||
// This is horrible hahah
|
||
if (localFirstrequest) {
|
||
getTriggerAuth()
|
||
setFolders()
|
||
setLocalFirstrequest(false)
|
||
}
|
||
|
||
const outlookButton =
|
||
<Button variant="contained" style={{flex: "1",}} onClick={() => {
|
||
// When window closes, it should get all the folders for the user from backend
|
||
const redirectUri = "http%3A%2F%2Fshuffler.io"
|
||
const url = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=70e37005-c954-4290-b573-d4b94e484336&redirect_uri="+redirectUri+"%2Ffunctions%2Foutlook%2Fregister&resource=https%3A%2F%2Fgraph.microsoft.com&response_type=code&scope=Mail.Read+User.Read+https%3A%2F%2Foutlook.office.com%2Fmail.read&state=workflow_id%3D"+props.match.params.key+"%26trigger_id%3D"+selectedTrigger.id+"%26username%3Drheyix.yt@gmail.com%26type%3Doutlook"
|
||
|
||
var newwin = window.open(url, "", "width=200,height=100")
|
||
var data = {}
|
||
|
||
// Check whether we got a callback somewhere
|
||
var id = setInterval(function () {
|
||
fetch(globalUrl+"/api/v1/triggers/"+selectedTrigger.id, {
|
||
method: "GET",
|
||
headers: {"content-type": "application/json"},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
throw new Error("No trigger info :o!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
setTriggerAuthentication(responseJson)
|
||
clearInterval(id)
|
||
newwin.close()
|
||
setFolders()
|
||
})
|
||
.catch(error => {
|
||
console.log(error.toString())
|
||
})
|
||
}, 2500)
|
||
|
||
console.log(data)
|
||
saveWorkflow(workflow)
|
||
}} color="primary">
|
||
Outlook
|
||
</Button>
|
||
|
||
|
||
// FIXME - set everything in here to multifolder etc
|
||
var triggerInfo = "SET UP BUT NO TYPE :)"
|
||
if (Object.getOwnPropertyNames(triggerAuthentication).length > 0) {
|
||
// Should get the folders if they don't already exist
|
||
|
||
if (triggerAuthentication.type === "outlook") {
|
||
triggerInfo = <div>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Login: </b>
|
||
</div>
|
||
</div>
|
||
{outlookButton}
|
||
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Folders: </b>(hold CTRL to select multiple)
|
||
</div>
|
||
</div>
|
||
<Select
|
||
multiple
|
||
native
|
||
rows="10"
|
||
value={selectedTrigger.parameters[0].value.split(splitter)}
|
||
style={{backgroundColor: inputColor, color: "white"}}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
onChange={(e) => {
|
||
//setTriggerFolderWrapper(e)
|
||
setTriggerFolderWrapperMulti(e)
|
||
}}
|
||
fullWidth
|
||
input={<Input id="select-multiple-native" />}
|
||
key={selectedTrigger}
|
||
>
|
||
{triggerFolders.map(folder => {
|
||
var folderItem = <option key={folder.displayName} value={folder.displayName} style={{marginLeft: "10px", fontSize: "1.2em"}}>
|
||
{folder.displayName}
|
||
</option>
|
||
if (folder.childFolderCount > 0) {
|
||
// Here to handle subfolders sometime later
|
||
folderItem =
|
||
<option key={folder.displayName} value={folder.displayName} style={{marginLeft: "10px", fontSize: "1.2em"}}>
|
||
{folder.displayName}
|
||
</option>
|
||
}
|
||
|
||
return folderItem
|
||
})}
|
||
</Select>
|
||
</div>
|
||
} else if (triggerAuthentication.type === "gmail") {
|
||
triggerInfo = "SPECIAL GMAIL"
|
||
}
|
||
}
|
||
|
||
|
||
// Check
|
||
const argumentView = Object.getOwnPropertyNames(triggerAuthentication).length > 0 ?
|
||
<div>
|
||
{triggerInfo}
|
||
</div>
|
||
:
|
||
<div>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Login to either: </b>
|
||
</div>
|
||
</div>
|
||
{outlookButton}
|
||
<Button variant="contained" style={{marginLeft: "5px", flex: "1",}} onClick={() => {
|
||
alert.error("REMOVE THIS FIELD FOR GMAIL - make it oAuth something")
|
||
}} color="primary">
|
||
Gmail
|
||
</Button>
|
||
</div>
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#email" style={{textDecoration: "none", color: "#f85a3e"}}>What are email triggers?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
|
||
<div style={{marginTop: "20px"}}>
|
||
Environment:
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
required
|
||
disabled
|
||
fullWidth
|
||
color="primary"
|
||
value={selectedTrigger.environment}
|
||
/>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
{argumentView}
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<Button style={{flex: "1",}} disabled={selectedTrigger.status === "running"} onClick={() => {
|
||
startMailSub(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Start
|
||
</Button>
|
||
<Button style={{flex: "1",}} disabled={selectedTrigger.status !== "running" } onClick={() => {
|
||
stopMailSub(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const SubflowSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||
if (workflow.triggers[selectedTriggerIndex] === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "workflow", "value": ""}
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "argument", "value": ""}
|
||
workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "user_apikey", "value": ""}
|
||
workflow.triggers[selectedTriggerIndex].parameters[3] = {"name": "startnode", "value": ""}
|
||
|
||
console.log("SETTINGS: ", userSettings)
|
||
if (userSettings !== undefined && userSettings !== null && userSettings.apikey !== null && userSettings.apikey !== undefined && userSettings.apikey.length > 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "user_apikey", "value": userSettings.apikey}
|
||
}
|
||
}
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#subflow" style={{textDecoration: "none", color: "#f85a3e"}}>What are subflows?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Select a workflow to execute </b>
|
||
</div>
|
||
</div>
|
||
{workflows === undefined || workflows === null || workflows.length === 0 ? null :
|
||
<Select
|
||
value={subworkflow}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setSubworkflow(e.target.value)
|
||
setUpdate(Math.random())
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = e.target.value.id
|
||
setWorkflow(workflow)
|
||
setSubworkflowStartnode(e.target.value.start)
|
||
|
||
// Sets the startnode
|
||
if (e.target.value.id !== workflow.id) {
|
||
const startnode = e.target.value.actions.find(action => action.id === e.target.value.start)
|
||
if (startnode !== undefined && startnode !== null) {
|
||
//ddsetSubworkflowStartnode(innernode)
|
||
setSubworkflowStartnode(startnode)
|
||
|
||
try {
|
||
workflow.triggers[selectedTriggerIndex].parameters[3].value = e.target.value.id
|
||
} catch {
|
||
workflow.triggers[selectedTriggerIndex].parameters[3] = {
|
||
"name": "startnode",
|
||
"value": e.target.value.id,
|
||
}
|
||
}
|
||
}
|
||
console.log("STARTNODE: ", startnode)
|
||
}
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{workflows.map((data, index) => {
|
||
/*
|
||
if (data.id === workflow.id) {
|
||
return null
|
||
}
|
||
*/
|
||
|
||
return (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: data.id === workflow.id ? "red" : "white"}} value={data}>
|
||
{data.name}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
}
|
||
{workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null :
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value === props.match.params.key ? null :
|
||
<span style={{marginTop: 5}}><a rel="norefferer" href={`/workflows/${workflow.triggers[selectedTriggerIndex].parameters[0].value}`} target="_blank" style={{textDecoration: "none", color: "#f85a3e", marginLeft: 5,}}>Explore selected workflow</a></span>
|
||
}
|
||
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Select the Startnode</b>
|
||
</div>
|
||
</div>
|
||
{subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null :
|
||
<Select
|
||
value={subworkflowStartnode}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setSubworkflowStartnode(e.target.value)
|
||
|
||
try {
|
||
workflow.triggers[selectedTriggerIndex].parameters[3].value = e.target.value.id
|
||
} catch {
|
||
workflow.triggers[selectedTriggerIndex].parameters[3] = {
|
||
"name": "startnode",
|
||
"value": e.target.value.id,
|
||
}
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
//setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{subworkflow.actions.map((action, index) => {
|
||
//console.log(action)
|
||
return (
|
||
<MenuItem disabled={getParents(selectedTrigger).find(parent => parent.id === action.id)} key={index} style={{backgroundColor: inputColor, color: "white"}} value={action}>
|
||
{action.label}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
}
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Execution Argument</b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
rows="6"
|
||
multiline
|
||
fullWidth
|
||
color="primary"
|
||
placeholder="Some execution data"
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[1].value}
|
||
onBlur={(e) => {
|
||
console.log("DATA: ", e.target.value)
|
||
workflow.triggers[selectedTriggerIndex].parameters[1].value = e.target.value
|
||
setWorkflow(workflow)
|
||
}}
|
||
/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>API-key: </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
height: 50,
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder="Your apikey"
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[2].value}
|
||
onBlur={(e) => {
|
||
workflow.triggers[selectedTriggerIndex].parameters[2].value = e.target.value
|
||
setWorkflow(workflow)
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const WebhookSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||
if (workflow.triggers[selectedTriggerIndex] === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "url", "value": referenceUrl+"webhook_"+selectedTrigger.id}
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "tmp", "value": "webhook_"+selectedTrigger.id}
|
||
setWorkflow(workflow)
|
||
}
|
||
//const cronValue = "*/15 * * * *"
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
<div style={{marginTop: "20px"}}>
|
||
<Typography>
|
||
Environment
|
||
</Typography>
|
||
<Select
|
||
value={selectedTrigger.environment}
|
||
disabled={selectedTrigger.status === "running"}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
selectedTrigger.environment = e.target.value
|
||
setSelectedTrigger(selectedTrigger)
|
||
if (e.target.value === "cloud") {
|
||
var tmpvalue = workflow.triggers[selectedTriggerIndex].parameters[0].value.split("/")
|
||
const urlpath = tmpvalue.slice(3, tmpvalue.length)
|
||
const newurl = "https://shuffler.io/"+urlpath.join("/")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = newurl
|
||
} else {
|
||
var tmpvalue = workflow.triggers[selectedTriggerIndex].parameters[0].value.split("/")
|
||
const urlpath = tmpvalue.slice(3, tmpvalue.length)
|
||
const newurl = window.location.origin+"/"+urlpath.join("/")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = newurl
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{triggerEnvironments.map(data => {
|
||
if (data.archived) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Webhook URI </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
id="webhook_uri_field"
|
||
onClick={() => {
|
||
var copyText = document.getElementById("webhook_uri_field");
|
||
navigator.clipboard.writeText(copyText.value)
|
||
copyText.select();
|
||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||
|
||
/* Copy the text inside the text field */
|
||
document.execCommand("copy");
|
||
alert.success("Copied Webhook URL")
|
||
}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
disabled
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[0].value}
|
||
color="primary"
|
||
placeholder="defaultValue"
|
||
onBlur={(e) => {
|
||
setTriggerCronWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<Button variant="contained" style={{flex: "1",}} disabled={selectedTrigger.status === "running"} onClick={() => {
|
||
newWebhook(selectedTrigger)
|
||
}} color="primary">
|
||
Start
|
||
</Button>
|
||
<Button variant="contained" style={{flex: "1",}} disabled={selectedTrigger.status !== "running"} onClick={() => {
|
||
deleteWebhook(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
//const getTriggerAuth = (trigger_id) => {
|
||
// fetch(globalUrl+"/api/v1/triggers/"+trigger_id, {
|
||
// method: "GET",
|
||
// headers: {"content-type": "application/json"},
|
||
// credentials: "include",
|
||
// })
|
||
// .then((response) => response.json())
|
||
// .then((responseJson) => {
|
||
// if (responseJson.success) {
|
||
// console.log("SUCCESS")
|
||
// console.log(responseJson)
|
||
// } else {
|
||
// console.log("FAIL")
|
||
// }
|
||
// })
|
||
// .catch(error => {
|
||
// console.log(error.toString())
|
||
// });
|
||
//}
|
||
|
||
const stopMailSub = (trigger, triggerindex) => {
|
||
// DELETE
|
||
if (trigger.id === undefined) {
|
||
return
|
||
}
|
||
alert.info("Stopping trigger")
|
||
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/outlook/"+trigger.id, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
throw new Error("Status not 200 for stream results :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.success) {
|
||
alert.success("Successfully stopped trigger")
|
||
// Set the status
|
||
workflow.triggers[triggerindex].status = "stopped"
|
||
trigger.status = "stopped"
|
||
setWorkflow(workflow)
|
||
setSelectedTrigger(trigger)
|
||
saveWorkflow(workflow)
|
||
} else {
|
||
alert.error("Failed stopping trigger: "+responseJson.reason)
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
})
|
||
}
|
||
|
||
const startMailSub = (trigger, triggerindex) => {
|
||
var folders = []
|
||
|
||
const splitItem = workflow.triggers[selectedTriggerIndex].parameters[0].value.split(splitter)
|
||
for (var key in splitItem) {
|
||
const item = splitItem[key]
|
||
const curfolder = triggerFolders.find(a => a.displayName === item)
|
||
if (curfolder === undefined) {
|
||
alert.error("Something went wrong with outlook folder "+item)
|
||
return
|
||
}
|
||
|
||
folders.push(curfolder.id)
|
||
}
|
||
|
||
alert.info("Creating outlook subscription with name " + trigger.name)
|
||
const data = {
|
||
"name": trigger.name,
|
||
"folders": folders,
|
||
"id": trigger.id,
|
||
}
|
||
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/outlook", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(data),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
alert.error("Failed to start outlook: " + responseJson.reason)
|
||
} else {
|
||
alert.success("Successfully started outlook sub")
|
||
|
||
workflow.triggers[triggerindex].status = "running"
|
||
trigger.status = "running"
|
||
setWorkflow(workflow)
|
||
setSelectedTrigger(trigger)
|
||
saveWorkflow(workflow)
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
})
|
||
}
|
||
|
||
const newWebhook = (trigger) => {
|
||
const hookname = trigger.label
|
||
if (hookname.length === 0) {
|
||
alert.error("Missing name")
|
||
return
|
||
}
|
||
|
||
if (trigger.id.length !== 36) {
|
||
alert.error("Missing id")
|
||
return
|
||
}
|
||
|
||
// Check the node it's connected to
|
||
var startNode = workflow.start
|
||
const branch = workflow.branches.find(branch => branch.source_id === trigger.id)
|
||
if (branch === undefined && (workflow.start === undefined || workflow.start === null || workflow.start.length === 0)) {
|
||
alert.error("No webhook node defined")
|
||
}
|
||
|
||
alert.info("Starting webhook")
|
||
if (branch !== undefined) {
|
||
startNode = branch.destination_id
|
||
}
|
||
|
||
const data = {
|
||
"name": hookname,
|
||
"type": "webhook",
|
||
"id": trigger.id,
|
||
"workflow": workflow.id,
|
||
"start": startNode,
|
||
"environment": trigger.environment,
|
||
}
|
||
|
||
fetch(globalUrl+"/api/v1/hooks/new", {
|
||
method: "POST",
|
||
headers: {"content-type": "application/json"},
|
||
body: JSON.stringify(data),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => response.json())
|
||
.then((responseJson) => {
|
||
if (responseJson.success) {
|
||
// Set the status
|
||
alert.success("Successfully started webhook")
|
||
trigger.status = "running"
|
||
setSelectedTrigger(trigger)
|
||
workflow.triggers[selectedTriggerIndex].status = "running"
|
||
setWorkflow(workflow)
|
||
saveWorkflow(workflow)
|
||
} else {
|
||
alert.error("Failed starting webhook: "+responseJson.reason)
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.log(error.toString())
|
||
})
|
||
}
|
||
|
||
const deleteWebhook = (trigger, triggerindex) => {
|
||
if (trigger.id === undefined) {
|
||
return
|
||
}
|
||
|
||
alert.info("Stopping webhook")
|
||
|
||
fetch(globalUrl+"/api/v1/hooks/"+trigger.id+"/delete", {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (workflow.triggers[triggerindex] !== undefined) {
|
||
workflow.triggers[triggerindex].status = "stopped"
|
||
}
|
||
|
||
if (responseJson.success) {
|
||
//alert.success("Successfully stopped webhook")
|
||
// Set the status
|
||
saveWorkflow(workflow)
|
||
} else {
|
||
if (responseJson.reason !== undefined) {
|
||
alert.error("Failed stopping webhook: "+responseJson.reason)
|
||
}
|
||
}
|
||
|
||
trigger.status = "stopped"
|
||
setWorkflow(workflow)
|
||
setSelectedTrigger(trigger)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
})
|
||
}
|
||
|
||
const UserinputSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) {
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "alertinfo", "value": "hello this is an alert"}
|
||
|
||
// boolean,
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "options", "value": "boolean"}
|
||
|
||
// email,sms,app ...
|
||
workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "type", "value": "email"}
|
||
|
||
workflow.triggers[selectedTriggerIndex].parameters[3] = {"name": "email", "value": "test@test.com"}
|
||
workflow.triggers[selectedTriggerIndex].parameters[4] = {"name": "sms", "value": "0000000"}
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#user_input" style={{textDecoration: "none", color: "#f85a3e"}}>What is the user input trigger?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
|
||
<div style={{marginTop: "20px"}}>
|
||
Environment:
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
required
|
||
disabled
|
||
fullWidth
|
||
color="primary"
|
||
value={selectedTrigger.environment}
|
||
/>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Information: </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
marginTop: "3px",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
rows="4"
|
||
multiline
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[0].value}
|
||
color="primary"
|
||
placeholder="defaultValue"
|
||
onBlur={(e) => {
|
||
setTriggerTextInformationWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Contact options: </b>
|
||
</div>
|
||
</div>
|
||
<FormGroup style={{paddingLeft: 10, backgroundColor: inputColor}} row>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email")}
|
||
onChange={() => {
|
||
setTriggerOptionsWrapper("email")
|
||
}}
|
||
color="primary"
|
||
value="email"
|
||
/>
|
||
}
|
||
label={<div style={{color: "white"}}>Email</div>}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms")}
|
||
onChange={() => {
|
||
setTriggerOptionsWrapper("sms")
|
||
}}
|
||
color="primary"
|
||
value="sms"
|
||
/>
|
||
}
|
||
label={<div style={{color: "white"}}>SMS</div>}
|
||
/>
|
||
</FormGroup>
|
||
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") ?
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"mail1@company.com,mail2@company.com"}
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[3].value}
|
||
onBlur={(event) => {
|
||
workflow.triggers[selectedTriggerIndex].parameters[3].value = event.target.value
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
: null
|
||
}
|
||
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") ?
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"+474823212,+460203042"}
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[4].value}
|
||
onBlur={(event) => {
|
||
workflow.triggers[selectedTriggerIndex].parameters[4].value = event.target.value
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
: null
|
||
}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const ScheduleSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) {
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "cron", "value": "120"}
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "execution_argument", "value": '{"example": {"json": "is cool"}}'}
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
<div style={{marginTop: "20px"}}>
|
||
<Typography>
|
||
Environment
|
||
</Typography>
|
||
<Select
|
||
value={selectedTrigger.environment}
|
||
disabled={selectedTrigger.status === "running"}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
selectedTrigger.environment = e.target.value
|
||
setSelectedTrigger(selectedTrigger)
|
||
if (e.target.value === "cloud") {
|
||
console.log("Set cloud config")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = "*/2 * * * *"
|
||
|
||
//var tmpvalue = workflow.triggers[selectedTriggerIndex].parameters[0].value.split("/")
|
||
//const urlpath = tmpvalue.slice(3, tmpvalue.length)
|
||
//const newurl = "https://shuffler.io/"+urlpath.join("/")
|
||
//workflow.triggers[selectedTriggerIndex].parameters[0].value = newurl
|
||
} else {
|
||
console.log("Set cloud config")
|
||
//var tmpvalue = workflow.triggers[selectedTriggerIndex].parameters[0].value.split("/")
|
||
//const urlpath = tmpvalue.slice(3, tmpvalue.length)
|
||
//const newurl = window.location.origin+"/"+urlpath.join("/")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = "120"
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{triggerEnvironments.map(data => {
|
||
if (data.archived) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Interval (seconds) </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
disabled={workflow.triggers[selectedTriggerIndex].status === "running"}
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[0].value}
|
||
color="primary"
|
||
placeholder="defaultValue"
|
||
onBlur={(e) => {
|
||
setTriggerCronWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Execution argument: </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
marginTop: "3px",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
disabled={workflow.triggers[selectedTriggerIndex].status === "running"}
|
||
fullWidth
|
||
rows="6"
|
||
multiline
|
||
color="primary"
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[1].value}
|
||
placeholder='{"example": {"json": "is cool"}}'
|
||
onBlur={(e) => {
|
||
setTriggerBodyWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<Button style={{flex: "1",}} variant="contained" disabled={selectedTrigger.status === "running"} onClick={() => {
|
||
submitSchedule(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Start
|
||
</Button>
|
||
<Button style={{flex: "1",}} variant="contained" disabled={selectedTrigger.status !== "running"} onClick={() => {
|
||
stopSchedule(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const cytoscapeViewWidths = 750
|
||
const bottomBarStyle = {
|
||
position: "fixed",
|
||
right: 20,
|
||
left: leftBarSize,
|
||
bottom: 0,
|
||
minWidth: cytoscapeViewWidths,
|
||
maxWidth: cytoscapeViewWidths,
|
||
marginLeft: 20,
|
||
marginBottom: 20,
|
||
zIndex: 10,
|
||
}
|
||
|
||
const topBarStyle= {
|
||
position: "fixed",
|
||
right: 0,
|
||
left: leftBarSize,
|
||
top: appBarSize,
|
||
minWidth: cytoscapeViewWidths,
|
||
maxWidth: cytoscapeViewWidths,
|
||
marginLeft: 20,
|
||
marginBottom: 20,
|
||
}
|
||
|
||
const TopCytoscapeBar = () => {
|
||
return (
|
||
<div style={topBarStyle}>
|
||
<div style={{margin: 10}}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white",}}>
|
||
<Link to="/workflows" style={{textDecoration: "none", color: "inherit",}}>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
|
||
<PolymerIcon style={{marginRight: 10}} />
|
||
Workflows
|
||
</h2>
|
||
</Link>
|
||
<h2>
|
||
{workflow.name}
|
||
</h2>
|
||
</Breadcrumbs>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const NoActionsBar = () => {
|
||
if (workflow.actions.length === 0 || !appAdded) {
|
||
return (
|
||
<div style={{position: "fixed", width: "100%", top: 300}}>
|
||
<div style={{width: 300, margin: "auto"}}>
|
||
<h3></h3>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const FileMenu = () => {
|
||
const [newAnchor, setNewAnchor] = React.useState(null);
|
||
const [showShuffleMenu, setShowShuffleMenu] = React.useState(false)
|
||
|
||
{ /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */}
|
||
return (
|
||
<div style={{"display": "inline-block"}}>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={newAnchor}
|
||
open={showShuffleMenu}
|
||
onClose={() => {
|
||
setShowShuffleMenu(false)
|
||
}}
|
||
>
|
||
<div style={{margin: 15, color: "white", maxWidth: 250, minWidth: 250, }}>
|
||
<h4>This menu is used to control the workflow itself.</h4>
|
||
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Exit on Error</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.exit_on_error} onChange={() => {
|
||
workflow.configuration.exit_on_error = !workflow.configuration.exit_on_error
|
||
setWorkflow(workflow)
|
||
setUpdate("exit_on_error_"+workflow.configuration.exit_on_error ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Start from top</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.start_from_top} onChange={() => {
|
||
workflow.configuration.start_from_top = !workflow.configuration.start_from_top
|
||
setWorkflow(workflow)
|
||
setUpdate("start_from_top_"+workflow.configuration.start_from_top ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
</div>
|
||
</Menu>
|
||
<Tooltip color="secondary" title="Workflow settings" placement="top-start">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={(event) => {
|
||
setShowShuffleMenu(!showShuffleMenu)
|
||
setNewAnchor(event.currentTarget)
|
||
}}>
|
||
<SettingsIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const WorkflowMenu = () => {
|
||
const [newAnchor, setNewAnchor] = React.useState(null);
|
||
const [showShuffleMenu, setShowShuffleMenu] = React.useState(false)
|
||
|
||
{ /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */}
|
||
return (
|
||
<div style={{"display": "inline-block"}}>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={newAnchor}
|
||
open={showShuffleMenu}
|
||
onClose={() => {
|
||
setShowShuffleMenu(false)
|
||
}}
|
||
>
|
||
<div style={{margin: 15, color: "white", maxWidth: 250, minWidth: 250, }}>
|
||
<h4>This menu is used to control the workflow itself.</h4>
|
||
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Exit on Error</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.exit_on_error} onChange={() => {
|
||
workflow.configuration.exit_on_error = !workflow.configuration.exit_on_error
|
||
setWorkflow(workflow)
|
||
setUpdate("exit_on_error_"+workflow.configuration.exit_on_error ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Start from top</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.start_from_top} onChange={() => {
|
||
workflow.configuration.start_from_top = !workflow.configuration.start_from_top
|
||
setWorkflow(workflow)
|
||
setUpdate("start_from_top_"+workflow.configuration.start_from_top ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
</div>
|
||
</Menu>
|
||
<Tooltip color="secondary" title="Workflow settings" placement="top-start">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={(event) => {
|
||
setShowShuffleMenu(!showShuffleMenu)
|
||
setNewAnchor(event.currentTarget)
|
||
}}>
|
||
<SettingsIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const BottomCytoscapeBar = () => {
|
||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||
|
||
const boxSize = 100
|
||
const executionButton = executionRunning ?
|
||
<Tooltip color="primary" title="Stop execution" placement="top">
|
||
<span>
|
||
<Button style={{height: boxSize, width: boxSize}} color="secondary" variant="contained" onClick={() => {
|
||
abortExecution()
|
||
}}>
|
||
<PauseIcon style={{ fontSize: 60}} />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
:
|
||
<Tooltip color="primary" title="Test execution" placement="top">
|
||
<span>
|
||
<Button disabled={executionRequestStarted || !workflow.isValid} style={{height: boxSize, width: boxSize}} color="primary" variant="contained" onClick={() => {
|
||
executeWorkflow(executionText, workflow.start)
|
||
}}>
|
||
<PlayArrowIcon style={{ fontSize: 60}} />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
|
||
return(
|
||
<div style={bottomBarStyle}>
|
||
{executionButton}
|
||
<div style={{marginLeft: "10px", left: boxSize, bottom: 0, position: "absolute"}}>
|
||
<Tooltip color="primary" title="An argument to be used for execution. This is a variable available to every node in your workflow." placement="top">
|
||
<TextField
|
||
id="execution_argument_input_field"
|
||
style={textFieldStyle}
|
||
InputProps={{
|
||
style: innerTextfieldStyle,
|
||
}}
|
||
color="secondary"
|
||
placeholder={"Execution Argument"}
|
||
defaultValue={executionText}
|
||
onBlur={(e) => {
|
||
setExecutionText(e.target.value)
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
<Tooltip color="primary" title="Save (ctrl+s)" placement="top">
|
||
<span>
|
||
<Button disabled={savingState !== 0} color="primary" style={{height: 50, width: 64, marginLeft: 10, }} variant={lastSaved ? "outlined" : "contained"} onClick={() => saveWorkflow()}>
|
||
{savingState === 2 ? <CircularProgress style={{height: 35, width: 35}} /> : savingState === 1 ? <DoneIcon style={{color: "green"}} /> : <SaveIcon /> }
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<Tooltip color="secondary" title="Fit to screen (ctrl+f)" placement="top">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => cy.fit(null, 50)}>
|
||
<AspectRatioIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<Tooltip color="secondary" title="Remove selected item (del)" placement="top-start">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
||
removeNode()
|
||
}}>
|
||
<DeleteIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<Tooltip color="secondary" title="Show executions" placement="top-start">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
||
setExecutionModalOpen(true)
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
}}>
|
||
<DirectionsRunIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
{/* <FileMenu /> */}
|
||
<WorkflowMenu />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const RightSideBar = () => {
|
||
if (!rightSideBarOpen) {
|
||
return null
|
||
}
|
||
|
||
if (Object.getOwnPropertyNames(selectedAction).length > 0) {
|
||
//console.time('ACTIONSTART')
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
{appApiView}
|
||
</div>
|
||
)
|
||
} else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||
if (selectedTrigger.trigger_type === "SCHEDULE") {
|
||
//console.log("SCHEDULE")
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<ScheduleSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "WEBHOOK") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<WebhookSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "SUBFLOW") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<SubflowSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "EMAIL") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<EmailSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "USERINPUT") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<UserinputSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === undefined) {
|
||
return null
|
||
} else {
|
||
console.log("Unable to handle invalid trigger type "+selectedTrigger.trigger_type)
|
||
return null
|
||
}
|
||
} else if (Object.getOwnPropertyNames(selectedEdge).length > 0) {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<EdgeSidebar />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return(
|
||
null
|
||
)
|
||
}
|
||
|
||
// This can execute a workflow with firestore. Used for test, as datastore is old and stuff
|
||
// Too much work to move everything over alone, so won't touch it for now
|
||
//<Button style={{borderRadius: "0px"}} color="primary" variant="contained" onClick={() => {
|
||
// executeWorkflowWebsocket()
|
||
//}}>Execute websocket</Button>
|
||
//
|
||
|
||
const leftView = leftViewOpen ? <div style={{minWidth: leftBarSize, maxWidth: leftBarSize, borderRight: "1px solid rgb(91, 96, 100)"}}>
|
||
<HandleLeftView />
|
||
</div> :
|
||
<div style={{minWidth: leftBarSize, maxWidth: leftBarSize, borderRight: "1px solid rgb(91, 96, 100)"}}>
|
||
<div style={{cursor: "pointer", height: 20, marginTop: 10, marginLeft: 10}} onClick={() => {
|
||
setLeftViewOpen(true)
|
||
setLeftBarSize(350)
|
||
}}>
|
||
<Tooltip color="primary" title="Maximize" placement="top">
|
||
<KeyboardArrowRightIcon />
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
const executionPaperStyle = {
|
||
minWidth: "95%",
|
||
maxWidth: "95%",
|
||
marginTop: "5px",
|
||
color: "white",
|
||
marginBottom: 10,
|
||
padding: 5,
|
||
backgroundColor: surfaceColor,
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
minHeight: 40,
|
||
maxHeight: 40,
|
||
}
|
||
|
||
const parsedExecutionArgument = () => {
|
||
var showResult = executionData.execution_argument.trim()
|
||
const validate = validateJson(showResult)
|
||
|
||
|
||
if (validate.valid) {
|
||
if (typeof(validate.result) === "string") {
|
||
try {
|
||
validate.result = JSON.parse(validate.result)
|
||
} catch(e) {
|
||
console.log("Error: ", e)
|
||
validate.valid = false
|
||
}
|
||
}
|
||
|
||
return (
|
||
<ReactJson
|
||
src={validate.result}
|
||
theme="solarized"
|
||
collapsed={true}
|
||
displayDataTypes={false}
|
||
onSelect={(select) => {
|
||
HandleJsonCopy(showResult, select, "exec")
|
||
console.log("SELECTED!: ", select)
|
||
}}
|
||
name={"Execution Argument"}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<h3>Execution Argument</h3>
|
||
<div style={{maxHeight: 200, overflowY: "scroll",}}>
|
||
{executionData.execution_argument}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
const getExecutionSourceImage = (execution) => {
|
||
// This is the playbutton at 150x150
|
||
const defaultImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg=="
|
||
const size = 40
|
||
if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) {
|
||
return <img alt="default" src={defaultImage} style={{width: size, height: size}} />
|
||
}
|
||
|
||
if (execution.execution_source === "webhook") {
|
||
return <img alt={"webhook"} src={triggers.find(trigger => trigger.trigger_type === "WEBHOOK").large_image} style={{width: size, height: size}} />
|
||
}
|
||
|
||
if (execution.execution_source === "schedule") {
|
||
return <img alt={"schedule"} src={triggers.find(trigger => trigger.trigger_type === "SCHEDULE").large_image} style={{width: size, height: size}} />
|
||
}
|
||
|
||
if (execution.execution_source === "EMAIL") {
|
||
return <img alt={"email"} src={triggers.find(trigger => trigger.trigger_type === "EMAIL").large_image} style={{width: size, height: size}} />
|
||
}
|
||
|
||
if (execution.execution_parent !== null && execution.execution_parent !== undefined && execution.execution_parent.length > 0) {
|
||
return <img alt={"parent workflow"} src={triggers.find(trigger => trigger.trigger_type === "SUBFLOW").large_image} style={{width: size, height: size}} />
|
||
}
|
||
|
||
return (
|
||
<img alt={execution.execution_source} src={defaultImage} style={{width: size, height: size}} />
|
||
)
|
||
}
|
||
|
||
const HandleJsonCopy = (base, copy, base_node_name) => {
|
||
console.log("COPY: ", copy)
|
||
var newitem = JSON.parse(base)
|
||
to_be_copied = "$"+base_node_name
|
||
for (var key in copy.namespace) {
|
||
if (copy.namespace[key].includes("Results for")) {
|
||
continue
|
||
}
|
||
|
||
if (newitem !== undefined && newitem !== null) {
|
||
newitem = newitem[copy.namespace[key]]
|
||
if (!isNaN(copy.namespace[key])) {
|
||
to_be_copied += ".#"
|
||
} else {
|
||
to_be_copied += "."+copy.namespace[key]
|
||
}
|
||
}
|
||
}
|
||
|
||
if (newitem !== undefined && newitem !== null) {
|
||
newitem = newitem[copy.name]
|
||
if (!isNaN(copy.name)) {
|
||
to_be_copied += ".#"
|
||
} else {
|
||
to_be_copied += "."+copy.name
|
||
}
|
||
}
|
||
|
||
//console.log(document.activeElement)
|
||
//if (document.activeElement.nodeName == 'TEXTAREA' || document.activeElement.nodeName == 'INPUT') {
|
||
// console.log("HANDLE INPUT FIELD FOR COPY!")
|
||
//}
|
||
|
||
to_be_copied.replace(" ", "_")
|
||
const elementName = "copy_element_shuffle"
|
||
var copyText = document.getElementById(elementName);
|
||
if (copyText !== null && copyText !== undefined) {
|
||
navigator.clipboard.writeText(to_be_copied)
|
||
copyText.select();
|
||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||
|
||
/* Copy the text inside the text field */
|
||
document.execCommand("copy");
|
||
alert.success("Copied "+to_be_copied)
|
||
console.log("COPYING!")
|
||
} else {
|
||
console.log("Couldn't find element ", elementName)
|
||
}
|
||
|
||
}
|
||
|
||
const executionModal =
|
||
<Drawer anchor={"right"} open={executionModalOpen} onClose={() => setExecutionModalOpen(false)} style={{resize: "both", overflow: "auto",}} PaperProps={{style: {resize: "both", overflow: "auto", minWidth: 400, maxWidth: 400, backgroundColor: "#1F2023", color: "white", fontSize: 18}}}>
|
||
{executionModalView === 0 ?
|
||
<div style={{padding: 25, }}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white", fontSize: 16}}>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
|
||
<DirectionsRunIcon style={{marginRight: 10}} />
|
||
All Executions
|
||
</h2>
|
||
</Breadcrumbs>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
}} color="primary">
|
||
<CachedIcon style={{marginRight: 10}}/>
|
||
Refresh executions
|
||
</Button>
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.5)", marginTop: 10, marginBottom: 10,}}/>
|
||
{workflowExecutions.length > 0 ?
|
||
<div>
|
||
{workflowExecutions.map((data, index) => {
|
||
const statusColor = data.status === "FINISHED" ? "green" : data.status === "ABORTED" || data.status === "FAILED" ? "red" : "orange"
|
||
const timeElapsed = data.completed_at-data.started_at
|
||
const resultsLength = data.results !== undefined && data.results !== null ? data.results.length : 0
|
||
|
||
const timestamp = new Date(data.started_at*1000).toISOString().split('.')[0].split("T").join(" ")
|
||
|
||
var calculatedResult = data.workflow.actions.length
|
||
for (var key in data.workflow.triggers) {
|
||
const trigger = data.workflow.triggers[key]
|
||
if ((trigger.app_name === "User Input" && trigger.trigger_type === "USERINPUT") || (trigger.app_name === "Shuffle Workflow" && trigger.trigger_type === "SUBFLOW")) {
|
||
calculatedResult += 1
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Tooltip key={data.execution_id} title={data.result} placement="left-start">
|
||
<Paper elevation={5} key={data.execution_id} square style={executionPaperStyle} onMouseOver={() => {}} onMouseOut={() => {}} onClick={() => {
|
||
|
||
if (data.result === undefined || data.result === null || data.result.length === 0) {
|
||
setExecutionRequest({
|
||
"execution_id": data.execution_id,
|
||
"authorization": data.authorization,
|
||
})
|
||
start()
|
||
setExecutionRunning(true)
|
||
setExecutionRequestStarted(false)
|
||
}
|
||
setExecutionModalView(1)
|
||
setExecutionData(data)
|
||
}}>
|
||
<div style={{display: "flex", flex: 1}}>
|
||
<div style={{marginLeft: 0, width: lastExecution === data.execution_id ? 4 : 2, backgroundColor: statusColor, marginRight: 5}} />
|
||
<div style={{height: "100%", width: 40, borderColor: "white", marginRight: 15}}>
|
||
{getExecutionSourceImage(data)}
|
||
</div>
|
||
<div style={{ marginTop: "auto", marginBottom: "auto", marginRight: 15, fontSize: 13, }}>
|
||
{timestamp}
|
||
</div>
|
||
{data.workflow.actions !== null ?
|
||
<Tooltip color="primary" title={resultsLength+" actions ran"} placement="top">
|
||
<div style={{marginRight: 10, marginTop: "auto", marginBottom: "auto",}}>
|
||
{resultsLength}/{calculatedResult}
|
||
</div>
|
||
</Tooltip>
|
||
: null}
|
||
</div>
|
||
<Tooltip title={"Inspect execution"} placement="top">
|
||
{lastExecution === data.execution_id ?
|
||
<KeyboardArrowRightIcon style={{color: "#f85a3e", marginTop: "auto", marginBottom: "auto"}}/>
|
||
:
|
||
<KeyboardArrowRightIcon style={{marginTop: "auto", marginBottom: "auto"}}/>
|
||
}
|
||
</Tooltip>
|
||
</Paper>
|
||
</Tooltip>
|
||
)
|
||
return
|
||
})}
|
||
</div>
|
||
:
|
||
<div>
|
||
There are no executions yet
|
||
</div>
|
||
}
|
||
</div>
|
||
:
|
||
<div style={{padding: 25, maxWidth: 365, overflowX: "hidden",}}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white", fontSize: 16}}>
|
||
<span style={{color: "rgba(255,255,255,0.5)", display: "flex"}} onClick={() => {
|
||
setExecutionRunning(false)
|
||
stop()
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
setExecutionModalView(0)
|
||
setLastExecution(executionData.execution_id)
|
||
}}>
|
||
<IconButton style={{paddingLeft: 0, marginTop: "auto", marginBottom: "auto", }} onClick={() => {}}>
|
||
<ArrowBackIcon style={{color: "rgba(255,255,255,0.5)",}} />
|
||
</IconButton>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)", cursor: "pointer"}} onClick={() => {
|
||
}}>
|
||
See other Executions
|
||
</h2>
|
||
</span>
|
||
</Breadcrumbs>
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.6)", marginTop: 10, marginBottom: 10,}}/>
|
||
<div style={{display: "flex"}}>
|
||
<h2>Executing Workflow</h2>
|
||
<Tooltip color="primary" title="Rerun workflow" placement="top">
|
||
<span style={{}}>
|
||
<Button color="primary" style={{float: "right", marginTop: 20, marginLeft: 10,}} onClick={() => {
|
||
console.log("DATA: ", executionData)
|
||
executeWorkflow(executionData.execution_argument, executionData.start)
|
||
setExecutionModalOpen(false)
|
||
//executionText, workflow.start)
|
||
}}>
|
||
<CachedIcon style={{}}/>
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
{executionData.status !== undefined && executionData.status.length > 0 ?
|
||
<div>
|
||
<b>Status: </b>{executionData.status}
|
||
</div>
|
||
: null
|
||
}
|
||
{executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ?
|
||
<div>
|
||
<b>Source: </b>{executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ?
|
||
executionData.execution_source === props.match.params.key ?
|
||
<span style={{cursor: "pointer", color: "#f85a3e"}} onClick={(event) => {
|
||
getWorkflowExecution(props.match.params.key, executionData.execution_parent)
|
||
}}>
|
||
Parent Execution
|
||
</span>
|
||
:
|
||
<a rel="norefferer" href={`/workflows/${executionData.execution_source}?view=executions&execution_id=${executionData.execution_parent}`} target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>Parent Workflow</a>
|
||
:
|
||
executionData.execution_source
|
||
}
|
||
</div>
|
||
: null
|
||
}
|
||
{executionData.started_at !== undefined ?
|
||
<div>
|
||
<b>Started: </b>{new Date(executionData.started_at*1000).toISOString()}
|
||
</div>
|
||
: null
|
||
}
|
||
{executionData.completed_at !== undefined && executionData.completed_at !== null && executionData.completed_at > 0 ?
|
||
<div>
|
||
<b>Finished: </b>{new Date(executionData.completed_at*1000).toISOString()}
|
||
</div>
|
||
: null
|
||
}
|
||
<div style={{marginTop: 10}}/>
|
||
{executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ?
|
||
parsedExecutionArgument()
|
||
: null }
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.6)", marginTop: 15, marginBottom: 30,}}/>
|
||
{executionData.results !== undefined && executionData.results !== null && executionData.results.length > 1 && executionData.results.find(result => result.status === "SKIPPED" || result.status === "FAILURE") ?
|
||
<FormControlLabel
|
||
style={{color: "white", marginBottom: 10, }}
|
||
label={<div style={{color: "white"}}>Show failed / skipped actions</div>}
|
||
control={
|
||
<Switch checked={showSkippedActions} onChange={() => {setShowSkippedActions(!showSkippedActions)}} />
|
||
}
|
||
/>
|
||
:
|
||
null
|
||
}
|
||
<div style={{display: "flex", marginTop: 10, marginBottom: 30,}}>
|
||
|
||
<b>Actions</b>
|
||
<div>
|
||
{executionData.status !== undefined && executionData.status !== "ABORTED" && executionData.status !== "FINISHED" && executionData.status !== "FAILURE" && executionData.status !== "WAITING" && !(executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING")? <CircularProgress style={{marginLeft: 20}}/> : null}
|
||
</div>
|
||
</div>
|
||
{executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ?
|
||
<CircularProgress />
|
||
:
|
||
executionData.results.map((data, index) => {
|
||
if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) {
|
||
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" : "orange"
|
||
|
||
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
|
||
}
|
||
|
||
/*
|
||
if (imgSrc.length === 0) {
|
||
console.log("CHECK IF ITS A
|
||
}
|
||
*/
|
||
}
|
||
|
||
var actionimg = curapp === null ?
|
||
null :
|
||
<img alt={data.action.app_name} src={imgSrc} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, borderRadius: executionData.start === data.action.id ? 25 : 5}} />
|
||
|
||
if (triggers.length > 2) {
|
||
if (data.action.app_name === "shuffle-subflow") {
|
||
actionimg = <img alt={"Shuffle Subflow"} src={triggers[1].large_image} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, borderRadius: executionData.start === data.action.id ? 25 : 5}} />
|
||
}
|
||
|
||
if (data.action.app_name === "User Input") {
|
||
actionimg = <img alt={"Shuffle Subflow"} src={triggers[2].large_image} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, borderRadius: executionData.start === data.action.id ? 25 : 5}} />
|
||
}
|
||
}
|
||
|
||
if (validate.valid && typeof(validate.result) === "string") {
|
||
validate.result = JSON.parse(validate.result)
|
||
}
|
||
|
||
return (
|
||
<div key={index} style={{marginBottom: 40,}}>
|
||
<div style={{display: "flex", marginBottom: 15,}}>
|
||
<IconButton style={{marginTop: "auto", marginBottom: "auto", height: 30, paddingLeft: 0, width: 30}} onClick={() => {
|
||
setSelectedResult(data)
|
||
setCodeModalOpen(true)
|
||
}}>
|
||
<Tooltip color="primary" title="Expand result window" placement="top">
|
||
<ArrowLeftIcon style={{color: "white"}}/>
|
||
</Tooltip>
|
||
</IconButton>
|
||
{actionimg}
|
||
<div>
|
||
<div style={{fontSize: 24, marginTop: "auto", marginBottom: "auto"}}><b>{data.action.label}</b></div>
|
||
<div style={{fontSize: 14}}>{data.action.name}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{marginBottom: 5}}><b>Status </b> {data.status}</div>
|
||
{validate.valid ? <span><ReactJson
|
||
src={validate.result}
|
||
theme="solarized"
|
||
collapsed={true}
|
||
displayDataTypes={false}
|
||
onSelect={(select) => {
|
||
HandleJsonCopy(showResult, select, data.action.label)
|
||
console.log("SELECTED!: ", select)
|
||
}}
|
||
name={"Results for "+data.action.label}
|
||
/>
|
||
{data.action.app_name === "shuffle-subflow" && validate.result.success !== undefined && validate.result.success === true ?
|
||
<span>
|
||
{validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ?
|
||
data.action.parameters[0].value === props.match.params.key ?
|
||
<span style={{cursor: "pointer", color: "#f85a3e"}} onClick={(event) => {
|
||
getWorkflowExecution(props.match.params.key, validate.result.execution_id)
|
||
}}>
|
||
See sub-execution
|
||
</span>
|
||
:
|
||
<a rel="norefferer" href={`/workflows/${data.action.parameters[0].value}?view=executions&execution_id=${validate.result.execution_id}`} target="_blank" style={{textDecoration: "none", color: "#f85a3e"}} onClick={(event) => {
|
||
}}>See subflow execution</a>
|
||
:
|
||
"TBD: Load subexecution result for"
|
||
}
|
||
</span>
|
||
: null
|
||
}
|
||
</span>
|
||
:
|
||
<div style={{maxHeight: 250, overflowX: "hidden", overflowY: "auto", whiteSpace: "pre-wrap"}}>
|
||
<b>Result</b>
|
||
{data.result}
|
||
</div>
|
||
}
|
||
</div>
|
||
)
|
||
})
|
||
}
|
||
</div>
|
||
}
|
||
</Drawer>
|
||
|
||
// This sucks :)
|
||
const curapp = !codeModalOpen ? {} : selectedResult.action.app_name === "shuffle-subflow" ? triggers[1] : selectedResult.action.app_name === "User Input" ? triggers[2] : apps.find(a => a.name === selectedResult.action.app_name && a.app_version === selectedResult.action.app_version)
|
||
const imgsize = 50
|
||
const statusColor = !codeModalOpen ? "red" : selectedResult.status === "FINISHED" || selectedResult.status === "SUCCESS" ? "green" : selectedResult.status === "ABORTED" || selectedResult.status === "FAILURE" ? "red" : "orange"
|
||
const validate = !codeModalOpen ? "" : validateJson(selectedResult.result.trim())
|
||
if (validate.valid && typeof(validate.result) === "string") {
|
||
validate.result = JSON.parse(validate.result)
|
||
}
|
||
|
||
//if (codeModalOpen && selectedResult.result.includes("file_id")) {
|
||
// console.log("SHOW RESULT WITH FILES: ", selectedResult.result)
|
||
// //const regex = "\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b"
|
||
// //const regex = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i
|
||
// const regex = /^[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i
|
||
// //const found = selectedResult.result.match(regex)
|
||
// const found = "hello how are you cf80fa70-65cf-4963-b474-b459a6dead81 what".match(regex)
|
||
// const regex = /\${(\w{8}-\w{4}-\w{3}-\w{3}-\w)}/g
|
||
// const found = placeholder.match(regex)
|
||
|
||
// console.log("FOUND: ", found)
|
||
|
||
// //cf80fa70-65cf-4963-b474-b459a6dead81
|
||
//}
|
||
|
||
const codePopoutModal = !codeModalOpen ? null :
|
||
<Draggable
|
||
onDrag={(e) => {
|
||
if (!dragging) {
|
||
setDragging(true)
|
||
}
|
||
}}
|
||
onStop={(e) => {
|
||
if (!dragging) {
|
||
return
|
||
}
|
||
|
||
setDragging(false)
|
||
|
||
const newoffsetX = parseInt(dragPosition.x)-parseInt(e.layerX-e.offsetX)
|
||
const newoffsetY = parseInt(dragPosition.y)-parseInt(e.layerY-e.offsetY)
|
||
if ((newoffsetX <= 40 && newoffsetX >= -40) && (newoffsetY <= 40 && newoffsetY >= -40)) {
|
||
console.log("SKIP X & Y")
|
||
return
|
||
}
|
||
|
||
setDragPosition({
|
||
x: e.layerX-e.offsetX,
|
||
y: e.layerY-e.offsetY,
|
||
})
|
||
}}
|
||
position={dragPosition}
|
||
>
|
||
<Dialog
|
||
disableEnforceFocus={true}
|
||
style={{pointerEvents: "none"}}
|
||
hideBackdrop={true}
|
||
open={codeModalOpen}
|
||
PaperProps={{
|
||
style: {
|
||
pointerEvents: "auto",
|
||
backgroundColor: inputColor,
|
||
color: "white",
|
||
minWidth: 750,
|
||
padding: 30,
|
||
maxHeight: 700,
|
||
overflowY: "auto",
|
||
overflowX: "hidden",
|
||
//boxShadow: "none",
|
||
},
|
||
}}
|
||
>
|
||
<Tooltip title="Find successful execution" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 170,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
for (var key in workflowExecutions) {
|
||
const execution = workflowExecutions[key]
|
||
//console.log(execution.results[0])
|
||
const result = execution.results.find(data => data.status === "SUCCESS" && data.action.id === selectedResult.action.id)
|
||
if (result !== undefined) {
|
||
setSelectedResult(result)
|
||
setUpdate(Math.random())
|
||
break
|
||
}
|
||
}
|
||
}}>
|
||
<DoneIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Find failed execution" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 136,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
for (var key in workflowExecutions) {
|
||
const execution = workflowExecutions[key]
|
||
//console.log(execution.results[0])
|
||
const result = execution.results.find(data => data.action.id === selectedResult.action.id && data.status !== "SUCCESS" && data.status !== "SKIPPED" && data.status !== "WAITING")
|
||
if (result !== undefined) {
|
||
setSelectedResult(result)
|
||
setUpdate(Math.random())
|
||
break
|
||
}
|
||
}
|
||
}}>
|
||
<ErrorIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Explore execution" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 98,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
const executionIndex = workflowExecutions.findIndex(data => data.execution_id === selectedResult.execution_id)
|
||
if (executionIndex !== -1) {
|
||
setExecutionModalOpen(true)
|
||
setExecutionModalView(1)
|
||
setExecutionData(workflowExecutions[executionIndex])
|
||
}
|
||
}}>
|
||
<VisibilityIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Close window" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 34,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
//console.log("CLICKING EXIT")
|
||
setCodeModalOpen(false)
|
||
}}>
|
||
<CloseIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<div style={{marginBottom: 40,}}>
|
||
<div style={{display: "flex", marginBottom: 15,}}>
|
||
{curapp === null ? null : <img alt={selectedResult.app_name} src={curapp === undefined ? "" : curapp.large_image} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`}} />}
|
||
|
||
<div>
|
||
<div style={{fontSize: 24, marginTop: "auto", marginBottom: "auto"}}><b>{selectedResult.action.label}</b></div>
|
||
<div style={{fontSize: 14}}>{selectedResult.action.name}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{marginBottom: 5}}><b>Status </b> {selectedResult.status}</div>
|
||
{validate.valid ? <ReactJson
|
||
src={validate.result}
|
||
theme="solarized"
|
||
collapsed={false}
|
||
displayDataTypes={false}
|
||
onSelect={(select) => {
|
||
HandleJsonCopy(JSON.stringify(validate.result), select, selectedResult.action.label)
|
||
}}
|
||
name={"Results for "+selectedResult.action.label}
|
||
/>
|
||
:
|
||
<div>
|
||
<b>Result</b>
|
||
<span onClick={() => {
|
||
console.log("IN HERE TO CLICK")
|
||
to_be_copied = selectedResult.result
|
||
var copyText = document.getElementById("copy_element_shuffle");
|
||
console.log("PRECOPY: ", to_be_copied)
|
||
if (copyText !== null && copyText !== undefined) {
|
||
console.log("COPY: ", copyText)
|
||
navigator.clipboard.writeText(to_be_copied)
|
||
copyText.select();
|
||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||
|
||
/* Copy the text inside the text field */
|
||
document.execCommand("copy");
|
||
alert.success("Copied "+to_be_copied)
|
||
} else {
|
||
console.log("Failed to copy. copy_element_shuffle is undefined")
|
||
}
|
||
}}>{selectedResult.result}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</Dialog>
|
||
</Draggable>
|
||
|
||
const newView = isLoggedIn ?
|
||
<div style={{color: "white"}}>
|
||
<div style={{display: "flex", borderTop: "1px solid rgba(91, 96, 100, 1)"}}>
|
||
{leftView}
|
||
<CytoscapeComponent
|
||
elements={elements}
|
||
minZoom={0.35}
|
||
maxZoom={2.00}
|
||
style={{width: bodyWidth-leftBarSize-15, height: bodyHeight-appBarSize-1, backgroundColor: surfaceColor}}
|
||
stylesheet={cystyle}
|
||
boxSelectionEnabled={true}
|
||
autounselectify={false}
|
||
cy={(incy) => {
|
||
// FIXME: There's something specific loading when
|
||
// you do the first hover of a node. Why is this different?
|
||
//console.log("CY: ", incy)
|
||
setCy(incy)
|
||
}}
|
||
/>
|
||
</div>
|
||
{executionModal}
|
||
<RightSideBar />
|
||
<BottomCytoscapeBar />
|
||
<TopCytoscapeBar />
|
||
</div>
|
||
:
|
||
<div style={{color: "white"}}>
|
||
TMP FOR NOT LOGGED IN
|
||
</div>
|
||
|
||
const executionVariableModal = executionVariablesModalOpen ?
|
||
<Dialog modal
|
||
open={executionVariablesModalOpen}
|
||
onClose={() => {
|
||
setNewVariableName("")
|
||
setExecutionVariablesModalOpen(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle><span style={{color: "white"}}>Execution Variable</span></DialogTitle>
|
||
<DialogContent>
|
||
Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more <a rel="norefferer" href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>here</a>
|
||
<TextField
|
||
onBlur={(event) => setNewVariableName(event.target.value)}
|
||
color="primary"
|
||
placeholder="Name"
|
||
style={{marginTop: 25}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
margin="dense"
|
||
fullWidth
|
||
defaultValue={newVariableName}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setNewVariableName("")
|
||
setExecutionVariablesModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0} onClick={() => {
|
||
console.log("VARIABLES! ", newVariableName)
|
||
if (workflow.execution_variables === undefined || workflow.execution_variables === null) {
|
||
workflow.execution_variables = []
|
||
}
|
||
|
||
// try to find one with the same name
|
||
const found = workflow.execution_variables.findIndex(data => data.name === newVariableName)
|
||
//console.log(found)
|
||
if (found !== -1) {
|
||
if (newVariableName.length > 0) {
|
||
workflow.execution_variables[found].name = newVariableName
|
||
}
|
||
} else {
|
||
workflow.execution_variables.push({
|
||
"name": newVariableName,
|
||
"description": "An execution variable",
|
||
"value": "",
|
||
"id": uuid.v4(),
|
||
})
|
||
}
|
||
|
||
setExecutionVariablesModalOpen(false)
|
||
setNewVariableName("")
|
||
setWorkflow(workflow)
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
{workflowExecutions.length > 0 ?
|
||
<DialogContent>
|
||
<Divider style={{backgroundColor: "white", marginTop: 15, marginBottom: 15,}}/>
|
||
<b style={{marginBottom: 10}}>Values from last 3 executions</b>
|
||
{workflowExecutions.slice(0,3).map((execution, index) => {
|
||
if (execution.execution_variables === undefined || execution.execution_variables === null || execution.execution_variables === 0) {
|
||
return null
|
||
}
|
||
|
||
const variable = execution.execution_variables.find(data => data.name === newVariableName)
|
||
if (variable === undefined || variable.value === undefined) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{index+1}: {variable.value}
|
||
</div>
|
||
)
|
||
})}
|
||
</DialogContent>
|
||
: null
|
||
}
|
||
</FormControl>
|
||
</Dialog>
|
||
: null
|
||
|
||
const variablesModal = variablesModalOpen ?
|
||
<Dialog
|
||
open={variablesModalOpen}
|
||
onClose={() => {
|
||
setNewVariableName("")
|
||
setNewVariableDescription("")
|
||
setNewVariableValue("")
|
||
setVariablesModalOpen(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle><span style={{color: "white"}}>Workflow Variable</span></DialogTitle>
|
||
<DialogContent>
|
||
<TextField
|
||
onBlur={(event) => setNewVariableName(event.target.value)}
|
||
color="primary"
|
||
placeholder="Name"
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
margin="dense"
|
||
fullWidth
|
||
defaultValue={newVariableName}
|
||
/>
|
||
<TextField
|
||
onBlur={(event) => setNewVariableDescription(event.target.value)}
|
||
color="primary"
|
||
placeholder="Description"
|
||
margin="dense"
|
||
fullWidth
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
defaultValue={newVariableDescription}
|
||
/>
|
||
<TextField
|
||
onChange={(event) => setNewVariableValue(event.target.value)}
|
||
rows="6"
|
||
multiline
|
||
color="primary"
|
||
placeholder="Value"
|
||
margin="dense"
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
fullWidth
|
||
defaultValue={newVariableValue}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setNewVariableName("")
|
||
setNewVariableDescription("")
|
||
setNewVariableValue("")
|
||
setVariablesModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0 || newVariableValue.length === 0} onClick={() => {
|
||
if (workflow.workflow_variables === undefined || workflow.workflow_variables === null) {
|
||
workflow.workflow_variables = []
|
||
}
|
||
|
||
// try to find one with the same name
|
||
const found = workflow.workflow_variables.findIndex(data => data.name === newVariableName)
|
||
//console.log(found)
|
||
if (found !== -1) {
|
||
if (newVariableName.length > 0) {
|
||
workflow.workflow_variables[found].name = newVariableName
|
||
}
|
||
if (newVariableDescription.length > 0) {
|
||
workflow.workflow_variables[found].description = newVariableDescription
|
||
}
|
||
if (newVariableValue.length > 0) {
|
||
workflow.workflow_variables[found].value = newVariableValue
|
||
}
|
||
} else {
|
||
workflow.workflow_variables.push({
|
||
"name": newVariableName,
|
||
"description": newVariableDescription,
|
||
"value": newVariableValue,
|
||
"id": uuid.v4(),
|
||
})
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
setVariablesModalOpen(false)
|
||
setNewVariableName("")
|
||
setNewVariableDescription("")
|
||
setNewVariableValue("")
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</FormControl>
|
||
</Dialog>
|
||
: null
|
||
|
||
|
||
const AuthenticationData = (props) => {
|
||
const selectedApp = props.app
|
||
|
||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||
fields: {},
|
||
label: "",
|
||
usage: [{
|
||
workflow_id: workflow.id,
|
||
}],
|
||
id: uuid.v4(),
|
||
active: true,
|
||
})
|
||
|
||
if (selectedApp.authentication === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (selectedApp.authentication.parameters === null || selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0) {
|
||
return null
|
||
}
|
||
|
||
authenticationOption.app.actions = []
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (authenticationOption.fields[selectedApp.authentication.parameters[key].name] === undefined) {
|
||
authenticationOption.fields[selectedApp.authentication.parameters[key].name] = ""
|
||
}
|
||
}
|
||
|
||
const handleSubmitCheck = () => {
|
||
console.log("NEW AUTH: ", authenticationOption)
|
||
if (authenticationOption.label.length === 0) {
|
||
authenticationOption.label = `Auth for ${selectedApp.name}`
|
||
//alert.info("Label can't be empty")
|
||
//return
|
||
}
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (authenticationOption.fields[selectedApp.authentication.parameters[key].name].length === 0) {
|
||
alert.info("Field "+selectedApp.authentication.parameters[key].name+" can't be empty")
|
||
return
|
||
}
|
||
}
|
||
|
||
selectedAction.authentication_id = authenticationOption.id
|
||
selectedAction.selectedAuthentication = authenticationOption
|
||
selectedAction.authentication.push(authenticationOption)
|
||
setSelectedAction(selectedAction)
|
||
|
||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption))
|
||
var newFields = []
|
||
for (const key in newAuthOption.fields) {
|
||
const value = newAuthOption.fields[key]
|
||
newFields.push({
|
||
key: key,
|
||
value: value,
|
||
})
|
||
}
|
||
|
||
console.log("FIELDS: ", newFields)
|
||
newAuthOption.fields = newFields
|
||
setNewAppAuth(newAuthOption)
|
||
//appAuthentication.push(newAuthOption)
|
||
//setAppAuthentication(appAuthentication)
|
||
setUpdate(authenticationOption.id)
|
||
|
||
/*
|
||
{selectedAction.authentication.map(data => (
|
||
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
*/
|
||
|
||
}
|
||
|
||
if (authenticationOption.label === null || authenticationOption.label === undefined) {
|
||
authenticationOption.label = selectedApp.name+" authentication"
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<DialogContent>
|
||
<a target="_blank" rel="norefferer" href="https://shuffler.io/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</a><div/>
|
||
These are required fields for authenticating with {selectedApp.name}
|
||
<div style={{marginTop: 15}}/>
|
||
<b>Name - what is this used for?</b>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"Auth july 2020"}
|
||
defaultValue={`Auth for ${selectedApp.name}`}
|
||
onChange={(event) => {
|
||
authenticationOption.label = event.target.value
|
||
}}
|
||
/>
|
||
{selectedApp.link.length > 0 ? <div style={{marginTop: 15}}><EndpointData /></div> : null}
|
||
<Divider style={{marginTop: 15, marginBottom: 15, backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{}}/>
|
||
{selectedApp.authentication.parameters.map((data, index) => {
|
||
return (
|
||
<div key={index} style={{marginTop: 10}}>
|
||
<LockOpenIcon style={{marginRight: 10}}/>
|
||
<b>{data.name}</b>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type={data.example !== undefined && data.example.includes("***") ? "password" : "text"}
|
||
color="primary"
|
||
placeholder={data.example}
|
||
onChange={(event) => {
|
||
authenticationOption.fields[data.name] = event.target.value
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
})}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setAuthenticationModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} onClick={() => {
|
||
handleSubmitCheck()
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const EndpointData = () => {
|
||
const [tmpVar, setTmpVar] = React.useState("")
|
||
|
||
return (
|
||
<div>
|
||
The API endpoint to use (URL) - predefined in the app
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type="text"
|
||
color="primary"
|
||
disabled={true}
|
||
placeholder="Bearer token"
|
||
defaultValue={selectedApp.link}
|
||
onChange={(event) => {
|
||
setTmpVar(event.target.value)
|
||
}}
|
||
onBlur={() => {
|
||
selectedApp.link = tmpVar
|
||
console.log("LINK: ", selectedApp.link)
|
||
setSelectedApp(selectedApp)
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
|
||
// This whole part is redundant. Made it part of Arguments instead.
|
||
const authenticationModal = authenticationModalOpen ?
|
||
<Dialog
|
||
open={authenticationModalOpen}
|
||
onClose={() => {
|
||
//setAuthenticationModalOpen(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
minWidth: 600,
|
||
padding: 15,
|
||
},
|
||
}}
|
||
>
|
||
<DialogTitle><div style={{color: "white"}}>Authentication for {selectedApp.name}</div></DialogTitle>
|
||
<AuthenticationData app={selectedApp} />
|
||
</Dialog> : null
|
||
|
||
const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
|
||
<div>
|
||
{newView}
|
||
{variablesModal}
|
||
{executionVariableModal}
|
||
{conditionsModal}
|
||
{authenticationModal}
|
||
{codePopoutModal}
|
||
<TextField
|
||
id="copy_element_shuffle"
|
||
value={to_be_copied}
|
||
style={{display: "none", }}
|
||
/>
|
||
</div>
|
||
:
|
||
<div>
|
||
</div>
|
||
|
||
|
||
return (
|
||
<div>
|
||
<Prompt
|
||
when={!lastSaved}
|
||
message={unloadText}
|
||
/>
|
||
{loadedCheck}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AngularWorkflow
|