diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index bd9d24b0..d2f3d2f5 100755 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1713,6 +1713,112 @@ class AppBase: ret = requests.post("%s%s" % (self.url, upload_path), files=files, headers=new_headers, verify=False, proxies=self.proxy_config) return file_ids + + def validate_condition(self, sourcevalue, check, destinationvalue): + if check == "=" or check == "==" or check.lower() == "equals": + if str(sourcevalue).lower() == str(destinationvalue).lower(): + return True + elif check == "!=" or check.lower() == "does not equal": + if str(sourcevalue).lower() != str(destinationvalue).lower(): + return True + elif check.lower() == "startswith": + if str(sourcevalue).lower().startswith(str(destinationvalue).lower()): + return True + elif check.lower() == "endswith": + if str(sourcevalue).lower().endswith(str(destinationvalue).lower()): + return True + elif check.lower() == "contains": + if destinationvalue.lower() in sourcevalue.lower(): + return True + + elif check.lower() == "is empty" or check.lower() == "is_empty": + try: + if len(json.loads(sourcevalue)) == 0: + return True + except Exception as e: + self.logger.info(f"[ERROR] Failed to check if empty as list: {e}") + + if len(str(sourcevalue)) == 0: + return True + + elif check.lower() == "contains_any_of": + newvalue = [destinationvalue.lower()] + if "," in destinationvalue: + newvalue = destinationvalue.split(",") + elif ", " in destinationvalue: + newvalue = destinationvalue.split(", ") + + for item in newvalue: + if not item: + continue + + if item.strip() in sourcevalue: + return True + + + # FIXME: This will be buggy if using > and >= operators in the future. + elif check.lower() == "larger than" or check.lower() == "bigger than" or check == ">" or check == ">=": + try: + if str(sourcevalue).isdigit() and str(destinationvalue).isdigit(): + if int(sourcevalue) > int(destinationvalue): + return True + + except AttributeError as e: + self.logger.info("[WARNING] Condition larger than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e)) + + try: + destinationvalue = len(json.loads(destinationvalue)) + except Exception as e: + self.logger.info(f"[WARNING] Failed to convert destination to list: {e}") + try: + # Check if it's a list in autocast and if so, check the length + if len(json.loads(sourcevalue)) > int(destinationvalue): + return True + except Exception as e: + self.logger.info(f"[WARNING] Failed to check if larger than as list: {e}") + + + # FIXME: This will be buggy if using < and <= operators in the future. + elif check.lower() == "smaller than" or check.lower() == "less than" or check == "<" or check == "<=": + self.logger.info("In smaller than check: %s %s" % (sourcevalue, destinationvalue)) + + try: + if str(sourcevalue).isdigit() and str(destinationvalue).isdigit(): + if int(sourcevalue) < int(destinationvalue): + return True + + except AttributeError as e: + pass + + try: + destinationvalue = len(json.loads(destinationvalue)) + except Exception as e: + self.logger.info(f"[WARNING] Failed to convert destination to list: {e}") + + try: + # Check if it's a list in autocast and if so, check the length + if len(json.loads(sourcevalue)) < int(destinationvalue): + return True + except Exception as e: + self.logger.info(f"[WARNING] Failed to check if smaller than as list: {e}") + + elif check.lower() == "re" or check.lower() == "matches regex": + try: + found = re.search(str(destinationvalue), str(sourcevalue)) + except re.error as e: + return False + except Exception as e: + return False + + if found == None: + return False + + return True + else: + self.logger.error("[DEBUG] Condition: can't handle %s yet. Setting to true" % check) + + return False + #async def execute_action(self, action): def execute_action(self, action): @@ -2596,7 +2702,6 @@ class AppBase: return returndata, is_loop - # Sending self as it's not a normal function def parse_liquid(template, self): @@ -3083,110 +3188,7 @@ class AppBase: return "", parameter["value"], is_loop - def run_validation(sourcevalue, check, destinationvalue): - #self.logger.info("[DEBUG] Checking %s '%s' %s" % (sourcevalue, check, destinationvalue)) - - if check == "=" or check.lower() == "equals": - if str(sourcevalue).lower() == str(destinationvalue).lower(): - return True - elif check == "!=" or check.lower() == "does not equal": - if str(sourcevalue).lower() != str(destinationvalue).lower(): - return True - elif check.lower() == "startswith": - if str(sourcevalue).lower().startswith(str(destinationvalue).lower()): - return True - elif check.lower() == "endswith": - if str(sourcevalue).lower().endswith(str(destinationvalue).lower()): - return True - elif check.lower() == "contains": - if destinationvalue.lower() in sourcevalue.lower(): - return True - - elif check.lower() == "is empty" or check.lower() == "is_empty": - try: - if len(json.loads(sourcevalue)) == 0: - return True - except Exception as e: - self.logger.info(f"[ERROR] Failed to check if empty as list: {e}") - - if len(str(sourcevalue)) == 0: - return True - - elif check.lower() == "contains_any_of": - newvalue = [destinationvalue.lower()] - if "," in destinationvalue: - newvalue = destinationvalue.split(",") - elif ", " in destinationvalue: - newvalue = destinationvalue.split(", ") - - for item in newvalue: - if not item: - continue - - if item.strip() in sourcevalue: - return True - - elif check.lower() == "larger than" or check.lower() == "bigger than": - try: - if str(sourcevalue).isdigit() and str(destinationvalue).isdigit(): - if int(sourcevalue) > int(destinationvalue): - return True - - except AttributeError as e: - self.logger.info("[WARNING] Condition larger than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e)) - - try: - destinationvalue = len(json.loads(destinationvalue)) - except Exception as e: - self.logger.info(f"[WARNING] Failed to convert destination to list: {e}") - try: - # Check if it's a list in autocast and if so, check the length - if len(json.loads(sourcevalue)) > int(destinationvalue): - return True - except Exception as e: - self.logger.info(f"[WARNING] Failed to check if larger than as list: {e}") - - - elif check.lower() == "smaller than" or check.lower() == "less than": - self.logger.info("In smaller than check: %s %s" % (sourcevalue, destinationvalue)) - - try: - if str(sourcevalue).isdigit() and str(destinationvalue).isdigit(): - if int(sourcevalue) < int(destinationvalue): - return True - - except AttributeError as e: - pass - - try: - destinationvalue = len(json.loads(destinationvalue)) - except Exception as e: - self.logger.info(f"[WARNING] Failed to convert destination to list: {e}") - - try: - # Check if it's a list in autocast and if so, check the length - if len(json.loads(sourcevalue)) < int(destinationvalue): - return True - except Exception as e: - self.logger.info(f"[WARNING] Failed to check if smaller than as list: {e}") - - elif check.lower() == "re" or check.lower() == "matches regex": - try: - found = re.search(str(destinationvalue), str(sourcevalue)) - except re.error as e: - return False - except Exception as e: - return False - - if found == None: - return False - - return True - else: - self.logger.error("[DEBUG] Condition: can't handle %s yet. Setting to true" % check) - - return False - + def check_branch_conditions(action, fullexecution, self): # relevantbranches = workflow.branches where destination = action try: @@ -3286,8 +3288,7 @@ class AppBase: self.logger.error("[ERROR] Skipping '%s' -> %s -> '%s' because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"])) continue - # Configuration = negated because of WorkflowAppActionParam.. - validation = run_validation(sourcevalue, condition["condition"]["value"], destinationvalue) + validation = self.validate_condition(sourcevalue, condition["condition"]["value"], destinationvalue) try: if condition["condition"]["configuration"]: validation = not validation diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index b4e4d0db..a9f8ac18 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -571,14 +571,20 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { //return } + if len(actionResult.ExecutionId) == 0 { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Provide execution_id and authorization"}`))) + return + } + ctx := context.Background() workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) - if err != nil { + if err != nil || workflowExecution.ExecutionId != actionResult.ExecutionId { if len(actionResult.ExecutionId) > 0 { log.Printf("[WARNING][%s] Failed getting execution (streamresult): %s", actionResult.ExecutionId, err) } - resp.WriteHeader(401) + resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`))) return } @@ -637,9 +643,26 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { } } + if workflowExecution.Workflow.Sharing == "form" { + newWorkflow := shuffle.Workflow{ + Name: workflowExecution.Workflow.Name, + ID: workflowExecution.Workflow.ID, + Owner: workflowExecution.Workflow.Owner, + OrgId: workflowExecution.Workflow.OrgId, + + Sharing: workflowExecution.Workflow.Sharing, + Description: workflowExecution.Workflow.Description, + InputQuestions: workflowExecution.Workflow.InputQuestions, + InputMarkdown: workflowExecution.Workflow.InputMarkdown, + } + + workflowExecution.Results = []shuffle.ActionResult{} + workflowExecution.Workflow = newWorkflow + } + newjson, err := json.Marshal(workflowExecution) if err != nil { - resp.WriteHeader(401) + resp.WriteHeader(500) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking workflow execution"}`))) return } diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx index 093c9550..87fe533e 100644 --- a/frontend/src/components/AppFramework.jsx +++ b/frontend/src/components/AppFramework.jsx @@ -1129,7 +1129,7 @@ const AppFramework = (props) => { }, [newSelectedApp]) - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); const imgSize = 50; var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData diff --git a/frontend/src/components/AppSelection.jsx b/frontend/src/components/AppSelection.jsx index 635302d6..5bbb5ff0 100644 --- a/frontend/src/components/AppSelection.jsx +++ b/frontend/src/components/AppSelection.jsx @@ -63,7 +63,7 @@ const AppSelection = props => { document.title = "Choose your apps" const ref = useRef() let navigate = useNavigate(); - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); useEffect(() => { if (newSelectedApp === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) { diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index e4f1782f..b5871f2c 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -24,7 +24,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52 const Appsearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs //const theme = useTheme(); diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index d5b39411..3f5327fb 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -42,6 +42,7 @@ import { Delete, RestaurantRounded, Cloud, + CheckCircle } from "@mui/icons-material"; //import { useAlert @@ -71,6 +72,7 @@ const Billing = (props) => { const [currentAppRunsInNumber, setCurrentAppRunsInNumber] = useState(0); const [alertThresholds, setAlertThresholds] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.AlertThreshold !== undefined && selectedOrganization.Billing.AlertThreshold !== null ? selectedOrganization.Billing.AlertThreshold : [{ percentage: '', count: '', Email_send: false }]); const [currentIndex, setCurrentIndex] = useState(0); + const [deleteAlertVerification, setDeleteAlertVerification] = useState(false); useEffect(() => { if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) { @@ -353,13 +355,13 @@ const Billing = (props) => { if (subscription.name === "Enterprise" && subscription.active === true) { top_text = "Current Plan" - newPaperstyle.border = "1px solid #f85a3e" + // newPaperstyle.border = "1px solid #f85a3e" } var showSupport = false if (subscription.name.includes("default")) { top_text = "Custom Contract" - newPaperstyle.border = "1px solid #f85a3e" + newPaperstyle.border = "1px solid rgba(255,255,255,0.3)" showSupport = true } @@ -379,7 +381,7 @@ const Billing = (props) => { if (highlight === true) { // Add an "Upgrade now" button - newPaperstyle.border = "1px solid #f85a3e" + newPaperstyle.border = "1px solid rgba(255,255,255,0.3)" } if (hovered) { @@ -820,7 +822,8 @@ const Billing = (props) => { height: 40, fontSize: 16, color: "white", - backgroundImage: userdata.has_card_available ? null : "linear-gradient(to right, #f86a3e, #f34079)", + backgroundColor: userdata.has_card_available ? null : "#f86743", + // backgroundImage: userdata.has_card_available ? null : "linear-gradient(to right, #f86a3e, #f34079)", textTransform: "none", }} @@ -852,7 +855,7 @@ const Billing = (props) => { height: 40, fontSize: 16, color: "white", - backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", + backgroundColor: "#f86743", textTransform: 'none' }} onClick={() => { @@ -1036,11 +1039,11 @@ const Billing = (props) => { { height: 40, fontSize: 16, color: "white", - backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", - textTransform: 'none' + backgroundColor: "#f86743", + textTransform: 'none', }} onClick={() => { if (Cloud) { @@ -1188,7 +1191,7 @@ const Billing = (props) => { height: 40, fontSize: 16, color: "white", - backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", + backgroundColor: "#f86743", textTransform: 'none', cursor: getProfessionalServices ? 'pointer' : 'not-allowed', opacity: getProfessionalServices ? 1 : 0.6, @@ -1315,7 +1318,7 @@ const Billing = (props) => { width: 340, backgroundColor: hovered ? "#232427" : theme.palette.platformColor, borderRadius: theme.palette.borderRadius * 2, - border: "1px solid #f85a3e", + border: "1px solid rgba(255,255,255,0.3)", marginRight: 10, marginTop: 15, }} @@ -1373,7 +1376,7 @@ const Billing = (props) => { height: 40, fontSize: 16, color: "white", - backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", + backgroundColor: "#f86743", textTransform: 'none' }} onClick={() => { @@ -1381,6 +1384,8 @@ const Billing = (props) => { ReactGA.event({ category: "Billing", action: "click_public_training_button", + label: "Public Training", + userId: userdata?.id }); } navigate("/training") @@ -1399,21 +1404,24 @@ const Billing = (props) => { height: 40, fontSize: 16, color: "white", - backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", + backgroundColor: "#f86743", textTransform: 'none' }} onClick={() => { if (Cloud) { ReactGA.event({ category: "Billing", - action: "click_public_training_button", + action: "click_private_training_button", + label: "Private Training", + userId: userdata?.id, }); } - setOpenPrivateTraining(true) + setOpenPrivateTraining(true); }} > Private Training + setOpenPrivateTraining(false)} fullWidth @@ -1818,6 +1826,7 @@ const Billing = (props) => { // Update currentIndex based on remaining elements const findCurrentIndex = newAlertThresholds.some(threshold => threshold.Email_send === false); setCurrentIndex(findCurrentIndex ? newAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1); + toast.info("Alert Threshold deleted successfully. Don't forget to save your changes."); }; const HandleEditOrgForAlertThreshold = (orgId) => { @@ -1997,15 +2006,6 @@ const Billing = (props) => { /> : null} - {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : - : null} - - - {isCloud && selectedOrganization.subscriptions !== undefined && @@ -2261,7 +2261,29 @@ const Billing = (props) => { ) : null*/} -
+ {!isChildOrg && isCloud && ( +
+ + Professional Services + + + We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at support@shuffler.io. + +
+ {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? ( + isChildOrg ? null : ( + + ) + ) : null} + +
+
+ )} +
{ : " " + 0 + " "} app runs. - + + Please note: Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification. +
{alertThresholds.map((threshold, index) => (
@@ -2354,6 +2378,7 @@ const Billing = (props) => { margin="normal" variant="outlined" /> + {alertThresholds[index].Email_send === true && } { alertThresholds.length > 1 && ( @@ -2362,18 +2387,24 @@ const Billing = (props) => { disableElevation sx={{ padding: 0, - color: 'red', '&:hover': { backgroundColor: 'transparent', }, }} - onClick={() => { handleDeleteAlertThreshold(index) }} + onClick={() => { setDeleteAlertVerification(true) }} > - + ) } + setDeleteAlertVerification(false)} sx={{ '& .MuiBackdrop-root': { backgroundColor: 'rgba(0, 0, 0, 0.3)', }, }}> + Are you sure you want to delete this threshold? + + + + +
))}
diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index bd653e7d..59d541ba 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import theme from "../theme.jsx"; import { toast } from 'react-toastify'; -import ReactJson from "react-json-view"; +import ReactJson from "react-json-view-ssr"; import { Typography, diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index f7bbb7ed..b444cb97 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { useInterval } from "react-powerhooks"; import { toast } from 'react-toastify'; import theme from "../theme.jsx"; +import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { InputAdornment, @@ -79,7 +80,7 @@ const ConfigureWorkflow = (props) => { useEffect(() => { if (requiredActions.length === 0) { if (setConfigurationFinished !== undefined) { - setConfigurationFinished(true) + setConfigurationFinished(true) } } }, [requiredActions]) @@ -141,17 +142,18 @@ const ConfigureWorkflow = (props) => { // Where is this from? if (workflow === undefined || workflow === null || workflow.id === undefined) { - return null; + //console.log("Workflow is undefined or null: ", workflow) + return null } if (apps === undefined || apps === null) { console.log("Apps is undefined or null: ", apps) - return null; + return null } if (appAuthentication === undefined || appAuthentication === null) { console.log("App authentication is undefined or null: ", appAuthentication) - return null; + return null } const getApp = (actionId, appId) => { @@ -1386,9 +1388,23 @@ const ConfigureWorkflow = (props) => { : null } +
+ + {/* + +
+ */} + {requiredActions.length > 0 ? ( - + Please configure the following steps to help us complete your workflow. This can also be done later. diff --git a/frontend/src/components/DetectionRuleCard.jsx b/frontend/src/components/DetectionRuleCard.jsx index f5a30f67..4d60b152 100644 --- a/frontend/src/components/DetectionRuleCard.jsx +++ b/frontend/src/components/DetectionRuleCard.jsx @@ -1,22 +1,57 @@ -import React from "react"; +import React, { useState, useEffect, } from "react"; import { Card, CardContent, IconButton, Typography, Switch, + Tooltip, + Select, + MenuItem, + Divider, + FormLabel, } from "@mui/material"; -import EditIcon from "@mui/icons-material/Edit"; + +import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx'; +import { + Edit as EditIcon, +} from "@mui/icons-material"; import { toast } from "react-toastify"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import theme from '../theme.jsx'; -const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, ...otherProps }) => { + +const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => { const [openCodeEditor, setOpenCodeEditor] = React.useState(false); const [fileData, setFileData] = React.useState(""); const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled); + const [filteredBarchart, setFilteredBarchart] = React.useState(null) + + const [responseValue, setResponseValue] = React.useState("No response action") const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host); + console.log("Rulemapping: ", ruleMapping) + useEffect(() => { + + //const url = `${globalUrl}/api/v1/stats/app_executions_test2` + //const resp = LoadStats(globalUrl, ruleName) + //const resp = LoadStats(globalUrl, "app_executions_test2") + const resp = LoadStats(globalUrl, "app_executions_cloud") + resp.then((data) => { + if (data === undefined) { + setFilteredBarchart([]) + } else { + setFilteredBarchart(data) + } + }) + + if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) { + console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping) + } + }, []) + + console.log("Response Value: ", responseValue) + const handleSwitchChange = (event) => { if (folderDisabled) { toast.warn("Enable the directory to enable individual rules"); @@ -32,7 +67,8 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i toggleRule(file_id, !newIsEnabled, globalUrl, () => { setIsEnabled(newIsEnabled); }) - }; + } + const UpdateText = (text) => { fetch(`${globalUrl}/api/v1/files/${file_id}/edit`, { @@ -64,32 +100,121 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i - +
- {ruleName} + {ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})
- openEditBar(file_id, setOpenCodeEditor, setFileData, globalUrl)}> - - - + + + + + + openEditBar(file_id, setOpenCodeEditor, setFileData, globalUrl)}> + + + + + +
+ +
+ {filteredBarchart === null ? null : + + } +
+ + {/* {description} + */} { - const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, } = props + const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, } = props const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove @@ -79,15 +80,30 @@ 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')) - const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) + const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) const [inputMarkdown, setInputMarkdown] = React.useState(workflow.input_markdown !== undefined && workflow.input_markdown !== null ? workflow.input_markdown : "") const [outputMarkdown, setOutputMarkdown] = React.useState(workflow.output_markdown !== undefined && workflow.output_markdown !== null ? workflow.output_markdown : "") + const [scrollDone, setScrollDone] = React.useState(false) const classes = useStyles(); + if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) { + setTimeout(() => { + const foundScroll = document.getElementById(scrollTo) + if (foundScroll !== null) { + // Smooth scroll + foundScroll.scrollIntoView({ behavior: "smooth" }) + } + + }, 200) + setScrollDone(true) + + } + // Gets the generated workflow - const getGeneratedWorkflow = (workflow_id) => { - fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + const getGeneratedWorkflow = (workflow_id) => { + const url = `${globalUrl}/api/v1/workflows/${workflow_id}` + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -95,54 +111,55 @@ const EditWorkflow = (props) => { }, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 when getting workflow"); + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting workflow"); + } + + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.id === workflow_id) { + console.log("GOT WORKFLOW: ", responseJson) + if (name === "") { + innerWorkflow.name = responseJson.name + setName(responseJson.name) } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.id === workflow_id) { - console.log("GOT WORKFLOW: ", responseJson) - if (name === "") { - innerWorkflow.name = responseJson.name - setName(responseJson.name) - } - - if (description === "") { - innerWorkflow.description = responseJson.description - setDescription(description) - } - - if (newWorkflowTags === []) { - innerWorkflow.tags = responseJson.tags - setNewWorkflowTags(responseJson.tags) - } - - if (selectedUsecases === []) { - selectedUsecases = responseJson.usecase_ids - } - - innerWorkflow.id = responseJson.id - innerWorkflow.blogpost = responseJson.blogpost - innerWorkflow.actions = responseJson.actions - innerWorkflow.triggers = responseJson.triggers - innerWorkflow.branches = responseJson.branches - innerWorkflow.comments = responseJson.comments - innerWorkflow.workflow_variables = responseJson.workflow_variables - innerWorkflow.execution_variables = responseJson.execution_variables - - - setInnerWorkflow(innerWorkflow) - setUpdate(Math.random()) + if (description === "") { + innerWorkflow.description = responseJson.description + setDescription(description) } - }) - .catch((error) => { - //toast(error.toString()); - console.log("Get workflow error: ", error.toString()); - }) - } + + if (newWorkflowTags === []) { + innerWorkflow.tags = responseJson.tags + setNewWorkflowTags(responseJson.tags) + } + + if (selectedUsecases === []) { + selectedUsecases = responseJson.usecase_ids + } + + innerWorkflow.id = responseJson.id + innerWorkflow.blogpost = responseJson.blogpost + innerWorkflow.actions = responseJson.actions + innerWorkflow.triggers = responseJson.triggers + innerWorkflow.branches = responseJson.branches + innerWorkflow.comments = responseJson.comments + innerWorkflow.workflow_variables = responseJson.workflow_variables + innerWorkflow.execution_variables = responseJson.execution_variables + + + setInnerWorkflow(innerWorkflow) + setUpdate(Math.random()) + } + }) + .catch((error) => { + //toast(error.toString()); + console.log("Get workflow error: ", error.toString()); + }) + } if (foundWorkflowId.length > 0) { getGeneratedWorkflow(foundWorkflowId) @@ -162,6 +179,7 @@ const EditWorkflow = (props) => { return ( { setModalOpen(false); @@ -191,7 +209,7 @@ const EditWorkflow = (props) => { { }
- Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more + Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more + {/*
+ */} {showUpload === true ?
@@ -247,7 +266,7 @@ const EditWorkflow = (props) => {
-
+
{/* -
- ) - })} - - - - {inputQuestions.length === 0 ? null : -
- - Input Markdown - - { - setInputMarkdown(e.target.value) - workflow.input_markdown = e.target.value - setWorkflow(workflow) - setUpdate(Math.random()) - }} - /> - - {/* - - Output Markdown - - - */} -
- } - - Git Backup Repository @@ -998,8 +872,152 @@ const EditWorkflow = (props) => { -
- : null} + + + + + Input fields + + + Input fields are fields that will be used during the startup of the workflow. These will be formatted in JSON and is most commonly used from the Form page for this workflow. If chosen in the User Input node, these will be required fields. Use Semi-Colon ";" to create dropdown options. The first key will be the name shown, and subsequent keys will be the available values. + + + + {inputQuestions.map((data, index) => { + var showListinfo = false + if (data.value !== undefined && data.value !== null && data.value.length > 0) { + if (data.value.includes(";")) { + showListinfo = true + } + } + + return ( +
+ { + inputQuestions[index].name = e.target.value + setInputQuestions(inputQuestions) + setUpdate(Math.random()); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + }} + /> + { + // Replace multiple semicolon with one + e.target.value = e.target.value.replace(";;", ";") + + inputQuestions[index].value = e.target.value + setInputQuestions(inputQuestions) + setUpdate(Math.random()); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + }} + /> + +
+ ) + })} + + + +
+ + Input Markdown + + + Markdown will be shown on the Form page. Output for a Workflow is also shown in Markdown, and is controlled by the LAST action that runs. + + { + //console.log("KEY: ", e.key) + if (e.key === "Tab") { + e.preventDefault() + } + }} + + onChange={(e) => { + setInputMarkdown(e.target.value) + workflow.input_markdown = e.target.value + setWorkflow(workflow) + setUpdate(Math.random()) + }} + /> +
+
+ : null} + @@ -1047,7 +1065,7 @@ const EditWorkflow = (props) => { : null} - {newWorkflow === true && name.length > 2 ? + {/*newWorkflow === true && name.length > 2 ?
{ onlyResults={true} />
- : null} + : null*/} ) diff --git a/frontend/src/components/NewHeader.jsx b/frontend/src/components/NewHeader.jsx index 3dc07460..9da1a48a 100644 --- a/frontend/src/components/NewHeader.jsx +++ b/frontend/src/components/NewHeader.jsx @@ -132,6 +132,8 @@ const Header = (props) => { isMobile, serverside, billingInfo, + + notifications, } = props; const [isHeader, setIsHeader] = React.useState(false); const [modalOpen, setModalOpen] = useState(false); @@ -311,18 +313,19 @@ const Header = (props) => { localStorage.setItem("globalUrl", responseJson.region_url); //globalUrl = responseJson.region_url } + if (responseJson["reason"] === "SSO_REDIRECT") { + toast.info("Redirecting to SSO login page as SSO is required for this organization.") setTimeout(() => { - toast.info("Redirecting to SSO login page as SSO is required for this organization.") window.location.href = responseJson["url"] return }, 2000) } else { + toast("Successfully changed active organization - refreshing!"); setTimeout(() => { window.location.reload() }, 2000); } - toast("Successfully changed active organization - refreshing!"); } else { if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { toast(responseJson.reason); @@ -416,26 +419,19 @@ const Header = (props) => { - - { - handleClose(); - }} - > - Notifications - - - - - { - handleClose(); - }} - > - About - - + + { + handleClose(); + }} + > + Notifications ({ + notifications === undefined || notifications === null ? 0 : + notifications?.filter((notification) => notification.read === false).length + }) + + {/* { - {userdata?.public_username === undefined || userdata?.public_username === null || userdata?.public_username.length <= 0 ? null : + {/*userdata?.public_username === undefined || userdata?.public_username === null || userdata?.public_username.length <= 0 ? null : { @@ -467,9 +463,18 @@ const Header = (props) => { Creator page - } + */} + + { + handleClose(); + }} + > + About + + { @@ -483,7 +488,7 @@ const Header = (props) => { - Version: 1.4.0 + Version: 1.4.5 @@ -910,7 +915,6 @@ const Header = (props) => { }} > {avatarMenu} - {/*notificationMenu*/} {supportMenu} {logoCheck} diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index 45324bd6..17d8d6d2 100755 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -167,7 +167,7 @@ const AuthenticationOauth2 = (props) => { //console.log("APP: ", selectedApp) if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") { handleOauth2Request( - "efe4c3fe-84a1-4821-a84f-23a6cfe8e72d", + "fd55c175-aa30-4fa6-b303-09a29fb3f750", "", "https://graph.microsoft.com", ["Mail.ReadWrite", "Mail.Send", "offline_access"], @@ -524,7 +524,7 @@ const AuthenticationOauth2 = (props) => { //alert('"Secure Payment" window closed!'); if (getAppAuthentication !== undefined) { - getAppAuthentication(true, true, true); + getAppAuthentication(true, true, true, selectedAction.id) } toast("Authentication successful!") @@ -538,7 +538,7 @@ const AuthenticationOauth2 = (props) => { setFinalized(true) } } else { - console.log("Not closed") + //console.log("Not closed") } }, 1000); //do { @@ -739,7 +739,7 @@ const AuthenticationOauth2 = (props) => { - Oauth2 requires a client ID and secret to authenticate, defined in the remote system. {authenticationType.type === "oauth2-app" ? null : Your redirect URL is {window.location.origin}/set_authentication - } + Oauth2 requires a client ID and secret to authenticate, defined in the remote system. Your redirect URL is {window.location.origin}/set_authentication -  { setSSORequired(event.target.checked); }; + + const HandleTestSSO = () => { + const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; + const data = { + org_id: selectedOrganization?.id, + sso_test: true, + } + fetch(url, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }).then((response) => { + if (response.status !== 200) { + toast.error("Failed to test sso. Please try again later or contact support@shuffler.io if issue persist.") + return + } + return response.json(); + }).then((responjson) => { + if (responjson["reason"] === "SSO_REDIRECT") { + setTimeout(() => { + toast.info("Redirecting to SSO login page as SSO is required for this organization.") + window.location.href = responjson["url"]; + return + }, 2000) + } else { + toast.error("No SSO found for this org. Please set up sso for this org.") + } + }).catch((err) => { + console.log("error for sso test is: ", err) + }) + } + return (
@@ -677,6 +715,42 @@ const OrgHeaderexpanded = (props) => { {SSORequired ? 'Required' : 'Optional'}
+
+ + You can test your SSO configuration by clicking the button below. Before testing, ensure you have set Open ID Connect or SAML SSO credentials. + + 0 || + ssoCertificate.length > 0 || + openidAuthorization.length > 0 || + openidClientId.length > 0 + ) + ? "Please ensure all SSO credentials are set before testing." + : "" + } + > + + + + +
OpenID connect diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index fb462566..d1a9a228 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -90,9 +90,10 @@ import { Circle as CircleIcon, SquareFoot as SquareFootIcon, Storage as StorageIcon, + Check as CheckIcon, } from '@mui/icons-material'; -const useStyles = makeStyles({ +export const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important", }, @@ -1500,92 +1501,7 @@ const ParsedAction = (props) => { - {/* - {}} - > -
- - - - - - */} - {/* - { - //setAuthenticationModalOpen(true); - console.log("Should enable/disable magic!") - console.log("Action: ", selectedAction) - if (selectedAction.run_magic_output === undefined) { - selectedAction.run_magic_output = true - } else { - if (selectedAction.run_magic_output === true) { - selectedAction.run_magic_output = false - } else { - selectedAction.run_magic_output = true - } - } - setSelectedAction(selectedAction) - setUpdate(Math.random()); - }} - > - - - - - */} - {/* - { - }} - > - - - - - - - */} { aiSubmit("Fill based on previous values", undefined, undefined, selectedAction) //} setAutocompleting(true) + + setTimeout(() => { + setAutocompleting(false) + }, 3000) }} > { for (let [key,keyval] in Object.entries(selectedAction.parameters)) { if (selectedAction.parameters[key].configuration === false) { - console.log("FIELDSKIP: ", selectedAction.parameters[key].name) + //console.log("FIELDSKIP: ", selectedAction.parameters[key].name) continue } @@ -2085,7 +2005,18 @@ const ParsedAction = (props) => { }} value={data} > - {data.last_modified === true ? + + {data?.validation?.valid === true ? + + + + : null } + {data?.last_modified === true ? { color="secondary" /> : null} - {data.app.app_version !== undefined && data.app.app_version !== null && data.app.app_version !== "" && data.app.app_version !== "undefined" ? + {/*data.app.app_version !== undefined && data.app.app_version !== null && data.app.app_version !== "" && data.app.app_version !== "undefined" ? - : null} + : null*/} {data.label} ); @@ -2342,6 +2273,7 @@ const ParsedAction = (props) => { value={selectedAction} classes={{ inputRoot: classes.inputRoot }} groupBy={(option) => { + // FIXME: Sorting // Most popular // Is categorized // Uncategorized @@ -2364,7 +2296,6 @@ const ParsedAction = (props) => { }, }} filterOptions={(options, { inputValue }) => { - //console.log("Option contains?: ", inputValue, options) const lowercaseValue = inputValue.toLowerCase() options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) @@ -2449,22 +2380,6 @@ const ParsedAction = (props) => { extraUrl = descSplit[descSplit.length-1] } - //for (let [line,lineval] in Object.entries(descSplit)) { - // if (descSplit[line].includes("http") && descSplit[line].includes("://")) { - // const urlsplit = descSplit[line].split("/") - // try { - // extraUrl = "/"+urlsplit.slice(3, urlsplit.length).join("/") - // } catch (e) { - // //console.log("Failed - running with -1") - // extraUrl = "/"+urlsplit.slice(3, urlsplit.length-1).join("/") - // } - - - // //console.log("NO BASEURL TOO!! Why missing last one in certain scenarios (sevco)?", extraUrl, urlsplit, descSplit[line]) - // //break - // } - //} - if (extraUrl.length > 0) { if (extraUrl.includes(" ")) { extraUrl = extraUrl.split(" ")[0] @@ -2492,6 +2407,7 @@ const ParsedAction = (props) => { ); }} renderInput={(params) => { + if (params.inputProps?.value) { const prefixes = ["Post", "Put", "Patch"]; for (let prefix of prefixes) { @@ -2512,84 +2428,86 @@ const ParsedAction = (props) => { } - const actionDescription = ( - - - - {params.inputProps.value} - - { - event.preventDefault(); - event.stopPropagation(); - }} - - onClick={() => { - setHiddenDescription(true) - const inputElement = document.getElementById(uiBox); - if (inputElement) { - inputElement.focus(); - } - }}> - - - - - - - Description: {selectedAction?.description} - - - - ); - - return ( - - + + {params.inputProps.value} + + { + event.preventDefault(); + event.stopPropagation(); + }} + + onClick={() => { + setHiddenDescription(true) + const inputElement = document.getElementById(uiBox); + if (inputElement) { + inputElement.focus(); + } + }} + > + + + + + + + Description: {selectedAction?.description} + + + + ) + */ - data-lpignore="true" - autocomplete="off" - dataLPIgnore="true" - autoComplete="off" - - color="primary" - id="checkbox-search" - variant="body1" - style={{ - backgroundColor: theme.palette.inputColor, - borderRadius: theme.palette.borderRadius, + return ( + - - ); - }} - /> + > + + + ); + }} + /> ) : null} {/*setNewSelectedAction !== undefined ? @@ -2939,58 +2857,24 @@ const ParsedAction = (props) => { } } - /* - if ( - (selectedAction.auth_not_required !== undefined && !selectedAction.auth_not_required) && - selectedActionParameters[count] !== undefined && - selectedActionParameters[count] !== null && - selectedActionParameters[count].value !== undefined && - selectedAction.parameters[count] !== undefined && - selectedAction.parameters[count] !== null && - selectedAction.parameters[count].value !== undefined && - selectedAction.selectedAuthentication !== undefined && - selectedAction.selectedAuthentication.fields !== undefined && - selectedAction.selectedAuthentication.fields[data.name] !== - undefined - ) { - */ - - /* - if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) { - - // This sets the placeholder in the frontend. (Replaced in backend) - selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name]; - selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name]; - setSelectedAction(selectedAction); - //setUpdate(Math.random()) - - if (authWritten) { - return null - } - - authWritten = true - return ( - - Authentication fields are hidden - - ) - } - */ + if (selectedAction.parameters === undefined || selectedAction.parameters === null || selectedAction.parameters.length !== selectedActionParameters.length) { + //selectedAction.parameters = selectedActionParameters + console.log("PARAM BUG: ", selectedAction) + } //!selectedAction.auth_not_required && if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) { + // This sets the placeholder in the frontend. (Replaced in backend) - selectedActionParameters[count].value = - selectedAction.selectedAuthentication.fields[data.name]; - selectedAction.parameters[count].value = - selectedAction.selectedAuthentication.fields[data.name]; + if (selectedActionParameters[count] !== undefined) { + selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name] + } + + if (selectedAction.parameters[count] !== undefined) { + selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name] + } + setSelectedAction(selectedAction); //setUpdate(Math.random()) @@ -3064,7 +2948,7 @@ const ParsedAction = (props) => { if (data.value.length === 0) { if (data.name.toLowerCase() === "headers") { - console.log("Should show headers field instead with + and -!") + //console.log("Should show headers field instead with + and -!") // Check if file ID exists // @@ -3360,13 +3244,6 @@ const ParsedAction = (props) => { { setUiBox("closed") - - /* - const inputElement = document.getElementById(uiBox); - if (inputElement) { - inputElement.focus(); - } - */ }} > diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx index 0663ee93..e66afe8f 100644 --- a/frontend/src/components/Priorities.jsx +++ b/frontend/src/components/Priorities.jsx @@ -393,7 +393,10 @@ const Priorities = (props) => { return (
-

Notifications

+

Notifications ({ + notifications?.filter((notification) => showRead === true || notification.read === false).length + })

+ Notifications help you find potential problems with your workflows and apps.  { const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props; - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); let navigate = useNavigate(); if (window.location.pathname === "/workflows") { diff --git a/frontend/src/components/SearchData.jsx b/frontend/src/components/SearchData.jsx index c390666a..89ab2050 100644 --- a/frontend/src/components/SearchData.jsx +++ b/frontend/src/components/SearchData.jsx @@ -71,7 +71,7 @@ const SearchData = props => { // return null //} - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); // if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) { // setModalOpen(false) // } diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index 69f2f429..84896f82 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -44,7 +44,7 @@ import { import { validateJson } from "../views/Workflows.jsx"; -import ReactJson from "react-json-view"; +import ReactJson from "react-json-view-ssr"; import PaperComponent from "../components/PaperComponent.jsx"; import { padding, textAlign } from '@mui/system'; diff --git a/frontend/src/components/UsecaseSearch.jsx b/frontend/src/components/UsecaseSearch.jsx index 474fcce8..19aa2e53 100644 --- a/frontend/src/components/UsecaseSearch.jsx +++ b/frontend/src/components/UsecaseSearch.jsx @@ -349,7 +349,7 @@ const UsecaseSearch = (props) => { const [selectedAction, setSelectedAction] = React.useState({}); const [firstRequest, setFirstRequest] = React.useState(true); - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); //const alert = useAlert() useEffect(() => { diff --git a/frontend/src/components/WelcomeForm2.jsx b/frontend/src/components/WelcomeForm2.jsx index 47c040db..8dc43d5f 100644 --- a/frontend/src/components/WelcomeForm2.jsx +++ b/frontend/src/components/WelcomeForm2.jsx @@ -161,7 +161,7 @@ const WelcomeForm = (props) => { const [clickdiff, setclickdiff] = useState(0); const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); //const alert = useAlert(); let navigate = useNavigate(); diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index 791b8835..5de17156 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -28,7 +28,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52 const AppGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs //const [apps, setApps] = React.useState([]); diff --git a/frontend/src/components/WorkflowTemplatePopup.jsx b/frontend/src/components/WorkflowTemplatePopup.jsx index 13a6735f..a8949db5 100644 --- a/frontend/src/components/WorkflowTemplatePopup.jsx +++ b/frontend/src/components/WorkflowTemplatePopup.jsx @@ -47,7 +47,7 @@ const WorkflowTemplatePopup = (props) => { const [requestSent, setRequestSent] = React.useState(false) - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); let navigate = useNavigate(); useEffect(() => { if (modalOpen !== true) { diff --git a/frontend/src/components/WorkflowValidationTimeline.jsx b/frontend/src/components/WorkflowValidationTimeline.jsx index 04eb2314..a0dc2ca0 100644 --- a/frontend/src/components/WorkflowValidationTimeline.jsx +++ b/frontend/src/components/WorkflowValidationTimeline.jsx @@ -23,6 +23,7 @@ import { grey, } from "../views/AngularWorkflow.jsx" +import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx" import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import theme from "../theme.jsx"; const itemHeight = 24 @@ -63,7 +64,7 @@ export const getParentNodes = (workflow, action) => { currentnode = workflow.triggers.find((element) => element.id === allkeys[parentkey]) if (currentnode === undefined) { - console.log("Could not find parent node for: ", allkeys[parentkey]) + //console.log("Could not find parent node for: ", allkeys[parentkey]) continue } } @@ -128,11 +129,13 @@ export const getParentNodes = (workflow, action) => { } const WorkflowValidationTimeline = (props) => { - const { workflow, originalWorkflow, apps, getParents, execution} = props + const { globalUrl, userdata, workflow, originalWorkflow, apps, getParents, execution, showHoverColor, } = props + + const [hovering, setHovering] = useState(false) + const [decidedColor, setDecidedColor] = useState(grey) + const [isClicked, setIsClicked] = useState(false) const showMiddle = false - - if (workflow === undefined || workflow === null) { return null } @@ -146,13 +149,11 @@ const WorkflowValidationTimeline = (props) => { } if (workflow.triggers === undefined || workflow.triggers === null) { - workflow.triggers = [] - + workflow.triggers = [] } if (workflow.branches === undefined || workflow.branches === null) { - workflow.branches = [] - + workflow.branches = [] } var results = [] @@ -260,6 +261,12 @@ const WorkflowValidationTimeline = (props) => { relevantactions.push(...newactions) } + console.log("Relevant actions (return null if 0-1): ", relevantactions) + + if (relevantactions.length <= 1) { + return null + } + // Sort according to how many parents a node has. MAY be wrong~ relevantactions.sort((a, b) => { if (a.order === undefined) { @@ -279,15 +286,81 @@ const WorkflowValidationTimeline = (props) => { var skipped = false var previousTools = false + var scheduleNotStarted = false + + if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.validation_ran === false) { + console.log("Validation didn't run. Why?") + return null + } + + if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null && workflow.validation.errors.length > 0) { + var newErrors = [] + for (var key in workflow.validation.errors) { + const error = workflow.validation.errors[key] + if (error.type === "SCHEDULE") { + scheduleNotStarted = true + continue + } + + newErrors.push(error) + } + + workflow.validation.errors = newErrors + } // Use this variable to control visualization //const showMiddle = false // border: workflow.validation.valid ? `2px solid ${green}` : "1px solid rgba(255,255,255,0.4)", var middleError = "" var startBranchColor = "" + var middleBranchColor = "" + + + const showHoverForClick = showHoverColor === true ? true : false return ( -
+
{ + if (isClicked === false) { + setHovering(true) + } + }} + onMouseLeave={() => { + if (isClicked === false) { + setHovering(false) + } + }} + onClick={() => { + if (showHoverForClick === true) { + setIsClicked(true) + } + }} + > + + {isClicked === false ? null : + + } +
+ + {scheduleNotStarted === true ? + null + : null} + {relevantactions.map((action, index) => { action.result = {} if (results !== undefined) { @@ -309,8 +382,10 @@ const WorkflowValidationTimeline = (props) => { const validate = validateJson(action.result.result) if (validate.valid) { if (validate.result.success === true) { + nodecolor = green branchcolor = green } else { + nodecolor = grey branchcolor = grey } } @@ -319,9 +394,12 @@ const WorkflowValidationTimeline = (props) => { } else if (action.status === "SKIPPED") { branchcolor = grey } else { + // FIXME: How do we handle this? if (action.status === undefined) { - branchcolor = green + nodecolor = grey + branchcolor = grey } else { + nodecolor = red branchcolor = red } } @@ -389,29 +467,64 @@ const WorkflowValidationTimeline = (props) => { } } + var appgroup = [] + if (action.app_name === "shuffle-subflow") { + if (action.status === "SUCCESS") { + nodecolor = green + branchcolor = green + } + + if (workflow.validation.subflow_apps !== undefined && workflow.validation.subflow_apps !== null && workflow.validation.subflow_apps.length > 0) { + nodecolor = red + branchcolor = red + + for (var subflowkey in workflow.validation.subflow_apps) { + const subflowApp = workflow.validation.subflow_apps[subflowkey] + founderror += "- " + subflowApp.error+"\n" + + if (subflowApp.error === action.id) { + appgroup.push(subflowApp) + } + } + } + } + if (!showMiddle && relevantactions.length > 2 && index > 0 && index === relevantactions.length - 2) { if (founderror.length > 0) { middleError += founderror+"\n" + + middleBranchColor = branchcolor } if (index === relevantactions.length-2 && relevantactions.length > 2) { const selectedIcon = middleError.length > 0 ? - + + {middleError} + + }> : null - return ( - selectedIcon - ) + return selectedIcon } else { return null } } + // Returns for anything non-middle + if (relevantactions.length > 2 && index >= 1 && index < relevantactions.length - 2) { + if (founderror.length > 0) { + middleError += founderror+"\n" + } + + return null + } + if (skipped && !lastitem) { nodecolor = grey branchcolor = grey @@ -423,28 +536,14 @@ const WorkflowValidationTimeline = (props) => { branchcolor = nodecolor } - var appgroup = [] if (action.trigger_type === "WEBHOOK") { nodecolor = green branchcolor = green - } else if (action.app_name === "shuffle-subflow") { - if (action.status === "SUCCESS") { - nodecolor = green - branchcolor = green - } - - for (var subflowkey in workflow.validation.subflow_apps) { - const subflowApp = workflow.validation.subflow_apps[subflowkey] - if (subflowApp.error === action.id) { - appgroup.push(subflowApp) - } - } - - } + } + var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3 - if (nodecolor === green) { branchcolor = green } else if (nodecolor === yellow) { @@ -455,12 +554,29 @@ const WorkflowValidationTimeline = (props) => { if (index === 0) { startBranchColor = branchcolor + } else if (index !== 0 && index !== relevantactions.length - 1) { + // FIXME: This doesn't work yet + middleBranchColor = branchcolor } - if (lastitem && middleError.length === 0) { - branchcolor = startBranchColor + if (lastitem) { + if (middleError.length === 0) { + branchcolor = startBranchColor + } else { + //branchcolor = middleBranchColor + } + + if (founderror === "") { + nodecolor = green + } } + // FIXME: This could mean the workflow hasn't ran yet + if (workflow.validation.valid === false && (workflow.validation.errors === undefined || workflow.validation.errors === null || workflow.validation.errors.length == 0) && (workflow.validation.subflow_apps === undefined || workflow.validation.subflow_apps === null || workflow.validation.subflow_apps.length == 0)) { + nodecolor = grey + branchcolor = grey + } + const branchTooltip = branchcolor === yellow ? "Check nodes for errors" : "" const appname = action.app_name.replaceAll('_', ' ').slice(0, 16) @@ -488,6 +604,14 @@ const WorkflowValidationTimeline = (props) => { console.log("MISSING IMAGE: ", appname, image, action) } + if (decidedColor === grey && nodecolor === green) { + setDecidedColor(red) + } + + if (decidedColor !== red && nodecolor === red) { + setDecidedColor(red) + } + return (
{lastitem ? @@ -528,7 +652,7 @@ const WorkflowValidationTimeline = (props) => { : - {founderror.length > 0 ? founderror : `App: ${appname}`} + {founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`} } placement="top"> diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index 6f2455b3..101d8c44 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -467,13 +467,66 @@ const data = [ "font-size": "0px", }, }, - { - selector: "node:selected", - css: { - "border-color": "#f86a3e", - "border-width": "7px", - }, - }, + { + selector: "node:selected", + css: { + "border-color": "#f86a3e", + "border-width": "7px", + }, + }, + { + selector: `node[buttonType="condition-drag"]`, + css: { + "width": "5px", + "height": "5px", + "background-color": "#f85a3e", + }, + }, + { + selector: `node[name="switch"]`, + css: { + label: function(element) { + // Load from the actual element + var nodeheight = 400 + var conditions = [{ + "name": "Condition 1", + "check": "X equals Y", + }, + { + "name": "Condition 2", + "check": "X2 equals Y2", + }, + { + "name": "Condition 3", + "check": "X3 equals Y3", + }] + + conditions.push({ + "name": "Else", + "check": "If all else fails", + }) + + const newlines = nodeheight / conditions.length + console.log("Newlines: ", newlines) + + const label = conditions.map((condition) => { + return `${condition.name}\n\n\n` + }).join("\n") + + return label + }, + color: "white", + "border-color": "#f85a3e", + "background-color": "#1f1f1f", + "font-size": "19px", + "text-margin-x": "-110px", + "text-wrap": "wrap", + shape: "roundrectangle", + width: "100", + height: "300", + + }, + }, ]; //{ diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 8acec204..ed520498 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -6274,7 +6274,7 @@ If you're interested, please let me know a time that works for you, or set up a style={{ minWidth: 50, maxWidth: 50 }} /> ) : ( Not running
) : ( - environment.running_ip.split(":")[0] + environment.running_ip ) ) : ( "N/A" diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index d95c0d9b..6a236161 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -7,10 +7,10 @@ import { useInterval } from "react-powerhooks"; import { makeStyles, } from "@mui/styles"; import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx" -import { v4 as uuidv4 } from "uuid"; +import { v4 as uuidv4, v5 as uuidv5, validate as isUUID, } from "uuid"; import { useNavigate, Link, useParams } from "react-router-dom"; import { useBeforeunload } from "react-beforeunload" -import ReactJson from "react-json-view"; +import ReactJson from "react-json-view-ssr"; import { NestedMenuItem } from 'mui-nested-menu'; import Markdown from "react-markdown"; //import { useAlert @@ -685,7 +685,6 @@ const releaseToConnectLabel = "Release to Connect" } if (loadedApps.includes(appId)) { - console.log("App already loaded: ", appId) return } @@ -703,7 +702,6 @@ const releaseToConnectLabel = "Release to Connect" return response.json() }) .then((responseJson) => { - console.log("Loaded app config: ", responseJson) if (responseJson.success === true && responseJson.app !== undefined && responseJson.app !== null && responseJson.app.length > 0) { // Base64 decode into json @@ -854,7 +852,7 @@ const releaseToConnectLabel = "Release to Connect" } useEffect(() => { - if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id) { + if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 && workflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { setOriginalWorkflow(workflow) } @@ -911,7 +909,7 @@ const releaseToConnectLabel = "Release to Connect" return false }, - preview: false, + preview: true, toggleOffOnLeave: true, loopAllowed: function (node) { return false; @@ -1004,13 +1002,19 @@ const releaseToConnectLabel = "Release to Connect" }, [authenticationModalOpen]) const listOrgCache = (orgId) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } + fetch(`${globalUrl}/api/v1/orgs/${orgId}/list_cache`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", + method: "GET", + headers: headers, + credentials: "include", }) .then((response) => { if (response.status !== 200) { @@ -1873,28 +1877,47 @@ const releaseToConnectLabel = "Release to Connect" continue; } - var type = cyelements[cyelementsKey].data()["type"]; + var type = cyelements[cyelementsKey].data()["type"] if (type === undefined) { - if ( - cyelements[cyelementsKey].data().source === undefined || - cyelements[cyelementsKey].data().target === undefined - ) { - continue; + if (cyelements[cyelementsKey].data().source === undefined || cyelements[cyelementsKey].data().target === undefined) { + continue } + // Get the parent item + var source_attachment = "" + const branchSource = cy.getElementById(cyelements[cyelementsKey].data().source) + if (branchSource === undefined || branchSource === null) { + } else { + const branchSourceData = branchSource.data() + if (branchSourceData !== undefined && branchSourceData !== null && branchSourceData.attachedTo !== undefined) { + source_attachment = branchSourceData.attachedTo + + // Check if it's the 'else' or not based on uuidv5 + const else_attachment = uuidv5(source_attachment, uuidv5.URL) + if (else_attachment === branchSourceData.id) { + source_attachment = source_attachment+"-else" + } + + console.log("Source parent: ", source_attachment) + } + } + var parsedElement = { id: cyelements[cyelementsKey].data().id, source_id: cyelements[cyelementsKey].data().source, destination_id: cyelements[cyelementsKey].data().target, conditions: cyelements[cyelementsKey].data().conditions, decorator: cyelements[cyelementsKey].data().decorator, - }; + + source_parent: source_attachment, + } if (parsedElement.decorator) { - newVBranches.push(parsedElement); + newVBranches.push(parsedElement) } else { - newBranches.push(parsedElement); + newBranches.push(parsedElement) } + } else { if (type === "ACTION") { const cyelement = cyelements[cyelementsKey].data(); @@ -2086,7 +2109,6 @@ const releaseToConnectLabel = "Release to Connect" credentials: "include", }) .then((response) => { - setSavingState(0); if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); } else { @@ -2106,11 +2128,13 @@ const releaseToConnectLabel = "Release to Connect" if (executionArgument !== undefined && startNode !== undefined) { //console.log("Running execution AFTER saving"); + setSavingState(0); executeWorkflow(executionArgument, startNode, true); return; } if (!responseJson.success) { + setSavingState(0); console.log(responseJson); if (responseJson.reason !== undefined && responseJson.reason !== null) { toast("Failed to save: " + responseJson.reason); @@ -2118,6 +2142,7 @@ const releaseToConnectLabel = "Release to Connect" toast("Failed to save. Please contact your support@shuffler.io or your local admin if this is unexpected.") } } else { + setSavingState(1); sendStreamRequest({ "item": "workflow", @@ -2156,7 +2181,6 @@ const releaseToConnectLabel = "Release to Connect" setWorkflow(workflow); } - setSavingState(1); setTimeout(() => { setSavingState(0); }, 1500); @@ -2170,7 +2194,7 @@ const releaseToConnectLabel = "Release to Connect" toast.warn("Failed to save the workflow. Is the network down?") }); - if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id) { + if (originalWorkflow.id === undefined || originalWorkflow.id === null || originalWorkflow.id.length === 0 || useworkflow.id === originalWorkflow.id && workflow.parentorg_workflow === "") { setOriginalWorkflow(useworkflow) } @@ -2370,13 +2394,20 @@ const releaseToConnectLabel = "Release to Connect" // // - const getAuthGroups = () => { + const getAuthGroups = (orgId) => { + setAuthGroups([]) + var headers = { + "content-type": "application/json", + "accept": "application/json", + } + + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } + fetch(globalUrl + "/api/v1/authentication/groups", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -2399,13 +2430,19 @@ const releaseToConnectLabel = "Release to Connect" }) } - const getAppAuthentication = (reset, updateAction, closeMenu) => { + const getAppAuthentication = (reset, updateAction, closeMenu, orgId) => { + var headers = { + "content-type": "application/json", + "accept": "application/json", + } + + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } + fetch(globalUrl + "/api/v1/apps/authentication", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -2418,7 +2455,7 @@ const releaseToConnectLabel = "Release to Connect" .then((responseJson) => { var shouldClose = false if (responseJson.success) { - getAuthGroups() + getAuthGroups(orgId) var newauth = []; for (let authkey in responseJson.data) { @@ -2685,13 +2722,19 @@ const releaseToConnectLabel = "Release to Connect" }) } - const getFiles = () => { + const getFiles = (orgId) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } + fetch(globalUrl + "/api/v1/files", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -3317,12 +3360,18 @@ const releaseToConnectLabel = "Release to Connect" }; const getChildWorkflows = (parentWorkflowId) => { + //toast("Loading child workflows 1 (should be 2)") + + /* if (originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0) { return } + */ const orgId = originalWorkflow.org_id === undefined || originalWorkflow.org_id === null || originalWorkflow.org_id === "" ? "" : originalWorkflow.org_id + //toast("Loading child workflows 2: " + orgId) + fetch(`${globalUrl}/api/v1/workflows/${parentWorkflowId}/child_workflows`, { method: "GET", headers: { @@ -3376,12 +3425,13 @@ const releaseToConnectLabel = "Release to Connect" setTimeout(() => { window.location.pathname = "/workflows"; }, 2000); -} else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") { - toast(`Injecting session token and reloading workflow..`) - setTimeout(() => { - setCookie("session_token", sessionToken, { path: "/" }); - window.location.href = "https://shuffler.io/workflows/3abdfb21-b40f-4e50-b855-ac0d62f83cbe"; - }, 2000); + + } else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") { + toast(`Injecting session token and reloading workflow..`) + setTimeout(() => { + setCookie("session_token", sessionToken, { path: "/" }); + window.location.href = "https://shuffler.io/workflows/3abdfb21-b40f-4e50-b855-ac0d62f83cbe"; + }, 2000) } } } @@ -3392,11 +3442,6 @@ const releaseToConnectLabel = "Release to Connect" }) .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}`) @@ -3406,7 +3451,6 @@ const releaseToConnectLabel = "Release to Connect" setDistributedFromParent(responseJson.parentorg_workflow) } - if (responseJson.childorg_workflow_ids !== undefined && responseJson.childorg_workflow_ids !== null && responseJson.childorg_workflow_ids.length > 0) { getChildWorkflows(responseJson.id) } @@ -3435,6 +3479,13 @@ const releaseToConnectLabel = "Release to Connect" if (responseJson.org_id !== undefined && responseJson.org_id !== null) { listOrgCache(responseJson.org_id) } + + if (responseJson.sharing !== undefined && responseJson.sharing !== null && (responseJson.sharing === "form" || responseJson.sharing === "forms")) { + if (responseJson.actions === undefined || responseJson.actions === null || responseJson.actions.length === 0) { + navigate("/forms/" + responseJson.id) + toast("Redirecting to Form from Workflow") + } + } // Wait for this to finish fetchRecommendations(responseJson) @@ -3636,7 +3687,10 @@ const releaseToConnectLabel = "Release to Connect" cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e)); } else { - setOriginalWorkflow(responseJson) + if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0 && responseJson.parentorg_workflow === "") { + setOriginalWorkflow(responseJson) + } + setWorkflow(responseJson); setWorkflowDone(true); @@ -5430,7 +5484,7 @@ const releaseToConnectLabel = "Release to Connect" } setRightSideBarOpen(true); - setLastSaved(false); + //setLastSaved(false); setScrollConfig({ top: 0, left: 0, @@ -5771,7 +5825,6 @@ const releaseToConnectLabel = "Release to Connect" setLastSaved(false); const edge = event.target.data(); - console.log("edge added: ", edge) if (edge.source === undefined && edge.target === undefined) { console.log("Edge added without source or target") @@ -5787,9 +5840,15 @@ const releaseToConnectLabel = "Release to Connect" const sourcenode = cy.getElementById(edge.source) const destinationnode = cy.getElementById(edge.target) + if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) } else { + if (sourcenode.data("name") === "switch") { + event.target.remove() + return + } + console.log("Edge added: Is it a trigger? If so, check if it already has a branch and remove it: ", sourcenode.data()) if (sourcenode.data("type") === "TRIGGER") { if (sourcenode.data("app_name") !== "Shuffle Workflow" && sourcenode.data("app_name") !== "User Input") { @@ -5827,7 +5886,7 @@ const releaseToConnectLabel = "Release to Connect" var targetnode = workflow.triggers.findIndex( (data) => data.id === edge.target - ); + ) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { console.log("User Input or Shuffle Workflow") @@ -6526,17 +6585,24 @@ const releaseToConnectLabel = "Release to Connect" document.addEventListener("paste", handlePaste); }; - const getEnvironments = () => { + const getEnvironments = (orgId) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } + fetch(globalUrl + "/api/v1/getenvironments", { method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { if (response.status !== 200) { + console.log("Status not 200 for envs :O!"); if (isCloud) { setEnvironments([{ Name: "Cloud", Type: "cloud" }]); @@ -6642,9 +6708,13 @@ const releaseToConnectLabel = "Release to Connect" } } - return; + return } + if (nodedata.name === "switch") { + return + } + // console.log("nodedata", nodedata); // console.log("nodedata.app_name: ", nodedata.app_name); if (nodedata.app_name !== undefined) { @@ -6654,10 +6724,12 @@ const releaseToConnectLabel = "Release to Connect" for (var nodekey in allNodes) { const currentNode = allNodes[nodekey]; // console.log("Current node: ", currentNode); - if ( - currentNode.data.isButton && - currentNode.data.attachedTo !== nodedata.id - ) { + if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) { + + if (currentNode.data.buttonType === "condition-drag") { + continue + } + cy.getElementById(currentNode.data.id).remove(); } @@ -6760,6 +6832,146 @@ const releaseToConnectLabel = "Release to Connect" // Maybe it shouldn't be onclick? } + const addConditionDraggers = (event, allElements, branches) => { + const nodedata = event.target.data() + const position = event.target.position() + + var conditions = [] + const foundParam = nodedata.parameters.find((param) => param.name.toLowerCase() === "conditions") + + try { + conditions = JSON.parse(foundParam.value) + } catch (e) { + //toast("Failed parsing conditions: ", e) + } + + // Test conditions + if (conditions === undefined || conditions === null || typeof conditions !== "object") { + return + } + + // Look for if it has the "Else" condition or not + const elseindex = conditions.findIndex((condition) => condition.name.toLowerCase() === "else") + const parentId = nodedata.id + + // Force following of Else at the least + const newId = uuidv5(parentId, uuidv5.URL) + if (elseindex === -1) { + conditions.push({ + name: "Else", + check: "Else", + id: newId, + parent_source: parentId, + }) + } else { + conditions[elseindex].id = newId + } + + // 4 conditions (with else) = 300px -> 75px each + const parentHeight = (conditions.length*75)*0.75 + + + var startheight = -parentHeight/2 + var newnodes = [] + for (let conditionkey in conditions) { + var circleId = conditions[conditionkey].id === undefined ? (newNodeId = uuidv4()) : conditions[conditionkey].id + + // Check if circleId is a valid uuid or not + if (circleId === undefined || circleId === null) { + circleId = uuidv4() + } + + if (!isUUID(circleId)) { + if (conditions[circleId].name !== undefined && conditions[circleId].name !== null) { + circleId = uuidv5(conditions[circleId].name, uuidv5.URL) + } else { + circleId = uuidv4() + conditions[conditionkey].name = circleId + conditions[conditionkey].id = circleId + } + } + + // Check if circleId already exists as a node + if (cy !== undefined && cy !== null) { + const existingNode = cy.getElementById(circleId) + if (existingNode !== undefined && existingNode !== null && existingNode.length > 0) { + continue + } + } + + // 1. Create "small" nodes at each point along the section based on the amount of conditions + // 2. Make these conditions have edgehandles + // 3. Make these conditions have a "drag" handle + const px = position.x + 65 + const py = position.y + startheight + + console.log("Y height: ", startheight) + + const node = { + group: "nodes", + data: { + name: conditions[conditionkey].name, + id: circleId, + buttonType: "condition-drag", + attachedTo: nodedata.id, + is_valid: true, + }, + position: { + x: px, + y: py, + }, + locked: true, + } + + newnodes.push(node) + + // Check if ANY of the incoming branches has the id as source + if (branches !== undefined && branches !== null && branches.length > 0) { + for (let branchkey in branches) { + const branch = branches[branchkey] + if (branch.source_id !== circleId) { + continue + } + + const branchid = uuidv4() + newnodes.push({ + group: "edges", + data: { + id: branchid, + _id: branchid, + + source: circleId, + target: branch.destination_id, + label: branch.label, + conditions: branch.conditions, + hasErrors: branch.has_errors, + decorator: false, + parent_source: parentId, + } + }) + } + } + + startheight = startheight + parentHeight/(conditions.length-1) + } + + if (cy !== undefined && cy !== null) { + cy.add(newnodes) + } else { + var newelements = elements + if (allElements !== undefined) { + newelements = allElements + } + + for (let nodekey in newnodes) { + newelements.push(newnodes[nodekey]) + } + + console.log("ELEMENTS: ", newelements) + setElements(newelements) + } + } + const addCopyButton = (event) => { var parentNode = cy.$("#" + event.target.data("id")); if (parentNode.data("isButton") || parentNode.data("buttonId")) return; @@ -7188,10 +7400,11 @@ const releaseToConnectLabel = "Release to Connect" } } - return; + return } + //var parentNode = cy.$("#" + event.target.data("id")); //if (parentNode.data("isButton") || parentNode.data("buttonId")) return; @@ -7244,7 +7457,12 @@ const releaseToConnectLabel = "Release to Connect" // console.log("CURRENT NODE: ", currentNode) if ((currentNode.data.buttonType === "ACTIONSUGGESTION" || currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== nodedata.id) { - cy.getElementById(currentNode.data.id).remove(); + + if (currentNode.data.buttonType === "condition-drag") { + continue + } + + cy.getElementById(currentNode.data.id).remove() } /*if ( @@ -7259,8 +7477,13 @@ const releaseToConnectLabel = "Release to Connect" } } + if (nodedata.name === "switch") { + addConditionDraggers(event) + return + } + if (!found) { - addDeleteButton(event); + addDeleteButton(event) if (nodedata.type === "TRIGGER") { if (nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT") { @@ -7268,25 +7491,29 @@ const releaseToConnectLabel = "Release to Connect" } else { // Check how many executions from the source addRunCountButton(event); - } - } else { - addCopyButton(event); - addStartnodeButton(event); - } + } + } else { - // autocomplete - // right click - // suggestions - addActionSuggestions(nodedata, event); - - if (workflow.actions.length < 4) { - addSuggestionButtons(nodedata, event); - } else { - //console.log("Too many actions to suggest (for now)") - } + addCopyButton(event); + addStartnodeButton(event); } + + // autocomplete + // right click + // suggestions + addActionSuggestions(nodedata, event); + + if (workflow.actions.length < 4) { + addSuggestionButtons(nodedata, event); + } else { + //console.log("Too many actions to suggest (for now)") + } + } } + if (nodedata.name === "switch") { + return + } var parsedStyle = { "border-width": "7px", @@ -7625,8 +7852,8 @@ const releaseToConnectLabel = "Release to Connect" node.data.example = example; - return node; - }); + return node + }) const decoratorNodes = inputworkflow.actions.map((action) => { if (!action.isStartNode) { @@ -7756,7 +7983,7 @@ const releaseToConnectLabel = "Release to Connect" hasErrors: branch.has_errors, decorator: false, parent_controlled: parentcontrolled, - }; + } // This is an attempt at prettier edges. The numbers are weird to work with. // Bezier curves @@ -7846,6 +8073,28 @@ const releaseToConnectLabel = "Release to Connect" } else { setElements(insertedNodes); } + + const additionalNodes = inputworkflow.actions.map((action) => { + // Looking for: el.data("name") != "switch" + if (action.name !== "switch") { + return null + } + + addConditionDraggers({ + target: { + // Run data() function + data: function() { + return action + }, + position: function() { + return action.position + } + } + }, + insertedNodes, + inputworkflow.branches, + ) + }) } const removeNode = (nodeId) => { @@ -8146,6 +8395,7 @@ const releaseToConnectLabel = "Release to Connect" handleNodes: (el) => { if (el.isNode() && el.data("buttonType") != "ACTIONSUGGESTION" && + el.data("name") != "switch" && !el.data("isButton") && !el.data("isDescriptor") && !el.data("isSuggestion") && @@ -8663,7 +8913,7 @@ const releaseToConnectLabel = "Release to Connect" variant="outlined" onClick={() => { setVariablesModalOpen(true); - setLastSaved(false); + setLastSaved(false) setVariableInfo({ "name": "", @@ -9441,7 +9691,6 @@ const releaseToConnectLabel = "Release to Connect" setHover(true) if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) { - console.log("HOVERING: ", app.id) loadAppConfig(app.id, false) } @@ -11488,12 +11737,12 @@ const releaseToConnectLabel = "Release to Connect" }; setConditionsModalOpen(false); - if (selectedEdge.conditions === undefined) { + if (selectedEdge.conditions === undefined || selectedEdge.conditions === null) { selectedEdge.conditions = [data]; } else { const curedgeindex = selectedEdge.conditions.findIndex( (data) => data.source.id === sourceValue.id - ); + ) if (curedgeindex < 0) { selectedEdge.conditions.push(data); } else { @@ -12565,7 +12814,6 @@ const releaseToConnectLabel = "Release to Connect" // Sets the startnode if (e.target.value.id !== workflow.id && e.target.value.id.length > 0 ) { - console.log("WORKFLOW: ", e.target.value); const startnode = e?.target?.value?.actions?.find((action) => action.id === e.target.value.start); @@ -13424,7 +13672,7 @@ const releaseToConnectLabel = "Release to Connect" } - return "TMP"; + return "Default"; } const newname = ( @@ -16062,9 +16310,17 @@ const releaseToConnectLabel = "Release to Connect" //globalUrl = responseJson.region_url } - setTimeout(() => { - window.location.reload(); - }, 2000); + if (responseJson["reason"] === "SSO_REDIRECT") { + setTimeout(() => { + toast.info("Redirecting to SSO login page as SSO is required for this organization.") + window.location.href = responseJson["url"] + return + }, 2000) + } else { + setTimeout(() => { + window.location.reload(); + }, 2000); + } toast("Successfully changed active organisation - refreshing!"); } else { @@ -16104,70 +16360,95 @@ const releaseToConnectLabel = "Release to Connect" style={{maxHeight: 50, maxWidth: 250, }} labelId="suborg-changer" value={workflow.org_id} + disabled={savingState !== 0} onChange={(e) => { if (workflow.org_id === e.target.value) { console.log("Same org selected. No change.") return + } else { + //if (savingState === 0) { + // saveWorkflow(workflow, undefined, undefined, undefined) + //} } - if (e.target.value === originalWorkflow.org_id) { - console.log("Original org selected. No change.") - - updateCurrentWorkflow(originalWorkflow) - return + // Unselect in cy + if (cy !== undefined && cy !== null) { + cy.nodes().unselect() + cy.edges().unselect() } - // Should look through childorg workflow - if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { - console.log("In childorg no exist. Suborgworkflows: ", suborgWorkflows) + ReactDOM.unstable_batchedUpdates(() => { + getEnvironments(e.target.value) + getAppAuthentication(undefined, undefined, undefined, e.target.value) + getFiles(e.target.value) + listOrgCache(e.target.value) - if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { - var found = false - for (var suborgkey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgkey] - if (suborgWorkflow.org_id === e.target.value) { - found = true - updateCurrentWorkflow(suborgWorkflow) - break + if (e.target.value === originalWorkflow.org_id) { + console.log("Original org selected. No change.") + + updateCurrentWorkflow(originalWorkflow) + return + } else { + // Load environments, auth, auth groups + //toast("Loading correct info for suborg") + } + + + // Should look through childorg workflow + console.log("Original: ", originalWorkflow) + if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { + console.log("In childorg doesn't exist. Suborgworkflows: ", suborgWorkflows) + + if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { + var found = false + for (var suborgkey in suborgWorkflows) { + const suborgWorkflow = suborgWorkflows[suborgkey] + if (suborgWorkflow.org_id === e.target.value) { + found = true + updateCurrentWorkflow(suborgWorkflow) + break + } } - } - if (!found) { - console.log("No workflow found out of suborg workflows.") - - saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + if (!found) { + toast("(3) Creating new workflow for this org. Please wait a second while we duplicate.") + //console.log("No workflow found out of suborg workflows.") + + //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } else { + console.log("Suborgworkflows: ", suborgWorkflows) + toast("(1) Loading NEW workflow for this org (?). Please wait a second.") + saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) } } else { - //toast("(1) Creating new workflow for this org. Please wait a second while we duplicate.") - saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("In childorg EXIST!") + console.log("In childorg EXIST!") - var workflowFound = false - for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { - const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] - for (var suborgWorkflowKey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] - if (suborgWorkflow.org_id === e.target.value) { - workflowFound = true + var workflowFound = false + for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { + const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] + for (var suborgWorkflowKey in suborgWorkflows) { + const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] + if (suborgWorkflow.org_id === e.target.value) { + workflowFound = true - updateCurrentWorkflow(suborgWorkflow) + updateCurrentWorkflow(suborgWorkflow) + break + } + } + + if (workflowFound) { break } } - if (workflowFound) { - break + if (!workflowFound) { + console.log("No workflow found.") + toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") + //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) } } - - if (!workflowFound) { - console.log("No workflow found.") - //toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") - saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } + }) }} label="Suborg Distribution" fullWidth @@ -16242,7 +16523,6 @@ const releaseToConnectLabel = "Release to Connect" } - {authGroups !== undefined && authGroups !== null && authGroups.length > 0 ? { - setLastSaved(false) + //setLastSaved(false) setSelectedAction({}); setSelectedApp({}) setWorkflow(inputworkflow) + if (inputworkflow !== undefined && inputworkflow !== null && inputworkflow.id !== undefined && inputworkflow.id !== null) { + getRevisionHistory(inputworkflow.id) + getWorkflowExecution(inputworkflow.id) + } + // Update props match key if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { setDistributedFromParent(inputworkflow.parentorg_workflow) @@ -17283,7 +17567,6 @@ const releaseToConnectLabel = "Release to Connect" variant={"outlined"} onClick={() => { setShowWorkflowRevisions(true) - //setOriginalWorkflow(workflow) }} > @@ -17662,7 +17945,8 @@ const releaseToConnectLabel = "Release to Connect"
*/} - {userdata.avatar !== undefined && (userdata.avatar === creatorProfile.github_avatar || allowList.includes(userdata.public_username)) ? + + {userdata.support === true || (userdata.avatar !== undefined && (userdata.avatar === creatorProfile.github_avatar || allowList.includes(userdata.public_username))) ? + + + + + { }; - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); useEffect(() => { if (window.location.pathname.includes("apps/edit")) { @@ -897,8 +897,31 @@ const AppCreator = (defaultprops) => { }; if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) { + console.log("LABEL: ", methodvalue["x-label"]) + + var correctlabel = "" + const labels = methodvalue["x-label"].split(",") + for (let labelkey in labels) { + var label = labels[labelkey].trim() + if (label.toLowerCase() === "no label") { + continue + } + + // Remove quotes and escapes + label = label.replace(/['"]+/g, '') + label = label.replace(/\\/g, '') + + //label = label.replace("_", " ", -1) + //label = label.charAt(0).toUpperCase() + label.slice(1) + + correctlabel = label + break + } + + console.log("LABEL: ", correctlabel) // FIX: Map labels only if they're actually in the category list - newaction.action_label = methodvalue["x-label"] + //newaction.action_label = methodvalue["x-label"] + newaction.action_label = correctlabel } if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) { @@ -3124,15 +3147,14 @@ const AppCreator = (defaultprops) => { Scopes for Oauth2 { required style={{ marginTop: 0, backgroundColor: inputColor }} fullWidth={true} - placeholder="Field Name (key, NOT your actual API-key)" + placeholder="The Key to use as the header/query - NOT your actual API-key" type="name" id="standard-required" margin="normal" @@ -4441,7 +4463,7 @@ const AppCreator = (defaultprops) => { setUpdate(Math.random()) } }} - value={data.action_label} + value={data?.action_label?.replace(" ", "_").toLowerCase()} style={{ border: data.action_label === undefined || data.action_label === "No Label" ? "" : `2px solid ${bgColor}`, borderRadius: theme.shape.borderRadius, @@ -4463,7 +4485,7 @@ const AppCreator = (defaultprops) => { return ( diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 1c1ca152..1b14fe20 100755 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -3035,7 +3035,7 @@ const Apps = (props) => { height: "50px", }} variant="contained" - disabled={openApi.length === 0 || appValidation.length > 0} + disabled={openApi.length === 0 || appValidation.length > 0 || validation} color="primary" onClick={() => { setOpenApiError(""); diff --git a/frontend/src/views/DashboardViews.jsx b/frontend/src/views/DashboardViews.jsx index 94b113f3..57b761a3 100644 --- a/frontend/src/views/DashboardViews.jsx +++ b/frontend/src/views/DashboardViews.jsx @@ -11,6 +11,7 @@ import { useNavigate, Link, useParams } from "react-router-dom"; //import { useAlert import { ToastContainer, toast } from "react-toastify" import Draggable from "react-draggable"; +import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx'; import { Autocomplete, @@ -18,6 +19,8 @@ import { TextField, IconButton, Button, + Select, + MenuItem, Typography, Grid, Paper, @@ -146,28 +149,8 @@ const inputdata = [ const LineChartWrapper = ({keys, height, width}) => { const [hovered, setHovered] = useState(""); - //console.log("Date: ", new Date("2019-11-14T08:00:00.000Z")) - console.log("Keys: ", keys) var inputdata = keys.data - /* - const inputdata = [{ - "key": "Intel", - "data": [ - { key: new Date('11/22/2019'), data: 3, metadata: {color: "orange", "name": "Intel"}}, - { key: new Date('11/24/2019'), data: 8, metadata: {color: "orange", "name": "Intel"}}, - { key: new Date('11/29/2019'), data: 2, metadata: {color: "orange", "name": "Intel"}}, - ]}, - { - "key": "Popper", - "data": [ - { key: new Date('11/24/2019'), data: 9, }, - { key: new Date('11/29/2019'), data: 3, }, - ] - } - ] - */ - return (
@@ -211,7 +194,6 @@ const LineChartWrapper = ({keys, height, width}) => { offset: '5px, 5px' }} content={(data, color) => { - console.log("DATA: ", data) const name = data.metadata !== undefined && data.metadata.name !== undefined ? data.metadata.name : "No" return ( @@ -350,12 +332,35 @@ const Dashboard = (props) => { const [frameworkData, setFrameworkData] = useState(undefined); const [widgetData, setWidgetData] = useState([]); + const [newWidgetData, setNewWidgetData] = useState([]); + + const [, setUpdate] = useState(0); let navigate = useNavigate(); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + useEffect(() => { + const widgetnames = ["app_executions_cloud"] + for (let widgetkey in widgetnames) { + const widgetName = widgetnames[widgetkey] + + console.log("NAME: ", widgetName) + + const resp = LoadStats(globalUrl, widgetName) + if (resp !== undefined) { + resp.then((data) => { + console.log("Got data in parent: ", data) + if (data === undefined) { + } else { + newWidgetData.push(data) + setNewWidgetData(newWidgetData) + } + }) + } + } + }, []) useEffect(() => { if (selectedUsecaseCategory.length === 0) { @@ -368,6 +373,7 @@ const Dashboard = (props) => { } }, [selectedUsecaseCategory]) + const checkSelectedParams = () => { const urlSearchParams = new URLSearchParams(window.location.search) const params = Object.fromEntries(urlSearchParams.entries()) @@ -409,23 +415,22 @@ const Dashboard = (props) => { }, [usecases]) const getWidget = (dashboard, widget) => { - fetch(`${globalUrl}/api/v1/dashboards/${dashboard}/widgets/${widget}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for framework!"); - } + fetch(`${globalUrl}/api/v1/dashboards/${dashboard}/widgets/${widget}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } - return response.json(); - }) - .then((responseJson) => { - console.log("Resp: ", responseJson) + return response.json(); + }) + .then((responseJson) => { if (responseJson.success === false) { if (responseJson.reason !== undefined) { //toast("Failed loading: " + responseJson.reason) @@ -441,14 +446,12 @@ const Dashboard = (props) => { } const foundWidget = widgetData.findIndex(data => data.title === widget) - console.log("Found: ", foundWidget) if (foundWidget !== undefined && foundWidget !== null && foundWidget >= 0) { widgetData[foundWidget] = tmpdata } else { widgetData.push(tmpdata) } - console.log("Data: ", widgetData) setWidgetData(widgetData) } }) @@ -480,7 +483,7 @@ const Dashboard = (props) => { useEffect(() => { getWidget("main", "Overall") getWidget("main", "Overall2") - }, []); + }, []) const fetchdata = (stats_id) => { fetch(globalUrl + "/api/v1/stats/" + stats_id, { @@ -646,7 +649,6 @@ const Dashboard = (props) => { stats["workflow_executions"].data !== undefined ) { setStatsRan(true); - //console.log("NEW DATA?: ", stats) console.log("SET WORKFLOW: ", stats["workflow_executions"]); //var curday = startDate.getDate() @@ -729,6 +731,100 @@ const Dashboard = (props) => {
) : null; + const WidgetController = (props) => { + const { data, index, availableStats, } = props + const [hovering, setHovering] = useState(false) + + const newname = data.key !== undefined ? data.key.replaceAll("_", " ") : "" + + console.log("KEYDATA: ", data) + + const loadNewStats = (newkey) => { + const resp = LoadStats(globalUrl, newkey) + if (resp !== undefined) { + resp.then((respdata) => { + if (respdata === undefined || respdata === null) { + toast("Failed to laod data. Please try again, or contact support@shuffler.io if this persists.") + } else { + newWidgetData[index] = respdata + setNewWidgetData(newWidgetData) + + setUpdate(Math.random()) + } + }) + } + } + + return ( + + { + setHovering(true) + }} + onMouseLeave={() => { + setHovering(false) + }} + > +
+ + {newname} + + + {data.available_keys === undefined || data.available_keys === null || data.available_keys.length === 0 ? null : + + } +
+ +
+
+ ) + } + const data = (
@@ -739,12 +835,25 @@ const Dashboard = (props) => { : null}
- {widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null : + {/*widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null : + */} + + {newWidgetData === undefined || newWidgetData === null || newWidgetData === [] ? null : + newWidgetData.map((data, index) => { + + return ( + + ) + }) }
); diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 9536dad7..3cbeb077 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react" import { toast } from 'react-toastify'; import Markdown from 'react-markdown' import theme from '../theme.jsx'; -import ReactJson from "react-json-view"; +import ReactJson from "react-json-view-ssr"; import { isMobile } from "react-device-detect"; import { BrowserView, MobileView } from "react-device-detect"; import { useParams, useNavigate, Link } from "react-router-dom"; @@ -152,9 +152,19 @@ export const OuterLink = (props) => { export const Img = (props) => { + var height = "auto" + var width = 750 + if (props.height !== undefined && props.height !== null) { + height = props.height + } + + if (props.width !== undefined && props.width !== null) { + width = props.width + } + return( {props.alt} diff --git a/frontend/src/views/RunWorkflow.jsx b/frontend/src/views/RunWorkflow.jsx index 8dbc019a..3b2c2e0c 100644 --- a/frontend/src/views/RunWorkflow.jsx +++ b/frontend/src/views/RunWorkflow.jsx @@ -7,15 +7,17 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx"; import { useNavigate, Link, useParams } from "react-router-dom"; import { validateJson, GetIconInfo } from "./Workflows.jsx"; import EditWorkflow from "../components/EditWorkflow.jsx" -import { ToastContainer, toast } from "react-toastify" +import { toast } from "react-toastify" import { makeStyles } from '@mui/material/styles'; import { useInterval } from "react-powerhooks"; import { isMobile } from "react-device-detect"; import Markdown from "react-markdown"; import theme from '../theme.jsx'; +import rehypeRaw from "rehype-raw"; import { - Tooltip, + Tooltip, + Select, IconButton, CircularProgress, TextField, @@ -28,6 +30,7 @@ import { Dialog, DialogTitle, DialogContent, + MenuItem, } from '@mui/material'; import { @@ -45,7 +48,7 @@ const bodyDivStyle = { width: isMobile? "100%":"500px", position: "relative", - marginTop: 25, + paddingBottom: 250, } @@ -71,7 +74,7 @@ const RunWorkflow = (defaultprops) => { const boxStyle = { color: "white", - padding: 50, + padding: "25px 50px 50px 50px", backgroundColor: theme.palette.surfaceColor, marginBottom: 150, borderRadius: 25, @@ -102,6 +105,17 @@ const RunWorkflow = (defaultprops) => { const [executionInfo, setExecutionInfo] = useState(""); const handleValidateForm = (executionArgument) => { + // Check if every field exists + if (executionArgument === undefined || executionArgument === null) { + return true + } + + for (var key in executionArgument) { + if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") { + return false + } + } + return true } @@ -124,7 +138,10 @@ const RunWorkflow = (defaultprops) => { return response.json(); }) .then((responseJson) => { - toast.success("Saved workflow") + if (responseJson.success === false) { + toast.error("Failed saving workflow. Please try again.") + } + //toast.success("Saved workflow") }) .catch((error) => { toast.error("Save workflow error: " + error) @@ -197,178 +214,25 @@ const RunWorkflow = (defaultprops) => { */} {executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ? - - {executionData.result} - +
+ + + {executionData.result} + +
: null} - {/*executionData.results.map((data, index) => { - if (executionData.results.length !== 1 && (data.status === "SKIPPED")) { - return null; - } - - // FIXME: The latter replace doens't really work if ' is used in a string - var showResult = data.result.trim(); - const validate = validateJson(showResult); - - const curapp = apps.find((a) => a.name === data.action.app_name && a.app_version === data.action.app_version); - const imgsize = 50; - const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? green : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : yellow; - - var imgSrc = curapp === undefined ? "" : curapp.large_image; - if ( - imgSrc.length === 0 && - workflow.actions !== undefined && - workflow.actions !== null - ) { - // Look for the node in the workflow - const action = workflow.actions.find( - (action) => action.id === data.action.id - ); - if (action !== undefined && action !== null) { - imgSrc = action.large_image; - } - } - - var actionimg = - curapp === null ? null : ( - {data.action.app_name} - ); - - if (data.action.app_name === "shuffle-subflow") { - //const parsedImage = triggers[2].large_image; - //actionimg = ( - // {"Shuffle - //); - } else if (data.action.app_name === "User Input") { - //actionimg = ( - // {"Shuffle - //); - } - - if (validate.valid && typeof validate.result === "string") { - validate.result = JSON.parse(validate.result); - } - - if (validate.valid && typeof validate.result === "object") { - if ( - validate.result.result !== undefined && - validate.result.result !== null - ) { - try { - validate.result.result = JSON.parse(validate.result.result); - } catch (e) { - //console.log("ERROR PARSING: ", e) - } - } - } - - - var similarActionsView = null - if (data.similar_actions !== undefined && data.similar_actions !== null) { - var minimumMatch = 85 - var matching_executions = [] - if (data.similar_actions !== undefined && data.similar_actions !== null) { - for (let [k,kval] in Object.entries(data.similar_actions)){ - if (data.similar_actions.hasOwnProperty(k)) { - if (data.similar_actions[k].similarity > minimumMatch) { - matching_executions.push(data.similar_actions[k].execution_id) - } - } - } - } - - if (matching_executions.length !== 0) { - var parsed_url = matching_executions.join(",") - - similarActionsView = - - { - //navigate(`?execution_highlight=${parsed_url}`) - }} - > - - - - } - } - - return ( -
{ - }} - onMouseOut={() => { - }} - > -
- - Status  - - - {data.status} - -
-
- ) - })*/}
) } @@ -388,7 +252,7 @@ const RunWorkflow = (defaultprops) => { var data = { "execution_argument": executionArgument, - "execution_source": "questions", + "execution_source": "form", } if (workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) { @@ -467,6 +331,10 @@ const RunWorkflow = (defaultprops) => { } } + if (response.status === 401 || response.status === 403) { + toast("This Form is not available for you to run. If you this is an error, contact support@shuffler.io with a link to this form") + } + return response.json() }) .then(responseJson => { @@ -509,7 +377,9 @@ const RunWorkflow = (defaultprops) => { } const getWorkflow = (workflow_id, selectedNode) => { - fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + const url = `${globalUrl}/api/v1/workflows/${workflow_id}` + + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -522,6 +392,10 @@ const RunWorkflow = (defaultprops) => { console.log("Status not 200 for workflows :O!"); } + if (response.status === 401 || response.status === 403) { + toast("This Form is not available to you. If you think this is an error, please contact support@shuffler.io with the URL.") + } + return response.json(); }) .then((responseJson) => { @@ -543,16 +417,24 @@ const RunWorkflow = (defaultprops) => { } if (responseJson.input_questions !== undefined && responseJson.input_questions !== null && responseJson.input_questions.length > 0) { + var newexec = {} for (let questionkey in responseJson.input_questions) { const question = responseJson.input_questions[questionkey] - newexec[question.value] = "" + + var multiChoiceOptions = question.value !== undefined && question.value !== null && question.value.length > 0 && question.value.includes(";") ? question.value.split(";") : [] + if (multiChoiceOptions.length > 1) { + newexec[multiChoiceOptions[0]] = "" + } else { + newexec[question.value] = "" + } } setExecutionArgument(newexec) } if (selectedNode !== undefined && selectedNode !== null && selectedNode.length > 0) { + var found = false for (var actionkey in responseJson.actions) { if (responseJson.actions[actionkey].id === selectedNode) { @@ -567,6 +449,7 @@ const RunWorkflow = (defaultprops) => { if (responseJson.triggers[triggerkey].id !== selectedNode) { continue } + setFoundSourcenode(responseJson.triggers[triggerkey]) @@ -864,215 +747,277 @@ const RunWorkflow = (defaultprops) => { const basedata =
- {workflow.input_markdown !== undefined && workflow.input_markdown !== null && workflow.input_markdown.length > 0 ? -
- - {workflow.input_markdown} - -
- : null} - -
{onSubmit(e)}} style={{margin: "50px 0px 15px 0px",}}> - {workflow.input_markdown !== undefined && workflow.input_markdown !== null && workflow.input_markdown.length > 0 ? null : -
- {workflow.name} - - - {organization} + {workflow.id === undefined || workflow.id === null ? +
+ + + Loding Form Details... - +
+ : +
- {disabledButtons && message.length > 0 ? null : - - {message} + {workflow.input_markdown !== undefined && workflow.input_markdown !== null && workflow.input_markdown.length > 0 ? +
+ + {workflow.input_markdown} + +
+ : null} + + {onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}> + {workflow.input_markdown !== undefined && workflow.input_markdown !== null && workflow.input_markdown.length > 0 ? null : +
+ {workflow.name} + + + {organization} - } + - {answer !== undefined && answer !== null ? null : - {workflow.name} - } - - {workflowQuestion.length > 0 ? -
- - {workflowQuestion} + {disabledButtons && message.length > 0 ? null : + + {message} -
- : null} + } -
- } - - {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? -
- {workflow.input_questions.map((question, index) => { - return ( -
- {question.name} - {workflow.name} + } - disabled={disabledButtons} - fullWidth={true} - placeholder="" - id="emailfield" - margin="normal" - variant="outlined" - onBlur={(e) => { - //setExecutionArgument(e.target.value) - executionArgument[question.value] = e.target.value - setUpdate(Math.random()) - }} - /> -
- ) - })} -
- : - answer !== undefined && answer !== null ? null : - - Runtime Argument -
- { - setExecutionArgument(e.target.value) - }} - /> -
-
- } - - {executionRunning ? - - - - {executionData.status !== undefined && executionData.status !== null && executionData.status !== "" ? - - Status: {executionData.status} - + {workflowQuestion.length > 0 ? +
+ + {workflowQuestion} + +
: null} -
- : - ((answer !== undefined && answer !== null) || (foundSourcenode !== undefined && foundSourcenode !== null)) ? - - {disabledButtons && message.length > 0 ? - - {message}. You may close this window. - - : - - {disabledButtons ? "Answered. You may close this window." : ""} - - } +
+ } + + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
+ {workflow.input_questions.map((question, index) => { - {disabledButtons ? null : - - What do you want to do? - - } -
- - -  or  - - + {multiChoiceOptions.length > 1 ? + + : + { + executionArgument[question.value] = e.target.value + setExecutionArgument(executionArgument) + setUpdate(Math.random()) + }} + /> + } +
+ ) + })} +
+ : + answer !== undefined && answer !== null ? null : + + Runtime Argument +
+ { + setExecutionArgument(e.target.value) + }} + />
- : -
- + } + + {executionRunning ? + + + + {executionData.status !== undefined && executionData.status !== null && executionData.status !== "" ? + + Status: {executionData.status} + + : null} + + : + ((answer !== undefined && answer !== null) || (foundSourcenode !== undefined && foundSourcenode !== null)) ? + + + {disabledButtons && message.length > 0 ? + + {message}. You may close this window. + + : + + {disabledButtons ? "Answered. You may close this window." : ""} + + } + + {disabledButtons ? null : + + What do you want to do? + + } +
+ + +  or  + + +
+
+ : +
+ +
+ } + + {/*buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ? + finalize workflow animation { + console.log("Img loaded.") + setTimeout(() => { + console.log("Img closing.") + setButtonClicked("finished") + + }, 1250) + + }} + /> + : null*/} + +
+ {executionInfo}
- } - - {/*buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ? - finalize workflow animation { - console.log("Img loaded.") - setTimeout(() => { - console.log("Img closing.") - setButtonClicked("finished") - - }, 1250) - - }} - /> - : null*/} - -
- {executionInfo} -
- {answer !== undefined && answer !== null ? null : - - } - + {answer !== undefined && answer !== null ? null : + + } + +
+ }
@@ -1092,6 +1037,7 @@ const RunWorkflow = (defaultprops) => { usecases={undefined} expanded={true} + scrollTo={"input_markdown"} /> : null} @@ -1103,20 +1049,59 @@ const RunWorkflow = (defaultprops) => { PaperProps={{ style: { color: "white", - minWidth: isMobile ? "90%" : 400, - maxWidth: isMobile ? "90%" : 400, - minHeight: 350, - paddingTop: 25, - paddingLeft: 50, + minWidth: isMobile ? "90%" : 500, + maxWidth: isMobile ? "90%" : 500, + minHeight: 400, + maxHeight: 400, + padding: 25, + borderRadius: theme.palette.borderRadius, //minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, //maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, }, }} > - - Share Landingpage + + Form Sharing Options for '{workflow.name}' - + + + General Access + + + + Form sharing and workflow sharing are not the same. By sharing a form, you are enabling anyone with the link to fill out the form AND run the workflow. They will NOT have access to seeing workflow details. By default, anyone with access to an organization can use a form. + + +
+ {workflow !== undefined && workflow !== null && workflow.sharing !== undefined && workflow.sharing !== null ? + + : null}
@@ -1135,19 +1120,19 @@ const RunWorkflow = (defaultprops) => { : null} + {basedata} : -
+
diff --git a/frontend/src/views/UpdateAuthentication.jsx b/frontend/src/views/UpdateAuthentication.jsx index ab78290e..01e4e730 100644 --- a/frontend/src/views/UpdateAuthentication.jsx +++ b/frontend/src/views/UpdateAuthentication.jsx @@ -24,7 +24,7 @@ const SetAuthentication = (props) => { const [loadFail, setLoadFail] = useState(""); const [appAuthentication, setAppAuthentication] = React.useState([]); - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); //const alert = useAlert(); const parseIncomingOpenapiData = (data) => { @@ -142,8 +142,8 @@ const SetAuthentication = (props) => { // 3. Help them set info for the app // Make sure to test both private and public apps - const appname = app.name !== undefined ? app.name : ""; - const appLink = "/apps/" + app.id || ""; + const appname = app.name !== undefined ? app.name : "" + const appLink = "/apps/" + app.id || "" console.log("App: ", app) @@ -154,21 +154,16 @@ const SetAuthentication = (props) => { : <>
- A Shuffle Organization has invited you to: Configure {appname} Authentication + You are invited to: Configure {appname} Authentication - {/* What does this mean box */} What does this mean? - + A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows. - - Authenticate Here: - - {app.authentication === undefined || app.authentication === null || app.authentication.length === 0 ? null @@ -195,7 +190,7 @@ const SetAuthentication = (props) => { appAuthentication={appAuthentication} />} - + What can they do with this? @@ -203,12 +198,11 @@ const SetAuthentication = (props) => { You can check the actions they want to use here. - - {/* Add a box below */} +
{app.actions?.map((item, index) => ( -
+
handleToggle(index)}> {item.label}
diff --git a/frontend/src/views/Welcome.jsx b/frontend/src/views/Welcome.jsx index 12811456..8c57c3ff 100644 --- a/frontend/src/views/Welcome.jsx +++ b/frontend/src/views/Welcome.jsx @@ -47,7 +47,7 @@ const Welcome = (props) => { } }, [activeStep]) - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); const [steps, setSteps] = useState([ "Help us get to know you", "Find your Apps", diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 75d3b799..0c55a8d7 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -3528,7 +3528,7 @@ const Workflows = (props) => { var workflowDelay = -150 var appDelay = -75 - const foundPriority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) + const foundPriority = userdata === undefined || userdata === null || userdata.priorities === undefined || userdata.priorities === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) return (