diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index ac18d39b..519c7470 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -134,6 +134,9 @@ const AppStats = (defaultprops) => { Accept: "application/json", }, credentials: "include", + }).catch((error) => { + console.log("Error getting workflow stats: " + error); + return workflow }) if (response.status !== 200) { diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index 6a7b55b7..6c8ae0ee 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -428,7 +428,6 @@ const ConfigureWorkflow = (props) => { console.log("Found webhook: ", trigger) if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") { - console.log("Actions: ", newactions) const findapp = trigger.app_association.name.toLowerCase() const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp) @@ -449,9 +448,6 @@ const ConfigureWorkflow = (props) => { newactions[foundindex].show_steps = true - console.log("CHANGED ACTION: ", newactions[foundindex]) - //console.log("Index: ", newactions[foundindex]) - continue } } @@ -1321,7 +1317,6 @@ const ConfigureWorkflow = (props) => { } if (step.type === "authenticate") { - console.log("AUTH STEP: ", step) if (data.must_authenticate === true ) { filled = false } else { diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 9ac7fabe..4b4aa3d5 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -78,7 +78,6 @@ const EditWorkflow = (props) => { const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day')) - console.log("WORKFLOW: ", workflow) const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) const classes = useStyles(); @@ -235,7 +234,7 @@ const EditWorkflow = (props) => { -
+
{/*
@@ -565,14 +564,22 @@ const EditWorkflow = (props) => { fullWidth /> - - MSSP Suborg Distribution (beta - contact support@shuffler.io) + + MSSP Suborg Distribution (beta - contact support@shuffler.io for more info) {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? - - You can only distribute to suborgs from a parent org. - + userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ? + + Your organization does not have any suborgs yet. Please make one, then try again. + + : + + {innerWorkflow.parentorg_workflow !== undefined && innerWorkflow.parentorg_workflow !== null && innerWorkflow.parentorg_workflow.length > 0 ? This workflow is distributed from your parent workflow (you may not have access). : null} +
+
+ You can only distribute to suborgs from a parent org. +
: {/* diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index a23de3e5..a6058704 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -627,8 +627,8 @@ const CodeEditor = (props) => { newMarkers.push({ startRow: i, startCol: startCh, - endRow: i+1, - endCol: endCh+1, + endRow: i, + endCol: endCh, className: correctVariable ? "good-marker" : "bad-marker", type: "text", }) diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index d36c8e8d..6f2455b3 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -133,6 +133,11 @@ const data = [ "background-gradient-stop-colors": "data(fillGradient)", }, }, + { + selector: `node[?parent_controlled]`, + css: { + }, + }, { selector: `node[app_name="Testing"]`, css: { diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index b074e404..f0f2dfd5 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -166,7 +166,9 @@ const Admin = (props) => { //console.log("Selected: ", selectedOrganization) const [appAuthenticationGroupModalOpen , setAppAuthenticationGroupModalOpen] = React.useState(false); const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); + const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState(""); const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState(""); const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); const [organizationFeatures, setOrganizationFeatures] = React.useState({}); @@ -431,23 +433,62 @@ const Admin = (props) => { }); }; - const createAppAuthenticationGroup = (name, description, appAuthIds) => { + const deleteAppAuthenticationGroup = (appAuthGroupId) => { + const url = `${globalUrl}/api/v1/authentication/group/${appAuthGroupId}` + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for deleting app auth group"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + toast("Failed to delete app authentication group"); + } else { + toast("App authentication group deleted") + getAppAuthenticationGroups() + } + }) + .catch((error) => { + toast(error.toString()) + }) + } + + const createAppAuthenticationGroup = (name, environment, description, appAuthIds) => { + // Makes list of ids into a full-on list of auth, but just with the ID + // The backend fills in the rest + console.log("INput auth: ", appAuthIds) let app_auths = appAuthIds.map((appAuthId) => { return { id: appAuthId }; }) - fetch(globalUrl + "/api/v1/apps/authentication/group", { + var parsedAppGroup = { + label: name, + environment: environment, + description: description, + app_auths: app_auths + } + + if (appAuthenticationGroupId !== undefined && appAuthenticationGroupId !== null && appAuthenticationGroupId !== "") { + parsedAppGroup.id = appAuthenticationGroupId + } + + fetch(globalUrl + "/api/v1/authentication/group", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", - body: JSON.stringify({ - label: name, - description: description, - app_auths: app_auths - }), + body: JSON.stringify(parsedAppGroup), }) .then((response) => { if (response.status !== 200) { @@ -457,8 +498,15 @@ const Admin = (props) => { return response.json(); }) .then((responseJson) => { - // getAppAuthenticationGroups(); - toast("App authentication group created"); + if (responseJson.success === false) { + toast("Failed to create. Please try again, or contact support@shuffler.io") + } else { + // Close the modal + setAppAuthenticationGroupModalOpen(false) + + toast("App authentication group created") + getAppAuthenticationGroups() + } }) .catch((error) => { toast(error.toString()); @@ -805,7 +853,7 @@ If you're interested, please let me know a time that works for you, or set up a .then((responseJson) => { setWebHooks(responseJson.webhooks || []); // Handling the case where the result is null or undefined setAllSchedules(responseJson.schedules || []); - setPipelines(responseJson.pipelines || []); + // setPipelines(responseJson.pipelines || []); }) .catch((error) => { // toast(error.toString()); @@ -990,13 +1038,11 @@ If you're interested, please let me know a time that works for you, or set up a } const data = { - command: pipeline.command, name: pipeline.name, type: state, environment: pipeline.environment, workflow_id: pipeline.workflow_id, trigger_id: pipeline.trigger_id, - start_node: pipeline.start_node, }; if (state === "start") toast("starting the pipeline"); @@ -1027,7 +1073,6 @@ If you're interested, please let me know a time that works for you, or set up a if (state === "start") toast("Successfully created pipeline"); else toast("Sucessfully stopped the pipeline"); } - setTimeout(handleGetAllTriggers, 1000); }) .catch((error) => { //toast(error.toString()); @@ -2093,10 +2138,10 @@ If you're interested, please let me know a time that works for you, or set up a }; const getAppAuthenticationGroups = () => { - console.log("DEBUG: Skipping app auth group loading") - return + //console.log("DEBUG: Skipping app auth group loading") + //return - fetch(globalUrl + "/api/v1/apps/authentication/group", { + fetch(globalUrl + "/api/v1/authentication/group", { method: "GET", headers: { "Content-Type": "application/json", @@ -4941,7 +4986,7 @@ If you're interested, please let me know a time that works for you, or set up a
+ + + + +
+ {/* Show a check box list of all app authentications to add to the auth group */} +
+ {authentication.map((data, index) => { + var checked = data.checked + if (checked === undefined || checked === null) { + checked = false + } + + if (appsForAppAuthGroup.includes(data.id)) { + checked = true + } + + return ( +
+ +
+ + { + handleAppAuthGroupCheckbox(data) + }} + name={data.label} + disabled={data.app.id in appsForAppAuthGroup} + /> +
+ + } + label={data.label} + /> +
+ ) + })} +
+
+ + )} @@ -5311,7 +5412,7 @@ If you're interested, please let me know a time that works for you, or set up a

