Files
shuffle-cracked/frontend/src/AngularWorkflow.js
T
2020-06-13 14:02:05 +02:00

4817 lines
163 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {useState, useEffect, useLayoutEffect} from 'react';
import { useInterval } from 'react-powerhooks';
import uuid from "uuid";
import {Link} from 'react-router-dom';
import { Prompt } from 'react-router'
import TextField from '@material-ui/core/TextField';
import Drawer from '@material-ui/core/Drawer';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import ButtonBase from '@material-ui/core/ButtonBase';
import Tooltip from '@material-ui/core/Tooltip';
import Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
import Divider from '@material-ui/core/Divider';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import FormControl from '@material-ui/core/FormControl';
import IconButton from '@material-ui/core/IconButton';
import Menu from '@material-ui/core/Menu';
import Input from '@material-ui/core/Input';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
import CircularProgress from '@material-ui/core/CircularProgress';
import ReactJson from 'react-json-view'
import { useBeforeunload } from 'react-beforeunload';
import DirectionsRunIcon from '@material-ui/icons/DirectionsRun';
import PolymerIcon from '@material-ui/icons/Polymer';
import CreateIcon from '@material-ui/icons/Create';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import AspectRatioIcon from '@material-ui/icons/AspectRatio';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import AppsIcon from '@material-ui/icons/Apps';
import ScheduleIcon from '@material-ui/icons/Schedule';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import PauseIcon from '@material-ui/icons/Pause';
import DeleteIcon from '@material-ui/icons/Delete';
import SaveIcon from '@material-ui/icons/Save';
import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
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 environmentdata from './environmentdata';
import cytoscapestyle from './defaultCytoscapeStyle';
import cxtmenu from 'cytoscape-cxtmenu';
import { w3cwebsocket as W3CWebSocket } from "websocket";
import { useAlert } from "react-alert";
const hoverColor = "#f85a3e"
const hoverOutColor = "#e8eaf6"
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;
}
const splitter = "|~|"
//const referenceUrl = "https://shuffler.io/functions/webhooks/"
//const referenceUrl = window.location.origin+"/api/v1/hooks/"
const AngularWorkflow = (props) => {
const { globalUrl, isLoggedIn, isLoaded } = props;
const referenceUrl = globalUrl+"/api/v1/hooks/"
const alert = useAlert()
const [bodyWidth, bodyHeight] = useWindowSize();
const appBarSize = 74
const [cystyle, ] = useState(cytoscapestyle)
const [cy, setCy] = React.useState()
const [appSearch, setAppSearch] = React.useState("")
const [currentView, setCurrentView] = React.useState(0)
const [triggerAuthentication, setTriggerAuthentication] = React.useState({})
const [triggerFolders, setTriggerFolders] = React.useState([])
const [workflow, setWorkflow] = React.useState({});
const [leftViewOpen, setLeftViewOpen] = React.useState(true);
const [leftBarSize, setLeftBarSize] = React.useState(350)
const [executionText, setExecutionText] = React.useState("");
const [executionRequestStarted, setExecutionRequestStarted] = React.useState(false);
const [appAuthentication, setAppAuthentication] = React.useState({});
const [variablesModalOpen, setVariablesModalOpen] = React.useState(false);
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
const [conditionsModalOpen, setConditionsModalOpen] = React.useState(false);
const [newVariableName, setNewVariableName] = React.useState("");
const [newVariableDescription, setNewVariableDescription] = React.useState("");
const [newVariableValue, setNewVariableValue] = React.useState("");
const [workflowDone, setWorkflowDone] = React.useState(false)
const [localFirstrequest, setLocalFirstrequest] = React.useState(true)
const [requiresAuthentication, setRequiresAuthentication] = React.useState(true)
const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false)
const [variableAnchorEl, setVariableAnchorEl] = React.useState(null)
const [sourceValue, setSourceValue] = React.useState({})
const [destinationValue, setDestinationValue] = React.useState({})
const [conditionValue, setConditionValue] = React.useState({})
// 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 [firstrequest, setFirstrequest] = React.useState(true)
//const [apps, setApps] = React.useState(appdata);
//const [filteredApps, setFilteredApps] = React.useState();
const [environments, setEnvironments] = React.useState([]);
const [established, setEstablished] = React.useState(false);
const [graphSetup, setGraphSetup] = React.useState(false);
const [selectedApp, setSelectedApp] = React.useState({});
const [selectedAction, setSelectedAction] = React.useState({});
const [selectedActionName, setSelectedActionName] = React.useState({});
const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({});
const [executionRequest, setExecutionRequest] = React.useState({})
const [, setExecutingNodes] = React.useState([])
const [executionRunning, setExecutionRunning] = React.useState(false)
const [executionModalOpen, setExecutionModalOpen] = React.useState(false)
const [executionModalView, setExecutionModalView] = React.useState(0)
const [executionData, setExecutionData] = React.useState({})
const [lastSaved, setLastSaved] = React.useState(true)
const [appAdded, setAppAdded] = useState(false)
const [update, setUpdate] = useState("");
const [workflowExecutions, setWorkflowExecutions] = React.useState([]);
const unloadText = 'Are you sure you want to leave?'
useBeforeunload(() => {
if (!lastSaved) {
return unloadText
}
})
const [elements, setElements] = useState([])
const { start, stop } = useInterval({
duration: 2500,
startImmediate: false,
callback: () => {
fetchUpdates()
}
})
const getWorkflowExecution = (id) => {
fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
}
return response.json()
})
.then((responseJson) => {
if (responseJson.length > 0) {
// FIXME: Sort this by time
setWorkflowExecutions(responseJson)
}
//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 => {
return (
<div>
{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!")
}
return response.json()
})
.then((responseJson) => {
handleUpdateResults(responseJson)
})
.catch(error => {
alert.error(error.toString())
});
}
const abortExecution = () => {
setExecutionRunning(false)
alert.info("Aborting execution")
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/executions/"+executionRequest.execution_id+"/abort", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
} else {
alert.success("Execution aborted")
}
return response.json()
})
.catch(error => {
alert.error(error.toString())
});
}
// Controls the colors and direction of execution results.
// Style is in defaultCytoscapeStyle.js
const handleUpdateResults = (responseJson) => {
//console.log(responseJson)
// Loop nodes and find results
// Update on every interval? idk
if (JSON.stringify(responseJson) !== JSON.stringify(executionData)) {
// FIXME: If another is selected, don't edit..
// Doesn't work because this is some async garbage
if (executionData.execution_id === undefined || responseJson.execution_id === executionData.execution_id) {
setExecutionData(responseJson)
}
}
if (responseJson.execution_id !== executionRequest.execution_id) {
cy.elements().removeClass('success-highlight failure-highlight executing-highlight')
return
}
if (responseJson.results !== null && responseJson.results !== []) {
for (var key in responseJson.results) {
var item = responseJson.results[key]
var currentnode = cy.getElementById(item.action.id)
if (currentnode.length === 0) {
continue
}
currentnode = currentnode[0]
const outgoingEdges = currentnode.outgoers('edge')
const incomingEdges = currentnode.incomers('edge')
//currentnode.removeClass('success-highlight failure-highlight executing-highlight')
switch (item.status) {
case "EXECUTING":
currentnode.removeClass('not-executing-highlight')
currentnode.removeClass('success-highlight')
currentnode.removeClass('failure-highlight')
currentnode.removeClass('awaiting-data-highlight')
incomingEdges.addClass('success-highlight')
currentnode.addClass('executing-highlight')
break
case "WAITING":
currentnode.removeClass('not-executing-highlight')
currentnode.removeClass('success-highlight')
currentnode.removeClass('failure-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('awaiting-data-highlight')
currentnode.addClass('success-highlight')
if (!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('failure-highlight')
targetnode.removeClass('awaiting-data-highlight')
targetnode.addClass('executing-highlight')
}
}
// const outgoingEdges = currentnode.outgoers('edge')
}
break
case "FAILURE":
currentnode.removeClass('not-executing-highlight')
currentnode.removeClass('executing-highlight')
currentnode.removeClass('success-highlight')
currentnode.removeClass('awaiting-data-highlight')
currentnode.addClass('failure-highlight')
if (!visited.includes(item.action.label)) {
alert.error("Success 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.addClass('awaiting-data-highlight')
break
default:
console.log("DEFAULT?")
break
}
}
}
if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE") {
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')
}
}
} else if (responseJson.status === "FINISHED") {
setExecutionRunning(false)
stop()
}
}
const saveWorkflow = (curworkflow) => {
var success = false
// This might not be the right course of action, but seems logical, as items could be running already
// Makes it possible to update with a version in current render
stop()
var useworkflow = workflow
if (curworkflow !== undefined) {
useworkflow = curworkflow
} else {
alert.info("Saving workflow")
}
var cyelements = cy.elements()
var newActions = []
var newTriggers = []
var newBranches = []
for (var key in cyelements) {
if (cyelements[key].data === undefined) {
continue
}
var type = cyelements[key].data()["type"]
if (type === undefined) {
if (cyelements[key].data().source === undefined ||
cyelements[key].data().target === undefined) {
continue
}
var parsedElement = {
id: cyelements[key].data().id,
source_id: cyelements[key].data().source,
destination_id: cyelements[key].data().target,
conditions: cyelements[key].data().conditions,
}
newBranches.push(parsedElement)
} else {
if (type === "ACTION") {
// FIXME - check whether position is new to not fuck up params etc.
var curworkflowAction = useworkflow.actions.find(a => a.id === cyelements[key].data()["id"])
if (curworkflowAction === undefined) {
curworkflowAction = cyelements[key].data()
}
curworkflowAction.position = cyelements[key].position()
// workaround to fix some edgecases
if (curworkflowAction.parameters === "" || curworkflowAction.parameters === null) {
curworkflowAction.parameters = []
}
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
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) => {
if (response.status !== 200) {
console.log("Status not 200 for setting workflows :O!")
}
return response.json()
})
.then((responseJson) => {
if (!responseJson.success) {
console.log(responseJson)
alert.error("Failed to save: "+responseJson.reason)
} else {
success = true
alert.success("Successfully saved workflow")
}
})
.catch(error => {
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 = () => {
if (!lastSaved) {
//alert.error("You might have forgotten to save before executing.")
console.log("FIXME: Might have forgotten to save before executing.")
}
var returncheck = monitorUpdates()
if (!returncheck) {
alert.error("No startnode set.")
return
}
setVisited([])
setExecutionRequest({})
setExecutionRequestStarted(true)
stop()
var curelements = cy.elements()
for (var i = 0; i < curelements.length; i++) {
curelements[i].addClass("not-executing-highlight")
}
if (executionText.length > 0) {
alert.success("Starting execution with argument "+executionText)
} else {
alert.success("Starting execution")
}
const data = {"execution_argument": executionText, "start": workflow.start}
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/execute", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
body: JSON.stringify(data),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
}
return response.json()
})
.then((responseJson) => {
if (!responseJson.success) {
alert.error("Failed to start: "+responseJson.reason)
setExecutionRunning(false)
setExecutionRequestStarted(false)
stop()
for (var i = 0; i < curelements.length; i++) {
curelements[i].removeClass("not-executing-highlight")
}
return
} else {
setExecutionRunning(true)
setExecutionRequestStarted(false)
}
setExecutionRequest({
"execution_id": responseJson.execution_id,
"authorization": responseJson.authorization,
})
setExecutingNodes([workflow.start])
setExecutionData({})
setExecutionModalOpen(true)
setExecutionModalView(1)
start()
})
.catch(error => {
alert.error(error.toString())
});
}
//const handleAppVersioning = (apps) => {
// var newapps = []
// for (var key in apps) {
// var item = apps[key]
// const previtem = newapps.findIndex(data => data.name === item.name)
// if (previtem === -1) {
// item["versions"] = [item.app_version]
// newapps.push(item)
// continue
// }
// // THere might be duplicates for some reason..
// if (!newapps[previtem]["versions"].includes(item.app_version)) {
// newapps[previtem]["versions"].push(item.app_version)
// }
// }
//
// // FIXME - handle this, as we can't have more than one of each :)
// //setVersionedApps(newapps)
//}
// Builtin actions that should ran in Worker and not apps
const getExtraApps = () => {
const data = [{
name: "Filter",
is_valid: true,
id: "0ca8887e-b4af-4e3e-887c-87e9d3bc3d3e",
link: "https://shuffler.io",
app_version: "1.0.0",
generated: true,
downloaded: false,
sharing: false,
verified: false,
tested: false,
owner: "",
private_id: "",
description: "Filter",
environment: "Shuffle",
small_image: "",
large_image: "",
contact_info: {name: "", url: ""},
authentication: {required: false, parameters: [],},
actions: [{
description: "Filter cases",
id: "",
name: "filter_cases",
node_type: "action",
environment: "Shuffle",
sharing: false,
private_id: "",
app_id: "",
authentication: null,
tested: true,
parameters: [{
description: "",
id: "",
name: "Field to look for",
example: "$testing_1.#.id",
value: "",
multiline: true,
action_field: "",
variant: "",
required: true,
schema: {type: "string"},
}]
}],
}]
return data
}
const getApps = () => {
fetch(globalUrl+"/api/v1/workflows/apps", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!")
}
return response.json()
})
.then((responseJson) => {
// FIXME - handle versions on left bar
//handleAppVersioning(responseJson)
//var tmpapps = []
//tmpapps = tmpapps.concat(getExtraApps())
//tmpapps = tmpapps.concat(responseJson)
setApps(responseJson)
setFilteredApps(responseJson)
})
.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) => {
setWorkflow(responseJson)
setWorkflowDone(true)
})
.catch(error => {
alert.error(error.toString())
});
}
const onUnselect = (event) => {
console.time("UNSELECT")
// FIXME - check if they have value before overriding like this for no reason.
// Would save a lot of time (400~ ms -> 30ms)
//console.log("ACTION: ", selectedAction)
//console.log("APP: ", selectedApp)
setSelectedAction({})
setSelectedApp({})
//setSelectedTrigger({})
//setSelectedEdge({})
// setSelectedTriggerIndex(-1)
//setSelectedActionEnvironment({})
//setSelectedEdge({})
//setTriggerAuthentication({})
//setSelectedTriggerIndex(-1)
//setTriggerFolders([])
// Can be used for right side view
setRightSideBarOpen(false)
console.timeEnd("UNSELECT")
}
const onEdgeSelect = (event) => {
setRightSideBarOpen(true)
setLastSaved(false)
const triggercheck = workflow.triggers.find(trigger => trigger.id === event.target.data()["source"])
if (triggercheck === undefined) {
setSelectedEdgeIndex(workflow.branches.findIndex(data => data.id === event.target.data()["id"]))
setSelectedEdge(event.target.data())
setSelectedAction({})
setSelectedTrigger({})
} else {
alert.info("Can't edit branches from triggers")
}
}
const onNodeSelect = (event) => {
const data = event.target.data()
console.log("NODE: ", data)
setLastSaved(false)
//console.log(data)
if (data.type === "ACTION") {
// FIXME - unselect
//console.log(cy.elements('[_id!="${data._id}"]`))
// Does it choose the wrong action?
const curaction = workflow.actions.find(a => a.id === data.id)
if (!curaction || curaction === undefined) {
//console.log("Action not found error")
return
}
const curapp = apps.find(a => a.name === curaction.app_name && a.app_version === curaction.app_version)
if (!curapp || curapp === undefined) {
alert.error("App "+curaction.app_name+" not found. Did someone delete it?")
return
}
var env = environments.find(a => a.name === curaction.environment)
if (!env || env === undefined) {
env = environments[0]
}
setSelectedApp(curapp)
setSelectedAction(curaction)
setSelectedActionEnvironment(env)
setSelectedActionName(curaction.name)
setRequiresAuthentication(curapp.authentication.required)
} else if (data.type === "TRIGGER") {
//console.log("Should handle trigger "+data.triggertype)
//console.log(data)
setSelectedTriggerIndex(workflow.triggers.findIndex(a => a.id === data.id))
setSelectedTrigger(data)
setSelectedActionName(data.name)
setSelectedActionEnvironment(data.env)
} else {
alert.error("Can't handle "+data.type)
}
setRightSideBarOpen(true)
}
const onEdgeAdded = (event) => {
setLastSaved(false)
const edge = event.target.data()
// 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 pointer 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) {
// Checks if NOT trigger
//workflow.triggers.find(data => data.id === workflow.branches[key].source_id).length === 0)
const triggercheck = workflow.triggers.find(data => data.id === workflow.branches[key].source_id)
if (workflow.triggers !== undefined && triggercheck !== undefined && triggercheck.length === 0) {
alert.error("That pointer already exists")
event.target.remove()
}
found = true
break
} else if (edge.target === workflow.start) {
var targetnode = workflow.triggers.findIndex(data => data.id === edge.source)
if (targetnode === -1) {
alert.error("Can't make arrow to starting node")
event.target.remove()
found = true
break
}
} else {
// 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) {
// alert.error("Can't have triggers as target")
// event.target.remove()
// found = true
// break
//}
}
}
var newbranch = {
"source_id": edge.source,
"destination_id": edge.target,
"id_": edge.id,
"id": edge.id,
"hasErrors": false,
}
if (!found) {
newbranch["hasErrors"] = false
}
workflow.branches.push(newbranch)
setWorkflow(workflow)
}
const onNodeAdded = (event) => {
setLastSaved(false)
const node = event.target
if (node.isNode() && cy.nodes().size() === 1) {
//setStartNode(node.data('id'))
workflow.start = node.data('id')
setWorkflow(workflow)
}
}
const onEdgeRemoved = (event) => {
setLastSaved(false)
const edge = event.target
workflow.branches = workflow.branches.filter(a => a.id !== edge.data().id)
setWorkflow(workflow)
// trigger as source check
const indexcheck = workflow.triggers.findIndex(data => edge.data()["source"] === data.id)
if (indexcheck !== -1) {
//alert.error("Can't remove edge from a trigger")
console.log("Shouldnt remove edge from trigger")
//const edgeToBeAdded = {
// group: "edges",
// data: newcybranch,
//}
}
}
const onNodeRemoved = (event) => {
const node = event.target
const data = node.data()
setLastSaved(false)
//var currentnode = cy.getElementById(data.id)
//if (currentnode.length === 0) {
//}
if (workflow.start === data.id && workflow.actions.length > 1) {
// FIXME - should check branches connected to startnode, as picking random
// might just be confusing
cy.nodes().forEach(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()
return
}
});
}
workflow.actions = workflow.actions.filter(a => a.id !== data.id)
workflow.triggers = workflow.triggers.filter(a => a.id !== data.id)
setWorkflow(workflow)
if (data.type === "TRIGGER") {
saveWorkflow(workflow)
}
}
var previouskey = 0
const handleKeyDown = (event) => {
// SHIFT = 16
// CTRL = 17
//console.log(event.keyCode)
switch( event.keyCode ) {
case 27:
console.log("ESCAPE")
break;
case 46:
removeNode()
console.log("DELETE")
break;
case 38:
console.log("UP")
break;
case 37:
console.log("LEFT")
break;
case 40:
console.log("DOWN")
break;
case 39:
console.log("RIGHT")
break;
case 90:
if (previouskey === 17) {
console.log("CTRL+Z")
}
break;
case 67:
if (previouskey === 17) {
console.log("CTRL+C")
}
break;
case 86:
if (previouskey === 17) {
console.log("CTRL+V")
}
break;
case 88:
if (previouskey === 17) {
console.log("CTRL+V")
}
break;
case 83:
if (previouskey === 17) {
event.preventDefault()
saveWorkflow()
}
break;
case 70:
if (previouskey === 17) {
event.preventDefault()
cy.fit(null, 50)
}
break;
case 65:
// As a poweruser myself, I found myself hitting this a few
// too many times to just edit text. Need a better bind
//
//if (previouskey === 17) {
// event.preventDefault()
// if (executionRunning || executionRequestStarted) {
// abortExecution()
// } else {
// executeWorkflow()
// }
// cy.fit(null, 50)
//}
break;
default:
//console.log(event.keyCode)
break;
}
previouskey = event.keyCode
}
const registerKeys = () => {
document.addEventListener("keydown", handleKeyDown);
}
const getEnvironments = () => {
fetch(globalUrl+"/api/v1/getenvironments", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!")
return
}
return response.json()
})
.then((responseJson) => {
setEnvironments(responseJson)
})
.catch(error => {
alert.error(error.toString())
});
}
useEffect(() => {
if (firstrequest) {
setFirstrequest(false)
getWorkflow()
getApps()
getEnvironments()
getWorkflowExecution(props.match.params.key)
return
}
// App length necessary cus of cy initialization
if (elements.length === 0 && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) {
setGraphSetup(true)
setupGraph()
} else if (!established && cy !== undefined && apps.length > 0 && Object.getOwnPropertyNames(workflow).length > 0){
setEstablished(true)
cy.edgehandles({
handleNodes: (el) => el.isNode(),
preview: false,
toggleOffOnLeave: false,
loopAllowed: function( node ){
return false;
},
})
cy.fit(null, 200)
cy.on('select', 'node', (e) => onNodeSelect(e))
cy.on('select', 'edge', (e) => onEdgeSelect(e))
cy.on('unselect', (e) => onUnselect(e))
cy.on('add', 'node', (e) => onNodeAdded(e))
cy.on('add', 'edge', (e) => onEdgeAdded(e))
cy.on('remove', 'node', (e) => onNodeRemoved(e))
cy.on('remove', 'edge', (e) => onEdgeRemoved(e))
cy.on('mouseover', 'edge', (e) => onEdgeHover(e))
cy.on('mouseout', 'edge', (e) => onEdgeHoverOut(e))
cy.on('mouseover', 'node', (e) => onNodeHover(e))
cy.on('mouseout', 'node', (e) => onNodeHoverOut(e))
//cy.on('mouseover', 'node', () => $(targetElement).addClass('mouseover'));
//cy.on('cxttapstart', 'node', (e) => edgeHandler.start(e.target))
//cy.on('cxttapend', 'node', (e) => edgeHandler.stop())
//cy.on('cxtdragover', 'node', (e) => edgeHandler.preview(e.target))
//cy.on('cxtdragout', 'node', (e) => edgeHandler.unpreview(e.target))
// RIGHT HERE..?
// This is wrong sometimes.. I'm mad
document.title = "Workflow - "+workflow.name
registerKeys()
//setStartNode(workflow.start)
} else if (established) {
//console.log("established - should fix colors of things")
//console.log(cy.elements())
}
})
var previousnodecolor = ""
//var previousedgecolor = ""
const animationDuration = 150
const onNodeHoverOut = (event) => {
event.target.animate({
style: {
"border-width": "1px",
}
}, {
duration: animationDuration,
})
}
const onNodeHover = (event) => {
//event.target.style("border-width", "5px")
event.target.animate({
style: {
"border-width": "5px",
"border-opacity": ".7",
}
}, {
duration: animationDuration,
})
previousnodecolor = event.target.style("border-color")
}
const onEdgeHoverOut = (event) => {
//event.target.removeStyle()
}
// This is here to have a proper transition for lines
const onEdgeHover = (event) => {
//console.log(event.target.data())
//const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color")
//const targetcolor = cy.getElementById(event.target.data("target")).style("border-color")
//event.target.animate({
// style: {
// "line-fill": "linear-gradient",
// 'target-arrow-color': targetcolor,
// "line-gradient-stop-colors": [sourcecolor, targetcolor],
// "line-gradient-stop-positions": [0, 1],
// },
// duration: 0,
//})
}
const setupGraph = () => {
const actions = workflow.actions.map(action => {
const node = {}
node.position = action.position
node.data = action
node.data._id = action["id"]
node.data.type = "ACTION"
node.isStartNode = action["id"] === workflow.start
return node;
})
const triggers = workflow.triggers.map(trigger => {
const node = {}
node.position = trigger.position
node.data = trigger
node.data._id = trigger["id"]
node.data.type = "TRIGGER"
return node;
})
// FIXME - tmp branch update
var insertedNodes = [].concat(actions, triggers)
const edges = workflow.branches.map((branch, index) => {
//workflow.branches[index].conditions = [{
const edge = { };
var conditions = workflow.branches[index].conditions
if (conditions === undefined || conditions === null) {
conditions = []
}
var label = ""
if (conditions.length === 1) {
label = conditions.length+" condition"
} else if (conditions.length > 1) {
label = conditions.length+" conditions"
}
edge.data = {
id: branch.id,
_id: branch.id,
source: branch.source_id,
target: branch.destination_id,
label: label,
conditions: conditions,
hasErrors: branch.has_errors
};
return edge;
})
setWorkflow(workflow)
// Verifies if a branch is valid and skips others
var newedges = []
for (var key in edges) {
var item = edges[key]
const sourcecheck = insertedNodes.find(data => data.data.id === item.data.source)
const destcheck = insertedNodes.find(data => data.data.id === item.data.target)
if (sourcecheck === undefined || destcheck === undefined) {
continue
}
newedges.push(item)
}
insertedNodes = insertedNodes.concat(newedges)
setElements(insertedNodes)
}
const removeNode = () => {
setSelectedApp({})
setSelectedAction({})
setSelectedActionName("")
const selectedNode = cy.$(':selected')
if (selectedNode.data() === undefined) {
return
}
if (selectedNode.data().type === "TRIGGER") {
console.log("Should remove trigger!")
console.log(selectedNode.data())
const triggerindex = workflow.triggers.findIndex(data => data.id === selectedNode.data().id)
setSelectedTriggerIndex(triggerindex)
if (selectedNode.data().trigger_type === "SCHEDULE") {
setSelectedTrigger(selectedNode.data())
stopSchedule(selectedNode.data(), triggerindex)
} else if (selectedNode.data().trigger_type === "WEBHOOK") {
setSelectedTrigger(selectedNode.data())
deleteWebhook(selectedNode.data(), triggerindex)
} else if (selectedNode.data().trigger_type === "EMAIL") {
setSelectedTrigger(selectedNode.data())
stopMailSub(selectedTrigger, triggerindex)
}
}
selectedNode.remove()
setSelectedTrigger({})
setSelectedTriggerIndex({})
}
const stopSchedule = (trigger, triggerindex) => {
alert.info("Stopping schedule")
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/schedule/"+trigger.id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!")
}
return response.json()
})
.then((responseJson) => {
if (!responseJson.success) {
alert.error("Failed to delete 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,
"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 scrollStyle = {
marginTop: 10,
overflow: "scroll",
height: "100%",
overflowX: "auto",
overflowY: "auto",
}
const paperAppStyle = {
minHeight: "100px",
maxHeight: "100px",
minWidth: "100%",
maxWidth: "100%",
marginTop: "5px",
color: "white",
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
}
const paperVariableStyle = {
minHeight: "50px",
maxHeight: "50px",
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);
if (workflow.workflow_variables === undefined || workflow.workflow_variables === null || workflow.workflow_variables.length === 0) {
return (
<div style={appViewStyle}>
<div style={appScrollStyle}>
<div style={{margin: 10}}>
Looks like you don't have any variables yet.
<div/>
<div style={{width: "100%", margin: "auto"}}>
<Button fullWidth style={{margin: "auto", marginTop: "10px", borderRadius: 0}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>Make a new workflow variable</Button>
</div>
</div>
</div>
</div>
)
}
const menuClick = (event) => {
setOpen(!open)
setAnchorEl(event.currentTarget);
}
const deleteVariable = (variableName) => {
workflow.workflow_variables = workflow.workflow_variables.filter(data => data.name !== variableName)
setWorkflow(workflow)
}
const variableScrollStyle = {
marginTop: "10px",
overflow: "scroll",
height: "66vh",
overflowX: "auto",
overflowY: "auto",
flex: "10",
}
return (
<div style={appViewStyle}>
<div style={variableScrollStyle}>
{workflow.workflow_variables.map(variable=> {
return (
<div>
<Paper square style={paperVariableStyle} onClick={() => {
}}>
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}} />
<div style={{display: "flex", width: "100%"}}>
<div style={{flex: "10", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}} onClick={() => {
setNewVariableName(variable.name)
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: surfaceColor, color: "white"}} onClick={() => {
setOpen(false)
setNewVariableName(variable.name)
setNewVariableDescription(variable.description)
setNewVariableValue(variable.value)
setVariablesModalOpen(true)
}} key={"Edit"}>{"Edit"}</MenuItem>
<MenuItem style={{backgroundColor: surfaceColor, color: "white"}} onClick={() => {
deleteVariable(variable.name)
setOpen(false)
}} key={"Delete"}>{"Delete"}</MenuItem>
</Menu>
</div>
</div>
</Paper>
</div>
)
})}
</div>
<div style={{flex: "1"}}>
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>New workflow variable</Button>
</div>
</div>
)
}
const curTab = 0
const handleSetTab = (event, newValue) => {
setCurrentView(newValue)
}
const HandleLeftView = () => {
// Defaults to apps.
var thisview = <AppView />
if (currentView === 1) {
thisview = <TriggersView />
} else if (currentView === 2) {
thisview = <VariablesView />
}
const tabStyle = {
maxWidth: leftBarSize/3,
minWidth: leftBarSize/3,
flex: 1,
textTransform: "none",
}
const iconStyle = {
marginTop: 3,
marginRight: 5,
}
return(
<div>
<div style={{minHeight: bodyHeight-appBarSize-50, maxHeight: bodyHeight-appBarSize-50}}>
{thisview}
</div>
<Divider style={{backgroundColor: "rgb(91, 96, 100)"}}/>
<Tabs
value={currentView}
indicatorColor="primary"
textColor="white"
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 TriggersView = () => {
const triggersViewStyle = {
marginLeft: "10px",
marginRight: "10px",
display: "flex",
flexDirection: "column",
}
// Predefined hurr
var 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": environments[0].Name,
"description": "Simple HTTP webhook",
"long_description": "Execute a workflow with an unauthicated POST request",
},
{
"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": false,
"label": "User input",
"environment": environments[0].Name,
"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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",
"is_valid": true,
"label": "Schedule",
"environment": environments[0].Name,
"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": false,
"label": "Email",
"environment": environments[0].Name,
"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",
}]
return (
<div style={triggersViewStyle}>
<div style={appScrollStyle}>
{triggers.map(trigger => {
var imageline = trigger.large_image.length === 0 ?
<img alt="" style={{width: "80px"}} />
:
<img alt="" src={trigger.large_image} style={{width: 80, height: 80}} />
const color = trigger.is_valid ? "green" : "orange"
return(
<Draggable
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("Sorry, "+data.name+" is not ready yet")
return
}
newNodeId = uuid.v4()
const newposition = {
"x": e.pageX-cycontainer.offsetLeft,
"y": e.pageY-cycontainer.offsetTop,
}
console.log(data)
const newAppData = {
app_name: data.name,
app_version: "1.0.0",
environment: data.environment,
description: data.description,
long_description: data.long_description,
errors: [],
id_: newNodeId,
_id_: newNodeId,
id: newNodeId,
label: data.label,
type: data.type,
is_valid: true,
trigger_type: data.trigger_type,
large_image: data.large_image,
status: "uninitialized",
name: data.name,
isStartNode: false,
position: newposition,
}
if (data.trigger_type === "WEBHOOK") {
newAppData.status = "running"
}
// Can all the data be in here? hmm
const nodeToBeAdded = {
group: "nodes",
data: newAppData,
renderedPosition: newposition,
}
cy.add(nodeToBeAdded)
if (workflow.triggers === undefined) {
workflow.triggers = [newAppData]
} else {
workflow.triggers.push(newAppData)
}
const newEdgeUuid = uuid.v4()
const newbranch = {
"source_id": newNodeId,
"destination_id": workflow.start,
"id_": newEdgeUuid,
"id": newEdgeUuid,
"hasErrors": false,
}
const newcybranch = {
"source": newNodeId,
"target": workflow.start,
"_id": newEdgeUuid,
"id": newEdgeUuid,
"hasErrors": false,
}
const edgeToBeAdded = {
group: "edges",
data: newcybranch,
}
if (data.name !== "User Input") {
workflow.branches.push(newbranch)
cy.add(edgeToBeAdded)
}
setWorkflow(workflow)
if (data.trigger_type === "WEBHOOK") {
newWebhook(newAppData)
saveWorkflow(workflow)
}
}
}
}
const handleAppDrag = (e, app) => {
const cycontainer = cy.container()
// Chrome lol
//if (e.srcElement !== undefined && e.srcElement.localName === "canvas") {
if (e.pageX > cycontainer.offsetLeft && e.pageX < cycontainer.offsetLeft+cycontainer.offsetWidth && e.pageY > cycontainer.offsetTop && e.pageY < cycontainer.offsetTop+cycontainer.offsetHeight) {
if (newNodeId.length > 0) {
var currentnode = cy.getElementById(newNodeId)
if (currentnode.length === 0) {
return
}
currentnode[0].renderedPosition("x", e.pageX-cycontainer.offsetLeft)
currentnode[0].renderedPosition("y", e.pageY-cycontainer.offsetTop)
} else{
if (app.actions === undefined || app.actions === null || app.actions.length === 0) {
alert.error("App "+app.name+" currently has no actions to perform. Please go to https://shuffler.io/apps to edit it.")
return
}
newNodeId = uuid.v4()
const actionType = "ACTION"
const actionLabel = getNextActionName(app.name)
var parameters = null
if (app.actions[0].parameters !== null && app.actions[0].parameters.length > 0) {
parameters = app.actions[0].parameters
}
var newAppPopup = false
if (app.authentication !== undefined && app.authentication !== null && app.authentication.required === true) {
console.log("Should make modal popup for new app")
newAppPopup = true
}
const newAppData = {
app_name: app.name,
app_version: app.app_version,
app_id: app.id,
sharing: app.sharing,
private_id: app.private_id,
environment: environments[0].Name,
errors: [],
id_: newNodeId,
_id_: newNodeId,
id: newNodeId,
is_valid: true,
label: actionLabel,
type: actionType,
name: app.actions[0].name,
parameters: parameters,
isStartNode: false,
large_image: app.large_image,
authentication: [],
}
// const image = "url("+app.large_image+")"
// FIXME - find the cytoscape offset position
// Can this be done with zoom calculations?
const nodeToBeAdded = {
group: "nodes",
data: newAppData,
renderedPosition: {
x: e.layerX,
y: e.layerY,
}
}
cy.add(nodeToBeAdded)
if (workflow.actions === undefined || workflow.actions.length === 0) {
workflow.start = newAppData.id
workflow.actions = []
newAppData.isStartNode = true
//setStartNode(newAppData.id)
}
if (workflow.actions.length > 0 && elements.length === 0) {
const actions = workflow.actions.map(action => {
const node = {}
node.position = action.position
node.data = action
node.data._id = action["id"]
node.data.type = "ACTION"
node.isStartNode = action["id"] === workflow.start
return node
})
const tmpelements = [].concat(actions)
setElements(tmpelements)
}
if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) {
const newEdgeUuid = uuid.v4()
const newcybranch = {
"source": workflow.start,
"target": newNodeId,
"_id": newEdgeUuid,
"id": newEdgeUuid,
"hasErrors": false,
}
const edgeToBeAdded = {
group: "edges",
data: newcybranch,
}
console.log("SHOULD STITCH WITH STARTNODE")
cy.add(edgeToBeAdded)
}
workflow.actions.push(newAppData)
setWorkflow(workflow)
if (newAppPopup) {
//alert.error("SHOULD MAKE USER AUTHENTICATE THE APP OR SET hasError")
//alert.info("Remember: set the authentication for the user itself, not the app")
}
}
}
}
const handleDragStop = (e, app) => {
newNodeId = ""
console.log("STOP!: ", e)
console.log("APP!: ", app)
}
const appScrollStyle = {
overflow: "scroll",
maxHeight: bodyHeight-appBarSize-55,
minHeight: bodyHeight-appBarSize-55,
overflowY: "auto",
overflowX: "hidden",
}
const runAppSearch = (event) => {
setAppSearch(event.target.value)
setFilteredApps(apps.filter(app => app.name.toLowerCase().includes(event.target.value.trim().toLowerCase())))
}
const AppView = () => {
return(
<div style={appViewStyle}>
<div style={{flex: "1"}}>
<div style={appScrollStyle}>
{/*
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
minHeight: "50px",
marginLeft: "5px",
maxWidth: "95%",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={"Search apps"}
onChange={(event) => {
runAppSearch(event)
}}
/>
*/}
{filteredApps.map(app=> {
// FIXME - add label to apps, as this might be slow with A LOT of apps
var newAppname = app.name
newAppname = newAppname.replace("_", " ").charAt(0).toUpperCase()+newAppname.substring(1)
const maxlen = 24
if (newAppname.length > maxlen) {
newAppname = newAppname.slice(0, maxlen)+".."
}
// Description fucks this up - fix overflow :)
//const maxdesclen = 5
//var desc = app.description
//if (newAppname.length > maxdesclen) {
// desc = desc.slice(0, maxdesclen)+".."
//}
const image = "url("+app.large_image+")"
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={paperAppStyle} >
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}}>
</div>
<Grid container style={{margin: "10px 10px 10px 10px", flex: "10"}}>
<Grid item>
<div style={{height: 80, width: 80, backgroundImage: image, backgroundSize: "cover", backgroundRepeat: "no-repeat"}} />
</Grid>
<Grid style={{display: "flex", flexDirection: "column", marginLeft: "20px"}}>
<Grid item style={{flex: 1}}>
<h4 style={{marginBottom: "0px", marginTop: "5px"}}>{newAppname}</h4>
</Grid>
<Grid item style={{flex: 1, width: "100%", }}>
Short description...
</Grid>
<Grid item style={{flex: 1}}>
Version: {app.app_version}
</Grid>
</Grid>
</Grid>
</Paper>
</Draggable>
)
})}
</div>
</div>
</div>
)
}
const getNextActionName = (appName) => {
var highest = ""
//label = name + _number
for (var key in workflow.actions) {
const item = workflow.actions[key]
if (item.app_name === appName) {
var number = item.label.split("_")
if (isNaN(number[-1]) && parseInt(number[number.length-1]) > highest) {
highest = number[number.length-1]
}
}
}
if (highest) {
return appName+"_"+(parseInt(highest)+1)
} else {
return appName+"_"+1
}
}
const setNewSelectedAction = (e) => {
const newaction = selectedApp.actions.find(a => a.name === e.target.value)
// Does this one find the wrong one?
selectedAction.name = newaction.name
selectedAction.parameters = JSON.parse(JSON.stringify(newaction.parameters))
// FIXME - this is broken sometimes lol
//var env = environments.find(a => a.name === newaction.environment)
//if ((!env || env === undefined) && selectedAction.environment === undefined ) {
// env = environments[0]
//}
//setSelectedActionEnvironment(env)
setSelectedAction(selectedAction)
setSelectedActionName(e.target.value)
}
// APPSELECT at top
// appname & version
// description
// ACTION select
const selectedNameChange = (event) => {
selectedAction.label = event.target.value
setSelectedAction(selectedAction)
}
const selectedTriggerChange = (event) => {
selectedTrigger.label = event.target.value
setSelectedTrigger(selectedTrigger)
}
const getParents = (action) => {
var allkeys = [action.id]
var handled = []
var results = []
while(true) {
for (var key in allkeys) {
var currentnode = cy.getElementById(allkeys[key])
if (handled.includes(currentnode.data().id)) {
continue
} else {
// Get the name / label here too?
handled.push(currentnode.data().id)
results.push(currentnode.data())
}
if (currentnode.length === 0) {
continue
}
const incomingEdges = currentnode.incomers('edge')
if (incomingEdges.length === 0) {
continue
}
for (var i = 0; i < incomingEdges.length; i++) {
var tmp = incomingEdges[i]
if (!allkeys.includes(tmp.data().source)) {
allkeys.push(tmp.data().source)
}
}
}
if (results.length === allkeys.length) {
break
}
}
// Remove self
results = results.filter(data => data.id !== action.id)
results = results.filter(data => data.type !== "TRIGGER")
results.push({"label": "Execution Argument", "type": "INTERNAL"})
return results
}
// BOLD name: type: required?
// FORM
// Dropdown -> static, action, local env, global env
// VALUE (JSON)
// {data.name}, {data.description}, {data.required}, {data.schema.type}
const AppActionArguments = (props) => {
const [selectedActionParameters, setSelectedActionParameters] = React.useState([])
const [selectedVariableParameter, setSelectedVariableParameter] = React.useState()
useEffect(() => {
if (selectedActionParameters !== null && selectedActionParameters.length === 0) {
if (requiresAuthentication) {
console.log("ADD AUTHENTICATION FIELDS")
}
if (selectedAction.parameters !== null && selectedAction.parameters.length > 0) {
setSelectedActionParameters(selectedAction.parameters)
}
}
if ((selectedVariableParameter === null || selectedVariableParameter === undefined) && (workflow.workflow_variables !== null && workflow.workflow_variables.length > 0)) {
// FIXME - this is the bad thing
setSelectedVariableParameter(workflow.workflow_variables[0].name)
}
})
const changeActionParameter = (event, count) => {
selectedActionParameters[count].value = event.target.value
selectedAction.parameters[count].value = event.target.value
setSelectedAction(selectedAction)
//setUpdate(event.target.value)
}
const changeActionParameterVariable = (fieldvalue, count) => {
setSelectedVariableParameter(fieldvalue)
// this isn't updated anywhere in the workflow
// setSelectedActionName({})
// setSelectedAction({})
// setSelectedTrigger({})
// setSelectedApp({})
// setSelectedEdge({})
selectedActionParameters[count].action_field = fieldvalue
selectedAction.parameters = selectedActionParameters
setSelectedActionName(selectedActionName)
setSelectedApp(selectedApp)
setSelectedAction(selectedAction)
setUpdate(fieldvalue)
}
// Sets ACTION_RESULT things
const changeActionParameterActionResult = (fieldvalue, count) => {
//cy.nodes().forEach(function( ele ) {
// if (ele.data()["label"] === fieldvalue) {
// selectedActionParameters[count].action_field = ele.id()
// return
// }
//});
selectedActionParameters[count].action_field = fieldvalue
selectedAction.parameters = selectedActionParameters
// FIXME - check if startnode
// Set value
setSelectedActionName(selectedActionName)
setSelectedApp(selectedApp)
setSelectedAction(selectedAction)
setUpdate(fieldvalue)
}
const changeActionParameterVariant = (data, count) => {
selectedActionParameters[count].variant = data
selectedActionParameters[count].value = ""
if (data === "ACTION_RESULT") {
var parents = getParents(selectedAction)
if (parents.length > 0) {
selectedActionParameters[count].action_field = parents[0].label
} else {
selectedActionParameters[count].action_field = ""
}
} else if (data === "WORKFLOW_VARIABLE") {
if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) {
selectedActionParameters[count].action_field = workflow.workflow_variables[0].name
}
}
selectedAction.parameters = selectedActionParameters
// This is a stupid workaround to make it refresh rofl
setSelectedActionName({})
setSelectedAction({})
setSelectedTrigger({})
setSelectedApp({})
setSelectedEdge({})
// FIXME - check if startnode
// Set value
setSelectedActionName(selectedActionName)
setSelectedApp(selectedApp)
setSelectedAction(selectedAction)
}
// FIXME: Issue #40 - selectedActionParameters not reset
if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) {
return (
<div style={{marginTop: "30px"}}>
<b>Arguments</b>
{selectedActionParameters.map((data, count) => {
if (data.variant === "") {
data.variant = "STATIC_VALUE"
}
var staticcolor = "inherit"
var actioncolor = "inherit"
var varcolor = "inherit"
var multiline
if (data.multiline !== undefined && data.multiline !== null && data.multiline === true) {
multiline = true
}
if (data.value !== undefined && data.value.startsWith("{") && data.value.endsWith("}")) {
multiline = 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}}
InputProps={{
style:{
color: "white",
minHeight: "50px",
marginLeft: "5px",
maxWidth: "95%",
fontSize: "1em",
},
}}
fullWidth
multiline={multiline}
rows="5"
color="primary"
defaultValue={data.value}
placeholder={placeholder}
onChange={(event) => {
changeActionParameter(event, count)
}}
onBlur={(event) => {
// Super basic check
if (event.target.value.startsWith("{")) {
console.log("VALIDATING JSON")
try {
JSON.parse(event.target.value)
} catch (e) {
alert.error("Failed to parse json: ", e)
}
}
}}
/>
// Remap data based on variant
if (data.variant === "STATIC_VALUE") {
staticcolor = "#f85a3e"
} else if (data.variant === "ACTION_RESULT") {
// Gets the parents of the current node
var parents = getParents(selectedAction)
actioncolor = "#f85a3e"
// set the datafield
//var datafieldvalue = "Error: No parents. Action not eligible"
//if (parents.length > 0) {
// datafieldvalue = parents[0].label
//}
const fixedActionText = selectedActionParameters[count].value
datafield =
<div>
<Select
PaperProps={{
style: {
backgroundColor: inputColor,
}
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
value={selectedActionParameters[count].action_field}
fullWidth
onChange={(e) => {
changeActionParameterActionResult(e.target.value, count)
}}
style={{backgroundColor: surfaceColor, color: "white", height: "50px"}}
>
{parents.map(data => (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.label}>
{data.label}
</MenuItem>
))}
</Select>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
defaultValue={data.value}
helperText={<div style={{marginLeft: "5px", color:"white", marginBottom: "2px",}}>Example: $.body will get "data" from {'{"body": "data"}'}</div>}
placeholder="Action variable ($.)"
onChange={(event) => {
changeActionParameter(event, count)
}}
/></div>
} else if (data.variant === "WORKFLOW_VARIABLE") {
varcolor = "#f85a3e"
if (workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) {
setCurrentView(2)
datafield =
<div>
<div>
Looks like you don't have any variables yet.
</div>
<div style={{width: "100%", margin: "auto"}}>
<Button style={{margin: "auto", marginTop: "10px"}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>New workflow variable</Button>
</div>
</div>
} else {
// FIXME - this is a shitty solution that needs re-renders all the time
datafield =
<Select
PaperProps={{
style: {
backgroundColor: inputColor,
}
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
fullWidth
value={selectedAction.parameters[count].action_field}
onChange={(e) => {
changeActionParameterVariable(e.target.value, count)
}}
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
>
{workflow.workflow_variables.map(data => (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
{data.name}
</MenuItem>
))}
</Select>
}
}
var itemColor = "#f85a3e"
if (!data.required) {
itemColor = "#ffeb3b"
}
return (
<div>
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: itemColor, marginRight: "10px"}}/>
<div style={{flex: "10"}}>
<b>{data.name}: </b>
</div>
<Tooltip color="primary" title="Static data" placement="top">
<div style={{cursor: "pointer", color: staticcolor}} onClick={(e) => {
e.preventDefault()
changeActionParameterVariant("STATIC_VALUE", count)
}}>
<CreateIcon />
</div>
</Tooltip>
&nbsp;|&nbsp;
<Tooltip color="primary" title="Data from previous action" placement="top">
<div style={{cursor: "pointer", color: actioncolor}} onClick={(e) => {
e.preventDefault()
changeActionParameterVariant("ACTION_RESULT", count)
}}>
<AppsIcon />
</div>
</Tooltip>
&nbsp;|&nbsp;
<Tooltip color="primary" title="Use local variable" placement="top">
<div style={{cursor: "pointer", color: varcolor}} onClick={(e) => {
e.preventDefault()
changeActionParameterVariant("WORKFLOW_VARIABLE", count)
}}>
<FavoriteBorderIcon />
</div>
</Tooltip>
</div>
{datafield}
</div>
)})}
</div>
)
}
return null
}
//height: "100%",
const appApiViewStyle = {
display: "flex",
flexDirection: "column",
backgroundColor: "#1F2023",
color: "white",
paddingRight: 10,
paddingLeft: 10,
minHeight: "100%",
}
const defineStartnode = () => {
var oldstartnode = cy.getElementById(workflow.start)
if (oldstartnode.length > 0) {
oldstartnode[0].data("isStartNode", false)
var oldnodecnt = workflow.actions.findIndex(a => a.id === workflow.start)
workflow.actions[oldnodecnt].isStartNode = false
}
var newstartnode = cy.getElementById(selectedAction.id)
if (newstartnode.length > 0) {
newstartnode[0].data("isStartNode", true)
var newnodecnt = workflow.actions.findIndex(a => a.id === selectedAction.id)
workflow.actions[newnodecnt].isStartNode = true
}
// Find branches with triggers as source nodes
// Move these targets to be the new node
// Set arrows pointing to new startnode with errors
for (var key in workflow.branches) {
var item = workflow.branches[key]
if (item.destination_id === oldstartnode[0].data()["id"]) {
var curbranch = cy.getElementById(item.id)
if (curbranch.length > 0) {
//console.log(curbranch[0].data())
//curbranch[0].data("target", selectedAction.id)
curbranch[0].data("hasErrors", true)
//workflow.branches[key].destination_id = selectedAction.id
//console.log(curbranch[0].data())
}
}
}
workflow.start = selectedAction.id
setWorkflow(workflow)
//setStartNode(selectedAction.id)
}
const appApiView = Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedApp).length > 0 ?
<div style={appApiViewStyle}>
<div style={{display: "flex", minHeight: 40, marginBottom: 30}}>
<div style={{flex: 1}}>
<h3 style={{marginBottom: 5}}>{selectedAction.app_name}</h3>
<Link to="/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}}>What are apps?</Link>
{selectedAction.errors !== null && selectedAction.errors.length > 0 ?
<div>
Errors: {selectedAction.errors.join("\n")}
</div>
: null
}
</div>
<div style={{flex: "1"}}>
<Button disabled={selectedAction.id === workflow.start} style={{zIndex: 5000, marginTop: "15px",}} color="primary" variant="outlined" onClick={(e) => {
defineStartnode(e)
}}>Set startnode</Button>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div>
Name
</div>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={selectedAction.label}
onChange={selectedNameChange}
/>
<div style={{marginTop: "20px"}}>
Environment
<Select
value={selectedActionEnvironment === undefined || selectedActionEnvironment.Name === undefined ? "" : selectedActionEnvironment.Name}
PaperProps={{
style: {
backgroundColor: inputColor,
}
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
fullWidth
onChange={(e) => {
const env = environments.find(a => a.Name === e.target.value)
console.log("FOUND: ", e.target.value)
console.log("FOUND2: ", env)
setSelectedActionEnvironment(env)
selectedAction.environment = env.Name
setSelectedAction(selectedAction)
}}
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
>
{environments.map(data => (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.Name}>
{data.Name}
</MenuItem>
))}
</Select>
</div>
{/*requiresAuthentication ?
<div style={{marginTop: "20px"}}>
<Button fullWidth style={{margin: "auto", marginTop: "10px",}} color="primary" variant="contained" onClick={() => setAuthenticationModalOpen(true)}>
AUTHENTICATE
</Button>
</div>
: null*/}
<Divider style={{marginTop: "20px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div style={{flex: "6", marginTop: "20px"}}>
<div>
<b>Actions</b>
</div>
<Select
value={selectedActionName}
fullWidth
onChange={setNewSelectedAction}
style={{backgroundColor: inputColor, color: "white", height: 50}}
SelectDisplayProps={{
style: {
marginLeft: 10,
maxHeight: 200,
}
}}
>
{selectedApp.actions.map(data => {
var newActionname = data.name
if (data.label !== undefined && data.label !== null && data.label.length > 0) {
newActionname = data.label
}
// ROFL FIXME - loop
newActionname = newActionname.replace("_", " ")
newActionname = newActionname.replace("_", " ")
newActionname = newActionname.replace("_", " ")
newActionname = newActionname.replace("_", " ")
newActionname = newActionname.charAt(0).toUpperCase()+newActionname.substring(1)
return (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.name}>
{newActionname}
</MenuItem>
)
})}
</Select>
<div style={{marginTop: "10px", borderColor: "white", borderWidth: "2px", marginBottom: 200}}>
<AppActionArguments key={selectedAction.id} selectedAction={selectedAction} />
</div>
</div>
</div>
: null
const headerSize = 68
const rightsidebarStyle = {
position: "fixed",
right: 0,
top: headerSize+1,
height: "100%",
bottom: 0,
minWidth: "350px",
maxWidth: "350px",
borderLeft: "1px solid rgb(91, 96, 100)",
overflow: "scroll",
overflowX: "auto",
overflowY: "auto",
}
const setTriggerFolderWrapperMulti = event => {
const { options } = event.target
const value = []
for (let i = 0, l = options.length; i < l; i += 1) {
if (options[i].selected) {
value.push(options[i].value)
}
}
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = [[]]
workflow.triggers[selectedTriggerIndex].parameters = [[]]
}
// This is a dirty workaround for the static values in the go backend and datastore db
const fixedValue = value.join(splitter)
selectedTrigger.parameters[0] = {"value": fixedValue, "name": "outlookfolder"}
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": fixedValue, "name": "outlookfolder"}
// This resets state for some reason (:
setSelectedActionName({})
setSelectedAction({})
setSelectedTrigger({})
setSelectedApp({})
setSelectedEdge({})
// Set value
setSelectedTrigger(selectedTrigger)
setWorkflow(workflow)
};
//const setTriggerFolderWrapper = (event) => {
// if (selectedTrigger.parameters === null) {
// selectedTrigger.parameters = []
// workflow.triggers[selectedTriggerIndex].parameters = []
// }
// const folder = triggerFolders.find(a => a.displayName === event.target.value)
// console.log(event.target.value)
// console.log(folder)
//
// if (folder !== undefined) {
// workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": folder.displayName, "name": "outlookfolder", "id": folder.id}
// selectedTrigger.parameters[0] = {"value": folder.displayName, "name": "outlookfolder", "id": folder.id}
// setWorkflow(workflow)
// // This resets state for some reason (:
// setSelectedActionName({})
// setSelectedActionEnvironment({})
// setSelectedAction({})
// setSelectedTrigger({})
// setSelectedApp({})
// setSelectedEdge({})
// // Set value
// setSelectedTrigger(selectedTrigger)
// } else {
// alert.error("Some error occurred with folder "+event.target.value)
// }
//}
const setTriggerCronWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = []
}
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "cron"}
setWorkflow(workflow)
}
const setTriggerOptionsWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = []
}
const splitItems = workflow.triggers[selectedTriggerIndex].parameters[2].value.split(",")
console.log(splitItems)
if (splitItems.includes(value)) {
for( var i = 0; i < splitItems.length; i++){
if (splitItems[i] === value) {
splitItems.splice(i, 1)
}
}
} else {
splitItems.push(value)
}
for( var i = 0; i < splitItems.length; i++){
if (splitItems[i] === "") {
splitItems.splice(i, 1)
}
}
workflow.triggers[selectedTriggerIndex].parameters[2].value = splitItems.join(",")
console.log(splitItems)
setWorkflow(workflow)
setLocalFirstrequest(!localFirstrequest)
}
const setTriggerTextInformationWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = []
}
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "alertinfo"}
setWorkflow(workflow)
}
const setTriggerBodyWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = []
workflow.triggers[selectedTriggerIndex].parameters[0] = {"value": value, "name": "cron"}
}
workflow.triggers[selectedTriggerIndex].parameters[1] = {"value": value, "name": "execution_argument"}
setWorkflow(workflow)
}
const AppConditionHandler = (props) => {
const { tmpdata, type } = props
if (tmpdata === undefined) {
return tmpdata
}
const [data, ] = useState(tmpdata)
const [multiline, setMultiline] = useState(false)
if (data.variant === "") {
data.variant = "STATIC_VALUE"
}
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}}
InputProps={{
style:{
color: "white",
minHeight: "50px",
marginLeft: "5px",
maxWidth: "95%",
fontSize: "1em",
},
}}
fullWidth
multiline={multiline}
rows={5}
color="primary"
defaultValue={data.value}
placeholder={placeholder}
onClick={() => {
console.log("CHANGE FIELD")
}}
onBlur={(e) => {
changeActionVariable(data.action_field, e.target.value)
}}
/>
console.log(data)
// Remap data based on variant
if (data.variant === "STATIC_VALUE") {
staticcolor = "#f85a3e"
} else if (data.variant === "ACTION_RESULT") {
// Gets the parents of the current node
var parents = getParents(workflow.actions.find(a => a.id === selectedEdge["target"]))
actioncolor = "#f85a3e"
// set the datafield
//var datafieldvalue = "Error: No parents. Action not eligible"
//if (parents.length > 0) {
// datafieldvalue = parents[0].label
//}
datafield =
<div>
<Select
value={data.action_field}
fullWidth
onChange={(e) => {
changeActionVariable(e.target.value, data.value)
}}
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
>
{parents.map(data => (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data.label}>
{data.label}
</MenuItem>
))}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
</Select>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
defaultValue={data.value}
placeholder="Action variable ($.)"
onBlur={(e) => {
changeActionVariable(data.action_field, e.target.value)
}}
/></div>
} else if (data.variant === "WORKFLOW_VARIABLE") {
varcolor = "#f85a3e"
if (workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) {
setCurrentView(2)
datafield =
<div>
<div>
Looks like you don't have any variables yet.
</div>
<div style={{width: "100%", margin: "auto"}}>
<Button style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => setVariablesModalOpen(true)}>New workflow variable</Button>
</div>
</div>
} else {
// FIXME - this is a shitty solution that needs re-renders all the time
datafield =
<Select
fullWidth
value={data.action_field}
PaperProps={{
style: {
backgroundColor: inputColor,
}
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
onChange={(e) => {
changeActionVariable(e.target.value, data.value)
}}
style={{backgroundColor: surfaceColor, color: "white", height: "50px"}}
>
{workflow.workflow_variables.map(data => (
<MenuItem style={{backgroundColor: surfaceColor, color: "white"}} value={data.name}>
{data.name}
</MenuItem>
))}
</Select>
}
}
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) {
console.log(parents)
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>
<Tooltip color="primary" title="Static data" placement="top">
<div style={{cursor: "pointer", color: staticcolor}} onClick={(e) => {
e.preventDefault()
changeActionParameterVariant("STATIC_VALUE")
}}>
<CreateIcon />
</div>
</Tooltip>
&nbsp;|&nbsp;
<Tooltip color="primary" title="Data from previous action" placement="top">
<div style={{cursor: "pointer", color: actioncolor}} onClick={(e) => {
e.preventDefault()
changeActionParameterVariant("ACTION_RESULT")
}}>
<AppsIcon />
</div>
</Tooltip>
&nbsp;|&nbsp;
<Tooltip color="primary" title="Use local variable" placement="top">
<div style={{cursor: "pointer", color: varcolor}} onClick={(e) => {
e.preventDefault()
changeActionParameterVariant("WORKFLOW_VARIABLE")
}}>
<FavoriteBorderIcon />
</div>
</Tooltip>
</div>
{datafield}
</div>
)
}
const menuItemStyle = {
color: "white",
}
const conditionsModal =
<Dialog modal
open={conditionsModalOpen}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: "800px",
},
}}
onClose={() => {
setConditionsModalOpen(false)
setSourceValue({})
setConditionValue({})
setDestinationValue({})
}}
>
<FormControl>
<DialogTitle><div style={{color:"white"}}>Condition</div></DialogTitle>
<DialogContent style={{display: "flex"}}>
<div style={{flex: "2"}}>
<AppConditionHandler tmpdata={sourceValue} setData={setSourceValue} type={"source"} />
</div>
<div style={{flex: "1", margin: "auto", marginBottom: "0px"}}>
<Button color="primary" variant="outlined" style={{height: "50px",}} 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 = "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 = "matches regex"
setConditionValue(conditionValue)
setVariableAnchorEl(null)
}} key={"matches regex"}>matches regex</MenuItem>
</Menu>
</div>
<div style={{flex: "2"}}>
<AppConditionHandler tmpdata={destinationValue} setData={setDestinationValue} type={"destination"} />
</div>
</DialogContent>
<DialogActions>
<Button
style={{borderRadius: "0px"}}
onClick={() => {
setSelectedEdge({})
var data = {
condition: conditionValue,
source: sourceValue,
destination: destinationValue,
}
setConditionsModalOpen(false)
if (selectedEdge.conditions === undefined) {
selectedEdge.conditions = [data]
} else {
const curedgeindex = selectedEdge.conditions.findIndex(data => data.source.id === sourceValue.id)
if (curedgeindex < 0) {
selectedEdge.conditions.push(data)
} else {
selectedEdge.conditions[curedgeindex] = data
}
}
setSelectedEdge(selectedEdge)
workflow.branches[selectedEdgeIndex] = selectedEdge
setWorkflow(workflow)
}} color="primary">
Submit
</Button>
</DialogActions>
</FormControl>
</Dialog>
const EdgeSidebar = () => {
const ConditionHandler = (condition, index) => {
const [open, setOpen] = React.useState(false)
const [anchorEl, setAnchorEl] = React.useState(null)
const deleteCondition = (conditionIndex) => {
console.log(selectedEdge)
if (selectedEdge.conditions.length === 1) {
selectedEdge.conditions = []
} else {
selectedEdge.conditions.splice(conditionIndex, 1)
}
setSelectedEdge(selectedEdge)
setOpen(false)
setUpdate("delete "+conditionIndex)
}
const paperVariableStyle = {
minHeight: "50px",
maxHeight: "50px",
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 square style={paperVariableStyle} onClick={() => {}}>
<div style={{marginLeft: "10px", marginTop: "5px", marginBottom: "5px", width: "2px", backgroundColor: "orange", marginRight: "5px"}} />
<div style={{display: "flex", width: "100%"}}>
<div style={{flex: "10", display: "flex"}} onClick={() => {
setSourceValue(condition.source)
setConditionValue(condition.condition)
setDestinationValue(condition.destination)
setConditionsModalOpen(true)
}}>
<div style={{flex: "1", textAlign: "center", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}}>
{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"}} 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: "center", marginTop: "15px", marginLeft: "10px", overflow: "hidden"}}>
{condition.destination.value}
</div>
</div>
<div style={{flex: "1", marginLeft: "0px"}}>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={menuClick}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
keepMounted
open={open}
PaperProps={{
style: {
backgroundColor: surfaceColor,
}
}}
onClose={() => {
setOpen(false)
setAnchorEl(null)
}}
>
<MenuItem style={{backgroundColor: surfaceColor, 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>
<Link to="/docs/conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div>
Conditions
</div>
{injectedData}
<Button style={{margin: "auto", marginTop: "10px",}} color="primary" variant="outlined" onClick={() => {
setSourceValue({"name": "source", "value": "", "variant": "STATIC_VALUE", "action_field": "", "id": conditionId})
setConditionValue({"name": "condition", "value": "equals", "id": conditionId})
setDestinationValue({"name": "destination", "value": "", "variant": "STATIC_VALUE", "action_field": "", "id": conditionId})
setConditionsModalOpen(true)
}} fullWidth>New condition</Button>
</div>
)
}
// 1. GET the trigger authentication data
// 2. Parse the fields that are used (outlook & gmail)
// 3. Parse the folders that are selected
// 4. Start / stop
const EmailSidebar = () => {
if (Object.getOwnPropertyNames(selectedTrigger).length === 0) {
return null
}
if (workflow.triggers[selectedTriggerIndex] === undefined) {
return null
}
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
workflow.triggers[selectedTriggerIndex].parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}]
selectedTrigger.parameters = [{"value": "No folders selected yet", "name": "outlookfolder"}]
setWorkflow(workflow)
setSelectedTrigger(selectedTrigger)
}
const setFolders = () => {
fetch(globalUrl+"/functions/outlook/getFolders?trigger_id="+selectedTrigger.id, {
method: "GET",
headers: {"content-type": "application/json"},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
throw new Error("No folders :o!")
}
return response.json()
})
.then((responseJson) => {
setTriggerFolders(responseJson)
if (workflow.triggers[selectedTriggerIndex].parameters.length === 0 && responseJson.length > 0) {
workflow.triggers[selectedTriggerIndex].parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}]
selectedTrigger.parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}]
setWorkflow(workflow)
setSelectedTrigger(selectedTrigger)
}
})
.catch(error => {
console.log(error.toString())
})
}
const getTriggerAuth = () => {
fetch(globalUrl+"/api/v1/triggers/"+selectedTrigger.id, {
method: "GET",
headers: {"content-type": "application/json"},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
throw new Error("No trigger info :o!")
}
return response.json()
})
.then((responseJson) => {
setTriggerAuthentication(responseJson)
})
.catch(error => {
console.log(error.toString())
})
}
// Getting the triggers and the folders if they exist
// This is horrible hahah
if (localFirstrequest) {
getTriggerAuth()
setFolders()
setLocalFirstrequest(false)
}
const outlookButton =
<Button variant="contained" style={{flex: "1",}} onClick={() => {
// When window closes, it should get all the folders for the user from backend
const redirectUri = "http%3A%2F%2Fshuffler.io"
const url = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=70e37005-c954-4290-b573-d4b94e484336&redirect_uri="+redirectUri+"%2Ffunctions%2Foutlook%2Fregister&resource=https%3A%2F%2Fgraph.microsoft.com&response_type=code&scope=Mail.Read+User.Read+https%3A%2F%2Foutlook.office.com%2Fmail.read&state=workflow_id%3D"+props.match.params.key+"%26trigger_id%3D"+selectedTrigger.id+"%26username%3Drheyix.yt@gmail.com%26type%3Doutlook"
var newwin = window.open(url, "", "width=200,height=100")
var data = {}
// Check whether we got a callback somewhere
var id = setInterval(function () {
fetch(globalUrl+"/api/v1/triggers/"+selectedTrigger.id, {
method: "GET",
headers: {"content-type": "application/json"},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
throw new Error("No trigger info :o!")
}
return response.json()
})
.then((responseJson) => {
setTriggerAuthentication(responseJson)
clearInterval(id)
newwin.close()
setFolders()
})
.catch(error => {
console.log(error.toString())
})
}, 2500)
console.log(data)
saveWorkflow(workflow)
}} color="primary">
Outlook
</Button>
// FIXME - set everything in here to multifolder etc
var triggerInfo = "SET UP BUT NO TYPE :)"
if (Object.getOwnPropertyNames(triggerAuthentication).length > 0) {
// Should get the folders if they don't already exist
if (triggerAuthentication.type === "outlook") {
triggerInfo = <div>
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
<div style={{flex: "10"}}>
<b>Login: </b>
</div>
</div>
{outlookButton}
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
<div style={{flex: "10"}}>
<b>Folders: </b>(hold CTRL to select multiple)
</div>
</div>
<Select
multiple
native
rows="10"
value={selectedTrigger.parameters[0].value.split(splitter)}
style={{backgroundColor: inputColor, color: "white"}}
PaperProps={{
style: {
height: "200px",
backgroundColor: inputColor,
}
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
onChange={(e) => {
//setTriggerFolderWrapper(e)
setTriggerFolderWrapperMulti(e)
}}
fullWidth
input={<Input id="select-multiple-native" />}
key={selectedTrigger}
>
{triggerFolders.map(folder => {
var folderItem = <option key={folder.displayName} value={folder.displayName} style={{marginLeft: "10px", fontSize: "1.2em"}}>
{folder.displayName}
</option>
if (folder.childFolderCount > 0) {
// Here to handle subfolders sometime later
folderItem =
<option key={folder.displayName} value={folder.displayName} style={{marginLeft: "10px", fontSize: "1.2em"}}>
{folder.displayName}
</option>
}
return folderItem
})}
</Select>
</div>
} else if (triggerAuthentication.type === "gmail") {
triggerInfo = "SPECIAL GMAIL"
}
}
// Check
const argumentView = Object.getOwnPropertyNames(triggerAuthentication).length > 0 ?
<div>
{triggerInfo}
</div>
:
<div>
<div style={{marginTop: "20px", marginBottom: "7px", display: "flex"}}>
<div style={{width: "17px", height: "17px", borderRadius: 17 / 2, backgroundColor: "#f85a3e", marginRight: "10px"}}/>
<div style={{flex: "10"}}>
<b>Login to either: </b>
</div>
</div>
{outlookButton}
<Button variant="contained" style={{marginLeft: "5px", flex: "1",}} onClick={() => {
alert.error("REMOVE THIS FIELD FOR GMAIL - make it oAuth something")
}} color="primary">
Gmail
</Button>
</div>
return(
<div style={appApiViewStyle}>
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
<Link to="/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div>
Name
</div>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={selectedTrigger.label}
onChange={selectedTriggerChange}
/>
<div style={{marginTop: "20px"}}>
Environment:
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
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 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>
<Link to="/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div>
Name
</div>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={selectedTrigger.label}
onChange={selectedTriggerChange}
/>
<div style={{marginTop: "20px"}}>
Environment:
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
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"}}>
<div>
<b>Arguments</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}}
onClick={() => {
//alert.info("Saved URI to clipboard")
console.log("Copy to clipboooooard")
}}
InputProps={{
style:{
color: "white",
height: "50px",
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)
}}
/>
<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}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
marginTop: "3px",
fontSize: "1em",
},
}}
fullWidth
disabled
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",}} disabled={selectedTrigger.status === "running"} onClick={() => {
newWebhook(selectedTrigger)
}} color="primary">
Start
</Button>
<Button style={{flex: "1",}} disabled={selectedTrigger.status !== "running"} onClick={() => {
deleteWebhook(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Stop
</Button>
</div>
</div>
</div>
</div>
)
}
return null
}
//const getTriggerAuth = (trigger_id) => {
// fetch(globalUrl+"/api/v1/triggers/"+trigger_id, {
// method: "GET",
// headers: {"content-type": "application/json"},
// credentials: "include",
// })
// .then((response) => response.json())
// .then((responseJson) => {
// if (responseJson.success) {
// console.log("SUCCESS")
// console.log(responseJson)
// } else {
// console.log("FAIL")
// }
// })
// .catch(error => {
// console.log(error.toString())
// });
//}
const stopMailSub = (trigger, triggerindex) => {
// DELETE
if (trigger.id === undefined) {
return
}
alert.info("Stopping trigger")
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/outlook/"+trigger.id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Status not 200 for stream results :O!")
}
return response.json()
})
.then((responseJson) => {
if (responseJson.success) {
alert.success("Successfully stopped trigger")
// Set the status
workflow.triggers[triggerindex].status = "stopped"
trigger.status = "stopped"
setWorkflow(workflow)
setSelectedTrigger(trigger)
saveWorkflow(workflow)
} else {
alert.error("Failed stopping trigger: "+responseJson.reason)
}
})
.catch(error => {
alert.error(error.toString())
})
}
const startMailSub = (trigger, triggerindex) => {
var folders = []
const splitItem = workflow.triggers[selectedTriggerIndex].parameters[0].value.split(splitter)
for (var key in splitItem) {
const item = splitItem[key]
const curfolder = triggerFolders.find(a => a.displayName === item)
if (curfolder === undefined) {
alert.error("Something went wrong with outlook folder "+item)
return
}
folders.push(curfolder.id)
}
alert.info("Creating outlook subscription with name " + trigger.name)
const data = {
"name": trigger.name,
"folders": folders,
"id": trigger.id,
}
fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/outlook", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!")
}
return response.json()
})
.then((responseJson) => {
if (!responseJson.success) {
alert.error("Failed to start outlook: " + responseJson.reason)
} else {
alert.success("Successfully started outlook sub")
workflow.triggers[triggerindex].status = "running"
trigger.status = "running"
setWorkflow(workflow)
setSelectedTrigger(trigger)
saveWorkflow(workflow)
}
})
.catch(error => {
alert.error(error.toString())
})
}
const newWebhook = (trigger) => {
const hookname = trigger.label
if (hookname.length === 0) {
alert.error("Missing name")
return
}
if (trigger.id.length !== 36) {
alert.error("Missing id")
return
}
alert.info("Starting webhook")
const data = {
"name": hookname,
"type": "webhook",
"id": trigger.id,
"workflow": workflow.id,
}
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")
workflow.triggers[selectedTriggerIndex].status = "running"
trigger.status = "running"
setWorkflow(workflow)
setSelectedTrigger(trigger)
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 (responseJson.success) {
alert.success("Successfully stopped webhook")
// Set the status
workflow.triggers[triggerindex].status = "stopped"
trigger.status = "stopped"
setWorkflow(workflow)
setSelectedTrigger(trigger)
saveWorkflow(workflow)
} else {
alert.error("Failed stopping webhook: "+responseJson.reason)
}
})
.catch(error => {
alert.error(error.toString())
})
}
const UserinputSidebar = () => {
if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) {
console.log(workflow.triggers[selectedTriggerIndex])
console.log(selectedTrigger)
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"}
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>
<Link to="/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div>
Name
</div>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={selectedTrigger.label}
onChange={selectedTriggerChange}
/>
<div style={{marginTop: "20px"}}>
Environment:
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
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>Arguments</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}}
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].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>
</div>
</div>
)
}
return null
}
const ScheduleSidebar = () => {
if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) {
if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) {
workflow.triggers[selectedTriggerIndex].parameters = []
workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "cron", "value": "120"}
workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "execution_argument", "value": '{"example": {"json": "is cool"}}'}
setWorkflow(workflow)
}
return(
<div style={appApiViewStyle}>
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
<Link to="/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
<div>
Name
</div>
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={selectedTrigger.label}
onChange={selectedTriggerChange}
/>
<div style={{marginTop: "20px"}}>
Environment:
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: "50px",
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"}}>
<div>
<b>Arguments</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}}
InputProps={{
style:{
color: "white",
height: "50px",
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}}
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",}} disabled={selectedTrigger.status === "running"} onClick={() => {
submitSchedule(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Start
</Button>
<Button style={{flex: "1",}} disabled={selectedTrigger.status !== "running"} onClick={() => {
stopSchedule(selectedTrigger, selectedTriggerIndex)
}} color="primary">
Stop
</Button>
</div>
</div>
</div>
</div>
)
}
return null
}
const cytoscapeViewWidths = 650
const bottomBarStyle = {
position: "fixed",
right: 20,
left: leftBarSize,
bottom: 0,
minWidth: cytoscapeViewWidths,
maxWidth: cytoscapeViewWidths,
marginLeft: 20,
marginBottom: 20,
zIndex: 10,
}
const topBarStyle= {
position: "fixed",
right: 0,
left: leftBarSize,
top: appBarSize,
minWidth: cytoscapeViewWidths,
maxWidth: cytoscapeViewWidths,
marginLeft: 20,
marginBottom: 20,
}
const TopCytoscapeBar = () => {
return (
<div style={topBarStyle}>
<div style={{margin: 10}}>
<Breadcrumbs aria-label="breadcrumb" separator="" style={{color: "white",}}>
<Link to="/workflows" style={{textDecoration: "none", color: "inherit",}}>
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
<PolymerIcon style={{marginRight: 10}} />
Workflows
</h2>
</Link>
<h2>
{workflow.name}
</h2>
</Breadcrumbs>
</div>
</div>
)
}
const NoActionsBar = () => {
if (workflow.actions.length === 0 || !appAdded) {
return (
<div style={{position: "fixed", width: "100%", top: 300}}>
<div style={{width: 300, margin: "auto"}}>
<h3></h3>
</div>
</div>
)
}
return null
}
const BottomCytoscapeBar = () => {
const boxSize = 100
const executionButton = executionRunning ?
<Tooltip color="primary" title="Stop execution" placement="top">
<Button style={{height: boxSize, width: boxSize}} color="secondary" variant="contained" onClick={() => {
abortExecution()
}}>
<PauseIcon style={{ fontSize: 60}} />
</Button>
</Tooltip>
:
<Tooltip color="primary" title="Test execution" placement="top">
<Button disabled={executionRequestStarted} style={{height: boxSize, width: boxSize}} color="primary" variant="contained" onClick={() => {
executeWorkflow()
}}>
<PlayArrowIcon style={{ fontSize: 60}} />
</Button>
</Tooltip>
return(
<div style={bottomBarStyle}>
{executionButton}
<div style={{marginLeft: "10px", left: boxSize, bottom: 0, position: "absolute"}}>
<Tooltip color="primary" title="An argument to be used for execution. This is a variable available to every node in your workflow." placement="top">
<TextField
style={{backgroundColor: inputColor, }}
InputProps={{
style:{
height: 50,
color: "white",
marginLeft: 5,
maxWidth: "95%",
fontSize: "1em",
},
}}
color="secondary"
placeholder={"Execution Argument"}
defaultValue={executionText}
onBlur={(e) => {
setExecutionText(e.target.value)
}}
/>
</Tooltip>
<Tooltip color="primary" title="Save (ctrl+s)" placement="top">
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => saveWorkflow()}>
<SaveIcon />
</Button>
</Tooltip>
<Tooltip color="secondary" title="Fit to screen (ctrl+f)" placement="top">
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => cy.fit(null, 50)}>
<AspectRatioIcon />
</Button>
</Tooltip>
<Tooltip color="secondary" title="Remove selected item (del)" placement="top-start">
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
removeNode()
}}>
<DeleteIcon />
</Button>
</Tooltip>
<Tooltip color="secondary" title="Show executions" placement="top-start">
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
setExecutionModalOpen(true)
}}>
<DirectionsRunIcon />
</Button>
</Tooltip>
</div>
</div>
)
}
const RightSideBar = () => {
if (!rightSideBarOpen) {
return null
}
if (Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedApp).length > 0) {
//console.time('ACTIONSTART')
return(
<div style={rightsidebarStyle}>
{appApiView}
</div>
)
} else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
if (selectedTrigger.trigger_type === "SCHEDULE") {
console.log("SCHEDULE")
return(
<div style={rightsidebarStyle}>
<ScheduleSidebar />
</div>
)
} else if (selectedTrigger.trigger_type === "WEBHOOK") {
return(
<div style={rightsidebarStyle}>
<WebhookSidebar />
</div>
)
} else if (selectedTrigger.trigger_type === "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 executionModal =
<Drawer anchor={"right"} open={executionModalOpen} onClose={() => setExecutionModalOpen(false)} PaperProps={{style: {minWidth: 375, maxWidth: 375, 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"}}
onClick={() => {
getWorkflowExecution(props.match.params.key)
}} color="primary">
Refresh executions
</Button>
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
{workflowExecutions.length > 0 ?
<div>
{workflowExecutions.map(data => {
const statusColor = data.status === "FINISHED" ? "green" : data.status === "ABORTED" ? "red" : "orange"
const timeElapsed = data.completed_at-data.started_at
const resultsLength = data.results !== undefined && data.results !== null ? data.results.length : 0
const timestamp = new Date(data.started_at*1000).toISOString().split('.')[0].split("T").join(" ")
return (
<Paper elevation={5} square style={executionPaperStyle} onMouseOver={() => {}} onMouseOut={() => {}} onClick={() => {
setExecutionModalView(1)
setExecutionData(data)
}}>
<div style={{display: "flex", flex: 1}}>
<div style={{marginLeft: 5, width: 2, backgroundColor: statusColor, marginRight: 15}} />
<div style={{ marginTop: "auto", marginBottom: "auto", marginRight: 15, }}>
{timestamp}
</div>
{data.workflow.actions !== null ?
<Tooltip color="primary" title={resultsLength+" actions ran"} placement="top">
<div style={{marginRight: 10, marginTop: "auto", marginBottom: "auto",}}>
{resultsLength}/{data.workflow.actions.length}
</div>
</Tooltip>
: null}
</div>
<Tooltip title={"Inspect execution"} placement="top">
<KeyboardArrowRightIcon style={{marginTop: "auto", marginBottom: "auto"}}/>
</Tooltip>
</Paper>
)
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}}>
<h2 style={{color: "rgba(255,255,255,0.5)", cursor: "pointer"}} onClick={() => {setExecutionModalView(0)}}>
<DirectionsRunIcon style={{marginRight: 10}} />
Other Executions
</h2>
</Breadcrumbs>
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
<h2>Executing Workflow</h2>
{executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ?
<div>
<h3>Execution Argument: </h3>{executionData.execution_argument}
</div>
: null }
{executionData.status !== undefined && executionData.status.length > 0 ?
<div>
<b>Status: </b>{executionData.status}
</div>
: null
}
{executionData.started_at !== undefined ?
<div>
<b>Started: </b>{new Date(executionData.started_at*1000).toISOString()}
</div>
: null
}
{executionData.execution_id !== undefined && executionData.execution_id.length > 0 ?
<div>
<b>ID: </b>{executionData.execution_id}
</div>
: null
}
<Divider style={{backgroundColor: "white", marginTop: 30, marginBottom: 30,}}/>
<div style={{display: "flex", marginTop: 10, marginBottom: 30,}}>
<b>Actions</b>
<div>
{executionData.status !== undefined && executionData.status !== "ABORTED" && executionData.status !== "FINISHED" && executionData.status !== "FAILURE" ? <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 => {
var showResult = data.result.trim()
showResult.split(" None").join(" \"None\"")
//showResult = replaceAll(showResult, " None", " \"None\"")
var jsonvalid = true
try {
JSON.parse(showResult)
} catch (e) {
jsonvalid = false
}
const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version)
const imgsize = 50
const actionimg = curapp === null ?
null :
<img alt={data.action.app_name} src={curapp.large_image} style={{marginRight: 20, width: imgsize, height: imgsize}} />
return (
<div style={{marginBottom: 40,}}>
<div style={{display: "flex", marginBottom: 15,}}>
{actionimg}
<span style={{fontSize: 24, marginTop: "auto", marginBottom: "auto"}}><b>{data.action.label}</b></span>
</div>
<div style={{marginBottom: 5}}><b>Status </b> {data.status}</div>
{jsonvalid ? <ReactJson
src={JSON.parse(showResult)}
theme="solarized"
collapsed={true}
displayDataTypes={false}
name={"Results for "+data.action.label}
/>
:
<div>
<b>Result</b>&nbsp;
{data.result}
</div>
}
</div>
)
})
}
</div>
}
</Drawer>
const newView = isLoggedIn ?
<div style={{color: "white"}}>
<div style={{display: "flex", borderTop: "1px solid rgba(91, 96, 100, 1)"}}>
{leftView}
<CytoscapeComponent
elements={elements}
minZoom={0.35}
maxZoom={2.00}
style={{width: bodyWidth-leftBarSize-15, height: bodyHeight-appBarSize-1, backgroundColor: surfaceColor}}
stylesheet={cystyle}
boxSelectionEnabled={true}
autounselectify={false}
cy={(incy) => {
setCy(incy)
}}
/>
</div>
{executionModal}
<RightSideBar />
<BottomCytoscapeBar />
<TopCytoscapeBar />
</div>
:
<div style={{color: "white"}}>
TMP FOR NOT LOGGED IN
</div>
const variablesModal = variablesModalOpen ?
<Dialog modal
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 = () => {
console.log("AUTH: ", selectedApp.authentication)
const [tmpVar, setTmpVar] = React.useState("")
if (selectedApp.authentication === undefined) {
return null
}
if (selectedApp.authentication.parameters.length === undefined ||
selectedApp.authentication.parameters.length === 0) {
return null
}
// Yes, it should be possible to have more than one, but.. :)
// This data should be written to a KMS, then have the ID point back
const currentAuth = selectedApp.authentication.parameters[0]
if (currentAuth.scheme.toLowerCase() === "bearer") {
return <div>
Insert your API token for {selectedApp.name}
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
type="password"
color="primary"
placeholder="Bearer token"
onChange={(event) => {
setTmpVar(event.target.value)
}}
onBlur={() => {
selectedApp.authentication.parameters[0].value = tmpVar
setSelectedApp(selectedApp)
}}
/>
</div>
}
return (
<div>
NOT IMPLEMENTED <div/>
Unknown auth: {currentAuth.scheme}
</div>
)
}
const EndpointData = () => {
const [tmpVar, setTmpVar] = React.useState("")
return (
<div>
The API endpoint to use (URL) - leave this if you're unsure
<TextField
style={{backgroundColor: inputColor}}
InputProps={{
style:{
color: "white",
height: "50px",
fontSize: "1em",
},
}}
fullWidth
type="text"
color="primary"
disabled={true}
placeholder="Bearer token"
defaultValue={selectedApp.link}
onChange={(event) => {
setTmpVar(event.target.value)
}}
onBlur={() => {
selectedApp.link = tmpVar
console.log("LINK: ", selectedApp.link)
setSelectedApp(selectedApp)
}}
/>
</div>
)
}
// This whole part is redundant. Made it part of Arguments instead.
const authenticationModal = authenticationModalOpen ?
<Dialog modal
open={authenticationModalOpen}
onClose={() => {
setAuthenticationModalOpen(false)
setAppAuthentication({})
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: "800px",
},
}}
>
<DialogTitle><div style={{color: "white"}}>Authentication for {selectedApp.name}</div></DialogTitle>
<DialogContent>
<Link to="/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</Link>
<div />
{selectedApp.link.length > 0 ? <EndpointData /> : null}
<div style={{marginTop: 15, marginBottom: 15, }}/>
<AuthenticationData />
</DialogContent>
<DialogActions>
<Button
style={{borderRadius: "0px"}}
onClick={() => {
setAuthenticationModalOpen(false)
}} color="primary">
Cancel
</Button>
<Button style={{borderRadius: "0px"}} onClick={() => {setAuthenticationModalOpen(false)}} color="primary">
Submit
</Button>
</DialogActions>
</Dialog> : null
const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
<div>
{newView}
{variablesModal}
{conditionsModal}
{authenticationModal}
</div>
:
<div>
</div>
return (
<div>
<Prompt
when={!lastSaved}
message={unloadText}
/>
{loadedCheck}
</div>
)
}
export default AngularWorkflow