7738 lines
286 KiB
React
7738 lines
286 KiB
React
import React, {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 {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';
|
||
|
||
const surfaceColor = "#27292D"
|
||
const inputColor = "#383B40"
|
||
|
||
// http://apps.cytoscape.org/apps/yfileslayoutalgorithms
|
||
cytoscape.use(edgehandles);
|
||
cytoscape.use(clipboard);
|
||
cytoscape.use(undoRedo);
|
||
cytoscape.use( cxtmenu );
|
||
|
||
// https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
|
||
function useWindowSize() {
|
||
const [size, setSize] = useState([0, 0]);
|
||
useLayoutEffect(() => {
|
||
function updateSize() {
|
||
setSize([window.innerWidth, window.innerHeight]);
|
||
}
|
||
window.addEventListener('resize', updateSize);
|
||
updateSize();
|
||
return () => window.removeEventListener('resize', updateSize);
|
||
}, []);
|
||
return size;
|
||
}
|
||
|
||
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/"
|
||
|
||
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 [scrollLocation, setScrollLocation] = React.useState(0)
|
||
|
||
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: 6000,
|
||
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 ?
|
||
<Draggable>
|
||
<div style={{color: "white", position: "fixed", top: appBarSize+65, left: leftBarSize+20, zIndex: 5000, minHeight: 100, padding: 15, maxHeight: 100, maxWidth: 500, overflowX: "hidden",}}>
|
||
{workflowExecutions.slice(0,15).map((data, index) => {
|
||
return (
|
||
<div key={index}>
|
||
{new Date(data.started_at*1000).toISOString()}
|
||
, {data.status}
|
||
{data.result.length > 0 ? ", "+data.result : ", "}
|
||
{data.execution_argument.length > 0 ? ", "+data.execution_argument : ", "}
|
||
<Divider style={{backgroundColor: "white"}}/>
|
||
</div>
|
||
)
|
||
return
|
||
})}
|
||
</div>
|
||
</Draggable>
|
||
: null
|
||
|
||
const fetchUpdates = () => {
|
||
fetch(globalUrl+"/api/v1/streams/results", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(executionRequest),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
stop()
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
handleUpdateResults(responseJson, 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) {
|
||
setExecutionData(responseJson)
|
||
}
|
||
}
|
||
|
||
//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 !== []) {
|
||
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
|
||
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()
|
||
|
||
setExecutionRunning(false)
|
||
var curelements = cy.elements()
|
||
for (var i = 0; i < curelements.length; i++) {
|
||
if (curelements[i].classes().includes("executing-highlight")) {
|
||
curelements[i].removeClass("executing-highlight")
|
||
curelements[i].addClass("failure-highlight")
|
||
} else {
|
||
//curelements[i].removeClass('not-executing-highlight')
|
||
//curelements[i].removeClass('executing-highlight')
|
||
//curelements[i].removeClass('success-highlight')
|
||
//curelements[i].removeClass('awaiting-data-highlight')
|
||
//curelements[i].removeClass('failure-highlight')
|
||
}
|
||
}
|
||
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
} else if (responseJson.status === "FINISHED") {
|
||
//console.log("STOPPING BECAUSE ITS OVAH!")
|
||
setExecutionRunning(false)
|
||
stop()
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
setUpdate(Math.random())
|
||
}
|
||
}
|
||
|
||
const saveWorkflow = (curworkflow) => {
|
||
var success = false
|
||
|
||
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")
|
||
|
||
// 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)
|
||
setScrollLocation(0)
|
||
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()
|
||
|
||
if (nodedata.id === selectedAction.id) {
|
||
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 !== "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 = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`
|
||
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)
|
||
} 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 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_id !== 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()
|
||
}
|
||
}
|
||
}
|
||
|
||
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()
|
||
console.log("NODE: ", data)
|
||
|
||
setLastSaved(false)
|
||
if (data.isButton) {
|
||
console.log("BUTTON CLICKED: ", data)
|
||
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)
|
||
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)))))
|
||
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(curaction)
|
||
//return
|
||
} else {
|
||
|
||
//console.log("AUTHENTICATION: ", curapp.authentication)
|
||
setRequiresAuthentication(curapp.authentication.required && curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null)
|
||
if (curapp.authentication.required) {
|
||
// Setup auth here :)
|
||
const authenticationOptions = []
|
||
var findAuthId = ""
|
||
if (curaction.authentication_id !== null && curaction.authentication_id !== undefined && curaction.authentication_id.length > 0) {
|
||
findAuthId = curaction.authentication_id
|
||
}
|
||
|
||
var tmpAuth = JSON.parse(JSON.stringify(newAppAuth))
|
||
console.log("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 = {}
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
setRightSideBarOpen(true)
|
||
} 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()
|
||
}
|
||
setRightSideBarOpen(true)
|
||
} else {
|
||
alert.error("Can't handle "+data.type)
|
||
}
|
||
|
||
}
|
||
|
||
// 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)
|
||
if (currentnode !== null && currentnode !== undefined) {
|
||
console.log("SHOULD CHECK IF TRIGGER HAS MULTIPLE EDGES: ", currentnode)
|
||
// 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)
|
||
}
|
||
}
|
||
|
||
const onNodeAdded = (event) => {
|
||
setLastSaved(false)
|
||
const node = event.target
|
||
const nodedata = event.target.data()
|
||
|
||
if (node.isNode() && cy.nodes().size() === 1) {
|
||
//setStartNode(node.data('id'))
|
||
workflow.start = node.data('id')
|
||
setWorkflow(workflow)
|
||
} else {
|
||
if (workflow.actions === null) {
|
||
return
|
||
}
|
||
|
||
// Remove bad startnode
|
||
const startnode_exists = false
|
||
for (var key in workflow.actions) {
|
||
const action = workflow.actions[key]
|
||
if (action.isStartNode && workflow.start !== action.id) {
|
||
action.isStartNode = false
|
||
}
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
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 !== "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 = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`
|
||
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,
|
||
//}
|
||
}
|
||
}
|
||
|
||
const onNodeRemoved = (event) => {
|
||
const node = event.target
|
||
const data = node.data()
|
||
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_id !== 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()
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
setWorkflow(workflow)
|
||
if (data.type === "TRIGGER") {
|
||
saveWorkflow(workflow)
|
||
}
|
||
}
|
||
|
||
var previouskey = 0
|
||
const handleKeyDown = (event) => {
|
||
// SHIFT = 16
|
||
// CTRL = 17
|
||
//console.log(event.keyCode)
|
||
switch( event.keyCode ) {
|
||
case 27:
|
||
console.log("ESCAPE")
|
||
break;
|
||
case 46:
|
||
//removeNode()
|
||
console.log("DELETE")
|
||
break;
|
||
case 38:
|
||
console.log("UP")
|
||
break;
|
||
case 37:
|
||
console.log("LEFT")
|
||
break;
|
||
case 40:
|
||
console.log("DOWN")
|
||
break;
|
||
case 39:
|
||
console.log("RIGHT")
|
||
break;
|
||
case 90:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+Z")
|
||
}
|
||
break;
|
||
case 67:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+C")
|
||
}
|
||
break;
|
||
case 86:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+V")
|
||
}
|
||
break;
|
||
case 88:
|
||
if (previouskey === 17) {
|
||
console.log("CTRL+V")
|
||
}
|
||
break;
|
||
case 83:
|
||
if (previouskey === 17) {
|
||
event.preventDefault()
|
||
saveWorkflow()
|
||
}
|
||
break;
|
||
case 70:
|
||
//if (previouskey === 17) {
|
||
// event.preventDefault()
|
||
// cy.fit(null, 50)
|
||
//}
|
||
//break;
|
||
case 65:
|
||
// As a poweruser myself, I found myself hitting this a few
|
||
// too many times to just edit text. Need a better bind
|
||
//
|
||
//if (previouskey === 17) {
|
||
// event.preventDefault()
|
||
// if (executionRunning || executionRequestStarted) {
|
||
// abortExecution()
|
||
// } else {
|
||
// executeWorkflow()
|
||
// }
|
||
// cy.fit(null, 50)
|
||
//}
|
||
break;
|
||
default:
|
||
//console.log(event.keyCode)
|
||
break;
|
||
}
|
||
|
||
previouskey = event.keyCode
|
||
}
|
||
|
||
const registerKeys = () => {
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
}
|
||
|
||
const getEnvironments = () => {
|
||
fetch(globalUrl+"/api/v1/getenvironments", {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for apps :O!")
|
||
if (isCloud) {
|
||
setEnvironments({"name": "Cloud", "type": "cloud"})
|
||
} else {
|
||
setEnvironments({"name": "Onprem", "type": "onprem"})
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
var found = false
|
||
var showEnvCnt = 0
|
||
for (var key in responseJson) {
|
||
if (responseJson[key].default) {
|
||
setDefaultEnvironmentIndex(key)
|
||
found = true
|
||
}
|
||
|
||
if (responseJson[key].archived === false) {
|
||
showEnvCnt += 1
|
||
}
|
||
}
|
||
|
||
if (showEnvCnt > 1) {
|
||
setShowEnvironment(true)
|
||
}
|
||
|
||
if (!found) {
|
||
for (var key in responseJson) {
|
||
if (!responseJson[key].archived) {
|
||
setDefaultEnvironmentIndex(key)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
setEnvironments(responseJson)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
|
||
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))
|
||
|
||
//cy.on('mouseover', 'node', () => $(targetElement).addClass('mouseover'));
|
||
|
||
//cy.on('cxttapstart', 'node', (e) => edgeHandler.start(e.target))
|
||
//cy.on('cxttapend', 'node', (e) => edgeHandler.stop())
|
||
//cy.on('cxtdragover', 'node', (e) => edgeHandler.preview(e.target))
|
||
//cy.on('cxtdragout', 'node', (e) => edgeHandler.unpreview(e.target))
|
||
|
||
// RIGHT HERE..?
|
||
// This is wrong sometimes.. I'm mad
|
||
document.title = "Workflow - "+workflow.name
|
||
registerKeys()
|
||
//setStartNode(workflow.start)
|
||
} else if (established) {
|
||
//console.log("established - should fix colors of things")
|
||
//console.log(cy.elements())
|
||
}
|
||
})
|
||
|
||
useEffect(() => {
|
||
console.log("Inside useeffect for action")
|
||
if (Object.getOwnPropertyNames(selectedAction).length !== 0) {
|
||
var rightSideActionView = document.getElementById("rightside_actions")
|
||
if (rightSideActionView !== undefined && rightSideActionView !== null) {
|
||
console.log("set scroll for ", rightSideActionView)
|
||
//rightSideActionView.scrollTop = 250
|
||
//rightSideActionView.scrollRight = 250
|
||
|
||
console.log("FOUND RIGHTSIDE: ", rightSideActionView.scrollTop, scrollLocation)
|
||
if (scrollLocation !== 0 && scrollLocation !== undefined && scrollLocation !== 0) {
|
||
console.log("SCROLL IS NOT 0: ", scrollLocation)
|
||
rightSideActionView.scrollTop = scrollLocation
|
||
} else {
|
||
console.log("SCROLL IS 0: ", scrollLocation, rightSideActionView.scrollTop)
|
||
if (rightSideActionView.scrollTop !== scrollLocation) {
|
||
setScrollLocation(rightSideActionView.scrollTop)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, [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 = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`
|
||
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 = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`
|
||
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)
|
||
|
||
var parentNode = cy.$('#' + event.target.data("id"));
|
||
if (parentNode.data('isButton') || parentNode.data('buttonId'))
|
||
return
|
||
|
||
const nodedata = event.target.data()
|
||
if (event.target.data().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 = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`
|
||
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) {
|
||
alert.WARNING("Failed to stop schedule: " + responseJson.reason)
|
||
} else {
|
||
alert.success("Successfully stopped schedule")
|
||
}
|
||
|
||
workflow.triggers[triggerindex].status = "stopped"
|
||
trigger.status = "stopped"
|
||
setSelectedTrigger(trigger)
|
||
setWorkflow(workflow)
|
||
saveWorkflow(workflow)
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
//const submitSchedule = (id, name, frequency, executionArg) => {
|
||
const submitSchedule = (trigger, triggerindex) => {
|
||
//const cronSplit = workflow.triggers[triggerindex].parameters[0].value.split("*")
|
||
//if (cronSplit.length <= 5 || cronSplit.length > 6) {
|
||
// alert.error("Error: Bad cron, example run every 1 minute: */1 * * * *")
|
||
// return
|
||
//}
|
||
|
||
if (trigger.name.length <= 0) {
|
||
alert.error("Error: name can't be empty")
|
||
return
|
||
}
|
||
|
||
alert.info("Attempting to create schedule with name " + trigger.name)
|
||
const data = {
|
||
"name": trigger.name,
|
||
"frequency": workflow.triggers[triggerindex].parameters[0].value,
|
||
"execution_argument": workflow.triggers[triggerindex].parameters[1].value,
|
||
"environment": workflow.triggers[triggerindex].environment,
|
||
"id": trigger.id,
|
||
}
|
||
|
||
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(data),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for stream results :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
alert.error("Failed to set schedule: " + responseJson.reason)
|
||
} else {
|
||
alert.success("Successfully created schedule")
|
||
workflow.triggers[triggerindex].status = "running"
|
||
trigger.status = "running"
|
||
setSelectedTrigger(trigger)
|
||
setWorkflow(workflow)
|
||
console.log("Should set the status to running and save")
|
||
saveWorkflow(workflow)
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const appViewStyle = {
|
||
marginLeft: 5,
|
||
marginRight: 5,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
height: "100%",
|
||
}
|
||
|
||
|
||
const paperAppStyle = {
|
||
borderRadius: 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 (
|
||
<div style={appViewStyle}>
|
||
<div style={variableScrollStyle}>
|
||
What are <a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#workflow_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>WORKFLOW variables?</a>
|
||
{workflow.workflow_variables === null ?
|
||
null : workflow.workflow_variables.map((variable, index) => {
|
||
return (
|
||
<div key={index} >
|
||
<Paper square style={paperVariableStyle} onClick={() => {
|
||
}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: yellow, marginRight: "5px"}} />
|
||
<div style={{display: "flex", width: "100%"}}>
|
||
<div style={{flex: "10", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}} onClick={() => {
|
||
setNewVariableName(variable.name)
|
||
setNewVariableDescription(variable.description)
|
||
setNewVariableValue(variable.value)
|
||
setVariablesModalOpen(true)}}>
|
||
Name: {variable.name}
|
||
</div>
|
||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||
<IconButton
|
||
aria-label="more"
|
||
aria-controls="long-menu"
|
||
aria-haspopup="true"
|
||
onClick={menuClick}
|
||
style={{color: "white"}}
|
||
>
|
||
<MoreVertIcon />
|
||
</IconButton>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={anchorEl}
|
||
keepMounted
|
||
open={open}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setOpen(false)
|
||
setAnchorEl(null)
|
||
}}
|
||
>
|
||
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
setOpen(false)
|
||
setNewVariableName(variable.name)
|
||
setNewVariableDescription(variable.description)
|
||
setNewVariableValue(variable.value)
|
||
setVariablesModalOpen(true)
|
||
}} key={"Edit"}>{"Edit"}</MenuItem>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
deleteVariable(variable.name)
|
||
setOpen(false)
|
||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
</div>
|
||
</Paper>
|
||
</div>
|
||
)
|
||
})}
|
||
<div style={{flex: "1"}}>
|
||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
|
||
setVariablesModalOpen(true)
|
||
setLastSaved(false)
|
||
}}>New workflow variable</Button>
|
||
</div>
|
||
<Divider style={{marginBottom: 20, marginTop: 20, height: 1, width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
What are <a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>EXECUTION variables?</a>
|
||
{workflow.execution_variables === null || workflow.execution_variables === undefined ?
|
||
null : workflow.execution_variables.map(variable=> {
|
||
return (
|
||
<div>
|
||
<Paper square style={paperVariableStyle} onClick={() => {
|
||
}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: yellow, marginRight: "5px"}} />
|
||
<div style={{display: "flex", width: "100%"}}>
|
||
<div style={{flex: "10", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}} onClick={() => {
|
||
setNewVariableName(variable.name)
|
||
setExecutionVariablesModalOpen(true)
|
||
}}>
|
||
Name: {variable.name}
|
||
</div>
|
||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||
<IconButton
|
||
aria-label="more"
|
||
aria-controls="long-menu"
|
||
aria-haspopup="true"
|
||
onClick={menuClick}
|
||
style={{color: "white"}}
|
||
>
|
||
<MoreVertIcon />
|
||
</IconButton>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={anchorEl}
|
||
keepMounted
|
||
open={open}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setOpen(false)
|
||
setAnchorEl(null)
|
||
}}
|
||
>
|
||
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
setOpen(false)
|
||
setNewVariableName(variable.name)
|
||
setExecutionVariablesModalOpen(true)
|
||
}} key={"Edit"}>{"Edit"}</MenuItem>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
deleteExecutionVariable(variable.name)
|
||
setOpen(false)
|
||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
</div>
|
||
</Paper>
|
||
</div>
|
||
)
|
||
})}
|
||
<div style={{flex: "1"}}>
|
||
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
|
||
setExecutionVariablesModalOpen(true)
|
||
setLastSaved(false)
|
||
}}>New execution variable</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const curTab = 0
|
||
const handleSetTab = (event, newValue) => {
|
||
setCurrentView(newValue)
|
||
}
|
||
|
||
const HandleLeftView = () => {
|
||
// Defaults to apps.
|
||
var thisview = <AppView allApps={apps} prioritizedApps={prioritizedApps} filteredApps={filteredApps} />
|
||
if (currentView === 1) {
|
||
thisview = <TriggersView />
|
||
} else if (currentView === 2) {
|
||
thisview = <VariablesView />
|
||
}
|
||
|
||
const tabStyle = {
|
||
maxWidth: leftBarSize/3,
|
||
minWidth: leftBarSize/3,
|
||
flex: 1,
|
||
textTransform: "none",
|
||
}
|
||
|
||
const iconStyle = {
|
||
marginTop: 3,
|
||
marginRight: 5,
|
||
}
|
||
|
||
return(
|
||
<div>
|
||
<div style={{minHeight: bodyHeight-appBarSize-50, maxHeight: bodyHeight-appBarSize-50}}>
|
||
{thisview}
|
||
</div>
|
||
<Divider style={{backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<Tabs
|
||
value={currentView}
|
||
indicatorColor="primary"
|
||
onChange={handleSetTab}
|
||
aria-label="Left sidebar tab"
|
||
>
|
||
<Tab label={
|
||
<Grid container direction="row" alignItems="center">
|
||
<Grid item>
|
||
<AppsIcon style={iconStyle} />
|
||
</Grid>
|
||
<Grid item>
|
||
Apps
|
||
</Grid>
|
||
</Grid>
|
||
} style={tabStyle} />
|
||
<Tab label={
|
||
<Grid container direction="row" alignItems="center">
|
||
<Grid item>
|
||
<ScheduleIcon style={iconStyle} />
|
||
</Grid>
|
||
<Grid item>
|
||
Triggers
|
||
</Grid>
|
||
</Grid>
|
||
} style={tabStyle} />
|
||
<Tab label={
|
||
<Grid container direction="row" alignItems="center">
|
||
<Grid item>
|
||
<FavoriteBorderIcon style={iconStyle} />
|
||
</Grid>
|
||
<Grid item>
|
||
Variables
|
||
</Grid>
|
||
</Grid>
|
||
}style={tabStyle} />
|
||
</Tabs>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const triggers = [{
|
||
"name": "Webhook",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"trigger_type": "WEBHOOK",
|
||
"errors": null,
|
||
"large_image": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH4wYNAxEP4A5uKQAAGipJREFUeNrtXHt4lNWZf8853zf3SSZDEgIJJtxCEnLRLSkXhSKgTcEL6yLK1hZWWylVbO1q7SKsSu3TsvVZqF2g4haoT2m9PIU+gJVHtFa5NQRD5FICIUAumBAmc81cvss5Z/845MtkAskEDJRu3r8Y8n3nfc/vvOe9zyDOOQxScoRvtAA3Ew2C1Q8aBKsfNAhWP2gQrH7QIFj9oEGw+kGDYPWDBsHqBw2C1Q+SbrQAPSg+/ULoRkvTjf4uwOKMAeeAEMI4AaBuf7rRhG5kIs05Zxxh1AUQ5yymUkVFgLBFxhZzbw///wGLUyZ2zikLn2oIVJ3o+NtZ5Xyb5u/gmgYAyCTLLqdlRKajaFRqeZFtTA7C+BJk5MZo2Y0Ai3EOHGGshyIX393btnNv5FQjjSoIYyQRRDBgdOkxyriuc8aJzeIozMu4d2rG16YQm4UzhtANULHrDRZnDGHMGW/b9lHzxh3RxlZslrHFjDAG4JxziBcHAUIIAHHGWFRhqmYblZ3z7bmZc24HAM75dTZk1xUsThkiWPn84umVv/btrSF2K7aYOGPA+pYBYQQIs5hCo8qQGRNGP/9vpky3WPAfECyxseCntSef+6Xq8UupDk4Z9Jc7QohgzR+yDMsY9/OlzpIx1xOv6wSW2JJvb03tM78AxrHVzHWaiAJGAMA5F4p2KYzgnHMG3WVEEqGRGDbJhWt+kFpedN3wuh5gCTsVPHzyb9/9L845Nkmcsm5CEMw0yiIxzhg2ycgkAQemqFyniBBiMyOJXOYVRcNmuXjDMntBnmBx84PFOSCktvmOLHxR9fiJ1Ry/bYQxZ0wPhk3pLtek4tQJhda84cRpA8Y0bzBS3xz4tDZYXav5QlKKXTwcjxcNxyy3ZJVufkFKtQtG/whg1f77Lzy7K+U0Z/ztQwTTiIJN0rAFdw97+G5TRlrihjkAAuVzT8tbu1ve3s01SmzdsZaI5g1m/cudY158/Doo18CCJTbg2V158plfXLLocUjpoYhtdE7+T5bYx+WKaJNR2mW8GOMciERESBWuPXdq2brIuRbJYU3QTRqOFq37oWtSyUDjNZBHwQFhzCk9v3knkqV4Iy2QcpaMKfnf5fZxuZxSRhlgREwykSVMCCaEyLJkkhHGlFKuU3tBXvH/LncU5Okd0QRzzjk/v2kngAjKBpAGULPEOXv/8umJ7/+3lGLvUgeMuKKZhrpLNv6nKcPFKeUIYYxVVa2srKyurm5ra+Ocp6enl5WVTZo0yW63M8YQ40giyueeo4+u1HwhJEtG2IEwohFl/Gv/kTqhcECVa8CrDhd3HUhw/MCBUzbquYXxSFVVVb322mv19fWcc0IIAFBKt2/fnp2dvWjRorvuuosBA52ah6ePfPYbtc++KpnkrmNGiGm65739qRMKYSAt8ICBxTnCWPd3hGrqsNXEO2N0RLAeDKffNTHtjjLOmEBq+/bta9askSQpJSUFRKjVeafa29tXrlx57ty5b3/72wwYMDZkZrl76q3eTw5LDptwjpxxYjEFDp2gkRixWQbOLQ6UxooNh+sa1Yu++CvDOUeEDJ03AwAYYxjjysrKNWvW2Gw2i8VCKaWUMsYYY+LfJpPJ7Xa/8cYbW7duxRgzygAga96M7k6TI5OstHgi9ecN1jcTWOI6hOuamKp12V2EWEyz5g1LuTUfAIgkqaq6YcMGSZIwxoyxnssI4FJSUjZv3nz+/HkiSxwg5bYCS3YGUzUDMoQRjamR+maD9U0FFgAAKOfb4j8ijLiq2gvzsNlENR0A/vrXv545c8ZqtV4WqUuwcy5JUiAQePfddwGA6TpxWO1jb2GKGuf+EHDeye6m0ywEAKB6At19E+KM20ZmCwwA4NChQ8ncGsaY2WyuqakBAIwwAFhGZHLGoOsuckBIC3R08b6ZwAIEACyqAEJxJ80BITnNCSJPBmhpaSGEJIMXIcTr9YZCIRFkSSkO4Im4cI32uc7fJ1gCMdTjUvD4/I5Smnwk2R3TnvhybJYHdDcDBxYHAOKwivwu/r/VNp+xc5fL1Yu1iidKqdvtdjgcwBgA6MEIksilAjQAAAIO8pDUK+D4dw4WBwAwZ6bF6xFwQBIJ1zaI3QFAfn5+Msol4vuioiKEEKUMAMKnGvVAB4sqAIAIRhghgq25WWAsfTOBBQAA1lHZ8QER54xYzKEjdbHzF4kkAcC0adNSU1P7xIsxJsvytGnToNPYDX/kazmP3WcdORwY1/whzRfEZpM9PxcGMkMcqAheSOwoHNmtSMAByUT1Bi5s+yj3yflU1YYPH37fffdt3rw5IyND07TLLoUxjkQixcXFZWVlAIAJBoC020vTbi/llEbPtgQ/O+X75DAgZM0bBgBxd/MmAUtIbBuTYxuT03H8LLaZRbGYMyY5bK1v7U6/a5J93C3A2KJFi86ePfvxxx+73W6EEO8kYyURZ/l8Pr/f73K5OOcIIc4YcECECBZZ/zIjsU49AERefPHFAVpatFH1QNi3t4ZYLV1FAkJoROk4Vp9RMRmbTRjgjqlTFUU5fvx4OBxmjBFCJEmKx0uW5ZaWFoTQhAkTRJKEuspeHBgDQNehDD+AYCEEgJBleMbFXQeYonRFp5xjsxxrbus4cTZ9ZjkyyRjQpMmTJk2a5HA4CCHBYDAYDJrNXb17zrnZbD516tTkyZPdbrdQrk4uCGE80JWsAQdLtOYlp412RPz7jxK7pas/yDmxWiKnmwNVf3NNKpZTHUyn6RkZEyZMqKiomDlzJqX02LFjstwVNxFCOjo6gsHgV77ylXiwricNJFidymUvHOn96FPNF8Iy6YqBOCdWS6y5zbPrgGVYhn3sCM454xwB2Gy2iRMnyrJ84MABi8Ui7iPn3GKxnD59urCwMCcnh4kO/j8SWIAQZ4xYTObsjIvv7sMmU7euKufEYqJR5eJ7+yOnmx35t5jcKUh08TkvLS2tra09d+6c2Ww2Klyapn3++ecVFRX4RkwgDXyvDWPOmHvabTmL7lG9ASSR+L9yypAkSU57+wdVNQ8tC59sAIRwZ1S5cOFCWe6qiDLG7Hb70aNH33vvPQCgdMDd3/UGS+AFnOc+9VBGxRTN40+skXMOCBBBriml9nG5wAEwEuWtwsLCmTNnhkIhUWgWeFkslt///vfBYDDJDPwmA6sTMzT25e+4Z5TTmJI43kcZtphzn3jwEnaXHkcAsGDBApfLpeu6+CjcYlNT0zvvvCOwS2DSM0z7uwNLVIF7ExEhTimxmrMeuJPTbrYZEaIHw8Mevts2dgSnzIi/EUKU0pycnHvuuaejo8MwUowxh8Oxffv2pqYmQohRiTbsmiDOuZAqyUR9wMHinAuMMMaEkN7dEyKEa3rz5h0ovm6DEI0ptlHZ2YvuATFXFC8cxgDw4IMPZmdnK4piKJckScFg8Le//a1AhxAiwlRKaSwWi8ViQhOFVBhjQ85rBOvq0x3hvAkhuq7X1tZ++OGHxcXFM2fOFBF2IqyUIYJb3v4gWH1STnMa2SLCiCvaiMUPSE5bz2EYsf/U1NSHHnpo9erVZrNZGHVKqcPh+Oijj2bNmjV06NCqqqqzZ8+2trYGAgFFUcRVTU1NzcrKys/PLykpycvLEwbusrIlT1fTZBVGAWOsKMr777+/Y8eOc+fOeb3emTNnrlq16jICcQ4IKa3tRx75T70jiiQiDJPoS6fdXlr0Pz/svX+l6/rSpUtPnz4dX60XKqZpWjgcRgiJrodgLVRJ3EGbzTZ69OhZs2bNmjXL4XCI168Osn6DZSB18ODB1157ra6uzmw2WywW4dc3bNhg5LpdrzCGMD790uutf/hIdjl5nMvnjJX8eoWjaOSlSeTLkUB///79K1asEN3pS6IjZGi3IVjXxhASMlBKFUVRVTU7O3vBggWzZ88mhFydivXvBYECY2z9+vXPPfdcY2Ojy+Uym82Ct8fjOXnyJHSv/wqkAlV/a9uxV0qxG0hdsuvzZjqKRl6aXL6SiBhzzqdMmTJ58uR4S28cSbyNN8joPAKA1Wp1uVxer/fnP//5s88+29zcjDG+ijCtH2AJ4SKRyIoVK7Zs2eJwOERb1HBDlFLRgOl2whhzxhrXvgPxCQpCLKZYc4flPHYf9LDrl2UNAN/85jeFCvd3kwI4WZbdbndNTc3SpUurqqqEJx0QsARSsVhs+fLl+/btGzJkiGh/xj9gsVg+++wzADBiSGHIW9/5MFBdSzq77QIdqqgjFv+z5HJyyvrstosYNT8/v6KioqOjw1i/60jifJ+4gD0dNOdc13Wn0xmLxZ5//vmPP/64v3j17xquWrWqqqoqLS1N1/WEzXDOI5GI1+v1+/1CMuAcEay2+Zp/vZ3YrF1ICbs+pSzz3qnimWRYGzGqqKkaKAhQdF0PhUJ+vz8cDiuKEovFxEdVVRMgEyomy/JPfvKTQ4cOCfuV5PaTMvDCJG3ZsuVXv/qV2+1OQIoQEo1GAWDOnDmPPPJIenr6pWImZYjg+pc3try9W3aldLPrlJX8erlj/Khe7HpPopQSQt56661169aJthBCKBqNapqWlZVVXFxcVFSUk5PjcDgope3t7bW1tZWVlWfPnrVarbIsx4MiOiB2u/2Xv/zl8OHDk6z59A2WQOrUqVPf+973hJLHvyJqdaNGjXr66adLSkqEQgHnopETrD55bPFPsVmOL5NqvmD2wntGPvP1/k4Ziy0pivLEE080NzcDgKIo48ePv/fee6dMmeJ0Onu+oqrqn//8502bNnk8HgFivOShUKi8vHzVqlVJgtW3rGKVTZs2xWKxhNwVYxwIBO666661a9eWlJRQTeeMI4wRJqK60LD2HR7fuUGIKYr1lqycbyVl13tKIvr43/jGN/x+//Dhw1944YVXX331q1/9ak+kdF3XNE2W5YqKivXr1992220i9zYeoJQ6nc7Kyspdu3aJlfsWoHfNEmpVXV397LPP2my2BE0OBoMPP/zwkiVLOOeMUiJJwHnH8TO+/UeiDa1Ki6fj+JluI3oEa/6O/Je/k3nftGsZXldVdffu3VOnTk1JSaGUCn1vbW09d+5cOBx2OBy5ublZWVnQOYQjYteXXnpp37594hUDfVVVc3Jy1q9fbzKZ+tSvpNKdnTt3JgBPCAkEAnPnzl2yZAljDBgnktRx4lzDq28Gqk4wRUUYIUnCVnPcMCPWO6JpU0oy75uWvF2/LMmyPGfOHM650J36+vrNmzfX1NSIfgfG2OFwFBUVPfzww7feeit0th2XL1/+gx/8oK6uzkgDhAc/c+bM/v37p0+f3idYvUksIvW2trbDhw8b5V2hU+FwuLS0dOmTS4WRwhK5+O7eo4te8h84hq0mOS1FSnUQmxl6qO2wf60A0ZK5NtJ1Xdd1WZb37Nnz5JNP7tmzR6QQTqfTbrfrun7w4MGnn3769ddfN3Jsi8Xy/e9/32QyJUQ8CKEPP/wwGaa9gSUWPXLkiNfrja9YiqTs8ccfl2SJ6RQT4v3o01PPr0cESyl2YJxTyinrhghCTNflNKejIA/6b60SSKQ4Aqkf//jHACDmK1knIYQcDofD4di0adPatWtF2EUp7RmpCeWqra31er0iALpKsAQdO3as2wsYh8Ph8vLykpISRimRJc0XPLPqDWySESH8ijEeRwhxnTJNv/yfe6WEhzVNa2xsXLt27cqVKyVJkiSpZ2wpUov09PS33nrrk08+MZz47Nmz7Xa78bw4eK/XW1dXB32NWPZms0QW1tDQEN/yFI7jjjvuABGgE3Lhjx/Hmi/IQ1J76wlzQBLR/aHQkTpLdgZw0HTtpZdeunDhgqGz8ZoLcSMLRj3PCFxCodCFCxcikYhwgldyZAJok8n05ptvTps2Texi9OjRY8eOPXbsmOGvEEK6rp85c2bixIlXCZYR1Hi93viIgVJqs9ny8/MBQMQH/n2fYbOczHcGOQf/viMZX5sCCBBAQ0NDY2OjyMMNXC4rSYJUGGNZlsVESe8cRc3+9OnTtbW1BQUFlFJJkvLz82tqarpVaxFqaWnpU/4+vGEsFotGo0aiLyyl3W53uVwAgDGm4ZjS6kXdu+1XQh+bpGhDi1iIcS5JktVqFT4brnwFeiIoVCbJtE7U3c6ePVtQUCBOJTs7O1EwjEWWdk2hA6XUaBYYS4uzvfSRMZ5kbsUBEKLRGNN0LEuqoookySif94JyUuv3SqFQyPh3zwhWdCT7BKsPA08Iib+D4hCi0WhHR4f4KDltl8rEffo3BMA5sVmQLAFAIBgIh8M9HRBOmvrVkbbZbMa/e842iX31uUgfmmWxWGw2WyAQiIcvHA6fP38+JyeH6TqRZcf40aGj9cRm4dDbvUAIMVW3jc4RW2xtbY1EIglZAQBEo9FkWvPC5ffp7MWTsizn5nbNuXk8noS3OOd2ux3iCor9A0v4HbPZ7Ha7m5ubDdcrzNaRI0cmTpwoBhIzKiZf+MOfk/i2M0IYDZn5ZfHh5MmT8ZUWQ+hx48bFB8BXIoxxXV2dqMD08rDwUSNGjCgoKIDOQlt9fX38W8K/Z2RkwLWEDmJUatSoUdXV1cauGGMmk+ngwYOLFi2SZZkzlvJP49Lvnti2Y4+c7uJXCKOQLGntAff0L6XdUSbKMtXV1fGBrrAamZmZr7zyitVq7f2ERU6za9eun/70p737REJIJBKZO3euLMuiwhMMBk+cOGHMTxjcher1cUJ9PlFaWhpflhH6X19ff+DAAQBgjAPAyB9+016Qp3mDSJa6RecIxE9baN6AbXTOmBWPcgCEUV1d3fHjx+NrxMJnFRUVWa1WsfleYlShCxUVFXPnzvV4PKKv01OnJElqb2+/884777//fuP/9+3b19raGn9OIhgaM2YMXIuBFxKUlZVlZmYmXBlRC1RVlUiEMyanOYvW/tBVXqRe9NGoIhwfIMQp18NRzRtMu71s/Gv/Ycp0i2+hbNu2LRqNxhdMBARixBbiAtFeiHP+1FNPPfDAA+3t7bFYTJRMBWGMNU3zeDzTp09ftmyZWJ8QEovFtm7dmqDRqqrm5uaOHDkS+mqR9TZyJA7QarU2NzcfO3ZM3A7oHDg4f/68pmnl5eWMMQQgOW0Zs6eYh6Vr7UE9GGaKyhmT7NaU0rF5Tz2Uu/QhyWnTNU2S5YMHD77++uvxpt0olSxevFiSJKOL1QsZKnb77bePGDGiqanJ4/FEIhHRkWaMDRs27Fvf+tbixYvFRJy4uZs2bfrLX/5idA+h01/df//9ZWVlotrTC9Ok6lmnT59eunRpwkIY446OjieeeGLevHmMMc4YxgRhxClTWjxaewAINg8dYspwAQCjjDEqyXJTU9Mzzzzj9/vjj5cQ4vf7n3zyyfnz5wvL0jtSRtdPhKaiXFVbW1tfX9/R0WGz2UaOHFlYWGhcc1HS2r1796pVq4wjN0CXJGn9+vXJFJeTLSuvXr1627ZtLpcrvnIGAOFweMGCBY899pjwL1TTESGks1bFAZiuIwBECELoxIkTK1eu9Hg88dbKMO3r1693OBy9SywagoSQ999/v7m5+ZFHHjFKLglnKXA0ksrdu3e/8sorwrolHNK8efOWLl2aTNu1b7CE9F6v97vf/a7P54uvBwlRgsFgUVHRwoULy8vLeyqFeP3ixYtbt27dtm2bqAvHx1aijvjCCy/MmDGjd4kNULZs2bJx40Zd10ePHj1//vzp06dbLBYDSsFRHJ5o3/3mN795++23E+IykT87nc5169YZTZZrBctQrn379okGekLZRLhnSunYsWPLy8sLCgoyMzOtVquu636/v6Gh4bPPPqupqfH5fA6HI+FLmGLAfc6cOc8991zvSInNqKq6Zs2anTt3ulwukUsoipKXlzdt2rSJEyfm5eWJ2FLI3NLScuDAgR07djQ0NIgUJ0HsQCCwbNmyu+++O8lufrKzDmK53/3ud+vWrXO73QkJneAUi8UURcEYm81mSZIYY6qqappGCLFarT2rTuIrl+PHj+8zthJ/8vl8K1eurK6uNqyBuGLCqJvN5vT09IyMDNHF8Xq9LS0twWDQYrGIznkC6/b29vnz5yd5AfsHloHXhg0b3njjjbS0tJ5lOSF6fMXOqED1fFiSJL/fn5+fv2rVqoTR9ssiFYlEHn/88aampiFDhqiq2pMvY0zTNF3XjWkRWZbFmfVk3d7ePmvWrBUrViTjeQ3qx7SyiCQmTJhgNpsPHDggiko9k6wEX9MTJoGg1+udMGHCyy+/LPS0l7MVcJtMJrvdXl1dLZQoIaMULAghpk4yRmsSWAOAz+ebPXv2j370I/FM8mD1e+RIuPa9e/euXr3a4/E4nU5xqsmsI2AKh8MA8OCDDz722GOiUZzMLRD6dfz48Z/97GeNjY0pKSn9moQRrCORCAAsWrTo61//ekI9dkDAMvDyeDwbN2784IMPFEWx2Wwi9rvs3RQC6bouClhlZWWPPvpoaWmpuC/JiytgDYVCGzdu/NOf/iT678LrXbZUD3GWQdjT4uLiJUuWFBcX95f11YMFnTOSCKFTp0798Y9/rKysbG9vFwGeMcoCnbM+uq5zzlNSUkpLS++9994vf/nLQhmvQlzjrZMnT7755ptVVVWhUEiWZXHv4hcUcZamaaqqSpI0ZsyYBx54YMaMGcKKXafJP4PEYRpW4PDhw0ePHhXzkiKSEG4xLS1txIgR48eP/9KXvjRs2LCEF6+Rb1NT0549ew4dOtTY2BgMBjVNM7YjSZLNZhs6dGhxcfHUqVNLS0tFw+JaWF/rD/f0jJ5VVY1EIrquY4ytVqvVau3l4S+Kr8/na2lpuXjxomhKWywWt9udlZU1dOhQw9KL0P9amH4xv3IkRIFOO9pzY+I8v/CvJiWzskh6vpAT+uJ/Eqqngf9i178S0wQbb1RyvkAuN/S3lW82uvE/hX0T0SBY/aBBsPpBg2D1gwbB6gcNgtUPGgSrHzQIVj9oEKx+0CBY/aD/A/ORNiwv2PAfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE5LTA2LTEzVDAzOjE3OjE2LTA0OjAwj3mANAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOS0wNi0xM1QwMzoxNzoxNS0wNDowMM/MIhUAAAAASUVORK5CYII=',
|
||
"is_valid": true,
|
||
"label": "Webhook",
|
||
"environment": "onprem",
|
||
"description": "Simple HTTP webhook",
|
||
"long_description": "Execute a workflow with an unauthicated POST request",
|
||
},
|
||
{
|
||
"name": "Shuffle Workflow",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"trigger_type": "SUBFLOW",
|
||
"errors": null,
|
||
"large_image": 'data:image/png;base64,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',
|
||
"is_valid": true,
|
||
"label": "Subflow",
|
||
"environment": "onprem",
|
||
"description": "Control another workflow",
|
||
"long_description": "Execute another workflow from this workflow",
|
||
},
|
||
{
|
||
"name": "User Input",
|
||
"type": "TRIGGER",
|
||
"status": "running",
|
||
"large_image": "data:image/png;base64,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",
|
||
"description": "Wait for user input",
|
||
"trigger_type": "USERINPUT",
|
||
"errors": null,
|
||
"is_valid": true,
|
||
"label": "User input",
|
||
"environment": "cloud",
|
||
"long_description": "Take user input to continue execution",
|
||
},
|
||
{
|
||
"name": "Schedule",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"description": "Schedule execution time",
|
||
"trigger_type": "SCHEDULE",
|
||
"errors": null,
|
||
"large_image": "data:image/png;base64,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",
|
||
"label": "Schedule",
|
||
"is_valid": true,
|
||
"environment": "onprem",
|
||
"long_description": "Create a schedule based on cron",
|
||
},
|
||
{
|
||
"name": "Email",
|
||
"type": "TRIGGER",
|
||
"status": "uninitialized",
|
||
"description": "Add your email provider",
|
||
"trigger_type": "EMAIL",
|
||
"errors": null,
|
||
"is_valid": 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 (
|
||
<div style={triggersViewStyle}>
|
||
<div style={appScrollStyle}>
|
||
{triggers.map((trigger, index) => {
|
||
var imageline = trigger.large_image.length === 0 ?
|
||
<img alt="" style={{width: "80px", pointerEvents: "none", }} />
|
||
:
|
||
<img alt="" src={trigger.large_image} style={{width: 80, height: 80, pointerEvents: "none", }} />
|
||
|
||
const color = trigger.is_valid ? green : yellow
|
||
return(
|
||
<Draggable
|
||
key={index}
|
||
onDrag={(e) => {handleTriggerDrag(e, trigger)}}
|
||
onStop={(e) => {handleDragStop(e)}}
|
||
dragging={false}
|
||
position={{
|
||
x: 0,
|
||
y: 0,
|
||
}}
|
||
>
|
||
<Paper square style={paperAppStyle} onClick={() => {}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: color, marginRight: "5px"}}>
|
||
</div>
|
||
<Grid container style={{margin: "10px 10px 10px 10px", flex: "10"}}>
|
||
<Grid item>
|
||
<ButtonBase>
|
||
{imageline}
|
||
</ButtonBase>
|
||
</Grid>
|
||
<Grid style={{display: "flex", flexDirection: "column", marginLeft: "20px"}}>
|
||
<Grid item style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "0px", marginTop: "10px"}}>{trigger.name}</h3>
|
||
</Grid>
|
||
<Grid item style={{flex: "1"}}>
|
||
{trigger.description}
|
||
</Grid>
|
||
</Grid>
|
||
</Grid>
|
||
</Paper>
|
||
</Draggable>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
var newNodeId = ""
|
||
const handleTriggerDrag = (e, data) => {
|
||
const cycontainer = cy.container()
|
||
// Chrome lol
|
||
//if (e.srcElement !== undefined && e.srcElement.localName === "canvas") {
|
||
if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) {
|
||
if (newNodeId.length > 0) {
|
||
var currentnode = cy.getElementById(newNodeId)
|
||
if (currentnode.length === 0) {
|
||
return
|
||
}
|
||
|
||
currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft)
|
||
currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop)
|
||
} else{
|
||
console.log(workflow)
|
||
if (workflow.start === "" || workflow.start === undefined) {
|
||
alert.error("Define a starting action first.")
|
||
return
|
||
}
|
||
|
||
if (data.is_valid === false) {
|
||
alert.error(data.name+" trigger isn't available yet")
|
||
return
|
||
}
|
||
|
||
const triggerLabel = getNextActionName(data.name)
|
||
|
||
newNodeId = uuid.v4()
|
||
const newposition = {
|
||
"x": e.pageX-cycontainer.offsetLeft,
|
||
"y": e.pageY-cycontainer.offsetTop,
|
||
}
|
||
|
||
console.log(data)
|
||
const newAppData = {
|
||
app_name: data.name,
|
||
app_version: "1.0.0",
|
||
environment: isCloud ? "cloud" : data.environment,
|
||
description: data.description,
|
||
long_description: data.long_description,
|
||
errors: [],
|
||
id_: newNodeId,
|
||
_id_: newNodeId,
|
||
id: newNodeId,
|
||
label: triggerLabel,
|
||
type: data.type,
|
||
is_valid: true,
|
||
trigger_type: data.trigger_type,
|
||
large_image: data.large_image,
|
||
status: "uninitialized",
|
||
name: data.name,
|
||
isStartNode: false,
|
||
position: newposition,
|
||
}
|
||
|
||
//if (data.trigger_type === "WEBHOOK") {
|
||
// newAppData.status = "running"
|
||
//}
|
||
|
||
// Can all the data be in here? hmm
|
||
const nodeToBeAdded = {
|
||
group: "nodes",
|
||
data: newAppData,
|
||
renderedPosition: newposition,
|
||
}
|
||
|
||
cy.add(nodeToBeAdded)
|
||
|
||
if (workflow.triggers === undefined) {
|
||
workflow.triggers = [newAppData]
|
||
} else {
|
||
workflow.triggers.push(newAppData)
|
||
}
|
||
|
||
const newEdgeUuid = uuid.v4()
|
||
const newcybranch = {
|
||
"source": newNodeId,
|
||
"target": workflow.start,
|
||
"source_id": newNodeId,
|
||
"destination_id": workflow.start,
|
||
"_id": newEdgeUuid,
|
||
"id": newEdgeUuid,
|
||
"hasErrors": false,
|
||
"decorator": false,
|
||
}
|
||
|
||
const edgeToBeAdded = {
|
||
group: "edges",
|
||
data: newcybranch,
|
||
}
|
||
|
||
if (data.name !== "User Input" && data.name !== "Shuffle Workflow") {
|
||
//workflow.branches.push(newbranch)
|
||
cy.add(edgeToBeAdded)
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
//if (data.trigger_type === "WEBHOOK") {
|
||
// newWebhook(newAppData)
|
||
// saveWorkflow(workflow)
|
||
//}
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleAppDrag = (e, app) => {
|
||
const cycontainer = cy.container()
|
||
|
||
// Chrome lol
|
||
//if (e.srcElement !== undefined && e.srcElement.localName === "canvas") {
|
||
if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) {
|
||
if (newNodeId.length > 0) {
|
||
var currentnode = cy.getElementById(newNodeId)
|
||
if (currentnode.length === 0) {
|
||
return
|
||
}
|
||
|
||
currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft)
|
||
currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop)
|
||
} else{
|
||
if (workflow.public) {
|
||
return
|
||
}
|
||
|
||
if (app.actions === undefined || app.actions === null || app.actions.length === 0) {
|
||
alert.error("App "+app.name+" currently has no actions to perform. Please go to https://shuffler.io/apps to edit it.")
|
||
return
|
||
}
|
||
|
||
newNodeId = uuid.v4()
|
||
const actionType = "ACTION"
|
||
const actionLabel = getNextActionName(app.name)
|
||
var parameters = null
|
||
var example = ""
|
||
|
||
if (app.actions[0].parameters !== null && app.actions[0].parameters.length > 0) {
|
||
parameters = app.actions[0].parameters
|
||
}
|
||
if (app.actions[0].returns.example !== undefined && app.actions[0].returns.example !== null && app.actions[0].returns.example.length > 0) {
|
||
example = app.actions[0].returns.example
|
||
}
|
||
|
||
var newAppPopup = false
|
||
|
||
/*
|
||
FIXME: Add auth.
|
||
selectedAction.selectedAuthentication = e.target.value
|
||
selectedAction.authentication_id = e.target.value.id
|
||
setSelectedAction(selectedAction)
|
||
setUpdate(Math.random())
|
||
*/
|
||
|
||
console.log("ENVS: ", environments)
|
||
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: "",
|
||
}
|
||
|
||
// FIXME: overwrite category if the ACTION chosen has a different category
|
||
|
||
// const image = "url("+app.large_image+")"
|
||
|
||
// FIXME - find the cytoscape offset position
|
||
// Can this be done with zoom calculations?
|
||
const nodeToBeAdded = {
|
||
group: "nodes",
|
||
data: newAppData,
|
||
renderedPosition: {
|
||
x: e.layerX,
|
||
y: e.layerY,
|
||
}
|
||
}
|
||
|
||
cy.add(nodeToBeAdded)
|
||
|
||
if (workflow.actions === undefined || workflow.actions.length === 0) {
|
||
workflow.start = newAppData.id
|
||
workflow.actions = []
|
||
newAppData.isStartNode = true
|
||
//setStartNode(newAppData.id)
|
||
}
|
||
|
||
if (workflow.actions.length > 0 && elements.length === 0) {
|
||
const actions = workflow.actions.map(action => {
|
||
const node = {}
|
||
node.position = action.position
|
||
node.data = action
|
||
|
||
node.data._id = action["id"]
|
||
node.data.type = "ACTION"
|
||
node.isStartNode = action["id"] === workflow.start
|
||
|
||
return node
|
||
})
|
||
|
||
const tmpelements = [].concat(actions)
|
||
setElements(tmpelements)
|
||
}
|
||
|
||
if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) {
|
||
const newEdgeUuid = uuid.v4()
|
||
const newcybranch = {
|
||
"source": workflow.start,
|
||
"target": newNodeId,
|
||
"_id": newEdgeUuid,
|
||
"id": newEdgeUuid,
|
||
"hasErrors": false,
|
||
}
|
||
|
||
const edgeToBeAdded = {
|
||
group: "edges",
|
||
data: newcybranch,
|
||
}
|
||
console.log("SHOULD STITCH WITH STARTNODE")
|
||
cy.add(edgeToBeAdded)
|
||
}
|
||
|
||
// 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)
|
||
//}
|
||
} else {
|
||
newAppData.authentication = []
|
||
newAppData.authentication_id = ""
|
||
newAppData.selectedAuthentication = {}
|
||
}
|
||
|
||
workflow.actions.push(newAppData)
|
||
setWorkflow(workflow)
|
||
|
||
if (newAppPopup) {
|
||
//alert.error("SHOULD MAKE USER AUTHENTICATE THE APP OR SET hasError")
|
||
//alert.info("Remember: set the authentication for the user itself, not the app")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleDragStop = (e, app) => {
|
||
newNodeId = ""
|
||
console.log("STOP!: ", e)
|
||
console.log("APP!: ", app)
|
||
}
|
||
|
||
const appScrollStyle = {
|
||
overflow: "scroll",
|
||
maxHeight: bodyHeight-appBarSize-55-50,
|
||
minHeight: bodyHeight-appBarSize-55-50,
|
||
marginTop: 1,
|
||
overflowY: "auto",
|
||
overflowX: "hidden",
|
||
}
|
||
|
||
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 (
|
||
<Draggable
|
||
onDrag={(e) => {handleAppDrag(e, app)}}
|
||
onStop={(e) => {handleDragStop(e, app)}}
|
||
key={app.id}
|
||
dragging={false}
|
||
position={{
|
||
x: 0,
|
||
y: 0,
|
||
}}
|
||
>
|
||
<Paper square style={newAppStyle} onMouseOver={() => {setHover(true)}} onMouseOut={() => {setHover(false)}}>
|
||
<Grid container style={{margin: "10px 10px 10px 15px", flex: "10"}}>
|
||
<Grid item>
|
||
<img alt={newAppname} src={image} style={{pointerEvents: "none", userDrag: "none", userSelect: "none", borderRadius: theme.palette.borderRadius, height: 80, width: 80,}} />
|
||
</Grid>
|
||
<Grid style={{display: "flex", flexDirection: "column", marginLeft: "20px", minWidth: 185, maxWidth: 185, overflow: "hidden", maxHeight: 77, }}>
|
||
<Grid item style={{flex: 1}}>
|
||
<Typography variant="body1" style={{marginBottom: 0, marginTop: 5}}>
|
||
{newAppname}
|
||
</Typography>
|
||
</Grid>
|
||
<Grid item style={{flex: 1}}>
|
||
<Typography variant="body2" color="textSecondary">
|
||
Version: {app.app_version}
|
||
</Typography>
|
||
</Grid>
|
||
<Grid item style={{flex: 1, width: "100%", maxHeight: 27, overflow: "hidden",}}>
|
||
<Typography variant="body2" color="textSecondary">
|
||
{app.description}
|
||
</Typography>
|
||
</Grid>
|
||
</Grid>
|
||
</Grid>
|
||
</Paper>
|
||
</Draggable>
|
||
)
|
||
}
|
||
|
||
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())))
|
||
setVisibleApps(newApps)
|
||
} else {
|
||
setVisibleApps(prioritizedApps.concat(filteredApps.filter(innerapp => !internalIds.includes(innerapp.id))))
|
||
}
|
||
}
|
||
|
||
return(
|
||
<div style={appViewStyle}>
|
||
<div style={{flex: "1"}}>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius, marginTop: 5, marginRight: 10,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<Tooltip title="Run search" placement="top">
|
||
<SearchIcon style={{cursor: "pointer"}} />
|
||
</Tooltip>
|
||
</InputAdornment>
|
||
)
|
||
}}
|
||
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 ?
|
||
<div style={appScrollStyle}>
|
||
{visibleApps.map((app, index) => {
|
||
if (app.invalid) {
|
||
return null
|
||
}
|
||
|
||
return(
|
||
<ParsedAppPaper key={index} app={app} />
|
||
)
|
||
})}
|
||
</div>
|
||
:
|
||
apps.length > 0 ?
|
||
<div style={{textAlign: "center", width: leftBarSize, marginTop: 10, }}>
|
||
<Typography variant="body1" color="textSecondary">
|
||
Couldn't find app. Is it active?
|
||
</Typography>
|
||
</div>
|
||
:
|
||
<div style={{textAlign: "center", width: leftBarSize}}>
|
||
<CircularProgress style={{marginTop: 25, height: 35, width: 35, marginLeft: "auto", marginRight: "auto", }} />
|
||
<Typography variant="body1" color="textSecondary">
|
||
Loading apps
|
||
</Typography>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const getNextActionName = (appName) => {
|
||
var highest = ""
|
||
//label = name + _number
|
||
const allitems = workflow.actions.concat(workflow.triggers)
|
||
for (var key in allitems) {
|
||
const item = allitems[key]
|
||
if (item.app_name === appName && item.label !== undefined && item.label !== null) {
|
||
var number = item.label.split("_")
|
||
if (isNaN(number[-1]) && parseInt(number[number.length-1]) > highest) {
|
||
highest = number[number.length-1]
|
||
}
|
||
}
|
||
}
|
||
|
||
if (highest) {
|
||
return appName+"_"+(parseInt(highest)+1)
|
||
} else {
|
||
return appName+"_"+1
|
||
}
|
||
}
|
||
|
||
const setNewSelectedAction = (e) => {
|
||
const newaction = selectedApp.actions.find(a => a.name === e.target.value)
|
||
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))
|
||
|
||
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 = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`
|
||
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 =
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
multiline={multiline}
|
||
rows={5}
|
||
color="primary"
|
||
defaultValue={data.value}
|
||
placeholder={placeholder}
|
||
helperText={data.value !== undefined && data.value !== null && data.value.includes(".#") ?
|
||
<span style={{color:"white", marginBottom: 5, marginleft: 5,}}>
|
||
Use "Shuffle Tools" app with "Filter List" action to handle loops
|
||
</span>
|
||
: null
|
||
}
|
||
onClick={() => {
|
||
//console.log("CHANGE FIELD")
|
||
}}
|
||
onBlur={(e) => {
|
||
changeActionVariable(data.action_field, e.target.value)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
|
||
const changeActionVariable = (variable, value) => {
|
||
// set the name
|
||
data.value = value
|
||
data.action_field = variable
|
||
|
||
//setConditionValue({})
|
||
|
||
if (type === "source") {
|
||
setSourceValue(data)
|
||
//setDestinationValue(destinationValue)
|
||
} else if (type === "destination") {
|
||
setDestinationValue(data)
|
||
//setSourceValue(sourceValue)
|
||
}
|
||
}
|
||
|
||
const changeActionParameterVariant = (variant) => {
|
||
if (data.variant === variant) {
|
||
return
|
||
}
|
||
|
||
data.variant = variant
|
||
data.value = ""
|
||
|
||
if (variant === "ACTION_RESULT") {
|
||
console.log("SHOULD FIND PARENTS OF EDGE")
|
||
|
||
// Uses the target's parents, as the target should be executing the checks (I think)
|
||
var parents = getParents(workflow.actions.find(a => a.id === selectedEdge["target"]))
|
||
if (parents.length > 0) {
|
||
data.action_field = parents[0].label
|
||
} else {
|
||
data.action_field = ""
|
||
}
|
||
} else if (variant === "WORKFLOW_VARIABLE") {
|
||
if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) {
|
||
data.action_field = workflow.workflow_variables[0].name
|
||
}
|
||
}
|
||
|
||
setSourceValue({})
|
||
setConditionValue({})
|
||
setDestinationValue({})
|
||
|
||
if (type === "source") {
|
||
setSourceValue(data)
|
||
setDestinationValue(destinationValue)
|
||
} else if (type === "destination") {
|
||
setDestinationValue(data)
|
||
setSourceValue(sourceValue)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>{data.name} </b>
|
||
</div>
|
||
</div>
|
||
{datafield}
|
||
{actionlist.length === 0 ? null :
|
||
<FormControl fullWidth>
|
||
<InputLabel id="action-autocompleter" style={{marginLeft: 10, color: "white"}}>Autocomplete</InputLabel>
|
||
<Select
|
||
labelId="action-autocompleter"
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setShowAutocomplete(false)
|
||
|
||
//if (!selectedActionParameters[count].value[selectedActionParameters[count].value.length-1] === "$") {
|
||
// setShowDropdown(false)
|
||
//}
|
||
|
||
setUpdate(Math.random())
|
||
}}
|
||
onClick={() => {
|
||
setShowAutocomplete(true)
|
||
}}
|
||
open={showAutocomplete}
|
||
fullWidth
|
||
style={{borderBottom: `1px solid #f85a3e`, color: "white", height: 50, marginTop: 2,}}
|
||
onChange={(e) => {
|
||
const autocomplete = e.target.value.autocomplete
|
||
const newValue = autocomplete.startsWith("$") ? data.value+autocomplete : `${data.value}$${autocomplete}`
|
||
|
||
changeActionVariable(data.action_field, newValue)
|
||
}}
|
||
>
|
||
{actionlist.map(data => {
|
||
const icon = data.type === "action" ? <AppsIcon style={{marginRight: 10}} /> : data.type === "workflow_variable" || data.type === "execution_variable" ? <FavoriteBorderIcon style={{marginRight: 10}} /> : <ScheduleIcon style={{marginRight: 10}} />
|
||
|
||
const handleExecArgumentHover = (inside) => {
|
||
var exec_text_field = document.getElementById("execution_argument_input_field")
|
||
if (exec_text_field !== null) {
|
||
if (inside) {
|
||
exec_text_field.style.border = "2px solid #f85a3e"
|
||
} else {
|
||
exec_text_field.style.border = ""
|
||
}
|
||
}
|
||
|
||
// Also doing arguments
|
||
if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > 0) {
|
||
for (var key in workflow.triggers) {
|
||
const item = workflow.triggers[key]
|
||
|
||
var node = cy.getElementById(item.id)
|
||
if (node.length > 0) {
|
||
if (inside) {
|
||
node.addClass('shuffle-hover-highlight')
|
||
} else {
|
||
node.removeClass('shuffle-hover-highlight')
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleActionHover = (inside, actionId) => {
|
||
var node = cy.getElementById(actionId)
|
||
if (node.length > 0) {
|
||
if (inside) {
|
||
node.addClass('shuffle-hover-highlight')
|
||
} else {
|
||
node.removeClass('shuffle-hover-highlight')
|
||
}
|
||
}
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data.name} style={{backgroundColor: inputColor, color: "white"}} value={data} onMouseOver={() => {
|
||
if (data.type === "Execution Argument") {
|
||
handleExecArgumentHover(true)
|
||
} else if (data.type === "action") {
|
||
handleActionHover(true, data.id)
|
||
}
|
||
}} onMouseOut={() => {
|
||
if (data.type === "Execution Argument") {
|
||
handleExecArgumentHover(false)
|
||
} else if (data.type === "action") {
|
||
handleActionHover(false, data.id)
|
||
}
|
||
}}>
|
||
<Tooltip color="primary" title={`Value: ${data.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {data.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</FormControl>
|
||
}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
const menuItemStyle = {
|
||
color: "white",
|
||
backgroundColor: inputColor
|
||
}
|
||
|
||
const conditionsModal =
|
||
<Dialog modal
|
||
open={conditionsModalOpen}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
minWidth: 800,
|
||
},
|
||
}}
|
||
onClose={() => {
|
||
setConditionsModalOpen(false)
|
||
setSourceValue({})
|
||
setConditionValue({})
|
||
setDestinationValue({})
|
||
}}
|
||
>
|
||
<span style={{position: "absolute", bottom: 10, left: 10, color: "rgba(255,255,255,0.6)",}}>
|
||
Conditions can't be used for loops [ .# ] <a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#conditions" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more</a>
|
||
</span>
|
||
<FormControl>
|
||
<DialogTitle><span style={{color:"white"}}>Condition</span>
|
||
</DialogTitle>
|
||
<DialogContent style={{}}>
|
||
|
||
<div style={{display: "flex"}}>
|
||
<Tooltip color="primary" title={conditionValue.configuration ? "Negated" : "Default"} placement="top">
|
||
<span style={{margin: "auto", height: 50, marginBottom: "auto", marginTop: "auto", marginRight: 5}}>
|
||
<Button color="primary" variant={conditionValue.configuration ? "contained" : "outlined"} style={{margin: "auto", height: 50, marginBottom: "auto", marginTop: "auto", marginRight: 5}} onClick={(e) => {
|
||
conditionValue.configuration = !conditionValue.configuration
|
||
setConditionValue(conditionValue)
|
||
setUpdate(Math.random())
|
||
}}>
|
||
{conditionValue.configuration ? "!" : "="}
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<div style={{flex: "2"}}>
|
||
<AppConditionHandler tmpdata={sourceValue} setData={setSourceValue} type={"source"} />
|
||
</div>
|
||
<div style={{flex: "1", margin: "auto", marginBottom: 0, marginLeft: 5, marginRight: 5,}}>
|
||
<Button color="primary" variant="outlined" style={{margin: "auto", height: 50, marginBottom: 50,}} fullWidth aria-haspopup="true" onClick={(e) => {setVariableAnchorEl(e.currentTarget)}}>
|
||
{conditionValue.value}
|
||
</Button>
|
||
<Menu
|
||
id="simple-menu"
|
||
keepMounted
|
||
open={Boolean(variableAnchorEl)}
|
||
anchorEl={variableAnchorEl}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
|
||
onClose={() => {
|
||
setVariableAnchorEl(null)
|
||
}}
|
||
>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "equals"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"equals"}>equals</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "does not equal"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"does not equal"}>does not equal</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "startswith"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"starts with"}>starts with</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "endswith"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"ends with"}>ends with</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "contains"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"contains"}>contains</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "contains_any_of"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"contains_any_of"}>contains any of</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "matches regex"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"matches regex"}>matches regex</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "larger than"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"larger than"}>larger than</MenuItem>
|
||
<MenuItem style={menuItemStyle} onClick={(e) => {
|
||
conditionValue.value = "less than"
|
||
setConditionValue(conditionValue)
|
||
setVariableAnchorEl(null)
|
||
}} key={"less than"}>less than</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
<div style={{flex: "2"}}>
|
||
<AppConditionHandler tmpdata={destinationValue} setData={setDestinationValue} type={"destination"} />
|
||
</div>
|
||
|
||
</div>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setSelectedEdge({})
|
||
|
||
|
||
var data = {
|
||
condition: conditionValue,
|
||
source: sourceValue,
|
||
destination: destinationValue,
|
||
}
|
||
|
||
setConditionsModalOpen(false)
|
||
if (selectedEdge.conditions === undefined) {
|
||
selectedEdge.conditions = [data]
|
||
} else {
|
||
const curedgeindex = selectedEdge.conditions.findIndex(data => data.source.id === sourceValue.id)
|
||
if (curedgeindex < 0) {
|
||
selectedEdge.conditions.push(data)
|
||
} else {
|
||
selectedEdge.conditions[curedgeindex] = data
|
||
}
|
||
}
|
||
|
||
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
|
||
</Button>
|
||
</DialogActions>
|
||
</FormControl>
|
||
</Dialog>
|
||
|
||
const EdgeSidebar = () => {
|
||
const ConditionHandler = (condition, index) => {
|
||
const [open, setOpen] = React.useState(false)
|
||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||
|
||
const duplicateCondition = (conditionIndex) => {
|
||
|
||
var newEdge = JSON.parse(JSON.stringify(selectedEdge.conditions[conditionIndex]))
|
||
const newUuid = uuid.v4()
|
||
newEdge.condition.id = newUuid
|
||
newEdge.source.id = newUuid
|
||
newEdge.destination.id = newUuid
|
||
selectedEdge.conditions.push(newEdge)
|
||
|
||
setUpdate(Math.random())
|
||
}
|
||
|
||
const deleteCondition = (conditionIndex) => {
|
||
console.log(selectedEdge)
|
||
if (selectedEdge.conditions.length === 1) {
|
||
selectedEdge.conditions = []
|
||
} else {
|
||
selectedEdge.conditions.splice(conditionIndex, 1)
|
||
}
|
||
|
||
setSelectedEdge(selectedEdge)
|
||
setOpen(false)
|
||
setUpdate(Math.random())
|
||
}
|
||
|
||
const paperVariableStyle = {
|
||
minHeight: 75,
|
||
maxHeight: 75,
|
||
minWidth: "100%",
|
||
maxWidth: "100%",
|
||
marginTop: "5px",
|
||
color: "white",
|
||
backgroundColor: surfaceColor,
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
}
|
||
|
||
const menuClick = (event) => {
|
||
console.log("MENU CLICK")
|
||
setOpen(!open)
|
||
setAnchorEl(event.currentTarget)
|
||
}
|
||
|
||
return (
|
||
<Paper key={condition.condition.id} square style={paperVariableStyle} onClick={() => {}}>
|
||
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: yellow, marginRight: "5px"}} />
|
||
<div style={{display: "flex", width: "100%"}}>
|
||
<div style={{flex: "10", display: "flex"}} onClick={() => {
|
||
setSourceValue(condition.source)
|
||
setConditionValue(condition.condition)
|
||
setDestinationValue(condition.destination)
|
||
setConditionsModalOpen(true)
|
||
}}>
|
||
<div style={{flex: 1, textAlign: "left", marginTop: "15px", marginLeft: "10px", overflow: "hidden", maxWidth: 72, }}>
|
||
{condition.source.value}
|
||
</div>
|
||
<Divider style={{height: "100%", width: "1px", marginLeft: "5px", marginRight: "5px", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: 1, textAlign: "center", marginTop: "15px", overflow: "hidden", maxWidth: 72,}} onClick={() => {}}>
|
||
{condition.condition.value}
|
||
</div>
|
||
<Divider style={{height: "100%", width: "1px", marginLeft: "5px", marginRight: "5px", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: 1, textAlign: "left", marginTop: "auto", marginBottom: "auto", marginLeft: "10px", overflow: "hidden", maxWidth: 72, }}>
|
||
{condition.destination.value}
|
||
</div>
|
||
</div>
|
||
<div style={{flex: "1", marginLeft: "0px"}}>
|
||
<IconButton
|
||
aria-label="more"
|
||
aria-controls="long-menu"
|
||
aria-haspopup="true"
|
||
onClick={menuClick}
|
||
style={{color: "white",}}
|
||
>
|
||
<MoreVertIcon />
|
||
</IconButton>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={anchorEl}
|
||
keepMounted
|
||
open={open}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
}
|
||
}}
|
||
onClose={() => {
|
||
setOpen(false)
|
||
setAnchorEl(null)
|
||
}}
|
||
>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
duplicateCondition(index)
|
||
}} key={"Duplicate"}>{"Duplicate"}</MenuItem>
|
||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||
setOpen(false)
|
||
deleteCondition(index)
|
||
}} key={"Delete"}>{"Delete"}</MenuItem>
|
||
</Menu>
|
||
</div>
|
||
</div>
|
||
</Paper>
|
||
)
|
||
}
|
||
|
||
var injectedData =
|
||
<div>
|
||
</div>
|
||
|
||
if (selectedEdge.conditions !== undefined && selectedEdge.conditions !== null && selectedEdge.conditions.length > 0) {
|
||
injectedData = selectedEdge.conditions.map((condition, index) => {
|
||
return ConditionHandler(condition, index)
|
||
})
|
||
}
|
||
|
||
// FIXME - remove index
|
||
const conditionId = uuid.v4()
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}} >Branch: Conditions - {selectedEdgeIndex}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/workflows#conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Conditions
|
||
</div>
|
||
{injectedData}
|
||
|
||
<Button style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
|
||
setSourceValue({"name": "source", "value": "", "variant": "STATIC_VALUE", "action_field": "", "id": conditionId})
|
||
setConditionValue({"name": "condition", "value": "equals", "id": conditionId})
|
||
setDestinationValue({"name": "destination", "value": "", "variant": "STATIC_VALUE", "action_field": "", "id": conditionId})
|
||
setConditionsModalOpen(true)
|
||
}} fullWidth>New condition</Button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 1. GET the trigger authentication data
|
||
// 2. Parse the fields that are used (outlook & gmail)
|
||
// 3. Parse the folders that are selected
|
||
// 4. Start / stop
|
||
const EmailSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length === 0) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex] === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}]
|
||
selectedTrigger.parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}]
|
||
setWorkflow(workflow)
|
||
setSelectedTrigger(selectedTrigger)
|
||
}
|
||
|
||
const setFolders = () => {
|
||
fetch(globalUrl+"/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 =
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
style={{flex: "1",}}
|
||
color="primary"
|
||
onClick={() => {
|
||
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
|
||
</Button>
|
||
|
||
|
||
// FIXME - set everything in here to multifolder etc
|
||
var triggerInfo = "SET UP BUT NO TYPE :)"
|
||
if (Object.getOwnPropertyNames(triggerAuthentication).length > 0) {
|
||
// Should get the folders if they don't already exist
|
||
|
||
if (triggerAuthentication.type === "outlook") {
|
||
triggerInfo = <div>
|
||
{selectedTrigger.status === "running" ? null :
|
||
<span>
|
||
<div style={{marginTop: 20, marginBottom: 7, display: "flex"}}>
|
||
<div style={{width: 17, height: 17, borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: 10}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Login </b>
|
||
</div>
|
||
</div>
|
||
{outlookButton}
|
||
</span>
|
||
}
|
||
|
||
{triggerFolders === undefined || triggerFolders === null ?
|
||
null :
|
||
<span>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Select a folder </b>
|
||
</div>
|
||
</div>
|
||
<Select
|
||
multiple
|
||
native
|
||
rows="10"
|
||
value={selectedTrigger.parameters[0].value.split(splitter)}
|
||
style={{backgroundColor: inputColor, color: "white"}}
|
||
disabled={selectedTrigger.status === "running"}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
onChange={(e) => {
|
||
//setTriggerFolderWrapper(e)
|
||
setTriggerFolderWrapperMulti(e)
|
||
}}
|
||
fullWidth
|
||
input={<Input id="select-multiple-native" />}
|
||
key={selectedTrigger}
|
||
>
|
||
{triggerFolders.map(folder => {
|
||
//console.log("FOLDER: ", folder)
|
||
//var folderItem = <option key={folder.displayName} value={folder.displayName} style={{marginLeft: 10, fontSize: "1.2em", backgroundColor: inputColor,}}>
|
||
var folderItem = <option key={folder.displayName} style={{backgroundColor: inputColor, fontSize: "1.2em"}} value={folder.displayName}>
|
||
{folder.displayName}
|
||
</option>
|
||
|
||
if (folder.childFolderCount > 0) {
|
||
// Here to handle subfolders sometime later
|
||
folderItem =
|
||
<option key={folder.displayName} value={folder.displayName} style={{marginLeft: "10px", }}>
|
||
{folder.displayName}
|
||
</option>
|
||
}
|
||
|
||
return folderItem
|
||
})}
|
||
</Select>
|
||
</span>
|
||
}
|
||
</div>
|
||
} else if (triggerAuthentication.type === "gmail") {
|
||
triggerInfo = "SPECIAL GMAIL"
|
||
}
|
||
}
|
||
|
||
|
||
// Check
|
||
const argumentView = Object.getOwnPropertyNames(triggerAuthentication).length > 0 ?
|
||
<div>
|
||
{triggerInfo}
|
||
</div>
|
||
:
|
||
<div>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Login to either </b>
|
||
</div>
|
||
</div>
|
||
{outlookButton}
|
||
{/*
|
||
<Button variant="contained" style={{marginLeft: "5px", flex: "1",}} onClick={() => {
|
||
alert.error("REMOVE THIS FIELD FOR GMAIL - make it oAuth something")
|
||
}} color="primary">
|
||
Gmail
|
||
</Button>
|
||
*/}
|
||
</div>
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#email" style={{textDecoration: "none", color: "#f85a3e"}}>What are email triggers?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
|
||
<div style={{marginTop: "20px"}}>
|
||
Environment:
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
required
|
||
disabled
|
||
fullWidth
|
||
color="primary"
|
||
value={selectedTrigger.environment}
|
||
/>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
{argumentView}
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<Button style={{flex: "1",}} disabled={selectedTrigger.status === "running"} onClick={() => {
|
||
startMailSub(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Start
|
||
</Button>
|
||
<Button style={{flex: "1",}} disabled={selectedTrigger.status !== "running" } onClick={() => {
|
||
stopMailSub(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const SubflowSidebar = () => {
|
||
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 (
|
||
<Menu
|
||
anchorReference="anchorPosition"
|
||
anchorPosition={menuPosition}
|
||
onClose={() => {
|
||
handleMenuClose()
|
||
}}
|
||
open={!!menuPosition}
|
||
style={{
|
||
border: `2px solid #f85a3e`,
|
||
color: "white",
|
||
marginTop: 2,
|
||
}}
|
||
>
|
||
{actionlist.map(innerdata => {
|
||
const icon = innerdata.type === "action" ? <AppsIcon style={{marginRight: 10}} /> : innerdata.type === "workflow_variable" || innerdata.type === "execution_variable" ? <FavoriteBorderIcon style={{marginRight: 10}} /> : <ScheduleIcon style={{marginRight: 10}} />
|
||
|
||
const handleExecArgumentHover = (inside) => {
|
||
var exec_text_field = document.getElementById("execution_argument_input_field")
|
||
if (exec_text_field !== null) {
|
||
if (inside) {
|
||
exec_text_field.style.border = "2px solid #f85a3e"
|
||
} else {
|
||
exec_text_field.style.border = ""
|
||
}
|
||
}
|
||
|
||
// Also doing arguments
|
||
if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > 0) {
|
||
for (var key in workflow.triggers) {
|
||
const item = workflow.triggers[key]
|
||
|
||
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 ?
|
||
<NestedMenuItem
|
||
key={innerdata.name}
|
||
label={
|
||
<div style={{display: "flex"}}>
|
||
{icon} {innerdata.name}
|
||
</div>
|
||
}
|
||
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" ? <VpnKeyIcon style={iconStyle} /> : pathdata.type === "list" ? <FormatListNumberedIcon style={iconStyle} /> : <ExpandMoreIcon style={iconStyle} />
|
||
return (
|
||
<MenuItem key={pathdata.name} style={{backgroundColor: theme.palette.inputColor, color: "white", minWidth: 250, }} value={pathdata} onMouseOver={() => {}}
|
||
onClick={() => {
|
||
handleItemClick([innerdata, pathdata])
|
||
}}
|
||
>
|
||
<Tooltip color="primary" title={`Ex. value: ${pathdata.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {pathdata.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
)
|
||
|
||
})}
|
||
</NestedMenuItem>
|
||
:
|
||
<MenuItem key={innerdata.name} style={{backgroundColor: theme.palette.inputColor, color: "white"}} value={innerdata} onMouseOver={() => handleMouseover()} onMouseOut={() => {handleMouseOut()}}
|
||
onClick={() => {
|
||
handleItemClick([innerdata])
|
||
}}
|
||
>
|
||
<Tooltip color="primary" title={`Value: ${innerdata.value}`} placement="left">
|
||
<div style={{display: "flex"}}>
|
||
{icon} {innerdata.name}
|
||
</div>
|
||
</Tooltip>
|
||
</MenuItem>
|
||
|
||
)
|
||
})}
|
||
</Menu>
|
||
)
|
||
}
|
||
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||
if (workflow.triggers[selectedTriggerIndex] === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "workflow", "value": ""}
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "argument", "value": ""}
|
||
workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "user_apikey", "value": ""}
|
||
workflow.triggers[selectedTriggerIndex].parameters[3] = {"name": "startnode", "value": ""}
|
||
|
||
console.log("SETTINGS: ", userSettings)
|
||
if (userSettings !== undefined && userSettings !== null && userSettings.apikey !== null && userSettings.apikey !== undefined && userSettings.apikey.length > 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "user_apikey", "value": userSettings.apikey}
|
||
}
|
||
}
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#subflow" style={{textDecoration: "none", color: "#f85a3e"}}>What are subflows?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Select a workflow to execute </b>
|
||
</div>
|
||
</div>
|
||
{workflows === undefined || workflows === null || workflows.length === 0 ? null :
|
||
<Select
|
||
value={subworkflow}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setUpdate(Math.random())
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = e.target.value.id
|
||
setWorkflow(workflow)
|
||
setSubworkflowStartnode(e.target.value.start)
|
||
|
||
// Sets the startnode
|
||
if (e.target.value.id !== workflow.id) {
|
||
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 {
|
||
setSubworkflow(workflow)
|
||
}
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{workflows.map((data, index) => {
|
||
/*
|
||
if (data.id === workflow.id) {
|
||
return null
|
||
}
|
||
*/
|
||
|
||
return (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: data.id === workflow.id ? "red" : "white"}} value={data}>
|
||
{data.name}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
}
|
||
{workflow.triggers[selectedTriggerIndex].parameters[0].value.length === 0 ? null :
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value === props.match.params.key ? null :
|
||
<span style={{marginTop: 5}}><a rel="norefferer" href={`/workflows/${workflow.triggers[selectedTriggerIndex].parameters[0].value}`} target="_blank" style={{textDecoration: "none", color: "#f85a3e", marginLeft: 5,}}>Explore selected workflow</a></span>
|
||
}
|
||
|
||
{subworkflow === undefined || subworkflow === null || subworkflow.id === undefined || subworkflow.actions === null || subworkflow.actions === undefined || subworkflow.actions.length === 0 ? null :
|
||
<span>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Select the Startnode</b>
|
||
</div>
|
||
</div>
|
||
<Select
|
||
value={subworkflowStartnode}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
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)
|
||
}
|
||
}
|
||
|
||
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: "50px"}}
|
||
>
|
||
{subworkflow.actions.map((action, index) => {
|
||
//console.log(action)
|
||
return (
|
||
<MenuItem disabled={getParents(selectedTrigger).find(parent => parent.id === action.id)} key={index} style={{backgroundColor: inputColor, color: "white"}} value={action}>
|
||
{action.label}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</span>
|
||
}
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Execution Argument</b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<Tooltip title="Autocomplete text" placement="top">
|
||
<AddCircleOutlineIcon style={{cursor: "pointer"}} onClick={(event) => {
|
||
setMenuPosition({
|
||
top: event.pageY+10,
|
||
left: event.pageX+10,
|
||
})
|
||
//setShowDropdownNumber(3)
|
||
setShowDropdown(true)
|
||
setShowAutocomplete(true)
|
||
}}/>
|
||
</Tooltip>
|
||
</InputAdornment>
|
||
)
|
||
}}
|
||
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 ?
|
||
<ActionlistWrapper actionlist={actionlist} data={workflow.triggers[selectedTriggerIndex]} />
|
||
: null}
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>API-key </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
height: 50,
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder="Your apikey"
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[2].value}
|
||
onBlur={(e) => {
|
||
workflow.triggers[selectedTriggerIndex].parameters[2].value = e.target.value
|
||
setWorkflow(workflow)
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const WebhookSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||
if (workflow.triggers[selectedTriggerIndex] === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "url", "value": referenceUrl+"webhook_"+selectedTrigger.id}
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "tmp", "value": "webhook_"+selectedTrigger.id}
|
||
setWorkflow(workflow)
|
||
}
|
||
//const cronValue = "*/15 * * * *"
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
<div style={{marginTop: "20px"}}>
|
||
<Typography>
|
||
Environment
|
||
</Typography>
|
||
<Select
|
||
value={selectedTrigger.environment}
|
||
disabled={selectedTrigger.status === "running"}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
selectedTrigger.environment = e.target.value
|
||
setSelectedTrigger(selectedTrigger)
|
||
if (e.target.value === "cloud") {
|
||
var tmpvalue = workflow.triggers[selectedTriggerIndex].parameters[0].value.split("/")
|
||
const urlpath = tmpvalue.slice(3, tmpvalue.length)
|
||
const newurl = "https://shuffler.io/"+urlpath.join("/")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = newurl
|
||
} else {
|
||
var tmpvalue = workflow.triggers[selectedTriggerIndex].parameters[0].value.split("/")
|
||
const urlpath = tmpvalue.slice(3, tmpvalue.length)
|
||
const newurl = window.location.origin+"/"+urlpath.join("/")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = newurl
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{triggerEnvironments.map(data => {
|
||
if (data.archived) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Webhook URI </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
id="webhook_uri_field"
|
||
onClick={() => {
|
||
var copyText = document.getElementById("webhook_uri_field")
|
||
navigator.clipboard.writeText(copyText.value)
|
||
copyText.select()
|
||
copyText.setSelectionRange(0, 99999) /* For mobile devices */
|
||
|
||
/* Copy the text inside the text field */
|
||
document.execCommand("copy")
|
||
alert.success("Copied Webhook URL")
|
||
}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
disabled
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[0].value}
|
||
color="primary"
|
||
placeholder="defaultValue"
|
||
onBlur={(e) => {
|
||
setTriggerCronWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<Button variant="contained" style={{flex: "1",}} disabled={selectedTrigger.status === "running"} onClick={() => {
|
||
newWebhook(selectedTrigger)
|
||
}} color="primary">
|
||
Start
|
||
</Button>
|
||
<Button variant="contained" style={{flex: "1",}} disabled={selectedTrigger.status !== "running"} onClick={() => {
|
||
deleteWebhook(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
//const getTriggerAuth = (trigger_id) => {
|
||
// fetch(globalUrl+"/api/v1/triggers/"+trigger_id, {
|
||
// method: "GET",
|
||
// headers: {"content-type": "application/json"},
|
||
// credentials: "include",
|
||
// })
|
||
// .then((response) => response.json())
|
||
// .then((responseJson) => {
|
||
// if (responseJson.success) {
|
||
// console.log("SUCCESS")
|
||
// console.log(responseJson)
|
||
// } else {
|
||
// console.log("FAIL")
|
||
// }
|
||
// })
|
||
// .catch(error => {
|
||
// console.log(error.toString())
|
||
// })
|
||
//}
|
||
|
||
const stopMailSub = (trigger, triggerindex) => {
|
||
// DELETE
|
||
if (trigger.id === undefined) {
|
||
return
|
||
}
|
||
alert.info("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(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#user_input" style={{textDecoration: "none", color: "#f85a3e"}}>What is the user input trigger?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
|
||
<div style={{marginTop: "20px"}}>
|
||
Environment:
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
required
|
||
disabled
|
||
fullWidth
|
||
color="primary"
|
||
value={selectedTrigger.environment}
|
||
/>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Information: </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
marginTop: "3px",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
rows="4"
|
||
multiline
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[0].value}
|
||
color="primary"
|
||
placeholder="defaultValue"
|
||
onBlur={(e) => {
|
||
setTriggerTextInformationWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Contact options: </b>
|
||
</div>
|
||
</div>
|
||
<FormGroup style={{paddingLeft: 10, backgroundColor: inputColor}} row>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email")}
|
||
onChange={() => {
|
||
setTriggerOptionsWrapper("email")
|
||
}}
|
||
color="primary"
|
||
value="email"
|
||
/>
|
||
}
|
||
label={<div style={{color: "white"}}>Email</div>}
|
||
/>
|
||
<FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
checked={workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms")}
|
||
onChange={() => {
|
||
setTriggerOptionsWrapper("sms")
|
||
}}
|
||
color="primary"
|
||
value="sms"
|
||
/>
|
||
}
|
||
label={<div style={{color: "white"}}>SMS</div>}
|
||
/>
|
||
</FormGroup>
|
||
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") ?
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"mail1@company.com,mail2@company.com"}
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[3].value}
|
||
onBlur={(event) => {
|
||
workflow.triggers[selectedTriggerIndex].parameters[3].value = event.target.value
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
: null
|
||
}
|
||
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") ?
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"+474823212,+460203042"}
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[4].value}
|
||
onBlur={(event) => {
|
||
workflow.triggers[selectedTriggerIndex].parameters[4].value = event.target.value
|
||
setWorkflow(workflow)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
: null
|
||
}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const ScheduleSidebar = () => {
|
||
if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) {
|
||
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
|
||
workflow.triggers[selectedTriggerIndex].parameters = []
|
||
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "cron", "value": isCloud ? "*/15 * * * *" : "120"}
|
||
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "execution_argument", "value": '{"example": {"json": "is cool"}}'}
|
||
setWorkflow(workflow)
|
||
}
|
||
|
||
return(
|
||
<div style={appApiViewStyle}>
|
||
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
|
||
<div style={{flex: "1"}}>
|
||
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
|
||
<a rel="norefferer" target="_blank" href="https://shuffler.io/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</a>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div>
|
||
Name
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={selectedTrigger.label}
|
||
onChange={selectedTriggerChange}
|
||
/>
|
||
<div style={{marginTop: "20px"}}>
|
||
<Typography>
|
||
Environment
|
||
</Typography>
|
||
<Select
|
||
value={selectedTrigger.environment}
|
||
disabled={selectedTrigger.status === "running"}
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
|
||
}
|
||
}}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
selectedTrigger.environment = e.target.value
|
||
setSelectedTrigger(selectedTrigger)
|
||
if (e.target.value === "cloud") {
|
||
console.log("Set cloud config")
|
||
workflow.triggers[selectedTriggerIndex].parameters[0].value = "*/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: "50px"}}
|
||
>
|
||
{triggerEnvironments.map(data => {
|
||
if (data.archived) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={data} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</div>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{flex: "6", marginTop: "20px"}}>
|
||
<div>
|
||
<b>Parameters</b>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Interval (seconds) </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
disabled={workflow.triggers[selectedTriggerIndex].status === "running"}
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[0].value}
|
||
color="primary"
|
||
placeholder="defaultValue"
|
||
onBlur={(e) => {
|
||
setTriggerCronWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
|
||
<div style={{flex: "10"}}>
|
||
<b>Execution argument: </b>
|
||
</div>
|
||
</div>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
marginTop: "3px",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
disabled={workflow.triggers[selectedTriggerIndex].status === "running"}
|
||
fullWidth
|
||
rows="6"
|
||
multiline
|
||
color="primary"
|
||
defaultValue={workflow.triggers[selectedTriggerIndex].parameters[1].value}
|
||
placeholder='{"example": {"json": "is cool"}}'
|
||
onBlur={(e) => {
|
||
setTriggerBodyWrapper(e.target.value)
|
||
}}
|
||
/>
|
||
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
|
||
<Button style={{flex: "1",}} variant="contained" disabled={selectedTrigger.status === "running"} onClick={() => {
|
||
submitSchedule(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Start
|
||
</Button>
|
||
<Button style={{flex: "1",}} variant="contained" disabled={selectedTrigger.status !== "running"} onClick={() => {
|
||
stopSchedule(selectedTrigger, selectedTriggerIndex)
|
||
}} color="primary">
|
||
Stop
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const cytoscapeViewWidths = 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 = () => {
|
||
return (
|
||
<div style={topBarStyle}>
|
||
<div style={{margin: "0px 10px 0px 10px", }}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white",}}>
|
||
<Link to="/workflows" style={{textDecoration: "none", color: "inherit",}}>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)", margin: "0px 0px 0px 0px"}}>
|
||
<PolymerIcon style={{marginRight: 10}} />
|
||
Workflows
|
||
</h2>
|
||
</Link>
|
||
<h2 style={{margin: 0,}}>
|
||
{workflow.name}
|
||
</h2>
|
||
|
||
{workflow.public ?
|
||
<h2 style={{margin: 0,}}>
|
||
Public Workflow PREVIEW
|
||
</h2>
|
||
:
|
||
null
|
||
}
|
||
</Breadcrumbs>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const NoActionsBar = () => {
|
||
if (workflow.actions.length === 0 || !appAdded) {
|
||
return (
|
||
<div style={{position: "fixed", width: "100%", top: 300}}>
|
||
<div style={{width: 300, margin: "auto"}}>
|
||
<h3></h3>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const FileMenu = () => {
|
||
const [newAnchor, setNewAnchor] = React.useState(null)
|
||
const [showShuffleMenu, setShowShuffleMenu] = React.useState(false)
|
||
|
||
{ /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */}
|
||
return (
|
||
<div style={{"display": "inline-block"}}>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={newAnchor}
|
||
open={showShuffleMenu}
|
||
onClose={() => {
|
||
setShowShuffleMenu(false)
|
||
}}
|
||
>
|
||
<div style={{margin: 15, color: "white", maxWidth: 250, minWidth: 250, }}>
|
||
<h4>This menu is used to control the workflow itself.</h4>
|
||
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Exit on Error</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.exit_on_error} onChange={() => {
|
||
workflow.configuration.exit_on_error = !workflow.configuration.exit_on_error
|
||
setWorkflow(workflow)
|
||
setUpdate("exit_on_error_"+workflow.configuration.exit_on_error ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Start from top</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.start_from_top} onChange={() => {
|
||
workflow.configuration.start_from_top = !workflow.configuration.start_from_top
|
||
setWorkflow(workflow)
|
||
setUpdate("start_from_top_"+workflow.configuration.start_from_top ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
</div>
|
||
</Menu>
|
||
<Tooltip color="secondary" title="Workflow settings" placement="top-start">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={(event) => {
|
||
setShowShuffleMenu(!showShuffleMenu)
|
||
setNewAnchor(event.currentTarget)
|
||
}}>
|
||
<SettingsIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const WorkflowMenu = () => {
|
||
const [newAnchor, setNewAnchor] = React.useState(null)
|
||
const [showShuffleMenu, setShowShuffleMenu] = React.useState(false)
|
||
|
||
{ /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */}
|
||
return (
|
||
<div style={{"display": "inline-block"}}>
|
||
<Menu
|
||
id="long-menu"
|
||
anchorEl={newAnchor}
|
||
open={showShuffleMenu}
|
||
onClose={() => {
|
||
setShowShuffleMenu(false)
|
||
}}
|
||
>
|
||
<div style={{margin: 15, color: "white", maxWidth: 250, minWidth: 250, }}>
|
||
<h4>This menu is used to control the workflow itself.</h4>
|
||
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Exit on Error</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.exit_on_error} onChange={() => {
|
||
workflow.configuration.exit_on_error = !workflow.configuration.exit_on_error
|
||
setWorkflow(workflow)
|
||
setUpdate("exit_on_error_"+workflow.configuration.exit_on_error ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
<FormControlLabel
|
||
style={{marginBottom: 15, color: "white",}}
|
||
label={<div style={{color: "white"}}>Start from top</div>}
|
||
control={
|
||
<Switch checked={workflow.configuration.start_from_top} onChange={() => {
|
||
workflow.configuration.start_from_top = !workflow.configuration.start_from_top
|
||
setWorkflow(workflow)
|
||
setUpdate("start_from_top_"+workflow.configuration.start_from_top ? "true" : "false")
|
||
setShowShuffleMenu(false)
|
||
}} />
|
||
}
|
||
/>
|
||
</div>
|
||
</Menu>
|
||
<Tooltip color="secondary" title="Workflow settings" placement="top-start">
|
||
<span>
|
||
<Button disabled={workflow.public} color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={(event) => {
|
||
setShowShuffleMenu(!showShuffleMenu)
|
||
setNewAnchor(event.currentTarget)
|
||
}}>
|
||
<SettingsIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const BottomCytoscapeBar = () => {
|
||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||
|
||
const boxSize = 100
|
||
const executionButton = executionRunning ?
|
||
<Tooltip color="primary" title="Stop execution" placement="top">
|
||
<span>
|
||
<Button style={{height: boxSize, width: boxSize}} color="secondary" variant="contained" onClick={() => {
|
||
abortExecution()
|
||
}}>
|
||
<PauseIcon style={{ fontSize: 60}} />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
:
|
||
<Tooltip color="primary" title="Test Execution" placement="top">
|
||
<span>
|
||
<Button disabled={workflow.public || executionRequestStarted || !workflow.isValid} style={{height: boxSize, width: boxSize}} color="primary" variant="contained" onClick={() => {
|
||
executeWorkflow(executionText, workflow.start)
|
||
}}>
|
||
<PlayArrowIcon style={{ fontSize: 60}} />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
|
||
return(
|
||
<div style={bottomBarStyle}>
|
||
{executionButton}
|
||
<div style={{marginLeft: "10px", left: boxSize, bottom: 0, position: "absolute"}}>
|
||
{workflow.public ? null :
|
||
<Tooltip color="primary" title="An argument to be used for execution. This is a variable available to every node in your workflow." placement="top">
|
||
<TextField
|
||
id="execution_argument_input_field"
|
||
style={theme.palette.textFieldStyle}
|
||
disabled={workflow.public}
|
||
InputProps={{
|
||
style: theme.palette.innerTextfieldStyle,
|
||
}}
|
||
color="secondary"
|
||
placeholder={"Execution Argument"}
|
||
defaultValue={executionText}
|
||
onBlur={(e) => {
|
||
setExecutionText(e.target.value)
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
}
|
||
<Tooltip color="primary" title="Save (ctrl+s)" placement="top">
|
||
<span>
|
||
<Button disabled={savingState !== 0} color="primary" style={{height: workflow.public ? 100 : 50, width: workflow.public ? 100 : 64, marginLeft: 10, }} variant={lastSaved && !(workflow.public) ? "outlined" : "contained"} onClick={() => saveWorkflow()}>
|
||
{savingState === 2 ? <CircularProgress style={{height: 35, width: 35}} /> : savingState === 1 ? <DoneIcon style={{color: green}} /> : <SaveIcon /> }
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
{workflow.public ?
|
||
<Tooltip color="secondary" title="Download workflow" placement="top-start">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
||
//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()
|
||
}}>
|
||
<GetAppIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
: null}
|
||
<Tooltip color="secondary" title="Fit to screen (ctrl+f)" placement="top">
|
||
<span>
|
||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => cy.fit(null, 50)}>
|
||
<AspectRatioIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<Tooltip color="secondary" title="Remove selected item (del)" placement="top-start">
|
||
<span>
|
||
<Button color="primary" disabled={workflow.public} style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
||
removeNode()
|
||
}}>
|
||
<DeleteIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
<Tooltip color="secondary" title="Show executions" placement="top-start">
|
||
<span>
|
||
<Button disabled={workflow.public} color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
||
setExecutionModalOpen(true)
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
}}>
|
||
<DirectionsRunIcon />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
{/* <FileMenu /> */}
|
||
{workflow.configuration !== null && workflow.configuration !== undefined && workflow.configuration.exit_on_error !== undefined ? <WorkflowMenu /> : null}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const RightSideBar = (props) => {
|
||
if (!rightSideBarOpen) {
|
||
return null
|
||
}
|
||
|
||
if (Object.getOwnPropertyNames(selectedAction).length > 0) {
|
||
if (Object.getOwnPropertyNames(selectedAction).length === 0) {
|
||
return null
|
||
}
|
||
|
||
console.time('ACTIONSTART')
|
||
return(
|
||
<div id="rightside_actions" style={rightsidebarStyle} onLoad={() => {
|
||
console.log("LOADED RIGHTSIDE")
|
||
}}>
|
||
HELO
|
||
</div>
|
||
)
|
||
} else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
|
||
if (selectedTrigger.trigger_type === "SCHEDULE") {
|
||
//console.log("SCHEDULE")
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<ScheduleSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "WEBHOOK") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<WebhookSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "SUBFLOW") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<SubflowSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "EMAIL") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<EmailSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === "USERINPUT") {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<UserinputSidebar />
|
||
</div>
|
||
)
|
||
} else if (selectedTrigger.trigger_type === undefined) {
|
||
return null
|
||
} else {
|
||
console.log("Unable to handle invalid trigger type "+selectedTrigger.trigger_type)
|
||
return null
|
||
}
|
||
} else if (Object.getOwnPropertyNames(selectedEdge).length > 0) {
|
||
return(
|
||
<div style={rightsidebarStyle}>
|
||
<EdgeSidebar />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return(
|
||
null
|
||
)
|
||
}
|
||
|
||
// This can execute a workflow with firestore. Used for test, as datastore is old and stuff
|
||
// Too much work to move everything over alone, so won't touch it for now
|
||
//<Button style={{borderRadius: "0px"}} color="primary" variant="contained" onClick={() => {
|
||
// executeWorkflowWebsocket()
|
||
//}}>Execute websocket</Button>
|
||
//
|
||
|
||
const leftView = leftViewOpen ?
|
||
<div style={{minWidth: leftBarSize, maxWidth: leftBarSize, borderRight: "1px solid rgb(91, 96, 100)"}}>
|
||
<HandleLeftView />
|
||
</div>
|
||
:
|
||
<div style={{minWidth: leftBarSize, maxWidth: leftBarSize, borderRight: "1px solid rgb(91, 96, 100)"}}>
|
||
<div style={{cursor: "pointer", height: 20, marginTop: 10, marginLeft: 10}} onClick={() => {
|
||
setLeftViewOpen(true)
|
||
setLeftBarSize(350)
|
||
}}>
|
||
<Tooltip color="primary" title="Maximize" placement="top">
|
||
<KeyboardArrowRightIcon />
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
const executionPaperStyle = {
|
||
minWidth: "95%",
|
||
maxWidth: "95%",
|
||
marginTop: "5px",
|
||
color: "white",
|
||
marginBottom: 10,
|
||
padding: 5,
|
||
backgroundColor: surfaceColor,
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
minHeight: 40,
|
||
maxHeight: 40,
|
||
}
|
||
|
||
const parsedExecutionArgument = () => {
|
||
var showResult = executionData.execution_argument.trim()
|
||
const validate = validateJson(showResult)
|
||
|
||
|
||
if (validate.valid) {
|
||
if (typeof(validate.result) === "string") {
|
||
try {
|
||
validate.result = JSON.parse(validate.result)
|
||
} catch(e) {
|
||
console.log("Error: ", e)
|
||
validate.valid = false
|
||
}
|
||
}
|
||
|
||
return (
|
||
<ReactJson
|
||
src={validate.result}
|
||
theme="solarized"
|
||
collapsed={true}
|
||
displayDataTypes={false}
|
||
enableClipboard={(copy) => {
|
||
handleReactJsonClipboard(copy)
|
||
}}
|
||
onSelect={(select) => {
|
||
HandleJsonCopy(showResult, select, "exec")
|
||
console.log("SELECTED!: ", select)
|
||
}}
|
||
name={"Execution Argument"}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<h3>Execution Argument</h3>
|
||
<div style={{maxHeight: 200, overflowY: "scroll",}}>
|
||
{executionData.execution_argument}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
const getExecutionSourceImage = (execution) => {
|
||
// This is the playbutton at 150x150
|
||
const defaultImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg=="
|
||
const size = 40
|
||
if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) {
|
||
return <img alt="default" src={defaultImage} style={{width: size, height: size}} />
|
||
}
|
||
|
||
if (execution.execution_source === "webhook") {
|
||
return <img alt={"webhook"} src={triggers.find(trigger => trigger.trigger_type === "WEBHOOK").large_image} style={{width: size, height: size}} />
|
||
} else if (execution.execution_source === "outlook") {
|
||
return <img alt={"email"} src={triggers.find(trigger => trigger.trigger_type === "EMAIL").large_image} style={{width: size, height: size}} />
|
||
} else if (execution.execution_source === "schedule") {
|
||
return <img alt={"schedule"} src={triggers.find(trigger => trigger.trigger_type === "SCHEDULE").large_image} style={{width: size, height: size}} />
|
||
} else if (execution.execution_source === "EMAIL") {
|
||
return <img alt={"email"} src={triggers.find(trigger => trigger.trigger_type === "EMAIL").large_image} style={{width: size, height: size}} />
|
||
}
|
||
|
||
if (execution.execution_parent !== null && execution.execution_parent !== undefined && execution.execution_parent.length > 0) {
|
||
return <img alt={"parent workflow"} src={triggers.find(trigger => trigger.trigger_type === "SUBFLOW").large_image} style={{width: size, height: size}} />
|
||
}
|
||
|
||
return (
|
||
<img alt={execution.execution_source} src={defaultImage} style={{width: size, height: size}} />
|
||
)
|
||
}
|
||
|
||
const handleReactJsonClipboard = (copy) => {
|
||
console.log("COPY: ", copy)
|
||
|
||
const elementName = "copy_element_shuffle"
|
||
var copyText = document.getElementById(elementName);
|
||
if (copyText !== null && copyText !== undefined) {
|
||
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")
|
||
}
|
||
}
|
||
|
||
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 =
|
||
<Drawer anchor={"right"} open={executionModalOpen} onClose={() => setExecutionModalOpen(false)} style={{resize: "both", overflow: "auto",}} PaperProps={{style: {resize: "both", overflow: "auto", minWidth: 400, maxWidth: 400, backgroundColor: "#1F2023", color: "white", fontSize: 18}}}>
|
||
{executionModalView === 0 ?
|
||
<div style={{padding: 25, }}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white", fontSize: 16}}>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
|
||
<DirectionsRunIcon style={{marginRight: 10}} />
|
||
All Executions
|
||
</h2>
|
||
</Breadcrumbs>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
}} color="primary">
|
||
<CachedIcon style={{marginRight: 10}}/>
|
||
Refresh executions
|
||
</Button>
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.5)", marginTop: 10, marginBottom: 10,}}/>
|
||
{workflowExecutions.length > 0 ?
|
||
<div>
|
||
{workflowExecutions.map((data, index) => {
|
||
const statusColor = data.status === "FINISHED" ? green : data.status === "ABORTED" || data.status === "FAILED" ? "red" : 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 (
|
||
<Tooltip key={data.execution_id} title={data.result} placement="left-start">
|
||
<Paper elevation={5} key={data.execution_id} square style={executionPaperStyle} onMouseOver={() => {}} onMouseOut={() => {}} onClick={() => {
|
||
|
||
if ((data.result === undefined || data.result === null || data.result.length === 0) && 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)
|
||
}}>
|
||
<div style={{display: "flex", flex: 1}}>
|
||
<div style={{marginLeft: 0, width: lastExecution === data.execution_id ? 4 : 2, backgroundColor: statusColor, marginRight: 5}} />
|
||
<div style={{height: "100%", width: 40, borderColor: "white", marginRight: 15}}>
|
||
{getExecutionSourceImage(data)}
|
||
</div>
|
||
<div style={{ marginTop: "auto", marginBottom: "auto", marginRight: 15, fontSize: 13, }}>
|
||
{timestamp}
|
||
</div>
|
||
{data.workflow.actions !== null ?
|
||
<Tooltip color="primary" title={resultsLength+" actions ran"} placement="top">
|
||
<div style={{marginRight: 10, marginTop: "auto", marginBottom: "auto",}}>
|
||
{resultsLength}/{calculatedResult}
|
||
</div>
|
||
</Tooltip>
|
||
: null}
|
||
</div>
|
||
<Tooltip title={"Inspect execution"} placement="top">
|
||
{lastExecution === data.execution_id ?
|
||
<KeyboardArrowRightIcon style={{color: "#f85a3e", marginTop: "auto", marginBottom: "auto"}}/>
|
||
:
|
||
<KeyboardArrowRightIcon style={{marginTop: "auto", marginBottom: "auto"}}/>
|
||
}
|
||
</Tooltip>
|
||
</Paper>
|
||
</Tooltip>
|
||
)
|
||
return
|
||
})}
|
||
</div>
|
||
:
|
||
<div>
|
||
There are no executions yet
|
||
</div>
|
||
}
|
||
</div>
|
||
:
|
||
<div style={{padding: 25, maxWidth: 365, overflowX: "hidden",}}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white", fontSize: 16}}>
|
||
<span style={{color: "rgba(255,255,255,0.5)", display: "flex"}} onClick={() => {
|
||
setExecutionRunning(false)
|
||
stop()
|
||
getWorkflowExecution(props.match.params.key, "")
|
||
setExecutionModalView(0)
|
||
setLastExecution(executionData.execution_id)
|
||
}}>
|
||
<IconButton style={{paddingLeft: 0, marginTop: "auto", marginBottom: "auto", }} onClick={() => {}}>
|
||
<ArrowBackIcon style={{color: "rgba(255,255,255,0.5)",}} />
|
||
</IconButton>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)", cursor: "pointer"}} onClick={() => {
|
||
}}>
|
||
See other Executions
|
||
</h2>
|
||
</span>
|
||
</Breadcrumbs>
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.6)", marginTop: 10, marginBottom: 10,}}/>
|
||
<div style={{display: "flex"}}>
|
||
<h2>Executing Workflow</h2>
|
||
<Tooltip color="primary" title="Rerun workflow" placement="top">
|
||
<span style={{}}>
|
||
<Button color="primary" style={{float: "right", marginTop: 20, marginLeft: 10,}} onClick={() => {
|
||
console.log("DATA: ", executionData)
|
||
executeWorkflow(executionData.execution_argument, executionData.start)
|
||
setExecutionModalOpen(false)
|
||
//executionText, workflow.start)
|
||
}}>
|
||
<CachedIcon style={{}}/>
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
{executionData.status === "EXECUTING" ?
|
||
<Tooltip color="primary" title="Abort workflow" placement="top">
|
||
<span style={{}}>
|
||
<Button color="primary" style={{float: "right", marginTop: 20, marginLeft: 10,}} onClick={() => {
|
||
abortExecution()
|
||
}}>
|
||
<PauseIcon style={{}} />
|
||
</Button>
|
||
</span>
|
||
</Tooltip>
|
||
: null}
|
||
</div>
|
||
{executionData.status !== undefined && executionData.status.length > 0 ?
|
||
<div style={{display: "flex"}}>
|
||
<Typography variant="body1">
|
||
<b>Status </b>
|
||
</Typography>
|
||
<Typography variant="body1" color="textSecondary">
|
||
{executionData.status}
|
||
</Typography>
|
||
</div>
|
||
: null
|
||
}
|
||
{executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ?
|
||
<div style={{display: "flex"}}>
|
||
<Typography variant="body1">
|
||
<b>Source </b>
|
||
</Typography>
|
||
<Typography variant="body1" color="textSecondary">
|
||
{executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ?
|
||
executionData.execution_source === props.match.params.key ?
|
||
|
||
<span style={{cursor: "pointer", color: "#f85a3e"}} onClick={(event) => {
|
||
getWorkflowExecution(props.match.params.key, executionData.execution_parent)
|
||
}}>
|
||
Parent Execution
|
||
</span>
|
||
|
||
:
|
||
<a rel="norefferer" href={`/workflows/${executionData.execution_source}?view=executions&execution_id=${executionData.execution_parent}`} target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>Parent Workflow</a>
|
||
:
|
||
executionData.execution_source
|
||
}
|
||
</Typography>
|
||
</div>
|
||
: null
|
||
}
|
||
{executionData.started_at !== undefined ?
|
||
<div style={{display: "flex"}}>
|
||
<Typography variant="body1">
|
||
<b>Started </b>
|
||
</Typography>
|
||
<Typography variant="body1" color="textSecondary">
|
||
{new Date(executionData.started_at*1000).toISOString()}
|
||
</Typography>
|
||
</div>
|
||
: null
|
||
}
|
||
{executionData.completed_at !== undefined && executionData.completed_at !== null && executionData.completed_at > 0 ?
|
||
<div style={{display: "flex"}}>
|
||
<Typography variant="body1">
|
||
<b>Finished </b>
|
||
</Typography>
|
||
<Typography variant="body1" color="textSecondary">
|
||
{new Date(executionData.completed_at*1000).toISOString()}
|
||
</Typography>
|
||
</div>
|
||
: null
|
||
}
|
||
<div style={{marginTop: 10}}/>
|
||
{executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ?
|
||
parsedExecutionArgument()
|
||
: null }
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.6)", marginTop: 15, marginBottom: 30,}}/>
|
||
{executionData.results !== undefined && executionData.results !== null && executionData.results.length > 1 && executionData.results.find(result => result.status === "SKIPPED" || result.status === "FAILURE") ?
|
||
<FormControlLabel
|
||
style={{color: "white", marginBottom: 10, }}
|
||
label={<div style={{color: "white"}}>Show failed / skipped actions</div>}
|
||
control={
|
||
<Switch checked={showSkippedActions} onChange={() => {setShowSkippedActions(!showSkippedActions)}} />
|
||
}
|
||
/>
|
||
:
|
||
null
|
||
}
|
||
<div style={{display: "flex", marginTop: 10, marginBottom: 30,}}>
|
||
|
||
<b>Actions</b>
|
||
<div>
|
||
{executionData.status !== undefined && executionData.status !== "ABORTED" && executionData.status !== "FINISHED" && executionData.status !== "FAILURE" && executionData.status !== "WAITING" && !(executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING")? <CircularProgress style={{marginLeft: 20}}/> : null}
|
||
</div>
|
||
</div>
|
||
{executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ?
|
||
<CircularProgress />
|
||
:
|
||
executionData.results.map((data, index) => {
|
||
if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) {
|
||
return null
|
||
}
|
||
|
||
// FIXME: The latter replace doens't really work if ' is used in a string
|
||
var showResult = data.result.trim()
|
||
const validate = validateJson(showResult)
|
||
|
||
const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version)
|
||
const imgsize = 50
|
||
const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? green : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : 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 :
|
||
<img alt={data.action.app_name} src={imgSrc} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, borderRadius: executionData.start === data.action.id ? 25 : 5}} />
|
||
|
||
if (triggers.length > 2) {
|
||
if (data.action.app_name === "shuffle-subflow") {
|
||
actionimg = <img alt={"Shuffle Subflow"} src={triggers[1].large_image} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, borderRadius: executionData.start === data.action.id ? 25 : 5}} />
|
||
}
|
||
|
||
if (data.action.app_name === "User Input") {
|
||
actionimg = <img alt={"Shuffle Subflow"} src={triggers[2].large_image} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, borderRadius: executionData.start === data.action.id ? 25 : 5}} />
|
||
}
|
||
}
|
||
|
||
if (validate.valid && typeof(validate.result) === "string") {
|
||
validate.result = JSON.parse(validate.result)
|
||
}
|
||
|
||
return (
|
||
<div key={index} style={{marginBottom: 40,}} onMouseOver={() => {
|
||
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')
|
||
}
|
||
}}>
|
||
<div style={{display: "flex", marginBottom: 15,}}>
|
||
<IconButton style={{marginTop: "auto", marginBottom: "auto", height: 30, paddingLeft: 0, width: 30}} onClick={() => {
|
||
setSelectedResult(data)
|
||
setCodeModalOpen(true)
|
||
}}>
|
||
<Tooltip color="primary" title="Expand result window" placement="top">
|
||
<ArrowLeftIcon style={{color: "white"}}/>
|
||
</Tooltip>
|
||
</IconButton>
|
||
{actionimg}
|
||
<div>
|
||
<div style={{fontSize: 24, marginTop: "auto", marginBottom: "auto"}}><b>{data.action.label}</b></div>
|
||
<div style={{fontSize: 14}}>
|
||
<Typography variant="body2" color="textSecondary">
|
||
{data.action.name}
|
||
</Typography>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{marginBottom: 5, display: "flex",}}>
|
||
<Typography variant="body1">
|
||
<b>Status </b>
|
||
</Typography>
|
||
<Typography variant="body1" color="textSecondary">
|
||
{data.status}
|
||
</Typography>
|
||
</div>
|
||
{validate.valid ? <span>
|
||
<ReactJson
|
||
src={validate.result}
|
||
theme="solarized"
|
||
collapsed={true}
|
||
enableClipboard={(copy) => {
|
||
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 ?
|
||
<span>
|
||
{validate.valid && data.action.parameters !== undefined && data.action.parameters !== null ?
|
||
data.action.parameters[0].value === props.match.params.key ?
|
||
<span style={{cursor: "pointer", color: "#f85a3e"}} onClick={(event) => {
|
||
getWorkflowExecution(props.match.params.key, validate.result.execution_id)
|
||
}}>
|
||
See sub-execution
|
||
</span>
|
||
:
|
||
<a rel="norefferer" href={`/workflows/${data.action.parameters[0].value}?view=executions&execution_id=${validate.result.execution_id}`} target="_blank" style={{textDecoration: "none", color: "#f85a3e"}} onClick={(event) => {
|
||
}}>See subflow execution</a>
|
||
:
|
||
"TBD: Load subexecution result for"
|
||
}
|
||
</span>
|
||
: null
|
||
}
|
||
</span>
|
||
:
|
||
<div style={{maxHeight: 250, overflowX: "hidden", overflowY: "auto", whiteSpace: "pre-wrap", }}>
|
||
<Typography variant="body1" style={{display: 'inline-block'}}>
|
||
<b>Result</b>
|
||
</Typography>
|
||
<Typography variant="body1" color="textSecondary" style={{display: 'inline-block'}}>
|
||
{data.result}
|
||
</Typography>
|
||
</div>
|
||
}
|
||
</div>
|
||
)
|
||
})
|
||
}
|
||
</div>
|
||
}
|
||
</Drawer>
|
||
|
||
// This sucks :)
|
||
const curapp = !codeModalOpen ? {} : selectedResult.action.app_name === "shuffle-subflow" ? triggers[1] : selectedResult.action.app_name === "User Input" ? triggers[2] : apps.find(a => a.name === selectedResult.action.app_name && a.app_version === selectedResult.action.app_version)
|
||
const imgsize = 50
|
||
const statusColor = !codeModalOpen ? "red" : selectedResult.status === "FINISHED" || selectedResult.status === "SUCCESS" ? green : selectedResult.status === "ABORTED" || selectedResult.status === "FAILURE" ? "red" : 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 :
|
||
<Draggable
|
||
onDrag={(e) => {
|
||
if (!dragging) {
|
||
setDragging(true)
|
||
}
|
||
}}
|
||
onStop={(e) => {
|
||
if (!dragging) {
|
||
return
|
||
}
|
||
|
||
setDragging(false)
|
||
|
||
const newoffsetX = parseInt(dragPosition.x)-parseInt(e.layerX-e.offsetX)
|
||
const newoffsetY = parseInt(dragPosition.y)-parseInt(e.layerY-e.offsetY)
|
||
if ((newoffsetX <= 40 && newoffsetX >= -40) && (newoffsetY <= 40 && newoffsetY >= -40)) {
|
||
console.log("SKIP X & Y")
|
||
return
|
||
}
|
||
|
||
setDragPosition({
|
||
x: e.layerX-e.offsetX,
|
||
y: e.layerY-e.offsetY,
|
||
})
|
||
}}
|
||
position={dragPosition}
|
||
>
|
||
<Dialog
|
||
disableEnforceFocus={true}
|
||
style={{pointerEvents: "none"}}
|
||
hideBackdrop={true}
|
||
open={codeModalOpen}
|
||
PaperProps={{
|
||
style: {
|
||
pointerEvents: "auto",
|
||
backgroundColor: inputColor,
|
||
color: "white",
|
||
minWidth: 750,
|
||
padding: 30,
|
||
maxHeight: 700,
|
||
overflowY: "auto",
|
||
overflowX: "hidden",
|
||
//boxShadow: "none",
|
||
},
|
||
}}
|
||
>
|
||
<Tooltip title="Find successful execution" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 170,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
for (var key in workflowExecutions) {
|
||
const execution = workflowExecutions[key]
|
||
//console.log(execution.results[0])
|
||
const result = execution.results.find(data => data.status === "SUCCESS" && data.action.id === selectedResult.action.id)
|
||
if (result !== undefined) {
|
||
setSelectedResult(result)
|
||
setUpdate(Math.random())
|
||
break
|
||
}
|
||
}
|
||
}}>
|
||
<DoneIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Find failed execution" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 136,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
for (var key in workflowExecutions) {
|
||
const execution = workflowExecutions[key]
|
||
//console.log(execution.results[0])
|
||
const result = execution.results.find(data => data.action.id === selectedResult.action.id && data.status !== "SUCCESS" && data.status !== "SKIPPED" && data.status !== "WAITING")
|
||
if (result !== undefined) {
|
||
setSelectedResult(result)
|
||
setUpdate(Math.random())
|
||
break
|
||
}
|
||
}
|
||
}}>
|
||
<ErrorIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Explore execution" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 98,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
const executionIndex = workflowExecutions.findIndex(data => data.execution_id === selectedResult.execution_id)
|
||
if (executionIndex !== -1) {
|
||
setExecutionModalOpen(true)
|
||
setExecutionModalView(1)
|
||
setExecutionData(workflowExecutions[executionIndex])
|
||
}
|
||
}}>
|
||
<VisibilityIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Close window" placement="top">
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 34, right: 34,}} onClick={(e) => {
|
||
e.preventDefault()
|
||
//console.log("CLICKING EXIT")
|
||
setCodeModalOpen(false)
|
||
}}>
|
||
<CloseIcon style={{color: "white"}}/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
<div style={{marginBottom: 40,}}>
|
||
<div style={{display: "flex", marginBottom: 15,}}>
|
||
{curapp === null ? null : <img alt={selectedResult.app_name} src={curapp === undefined ? "" : curapp.large_image} style={{marginRight: 20, width: imgsize, height: imgsize, border: `2px solid ${statusColor}`}} />}
|
||
|
||
<div>
|
||
<div style={{fontSize: 24, marginTop: "auto", marginBottom: "auto"}}><b>{selectedResult.action.label}</b></div>
|
||
<div style={{fontSize: 14}}>{selectedResult.action.name}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{marginBottom: 5}}><b>Status </b> {selectedResult.status}</div>
|
||
{validate.valid ? <ReactJson
|
||
src={validate.result}
|
||
theme="solarized"
|
||
collapsed={false}
|
||
displayDataTypes={false}
|
||
enableClipboard={(copy) => {
|
||
handleReactJsonClipboard(copy)
|
||
}}
|
||
onSelect={(select) => {
|
||
HandleJsonCopy(JSON.stringify(validate.result), select, selectedResult.action.label)
|
||
}}
|
||
name={"Results for "+selectedResult.action.label}
|
||
/>
|
||
:
|
||
<div>
|
||
<b>Result</b>
|
||
<span onClick={() => {
|
||
console.log("IN HERE TO CLICK")
|
||
to_be_copied = selectedResult.result
|
||
var copyText = document.getElementById("copy_element_shuffle");
|
||
console.log("PRECOPY: ", to_be_copied)
|
||
if (copyText !== null && copyText !== undefined) {
|
||
console.log("COPY: ", copyText)
|
||
navigator.clipboard.writeText(to_be_copied)
|
||
copyText.select();
|
||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||
|
||
/* Copy the text inside the text field */
|
||
document.execCommand("copy");
|
||
alert.success("Copied "+to_be_copied)
|
||
} else {
|
||
console.log("Failed to copy. copy_element_shuffle is undefined")
|
||
}
|
||
}}>{selectedResult.result}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</Dialog>
|
||
</Draggable>
|
||
|
||
const newView = //isLoggedIn ?
|
||
<div style={{color: "white"}}>
|
||
<div style={{display: "flex", borderTop: "1px solid rgba(91, 96, 100, 1)"}}>
|
||
{leftView}
|
||
<CytoscapeComponent
|
||
elements={elements}
|
||
minZoom={0.35}
|
||
maxZoom={2.00}
|
||
style={{width: bodyWidth-leftBarSize-15, height: bodyHeight-appBarSize-5, backgroundColor: surfaceColor}}
|
||
stylesheet={cystyle}
|
||
boxSelectionEnabled={true}
|
||
autounselectify={false}
|
||
showGrid={true}
|
||
id="cytoscape_view"
|
||
cy={(incy) => {
|
||
// FIXME: There's something specific loading when
|
||
// you do the first hover of a node. Why is this different?
|
||
//console.log("CY: ", incy)
|
||
setCy(incy)
|
||
}}
|
||
/>
|
||
</div>
|
||
{executionModal}
|
||
|
||
{rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length !== 0 ?
|
||
<div id="rightside_actions" style={rightsidebarStyle}>
|
||
<ParsedAction
|
||
selectedAction={selectedAction}
|
||
workflow={workflow}
|
||
setWorkflow={setWorkflow}
|
||
setSelectedAction={setSelectedAction}
|
||
setUpdate={setUpdate}
|
||
selectedApp={selectedApp}
|
||
workflowExecutions={workflowExecutions}
|
||
setSelectedResult={setSelectedResult}
|
||
setSelectedApp={setSelectedApp}
|
||
setSelectedTrigger={setSelectedTrigger}
|
||
setSelectedEdge={setSelectedEdge}
|
||
setCurrentView={setCurrentView}
|
||
cy={cy}
|
||
setAuthenticationModalOpen={setAuthenticationModalOpen}
|
||
|
||
setVariablesModalOpen={setVariablesModalOpen}
|
||
setLastSaved={setLastSaved}
|
||
setCodeModalOpen={setCodeModalOpen}
|
||
selectedNameChange={selectedNameChange}
|
||
rightsidebarStyle={rightsidebarStyle}
|
||
showEnvironment={showEnvironment}
|
||
selectedActionEnvironment={selectedActionEnvironment}
|
||
environments={environments}
|
||
setNewSelectedAction={setNewSelectedAction}
|
||
sortByKey={sortByKey}
|
||
|
||
appApiViewStyle={appApiViewStyle}
|
||
globalUrl={globalUrl}
|
||
setSelectedActionEnvironment={setSelectedActionEnvironment}
|
||
requiresAuthentication={requiresAuthentication}
|
||
/>
|
||
</div>
|
||
:
|
||
<RightSideBar />
|
||
}
|
||
<BottomCytoscapeBar />
|
||
<TopCytoscapeBar />
|
||
</div>
|
||
|
||
/*
|
||
:
|
||
<div style={{color: "white"}}>
|
||
TMP FOR NOT LOGGED IN
|
||
</div>
|
||
*/
|
||
|
||
|
||
const executionVariableModal = executionVariablesModalOpen ?
|
||
<Dialog modal
|
||
open={executionVariablesModalOpen}
|
||
onClose={() => {
|
||
setNewVariableName("")
|
||
setExecutionVariablesModalOpen(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle><span style={{color: "white"}}>Execution Variable</span></DialogTitle>
|
||
<DialogContent>
|
||
Execution Variables are TEMPORARY variables that you can ony be set and used during execution. Learn more <a rel="norefferer" href="https://shuffler.io/docs/workflows#execution_variables" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>here</a>
|
||
<TextField
|
||
onBlur={(event) => setNewVariableName(event.target.value)}
|
||
color="primary"
|
||
placeholder="Name"
|
||
style={{marginTop: 25}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
margin="dense"
|
||
fullWidth
|
||
defaultValue={newVariableName}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setNewVariableName("")
|
||
setExecutionVariablesModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0} onClick={() => {
|
||
console.log("VARIABLES! ", newVariableName)
|
||
if (workflow.execution_variables === undefined || workflow.execution_variables === null) {
|
||
workflow.execution_variables = []
|
||
}
|
||
|
||
// try to find one with the same name
|
||
const found = workflow.execution_variables.findIndex(data => data.name === newVariableName)
|
||
//console.log(found)
|
||
if (found !== -1) {
|
||
if (newVariableName.length > 0) {
|
||
workflow.execution_variables[found].name = newVariableName
|
||
}
|
||
} else {
|
||
workflow.execution_variables.push({
|
||
"name": newVariableName,
|
||
"description": "An execution variable",
|
||
"value": "",
|
||
"id": uuid.v4(),
|
||
})
|
||
}
|
||
|
||
setExecutionVariablesModalOpen(false)
|
||
setNewVariableName("")
|
||
setWorkflow(workflow)
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
{workflowExecutions.length > 0 ?
|
||
<DialogContent>
|
||
<Divider style={{backgroundColor: "white", marginTop: 15, marginBottom: 15,}}/>
|
||
<b style={{marginBottom: 10}}>Values from last 3 executions</b>
|
||
{workflowExecutions.slice(0,3).map((execution, index) => {
|
||
if (execution.execution_variables === undefined || execution.execution_variables === null || execution.execution_variables === 0) {
|
||
return null
|
||
}
|
||
|
||
const variable = execution.execution_variables.find(data => data.name === newVariableName)
|
||
if (variable === undefined || variable.value === undefined) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{index+1}: {variable.value}
|
||
</div>
|
||
)
|
||
})}
|
||
</DialogContent>
|
||
: null
|
||
}
|
||
</FormControl>
|
||
</Dialog>
|
||
: null
|
||
|
||
const variablesModal = variablesModalOpen ?
|
||
<Dialog
|
||
open={variablesModalOpen}
|
||
onClose={() => {
|
||
setNewVariableName("")
|
||
setNewVariableDescription("")
|
||
setNewVariableValue("")
|
||
setVariablesModalOpen(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle><span style={{color: "white"}}>Workflow Variable</span></DialogTitle>
|
||
<DialogContent>
|
||
<TextField
|
||
onBlur={(event) => setNewVariableName(event.target.value)}
|
||
color="primary"
|
||
placeholder="Name"
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
margin="dense"
|
||
fullWidth
|
||
defaultValue={newVariableName}
|
||
/>
|
||
<TextField
|
||
onBlur={(event) => setNewVariableDescription(event.target.value)}
|
||
color="primary"
|
||
placeholder="Description"
|
||
margin="dense"
|
||
fullWidth
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
defaultValue={newVariableDescription}
|
||
/>
|
||
<TextField
|
||
onChange={(event) => setNewVariableValue(event.target.value)}
|
||
rows="6"
|
||
multiline
|
||
color="primary"
|
||
placeholder="Value"
|
||
margin="dense"
|
||
InputProps={{
|
||
style:{
|
||
color: "white"
|
||
}
|
||
}}
|
||
fullWidth
|
||
defaultValue={newVariableValue}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setNewVariableName("")
|
||
setNewVariableDescription("")
|
||
setNewVariableValue("")
|
||
setVariablesModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0 || newVariableValue.length === 0} onClick={() => {
|
||
if (workflow.workflow_variables === undefined || workflow.workflow_variables === null) {
|
||
workflow.workflow_variables = []
|
||
}
|
||
|
||
// try to find one with the same name
|
||
const found = workflow.workflow_variables.findIndex(data => data.name === newVariableName)
|
||
//console.log(found)
|
||
if (found !== -1) {
|
||
if (newVariableName.length > 0) {
|
||
workflow.workflow_variables[found].name = newVariableName
|
||
}
|
||
if (newVariableDescription.length > 0) {
|
||
workflow.workflow_variables[found].description = newVariableDescription
|
||
}
|
||
if (newVariableValue.length > 0) {
|
||
workflow.workflow_variables[found].value = newVariableValue
|
||
}
|
||
} else {
|
||
workflow.workflow_variables.push({
|
||
"name": newVariableName,
|
||
"description": newVariableDescription,
|
||
"value": newVariableValue,
|
||
"id": uuid.v4(),
|
||
})
|
||
}
|
||
|
||
setWorkflow(workflow)
|
||
setVariablesModalOpen(false)
|
||
setNewVariableName("")
|
||
setNewVariableDescription("")
|
||
setNewVariableValue("")
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</FormControl>
|
||
</Dialog>
|
||
: null
|
||
|
||
|
||
const AuthenticationData = (props) => {
|
||
const selectedApp = props.app
|
||
|
||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||
fields: {},
|
||
label: "",
|
||
usage: [{
|
||
workflow_id: workflow.id,
|
||
}],
|
||
id: uuid.v4(),
|
||
active: true,
|
||
})
|
||
|
||
if (selectedApp.authentication === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (selectedApp.authentication.parameters === null || selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0) {
|
||
return null
|
||
}
|
||
|
||
authenticationOption.app.actions = []
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (authenticationOption.fields[selectedApp.authentication.parameters[key].name] === undefined) {
|
||
authenticationOption.fields[selectedApp.authentication.parameters[key].name] = ""
|
||
}
|
||
}
|
||
|
||
const handleSubmitCheck = () => {
|
||
console.log("NEW AUTH: ", authenticationOption)
|
||
if (authenticationOption.label.length === 0) {
|
||
authenticationOption.label = `Auth for ${selectedApp.name}`
|
||
//alert.info("Label can't be empty")
|
||
//return
|
||
}
|
||
|
||
// 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 => (
|
||
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
*/
|
||
|
||
}
|
||
|
||
if (authenticationOption.label === null || authenticationOption.label === undefined) {
|
||
authenticationOption.label = selectedApp.name+" authentication"
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<DialogContent>
|
||
<a target="_blank" rel="norefferer" href="https://shuffler.io/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</a><div/>
|
||
These are required fields for authenticating with {selectedApp.name}
|
||
<div style={{marginTop: 15}}/>
|
||
<b>Name - what is this used for?</b>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"Auth july 2020"}
|
||
defaultValue={`Auth for ${selectedApp.name}`}
|
||
onChange={(event) => {
|
||
authenticationOption.label = event.target.value
|
||
}}
|
||
/>
|
||
<Divider style={{marginTop: 15, marginBottom: 15, backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
{/*selectedApp.link.length > 0 ? <div style={{marginTop: 15}}><EndpointData /></div> : null*/}
|
||
<div style={{}}/>
|
||
{selectedApp.authentication.parameters.map((data, index) => {
|
||
console.log("AUTH: ", data)
|
||
|
||
return (
|
||
<div key={index} style={{marginTop: 10}}>
|
||
<LockOpenIcon style={{marginRight: 10}}/>
|
||
<b>{data.name}</b>
|
||
|
||
{data.schema !== undefined && data.schema !== null && data.schema.type === "bool" ?
|
||
<Select
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
defaultValue={"false"}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
authenticationOption.fields[data.name] = e.target.value
|
||
}}
|
||
style={{backgroundColor: theme.palette.surfaceColor, color: "white", height: "50px"}}
|
||
>
|
||
<MenuItem key={"false"} style={{backgroundColor: theme.palette.inputColor, color: "white"}} value={"false"}>
|
||
false
|
||
</MenuItem>
|
||
<MenuItem key={"true"} style={{backgroundColor: theme.palette.inputColor, color: "white"}} value={"true"}>
|
||
true
|
||
</MenuItem>
|
||
</Select>
|
||
:
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type={data.example !== undefined && data.example.includes("***") ? "password" : "text"}
|
||
color="primary"
|
||
defaultValue={data.value !== undefined && data.value !== null ? data.value : ""}
|
||
placeholder={data.example}
|
||
onChange={(event) => {
|
||
authenticationOption.fields[data.name] = event.target.value
|
||
}}
|
||
/>
|
||
}
|
||
</div>
|
||
)
|
||
})}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setAuthenticationModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} onClick={() => {
|
||
setAuthenticationOptions(authenticationOption)
|
||
handleSubmitCheck()
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const EndpointData = () => {
|
||
const [tmpVar, setTmpVar] = React.useState("")
|
||
|
||
return (
|
||
<div>
|
||
The API endpoint to use (URL) - predefined in the app
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type="text"
|
||
color="primary"
|
||
placeholder="Bearer token"
|
||
defaultValue={selectedApp.link}
|
||
onChange={(event) => {
|
||
setTmpVar(event.target.value)
|
||
}}
|
||
onBlur={() => {
|
||
selectedApp.link = tmpVar
|
||
console.log("LINK: ", selectedApp.link)
|
||
setSelectedApp(selectedApp)
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const configureWorkflowModal = configureWorkflowModalOpen && apps.length !== 0 ?
|
||
<Dialog
|
||
open={configureWorkflowModalOpen}
|
||
onClose={() => {
|
||
//setConfigureWorkflowModalOpen(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
minWidth: 600,
|
||
padding: 50,
|
||
},
|
||
}}
|
||
>
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 14, right: 14, color: "grey"}} onClick={() => {
|
||
setConfigureWorkflowModalOpen(false)
|
||
}}>
|
||
<CloseIcon />
|
||
</IconButton>
|
||
<ConfigureWorkflow theme={theme} globalUrl={globalUrl} workflow={workflow} setSelectedAction={setSelectedAction} setSelectedApp={setSelectedApp} setAuthenticationModalOpen={setAuthenticationModalOpen} appAuthentication={appAuthentication} selectedAction={selectedAction} apps={apps} setConfigureWorkflowModalOpen={setConfigureWorkflowModalOpen} saveWorkflow={saveWorkflow} newWebhook={newWebhook} submitSchedule={submitSchedule} referenceUrl={referenceUrl} isCloud={isCloud} />
|
||
</Dialog>
|
||
: null
|
||
|
||
|
||
// This whole part is redundant. Made it part of Arguments instead.
|
||
const authenticationModal = authenticationModalOpen ?
|
||
<Dialog
|
||
open={authenticationModalOpen}
|
||
onClose={() => {
|
||
//setAuthenticationModalOpen(false)
|
||
//
|
||
if (configureWorkflowModalOpen) {
|
||
setSelectedAction({})
|
||
}
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
minWidth: 600,
|
||
padding: 15,
|
||
},
|
||
}}
|
||
>
|
||
<IconButton style={{zIndex: 5000, position: "absolute", top: 14, right: 14, color: "grey"}} onClick={() => {
|
||
setAuthenticationModalOpen(false)
|
||
if (configureWorkflowModalOpen) {
|
||
setSelectedAction({})
|
||
}
|
||
}}>
|
||
<CloseIcon />
|
||
</IconButton>
|
||
<DialogTitle><div style={{color: "white"}}>Authentication for {selectedApp.name}</div></DialogTitle>
|
||
<AuthenticationData app={selectedApp} />
|
||
</Dialog> : null
|
||
|
||
//const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
|
||
const loadedCheck = isLoaded && workflowDone ?
|
||
<div>
|
||
{newView}
|
||
{variablesModal}
|
||
{executionVariableModal}
|
||
{conditionsModal}
|
||
{authenticationModal}
|
||
{codePopoutModal}
|
||
{configureWorkflowModal}
|
||
<TextField
|
||
id="copy_element_shuffle"
|
||
value={to_be_copied}
|
||
style={{display: "none", }}
|
||
/>
|
||
</div>
|
||
:
|
||
<div>
|
||
</div>
|
||
|
||
return (
|
||
<div>
|
||
<Prompt
|
||
when={!lastSaved}
|
||
message={unloadText}
|
||
/>
|
||
{loadedCheck}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AngularWorkflow
|