Workflow Action Rerun optimization

This commit is contained in:
Frikky
2025-03-25 00:54:32 +01:00
parent a2e4d37eea
commit a3b0093cf5
9 changed files with 301 additions and 55 deletions
+2
View File
@@ -545,6 +545,8 @@ const EditWorkflow = (props) => {
}}
>
<FormControlLabel value="test" control={<Radio />} label="Test" />
<FormControlLabel value="staging" control={<Radio />} label="Staging" />
<FormControlLabel value="preprod" control={<Radio />} label="Pre-production" />
<FormControlLabel value="production" control={<Radio />} label="Production" />
</RadioGroup>
+2 -1
View File
@@ -1363,6 +1363,7 @@ const Navbar = (props) => {
sx={buttonStyles}
onClick={() => {
if(isCloud) {
// navigate("/new-pricing");
navigate("/pricing");
ReactGA.event({
category: "navbar",
@@ -1370,7 +1371,7 @@ const Navbar = (props) => {
label: "go_to_pricing",
})
} else {
window.open("https://shuffler.io/pricing", '_blank');
window.open("https://shuffler.io/pricing?env=Self-hosted", '_blank');
return;
}
}}
+49 -10
View File
@@ -179,6 +179,7 @@ const ParsedAction = (props) => {
suborgWorkflows,
originalWorkflow,
runFromHere,
} = props;
let navigate = useNavigate()
@@ -233,11 +234,16 @@ const ParsedAction = (props) => {
// auth, required, optional
var changed = false
if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) {
console.log("Returning because no params")
return
}
if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated !== true) {
return
if (isAgent || isIntegration) {
} else {
return
}
}
// Fixing required fields with a shitty structure :)
@@ -346,13 +352,23 @@ const ParsedAction = (props) => {
// 3. generated fields & all else
const newparams = auth
var newparams = auth
.concat(bodyfield)
.concat(required)
.concat(generated_optional)
.concat(special_optional)
.concat(optional)
const dedupedParams = []
for (var paramKey in newparams) {
const param = newparams[paramKey]
if (dedupedParams.find(item => item.name === param.name) === undefined) {
dedupedParams.push(param)
}
}
newparams = dedupedParams
var newkeyorder = []
for (let paramkey in newparams) {
//console.log("Param: ", newparams[paramkey])
@@ -1814,6 +1830,29 @@ const ParsedAction = (props) => {
</Tooltip>
</IconButton>
<Button
color="secondary"
variant="outlined"
style={{
marginTop: "auto",
marginBottom: "auto",
height: 30,
marginLeft: 115,
textTransform: "none",
}}
disabled={autoCompleting}
onClick={() => {
if (runFromHere !== undefined) {
runFromHere(selectedAction)
} else {
toast.error("Function not available. Please contact support@shuffler.io")
}
}}
>
<PlayArrowIcon style={{marginRight: 5, }}/>
Rerun
</Button>
{(selectedAction?.generated === true && selectedAction?.app_version === "1.0.0") || (selectedAction?.app_name === "Shuffle Tools" && selectedAction?.app_version !== "1.2.0") ?
<Button
variant="contained"
@@ -1875,12 +1914,12 @@ const ParsedAction = (props) => {
toast.success("Changed version of all nodes to " + event.target.value)
}}
style={{
marginTop: 10,
position: "absolute",
top: 10, right: 10,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.inputColor,
color: "white",
height: 35,
marginleft: 10,
borderRadius: theme.palette?.borderRadius,
}}
SelectDisplayProps={{
@@ -4120,15 +4159,12 @@ const ParsedAction = (props) => {
fullWidth
id={"rightside_field_" + count}
onChange={(e) => {
console.log("MULTI SELECT: ", multi, e.target.value)
changeActionParameter(e, count, data);
setUpdate(Math.random());
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: "50px",
backgroundColor: theme.palette.platformColor,
height: 40,
borderRadius: theme.palette?.borderRadius,
}}
>
@@ -4685,8 +4721,9 @@ const ParsedAction = (props) => {
</Tooltip>
: null}
<Tooltip title="Expand editor window" placement="top">
{((data.options !== undefined && data.options !== null && data.options.length > 0) || (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0)) ? null :
<Tooltip title="Expand editor window" placement="top">
<OpenInFullIcon
style={{ color: "rgba(255,255,255,0.7)", cursor: "pointer", margin: multiline ? 5 : 0, height: 20, width: 20, }}
onMouseOver={(event) => {
@@ -4728,6 +4765,7 @@ const ParsedAction = (props) => {
}}
/>
</Tooltip>
}
</div>
@@ -4737,6 +4775,7 @@ const ParsedAction = (props) => {
showDropdownNumber === count &&
data.variant === "STATIC_VALUE" &&
jsonList.length > 0 ? (
<FormControl fullWidth style={{ marginTop: 0 }}>
<InputLabel
id="action-autocompleter"
+47 -8
View File
@@ -46,6 +46,7 @@ import {
DragIndicator as DragIndicatorIcon,
RestartAlt as RestartAltIcon,
ArrowForward as ArrowForwardIcon,
KeyboardReturn as KeyboardReturnIcon,
} from '@mui/icons-material';
@@ -94,9 +95,12 @@ const pythonFilters = [
{ "name": "Get full execution details", "value": `print(self.full_execution)`, "example": `` },
{ "name": "Use files", "value": `# Create a sample file\nfiles = [{\n \"filename\": \"test.txt\",\n \"data\": \"Testdata\"\n}]\nret = self.set_files(files)\n\n# Get the content of the file from Shuffle storage\n# Originally a byte string in the \"data\" key\nfile_content = (self.get_file(ret[0])[\"data\"]).decode()\nprint(file_content)`, "example": `` },
{ "name": "Use datastore", "value": `key = \"testkey\"\nvalue = \"The value of the testkey\"\n\nself.set_cache(key, value)\n\n# Print the details of the key after it's been updated\n# To get the value, use self.get_cache(key)[\"value\"]\nprint(self.get_cache(key))`, "example": `` },
{ "name": "Run an App Action", "value": `response = shuffle.run_app(app_id="app", action="action_name", auth="authentication_id", params={})\nprint(response)`, "example": ``, "disabled": true, },
{ "name": "Run a Singul AI Action", "value": `response = singul.create_ticket(app="jira/iris/ticketingsystem", fields={"title": "Test ticket!"})\nprint(response)`, "example": ``, "disabled": true, },
{ "name": "Use datastore", "value": `key = \"testkey\"\nvalue = \"The value of the testkey\"\n\nself.set_key(key, value)\n\n# Print the details of the key after it's been updated\n# To get the value, use self.get_key(key)[\"value\"]\nprint(self.get_key(key))`, "example": `` },
{ "name": "Run a Subflow", "value": `response = shuffle.run_workflow(workflow_id="", start_command="Runtime arg here!", wait=True)\nprint(response)`, "example": ``, "disabled": false, },
{ "name": "Run an App Action", "value": `response = shuffle.run_app(app_id="app", action="action_name", auth="authentication_id", params={})\nprint(response)`, "example": ``, "disabled": false, },
{ "name": "Run a Singul AI Action", "value": `response = singul.cases.create_ticket(app="jira", fields={"title": "Test ticket!"})\nprint(response)`, "example": ``, "disabled": false, },
]
@@ -299,8 +303,19 @@ const CodeEditor = (props) => {
setMainVariables(tmpVariables)
}
const handleKeyDown = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault()
const tryItButton = document.getElementById("try-it-button")
if (tryItButton !== undefined && tryItButton !== null) {
tryItButton.click()
}
}
}
// Remove the original useEffect for actionlist since we'll update on action/trigger changes
useEffect(() => {
document.addEventListener("keydown", handleKeyDown)
updateAvailableVariables(actionlist)
}, [])
@@ -1721,13 +1736,13 @@ const CodeEditor = (props) => {
color="secondary"
style={{
textTransform: "none",
width: 120,
width: 145,
}}
onClick={(event) => {
setAnchorEl3(event.currentTarget);
}}
>
Python Code
Python Examples
</Button>
<Button
id="basic-button"
@@ -1762,7 +1777,12 @@ const CodeEditor = (props) => {
>
{pythonFilters.map((item, index) => {
return (
<MenuItem key={index} onClick={() => {
<MenuItem
style={{
borderTop: item.name === "Use files" || (item.name.toLowerCase().includes("run") && item.name.toLowerCase().includes("subflow")) ? "2px solid rgba(255,255,255,0.3)" : "none",
}}
key={index} onClick={() => {
if (item.disabled) {
toast.error("This feature may not work in your environment until you update your Shuffle Tools app.", { autoClose: 10000 })
}
@@ -2270,6 +2290,7 @@ const CodeEditor = (props) => {
<div style={{}}>
<Tooltip title="Try it! This runs the Shuffle Tools 'repeat back to me' or 'execute python' action with what you see in the expected output window. Commonly used to test your Python scripts or Liquid filters, not requiring the full workflow to run again." placement="top">
<Button
id="try-it-button"
variant="outlined"
disabled={executing}
color="primary"
@@ -2283,11 +2304,13 @@ const CodeEditor = (props) => {
zIndex: 1200,
fontWeight: 500,
fontSize: 14,
textTransform: "none",
backgroundColor: "rgba(33, 33, 33, 0.95)",
backdropFilter: "blur(8px)",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)",
transition: "all 0.2s ease",
borderRadius: "4px",
paddingRight: 20,
borderRadius: theme.palette?.borderRadius,
"&:hover": {
backgroundColor: "rgba(45, 45, 45, 0.95)",
transform: "translateY(-1px)",
@@ -2304,7 +2327,23 @@ const CodeEditor = (props) => {
{executing ?
<CircularProgress style={{ height: 18, width: 18, }} />
:
<span>{selectedAction === undefined ? "Try it" : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"}<PlayArrowIcon style={{ height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
<span>
<PlayArrowIcon style={{ height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} />
{selectedAction === undefined ? "Try it" : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"}
<span
style={{
color: "#C8C8C8",
fontSize: "12px",
whiteSpace: "nowrap",
marginLeft: 5,
marginRight: 10,
}}
>
<kbd>Ctrl</kbd> + <kbd><KeyboardReturnIcon style={{width: 13, position: "absolute", marginLeft: 3, top: 5, }}/></kbd>
</span>
</span>
}
</Button>
</Tooltip>
+11 -11
View File
@@ -27,7 +27,7 @@ const theme = createTheme(adaptV4Theme({
distributionColor: "#40E0D0",
green: "#5cc879",
borderRadius: 10,
borderRadius: 8,
defaultBorder: "1px solid rgba(255,255,255,0.3)",
//jsonTheme: "brewer",
@@ -112,39 +112,39 @@ const theme = createTheme(adaptV4Theme({
MuiCssBaseline: {
styleOverrides: `
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light');
src: local('Inter Light'), local('Inter-Light');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular');
src: local('Inter Regular'), local('Inter-Regular');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium');
src: local('Inter Medium'), local('Inter-Medium');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 600;
src: local('Roboto SemiBold'), local('Roboto-SemiBold');
src: local('Inter SemiBold'), local('Inter-SemiBold');
}
@font-face {
font-family: 'Roboto';
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold');
src: local('Inter Bold'), local('Inter-Bold');
}
`,
},
+171 -17
View File
@@ -652,6 +652,31 @@ const AngularWorkflow = (defaultprops) => {
],
"multiselect": true,
},
{
"name": "memory",
"value": "",
"required": true,
"description": "Whether to store the conversation in memory",
"options": [
"Nothing",
"Shuffle Datastore",
],
"multiselect": false,
"disabled": true,
},
{
"name": "knowledge",
"value": "",
"required": true,
"description": "The knowledge we should inject into the context window",
"options": [
"Nothing",
"Shuffle Files",
],
"multiselect": false,
"disabled": true,
},
]
}],
large_image: theme.palette.singulBlackWhite,
@@ -772,7 +797,7 @@ const AngularWorkflow = (defaultprops) => {
const [loadedApps, setLoadedApps] = React.useState([])
const loadAppConfig = (appId, select) => {
const loadAppConfig = (appId, select, skipAppLoad) => {
if (appId === undefined || appId === null || appId.length === 0) {
console.log("No appId to load")
return
@@ -868,7 +893,7 @@ const AngularWorkflow = (defaultprops) => {
setSelectedApp(foundapp)
}
if (apps === undefined || apps === null || apps.length === 0) {
if ((apps === undefined || apps === null || apps.length === 0) && skipAppLoad !== true) {
console.log("No apps to update :(")
getApps()
return
@@ -2785,6 +2810,94 @@ const AngularWorkflow = (defaultprops) => {
return true;
};
const runFromHere = (curAction) => {
if (curAction.app_id === undefined || curAction.app_id === null || curAction.app_id.length === 0) {
toast.error("No app id found for action. Please contact support@shuffler.io if this persists")
return
}
if (workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0) {
curAction.source_workflow = workflow.id
}
// Based on the previous execution id
console.log("WORKFLOW EXEC: ", workflowExecutions)
// Look for the "execution_id" parameter
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const execFound = new URLSearchParams(cursearch).get("execution_id");
if (execFound !== undefined && execFound !== null && execFound.length > 0) {
toast.info("Rerunning based on previously watched execution id")
curAction.source_execution = execFound
} else if (workflowExecutions !== undefined && workflowExecutions !== null && workflowExecutions.length > 0) {
curAction.source_execution = workflowExecutions[0].execution_id
} else {
toast.error("No previous execution found. Please run the workflow first.")
return
}
setExecutionRunning(true)
setExecutionRequestStarted(true)
var headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
headers["Org-Id"] = workflow.org_id
}
// Rerun makes it return execution_id + authorization
const appRunUrl = `${globalUrl}/api/v1/apps/${curAction.app_id}/run?rerun=true`
fetch(appRunUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(curAction),
credentials: "include",
})
.then((response) => {
setExecutionRunning(false)
setExecutionRequestStarted(false)
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!")
}
return response.json()
})
.then((responseJson) => {
setExecutionRequestStarted(false)
if (responseJson?.success === false) {
setExecutionRunning(false)
if (responseJson?.reason !== undefined && responseJson?.reason !== null && responseJson?.reason.length > 0) {
toast.error(responseJson.reason)
} else {
toast.error("Failed to run the action. Please try again or contact support@shuffler.io")
}
return
} else if (responseJson?.success === true && responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id.length > 0) {
navigate(`?execution_id=${responseJson.execution_id}`)
setExecutionRequest({
execution_id: responseJson.execution_id,
authorization: responseJson.authorization,
})
setExecutionData({})
setExecutionModalOpen(true)
setExecutionModalView(1)
start()
}
})
.catch((error) => {
toast.error("Failed to run the action. "+error.toString())
setExecutionRunning(false)
setExecutionRequestStarted(false)
})
}
const executeWorkflow = (executionArgument, startNode, hasSaved, skip_popup) => {
if (hasSaved === false) {
@@ -3203,6 +3316,13 @@ const AngularWorkflow = (defaultprops) => {
return
}
for (var key in responseJson) {
const curapp = responseJson[key]
if (curapp?.actions === undefined || curapp?.actions === null || curapp?.actions?.length === 0 || curapp?.actions?.length === 1) {
loadAppConfig(curapp?.id, false, true)
}
}
// Find app with ID "794e51c3c1a8b24b89ccc573a3defc47" (gmail) to force-break it,
// Find app with ID "3e2bdf9d5069fe3f4746c29d68785a6a" (shuffle tools) to force-break it,
// as to ensure the autocorrect works.
@@ -13580,14 +13700,18 @@ const AngularWorkflow = (defaultprops) => {
setSourceValue({
...sourceValue,
value: value
});
})
setUpdate(Math.random())
} else if (fieldType === "destination") {
setDestinationValue({
...destinationValue,
value: value
});
}
};
})
setUpdate(Math.random())
}
}
const conditionsModal = (
@@ -14763,6 +14887,7 @@ const AngularWorkflow = (defaultprops) => {
]
const handleSubflowParamChange = (triggerId, triggerField, newData) => {
var updateFail = ""
if (workflow !== undefined && workflow !== null) {
// Find the trigger with matching id
@@ -14778,9 +14903,20 @@ const AngularWorkflow = (defaultprops) => {
setWorkflow(workflow);
setSelectedTriggerValue(newData)
setLastSaved(false);
}
}
}
setUpdate(Math.random())
} else {
updateFail = "Parameter is undefined or null"
}
} else {
updateFail = "Trigger is undefined or null"
}
} else {
updateFail = "Workflow is undefined or null"
}
if (updateFail !== "") {
toast.error(updateFail + " - Failed to update subflow parameter value. Please try again.")
}
}
const SubflowSidebar = Object.getOwnPropertyNames(selectedTrigger).length === 0 || workflow.triggers[selectedTriggerIndex] === undefined || selectedTrigger.trigger_type !== "SUBFLOW" ? null :
@@ -20801,9 +20937,9 @@ const AngularWorkflow = (defaultprops) => {
onClose={() => {
setExecutionModalOpen(false)
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const newitem = removeParam("execution_id", cursearch);
navigate(curpath + newitem)
//const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
//const newitem = removeParam("execution_id", cursearch);
//navigate(curpath + newitem)
}}
style={{
resize: "both",
@@ -21256,7 +21392,11 @@ const AngularWorkflow = (defaultprops) => {
<h2>Details</h2>
<Tooltip
color="primary"
title="Rerun workflow (uses same startnode as the original)"
title={
<Typography variant="body1">
Rerun workflow. Uses same startnode as the original. Runs from scratch.
</Typography>
}
placement="top"
style={{ zIndex: 50000 }}
>
@@ -21558,7 +21698,7 @@ const AngularWorkflow = (defaultprops) => {
</div>
: null}
{userdata.support === true && executionData.workflow !== undefined && executionData.workflow !== null && executionData.status !== "EXECUTING" ?
{userdata.support === true && executionData.workflow !== undefined && executionData.workflow !== null && executionData.status !== "EXECUTING" && executionData.status !== "ABORTED" ?
<div style={{ marginTop: 5, marginBottom: 5, }}>
<WorkflowValidationTimeline
originalWorkflow={workflow}
@@ -22857,6 +22997,7 @@ const AngularWorkflow = (defaultprops) => {
suborgWorkflows={suborgWorkflows}
originalWorkflow={originalWorkflow}
runFromHere={runFromHere}
/>
</div>
</Fade>
@@ -24731,6 +24872,8 @@ const AngularWorkflow = (defaultprops) => {
}
const handleActionParamChange = (actionId, fieldName, newData) => {
var updateFail = ""
if (workflow !== undefined) {
// Find the action with matching id
const actionIndex = workflow?.actions.findIndex(action => action.id === actionId);
@@ -24745,9 +24888,20 @@ const AngularWorkflow = (defaultprops) => {
// Update workflow state to trigger re-render
setWorkflow({...workflow});
setLastSaved(false);
}
}
}
setUpdate(Math.random())
} else {
updateFail = "Parameter is undefined or null"
}
} else {
updateFail = "Trigger is undefined or null"
}
} else {
updateFail = "Workflow is undefined or null"
}
if (updateFail !== "") {
toast.error(updateFail + " - Failed to update subflow parameter value. Please try again.")
}
}
/*
var foundusecase = {}
+4 -5
View File
@@ -1088,14 +1088,13 @@ const AppExplorer = (props) => {
setNewWorkflowTags(newWorkflowTags);
}
// This is annoying (:
var securitySchemes = data.components.securityDefinitions;
var securitySchemes = data?.components?.securityDefinitions;
if (securitySchemes === undefined) {
securitySchemes = data.securitySchemes;
securitySchemes = data?.securitySchemes;
}
if (securitySchemes === undefined) {
securitySchemes = data.components.securitySchemes;
securitySchemes = data?.components?.securitySchemes;
}
const allowedfunctions = [
@@ -1681,7 +1680,7 @@ const AppExplorer = (props) => {
setExecutionResult({
valid: false,
result:
"Couldn't finish execution (2). Please fill all the required fields, and validate the execution.",
"Couldn't finish execution OR no result was returned (2). Please fill all the required fields, and validate the execution.",
});
}
+6 -1
View File
@@ -343,10 +343,15 @@ const Docs = (defaultprops) => {
return
}
console.log("PROPKEY: ", propkey)
if (location.pathname.includes("/docs/")) {
if (propkey === "cookie_policy" || propkey === "compliance" || propkey === "privacy_policy" || propkey === "terms_of_service") {
navigate(`/legal/${propkey}`)
}
if (propkey === "app_creation") {
navigate('/docs/apps#app-creation-introduction')
}
}
}, [location]);
@@ -932,7 +937,7 @@ const Docs = (defaultprops) => {
}
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found") && !isArticlePage) {
navigate("/docs")
//navigate("/docs")
return
}
+9 -2
View File
@@ -527,6 +527,8 @@ const Settings = (props) => {
};
const generateApikey = () => {
toast.info("Generating new API key. This may take a bit.");
fetch(globalUrl + "/api/v1/generateapikey", {
method: "GET",
headers: {
@@ -538,7 +540,7 @@ const Settings = (props) => {
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
}
return response.json();
})
@@ -836,7 +838,12 @@ const Settings = (props) => {
variant="outlined"
/>
<Button
style={{ width: "100%", height: "40px", marginTop: "10px" }}
style={{
width: "100%",
height: "40px",
marginTop: "10px",
textTransform: "none",
}}
variant="outlined"
color="primary"
onClick={() => generateApikey()}