diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 6e5a56e7..98a5f8aa 100755 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -22,8 +22,7 @@ import dateutil import threading import concurrent.futures -from io import StringIO as StringBuffer -from io import BytesIO +from io import StringIO as StringBuffer, BytesIO from liquid import Liquid, defaults runtime = os.getenv("SHUFFLE_SWARM_CONFIG", "") @@ -1296,7 +1295,7 @@ class AppBase: def delete_cache(self, key): org_id = self.full_execution["workflow"]["execution_org"]["id"] - url = "%s/api/v1/orgs/%s/delete_cache" % (self.url, org_id, key) + url = "%s/api/v1/orgs/%s/delete_cache" % (self.url, org_id) data = { "workflow_id": self.full_execution["workflow"]["id"], @@ -1309,11 +1308,11 @@ class AppBase: response = requests.post(url, json=data, verify=False) try: allvalues = response.json() - return allvalues + return json.dumps(allvalues) except Exception as e: self.logger.info("[ERROR} Failed to parse response from delete_cache: %s" % e) #return response.json() - return {"success": False, "reason": f"Failed to delete cache for key {key}"} + return json.dumps({"success": False, "reason": f"Failed to delete cache for key '{key}'"}) def set_cache(self, key, value): org_id = self.full_execution["workflow"]["execution_org"]["id"] @@ -2301,13 +2300,13 @@ class AppBase: #if len(template) > 100: # self.logger.info("[DEBUG] Running liquid with data of length %d" % len(template)) #self.logger.info(f"[DEBUG] Data: {template}") - run = Liquid(template, mode="wild", from_file=False, filters=shuffle_filters.filters) - # Can't handle self yet (?) all_globals = globals() - all_globals["self"] = self + all_globals["self"] = self + run = Liquid(template, mode="wild", from_file=False, filters=shuffle_filters.filters, globals=all_globals) - ret = run.render(**all_globals) + # Add locals that are missing to globals + ret = run.render() return ret except jinja2.exceptions.TemplateNotFound as e: self.logger.info(f"[ERROR] Liquid Template error: {e}") diff --git a/backend/app_sdk/requirements.txt b/backend/app_sdk/requirements.txt index bacb3bc8..4f432bfc 100755 --- a/backend/app_sdk/requirements.txt +++ b/backend/app_sdk/requirements.txt @@ -1,7 +1,7 @@ urllib3==1.26.5 requests==2.25.1 MarkupSafe==2.0.1 -liquidpy==0.7.6 +liquidpy==0.8.1 flask[async]==2.0.2 waitress==2.1.0 #flask==1.1.2 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 28b665b0..2180be03 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -39,6 +39,9 @@ import AlertTemplate from "./components/AlertTemplate"; import { useAlert, positions, Provider } from "react-alert"; import { isMobile } from "react-device-detect"; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + import Drift from "react-driftjs"; // Production - backend proxy forwarding in nginx @@ -561,6 +564,18 @@ const App = (message, props) => { {includedData} + ); diff --git a/frontend/src/components/OrgHeader.jsx b/frontend/src/components/OrgHeader.jsx index 4ad76cc3..95020bb1 100644 --- a/frontend/src/components/OrgHeader.jsx +++ b/frontend/src/components/OrgHeader.jsx @@ -38,7 +38,6 @@ const OrgHeader = (props) => { handleEditOrg, } = props; - //const alert = useAlert(); const classes = useStyles(); var upload = ""; diff --git a/frontend/src/components/OrgHeaderexpanded.jsx b/frontend/src/components/OrgHeaderexpanded.jsx index 2d16effc..268350a2 100644 --- a/frontend/src/components/OrgHeaderexpanded.jsx +++ b/frontend/src/components/OrgHeaderexpanded.jsx @@ -1,8 +1,8 @@ import React, { useEffect } from "react"; import { makeStyles } from "@mui/styles"; -import { useAlert } from "react-alert"; import theme from '../theme.jsx'; +import { toast } from "react-toastify" import { FormControl, @@ -24,6 +24,7 @@ import { Tab, Grid, IconButton, + Autocomplete, } from "@mui/material"; import { @@ -48,7 +49,6 @@ const OrgHeaderexpanded = (props) => { adminTab, } = props; - const alert = useAlert(); const classes = useStyles(); const defaultBranch = "master"; @@ -155,6 +155,47 @@ const OrgHeaderexpanded = (props) => { : selectedOrganization.sso_config.openid_token ) + const [workflows, setWorkflows] = React.useState([]) + const [workflow, setWorkflow] = React.useState({}) + + const getAvailableWorkflows = (trigger_index) => { + fetch(globalUrl + "/api/v1/workflows", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson !== undefined) { + setWorkflows(responseJson) + + if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { + + const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) + if (workflow !== undefined && workflow !== null) { + setWorkflow(workflow) + } + } + } + }) + .catch((error) => { + console.log("Error getting workflows: " + error); + }) + } + + useEffect(() => { + getAvailableWorkflows() + }, []) + const handleEditOrg = ( name, description, @@ -188,17 +229,29 @@ const OrgHeaderexpanded = (props) => { .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - alert.error("Failed updating org: ", responseJson.reason); + toast("Failed updating org: ", responseJson.reason); } else { - alert.success("Successfully edited org!"); + toast("Successfully edited org!"); } }) ) .catch((error) => { - alert.error("Err: " + error.toString()); + toast("Err: " + error.toString()); }); }; + + const handleWorkflowSelectionUpdate = (e, isUserinput) => { + if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { + console.log("Returning as there's no id") + return null + } + + setWorkflow(e.target.value) + setNotificationWorkflow(e.target.value.id) + toast("Updated notification workflow. Don't forget to save!") + } + const orgSaveButton = (
@@ -247,34 +300,149 @@ const OrgHeaderexpanded = (props) => { - Notification Workflow ID - { - setNotificationWorkflow(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> + Notification Workflow + {/* + + Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows + + */} +
+ {workflows !== undefined && workflows !== null && workflows.length > 0 ? + { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette.borderRadius, + }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) + + var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { + setNotificationWorkflow(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + } +
+ {orgSaveButton} +
+
diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index 209c7516..d963ec0b 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -1131,7 +1131,7 @@ const ParsedAction = (props) => { borderRadius: theme.palette.borderRadius, }} onChange={(event, newValue) => { - console.log("SELECT: ", event, newValue) + console.log("SELECT: ", event, newValue) // Workaround with event lol //if (newValue !== undefined && newValue !== null) { // setNewSelectedAction({ target: { value: newValue.name } }); diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 4fd0e6bf..0e3868b0 100755 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -2718,7 +2718,7 @@ const Apps = (props) => { - Paste in a URL, and we will make it into an app for you. This may take multiple minutes based on the size of the documentation. {isCloud ? "" : "Uses to Shuffle Cloud (https://shuffler.io) for processing."} + Paste in a URL, and we will make it into an app for you. This may take multiple minutes based on the size of the documentation. {isCloud ? "" : "Uses Shuffle Cloud (https://shuffler.io) for processing (for now)."}