import React, {useRef, useState, useEffect, useLayoutEffect} from 'react';
import { useInterval } from 'react-powerhooks';
import { useTheme } from '@material-ui/core/styles';
import uuid from "uuid";
import {Link} from 'react-router-dom';
import { Prompt } from 'react-router'
import { useBeforeunload } from 'react-beforeunload';
import ReactJson from 'react-json-view'
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 {Undo as UndoIcon, FileCopy as FileCopyIcon, GetApp as GetAppIcon, Search as SearchIcon, 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, GetIconInfo } from "./Workflows.jsx";
import { GetParsedPaths } from "./Apps.jsx";
import ConfigureWorkflow from '../components/ConfigureWorkflow.jsx';
import ParsedAction from '../components/ParsedAction.jsx';
import Scroll from 'react-scroll'
import { Element as ScrollElement, animateScroll as scroll, scrollSpy, scroller } from 'react-scroll'
const surfaceColor = "#27292D"
const inputColor = "#383B40"
// http://apps.cytoscape.org/apps/yfileslayoutalgorithms
cytoscape.use(edgehandles);
cytoscape.use(clipboard);
cytoscape.use(undoRedo);
cytoscape.use(cxtmenu);
// Adds specific text to items
//import popper from 'cytoscape-popper';
//cytoscape.use(popper);
// 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;
}
export function sortByKey(array, key) {
if (array === undefined) {
return []
}
if (key.startsWith("-") && key.length > 2) {
key = key.slice(1, key.length)
return array.sort(function(a, b) {
var x = a[key]; var y = b[key]
return ((x < y) ? -1 : ((x > y) ? 1 : 0))
}).reverse()
}
return array.sort(function(a, b) {
var x = a[key]; var y = b[key]
return ((x < y) ? -1 : ((x > y) ? 1 : 0))
})
}
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 svgSize = 24
//const referenceUrl = "https://shuffler.io/functions/webhooks/"
//const referenceUrl = window.location.origin+"/api/v1/hooks/"
function useTraceUpdate(props) {
const prev = useRef(props)
useEffect(() => {
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
if (prev.current[k] !== v) {
ps[k] = [prev.current[k], v];
}
return ps;
}, {});
if (Object.keys(changedProps).length > 0) {
console.log('Changed props:', changedProps);
}
prev.current = props;
});
}
const AngularWorkflow = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
const referenceUrl = globalUrl+"/api/v1/hooks/"
const alert = useAlert()
const theme = useTheme();
const green = "#86c142"
const yellow = "#FECC00"
const [bodyWidth, bodyHeight] = useWindowSize();
var to_be_copied = ""
const [cystyle, ] = useState(cytoscapestyle)
const [cy, setCy] = 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 [scrollConfig, setScrollConfig] = React.useState({
top: 0,
left: 0,
selected: "",
})
const [history, setHistory] = React.useState([])
const [historyIndex, setHistoryIndex] = React.useState(history.length)
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 [authLoaded, setAuthLoaded] = 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 [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false)
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 [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)
// This should all be set once, not on every iteration
// Use states and don't update lol
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 appBarSize = isCloud ? 75 : 60
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([])
// No point going as fast, as the nodes aren't realtime anymore, but bulk updated.
// Set it from 2500 to 6000 to reduce overall load
const { start, stop } = useInterval({
duration: 3000,
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)
// Sets up subflow trigger with the right info
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") {
if (param.value === workflow.id) {
setSubworkflow(workflow)
outersub = workflow
} else {
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 APIKEY gen :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 get settings :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
// - means it's opposite
const newkeys = sortByKey(responseJson, "-started_at")
setWorkflowExecutions(newkeys)
//console.log("NEWKEYS: ", newkeys)
//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)
start()
setExecutionRequest({
"execution_id": execution.execution_id,
"authorization": execution.authorization,
})
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 ?
{workflowExecutions.slice(0,15).map((data, index) => {
return (
{new Date(data.started_at*1000).toISOString()}
, {data.status}
{data.result.length > 0 ? ", "+data.result : ", "}
{data.execution_argument.length > 0 ? ", "+data.execution_argument : ", "}
)
return
})}
: 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, executionRequest)
})
.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 ABORT 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, executionRequest) => {
//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 && responseJson.results !== undefined && responseJson.results !== null)) {
if (executionData.status !== responseJson.status || executionData.result !== responseJson.result || executionData.results.length !== responseJson.results.length) {
//console.log("Updated state with this data:")
//console.log(responseJson)
//console.log(executionData)
setExecutionData(responseJson)
} else {
console.log("NOT updating state.")
}
}
} else {
//console.log("JSON is same")
}
//console.log("PRE LOOPING RESULTS: !", responseJson.execution_id, executionRequest.execution_id)
if (responseJson.execution_id !== executionRequest.execution_id) {
cy.elements().removeClass('success-highlight failure-highlight executing-highlight')
return
}
//console.log("LOOPING RESULTS!")
if (responseJson.results !== null && responseJson.results.length > 0) {
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')
incomingEdges.addClass('success-highlight')
outgoingEdges.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
if (executionRunning) {
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 !== undefined && item.action.result !== null && !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()
if (executionRunning) {
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())
} else {
//console.log("Nothing to update")
}
}
const saveWorkflow = (curworkflow) => {
var success = false
if (isCloud && !isLoggedIn) {
console.log("Should redirect to register with redirect.")
window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle`
return
}
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 = []
var newVBranches = []
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,
decorator: cyelements[key].data().decorator,
}
if (parsedElement.decorator) {
newVBranches.push(parsedElement)
} else {
newBranches.push(parsedElement)
}
} else {
if (type === "ACTION") {
const cyelement = cyelements[key].data()
const elementid = cyelement.id === undefined || cyelement.id === null ? cyelement["_id"] : cyelement.id
var curworkflowAction = useworkflow.actions.find(a => a !== undefined && (a["id"] === elementid || a["_id"] === elementid))
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
useworkflow.visual_branches = newVBranches
// Errors are backend defined
useworkflow.errors = []
useworkflow.previously_saved = true
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 {
if (responseJson.new_id !== undefined && responseJson.new_id !== null) {
window.location.pathname = "/workflows/"+responseJson.new_id
}
success = true
if (responseJson.errors !== undefined) {
//console.log(responseJson)
workflow.errors = responseJson.errors
if (responseJson.errors.length === 0) {
workflow.isValid = true
workflow.is_valid = true
//console.log("ELEMENTS: ", cy.elements())
//const setupGraph = () => {
const cyelements = cy.elements()
for (var i = 0; i < cyelements.length; i++) {
cyelements[i].removeStyle()
cyelements[i].data().is_valid = true
cyelements[i].data().errors = []
}
for (var key in workflow.actions) {
workflow.actions[key].is_valid = true
workflow.actions[key].errors = []
}
}
for (var key in workflow.errors) {
//console.log("Error: ", workflow.errors[key])
alert.info(workflow.errors[key])
}
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.")
}
if (workflow.public) {
alert.info("Save it to get a new version")
}
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)
}
if (responseJson.execution_id === "" || responseJson.execution_id === undefined || responseJson.authorization === "" || responseJson.authorization === undefined ) {
alert.error("Something went wrong during execution startup")
console.log("BAD RESPONSE FOR EXECUTION: ", responseJson)
setExecutionRunning(false)
setExecutionRequestStarted(false)
stop()
for (var i = 0; i < curelements.length; i++) {
curelements[i].removeClass("not-executing-highlight")
}
return
}
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 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 (cy !== undefined) {
console.log("NEW AUTH = reset cy's onnodeselect")
// Remove the old listener for select, run with new one
cy.removeListener('select')
cy.on('select', 'node', (e) => onNodeSelect(e, newauth))
cy.on('select', 'edge', (e) => onEdgeSelect(e))
}
setAppAuthentication(newauth)
setAuthLoaded(true)
} else {
setAuthLoaded(true)
//alert.error("Failed getting authentications")
}
})
.catch(error => {
setAuthLoaded(true)
alert.error("Auth loading error: "+error.toString())
})
}
const getApps = () => {
fetch(globalUrl+"/api/v1/apps", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!")
}
return response.json()
})
.then((responseJson) => {
// FIXME - handle versions on left bar
//handleAppVersioning(responseJson)
//var tmpapps = []
//tmpapps = tmpapps.concat(getExtraApps())
//tmpapps = tmpapps.concat(responseJson)
console.log("APPS: ", responseJson)
setApps(responseJson)
if (isCloud) {
setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name)))
setPrioritizedApps(responseJson.filter(app => internalIds.includes(app.name)))
} else {
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 = []
}
if (responseJson.public) {
alert.info("This workflow is public. You will have to save it to make it your own!")
//setLastSaved(false)
}
setWorkflow(responseJson)
setWorkflowDone(true)
//console.log(responseJson)
// Add error checks
console.log("Workflow: ", responseJson)
if (!responseJson.public) {
if ((!responseJson.previously_saved || (!responseJson.is_valid || (responseJson.errors !== undefined || responseJson.errors !== null || responseJson.errors !== responseJson.errors.length > 0)))) {
setConfigureWorkflowModalOpen(true)
}
}
})
.catch(error => {
alert.error(error.toString())
});
}
const onUnselect = (event) => {
console.time("UNSELECT")
const nodedata = event.target.data()
if (nodedata.app_name === undefined && nodedata.source === undefined) {
return
}
// 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)
setScrollConfig({
top: 0,
left: 0,
selected: "",
})
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) {
*/
//console.log(event.target.data())
if (event.target.data().decorator) {
alert.info("This edge can't be edited.")
} else {
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({})
}
// Comparing locations between nodes and setting views
var styledElements = []
var originalLocation = {
x: 0,
y: 0,
}
var hiddenNodes = []
const onNodeDragStop = (event, selectedAction) => {
const nodedata = event.target.data()
console.log("IN NODE DRAG STOP: ", nodedata)
if (nodedata.id === selectedAction.id) {
return
}
if (nodedata.finished === false) {
return
}
//console.log("Drag: ", nodedata)
//return
//console.log("DRAGGED NODE: ", nodedata)
//console.log("TARGET NODE: ", selectedAction)
if (styledElements.length === 1) {
console.log("Should reset location and autofill: ", styledElements, selectedAction)
//event.target.position = originalLocation
//curworkflowTrigger.position = cyelements[key].position()
if (originalLocation.x !== 0 || originalLocation.y !== 0) {
const currentnode = cy.getElementById(nodedata.id)
if (currentnode !== null && currentnode !== undefined) {
//currentnode.position = originalLocation
currentnode.position("x", originalLocation.x)
currentnode.position("y", originalLocation.y)
}
originalLocation = {x: 0, y: 0}
}
const curElement = document.getElementById(styledElements[0])
if (curElement !== null && curElement !== undefined) {
console.log("ELE: ", curElement)
curElement.style.border = curElement.style.original_border
const newValue = "$"+nodedata.label.toLowerCase().replaceAll(" ", "_")
curElement.value = newValue
if (curElement.id.startsWith("rightside_field_")) {
console.log("FOUND FIELD WITH NUMBER: ", curElement.id)
const idsplit = curElement.id.split("_")
console.log(idsplit)
if (idsplit.length === 3 && !isNaN(idsplit[2])) {
console.log("ADDING TO PARAM ", idsplit[2])
console.log("PARAM: ", selectedAction)
selectedAction.parameters[idsplit[2]].value = newValue
}
}
}
}
const skipnames = []
if (nodedata.app_name !== undefined && ((
nodedata.app_name !== "Shuffle Tools" &&
nodedata.app_name !== "Testing" &&
nodedata.app_name !== "Shuffle Workflow" &&
nodedata.app_name !== "User Input" &&
nodedata.app_name !== "Webhook" &&
nodedata.app_name !== "Schedule" &&
nodedata.app_name !== "Email") || nodedata.isStartNode)
) {
const allNodes = cy.nodes().jsons()
var found = false
for (var key in allNodes) {
const currentNode = allNodes[key]
if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) {
found = true
console.log("FOUND THE NODE!")
break
}
}
// Readding the icon after moving the node
if (!found) {
//console.log("Node wasn't found")
const iconInfo = GetIconInfo(nodedata)
const svg_pin = ` `
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
const offset = nodedata.isStartNode ? 36 : 44
//console.log(event.target.position())
const decoratorNode = {
position: {
x: event.target.position().x+offset,
y: event.target.position().y+offset,
},
locked: true,
data: {
"isDescriptor": true,
"isValid": true,
"is_valid": true,
"label": "",
"image": svgpin_Url,
"imageColor": iconInfo.iconBackgroundColor,
"attachedTo": nodedata.id,
},
}
cy.add(decoratorNode).unselectify()
} else {
console.log("Node already exists - don't add descriptor node")
}
} else {
//console.log("Shouldnt re-add info? ")
}
originalLocation = {
x: 0,
y: 0,
}
}
const onNodeDrag = (event, selectedAction) => {
//if (Object.getOwnPropertyNames(selectedAction).length === 0) {
// return
//}
const nodedata = event.target.data()
//console.log("Dragging: ", nodedata)
if (nodedata.finished === false) {
return
}
//console.log("Dragging node!!")
if (nodedata.app_name == "Shuffle Tools" || nodedata.app_name == "Testing") {
//console.log("NODE: ",
//selector: `node[app_name="Shuffle Tools"]`,
//console.log(event.target)
// 1. Find location of node
// 2. Check if it's within view of another node (inside)
// 3. If it is, then hide text
}
if (nodedata.app_name !== undefined) {
//console.log("Trying to remove friendly nodes")
const allNodes = cy.nodes().jsons()
for (var key in allNodes) {
const currentNode = allNodes[key]
if (currentNode.data.attachedTo === nodedata.id) {
cy.getElementById(currentNode.data.id).remove()
}
}
} else {
console.log("No appid? ", nodedata)
}
if (nodedata.id === selectedAction.id) {
return
}
if (originalLocation.x === 0 && originalLocation.y === 0 && nodedata.position !== undefined) {
//console.log("Updating location!: ", nodedata)
originalLocation.x = nodedata.position.x
originalLocation.y = nodedata.position.y
}
// Part of autocomplete. Styles elements in frontend to indicate
// what and where we may input data for the user.
const onMouseUpdate = (e) => {
const x = e.pageX;
const y = e.pageY;
const elementMouseIsOver = document.elementFromPoint(x, y)
if (elementMouseIsOver !== undefined && elementMouseIsOver !== null) {
// Color for #f85a3e translated to rgb
const newBorder = "3px solid rgb(248, 90, 62)"
if (elementMouseIsOver.style.border != newBorder && elementMouseIsOver.id.includes("rightside")) {
console.log(elementMouseIsOver.style.border)
if (elementMouseIsOver.style.border !== undefined) {
elementMouseIsOver.style.original_border = elementMouseIsOver.style.border
} else {
elementMouseIsOver.style.original_border = ""
}
elementMouseIsOver.style.border = newBorder
console.log("STYLED: ", styledElements)
for (var key in styledElements) {
const curElement = document.getElementById(styledElements[key])
if (curElement !== null && curElement !== undefined) {
curElement.style.border = curElement.style.original_border
}
}
styledElements = []
styledElements.push(elementMouseIsOver.id)
} else if (elementMouseIsOver.id === "cytoscape_view" || elementMouseIsOver.id === "") {
for (var key in styledElements) {
const curElement = document.getElementById(styledElements[key])
if (curElement !== null && curElement !== undefined) {
curElement.style.border = curElement.style.original_border
}
}
styledElements = []
}
}
// Ensure it only happens once
document.removeEventListener('mousemove', onMouseUpdate, false)
}
document.addEventListener('mousemove', onMouseUpdate, false);
/*
event.target.animate({
style: {
"border-width": "12px",
"border-opacity": ".7",
}
}, {
duration: animationDuration,
})
event.target.animate({
style: {
"border-width": "12px",
"border-opacity": ".7",
}
}, {
duration: animationDuration,
})
*/
}
// Nodeselectbatching:
// https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once
const onNodeSelect = (event, newAppAuth) => {
const data = event.target.data()
if (data.isButton) {
console.log("BUTTON CLICKED: ", data)
if (data.buttonType === "delete") {
console.log("DELETE!")
const parentNode = cy.getElementById(data.attachedTo)
if (parentNode !== null && parentNode !== undefined) {
parentNode.remove()
}
//for (var key in allNodes) {
// const currentNode = allNodes[key]
// if (currentNode.data.attachedTo === data.attachedTo) {
// cy.getElementById(currentNode.data.id).remove()
// }
//}
} else if (data.buttonType === "copy") {
console.log("COPY!")
// 1. Find parent
// 2. Find branches for parent
// 3. Make a new node that's moved a little bit
const parentNode = cy.getElementById(data.attachedTo)
if (parentNode !== null && parentNode !== undefined) {
//parentNode.data()
var newNodeData = JSON.parse(JSON.stringify(parentNode.data()))
newNodeData.id = uuid.v4()
if (newNodeData.position !== undefined) {
newNodeData.position = {
"x": newNodeData.position.x+100,
"y": newNodeData.position.y+100,
}
}
newNodeData.isStartNode = false
newNodeData.errors = []
newNodeData.is_valid = true
newNodeData.isValid = true
cy.add({
group: 'nodes',
data: newNodeData,
position: newNodeData.position,
})
// Readding the icon after moving the node
//console.log("Node wasn't found")
if (newNodeData.app_name !== "Testing" || newNodeData.app_name !== "Shuffle Workflow") {
} else {
const iconInfo = GetIconInfo(newNodeData)
const svg_pin = ` `
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
const offset = newNodeData.isStartNode ? 36 : 44
//console.log(event.target.position())
const decoratorNode = {
position: {
x: newNodeData.position.x+offset,
y: newNodeData.position.y+offset,
},
locked: true,
data: {
"isDescriptor": true,
"isValid": true,
"is_valid": true,
"label": "",
"image": svgpin_Url,
"imageColor": iconInfo.iconBackgroundColor,
"attachedTo": newNodeData.id,
},
}
cy.add(decoratorNode).unselectify()
}
workflow.actions.push(newNodeData)
const sourcebranches = workflow.branches.filter(foundbranch => foundbranch.source_id === parentNode.data("id"))
const destinationbranches = workflow.branches.filter(foundbranch => foundbranch.destination_id === parentNode.data("id"))
//for (var key in sourcebranches) {
// var newbranch = JSON.parse(JSON.stringify(sourcebranches[key]))
// newbranch.id = uuid.v4()
// newbranch.source_id = newNodeData.id
// cy.add({
// group: "edges",
// data: newbranch,
// })
//}
for (var key in sourcebranches) {
var newbranch = JSON.parse(JSON.stringify(sourcebranches[key]))
newbranch.id = uuid.v4()
newbranch.source_id = newNodeData.id
newbranch._id = newbranch.id
newbranch.source = newbranch.source_id
newbranch.target = newbranch.destination_id
cy.add({
group: "edges",
data: newbranch,
})
}
for (var key in destinationbranches) {
var newbranch = JSON.parse(JSON.stringify(destinationbranches[key]))
newbranch.id = uuid.v4()
newbranch.destination_id = newNodeData.id
newbranch._id = newbranch.id
newbranch.source = newbranch.source_id
newbranch.target = newbranch.destination_id
cy.add({
group: "edges",
data: newbranch,
})
}
}
}
event.target.unselect()
return
} else if (data.isDescriptor) {
console.log("Can't select descriptor")
event.target.unselect()
return
}
//const node = cy.getElementById(data.id)
//if (node.length > 0) {
// node.addClass('shuffle-hover-highlight')
//}
//const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id)
//console.log("APPAUTH: ", newAppAuth)
//console.log("BRANCHES: ", branch)
if (data.type === "ACTION") {
var curaction = workflow.actions.find(a => a.id === data.id)
//console.log("INSIDE CURACTION: ", curaction)
if (!curaction || curaction === undefined) {
//event.target.unselect()
//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 || (a.loop_versions !== null && a.loop_versions.includes(curaction.app_version)))))
//console.log("APP: ", curapp)
if (!curapp || curapp === undefined) {
alert.error(`App ${curaction.app_name}:${curaction.app_version} not found. Is it activated?`)
const tmpapp = {
name: curaction.app_name,
app_name: curaction.app_name,
app_version: curaction.app_version,
id: curaction.app_id,
actions: [curaction],
}
console.log(tmpapp)
console.log(curaction)
setSelectedApp(tmpapp)
//setSelectedAction(JSON.parse(JSON.stringify(curaction)))
setSelectedAction(curaction)
//return
} else {
//console.log("AUTHENTICATION: ", curapp.authentication)
setRequiresAuthentication(curapp.authentication.required && curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null)
if (curapp.authentication.required) {
//console.log("App requires auth.")
// 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("FOUND AUTH: ", tmpAuth)
//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
}
}
}
//console.log("OPTIONS: ", authenticationOptions)
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 = {}
}
//setSelectedAction(JSON.parse(JSON.stringify(curaction)))
//console.log("CURAPP: ", curapp, selectedApp)
setSelectedApp(curapp)
setSelectedAction(curaction)
cy.removeListener('drag')
cy.removeListener('free')
cy.on('drag', 'node', (e) => onNodeDrag(e, curaction))
cy.on('free', 'node', (e) => onNodeDragStop(e, curaction))
}
if (environments !== undefined && environments !== null) {
var env = environments.find(a => a.Name === curaction.environment)
if (!env || env === undefined) {
env = environments[defaultEnvironmentIndex]
}
setSelectedActionEnvironment(env)
}
} 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)
setSelectedActionEnvironment(data.env)
if (data.app_name === "Shuffle Workflow") {
getAvailableWorkflows(trigger_index)
getSettings()
}
} else {
alert.error("Can't handle "+data.type)
}
//console.log("BAR: ", rightSideBarOpen, "SAVE: ", lastSaved)
setRightSideBarOpen(true)
setLastSaved(false)
// Refresh listeners
//cy.removeListener('select')
//cy.on('select', 'node', (e) => onNodeSelect(e, newAppAuth, curapp, rightSideBarOpen, lastSaved))
//cy.on('select', 'edge', (e) => onEdgeSelect(e))
setScrollConfig({
top: 0,
left: 0,
selected: "",
})
}
// Checks for errors in edges when they're added
const onEdgeAdded = (event) => {
setLastSaved(false)
const edge = event.target.data()
var targetnode = workflow.triggers.findIndex(data => data.id === edge.target)
if (targetnode !== -1) {
console.log("TARGETNODE: ", targetnode)
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()
}
}
targetnode = -1
var sourcenode = workflow.triggers.findIndex(data => data.id === edge.source)
console.log("SOURCENODE: ", sourcenode)
if (sourcenode !== -1) {
if (workflow.triggers[sourcenode].app_name === "User Input" || workflow.triggers[sourcenode].app_name === "Shuffle Workflow") {
//console.log("NORMAL TRIGGER")
} else {
//var currentnode = cy.getElementById(workflow.triggers[sourcenode].id)
//console.log("NODE: ", currentnode)
//if (currentnode !== null && currentnode !== undefined) {
// console.log("SHOULD CHECK IF TRIGGER HAS MULTIPLE EDGES: ", currentnode)
//if (workflow.branches !== undefined && workflow.branches !== null) {
// const found_branches = workflow.branches.filter(branch => branch.source == workflow.triggers[sourcenode].id)
// console.log("FOUND BRANCHES: ", found_branches)
// if (found_branches.length > 0) {
// alert.error("Can't have multiple branches from this trigger")
// event.target.remove()
// }
//}
//if (cy.edges().size() === 1) {
// https://js.cytoscape.org/#edges.connectedNodes
//console.log("CURRENTNODE: ", currentnode)
//console.log("EDGES: ", currentnode.connectedEdges(`node[id=${workflow.triggers[sourcenode].id}]`))
//console.log("EDGES2: ", currentnode.connectedEdges())
//currentnode.connectedEdges().animate({style: {lineColor: "red"}})
//console.log("OUTGOERS: ", currentnode.outgoers())
//console.log("LEN2: ", currentnode.edges().length)
//if (currentnode.connectedNodes().length > 0) {
// alert.error("Can't have multiple branches from this trigger")
// event.target.remove()
//}
}
}
//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) {
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 {
//console.log("INSIDE LAST CHECK: ", edge)
// 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) {
console.log("TARGETNODE: ", targetnode)
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,
"decorator": false,
}
if (!found) {
newbranch["hasErrors"] = false
workflow.branches.push(newbranch)
setWorkflow(workflow)
}
history.push({
"type": "edge",
"action": "added",
"data": edge,
})
setHistory(history)
setHistoryIndex(history.length)
}
const onNodeAdded = (event) => {
const node = event.target
const nodedata = event.target.data()
if (nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined)) {
console.log("RETURNING (NOT ADDING) NODE ADD FOR: ", nodedata)
return
}
//console.log("IS IT ADDED TO THE WORKFLOW?: ", nodedata)
if (node.isNode() && cy.nodes().size() === 1) {
//setStartNode(node.data('id'))
workflow.start = node.data('id')
nodedata.isStartNode = true
} 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
}
}
}
if (nodedata.type === "ACTION") {
if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) {
const newEdgeUuid = uuid.v4()
const newcybranch = {
"source": workflow.start,
"target": nodedata.id,
"_id": newEdgeUuid,
"id": newEdgeUuid,
"hasErrors": false,
}
const edgeToBeAdded = {
group: "edges",
data: newcybranch,
}
console.log("SHOULD STITCH WITH STARTNODE")
cy.add(edgeToBeAdded)
}
if (workflow.actions === undefined || workflow.actions === null) {
workflow.actions = [nodedata]
} else {
workflow.actions.push(nodedata)
}
setWorkflow(workflow)
} else if (nodedata.type === "TRIGGER") {
if (workflow.triggers === undefined) {
workflow.triggers = [nodedata]
} else {
workflow.triggers.push(nodedata)
}
const newEdgeUuid = uuid.v4()
const newcybranch = {
"source": nodedata.id,
"target": workflow.start,
"source_id": nodedata.id,
"destination_id": workflow.start,
"_id": newEdgeUuid,
"id": newEdgeUuid,
"hasErrors": false,
"decorator": false,
}
const edgeToBeAdded = {
group: "edges",
data: newcybranch,
}
if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") {
//workflow.branches.push(newbranch)
if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) {
cy.add(edgeToBeAdded)
}
}
//if (data.trigger_type === "WEBHOOK") {
// newWebhook(newAppData)
// saveWorkflow(workflow)
//}
setWorkflow(workflow)
}
if (nodedata.app_name !== undefined) {
history.push({
"type": "node",
"action": "added",
"data": nodedata,
})
setHistory(history)
setHistoryIndex(history.length)
}
//if (nodedata.app_name !== undefined && ((
// nodedata.app_name !== "Shuffle Tools" &&
// nodedata.app_name !== "Testing" &&
// nodedata.app_name !== "Shuffle Workflow" &&
// nodedata.app_name !== "User Input" &&
// nodedata.app_name !== "Webhook" &&
// nodedata.app_name !== "Schedule" &&
// nodedata.app_name !== "Email") || nodedata.isStartNode)
//) {
// const allNodes = cy.nodes().jsons()
// var found = false
// for (var key in allNodes) {
// const currentNode = allNodes[key]
// if (currentNode.data.attachedTo === nodedata.id && currentNode.data.isDescriptor) {
// found = true
// console.log("FOUND THE NODE!")
// break
// }
// }
// // Readding the icon after moving the node
// if (!found) {
// console.log("Node wasn't found")
// const iconInfo = GetIconInfo(nodedata)
// const svg_pin = ` `
// const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
// const offset = nodedata.isStartNode ? 36 : 44
// const decoratorNode = {
// position: {
// x: event.target.position().x+offset,
// y: event.target.position().y+offset,
// },
// locked: true,
// data: {
// "isDescriptor": true,
// "isValid": true,
// "is_valid": true,
// "label": "",
// "image": svgpin_Url,
// "imageColor": iconInfo.iconBackgroundColor,
// "attachedTo": nodedata.id,
// },
// }
// cy.add(decoratorNode)
// } else {
// console.log("Node already exists - don't add descriptor node")
// }
//} else {
// //console.log("Shouldnt re-add info? ")
//}
}
const onEdgeRemoved = (event) => {
setLastSaved(false)
const edge = event.target
workflow.branches = workflow.branches.filter(a => a.id !== edge.data().id)
setWorkflow(workflow)
event.target.remove()
// 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,
//}
}
if (edge.data().source !== undefined) {
history.push({
"type": "edge",
"action": "removed",
"data": edge.data().source,
})
setHistory(history)
setHistoryIndex(history.length)
}
}
const onNodeRemoved = (event) => {
const node = event.target
const data = node.data()
if (data.finished === false) {
return
}
//setLastSaved(false)
workflow.actions = workflow.actions.filter(a => a.id !== data.id)
workflow.triggers = workflow.triggers.filter(a => a.id !== data.id)
if (workflow.start === data.id && workflow.actions.length > 0) {
// FIXME - should check branches connected to startnode, as picking random
// is just confusing
if (workflow.actions[0].id !== data.id) {
const ele = cy.getElementById(workflow.actions[0].id)
if (ele !== undefined && ele !== null) {
ele.data("isStartNode", true)
workflow.start = ele.id()
}
} else {
if (workflow.actions.length > 1) {
const ele = cy.getElementById(workflow.actions[1].id)
if (ele !== undefined && ele !== null) {
ele.data("isStartNode", true)
workflow.start = ele.id()
}
}
}
//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
// }
//})
}
if (data.app_name !== undefined) {
//console.log("Trying to remove friendly nodes")
const allNodes = cy.nodes().jsons()
//console.log("NOT UNDEFINED IN HOVEROUT!", allNodes)
for (var key in allNodes) {
const currentNode = allNodes[key]
if (currentNode.data.attachedTo === data.id) {
cy.getElementById(currentNode.data.id).remove()
}
}
history.push({
"type": "node",
"action": "removed",
"data": data,
})
setHistory(history)
setHistoryIndex(history.length)
}
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")
//handleHistoryUndo()
}
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())
});
}
if (!firstrequest && graphSetup && established && props.match.params.key !== workflow.id && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0) {
//console.log(props.match.params.key, workflow.id)
//getWorkflow()
//setCy()
//getWorkflowExecution(props.match.params.key, "")
//setEstablished(false)
//setGraphSetup(false)
window.location.pathname = "/workflows/"+props.match.params.key
}
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
//console.log("PRE ELEMENTS: !", workflow.actions, graphSetup, apps, authLoaded, cy)
if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) {
setGraphSetup(true)
setupGraph()
//console.log("IN ELEMENT CHECK!")
} else if (!established && cy !== undefined && apps !== null && apps !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0 && authLoaded){
//This part has to load LAST, as it's kind of not async.
//This means we need everything else to happen first.
//
//console.log("IN THIS PART AGAIN")
//console.log("IN ESTABLISHED!")
setEstablished(true)
// Validate if the node is just a node lol
cy.edgehandles({
handleNodes: (el) => el.isNode() && !el.data("isButton") && !el.data("isDescriptor"),
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))
// Handles dragging
cy.on('drag', 'node', (e) => onNodeDrag(e, selectedAction))
cy.on('free', 'node', (e) => onNodeDragStop(e, selectedAction))
//let popper2 = cy.popper({
// content: () => {
// let div = document.createElement('div');
//
// div.innerHTML = 'Popper content'
//
// document.body.appendChild(div)
//
// return div
// },
// renderedPosition: () => ({ x: 100, y: 200 }),
// popper: {} // my popper options here
//});
//let popper1 = cy.nodes()[0].popper({
// content: () => {
// let div = document.createElement('div')
//
// div.innerHTML = 'Popper content'
// document.body.appendChild(div)
//
// return div
// },
// popper: {} // my popper options here
//})
//let update = () => {
// popper.update()
//}
//let node = cy.nodes().first()
//node.on('position', update)
//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))
document.title = "Workflow - "+workflow.name
registerKeys()
//setStartNode(workflow.start)
} else if (established) {
//console.log("established - should fix colors of things")
//console.log(cy.elements())
}
})
//}, [selectedAction])
var previousnodecolor = ""
//var previousedgecolor = ""
const animationDuration = 150
const onNodeHoverOut = (event) => {
const nodedata = event.target.data()
if (nodedata.app_name !== undefined) {
const allNodes = cy.nodes().jsons()
for (var key in allNodes) {
const currentNode = allNodes[key]
if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) {
cy.getElementById(currentNode.data.id).remove()
}
}
}
var parsedStyle = {
"border-width": "1px",
'font-size': '18px',
}
if ((nodedata.app_name === "Testing" || nodedata.app_name === "Shuffle Tools") && !nodedata.isStartNode) {
parsedStyle = {
"border-width": "1px",
'font-size': '0px',
}
}
event.target.animate({
style: parsedStyle,
}, {
duration: animationDuration,
})
}
const addCopyButton = (event) => {
var parentNode = cy.$('#' + event.target.data("id"));
if (parentNode.data('isButton') || parentNode.data('buttonId'))
return
//parentNode.lock()
const px = parentNode.position('x') - 65
const py = parentNode.position('y') - 25
const circleId = newNodeId = uuid.v4()
parentNode.data('circleId', circleId)
const iconInfo = {
"icon": "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z",
"iconColor": "black",
"iconBackgroundColor": "white",
}
const svg_pin = ` `
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
cy.add({
group: 'nodes',
data: {
weight: 30,
id: circleId,
name: "TEEEXT",
isButton: true,
buttonType: "copy",
attachedTo: event.target.data("id"),
icon: svgpin_Url,
iconBackground: iconInfo.iconBackgroundColor,
is_valid: true,
},
position: { x: px, y: py },
locked: true
})
//.unselectify()
}
const addDeleteButton = (event) => {
var parentNode = cy.$('#' + event.target.data("id"));
if (parentNode.data('isButton') || parentNode.data('buttonId'))
return
//parentNode.lock()
const px = parentNode.position('x') - 65
const py = parentNode.position('y') + 25
const circleId = newNodeId = uuid.v4()
parentNode.data('circleId', circleId)
const iconInfo = {
"icon": "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
"iconColor": "black",
"iconBackgroundColor": "white",
}
const svg_pin = ` `
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
cy.add({
group: 'nodes',
data: {
weight: 30,
id: circleId,
name: "TEEEXT",
isButton: true,
buttonType: "delete",
attachedTo: event.target.data("id"),
icon: svgpin_Url,
iconBackground: iconInfo.iconBackgroundColor,
is_valid: true,
},
position: { x: px, y: py },
locked: true
})
//.unselectify()
}
const onNodeHover = (event) => {
//console.log("TAR: ", event.target)
const nodedata = event.target.data()
var parentNode = cy.$('#' + event.target.data("id"));
if (parentNode.data('isButton') || parentNode.data('buttonId'))
return
if (nodedata.app_name !== undefined) {
const allNodes = cy.nodes().jsons()
var found = false
for (var key in allNodes) {
const currentNode = allNodes[key]
if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) {
cy.getElementById(currentNode.data.id).remove()
}
if (currentNode.data.isButton && currentNode.data.attachedTo === nodedata.id) {
found = true
}
}
if (!found) {
addDeleteButton(event)
addCopyButton(event)
}
}
const parsedStyle = {
"border-width": "7px",
"border-opacity": ".7",
'font-size': '25px',
'color': 'white',
}
event.target.animate({
style: parsedStyle
}, {
duration: animationDuration,
})
previousnodecolor = event.target.style("border-color")
}
const onEdgeHoverOut = (event) => {
if (event === null || event === undefined) {
event.target.removeStyle()
return
}
const edgeData = event.target.data()
if (edgeData.decorator === true) {
return
}
event.target.removeStyle()
}
// This is here to have a proper transition for lines
const onEdgeHover = (event) => {
if (event === null || event === undefined) {
return
}
const edgeData = event.target.data()
if (edgeData.decorator === true) {
return
}
const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color")
const targetcolor = cy.getElementById(event.target.data("target")).style("border-color")
if (sourcecolor !== null && sourcecolor !== undefined && targetcolor !== null && targetcolor !== undefined) {
event.target.animate({
style: {
'target-arrow-color': targetcolor,
"line-fill": "linear-gradient",
"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 decoratorNodes = workflow.actions.map(action => {
if (!action.isStartNode) {
if (action.app_name === "Testing" || action.app_name === "Shuffle Tools") {
return null
}
}
const iconInfo = GetIconInfo(action)
const svg_pin = ` `
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
const offset = action.isStartNode ? 36 : 44
const decoratorNode = {
position: {
x: action.position.x+offset,
y: action.position.y+offset,
},
locked: true,
data: {
"isDescriptor": true,
"isValid": true,
"is_valid": true,
"label": "",
"image": svgpin_Url,
"imageColor": iconInfo.iconBackgroundColor,
"attachedTo": action.id,
},
}
return decoratorNode
return null
})
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, decoratorNodes)
insertedNodes = insertedNodes.filter(node => node !== null)
var 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,
decorator: false,
}
// This is an attempt at prettier edges. The numbers are weird to work with.
/*
//http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html
const sourcenode = actions.find(node => node.data._id === branch.source_id)
const destinationnode = actions.find(node => node.data._id === branch.destination_id)
if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) {
//node.data._id = action["id"]
console.log("SOURCE: ", sourcenode.position)
console.log("DESTINATIONNODE: ", destinationnode.position)
var opposite = true
if (sourcenode.position.x > destinationnode.position.x) {
opposite = false
} else {
opposite = true
}
edge.style = {
'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"],
'control-point-weight': ['0.3', '0.7'],
}
}
*/
return edge;
})
if (workflow.visual_branches !== undefined && workflow.visual_branches !== null && workflow.visual_branches.length > 0) {
const visualedges = workflow.visual_branches.map((branch, index) => {
const edge = { };
var conditions = workflow.branches[index].conditions
if (conditions === undefined || conditions === null) {
conditions = []
}
const label = "Subflow"
edge.data = {
id: branch.id,
_id: branch.id,
source: branch.source_id,
target: branch.destination_id,
label: label,
decorator: true,
}
return edge;
})
edges = edges.concat(visualedges)
}
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({})
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)
}
}
if (selectedNode.data().decorator === true) {
alert.info("This edge can't be deleted.")
} else {
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) {
if (responseJson.reason !== undefined) {
alert.error("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: theme.palette.borderRadius,
minHeight: 100,
maxHeight: 100,
minWidth: "100%",
maxWidth: "100%",
marginTop: "5px",
color: "white",
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
}
const paperVariableStyle = {
borderRadius: theme.palette.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) => {
console.log("Delete:" ,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 (
What are
WORKFLOW variables?
{workflow.workflow_variables === null ?
null : workflow.workflow_variables.map((variable, index) => {
return (
{
}}>
{
setNewVariableName(variable.name)
setNewVariableDescription(variable.description)
setNewVariableValue(variable.value)
setVariablesModalOpen(true)}}>
Name: {variable.name}
)
})}
{
setVariablesModalOpen(true)
setLastSaved(false)
}}>New workflow variable
What are
EXECUTION variables?
{workflow.execution_variables === null || workflow.execution_variables === undefined ?
null : workflow.execution_variables.map(variable=> {
return (
{
}}>
{
setNewVariableName(variable.name)
setExecutionVariablesModalOpen(true)
}}>
Name: {variable.name}
)
})}
{
setExecutionVariablesModalOpen(true)
setLastSaved(false)
}}>New execution variable
)
}
const curTab = 0
const handleSetTab = (event, newValue) => {
setCurrentView(newValue)
}
const HandleLeftView = () => {
// Defaults to apps.
var thisview =
if (currentView === 1) {
thisview =
} else if (currentView === 2) {
thisview =
}
const tabStyle = {
maxWidth: leftBarSize/3,
minWidth: leftBarSize/3,
flex: 1,
textTransform: "none",
}
const iconStyle = {
marginTop: 3,
marginRight: 5,
}
return(
{thisview}
Apps
} style={tabStyle} />
Triggers
} style={tabStyle} />
Variables
}style={tabStyle} />
)
}
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,iVBORw0KGgoAAAANSUhEUgAAAK4AAACuCAYAAACvDDbuAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAACE4AAAhOAFFljFgAAAAB3RJTUUH5AsGCjIrX+G1HgAAMc5JREFUeNrtfW2stll11rWec78jztjOaIFAES2R0kGpP0q1NFKIiX/EpNE0/GgbRUzUaERj8CM2qUZN+s+ayA9jTOyY2JqItU2jnabBRKCJQKExdqRQPoZgUiylgFPnhXnfc87yx3N/7PW97uc8Z/5wdjLvnGff+2PttddeH9fe974JzXT1tx8F3/s6cD09BvAbAHwfgDcBeBLAKwH6ZjAmAOC1Fh3/x+DjDwpaJ4DHeqr+2qioL9qcf4DZ6cPWBWtaOKJtfkYACwI1bbaPYfwbfaZfr96Wx5Khtl+ioR1nbA3a3LbHRxzUxcKTgndbugTwHED/B8CvAfjvAD4I4H8dLqbnLx8+wBNPfQ6dRFWBy79/D7gkgPhbALwNwNsB/HEALwVwETGGG4w5DpzWXI5IcgXPKcuBALCTN7bD+YJyx5W2r2hbBZf8xRnQxwCIM75Ynox9EM/s5Vxow/Zd3sTKhHW5hC/zQr5ixm8R6KMA/iPATz94ePnFe4/cw+P/5jPIUiq4l3/3Hpj5UQL+LEB/DUeBfaQirr3i2RmsnollCsRDh1nuyo/pO2rnjA0zg3n7JSaYR/5TWHerF42v0mYJfY7Aj1lHoY/HppWKaY7zugs/CGjVXfi+ds2i/kMAHwPjXzDwcwA9/8RTn0aUXMG9fvdL8PDe13FxOb0OwI8AeDtAj1pKvNWm3QOvGykUbpnKxInBB+UqbVm5B4acbn0tuLW29OvdZGxevSEvFdrCivDpWnoUWLd9xn0AP8PAP2bmTx0OBzz+E58yJQ864+G77+Hzv/m7cHF1760AfgrAO4zQcs747Qe5TGfWQqsH7ra3/mZTJJ5YXpvcIXQsXGeYcXBVfxufL7RBPVPE4R/PKiFqamVO0q/yh3cJLU4WWh7+zdKjAH6YQP/+QIe3fO3qs/i/7/xDObcevvseplc9xNVvTH8GwHsAvCbwrVZt6mqFhnsQ+rWBP0uGcRGTC22UBhP+GFg2FoxtyNvhl471cgsSj01o58z9KFyLHn39eMDwhPbUJwB4FuB3PXj4xH955N5X8PhTn9GtAtd/7xFcXV2BDoe3gPkpuELrRbCKURvjrF/KurqOkGPGWeG5JfTANNkR+oa/fqzPGwygxla5TdR1D0R96YOnwVgieJVrsPEujkUqhREHqM8CeCdA7weusfi9BwDgf/h7cX19DSJ6HZj/OSJNuxKwMI22ToX9dkaZCa1h/NhSEIm7dQM4RzLXZ5zS6IxNMKijZUv6CFZo81YEbdypYVwLR2hJWC8x4LStFu+M0DIKt7AOMl8D4J8B/DqA8KW/dPRaDwBwdf93MPuxPwrguxANjG22pN3zaQEwqepRIKEZQLY0h0KhmtR+aeEeJBozNZHLwMil1h+vQ5Pv09rYdllQCj3gCgEYf7es1/CcM6ENhR7qmWe98wU5yMsbAfwIMz86Xb3qmHP5d+6BjjbshwD61zg6x+GATZRc4LRLMLb5tPuiX+m7BQzIBr/DRMULq3YPDFyW1t34EtHtj42GOUAudAZjJd1ywhu/XznOhC9oYNAdf32se0Qb/gqAnwSAwzGIppcD9C6MQuswMxZaHz3Qfq0bAUfC7g7AEQrWzW51S6E1OUEfBU8WpvfrUj42l1lwhDZK2vWxtoCtlRoQlK2e1u6x0A7tzchH6Eu7gZgWWtEeADwK0F8F8FKAcJixqbcB/MZogkafj4Iy7uAzjbIEHM7kRK3CzxN+VRkgjfUHjWlgoQry41G+HKFraKPctSB2FQOPdRH2Mf6WCE7ATxCtfVaavJzXxXGiRn3ysxfYT1ra7wHoT4OBAxE9BtAPAHRPMImle+D6R5awY2gpcEx3cKxWncBLBb0eEyv0oMRZq7rRxFHIePkz0UZzH2PwJ4kht+ERgt3qkioq6XPdMvYESvZpsd1kLJr8aFwO3/XYVPseOvEIgLeD8NjEjDcA+O6tFRaa1dUK6eaB61zrug7ntp5S/43NfC6jZGahXJzKFnUQ5wcaGGoNeSVaHoX74ga/kWuWj81UCefMBHCCd4QhRKmgSlb/H/Nd5avm3LXUJu+PAfjOCcCbAbxMF+W4YsCohkbmG9T1J4YCuMwXWuMNnyoUgQl08u25iKXwPl864YGif+u3os3rbwGaBxm00Ib6LXxnV1/Ewd5Gei20M9dexsCbJwDfi/WUVzq4gdtGIBe3LYmuo/ajnSndu55sI7Dkn76a+zXoQVdr2ejfRr5x3aWsCWzF2GhsdOblEKw0cdCyn4TmtIvEiuToRqT0NB93zAVwQeDvmQA8WZr3NSPsYP3HmN/OwE7Km58wmGcNGwqTo2nTiVL1FxW0MLm3sSFNaNgfGxpJCDwD7OyaRXzhvTxlGXTbICl0m3hj5L4+pUeRaFqRv/GEQU9OYLyy7LSLtc55HW1kEYosmHLIWZhO5BExP/ecq52LSgSZHZ+2D79l5ldMbgbwYwjkDW26j3he3TnLFwtxZ/WnLlgyNlFERloEfOsE0DfLajuCFdNxL4jjgVlLDOkyoTo70NRE48TuCcRUsO2XyVIaiHWDqUC4ncUsYa/aXeC2MG19S1XQ5Ut3MVeadqX7myZgfN3Gq2j4gtHviwch/LaQuOPcdqPzHeiGItwlo+pDmviNB0WEvndvXqM4vENox3mLNwc0S6jwlfJAjLPxB/Ot8HmHBit74U4a497k0mAbNCqsNPMdn2wRHq7qbYPItxFtfR/O60BWhmYPV7TlMgsUaOnFQWcxSz0UoBq/Q58YUO02qanf6dNaZIz8Bw79gdACmLVtixCGMM2clWv5w1QzGVaj0LJaw0GPZZuIxdgHNxCAcmwRX/x+rSceWKBhJuMD6rH5HnmpnaGw37HpHRj0xkWyspLjtHDlWvF96gLcTBiO9wXl3B0tJ3Bw/dmob91edDRwPwSkf9tXZsKxrZPefSm00khMdb1tJ8AxoQ1tqRQC5XVl3mbp9vDUE0Dze4VS3dYC2ibboCm0+iMpThvkkdYoHiFu393of843zqjyyzKGM9TCL5ECrf7C8Z9izbK6rs9eCS1XizpZVNliueGiXZuNtG085zzZjg2nHJ+o57sJjdKcXD+Yyhjvuy/CbW6cVQ3py3z12XzHC8M+CMdXBLFjXs0bD8rLeafrjnT23xQOshoLclRwjU0NmiRTNPApB+F2ykH7xn0KhDuBvMgELKpHE13nDI1o2yLe7sbE1kwYLLpu084glm1dwQ6ulcBoe7r+vlU2SaGhXwFvejzReYpHgQIYJGwrq4IzkkI7+DWp31euqB7eas1RtiI6JqkwkQLSUyaq4XOiLbTzUf3lcYU81CQ3+EkIi3b84SzmCOZbLpCuFRE/j/txsk93vqeeAEREe5qrP9AQRJ8Zy06ZmL5Tg7NIE+YYar64YlMY8G8eehQongbnlb6923Y2jlwYWzzJ0QMqfX0Jh/nEFZrPqdl5R58Ce9AUvN6xyaKNDM6rNV7oNhU+I8J6dvYXoRM7fh34MA1ivfEde1lC+9TE7xHaBuQ1jtNpyPJqoG9KGp9j3f4WsEEdUthrZFuH8bF2lw59r76bFwYa8mgi75zYlS9NZERj19sf8Rg8we4GgK1g7ySh9ZCXY75ldQtVYYCIVlTBHyyhKbT+nVA1YewyvR8l06AtupsLcR9x35v7EqQCUdn63ulWNGgbaege69Sk0w0WpKGlEe/sFtpt3tZgTPi4edDgE7H8m2sGn3Gr4IU4puLw0CvvmdzVg5RBUkVfT+DjPGsJgrouOoIiU9alcDEiVSQplNcW2pq+vTBqNgyG2oCIg4G6sapTVYVkP2YAMljJfO6GULH4p8e89WyEM8Yc7hna2GFFQv5l7kWF07q3y3jQWm9Bem8Ld11Cb/vdXzQ25vCan9LB14yXTNkc/0UPmF550X0iQDK8FQ7Uhg96fh8Acw3p1kTvRkifeVHdTENJfzblnRjb8acbKLrY+vKYmCoBMHXJLzf01UGP5vyonHBv3UBsvG4D/bk5lvVRhagxzTih0rTgFYMn5NpkqD+yO/C5KaobjiPBMlvXfBrmngYLLWNb/XZeeKMFz0c/+q6FLeP4tM68LQ06GDtX7Uuqt7nz5sxLsT88uUxuDLyOdvWwx2FUg5bReD+YInjbPTSu7gxYd5vZCXk1gsQsws5vNJT8qfkR9733FR/eRl4qpZHH7hrI+q0s1fE5T1mnQw75g+35bsbEZ2mYlE0bsWo3G5Q9RtcW2k5wejKkJAstOsxObKWIim3pxF9PhZbrJjnUyI12OqjKMH8e1jK27bgKpgMBi+WRsh9wSKe8oRWgzUzDqpi+9mmlUvBab7pG7dvJ7gut9IcbxKd998/xSquXIg+Khl3vEyrejchBUpc6xxrXXF6KNT8Swkr0OJ581oGcu2NE+YpNB5tMTmf8/ux1DuXkfHFpcfPcAGd4HJvvLfiLBpaPa5uDvsU9RYGweDJAlw7fp6qxHiGxCV0ibM4HQHLFBuUi6Kj0t26iLQHvyiYhQDs2F3bdVrk4lhwJbRSXyPwQQ434XObFUF4vWByUDW+SIpTjyAOn7nAe14M8dGjkNBS4DKvK3+kPdfBRM3iH0m5dGupHEaxDolLzta+/+LSp8Om8obBLX4Z9A7teukx9S4+AQGjVgvabHJ5pVCXh+9rUcQNCMn7dHCAjfD3za8H6IDBTg+KhrG2sF+xEAw1TFv0Gq31REAVOOwS0FvLa+q98WuspBvGFENo8PqgRII+WPCmLsPGGvHJzEfJlg8oFCbbHGme7XUBjw1qRrfsRYjBQNbE6iEsYzaTfHbTlAmY7whBuXui8FipybFGAsKd8LspTCEnAIyZ22dwJzy6wbtbRlk7gJECx9RHp0tHRxG0IBidP+WkYRYD1cduaK/NvGtiubOoEfyv03ZAKhdQKmwYV5Rx/UY6tNz6XvlaQGFnXnE9S+FpWRLoWBQ90sDgK3yqxrm+4KSqwo+TKfgf65ufmWKP0UfY79DTSFpVT6EEXy+R5YGY4RpjGFmkdsdXOOmolWxccbP2OdUeKttj4GJhyo67t0zTnlTOraeRlzJdFsC23imA3c602RTLMPUVlTnNJBrlYX90xZpq6GlOJUbid5+N9emBpP0rOaHoJcDEFuLFleLTKrYviGOXsWw4u7ST/Va/zuaacxzo+77LAjoqgBpBRPK6vwA9fSOYghqySFArtXkRKdzgu3UXjqmsT9qMHpXvASTMR0eNvVjTSARd/6q/j4vV/Eri+Slts99lzunakOqg8uY2gWU/3e20RHXD5uWfwwk//OPjBCwhx4E5glgmeypNaPkAsliKsTxFuaZoxScrpK3zGUmgrl6PWFPIHASAcXvkdOLz2TRVn71KQmK+BwwTghVLTDnaHpLLqdOR9tSfGgpc/Mmh0EtHgCTtitaatzGNU0TNTfe1zlxppp4Vh7etUUJuDLDWFdrawFChUWk+HUaZV87xMmHTE2twiHeuwzr1Lt5fiQCyB4qK6zNkdb5TUhxODqzJTqvFMcEvh86huWKQBmY2Hze+07S0mDvSWB5U1tPQCDlvX1V8Yoi4vjqDenJBaOrit0V9REiqLyLa7NwaEL4Q2vXt3qdtGPe5SmsIg/IQXNof67hFYsVsa7w3Ux2cJwXlcZojvKiggKf0Cufwz3DkKGMCZlh8F/uwIwDd6quaISp5vCse5qsB0Yv0F/xWftV+x9RudDhNCK/7eEUyNxOQ7Ttos5OXu0jlTB1/va1ovrsnqjruYNaFmA6IYlBqYW451zk5Tk8Ex0cDutO6Zk9kBLcvFAjvnFRivZ5GFkgz2s6YSQy335v3dFdIZEbzFGvt2/BuXi4Q7DXzzFPE9trBBOa9MZ2Oi8R0Oj+jgJhu98jKfVhz6gTH72Vbf2ISnZcNlfyew50x7b7AZKmLX+4S24zWYchcQAd5dcURIXk8vj571Btbf/vOK3Pm5L0rycNVobnmskriDiWwMdU0sJRWWd2bk+EjguNbXSNyDIdLrH9+TBI7nqHonxDbk4Q4NO1Ny4bAEjpqFirrtBW3lb8Z47ohsY9oednfEvPMDUYQZ1FWBWHU6S5dbzkfcxWbnSI1zJEviolxxyivavrVtB8k71pgTMFYOxiEyY58pwPf6jBw2J+6U7k3TuEWk8dLx+ZZ3qj+cHZjJ6kV5E3eFlhuENAIxdzcsrTs/Ni7NGcT2+grg65u3c440mhOdT065KlFR9uICuLr02rVWlAPLuoumBqKQ3Uau8p17FXLCQtiq2gLmTNN2NyVk2Zu6Cpcf/Clc/ur7ALpo0AP4t6En5VUd82g+sL+4PhEP/Mr2uZRx/7DS9vgAfu7L80Hy0j1guCe1egF66IaKxWXPxsR8Sb8smTHvjJrWY6pugfeYmn66+t8fx+WvPA06TL4gbpliu3Ecb32IRJZtXaIcRNUCYhTaSfTrngdwNwWInEU7jndFAChUIIFm30iTAknQ97jFi4+Tc9zJBgSsmQonh5yaJGY2FLwAPbCHeW7Boz0cQIcL4HDh45HmxkSPB0UQKyZtmWWqv7dwrCuEcKvtwFFCMrX26vJ97VsIloHUG9v+R23pbjqVwG/5YgKqY41DBwuj7AJzGU8LAal7EJiZVSFEk8trF2dIm78cCK071oW5QoMEvJOkqy+lR2SJgfvxBbPXiNSE5uBKyDutkCJIqrbA6WcDUmQiyneCs7Bipv6BfMUO0EccxNksvapT9+BMWJgfE9UTu2g7jmdYjc3i112Xa6TR7GRqi6QEY+Fp+/yAE8+IPivBczeuakHc5r3y648p/FzUJkjeu7Gx0K4LOjLxQRAiNELRx1mTcGW28Yd9pmas4AsKCxRpT0FqT4BOWZDjhpBsq9lnZeIrZVNesbr1HV56pxGYTbBywohQ31kV5bGjUXTZlqlpJk9AS03epW0ru9w0IF4ND8av8xtumVNfa1p3XHL/seJ9KXQFfZlCGrD5egv5+FteCGLgCO3TeUmuWPIIdH0x8cTRfKqPW9smWxjTCbKCuMINYhX5KZqAcnzWIg20qXxDjm/lHEL0JSFN96C1mCOrQKuFLpAdjPyfbAeD898izmFuK4o9Rq8tedyLcXbTOkt9xtvtacs/UX7VJt0DKf7i4Gjx8tiM49Z1DoEHfWY46kgXcaIp3f5UG9GOncP7paVJE21NGXJtEK0UUdePX2j44ZupPav7hNS1IthxpxrUW7EE+J88cqEip1RyU84Si2wIx8DLBv8CLd1VXJsVCfoI+BsEYi6C4tel7X5cV3DSQElhdm45n4g1Ih/a70ysKXdj98EYVPJW+x6hHVsrN00CBMCMN/HFxwB6hBGzG8pl9zY4qwRveeaf6pvJKQO1nqxE1mA4HVbTajoooZ2EUS1nX/Z7a27uOoAdzCyQh8bmQjG2xI1wrNRYrGW52lirU44TtKkQ2s7mwkhf5PdOsVZtmZn5Sp4OHpncDngC487mMYQ4KHklg9XTdWliYSQR2+ZB0RbMdPtQAxlci1rLKo3s1h1GYMe+zlZxTerA33pc0+LDSfA/rqg+Xkdm3YVXhOo/wgGYeuTVb2N+RSKvz3G7YHQdc/eFUrooXHCDAg141NDm1YSLdhN0JKqr+oiVD3mN1Is+fT/MHkSaRHtNWzwS3UMeOrBKB/iv+jkhLZPBIodqYbIMiYsM7Svfub1BcNLGzI68arGQp2UdPsocmv+h3thi+dXoRvFJVH9gIbTTMqMljrkpc13tXFpWdauCyyCKseNg5uCKU18QjPWgi2GYfbO/YboMXF/b8luddZNBaMjVRBBwuOjhtNHcBgtq7HgQRgt5qfZEkJkEp+o8bi5kPkwiHqosqoDaYwwq+zFVjsSfX2iFFwD4GiW8wfIaF698LS7+8Jsh5mKHD3z5yQ/j6vOfAOgQ8nAMgOVpMMbFK16D6Q1/wnF3yPwh2wFAB1z/9hfw4H98ALi8NMW3biKjE+O0i6p1XBqreUP0xruVf6s7+QJrG26ZwbHTcrAAXBPimKRbEVrLIAO1JSfQ+PoaF9/2nXjJD/4ocDic1PPXfvKf4upzHwcudP0GFHh9jcMffD1+9w//g+PbDCeky4//Mh4+8yHw5aXfJ2tsV9EXIkdH18J5vmnc3a6fLDN1TFSKRxa+0dISe5/piSL3+Z82dHIrqUY3bgzPWf8avlAkFuHmBNig0YWiFG2OVzXAB1FdZzIDaxEJ9tzJ6OMGqyHoL/R7XJyR6vYAjCft02+AMW73/fQEVVn/uU3XZUujXyyD6AULumFi/3eJK3OsyMpLDkN0w8iUP9EsBddVf63BztXdhesxIPCHZaUMrKWzCA8LaVgyT8NGb546sNV5FwsvwxVdH5dDrt2D+BUFtjwUbH7B3dXQDOdzUYuap6heITAcqfjlN9vmqDIPzu9zyBA1csxjYd5vKkiqnWo3jdOfp42fNwO2BGI5xFkpk6BsBuct8QRV5TaeiC1fqS39igkL1CmhPk7bwIK5tj+npgFZSCAsB/g/L03BhCmorubUri43dcuty+e6cc6eNL/+lPHSaX8+ZEPR86Fyx+9zBrz8FiDzoGWR1Vv7pmbJXanlTwPBwfjbcLI3fq7RfART8nmszrG14pBTgh6sCFekbdPdujWPTL2hI89pnMZJc1d3yJ1I2BXYXL3FuzbS39GJBf/8KX+d/OZpxLqFwC7jDybgVpT9Nmr1UC6oJJiCLzvduCGSO7uwDkt58iqvv/24zW4VO2BzoivJC44Cxh7/i8DwU1O0qBbzuYzz9hbJqCyMUBga41k6L0VzUgpF9MnOTlFuRdnkZZNIi9z5bU4pvLNuGxpVrj7S1ooQoVcOE9CJ4gdYbVsxtzJz0qc9jnOHS3NC8ncjqWnpbkaHFUnfvIssxSvV2nIa163AocDGFj7ii3+TjRRG63+sUWjNEneglQmpvs5y1mQ1G9f0vQh0iSTxzDM5t6sSGHcJqyCxUjQKGTBZTdor3TS5jjnFqrzUtC7G19Va5D70txvPJDiOOYy12Yuw6eBrIxFQrPSdRYDrceZCp9xDjuqX/vAsq9H6lG1Opi02MrNqvhyjbRG3q24TpD5bWg6HtOg7F44rCIgx00UprijDbfAhGBMhQTdE3S2v9ua6QJFRogwQTXVjO7RlgAB4A8vqxsjG+TWeRje697+eRW54+C/DSGnByG/RPeF4e/UEiznwqLG55CI3od9LgLhmNN8N24ZURo7WN01nuRDajFGLCbg+8Y7b+fKtFPLKFuPZUh/8jwGym/RNwHb/kIIBGoulinVKoU14GtSd7ENZaHWu14MdcaS3VOK2tixci4wpzHj43/4dLp/5ALbLmeO63iRcP/s/gcNht0+7oBx8dQV+8LXjsca90nQ4zBcrd/o9e1wWpcC5VUXU+1tj1TbGu+X5lwYWdSf4V0GuRfWB4Ay2iffWe2aFkgnzBnb16x8Bf/LDCoVYmiOXGZLXF1gPcQd9aFrWrMMBV5/8CO6/52/ktAbBCoFw9YXPOmd5Ny2VL6gI49yXvPibQxW4GFPfpO8S2i3QpLRckOff1jiLQXoe1gkkfNijZ35HO8VBGTueA4hWXHkT3uCAOob2j1YkhByLLWwARLj+6hdx/eXfdPsJA9mRL3RA69WdMu/0tFrTpdlw3nKsdcO+e66Fvyh7rgVDXXq3vt3qz+YgGD5x6dah/j30YffmvbrLatpo1Y2vGxWBGRtpTDHodG9+aI9o2Xs0def1FFih2rL45N0CmqAtJhGNW9ARfZ4FJspft9m3BZzzZHKqJlyLOjSfl0S48nj8vwL9wwBQC7yvKbPrMDeoS/Ub9TPk2T161Bo5YmGV5ui+tkDnS5pvIU+40UYxXh8ma/JPfy7K36UrBE+UGb8RkPulegBSlvLBH4VPrg+zYRAwl0z5mLYlb6Ex78NObGgGKwB/VnljF2tbmvCzCfJsp/iQN2kX/RBUtQMxXwEkFnLrTAdnUJelZaasML8lLKLyskDP1Atgk3wXaQh0Gguj5GdzXMZdDHlTjy/i/bkw3YV/qZuntnDnp1QpC13XFqWThBYgdZA8cq5Lnw8r422yvotdcbHvtpmWZKISgV99rvYrOclFHZ2Ay+ujsSD7KQDpX6S0yFmsrHYEmaXViMscXYUMgG+B4165xmoMB7/Vt4FCRZvXbsftUUFii0bkGqWhZdnmyrKNHafzpkZkD6B1VoQjNxQtgc++/TbtvfZeRJEZ1uhuke7Aaf0+paUqNk7i5Ne1B46ixUdCyrk1Dvt7wXOlP4tAC3eRh5ukOsbolDsWphle14vZi/H3xwNTR42PWmGErWKh3XyXLcDRAtXwS+sR+o92atpjlsR592CKfvuB7zwg7qSrvAi4rW2b5vnKfVpRp9xcILMoRTnJU+GZuZtyTr+tu8O0j1muPDO/qgXfzIjdLitIkjFZCgOrhibzhdY30fvcl3k8K2GBZUghp4VLowq5QRLOqu5f3UKp8tzxKr60kKZx76IKUIeegu+c2YrWd0s0NfllcjzS/+JgjTx4g+3eIt4N+IRfAKSLKumT92pWb8GQL3CnJKvyQz75c2eElhG9RFpq6YpA+UAKbnJuIStjBJS93EbddGLDMRSDj9oL4Bm3nZweWvhUCJPFnmv6trrRpsm5Exk6yw9kD6q1di2a+G7EvCU42x6sGnsFlesI2W+cvPoVVBbirBEDPOb14ajAHCkkzDODEqddGS4bNJpnxMd3R9i3JanGFfIsZC9oV8xjZB+uFp1INmGQvzlnzjMbEKLBefail797guF+gyCbHNpAZlcbRXWvr4931ApOI8CF9UJ0R7ihFbSV0QLqaz9SP3aeUxj8Q+rUvbo6/RyyOw+BW9KuL+auA+2IOZBGWvDEJcL9eroRnqY2s/3UsEYsCNXACRdPvgmHl38b9Hlct1rrHbUYCajK5HzSwyE9U41ARo2DrzG99o/i3PeSbG5CIxBTgpLPXTfAzdGcRcYmXWDwb4ZzmbWW6Puluq71hKFXnq7PAA6Ee2/9Qdx70/cPgvuNlujGgjvGeSPq03LzjPDlMcK4C3pi8LxiKlNUaLvLqY+FyucFQ90jcOOo4kHw0Mbx/6ddrHyX5jR65M0dMZvqAHP5/lp49LEMbmmVeOHj5js3EYHeAfIaPciZkgjt7YbU36Bpnv82etB9R2zInnUlVy6Ek7dq6kHDi50zAnqXfHQGoOu26ozkSXRD0kZNn/Uu1WlRWh1lkyizRCDX3bAaB/aT80nZya/XiXxjJnRYleyGpXAUtRfLXWqlHcB/L5Cy9Vec2zbnfPi4R8sk+s1WlQrEOCnjdjpDVL16lo79GxN3aVdad74cJkdy0TH77nkOBdon7mXkvky50ObQBDp1h/r9vf2t36Xp9cPVuEu3mMjMGwlXcrb6PQUS7wdU9WrXIvmWL8QqjJ3xXL0fR3oaMrENoP9ttbt0QvJ2LllOvNnFbymzxL0QD+2xAgcmHuvw1PAr4vfJbiS0dd2YKTfHL+/SmAj9eKJ39gDq9VenrtnK1e5gsilD8b0KQWPbg14U2tSUAvEYNyGTIO4unSVFgVhnzp3dsBXyGjVLsr0uMrqvtzfO49oD4GgLbcAR9cyerlk0tbvaXWm+S6el7vmEZl77XgVb36/nTfSxnvu5qK0fGp/MmXkwtfO8gDAnPLRRQi93wnsLqZrbIHFdJBRaRv0GtuM2qvO44/8cX6PyS5Phx+ZCHwvcw7w7t+HGaVE23H81HlE527YDLEjFRsFJPkmfzGOsFwhZh9vqxphgBuZ7uAJtG+JYtESuJmgVfbj174T2HKnYxSw1LcuvNrGoT6QwTOsjn3pl07S82Kjrn3L2IA7EHIKxqdsYG3SPRdoPWt+lGyT7xkNXq7JVVPJgVnE0sefu+cpwuR+XCc4XZqqNBSC/0bEj8OFlIDJ/YOj6ctrVp3/leE1ncqA6hvI6TMvGVrWzb21Z3JKKsyM37JMOuPz8Z44H0oVw9NyD9GxDFoSFPDOK0GwGj/TR//vLv59DQkr4w9uWRa2lF4c8K6cG5zNquKazPCrnoSMFncM4e8FmBg9ltAV0sNGAjOCl0nF8K2s77/gJ4ppCm30adyUggNmau6zVYlmPNVrwQQT7ElgAzOVzISFD3rLaKK4YDtinj4FrDuqPSLeqLfrwQsptr5lVW3ldVucxCMM1QQPg7vcZv2kxVzVvZA5v3w7XE7DUILIt3s62GA5XcQQX870iPsn8o4NoFpsQDJ7cK3Jkx6Qrjvu/icC75HBWLjchsztjGGV2YMZ6ngnOGJUfvxzlztMIqyHz6lFUT/JnH44qlFvvpNd6AqHNkyGvruen0UXI3bdtvtmRvcVdnESlUMWfvCMm6uZ3lHmMECsvcvxdobW6pqFRQgy6EWFvvFEWavzT9+cXC9bb1w/orHYUt6/qKIOXugebenY9FGdXN5qLhjbvuW3H/02iTnl+IHv/vaqblHOI7n62KZrchc5E1gbbiiRRNKmqtbWpQBXHQhsqAmdc4vscaCoBtQUrXZmknouze23HvDlVaA3SNCweQHx1J3eY8w2rmAH55kJhAptlxzG4roFvCaQyXkNU8ttPUhqsCPqCnz0TD/s2NO2oW5fxlUrCg0xoOYItc6ENfFoz1qk78ErT5IRl9eWfvkanuC1OmwySw5SIcWXkXy2oRIjcuyTyic0DG6uhFkext5hlv/3Loz3+YbfQxlVtmanUKumKo5CTp3w+lbJBeIyuAqkWXrinrh5jN9DzSjjXiwZ13Wi8c8NMFQwl+VwJT+r69ayjba7fln86bN5jZmf5GMZ1BDRgqC7TC4oGrDBqLkx5ENfSZEs57izGLVATweIJmx8bfb0AJr/tMuNJ5voUfJkFMnXzBFRHbUujK/jf8iUQR5sLQL2iduehqfW03x0cU8+0ES8ckJp2jxlNEQAJlxk59YUvX4VCaBvBaXpFVGVgK/dFWTkZypGD9aoOqeZvfD3WNjZ5k8YShnMkEAgYJ9GD+M0HS7Rvkhz4zvFzdwutaobMg5U+9sa2PKlhK3KfEjfoW4EJHdCfpgV9+oKxwfvwysAw5R5I5MYVSIueue6LCoijoQ6Lyvi4oUNegOd6Qs2qcwBA55BGM0r2XIsGTjuYMss8t+Jm6rfFGOO0YaC38KelaUctTXRUI1RdpMFqbHv5WV4G4vNp89d990NNFCUtIY6nHOsqNyB2p9MiQszLyjVHxkHqCC1gX3HKTagp5wcbQ2c0quGgjy7y4tPn+5sNVcub67M/EGsIbYbccPEmrxqb2hgaJq1WmGMwaz6J6lcOGb86zCl/q12fouxp6IEtVwcqETmVY2jLxNF/2LcJ5FrCV1nCqF46/nQxD3UDyxe0PY5LKoF8nKxyVsENjyYmGxOzrY/HPgws51OCTggl2voizlyjAznUrgWH9c3ECkrZ/BH3MeQTl+V810dqMQr63xk8V0Lb8qWbFplFrVWYPfdoyZqi7+JWK3muk6yYGBskZBdLSB+R1ORsfbMX6JDXXs8L8nxGiz4EC1kILSGyQDQWEO2J7PaNidLnrmOF7ZmPyMzNBOZzmY/+QSDJk5AfXiOcNAZ9BZNlitfRkX6KyngEGU3WInDUIONc+99bMFqWI0EuNa3jV4lyhTZq0rf8RSyyKQBffSsieJlZL6ctN7glp73RJdz5jpgZm2SUn7hWNpNfwB/sYodjE4pQW0jhc828yYtRioLeWeVxVS5gpis3DaHtuxZrPrnCk9FYmeDdmwu++6ELcTZnSXtmzqtxNhds6ztnq+4TkFcHPTArO9aCTp4RoIYJjWErlEwvv7JZCm3fhJ7yNaPR+sjhnCq0Td64b+uqPjIoUJaLrxSrNi+GPAWHeW5n1/zUvJilliXE45sZBZscmZ5BZbauEtp8oR2H38CCozG69TzzcQKEOAiFBphLt2Qpy8VWrJM3uC7U1YT52Gb/0muLg2aEqd7amtyCutNU024d6zxPM7hCq3SjcRGqcxGDUJg+GVrJu7FCuo3rCN24sMpjjWpREUax7sB5nibrfUdh4V8PspJCRuDsfbkCg03RA5E/fibW1A9YO0UNtiPH4v2i7sDCa5fqk/2CXqlVXIE3UuyD6F0s08uMg5UxK/Wlxdgq2nz68jew4/qrkst5T2E9r6gbz/gLMuPdklIfl9bBe5VjgaegvYh5MvKsUgqz1ZZB9W398FwoVo05093Zxo2cOmvm/brj0x6PEJzUyi3DtqC6vno8F6JMIbTZ+GXbGw+mkBO7fLIof5+JryPsnFF7gpX0CtSkzxijVf16xmmsX/qMQQDY0bauJjMNBYEyHNOdIw/bcDsBZ8RTtMous+YfawxNVMzQxcy3iQiYRyFWKOolgyZHcHwm135fLwj1eOcFVAvwn/KzCnR8gXVHnJvfHCXYAz/2gj5/jHtchAUrOmrcoYAcfRKEEYy2zBkcEqbA9wgLVfFcZ/cmEj7ONCZKoS3fesC2kE2T7YVAkdKOK0D6tL4q9WmOg8Rc0+blghT5tY2+R5dLfnWnq2nZtFnQLOsujNJASSsgdH23bWcnrdsx8UG5BVLysu3Au0KbT05nYlcCFF/aboUl1H9Gti677WXta6ssEIohw7YpFRwN53GdYCAf2M39XCHwHQCeDb4784EonZwZElu/t5WOTQrEqD2daFk9oKDZtgUy/ZS3dCMO/jJ+jrQRa7e2hx4ZXobnaROBN9LvoC/OopoAXIIxpUFHAN7XsEdnhUZ5eRnuV5y1ZeWLBeEIY7jnzCl7U6Gdf0eQV5ZPQwGLzHTgPFosoC+0wYLSC/r4sJYd1/oot1NSd1y0zpxdHgA8d2w0GlhsCuRfEewBP18xz/axMZ7nNvyF0lssIRkZ1rk8iwPFRHkPYwj4NxLlOiGGaIuMpH0kmtzPQNiWdAcJYrFxxr+YfkWjJc+Mfa3/3IEZv7H3vqqRp2SYpDpZH6uJZDhCZ/sdEQv5wFtQjosQuiCUCy2ixewRa8/ECkZ59A35yv1xaLJjI+6V8zjLoULab/lyTZto25U3azmphvM3K75wAOgTLnFqtbsKAN69UORU2Lcw9MAic5lpDO4gDxFyACzXTTngm9Vmo5lmh3chjWpij7zMLchSrwerWeHpnG3YFnzVR65pBzZIMczmzX1s6Pu1A4APA7jKGHXUeYGab0SstbZUddXV+sb7TBi/CV3CVEcTr9nDMy4HFPRBQYaD8Rr/rYRAywJaWQhjTJXQulrSKZng7Azz6v92FLYKonn1u/2YiHEF4EMHAn4JwG/FBEUaj+zvYWLW1Q2ludgbvhRanmdQQyB9rBEZzVxKR3O8ngVK3YN1/N7mRwO6YjC5k0/HxZ5gkQwCU+MMiWMhfZ82Qh4CFyT2V49VmRqLkoCjrP7SAUS/CtAvr4GOE+XJoLow0yxXysznAJ9T7c0Vwy+kN7QFZYzjufmhORY/EvPo+EpyCVKtTVCc7E+w4HliKa0f1W3MWRQPsF6w6U3jqBfGrjw3JvgoM545gPE8mN8LxoNocLnQFnDH8ssPxByKOyvWlouRh5hWGv72t+0bWnChL1MWHP1sa1pd2qOPwhKu41j4rzov89tn14yzulkfFca9WYFLAD9NRM8fZkjlaRx9XctQn0lu3qKfdc3+5XBd2CbUZrLkslgCwdmHta59SH5WsFU4aVEZO7ZxA6TCacfnDApe1fEie5lVBn9LeQ5cpnL8s1xwVVfw5KMAfh4ADnOlLzHwrxi4bwfgMckhJAs4TIphoRwHdXwj2d8wAznjN01U+F8O43P8sjM2OGX8Z8ZUG9dH9rm4dey2nfuQIX3JKa8URKuEFqXAj4/vM/AeBr5IBByeeOrTy9OfAei9pR82dqCYZyanEgrIumpkoXlaJmVDAJzJJfU7G0fEdAfyEmPL2ks1rWcJrCJwr6hqjaFvQTxNLV/0TXiX+bWVe9Dj3VjwPwH4WQD41v/8i8dL776ObwJA9wH8GAMfy5kcd+r6X1FafdJIKGKNuQWLlfn1gg2s4L2ll1RTW6Qr+mRnoTi02LFFlksHQImpToSdh/pwSgu+WKwVRojduMT0MXCogNY4a1NTLNr6GDP+CQH3n3v+nnz61Xd8+/LrrQB+AqDXpIxyfKN1chvf/PUu+ognCCzQgNY7Xr7QRs90v8fxlJp2HrbPF8GTpY9OkGQ02UlCa8txUVeMKhfaekE6/cS8dy4hWX9+lhnvPBwOH7i6usSrn34fgOGa0Sf+7afAIDz+4PB+AO8C8GwdcMgVqwjjsG6FZdp8oQp9sY2F9hSIZuxlG5ERMvL4IngyttLBMmMPKaT3JkJr8yIUB0i36Dlvk5FAlcbUrQv/WQb+5u/7PS/9wNX11Sq07ii+8hdfh/uPXuKx56e3APhxAG/0iNv9JUqhRU6AnsLgD4rRQfQ7lzEaUNG3LKiBmuAyDB+yCgPUQlv6Vxu5ykJaH8WbsC7ZcqG75QqtHZuwSJlPvwVa5PLeH8PHGHj3l164fv+3PHLAq3/+F6PRbekr73gSwBUIh28H8I8A/DkAjw7E2deSuCeM7st/RV0ZOPRMqDTTidlTTOfhSYyoKNaFyEND4LEtqj3tj30QgPSbG87RwsUvzXmJVaBqly4P4lKrJ4X2PoD3MuPHDtPFr189eIhX/8L7vFbj9NW/8B0A+DEQfT8Yf4tB30XAPUFv6LcMmqGCyhqarL7bNvBpgfaiCieoEDyfxtpXX2jr+fkxpJSMzfjgvsaLFufGu+a8MVRn8sulqZV6AOAjAP4lM/8sge6/SmnZYhZk+vKffz2mi4e4up5eTsDbGPgBAN8N4GUAXaSabCX+mFdrS8uA3uZF4NNWgZhiXh1MxYuqDsJk/djS5P2uNLbeGJH5oZuWfIjF8qTBl4WfVSDKuALoSzgK7H8A8PQ1X//2vekeXvFzTyNLpeAu6Ss/9CToEYAv8RgIfwRE3wfG9wJ4EsArADwOfTU/R8zqRNdorvbAp638PqeN3L2ING34HlXabw/Os2UCgZeaPPE3e3i0zA/NfHjB4XZ7m1JUDwH8DhhfANEnAP4QGB8E8AxNh+cvn7/EH3jff0Un/X9D3uNHk45pqgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0xMS0wNlQxMDo1MDo1NSswMTowMKO0v5oAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMTEtMDZUMTA6NTA6NDMrMDE6MDB9kzKCAAAAAElFTkSuQmCC',
"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": cloudSyncEnabled || isCloud ? true : false,
"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 (
{triggers.map((trigger, index) => {
var imageline = trigger.large_image.length === 0 ?
:
const color = trigger.is_valid ? green : yellow
return(
{handleTriggerDrag(e, trigger)}}
onStop={(e) => {
handleDragStop(e)
}}
dragging={false}
position={{
x: 0,
y: 0,
}}
>
{}}>
{imageline}
{trigger.name}
{trigger.description}
)
})}
)
}
var newNodeId = ""
var parsedApp = {}
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{
if (workflow.start === "" || workflow.start === undefined) {
alert.error("Define a starting action first.")
return
}
if (data.is_valid === false) {
alert.error(data.name+" requires hybrid version of Shuffle")
return
}
const triggerLabel = getNextActionName(data.name)
newNodeId = uuid.v4()
const newposition = {
"x": e.pageX-cycontainer.offsetLeft,
"y": e.pageY-cycontainer.offsetTop,
}
const newAppData = {
app_name: data.name,
app_version: "1.0.0",
environment: isCloud ? "cloud" : data.environment,
description: data.description,
long_description: data.long_description,
errors: [],
id_: newNodeId,
_id_: newNodeId,
id: newNodeId,
finished: false,
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)
parsedApp = nodeToBeAdded
return
}
}
}
const handleDragStop = (e, app) => {
console.log("STOP!: ", e)
console.log("APP!: ", parsedApp)
//const onNodeAdded = (event) => {
//const node = event.target
//const nodedata = event.target.data()
var currentnode = cy.getElementById(newNodeId)
if (currentnode === undefined || currentnode === null || currentnode.length === 0) {
return
}
// Using remove & replace, as this triggers the function
// onNodeAdded() with this node after it's added
currentnode.remove()
parsedApp.data.finished = true
parsedApp.data.position = currentnode.renderedPosition()
parsedApp.position = currentnode.renderedPosition()
parsedApp.renderedPosition = currentnode.renderedPosition()
var newAppData = parsedApp.data
if (newAppData.type === "ACTION") {
// AUTHENTICATION
if (app.authentication.required) {
// Setup auth here :)
const authenticationOptions = []
var findAuthId = ""
if (newAppData.authentication_id !== null && newAppData.authentication_id !== undefined && newAppData.authentication_id.length > 0) {
findAuthId = newAppData.authentication_id
}
var tmpAuth = JSON.parse(JSON.stringify(appAuthentication))
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 === app.name) {
authenticationOptions.push(item)
if (item.id === findAuthId) {
newAppData.selectedAuthentication = item
}
}
}
if (authenticationOptions !== undefined && authenticationOptions !== null && authenticationOptions.length > 0) {
for (var key in authenticationOptions) {
const option = authenticationOptions[key]
if (option.active) {
newAppData.selectedAuthentication = option
newAppData.authentication_id = option.id
break
}
}
}
//newAppData.authentication = authenticationOptions
//if (newAppData.selectedAuthentication === null || newAppData.selectedAuthentication === undefined || newAppData.selectedAuthentication.length === "") {
// newAppData.selectedAuthentication = {}
//} else {
// console.log("CAN WE SELECT AUTH?: ", authenticationOptions)
//}
//
//
//console.log(parsedApp)
} else {
newAppData.authentication = []
newAppData.authentication_id = ""
newAppData.selectedAuthentication = {}
}
parsedApp.data = newAppData
cy.add(parsedApp)
} else if (newAppData.type === "TRIGGER") {
cy.add(parsedApp)
}
newNodeId = ""
parsedApp = {}
}
const appScrollStyle = {
overflow: "scroll",
maxHeight: bodyHeight-appBarSize-55-50,
minHeight: bodyHeight-appBarSize-55-50,
marginTop: 1,
overflowY: "auto",
overflowX: "hidden",
}
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) {
console.log("NODEID: ", newNodeId)
if (newNodeId.length > 0) {
var currentnode = cy.getElementById(newNodeId)
if (currentnode === undefined || currentnode === null || currentnode.length === 0) {
return
}
currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft)
currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop)
} else {
console.log("IN NEW NODE!")
if (workflow.public) {
console.log("workflow is public - not adding")
return
}
console.log("IN NEW NODE2!")
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
}
console.log("IN NEW NODE3!")
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
}
const parsedEnvironments = environments === null || environments === [] ? "cloud" : environments[defaultEnvironmentIndex] === undefined ? "cloud" : environments[defaultEnvironmentIndex].Name
const newAppData = {
app_name: app.name,
app_version: app.app_version,
app_id: app.id,
sharing: app.sharing,
private_id: app.private_id,
environment: parsedEnvironments,
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] : "",
authentication_id: "",
finished: false,
}
// 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,
x: e.pageX-cycontainer.offsetLeft,
y: e.pageY-cycontainer.offsetTop,
}
}
console.log("IN NEW NODE4: !", nodeToBeAdded)
parsedApp = nodeToBeAdded
cy.add(nodeToBeAdded)
return
}
}
}
const AppView = (props) => {
const { allApps, prioritizedApps, filteredApps } = props;
const [visibleApps, setVisibleApps] = React.useState(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id))))
const ParsedAppPaper = (props) => {
const app = props.app
const [hover, setHover] = React.useState(false)
const maxlen = 24
var newAppname = app.name
newAppname = newAppname.charAt(0).toUpperCase()+newAppname.substring(1)
if (newAppname.length > maxlen) {
newAppname = newAppname.slice(0, maxlen)+".."
}
newAppname = newAppname.replaceAll("_", " ")
//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 ${yellow}`
return (
{handleAppDrag(e, app)}}
onStop={(e) => {
handleDragStop(e, app)
}}
key={app.id}
dragging={false}
position={{
x: 0,
y: 0,
}}
>
{setHover(true)}} onMouseOut={() => {setHover(false)}}>
{newAppname}
Version: {app.app_version}
{app.description}
)
}
const runSearch = (value) => {
if (value.length > 0) {
const newApps = allApps.filter(app => (app.name.toLowerCase().includes(value.trim().toLowerCase() || app.description.toLowerCase().includes(value.trim().toLowerCase()))) && !(!app.activated && app.generated))
//setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated)))
//console.log("FOUND: ", newApps)
setVisibleApps(newApps)
} else {
setVisibleApps(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id))))
}
}
return(
)
}}
fullWidth
color="primary"
placeholder={"Search Active Apps"}
id="appsearch"
onKeyPress={(event) => {
if (event.key === "Enter") {
event.target.blur(event)
}
}}
onBlur={(event) => {
console.log("BLUR: ", event.target.value)
runSearch(event.target.value)
}}
/>
{visibleApps.length > 0 ?
{visibleApps.map((app, index) => {
if (app.invalid) {
return null
}
return(
)
})}
:
apps.length > 0 ?
Couldn't find app. Is it active?
:
Loading apps
}
)
}
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)
if (newaction === undefined || newaction === null) {
alert.error("Failed to find the action")
return
}
// Does this one find the wrong one?
selectedAction.name = newaction.name
selectedAction.parameters = JSON.parse(JSON.stringify(newaction.parameters))
selectedAction.errors = []
selectedAction.isValid = true
selectedAction.is_valid = true
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)
setUpdate(Math.random())
// FIXME - should change icon-node (descriptor) as well
const allNodes = cy.nodes().jsons()
for (var key in allNodes) {
const currentNode = allNodes[key]
if (currentNode.data.attachedTo === selectedAction.id && currentNode.data.isDescriptor) {
const foundnode = cy.getElementById(currentNode.data.id)
if (foundnode !== null && foundnode !== undefined) {
const iconInfo = GetIconInfo(newaction)
const svg_pin = ` `
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
foundnode.data("image", svgpin_Url)
foundnode.data("imageColor", iconInfo.iconBackgroundColor)
}
break
}
}
}
// APPSELECT at top
// appname & version
// description
// ACTION select
//
const selectedNameChange = (event) => {
//console.log("OLDNAME: ", selectedAction.name)
event.target.value = event.target.value.replaceAll("(", "")
event.target.value = event.target.value.replaceAll(")", "")
event.target.value = event.target.value.replaceAll("$", "")
event.target.value = event.target.value.replaceAll("#", "")
event.target.value = event.target.value.replaceAll(".", "")
event.target.value = event.target.value.replaceAll(",", "")
event.target.value = event.target.value.replaceAll(" ", "_")
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)
}
// Starts on current node and climbs UP the tree to the root object.
// Sends back everything in it's path
const getParents = (action) => {
var allkeys = [action.id]
var handled = []
var results = []
// maxiter = max amount of parent nodes to loop
// also handles breaks if there are issues
var iterations = 0
var maxiter = 10
while(true) {
for (var key in allkeys) {
var currentnode = cy.getElementById(allkeys[key])
if (currentnode === undefined || currentnode === null) {
continue
}
if (currentnode.data() === undefined) {
//console.log("Node doesn't have any data (getParents)! Probably a trigger.")
handled.push(allkeys[key])
results.push({"id": allkeys[key], "type": "TRIGGER"})
} else {
if (handled.includes(currentnode.data().id)) {
continue
} else {
handled.push(currentnode.data().id)
results.push(currentnode.data())
}
}
// Get the name / label here too?
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 || iterations === maxiter) {
break
}
iterations += 1
}
// 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}
//height: "100%",
const appApiViewStyle = {
display: "flex",
flexDirection: "column",
backgroundColor: "#1F2023",
color: "white",
paddingRight: 15,
paddingLeft: 15,
minHeight: "100%",
zIndex: 1000,
resize: "vertical",
overflow: "auto",
}
var rightsidebarStyle = {
position: "fixed",
right: 0,
top: appBarSize+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 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 (:
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 (:
// 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 =
Use "Shuffle Tools" app with "Filter List" action to handle loops
: 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 (
{datafield}
{actionlist.length === 0 ? null :
Autocomplete
{
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" ? : data.type === "workflow_variable" || data.type === "execution_variable" ? :
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 (
{
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)
}
}}>
{icon} {data.name}
)
})}
}
)
}
const menuItemStyle = {
color: "white",
backgroundColor: inputColor
}
const conditionsModal =
{
setConditionsModalOpen(false)
setSourceValue({})
setConditionValue({})
setDestinationValue({})
}}
>
Conditions can't be used for loops [ .# ] Learn more
Condition
{
conditionValue.configuration = !conditionValue.configuration
setConditionValue(conditionValue)
setUpdate(Math.random())
}}>
{conditionValue.configuration ? "!" : "="}
{setVariableAnchorEl(e.currentTarget)}}>
{conditionValue.value}
{
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
}
}
var label = ""
if (selectedEdge.conditions.length === 1) {
label = selectedEdge.conditions.length+" condition"
} else if (selectedEdge.conditions.length > 1) {
label = selectedEdge.conditions.length+" conditions"
}
var currentedge = cy.getElementById(selectedEdge.id)
if (currentedge !== undefined && currentedge !== null) {
currentedge.data().label = label
}
setSelectedEdge(selectedEdge)
workflow.branches[selectedEdgeIndex] = selectedEdge
setWorkflow(workflow)
}} color="primary">
Submit
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 (
{}}>
{
setSourceValue(condition.source)
setConditionValue(condition.condition)
setDestinationValue(condition.destination)
setConditionsModalOpen(true)
}}>
{condition.source.value}
{}}>
{condition.condition.value}
{condition.destination.value}
)
}
var injectedData =
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(
Conditions
{injectedData}
{
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
)
}
// 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+"/api/v1/triggers/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) => {
if (responseJson !== null && responseJson.success !== false) {
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/outlook/"+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 =
{
const redirectUri = isCloud ? "https%3A%2F%2Fshuffler.io%2Fapi%2Fv1%2Ftriggers%2Foutlook%2Fregister" : "http%3A%2F%2Flocalhost:5001%2Fapi%2Fv1%2Ftriggers%2Foutlook%2Fregister"
//const redirectUri = isCloud ? "http%3A%2F%2Flocalhost:5002%2Fapi%2Fv1%2Ftriggers%2Foutlook%2Fregister" : "http%3A%2F%2Flocalhost:5001%2Fapi%2Fv1%2Ftriggers%2Foutlook%2Fregister"
const client_id = "fd55c175-aa30-4fa6-b303-09a29fb3f750"
const username = isCloud ? userdata.username : userdata.id
console.log(redirectUri)
console.log("USER: ", username, userdata)
const branch = workflow.branches.find(branch => branch.source_id === selectedTrigger.id)
if (branch === undefined || branch === null) {
alert.error("No startnode connected to node. Connect it to an action.")
return
}
console.log("BRANCH: ", branch)
const startnode = branch.destination_id
const url = "https://login.microsoftonline.com/common/oauth2/authorize?client_id="+client_id+"&redirect_uri="+redirectUri+"&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%3D"+username+"%26type%3Doutlook%26start%3d"+startnode
console.log("URL: ", url)
var newwin = window.open(url, "", "width=200,height=100")
// Check whether we got a callback somewhere
var id = setInterval(function () {
fetch(globalUrl+"/api/v1/triggers/outlook/"+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)
saveWorkflow(workflow)
}} >
Microsoft Outlook
// 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 =
{selectedTrigger.status === "running" ? null :
{outlookButton}
}
{triggerFolders === undefined || triggerFolders === null ?
null :
{
//setTriggerFolderWrapper(e)
setTriggerFolderWrapperMulti(e)
}}
fullWidth
input={ }
key={selectedTrigger}
>
{triggerFolders.map(folder => {
//console.log("FOLDER: ", folder)
//var folderItem =
var folderItem =
{folder.displayName}
if (folder.childFolderCount > 0) {
// Here to handle subfolders sometime later
folderItem =
{folder.displayName}
}
return folderItem
})}
}
} else if (triggerAuthentication.type === "gmail") {
triggerInfo = "SPECIAL GMAIL"
}
}
// Check
const argumentView = Object.getOwnPropertyNames(triggerAuthentication).length > 0 ?
{triggerInfo}
:
{outlookButton}
{/*
{
alert.error("REMOVE THIS FIELD FOR GMAIL - make it oAuth something")
}} color="primary">
Gmail
*/}
return(
Name
Environment:
{argumentView}
{
startMailSub(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Start
{
stopMailSub(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Stop
)
}
const SubflowSidebar = () => {
const [menuPosition, setMenuPosition] = useState(null)
const [showDropdown, setShowDropdown] = React.useState(false)
const [showDropdownNumber, setShowDropdownNumber] = React.useState(0)
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
const [actionlist, setActionlist] = React.useState([])
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": ""})
}
}
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)
}
}
//console.log("ACTIONS: ", actionlist)
setActionlist(actionlist)
}
// Shows nested list of nodes > their JSON lists
const ActionlistWrapper = (props) => {
const { data } = 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 = workflow.triggers[selectedTriggerIndex].parameters[1].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
}
}
workflow.triggers[selectedTriggerIndex].parameters[1]["value_replace"] = paramcheck
setSelectedAction(selectedAction)
setUpdate(Math.random())
setShowDropdown(false)
setMenuPosition(null)
return
}
}
//workflow.triggers[selectedTriggerIndex].parameters[1].value = selectedActionParameters[count].value
setSelectedAction(selectedAction)
setUpdate(Math.random())
setShowDropdown(false)
setMenuPosition(null)
}
const iconStyle = {
marginRight: 15,
}
return (
{
handleMenuClose()
}}
open={!!menuPosition}
style={{
border: `2px solid #f85a3e`,
color: "white",
marginTop: 2,
}}
>
{actionlist.map(innerdata => {
const icon = innerdata.type === "action" ? : innerdata.type === "workflow_variable" || innerdata.type === "execution_variable" ? :
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]
if (cy !== undefined) {
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) => {
if (cy !== undefined) {
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 ?
{icon} {innerdata.name}
}
parentMenuOpen={!!menuPosition}
style={{backgroundColor: theme.palette.inputColor, color: "white", minWidth: 250,}}
onClick={() => {
handleItemClick([innerdata])
}}
>
{parsedPaths.map((pathdata, index) => {
// FIXME: Should be recursive in here
const icon = pathdata.type === "value" ? : pathdata.type === "list" ? :
return (
{}}
onClick={() => {
handleItemClick([innerdata, pathdata])
}}
>
{icon} {pathdata.name}
)
})}
:
handleMouseover()} onMouseOut={() => {handleMouseOut()}}
onClick={() => {
handleItemClick([innerdata])
}}
>
{icon} {innerdata.name}
)
})}
)
}
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": ""}
workflow.triggers[selectedTriggerIndex].parameters[4] = {"name": "check_result", "value": "true"}
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(
Name
{
const newvalue = workflow.triggers[selectedTriggerIndex].parameters[4] === undefined || workflow.triggers[selectedTriggerIndex].parameters[4].value === "false" ? "true" : "false"
workflow.triggers[selectedTriggerIndex].parameters[4] = {
"name": "check_result",
"value": newvalue,
}
setWorkflow(workflow)
setUpdate(Math.random())
}}
color="primary"
value="Wait for results"
/>
}
style={{marginTop: 10}}
label={Wait for results
}
/>
Parameters
Select a workflow to execute
{workflows === undefined || workflows === null || workflows.length === 0 ? null :
{
setUpdate(Math.random())
workflow.triggers[selectedTriggerIndex].parameters[0].value = e.target.value.id
setSubworkflowStartnode(e.target.value.start)
// Sets the startnode
if (e.target.value.id !== workflow.id) {
console.log("WORKFLOW: ", e.target.value)
setSubworkflow(e.target.value)
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)
} else {
console.log("WORKFLOW: ", workflow)
setSubworkflow(e.target.value)
}
setWorkflow(workflow)
}}
style={{backgroundColor: inputColor, color: "white", height: 50}}
>
{workflows.map((data, index) => {
if (data.id === workflow.id) {
//return null
data = workflow
}
return (
{data.name}
)
})}
}
{workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null :
workflow.triggers[selectedTriggerIndex].parameters[0].value === props.match.params.key ? null :
Explore selected workflow
}
{subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null :
{
setSubworkflowStartnode(e.target.value)
//console.log("WF: ", workflow)
const branchId = uuid.v4()
const newbranch = {
"source_id": workflow.triggers[selectedTriggerIndex].id,
"destination_id": e.target.value.id,
"source": workflow.triggers[selectedTriggerIndex].id,
"target": e.target.value.id,
"has_errors": false,
"id": branchId,
"_id": branchId,
"label": "Subflow",
"decorator": true,
}
if (workflow.visual_branches !== undefined) {
if (workflow.visual_branches === null) {
workflow.visual_branches = [newbranch]
} else if (workflow.visual_branches.length === 0) {
workflow.visual_branches.push(newbranch)
} else {
const foundIndex = workflow.visual_branches.findIndex(branch => branch.source_id === newbranch.source_id)
if (foundIndex !== -1) {
const currentEdge = cy.getElementById(workflow.visual_branches[foundIndex].id)
if (currentEdge !== undefined && currentEdge !== null) {
currentEdge.remove()
}
}
workflow.visual_branches.splice(foundIndex, 1)
workflow.visual_branches.push(newbranch)
}
}
if (workflow.id == subworkflow.id) {
const cybranch = {
group: "edges",
source: newbranch.source_id,
target: newbranch.destination_id,
id: branchId,
data: newbranch,
}
cy.add(cybranch)
}
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: 50}}
>
{subworkflow.actions.map((action, index) => {
//console.log(action)
return (
parent.id === action.id)} key={index} style={{backgroundColor: inputColor, color: "white"}} value={action}>
{action.label}
)
})}
}
{
setMenuPosition({
top: event.pageY+10,
left: event.pageX+10,
})
//setShowDropdownNumber(3)
setShowDropdown(true)
setShowAutocomplete(true)
}}/>
)
}}
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)
}}
/>
{showDropdown ?
: null}
{
workflow.triggers[selectedTriggerIndex].parameters[2].value = e.target.value
setWorkflow(workflow)
}}
/>
)
}
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(
Name
Environment
{
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: 50}}
>
{triggerEnvironments.map(data => {
if (data.archived) {
return null
}
return (
{data}
)
})}
Parameters
{
var copyText = document.getElementById("webhook_uri_field")
if (copyText !== undefined && copyText !== null) {
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")
} else {
console.log("Couldn't find webhook URI field: ", copyText)
}
}}
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)
}}
/>
{
newWebhook(selectedTrigger)
}} color="primary">
Start
{
deleteWebhook(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Stop
)
}
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("Deleting mail 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 = []
if (triggerFolders === null || triggerFolders === undefined) {
return null
}
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(
Name
Environment:
Parameters
{
setTriggerTextInformationWrapper(e.target.value)
}}
/>
{
setTriggerOptionsWrapper("email")
}}
color="primary"
value="email"
/>
}
label={Email
}
/>
{
setTriggerOptionsWrapper("sms")
}}
color="primary"
value="sms"
/>
}
label={SMS
}
/>
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") ?
{
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") ?
{
workflow.triggers[selectedTriggerIndex].parameters[4].value = event.target.value
setWorkflow(workflow)
setUpdate(Math.random())
}}
/>
: null
}
)
}
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": isCloud ? "*/15 * * * *" : "120"}
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "execution_argument", "value": '{"example": {"json": "is cool"}}'}
setWorkflow(workflow)
}
return(
Name
Environment
{
selectedTrigger.environment = e.target.value
setSelectedTrigger(selectedTrigger)
if (e.target.value === "cloud") {
console.log("Set cloud config")
workflow.triggers[selectedTriggerIndex].parameters[0].value = "*/15 * * * *"
//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: 50}}
>
{triggerEnvironments.map(data => {
if (data.archived) {
return null
}
return (
{data}
)
})}
Parameters
{
setTriggerCronWrapper(e.target.value)
}}
/>
{
setTriggerBodyWrapper(e.target.value)
}}
/>
{
submitSchedule(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Start
{
stopSchedule(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Stop
)
}
return null
}
const cytoscapeViewWidths = 800
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+20,
top: appBarSize+20,
/*
minWidth: cytoscapeViewWidths,
maxWidth: cytoscapeViewWidths,
*/
}
const TopCytoscapeBar = (props) => {
return (
Workflows
{workflow.name}
{workflow.public ?
Public Workflow PREVIEW
:
null
}
)
}
const NoActionsBar = () => {
if (workflow.actions.length === 0 || !appAdded) {
return (
)
}
return null
}
const FileMenu = () => {
const [newAnchor, setNewAnchor] = React.useState(null)
const [showShuffleMenu, setShowShuffleMenu] = React.useState(false)
{ /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */}
return (
}
control={
{
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)
}} />
}
/>
{
setShowShuffleMenu(!showShuffleMenu)
setNewAnchor(event.currentTarget)
}}>
)
}
const WorkflowMenu = () => {
const [newAnchor, setNewAnchor] = React.useState(null)
const [showShuffleMenu, setShowShuffleMenu] = React.useState(false)
{ /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */}
return (
}
control={
{
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)
}} />
}
/>
{
setShowShuffleMenu(!showShuffleMenu)
setNewAnchor(event.currentTarget)
}}>
)
}
const handleHistoryUndo = () => {
console.log("history: ", history, "index: ", historyIndex)
var item = history[historyIndex-1]
if (historyIndex === 0) {
item = history[historyIndex]
}
if (item === undefined) {
console.log("Couldn't find the action you're looking for")
return
}
console.log("HANDLE: ", item)
if (item.type === "node" && item.action === "removed") {
// Re-add the node
cy.add({
group: 'nodes',
data: item.data,
position: item.data.position,
})
} else if (item.action === "added") {
console.log("Should remove item!")
const currentitem = cy.getElementById(item.data.id)
if (currentitem !== undefined && currentitem !== null) {
currentitem.remove()
}
}
if (historyIndex > 0) {
setHistoryIndex(historyIndex-1)
}
}
const BottomCytoscapeBar = () => {
const [anchorEl, setAnchorEl] = React.useState(null)
const boxSize = 100
const executionButton = executionRunning ?
{
abortExecution()
}}>
:
{
executeWorkflow(executionText, workflow.start)
}}>
return(
{executionButton}
{workflow.public ? null :
{
setExecutionText(e.target.value)
}}
/>
}
saveWorkflow()}>
{savingState === 2 ? : savingState === 1 ? : }
{workflow.public ?
{
//setExecutionModalOpen(true)
//getWorkflowExecution(props.match.params.key, "")
//data = sanitizeWorkflow(workflow)
const data = workflow
let exportFileDefaultName = data.name+'.json';
let dataStr = JSON.stringify(data)
let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)
let linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click()
}}>
: null}
cy.fit(null, 50)}>
{
handleHistoryUndo(history)
}}>
{
removeNode()
}}>
{
setExecutionModalOpen(true)
getWorkflowExecution(props.match.params.key, "")
}}>
{/*
*/}
{workflow.configuration !== null && workflow.configuration !== undefined && workflow.configuration.exit_on_error !== undefined ?
: null}
)
}
const RightSideBar = (props) => {
const {workflow, setWorkflow, setAction, setSelectedAction, setUpdate, appActionArguments, selectedApp, workflowExecutions, setSelectedResult, selectedAction, setSelectedApp, setSelectedTrigger, setSelectedEdge, setCurrentView, cy, setAuthenticationModalOpen,setVariablesModalOpen, setCodeModalOpen, selectedNameChange, rightsidebarStyle, showEnvironment, selectedActionEnvironment, environments, setNewSelectedAction, appApiViewStyle, globalUrl, setSelectedActionEnvironment, requiresAuthentication, hideExtraTypes, scrollConfig, setScrollConfig } = props
if (!rightSideBarOpen) {
return null
}
if (Object.getOwnPropertyNames(selectedAction).length > 0) {
if (Object.getOwnPropertyNames(selectedAction).length === 0) {
return null
}
return (
)
} else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
if (selectedTrigger.trigger_type === "SCHEDULE") {
//console.log("SCHEDULE")
return(
)
} else if (selectedTrigger.trigger_type === "WEBHOOK") {
return(
)
} else if (selectedTrigger.trigger_type === "SUBFLOW") {
return(
)
} else if (selectedTrigger.trigger_type === "EMAIL") {
return(
)
} else if (selectedTrigger.trigger_type === "USERINPUT") {
return(
)
} 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(
)
}
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
// {
// executeWorkflowWebsocket()
//}}>Execute websocket
//
const leftView = leftViewOpen ?
:
{
setLeftViewOpen(true)
setLeftBarSize(350)
}}>
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 (
{
handleReactJsonClipboard(copy)
}}
onSelect={(select) => {
HandleJsonCopy(showResult, select, "exec")
console.log("SELECTED!: ", select)
}}
name={"Execution Argument"}
/>
)
}
return (
Execution Argument
{executionData.execution_argument}
)
}
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
}
if (execution.execution_source === "webhook") {
return trigger.trigger_type === "WEBHOOK").large_image} style={{width: size, height: size}} />
} else if (execution.execution_source === "outlook") {
return trigger.trigger_type === "EMAIL").large_image} style={{width: size, height: size}} />
} else if (execution.execution_source === "schedule") {
return trigger.trigger_type === "SCHEDULE").large_image} style={{width: size, height: size}} />
} else if (execution.execution_source === "EMAIL") {
return 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 trigger.trigger_type === "SUBFLOW").large_image} style={{width: size, height: size}} />
}
return (
)
}
const handleReactJsonClipboard = (copy) => {
console.log("COPY: ", copy)
const elementName = "copy_element_shuffle"
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
if (copy.namespace !== undefined && copy.name !== undefined && copy.src !== undefined) {
copy = copy.src
}
console.log("NEW: ", copy)
navigator.clipboard.writeText(JSON.stringify(copy))
copyText.select()
copyText.setSelectionRange(0, 99999) /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy")
alert.success("Copied data")
} else {
console.log("Failed to copy from "+elementName+": ", copyText)
}
}
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.replaceAll(" ", "_")
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 =
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 ?
All Executions
{
getWorkflowExecution(props.match.params.key, "")
}} color="primary">
Refresh executions
{workflowExecutions.length > 0 ?
{workflowExecutions.map((data, index) => {
const statusColor = data.status === "FINISHED" ? green : data.status === "ABORTED" || data.status === "FAILED" ? "red" : yellow
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 !== undefined && data.workflow.actions !== null ? data.workflow.actions.length : 0
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 (
{}} onMouseOut={() => {}} onClick={() => {
if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") {
start()
setExecutionRunning(true)
setExecutionRequestStarted(false)
}
const cur_execution = {
"execution_id": data.execution_id,
"authorization": data.authorization,
}
setExecutionRequest(cur_execution)
setExecutionModalView(1)
setExecutionData(data)
handleUpdateResults(data, cur_execution)
}}>
{getExecutionSourceImage(data)}
{timestamp}
{data.workflow.actions !== null ?
{resultsLength}/{calculatedResult}
: null}
{lastExecution === data.execution_id ?
:
}
)
return
})}
:
There are no executions yet
}
:
{
setExecutionRunning(false)
stop()
getWorkflowExecution(props.match.params.key, "")
setExecutionModalView(0)
setLastExecution(executionData.execution_id)
}}>
{}}>
{
}}>
See other Executions
Executing Workflow
{
console.log("DATA: ", executionData)
executeWorkflow(executionData.execution_argument, executionData.start)
setExecutionModalOpen(false)
//executionText, workflow.start)
}}>
{executionData.status === "EXECUTING" ?
{
abortExecution()
}}>
: null}
{executionData.status !== undefined && executionData.status.length > 0 ?
Status
{executionData.status}
: null
}
{executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ?
Source
{executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ?
executionData.execution_source === props.match.params.key ?
{
getWorkflowExecution(props.match.params.key, executionData.execution_parent)
}}>
Parent Execution
:
Parent Workflow
:
executionData.execution_source
}
: null
}
{executionData.started_at !== undefined ?
Started
{new Date(executionData.started_at*1000).toISOString()}
: null
}
{executionData.completed_at !== undefined && executionData.completed_at !== null && executionData.completed_at > 0 ?
Finished
{new Date(executionData.completed_at*1000).toISOString()}
: null
}
{executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ?
parsedExecutionArgument()
: null }
{executionData.results !== undefined && executionData.results !== null && executionData.results.length > 1 && executionData.results.find(result => result.status === "SKIPPED" || result.status === "FAILURE") ?
Show failed / skipped actions }
control={
{setShowSkippedActions(!showSkippedActions)}} />
}
/>
:
null
}
Actions
{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")? : null}
{executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ?
:
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" : yellow
var imgSrc = curapp === undefined ? "" : curapp.large_image
if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) {
// Look for the node in the workflow
const action = workflow.actions.find(action => action.id === data.action.id)
if (action !== undefined && action !== null) {
imgSrc = action.large_image
}
/*
if (imgSrc.length === 0) {
console.log("CHECK IF ITS A
}
*/
}
var actionimg = curapp === null ?
null :
if (triggers.length > 2) {
if (data.action.app_name === "shuffle-subflow") {
actionimg =
}
if (data.action.app_name === "User Input") {
actionimg =
}
}
if (validate.valid && typeof(validate.result) === "string") {
validate.result = JSON.parse(validate.result)
}
return (
{
var currentnode = cy.getElementById(data.action.id)
if (currentnode.length !== 0) {
currentnode.addClass('shuffle-hover-highlight')
}
}} onMouseOut={() => {
var currentnode = cy.getElementById(data.action.id)
if (currentnode.length !== 0) {
currentnode.removeClass('shuffle-hover-highlight')
}
}}>
{
setSelectedResult(data)
setCodeModalOpen(true)
}}>
{actionimg}
{data.action.label}
{data.action.name}
Status
{data.status}
{validate.valid ?
{
handleReactJsonClipboard(copy)
}}
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 ?
{validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ?
data.action.parameters[0].value === props.match.params.key ?
{
getWorkflowExecution(props.match.params.key, validate.result.execution_id)
}}>
See sub-execution
:
{
}}>See subflow execution
:
"TBD: Load subexecution result for"
}
: null
}
:
Result
{data.result}
}
)
})
}
}
// 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" : yellow
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 :
{
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}
>
{
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
}
}
}}>
{
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
}
}
}}>
{
e.preventDefault()
const executionIndex = workflowExecutions.findIndex(data => data.execution_id === selectedResult.execution_id)
if (executionIndex !== -1) {
setExecutionModalOpen(true)
setExecutionModalView(1)
setExecutionData(workflowExecutions[executionIndex])
}
}}>
{
e.preventDefault()
//console.log("CLICKING EXIT")
setCodeModalOpen(false)
}}>
{curapp === null ? null :
}
{selectedResult.action.label}
{selectedResult.action.name}
Status {selectedResult.status}
{validate.valid ?
{
handleReactJsonClipboard(copy)
}}
onSelect={(select) => {
HandleJsonCopy(JSON.stringify(validate.result), select, selectedResult.action.label)
}}
name={"Results for "+selectedResult.action.label}
/>
:
Result
{
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}
}
const newView =
{leftView}
{
// 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)
}}
/>
{executionModal}
/*
:
TMP FOR NOT LOGGED IN
*/
const executionVariableModal = executionVariablesModalOpen ?
{
setNewVariableName("")
setExecutionVariablesModalOpen(false)
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
},
}}
>
Execution Variable
Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more here
setNewVariableName(event.target.value)}
color="primary"
placeholder="Name"
style={{marginTop: 25}}
InputProps={{
style:{
color: "white"
}
}}
margin="dense"
fullWidth
defaultValue={newVariableName}
/>
{
setNewVariableName("")
setExecutionVariablesModalOpen(false)
}} color="primary">
Cancel
{
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
{workflowExecutions.length > 0 ?
Values from last 3 executions
{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 (
{index+1}: {variable.value}
)
})}
: null
}
: null
const variablesModal = variablesModalOpen ?
{
setNewVariableName("")
setNewVariableDescription("")
setNewVariableValue("")
setVariablesModalOpen(false)
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
},
}}
>
Workflow Variable
setNewVariableName(event.target.value)}
color="primary"
placeholder="Name"
InputProps={{
style:{
color: "white"
}
}}
margin="dense"
fullWidth
defaultValue={newVariableName}
/>
setNewVariableDescription(event.target.value)}
color="primary"
placeholder="Description"
margin="dense"
fullWidth
InputProps={{
style:{
color: "white"
}
}}
defaultValue={newVariableDescription}
/>
setNewVariableValue(event.target.value)}
rows="6"
multiline
color="primary"
placeholder="Value"
margin="dense"
InputProps={{
style:{
color: "white"
}
}}
fullWidth
defaultValue={newVariableValue}
/>
{
setNewVariableName("")
setNewVariableDescription("")
setNewVariableValue("")
setVariablesModalOpen(false)
}} color="primary">
Cancel
{
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
: 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
}
// Automatically mapping fields that already exist (predefined).
// Warning if fields are NOT filled
for (var key in selectedApp.authentication.parameters) {
if (authenticationOption.fields[selectedApp.authentication.parameters[key].name].length === 0) {
if (selectedApp.authentication.parameters[key].value !== undefined && selectedApp.authentication.parameters[key].value !== null && selectedApp.authentication.parameters[key].value.length > 0) {
authenticationOption.fields[selectedApp.authentication.parameters[key].name] = selectedApp.authentication.parameters[key].value
} else {
alert.info("Field "+selectedApp.authentication.parameters[key].name+" can't be empty")
return
}
}
}
console.log("Action: ", selectedAction)
selectedAction.authentication_id = authenticationOption.id
selectedAction.selectedAuthentication = authenticationOption
if (selectedAction.authentication === undefined || selectedAction.authentication === null) {
selectedAction.authentication = [authenticationOption]
} else {
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)
//
if (configureWorkflowModalOpen) {
setSelectedAction({})
}
setUpdate(authenticationOption.id)
/*
{selectedAction.authentication.map(data => (
*/
}
if (authenticationOption.label === null || authenticationOption.label === undefined) {
authenticationOption.label = selectedApp.name+" authentication"
}
return (
What is this?
These are required fields for authenticating with {selectedApp.name}
Name - what is this used for?
{
authenticationOption.label = event.target.value
}}
/>
{/*selectedApp.link.length > 0 ?
: null*/}
{selectedApp.authentication.parameters.map((data, index) => {
console.log("AUTH: ", data)
return (
{data.name}
{data.schema !== undefined && data.schema !== null && data.schema.type === "bool" ?
{
authenticationOption.fields[data.name] = e.target.value
}}
style={{backgroundColor: theme.palette.surfaceColor, color: "white", height: 50}}
>
false
true
:
{
authenticationOption.fields[data.name] = event.target.value
}}
/>
}
)
})}
{
setAuthenticationModalOpen(false)
}} color="primary">
Cancel
{
setAuthenticationOptions(authenticationOption)
handleSubmitCheck()
}} color="primary">
Submit
)
}
const EndpointData = () => {
const [tmpVar, setTmpVar] = React.useState("")
return (
The API endpoint to use (URL) - predefined in the app
{
setTmpVar(event.target.value)
}}
onBlur={() => {
selectedApp.link = tmpVar
console.log("LINK: ", selectedApp.link)
setSelectedApp(selectedApp)
}}
/>
)
}
const configureWorkflowModal = configureWorkflowModalOpen && apps.length !== 0 ?
{
//setConfigureWorkflowModalOpen(false)
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: 600,
padding: 50,
},
}}
>
{
setConfigureWorkflowModalOpen(false)
}}>
: null
// This whole part is redundant. Made it part of Arguments instead.
const authenticationModal = authenticationModalOpen ?
{
//setAuthenticationModalOpen(false)
//
if (configureWorkflowModalOpen) {
setSelectedAction({})
}
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: 600,
padding: 15,
},
}}
>
{
setAuthenticationModalOpen(false)
if (configureWorkflowModalOpen) {
setSelectedAction({})
}
}}>
Authentication for {selectedApp.name}
: null
//const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
const loadedCheck = isLoaded && workflowDone ?
{newView}
{variablesModal}
{executionVariableModal}
{conditionsModal}
{authenticationModal}
{codePopoutModal}
{configureWorkflowModal}
:
// Awful way of handling scroll
if (scrollConfig !== undefined && setScrollConfig !== undefined && Object.getOwnPropertyNames(selectedAction).length !== 0) {
//console.log("SET CONFIG: ", scrollConfig)
const rightSideActionView = document.getElementById("rightside_actions")
if (rightSideActionView !== undefined && rightSideActionView !== null) {
//console.log("FOUND RIGHTSIDE: ", rightSideActionView.scrollTop, scrollConfig)
if (scrollConfig.top !== 0 && scrollConfig.top !== undefined && scrollConfig.top !== 0) {
//console.log("SCROLL IS NOT 0: ", scrollConfig.top)
//rightSideActionView.scrollTop = scrollConfig.top
setTimeout(() => {
scroller.scrollTo('elements_wrapper', {
containerId: 'rightside_actions',
offset: scrollConfig.top,
})
if (scrollConfig.selected !== undefined && scrollConfig.selected !== null) {
const selectedField = document.getElementById(scrollConfig.selected)
if (selectedField !== undefined && selectedField !== null) {
selectedField.focus()
//const val = selectedField.value
//console.log("VAL: ", val)
//selectedField.value = ''
//selectedField.value = val
}
}
}, 5)
} else {
//console.log("SCROLL IS 0: ", scrollConfig.top, rightSideActionView.scrollTop)
if (rightSideActionView.scrollTop !== scrollConfig.top) {
setScrollConfig({
top: rightSideActionView.scrollTop,
left: 0,
selected: "",
})
}
}
}
}
return (
)
}
export default AngularWorkflow