App Authentication

- Control the authentication options for individual apps. + Control the authentication options for individual apps. App Groups are farther down on this page.  
- {/*
+ + +
-

App Authentication Groups

- +

App Authentication Groups

+ Groups of authentication options for subflows.{" "}
- + + + + - - {data.app_auths.map((appAuth, index) => ( - - {appAuth.app.name} - - ))} + {data.app_auths.map((appAuth, index) => { + if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") { + const foundImage = authentication.find((auth) => auth.app.id === appAuth.app.id) + if (foundImage !== undefined) { + appAuth.app.large_image = foundImage.app.large_image + + appAuth.app.name = foundImage.app.name + } + } + + const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})` + + return ( + + {appAuth.app.name} + + ) + })}
} style={{ minWidth: 250, maxWidth: 250 }} @@ -5676,16 +5813,22 @@ If you're interested, please let me know a time that works for you, or set up a
{ + setAppAuthenticationGroupId(data.id) + + setAppAuthenticationGroupName(data.label) + setAppAuthenticationGroupDescription(data.description) + + setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id)) + setAppAuthenticationGroupEnvironment(data.environment) + setAppAuthenticationGroupModalOpen(true) }} - disabled={true} > { - // deleteAppAuthenticationGroup(data); + deleteAppAuthenticationGroup(data.id) }} - disabled={true} > @@ -5700,20 +5843,10 @@ If you're interested, please let me know a time that works for you, or set up a )} -
-
*/} - +
+ ) : null; const getLogs = async (ip, userId) => { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 492e8ad9..f10ffae8 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -451,6 +451,9 @@ const AngularWorkflow = (defaultprops) => { const [lastExecution, setLastExecution] = React.useState(""); const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false); + const [authgroupModalOpen, setAuthgroupModalOpen] = React.useState(false); + const [authGroups, setAuthGroups] = React.useState([]) + const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname; @@ -524,12 +527,13 @@ const AngularWorkflow = (defaultprops) => { const [workflowRecommendations, setWorkflowRecommendations] = React.useState(undefined); const [showErrors, setShowErrors] = React.useState(true); const [highlightedApp, setHighlightedApp] = React.useState("") - const [listCache, setListCache] = React.useState([]); - const [selectedOption, setSelectedOption] = React.useState(""); const [tenzirConfigModalOpen, setTenzirConfigModalOpen] = React.useState(false); + const [distributedFromParent, setDistributedFromParent] = React.useState("") + const [suborgWorkflows, setSuborgWorkflows] = React.useState([]) + const [suggestionBox, setSuggestionBox] = React.useState({ "position": { "top": 500, @@ -539,6 +543,12 @@ const AngularWorkflow = (defaultprops) => { "attachedTo": "", }) + useEffect(() => { + if (!firstrequest && isLoaded && isLoggedIn && editWorkflowModalOpen === false) { + saveWorkflow(workflow) + } + }, [editWorkflowModalOpen]) + // New for generated stuff const releaseToConnectLabel = "Release to Connect" const integrationApps = [{ @@ -828,6 +838,75 @@ const AngularWorkflow = (defaultprops) => { loopRunning2 = true } + useEffect(() => { + if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id) { + setOriginalWorkflow(workflow) + } + + // Special multi-workflow edgecase handler for events + if (distributedFromParent === "" && suborgWorkflows === []) { + } else { + if (cy !== undefined) { + cy.removeListener("select"); + cy.removeListener("unselect"); + cy.removeListener("add"); + cy.removeListener("remove"); + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); + + setTimeout(() => { + setupGraph(workflow) + + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication); + }); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + + cy.on("unselect", (e) => onUnselect(e)); + + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + cy.on("remove", "node", (e) => onNodeRemoved(e)); + cy.on("remove", "edge", (e) => onEdgeRemoved(e)); + + cy.on("mouseover", "edge", (e) => onEdgeHover(e)); + cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); + cy.on("mouseover", "node", (e) => onNodeHover(e)); + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + + // Handles dragging + cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); + cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + + cy.on("cxttap", "node", (e) => onCtxTap(e)); + + cy.edgehandles({ + handleNodes: (el) => { + if (el.isNode() && + el.data("buttonType") != "ACTIONSUGGESTION" && + !el.data("isButton") && + !el.data("isDescriptor") && + !el.data("isSuggestion") && + el.data("type") !== "COMMENT") { + return true + } + + return false + }, + preview: false, + toggleOffOnLeave: true, + loopAllowed: function (node) { + return false; + }, + }) + }, 50) + } + } + }, [workflow]) + useEffect(() => { // Current variable + future state controlled // This is so that the loop can stop itself as well @@ -958,7 +1037,7 @@ const AngularWorkflow = (defaultprops) => { }; const getAvailableWorkflows = (trigger_index) => { - fetch(globalUrl + "/api/v1/workflows", { + fetch(globalUrl + "/api/v1/workflows?subflow=true", { method: "GET", headers: { "Content-Type": "application/json", @@ -1307,7 +1386,7 @@ const AngularWorkflow = (defaultprops) => { if (response.status !== 200) { stop(); setExecutionModalView(0); - toast("Failed loading the workflow run") + //toast("Failed loading the workflow run") console.log("Status not 200 for stream results :O!"); //const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; @@ -1675,12 +1754,18 @@ const AngularWorkflow = (defaultprops) => { }) } - const saveWorkflow = (curworkflow, executionArgument, startNode) => { + const saveWorkflow = (curworkflow, executionArgument, startNode, duplicationOrg) => { var success = false; if (isCloud && !isLoggedIn) { console.log("Should redirect to register with redirect.") - window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` + + setTimeout(() => { + toast("You may not have access to this workflow.") + //window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` + window.location.href = `/workflows` + }, 2500) + return } @@ -1910,13 +1995,19 @@ const AngularWorkflow = (defaultprops) => { useworkflow.id = props.match.params.key } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (useworkflow.org_id !== undefined && useworkflow.org_id !== null && useworkflow.org_id.length > 0) { + headers["Org-Id"] = useworkflow.org_id + } + setLastSaved(true); fetch(`${globalUrl}/api/v1/workflows/${useworkflow.id}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, body: JSON.stringify(useworkflow), credentials: "include", }) @@ -1924,11 +2015,20 @@ const AngularWorkflow = (defaultprops) => { setSavingState(0); if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); - } + } else { + if (distributedFromParent === "" && suborgWorkflows === []) { + } else { + getChildWorkflows(useworkflow.id) + } + } return response.json(); }) .then((responseJson) => { + if (duplicationOrg !== undefined && duplicationOrg !== null && duplicationOrg.length > 0) { + duplicateParentWorkflow(useworkflow, duplicationOrg, true) + } + if (executionArgument !== undefined && startNode !== undefined) { //console.log("Running execution AFTER saving"); executeWorkflow(executionArgument, startNode, true); @@ -1993,8 +2093,11 @@ const AngularWorkflow = (defaultprops) => { console.log("Save workflow error: ", error.toString()); }); - setOriginalWorkflow(useworkflow) - return success; + if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id) { + setOriginalWorkflow(useworkflow) + } + + return success }; const monitorUpdates = () => { @@ -2087,14 +2190,20 @@ const AngularWorkflow = (defaultprops) => { curelements[i].addClass("not-executing-highlight"); } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + const data = { execution_argument: executionArgument, start: startNode }; fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`, { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", body: JSON.stringify(data), } @@ -2167,6 +2276,37 @@ const AngularWorkflow = (defaultprops) => { // This can be used to only show prioritzed ones later // Right now, it can prioritize authenticated ones //"Testing", + // + // + + const getAuthGroups = () => { + fetch(globalUrl + "/api/v1/authentication/groups", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAuthGroups(responseJson.data) + } else { + console.log("AppAuth group loading error: " + responseJson.reason); + } + }) + .catch((error) => { + setAuthGroups([]); + console.log("AppAuth group loading error: " + error.toString()); + }) + } const getAppAuthentication = (reset, updateAction, closeMenu) => { fetch(globalUrl + "/api/v1/apps/authentication", { @@ -2177,127 +2317,130 @@ const AngularWorkflow = (defaultprops) => { }, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for app auth :O!"); + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + var shouldClose = false + if (responseJson.success) { + getAuthGroups() + + var newauth = []; + for (let authkey in responseJson.data) { + if (responseJson.data[authkey].defined === false) { + continue; + } + + newauth.push(responseJson.data[authkey]); + } + + setAppAuthentication(newauth); + + if (cy !== undefined) { + // Remove the old listener for select, run with new one + cy.removeListener("select"); + + cy.on("select", "node", (e) => onNodeSelect(e, newauth)); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + } + + if (updateAction === true) { + if (selectedApp.authentication.required) { + // Setup auth here :) + var appUpdates = false; + const authenticationOptions = []; + + var tmpAuth = JSON.parse(JSON.stringify(newauth)); + var latest = 0; + for (let authkey in tmpAuth) { + var item = tmpAuth[authkey]; + + //console.log("Got auth: ", item); + + const newfields = {}; + for (let filterkey in item.fields) { + newfields[item.fields[filterkey].key] = item.fields[filterkey].value; } - return response.json(); - }) - .then((responseJson) => { - var shouldClose = false - if (responseJson.success) { - var newauth = []; - for (let authkey in responseJson.data) { - if (responseJson.data[authkey].defined === false) { - continue; - } + item.fields = newfields; - newauth.push(responseJson.data[authkey]); - } + const appname = selectedApp.name.toLowerCase().replaceAll(" ", "_", -1) + const itemname = item.app.name.toLowerCase().replaceAll(" ", "_", -1) + if (itemname === appname) { + authenticationOptions.push(item); - setAppAuthentication(newauth); + // Always becoming the last one + if (item.edited > latest) { + latest = item.edited; + selectedAction.selectedAuthentication = item; - if (cy !== undefined) { - // Remove the old listener for select, run with new one - cy.removeListener("select"); - - cy.on("select", "node", (e) => onNodeSelect(e, newauth)); - cy.on("select", "edge", (e) => onEdgeSelect(e)); - } - - if (updateAction === true) { - if (selectedApp.authentication.required) { - // Setup auth here :) - var appUpdates = false; - const authenticationOptions = []; - - var tmpAuth = JSON.parse(JSON.stringify(newauth)); - var latest = 0; - for (let authkey in tmpAuth) { - var item = tmpAuth[authkey]; - - //console.log("Got auth: ", item); - - const newfields = {}; - for (let filterkey in item.fields) { - newfields[item.fields[filterkey].key] = item.fields[filterkey].value; - } - - item.fields = newfields; - - const appname = selectedApp.name.toLowerCase().replaceAll(" ", "_", -1) - const itemname = item.app.name.toLowerCase().replaceAll(" ", "_", -1) - if (itemname === appname) { - authenticationOptions.push(item); - - // Always becoming the last one - if (item.edited > latest) { - latest = item.edited; - selectedAction.selectedAuthentication = item; - - for (let actionkey in workflow.actions) { - const actionAppname = workflow.actions[actionkey].app_name.toLowerCase().replaceAll(" ", "_", -1) - if (actionAppname === appname) { - workflow.actions[actionkey].selectedAuthentication = item; - workflow.actions[actionkey].authentication_id = item.id; - appUpdates = true; - } - } - } else { - //console.log("Not newer: ", item.edited, " vs ", latest) - } - } else { - //console.log("Appname is wrong: ", appname, " vs ", itemname) - } - } - - selectedAction.authentication = authenticationOptions; - if ( - selectedAction.selectedAuthentication === null || - selectedAction.selectedAuthentication === undefined || - selectedAction.selectedAuthentication.length === "" - ) { - selectedAction.selectedAuthentication = {}; - } - - if (appUpdates === true) { - console.log("Closing auth modal: Success") - - setAuthenticationModalOpen(false); - setSelectedAction(selectedAction); - setWorkflow(workflow); - saveWorkflow(workflow); - - toast("Added and updated authentication!"); - shouldClose = true - } else { - console.log("Closing auth modal? FAIL") - - toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); - shouldClose = false - } - } else { - toast("No authentication to update"); - } - } else { - shouldClose = true - } - } else { - setAppAuthentication([]); - shouldClose = true - } - - // Auto-closing if changes were made - if (closeMenu === true && shouldClose === true) { - setAuthenticationModalOpen(false); + for (let actionkey in workflow.actions) { + const actionAppname = workflow.actions[actionkey].app_name.toLowerCase().replaceAll(" ", "_", -1) + if (actionAppname === appname) { + workflow.actions[actionkey].selectedAuthentication = item; + workflow.actions[actionkey].authentication_id = item.id; + appUpdates = true; + } } - }) - .catch((error) => { - setAppAuthentication([]); - //toast("Auth loading error: " + error.toString()); - console.log("AppAuth error: " + error.toString()); - }); + } else { + //console.log("Not newer: ", item.edited, " vs ", latest) + } + } else { + //console.log("Appname is wrong: ", appname, " vs ", itemname) + } + } + + selectedAction.authentication = authenticationOptions; + if ( + selectedAction.selectedAuthentication === null || + selectedAction.selectedAuthentication === undefined || + selectedAction.selectedAuthentication.length === "" + ) { + selectedAction.selectedAuthentication = {}; + } + + if (appUpdates === true) { + console.log("Closing auth modal: Success") + + setAuthenticationModalOpen(false); + setSelectedAction(selectedAction); + setWorkflow(workflow); + saveWorkflow(workflow); + + toast("Added and updated authentication!"); + shouldClose = true + } else { + console.log("Closing auth modal? FAIL") + + toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); + shouldClose = false + } + } else { + toast("No authentication to update"); + } + } else { + shouldClose = true + } + + } else { + setAppAuthentication([]); + shouldClose = true + } + + // Auto-closing if changes were made + if (closeMenu === true && shouldClose === true) { + setAuthenticationModalOpen(false); + } + }) + .catch((error) => { + setAppAuthentication([]); + //toast("Auth loading error: " + error.toString()); + console.log("AppAuth error: " + error.toString()); + }); }; const getApps = () => { @@ -3082,6 +3225,36 @@ const AngularWorkflow = (defaultprops) => { return apps }; + const getChildWorkflows = (parentWorkflowId) => { + if (workflow.suborg_distribution === undefined || workflow.suborg_distribution === null || workflow.suborg_distribution.length === 0) { + return + } + + fetch(`${globalUrl}/api/v1/workflows/${parentWorkflowId}/child_workflows`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + setSuborgWorkflows(responseJson) + } + }) + .catch((error) => { + console.log("Get child workflows error: ", error); + }) + } + const getWorkflow = (workflow_id, sourcenode) => { fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { method: "GET", @@ -3125,9 +3298,24 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { // Load as JSON + // + // //console.log("Got workflow TXT: ", responseText) //const responseJson = JSON.parse(responseText) //console.log("Got workflow JSON: ", responseJson) + if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.id !== workflow_id) { + toast("Workflow ID mismatch. Redirecting to your workflow") + navigate(`/workflows/${responseJson.id}`) + } + + if (responseJson.parentorg_workflow !== undefined && responseJson.parentorg_workflow !== null && responseJson.parentorg_workflow !== "") { + setDistributedFromParent(responseJson.parentorg_workflow) + } + + + if (responseJson.childorg_workflow_ids !== undefined && responseJson.childorg_workflow_ids !== null && responseJson.childorg_workflow_ids.length > 0) { + getChildWorkflows(responseJson.id) + } // Not sure why this is necessary. if (responseJson.isValid === undefined) { @@ -3350,7 +3538,7 @@ const AngularWorkflow = (defaultprops) => { cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); } else { - setOriginalWorkflow(responseJson); + setOriginalWorkflow(responseJson) setWorkflow(responseJson); setWorkflowDone(true); @@ -3704,14 +3892,11 @@ const AngularWorkflow = (defaultprops) => { } if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { - console.log("Found triggers. Add!") - if (!found) { console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) // Find how many executions it has var executions = 0 const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) - console.log("Matches: ", matchingExecutions.length) const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" const decoratorNode = { position: { @@ -4220,7 +4405,7 @@ const AngularWorkflow = (defaultprops) => { return } - // Inject HTML at a fixed location + // Inject HTML at a fixed location? //const newHtml = "

Do you want to add this suggestion?

" // Find mouse cursor position on screen @@ -4415,13 +4600,24 @@ const AngularWorkflow = (defaultprops) => { return; } else if (data.isDescriptor) { - console.log("Can't select descriptor"); + // Find parent + event.target.unselect(); + + if (data.attachedTo !== undefined && data.attachedTo !== null && data.attachedTo.length > 0) { + const parentNode = cy.getElementById(data.attachedTo) + if (parentNode !== null && parentNode !== undefined) { + setTimeout(() => { + parentNode.select() + }, 100) + } + } + + //console.log("Can't select descriptor"); if (data.isTrigger) { console.log("But maybe we can select trigger descriptor? Maybe open execution tab?") setExecutionModalOpen(true) } - event.target.unselect(); return; } @@ -5701,9 +5897,6 @@ const AngularWorkflow = (defaultprops) => { } setWorkflow(workflow); - //if (data.type === "TRIGGER") { - // saveWorkflow(workflow); - //} } //var previouskey = 0 @@ -5983,7 +6176,15 @@ const AngularWorkflow = (defaultprops) => { workflow.id !== null && workflow.id.length > 0 ) { - window.location.pathname = "/workflows/" + props.match.params.key; + + // Check if + if (distributedFromParent === "" && suborgWorkflows === []) { + toast.info("Redirecting as the workflow ID does not match the URL") + + setTimeout(() => { + window.location.pathname = "/workflows/" + props.match.params.key; + }, 2500) + } } const animationDuration = 150; @@ -6171,7 +6372,6 @@ const AngularWorkflow = (defaultprops) => { }; const addActionSuggestions = (nodedata, event) => { - console.log("App Action suggestions being added") if (nodedata.type !== "ACTION") { return } @@ -6187,10 +6387,37 @@ const AngularWorkflow = (defaultprops) => { const parentlabel = parentNode.data("label").toLowerCase().replace(" ", "_") const parentname = parentNode.data("app_name").toLowerCase().replace(" ", "_") if (!parentlabel.startsWith(parentname)) { - console.log("Bad startname to start with: ", parentname, parentlabel) return } + // Check if action has changed + const parentAppId = parentNode.data("app_id") + const parentActionname = parentNode.data("name") + for (var appkey in apps) { + const curapp = apps[appkey] + + if (curapp.id !== parentAppId) { + continue + } + + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { + continue + } + + var startIndex = curapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) + if (startIndex === -1) { + startIndex = 0 + } + + if (curapp.actions[startIndex].name !== parentActionname) { + return + } + + break + } + + //const parentAction = parentNode.data("name") + const iconInfo = { icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", iconColor: buttonColor, @@ -6204,8 +6431,7 @@ const AngularWorkflow = (defaultprops) => { // 2. Loop the apps' actions // 3. Find actions based on category label IF it exists - console.log("Fidning app match for: ", parentname) - var added = 0 + var addedLabels = [] for (let appKey in apps) { const curapp = apps[appKey] if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { @@ -6216,7 +6442,6 @@ const AngularWorkflow = (defaultprops) => { continue } - console.log("Found matching: ", curapp.name, parentname, curapp.actions.length) for (let actionKey in curapp.actions) { const curaction = curapp.actions[actionKey] @@ -6226,7 +6451,13 @@ const AngularWorkflow = (defaultprops) => { } if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { - console.log("IN NODE ADD") + if (addedLabels.includes(curaction.category_label[0])) { + continue + } + + if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { + continue + } cy.add({ group: "nodes", @@ -6240,12 +6471,13 @@ const AngularWorkflow = (defaultprops) => { }, position: { x: px, - y: py + (added * 50), + y: py + (addedLabels.length * 50), }, + locked: true, }) - added += 1 - if (added >= 3) { + addedLabels.push(curaction.category_label[0]) + if (addedLabels.length >= 2) { break } } @@ -6555,7 +6787,6 @@ const AngularWorkflow = (defaultprops) => { } if (!found) { - console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) // Find how many executions it has var executions = 0 const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) @@ -7087,6 +7318,11 @@ const AngularWorkflow = (defaultprops) => { } */ + var parentcontrolled = false + if (branch.parent_controlled !== undefined && branch.parent_controlled !== null && branch.parent_controlled === true) { + parentcontrolled = true + } + edge.data = { id: branch.id, _id: branch.id, @@ -7096,6 +7332,7 @@ const AngularWorkflow = (defaultprops) => { conditions: conditions, hasErrors: branch.has_errors, decorator: false, + parent_controlled: parentcontrolled, }; // This is an attempt at prettier edges. The numbers are weird to work with. @@ -7125,7 +7362,6 @@ const AngularWorkflow = (defaultprops) => { return edge; }); - console.log("VISUAL BRANCHES: ", inputworkflow.visual_branches) if (inputworkflow.visual_branches !== undefined && inputworkflow.visual_branches !== null && inputworkflow.visual_branches.length > 0) { const visualedges = inputworkflow.visual_branches.map((branch, index) => { const edge = {}; @@ -7187,8 +7423,6 @@ const AngularWorkflow = (defaultprops) => { } else { setElements(insertedNodes); } - - console.log("Setupgraph done 2!") } const removeNode = (nodeId) => { @@ -7350,7 +7584,6 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (responseJson.success !== false) { - console.log("Usecases: ", usecases) setUsecases(responseJson) } else { } @@ -7402,6 +7635,7 @@ const AngularWorkflow = (defaultprops) => { if (firstrequest) { setFirstrequest(false); getWorkflow(props.match.params.key, {}); + getChildWorkflows(props.match.params.key) getRevisionHistory(props.match.params.key) getApps() fetchUsecases() @@ -7502,7 +7736,7 @@ const AngularWorkflow = (defaultprops) => { loopAllowed: function (node) { return false; }, - }); + }) //cy.edgehandles({ // preview: false, @@ -7606,7 +7840,7 @@ const AngularWorkflow = (defaultprops) => { trigger.status = "stopped"; setSelectedTrigger(trigger); setWorkflow(workflow); - saveWorkflow(workflow); + saveWorkflow(workflow) }) .catch((error) => { console.log("Stop schedule error: ", error.toString()) @@ -7690,8 +7924,8 @@ const AngularWorkflow = (defaultprops) => { return; } - var mappedStartnode = "" - const alledges = cy.edges().jsons() + var mappedStartnode = "" + const alledges = cy.edges().jsons() if (alledges !== undefined && alledges !== null && alledges.length > 0) { for (let edgekey in alledges) { const tmp = alledges[edgekey] @@ -10153,6 +10387,142 @@ const AngularWorkflow = (defaultprops) => { } } + const handleAppAuthGroupCheckbox = (data) => { + console.log("CHECKED: ", data) + + if (workflow.auth_groups === undefined || workflow.auth_groups === null) { + workflow.auth_groups = [] + } + + const foundIndex = workflow.auth_groups.findIndex(auth => auth === data.id) + if (foundIndex >= 0) { + workflow.auth_groups.splice(foundIndex, 1) + } else { + workflow.auth_groups.push(data.id) + } + + setWorkflow(workflow) + console.log("WF: ", workflow) + setUpdate(Math.random()) + } + + const authgroupModal = + { + }} + > + + { + e.preventDefault(); + setAuthgroupModalOpen(false) + }} + > + + + + + Authgroup Selection + + + + Authgroups are a way to control how a workflow runs. If chosen, the workflow will use the groups on the nodes that have them selected. If three are chosen, the workflows runs three times. This is an experimental MSSP feature to handle multiple environments. + + + + + {authGroups.map((data, index) => { + var checked = false + if (workflow.auth_groups !== undefined && workflow.auth_groups !== null) { + checked = workflow.auth_groups.includes(data.id) + } + + return ( +
{ + handleAppAuthGroupCheckbox(data) + }} + > + + + + {data.label} + + + + {data.environment} + + + {data.app_auths.map((appAuth, index) => { + if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") { + const foundImage = appAuthentication.find((auth) => auth.app.id === appAuth.app.id) + if (foundImage !== undefined) { + appAuth.app.large_image = foundImage.app.large_image + + appAuth.app.name = foundImage.app.name + } + } + + const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})` + + return ( + + {appAuth.app.name} + + ) + })} + +
+ ) + })} + + + +
+
+ const executionArgumentModal = { // value: auth.id, // }); setSelectedAuth(auth.id); - }; - - console.log("TRANSFORMED AUTH DATA: ", transformedAuthData); + } return (
@@ -12643,9 +13011,11 @@ const AngularWorkflow = (defaultprops) => {
)} + {workflows === undefined || workflows === null || workflows.length === 0 ? null : ( + { - const cytoscapeViewWidths = isMobile ? 50 : 850; + const cytoscapeViewWidths = isMobile ? 50 : 950; const bottomBarStyle = { position: "fixed", right: isMobile ? 20 : 20, @@ -14980,7 +15350,8 @@ const AngularWorkflow = (defaultprops) => { }}>{workflow.name} - {isCorrectOrg ? null : + {!distributedFromParent ? + isCorrectOrg ? null : Warning: Change { }} >Active Organization to edit this Workflow. + : + + Warning: This workflow is controlled by your parent org and may not be editable. + } + + {originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0 || originalWorkflow.suborg_distribution.includes("none") ? null : + + + + View Suborg workflow + + + + } + + + {authGroups !== undefined && authGroups !== null && authGroups.length > 0 ? + + + + + + : null} +
{parentWorkflows.slice(0,5).map((wf, index) => { @@ -15346,7 +15940,7 @@ const AngularWorkflow = (defaultprops) => { ) } - const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ? + const shownErrors = !distributedFromParent && !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
{ } } + /* if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { console.log("Shift key pressed") if (!workflow.public && executionModalOpen) { @@ -15544,6 +16139,7 @@ const AngularWorkflow = (defaultprops) => { setExecutionModalView(0); } } + */ }; document.addEventListener('keydown', handleKeyDown); @@ -15594,6 +16190,106 @@ const AngularWorkflow = (defaultprops) => { ) } + // Used for handling suborg workflow distribution management + const updateCurrentWorkflow = (inputworkflow) => { + setLastSaved(false) + setSelectedAction({}); + setSelectedApp({}) + setWorkflow(inputworkflow) + + // Update props match key + if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { + setDistributedFromParent(inputworkflow.parentorg_workflow) + } else { + setDistributedFromParent("") + } + + if (cy !== undefined) { + cy.removeListener("select"); + cy.removeListener("unselect"); + + cy.removeListener("add"); + cy.removeListener("remove"); + + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); + + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); + + setElements([]) + + // Remove all edges + cy.edges().remove() + cy.nodes().remove() + } + + // Remove all cytoscape triggers first? + /* + setTimeout(() => { + setupGraph(inputworkflow) + + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication); + }); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + + cy.on("unselect", (e) => onUnselect(e)); + + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + cy.on("remove", "node", (e) => onNodeRemoved(e)); + cy.on("remove", "edge", (e) => onEdgeRemoved(e)); + + cy.on("mouseover", "edge", (e) => onEdgeHover(e)); + cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); + cy.on("mouseover", "node", (e) => onNodeHover(e)); + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + + // Handles dragging + cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); + cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + + cy.on("cxttap", "node", (e) => onCtxTap(e)); + }, 25) + */ + + } + + // Uses Org-Id referencing header to create a workflow while getting it in realtime + // This further ensures the user needs access to GET the workflow properly + const duplicateParentWorkflow = (inputWorkflow, org_id, setWorkflow) => { + fetch(`${globalUrl}/api/v1/workflows/${inputWorkflow.id}`, { + method: "GET", + headers: { + "Org-Id": org_id, + "Content-Type": "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + getChildWorkflows(inputWorkflow.id) + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + //toast("Failed to duplicate workflow") + } else { + //toast("Successfully duplicated workflow. Reloading child workflows.") + if (setWorkflow === true) { + updateCurrentWorkflow(responseJson) + } + } + }) + .catch((error) => { + console.log("Dupe workflow for suborg error: ", error.toString()) + }) + } + const BottomCytoscapeBar = () => { if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) { return null; @@ -15616,11 +16312,11 @@ const AngularWorkflow = (defaultprops) => { ) : ( - - ); return ( @@ -15669,6 +16364,7 @@ const AngularWorkflow = (defaultprops) => { /> )} + {/*userdata.avatar === creatorProfile.github_avatar ? null :*/} @@ -15903,6 +16599,8 @@ const AngularWorkflow = (defaultprops) => { + +
); @@ -16021,6 +16719,7 @@ const AngularWorkflow = (defaultprops) => { aiSubmit={aiSubmit} listCache={listCache} + authGroups={authGroups} apps={apps} expansionModalOpen={codeEditorModalOpen} setExpansionModalOpen={setCodeEditorModalOpen} @@ -16600,7 +17299,8 @@ const AngularWorkflow = (defaultprops) => { // This is the playbutton at 150x150 const defaultImage = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg=="; + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" + const size = 40; const borderRadius = 5 if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { @@ -16617,7 +17317,27 @@ const AngularWorkflow = (defaultprops) => { ) } - if (execution.execution_source === "webhook") { + if (execution.execution_source === "authgroups") { + const iconMargin = 7 + return ( +
+ +
+ ) + + } else if (execution.execution_source === "webhook") { return ( {"webhook"} { /> 0 ? ` Authgroup: ${data.authgroup}` : '')} + placement="left" >
{ {data.workflow.actions !== null ? (
{ marginBottom: "auto", }} > - {successActions} / {skippedActions > 0 ? skippedActions : {skippedActions}} / {calculatedResult} + {successActions} + {skippedActions > 0 ? skippedActions : {skippedActions}} = {calculatedResult}
) : null} @@ -17453,7 +18173,7 @@ const AngularWorkflow = (defaultprops) => { ) : null}
- {executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 && executionData.workflow.actions[0].environment !== "Cloud" ? + {executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 ?
Env      @@ -17481,16 +18201,29 @@ const AngularWorkflow = (defaultprops) => { {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && - executionData.execution_source !== "default" ? ( + executionData.execution_source !== "default" || + (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? (
Source    - {executionData.execution_parent !== null && + + + {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? + + Auth Group '{executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0 ? `${executionData.authgroup}` : null}' + + : + executionData.execution_parent !== null && executionData.execution_parent !== undefined && executionData.execution_parent.length > 0 ? ( - executionData.execution_source === props.match.params.key ? ( + executionData.execution_source === props.match.params.key ? { @@ -17502,7 +18235,7 @@ const AngularWorkflow = (defaultprops) => { > Parent Execution - ) : ( + : { Parent Workflow ) - ) : ( + : executionData.execution_source === "questions" || executionData.execution_source === "web" ? { : executionData.execution_source - )} + }
) : null} @@ -20657,6 +21390,7 @@ const AngularWorkflow = (defaultprops) => { {authenticationModal} {tenzirConfigModal} {/*editWorkflowModal*/} + {authgroupModal} {executionArgumentModal} {configureWorkflowModal} {/*usecaseSlidein*/} @@ -20768,7 +21502,7 @@ const AngularWorkflow = (defaultprops) => {
) : (
- + Loading Workflow & Apps... diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 565f051f..3ba402cb 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -46,6 +46,7 @@ const Body = { height: "100%", color: "white", position: "relative", + paddingTop: 40, //textAlign: "center", }; diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 5c47c76b..99d4fca1 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -74,6 +74,7 @@ import { ArrowLeft as ArrowLeftIcon, ArrowRight as ArrowRightIcon, QueryStats as QueryStatsIcon, + Visibility as VisibilityIcon, } from "@mui/icons-material"; import { DataGrid, GridToolbar } from "@mui/x-data-grid"; @@ -614,6 +615,7 @@ const Workflows = (props) => { const [videoViewOpen, setVideoViewOpen] = React.useState(false) const [gettingStartedItems, setGettingStartedItems] = React.useState([]) const [selectedWorkflowIndexes, setSelectedWorkflowIndexes] = React.useState([]) + const [highlightIds, setHighlightIds] = React.useState([]) const [apps, setApps] = React.useState([]); @@ -1213,7 +1215,24 @@ const Workflows = (props) => { } setFirstLoad(false) - }, 100) + }, 250) + + /* + setTimeout(() => { + var timeout = 0 + for (var key in newarray) { + const wf = newarray[key] + if (wf.actions === undefined || wf.actions === null || wf.actions.length === 0) { + setTimeout(() => { + sideloadWorkflow(wf.id, false) + }, timeout) + + timeout += 1000 + } + + } + }, 1000) + */ } else { if (isLoggedIn) { @@ -1637,20 +1656,24 @@ const Workflows = (props) => { }); }; - const copyWorkflow = (data) => { - data = JSON.parse(JSON.stringify(data)); - toast("Copying workflow " + data.name); - data.id = ""; - data.name = data.name + "_copy"; - data = deduplicateIds(data, true); + const duplicateWorkflow = (data) => { + //data = JSON.parse(JSON.stringify(data)); + toast("Copying workflow '" + data.name + "'. The new workflow will load in and be highlighted."); + //data.id = ""; + //data.name = data.name + "_copy"; + //data = deduplicateIds(data, true); - fetch(globalUrl + "/api/v1/workflows", { + const duplicateData = { + name: data.name + "_copy", + } + + fetch(`${globalUrl}/api/v1/workflows/${data.id}/duplicate`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(data), + body: JSON.stringify(duplicateData), credentials: "include", }) .then((response) => { @@ -1660,7 +1683,20 @@ const Workflows = (props) => { } return response.json(); }) - .then(() => { + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + toast("Failed copying workflow: " + responseJson.reason) + } else { + toast("Failed copying workflow") + } + + return + } + + if (responseJson.id !== undefined) { + setHighlightIds([responseJson.id]) + } setTimeout(() => { getAvailableWorkflows(); }, 1000); @@ -1689,15 +1725,68 @@ const Workflows = (props) => { }) } - const sideloadWorkflow = (id, openEdit) => { + const exportSingleWorkflow = (data, setOpen) => { + setExportModalOpen(true) + + if (data.triggers !== null && data.triggers !== undefined) { + var newSubflows = []; + for (var key in data.triggers) { + const trigger = data.triggers[key]; + + if ( + trigger.parameters !== null && + trigger.parameters !== undefined + ) { + for (var subkey in trigger.parameters) { + const param = trigger.parameters[subkey]; + if ( + param.name === "workflow" && + param.value !== data.id && + !newSubflows.includes(param.value) + ) { + newSubflows.push(param.value); + } + } + } + } + + var parsedworkflows = []; + for (var key in newSubflows) { + const foundWorkflow = workflows.find( + (workflow) => workflow.id === newSubflows[key] + ); + if (foundWorkflow !== undefined && foundWorkflow !== null) { + parsedworkflows.push(foundWorkflow); + } + } + + if (parsedworkflows.length > 0) { + console.log( + "Appending subflows during export: ", + parsedworkflows.length + ); + data.subflows = parsedworkflows; + } + } + + setExportData(data) + setOpen(false) + } + + const sideloadWorkflow = (id, action, setOpen) => { + const storagewf = localStorage.getItem("workflows") const storageWorkflows = JSON.parse(storagewf) if (storageWorkflows === null || storageWorkflows === undefined || storageWorkflows.length === 0) { } else { for (var i = 0; i < storageWorkflows.length; i++) { if (storageWorkflows[i].id === id) { - if (storageWorkflows[i].image !== "") { - return + if (storageWorkflows[i].image !== "" && storageWorkflows[i].image !== undefined && storageWorkflows[i].image !== null) { + + if (action === undefined || action === null || action === "") { + console.log("RETURNING") + return + } } } } @@ -1718,8 +1807,16 @@ const Workflows = (props) => { return response.json() }) .then((responseJson) => { - if (openEdit) { - setEditing(responseJson) + if (responseJson.success !== false && responseJson.id !== undefined) { + if (action === "edit") { + setEditing(responseJson) + } else if (action === "publish") { + setPublishModalOpen(true) + setSelectedWorkflow(responseJson) + } else if (action === "export") { + exportSingleWorkflow(responseJson, setOpen) + } + } for (var i = 0; i < storageWorkflows.length; i++) { @@ -1730,8 +1827,9 @@ const Workflows = (props) => { } } - setWorkflows(storageWorkflows) - //setFilteredWorkflows(storageWorkflows) + //setWorkflows(storageWorkflows) + setFilteredWorkflows(storageWorkflows) + //setUpdate(Math.random()) }) .catch((error) => { console.log(error.toString()) @@ -1878,7 +1976,9 @@ const Workflows = (props) => { const appGroup = getWorkflowAppgroup(data) const [triggers, subflows] = getWorkflowMeta(data) - const isDistributed = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.includes(userdata.active_org.id) + const hasSuborgs = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.length > 0 + const isDistributed = (data.parentorg_workflow !== undefined && data.parentorg_workflow !== null && data.parentorg_workflow.length > 0) //|| (data.org_id !== userdata.active_org.id && data.org_id !== undefined && data.org_id !== null && data.org_id.length > 0) + const workflowMenuButtons = ( { setAnchorEl(null); }} > + {isDistributed ? + { + navigate(`/workflows/${data.id}`) + }} + > + + Explore Workflow + + : null} { event.stopPropagation() if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") { @@ -1899,7 +2011,9 @@ const Workflows = (props) => { } else { //toast("Need to side-load workflow to be edited properly") - sideloadWorkflow(data.id, true) + sideloadWorkflow(data.id, "edit") + + toast.info("Loading full workflow for editing. Please wait...") } }} key={"change"} @@ -1909,9 +2023,11 @@ const Workflows = (props) => { { - setSelectedWorkflow(data); - setPublishModalOpen(true); + sideloadWorkflow(data.id, "publish") + + toast.info("Loading full workflow for publishing. Please wait...") }} key={"publish"} > @@ -1920,9 +2036,10 @@ const Workflows = (props) => { { - copyWorkflow(data); - setOpen(false); + duplicateWorkflow(data) + setOpen(false) }} key={"duplicate"} > @@ -1931,52 +2048,11 @@ const Workflows = (props) => { { - setExportModalOpen(true); + sideloadWorkflow(data.id, "export", setOpen) - if (data.triggers !== null && data.triggers !== undefined) { - var newSubflows = []; - for (var key in data.triggers) { - const trigger = data.triggers[key]; - - if ( - trigger.parameters !== null && - trigger.parameters !== undefined - ) { - for (var subkey in trigger.parameters) { - const param = trigger.parameters[subkey]; - if ( - param.name === "workflow" && - param.value !== data.id && - !newSubflows.includes(param.value) - ) { - newSubflows.push(param.value); - } - } - } - } - - var parsedworkflows = []; - for (var key in newSubflows) { - const foundWorkflow = workflows.find( - (workflow) => workflow.id === newSubflows[key] - ); - if (foundWorkflow !== undefined && foundWorkflow !== null) { - parsedworkflows.push(foundWorkflow); - } - } - - if (parsedworkflows.length > 0) { - console.log( - "Appending subflows during export: ", - parsedworkflows.length - ); - data.subflows = parsedworkflows; - } - } - - setExportData(data); - setOpen(false); + toast.info("Loading full workflow to be exported. Please wait...") }} key={"export"} > @@ -1985,6 +2061,7 @@ const Workflows = (props) => { { setDeleteModalOpen(true); setSelectedWorkflowId(data.id); @@ -2076,7 +2153,7 @@ const Workflows = (props) => { } return ( -
+
{selectedCategory !== "" ? @@ -2462,6 +2539,8 @@ const Workflows = (props) => { } const reader = new FileReader(); + var workflowids = [] + // Waits for the read reader.addEventListener("load", (event) => { var data = reader.result; @@ -2497,7 +2576,8 @@ const Workflows = (props) => { data.org_id = userdata.active_org.id data.org = [] data.execution_org = {} - + + workflowids.push(data.id) // Actually create it setNewWorkflow( @@ -2528,6 +2608,10 @@ const Workflows = (props) => { } setLoadWorkflowsModalOpen(false); + + if (workflowids.length > 0) { + setHighlightIds(workflowids) + } }; const getWorkflowMeta = (data) => { @@ -3682,7 +3766,7 @@ const Workflows = (props) => { workflowDelay += 75 } else { return ( - + ) diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index ace7c908..1aa83b26 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -1897,87 +1897,87 @@ func buildEnvVars(envMap map[string]string) []corev1.EnvVar { } func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { -if request.Body == nil { - log.Printf("[WARNING] (2) No body in request for workflowqueue") - resp.WriteHeader(http.StatusBadRequest) - return -} - -defer request.Body.Close() -body, err := ioutil.ReadAll(request.Body) -if err != nil { - log.Printf("[WARNING] (3) Failed reading body for workflowqueue") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return -} - -var actionResult shuffle.ActionResult -err = json.Unmarshal(body, &actionResult) -if err != nil { - log.Printf("[ERROR] Failed shuffle.ActionResult unmarshaling (2): %s", err) - //resp.WriteHeader(401) - //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - //return -} - -if len(actionResult.ExecutionId) == 0 { - log.Printf("[ERROR] No workflow execution id in action result. Data: %s", string(body)) - resp.WriteHeader(400) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflow execution id in action result"}`))) - return -} - -// 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database -// 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit -// 3. Add to and update actionResult in workflowExecution -// 4. Push to db -// IF FAIL: Set executionstatus: abort or cancel -ctx := context.Background() -workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) -if err != nil { - log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err) - resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId))) - return -} - -if workflowExecution.Authorization != actionResult.Authorization { - log.Printf("[ERROR][%s] Bad authorization key when updating node (workflowQueue). Want: %s, Have: %s", actionResult.ExecutionId, workflowExecution.Authorization, actionResult.Authorization) - resp.WriteHeader(403) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key"}`))) - return -} - -if workflowExecution.Status == "FINISHED" { - log.Printf("[DEBUG][%s] Workflowexecution is already FINISHED. No further action can be taken", workflowExecution.ExecutionId) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because it has status %s. Lastnode: %s"}`, workflowExecution.Status, workflowExecution.LastNode))) - return -} - -if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { - log.Printf("[WARNING][%s] Workflowexecution already has status %s. No further action can be taken", workflowExecution.ExecutionId, workflowExecution.Status) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status))) - return -} - -retries := 0 -retry, retriesok := request.URL.Query()["retries"] -if retriesok && len(retry) > 0 { - val, err := strconv.Atoi(retry[0]) - if err == nil { - retries = val + if request.Body == nil { + log.Printf("[WARNING] (2) No body in request for workflowqueue") + resp.WriteHeader(http.StatusBadRequest) + return } -} -log.Printf("[DEBUG][%s] Action: Received, Label: '%s', Action: '%s', Status: %s, Run status: %s, Extra=Retry:%d", workflowExecution.ExecutionId, actionResult.Action.Label, actionResult.Action.AppName, actionResult.Status, workflowExecution.Status, retries) + defer request.Body.Close() + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("[WARNING] (3) Failed reading body for workflowqueue") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } -//results = append(results, actionResult) -//log.Printf("[INFO][%s] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", workflowExecution.ExecutionId, action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) -//log.Printf("[DEBUG][%s] In workflowQueue with transaction", workflowExecution.ExecutionId) -runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) + var actionResult shuffle.ActionResult + err = json.Unmarshal(body, &actionResult) + if err != nil { + log.Printf("[ERROR] Failed shuffle.ActionResult unmarshaling (2): %s", err) + //resp.WriteHeader(401) + //resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + //return + } + + if len(actionResult.ExecutionId) == 0 { + log.Printf("[ERROR] No workflow execution id in action result. Data: %s", string(body)) + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflow execution id in action result"}`))) + return + } + + // 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database + // 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit + // 3. Add to and update actionResult in workflowExecution + // 4. Push to db + // IF FAIL: Set executionstatus: abort or cancel + ctx := context.Background() + workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) + if err != nil { + log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err) + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId))) + return + } + + if workflowExecution.Authorization != actionResult.Authorization { + log.Printf("[ERROR][%s] Bad authorization key when updating node (workflowQueue). Want: %s, Have: %s", actionResult.ExecutionId, workflowExecution.Authorization, actionResult.Authorization) + resp.WriteHeader(403) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key"}`))) + return + } + + if workflowExecution.Status == "FINISHED" { + log.Printf("[DEBUG][%s] Workflowexecution is already FINISHED. No further action can be taken", workflowExecution.ExecutionId) + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because it has status %s. Lastnode: %s"}`, workflowExecution.Status, workflowExecution.LastNode))) + return + } + + if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { + log.Printf("[WARNING][%s] Workflowexecution already has status %s. No further action can be taken", workflowExecution.ExecutionId, workflowExecution.Status) + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status))) + return + } + + retries := 0 + retry, retriesok := request.URL.Query()["retries"] + if retriesok && len(retry) > 0 { + val, err := strconv.Atoi(retry[0]) + if err == nil { + retries = val + } + } + + log.Printf("[DEBUG][%s] Action: Received, Label: '%s', Action: '%s', Status: %s, Run status: %s, Extra=Retry:%d", workflowExecution.ExecutionId, actionResult.Action.Label, actionResult.Action.AppName, actionResult.Status, workflowExecution.Status, retries) + + //results = append(results, actionResult) + //log.Printf("[INFO][%s] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", workflowExecution.ExecutionId, action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) + //log.Printf("[DEBUG][%s] In workflowQueue with transaction", workflowExecution.ExecutionId) + runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) } // Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times @@ -2090,7 +2090,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl */ } } - + if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) //result(s)", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results))