Onprem X Cloud sync commit. Large changes to the navbar mechanisms

This commit is contained in:
Frikky
2025-02-19 01:29:40 +01:00
parent 440a6bcb23
commit 80782d8270
18 changed files with 603 additions and 208 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3 github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.7.92 github.com/shuffle/shuffle-shared v0.7.95
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.32.0
google.golang.org/api v0.176.1 google.golang.org/api v0.176.1
google.golang.org/grpc v1.68.1 google.golang.org/grpc v1.68.1
+61 -4
View File
@@ -88,6 +88,8 @@ const EditWorkflow = (props) => {
const [inputMarkdown, setInputMarkdown] = React.useState(workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null ? workflow?.form_control?.input_markdown : "") const [inputMarkdown, setInputMarkdown] = React.useState(workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null ? workflow?.form_control?.input_markdown : "")
const [scrollDone, setScrollDone] = React.useState(false) const [scrollDone, setScrollDone] = React.useState(false)
const [selectedYieldActions, setSelectedYieldActions] = React.useState(workflow?.form_control?.output_yields !== undefined && workflow?.form_control?.output_yields !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.output_yields)) : []) const [selectedYieldActions, setSelectedYieldActions] = React.useState(workflow?.form_control?.output_yields !== undefined && workflow?.form_control?.output_yields !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.output_yields)) : [])
const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : [])
const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth) const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth)
const classes = useStyles(); const classes = useStyles();
@@ -323,6 +325,7 @@ const EditWorkflow = (props) => {
innerWorkflow.form_control.input_markdown = inputMarkdown innerWorkflow.form_control.input_markdown = inputMarkdown
innerWorkflow.form_control.output_yields = selectedYieldActions innerWorkflow.form_control.output_yields = selectedYieldActions
innerWorkflow.form_control.form_width = formWidth innerWorkflow.form_control.form_width = formWidth
innerWorkflow.form_control.cleanup_actions = selectedCleanupActions
innerWorkflow.name = name innerWorkflow.name = name
innerWorkflow.description = description innerWorkflow.description = description
@@ -568,7 +571,7 @@ const EditWorkflow = (props) => {
<Divider id="mssp_control" style={{ marginTop: 20, marginBottom: 20, }} /> <Divider id="mssp_control" style={{ marginTop: 20, marginBottom: 20, }} />
<Typography variant="h4" style={{ marginTop: 50, }}> <Typography variant="h4" style={{ marginTop: 50, }}>
MSSP & Distribution controls Multi-Tenancy, Backups & Security
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 30, marginBottom: 10, }}> <Typography variant="body2" color="textSecondary" style={{ marginTop: 30, marginBottom: 10, }}>
@@ -678,7 +681,7 @@ const EditWorkflow = (props) => {
} }
<Typography variant="body1" style={{ marginTop: 75, }}> <Typography variant="h6" style={{ marginTop: 75, }}>
Git Backup Repository Git Backup Repository
</Typography> </Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary"> <Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
@@ -824,6 +827,60 @@ const EditWorkflow = (props) => {
</Grid> </Grid>
</Grid> </Grid>
<div id="cleanup">
<Typography variant="h6" style={{ marginTop: 50, }}>
Result cleanup ({selectedCleanupActions.length === 0 ? "No cleanup yet" : selectedCleanupActions.length === 1 ? "Cleaning up 1 node" : `Cleaning up ${selectedCleanupActions.length} nodes`})
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}>
<b>Beta Feature</b>: When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up.
</Typography>
<FormControl style={{ marginTop: 15, }}>
<Select
defaultValue=""
id="result-cleanup-control"
label="Cleaned Up nodes"
multiple
fullWidth
style={{ width: 500, }}
value={selectedCleanupActions === [] ? ["none"] : selectedCleanupActions}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
if (event.target.value.length > 0) {
if (event.target.value.includes("none")) {
setSelectedCleanupActions([])
return
}
}
const newvalue = event?.target?.value
if (newvalue === undefined || newvalue === null) {
} else {
setSelectedCleanupActions(newvalue)
}
}}
>
<MenuItem value="none">
<em>None</em>
</MenuItem>
{workflow?.actions?.map((action, actionIndex) => {
return (
<MenuItem
key={actionIndex}
value={action.id}
>
<Tooltip title={action.app_name} key={actionIndex}>
<img src={action.large_image !== undefined && action.large_image !== null && action.large_image.length > 0 ? action.large_image : theme.palette.defaultImage} style={{ width: 20, height: 20, marginRight: 10, }} />
</Tooltip>
{action.label}
</MenuItem>
)
})}
</Select>
</FormControl>
</div>
<Divider style={{ marginTop: 20, marginBottom: 20, }} /> <Divider style={{ marginTop: 20, marginBottom: 20, }} />
@@ -1023,11 +1080,11 @@ const EditWorkflow = (props) => {
<div id="output_control"> <div id="output_control">
<Typography variant="h6" style={{ marginTop: 50, }}> <Typography variant="h6" style={{ marginTop: 50, }}>
Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`}) Form Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`})
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}> <Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}>
When running this workflow, the output will be shown as a Markdown object by default, with JSON objects being rendered. By adding nodes below, they will be shown while the workflow is running as soon as they get a result. Failing/Skipped nodes are not shown. This makes it possible to track progress for more complex usecases. When running this workflow as a form, the output will be shown as a Markdown object by default, with JSON objects being rendered. By adding nodes below, they will be shown while the workflow is running as soon as they get a result. Failing/Skipped nodes are not shown. This makes it possible to track progress for more complex usecases.
</Typography> </Typography>
<FormControl style={{ marginTop: 15, }}> <FormControl style={{ marginTop: 15, }}>
+20 -11
View File
@@ -1064,17 +1064,26 @@ const EnvironmentTab = memo((props) => {
}} }}
style={{minWidth:120, display: "table-cell",}} style={{minWidth:120, display: "table-cell",}}
primary={ primary={
<Tooltip title={environment.Type !== "cloud" <Tooltip title={
? environment.running_ip === undefined || <Typography variant="body1" style={{margin: 10, }}>
environment.running_ip === null || {environment.Type !== "cloud"
environment.running_ip.length === 0 ? environment.running_ip === undefined ||
? environment.running_ip === null ||
"Not running. Click to get the start command that can be ran on your server." environment.running_ip.length === 0
: ?
<span>IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus.</span> "Not running. Click to get the start command that can be ran on your server."
: :
"Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions." <span>IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus.</span>
} placement="top"> :
"Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions."
}
<br />
<br />
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"}
</Typography>
} placement="top">
<Typography <Typography
style={{ style={{
minWidth: 100, minWidth: 100,
+1 -1
View File
@@ -1263,7 +1263,7 @@ const Files = memo((props) => {
} }
const isDistributed = file?.suborg_distribution?.length > 0 ? true : false; const isDistributed = file?.suborg_distribution?.length > 0 ? true : false;
const filenamesplit = file.filename.split(".") const filenamesplit = file.filename.split(".")
const iseditable = file.filesize < 2000000 && file.status === "active" && allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) const iseditable = file.filesize < 2000000 && file.status === "active" && (allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) || !file?.filename.includes("."))
return ( return (
<ListItem <ListItem
key={index} key={index}
+33 -10
View File
@@ -29,6 +29,7 @@ import {
Fade, Fade,
Portal, Portal,
Collapse, Collapse,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import theme from "../theme.jsx"; import theme from "../theme.jsx";
import RecentWorkflow from "../components/RecentWorkflow.jsx"; import RecentWorkflow from "../components/RecentWorkflow.jsx";
@@ -524,7 +525,7 @@ useEffect(() => {
<Divider style={{ marginBottom: 10, }} /> <Divider style={{ marginBottom: 10, }} />
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}> <Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: 2.0.0-rc6 Version: 2.0.0-rc7
</Typography> </Typography>
</Menu> </Menu>
</span> </span>
@@ -812,14 +813,36 @@ useEffect(() => {
setExpandLeftNav(false) setExpandLeftNav(false)
} }
}} }}
> >
<a href="/" style={{ textDecoration: "none" }}> <Tooltip
<img title="Go to Home"
src={ShuffleLogo} placement="top"
alt="Shuffle Logo" arrow
style={{ width: 24, height: 24 }} componentsProps={{
/> tooltip: {
</a> sx: {
backgroundColor: "rgba(33, 33, 33, 1)",
color: "rgba(241, 241, 241, 1)",
fontSize: 12,
border: "1px solid rgba(73, 73, 73, 1)",
fontFamily: theme?.typography?.fontFamily,
}
},
popper: {
sx: {
zIndex: 1000019,
}
}
}}
>
<Link to="/">
<img
src={ShuffleLogo}
alt="Shuffle Logo"
style={{ width: 24, height: 24 }}
/>
</Link>
</Tooltip>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -1454,7 +1477,7 @@ useEffect(() => {
: "#C8C8C8" : "#C8C8C8"
}} }}
> >
Shuffle Agent Hybrid Locations
</span> </span>
</Button> </Button>
</Link> </Link>
+73 -38
View File
@@ -688,7 +688,7 @@ const ParsedAction = (props) => {
if (newActionList.find((item) => item.type === "Shuffle DB") === undefined) { if (newActionList.find((item) => item.type === "Shuffle DB") === undefined) {
let cacheKey = { let cacheKey = {
type: "Shuffle DB", type: "Shuffle DB",
name: "Shuffle DB", name: "Shuffle Datastore",
value: "$shuffle_cache", value: "$shuffle_cache",
highlight: "shuffle_cache", highlight: "shuffle_cache",
autocomplete: "shuffle_cache", autocomplete: "shuffle_cache",
@@ -753,44 +753,78 @@ const ParsedAction = (props) => {
if (parents.length > 1) { if (parents.length > 1) {
const labels = []; const labels = [];
for (let parentNode of parents) { for (let parentNode of parents) {
if (parentNode.label !== "Runtime Argument" && !labels.includes(parentNode.label)) { if (parentNode.label === "Runtime Argument" || labels.includes(parentNode.label)) {
labels.push(parentNode.label); continue
let exampleData = parentNode.example ?? ""; }
if (!exampleData && workflowExecutions.length > 0) {
for (let exec of workflowExecutions) { labels.push(parentNode.label);
const foundResult = exec.results?.find(result => result.action.id === parentNode.id); let exampleData = parentNode.example ?? "";
if (foundResult) { if (parentNode?.app_name === "http") {
const valid = validateJson(foundResult.result); exampleData = ""
if (valid.valid && valid.result.success !== false) { }
exampleData = valid.result;
break; if (workflowExecutions.length > 0) {
} for (let exec of workflowExecutions) {
const foundResult = exec.results?.find(result => result?.action?.id === parentNode?.id);
if (foundResult) {
const valid = validateJson(foundResult.result);
if (valid.valid && valid.result.success !== false) {
exampleData = valid.result
break
} }
} }
} }
if (parentNode.label === undefined) {
parentNode.label = ""
}
newActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
parentActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
} }
if (exampleData === "" && apps !== undefined && apps !== null && apps?.length > 0) {
// Check apps if it exists, then if it
const foundApp = apps?.find(app => app?.id === parentNode?.app_id)
if (foundApp !== undefined && foundApp !== null) {
if (foundApp?.generated === true || foundApp?.name === "http") {
const validationData = validateJson(`{
"status": 200,
"body": {
"example": "json",
"values": "json"
},
"url": "https://example.com",
"headers": {
"Content-Type": "application/json",
"Example-Header": "two"
},
"cookies": {
"example": "session",
"__session": "sessionid"
},
"success": true
}`)
if (validationData.valid) {
exampleData = validationData.result
}
}
}
}
if (parentNode.label === undefined) {
parentNode.label = ""
}
newActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
parentActionList.push({
type: "action",
id: parentNode.id,
name: parentNode.label,
autocomplete: parentNode.label.split(" ").join("_"),
example: exampleData,
});
} }
} }
} }
@@ -840,8 +874,8 @@ const ParsedAction = (props) => {
return { ...param, value: paramvalue, error: message } return { ...param, value: paramvalue, error: message }
}); });
setSelectedActionParameters(newParameters); setSelectedActionParameters(newParameters)
setActionlist(newActionList); setActionlist(newActionList)
}, [workflow.execution_variables, paramUpdate, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents, setNewSelectedAction]); }, [workflow.execution_variables, paramUpdate, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents, setNewSelectedAction]);
useEffect(() => { useEffect(() => {
@@ -2575,7 +2609,8 @@ const ParsedAction = (props) => {
</div> </div>
*/} */}
{setNewSelectedAction !== undefined ? ( {isAgent ? null :
setNewSelectedAction !== undefined ? (
<Autocomplete <Autocomplete
id="action_search" id="action_search"
disabled={isAgent || (selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0)} disabled={isAgent || (selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0)}
+13 -6
View File
@@ -276,6 +276,8 @@ const CodeEditor = (props) => {
parsedPaths = GetParsedPaths(actionlist[i].value, ""); parsedPaths = GetParsedPaths(actionlist[i].value, "");
} }
} else { } else {
//console.log("EXAMPLE: ", actionlist[i])
// Handle regular action results // Handle regular action results
if (typeof actionlist[i].example === "object") { if (typeof actionlist[i].example === "object") {
parsedPaths = GetParsedPaths(actionlist[i].example, ""); parsedPaths = GetParsedPaths(actionlist[i].example, "");
@@ -1228,7 +1230,7 @@ const CodeEditor = (props) => {
} }
const editorLoad = (editor) => { const editorLoad = (editor) => {
console.log("EDITOR: ", editor) //console.log("EDITOR: ", editor)
editor.completers = [customCompleter] editor.completers = [customCompleter]
} }
@@ -1526,7 +1528,7 @@ const CodeEditor = (props) => {
Source Data Source Data
</Typography> </Typography>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
Drag the data you want into the text editor Drag the data you want into the text editor! <b>PS: Only support users can see this test-section!</b>
</Typography> </Typography>
{actionlist?.map((innerdata) => { {actionlist?.map((innerdata) => {
@@ -2246,8 +2248,8 @@ const CodeEditor = (props) => {
{selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? {selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ?
"Code to run" : "Code to run" :
triggerId ? triggerId ?
`Output : ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)}(${triggerField})` : `Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` :
`Output : ${appName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + appName?.replaceAll("_", " ").slice(1)}(${fieldName})` `Output: ${appName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + appName?.replaceAll("_", " ").slice(1)} (${fieldName})`
} }
</span> </span>
} }
@@ -2429,7 +2431,10 @@ const CodeEditor = (props) => {
variant="outlined" variant="outlined"
color="secondary" color="secondary"
onClick={() => { onClick={() => {
navigate("") if (isFileEditor !== true) {
navigate("")
}
setExpansionModalOpen(false); setExpansionModalOpen(false);
}} }}
> >
@@ -2453,7 +2458,9 @@ const CodeEditor = (props) => {
} }
*/ */
navigate("") if (isFileEditor !== true) {
navigate("")
}
// Take localcodedata through the Shuffle JSON parser just in case // Take localcodedata through the Shuffle JSON parser just in case
// This is to make it so we don't need to handle these fixes on the // This is to make it so we don't need to handle these fixes on the
// backend by itself // backend by itself
@@ -678,7 +678,7 @@ const WorkflowTemplatePopup = (props) => {
} }
{img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ? {img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ?
<Tooltip title={dstapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top"> <Tooltip title={dstapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
<div style={{display: "flex", }}> <div style={{display : dstapp === "NA" ? "none" : "flex" }}>
<TrendingFlatIcon style={{ marginTop: 7, }} /> <TrendingFlatIcon style={{ marginTop: 7, }} />
<div style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}> <div style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
<img src={img2} style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleDefault : imagestyle} /> <img src={img2} style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleDefault : imagestyle} />
@@ -50,12 +50,13 @@ const WorkflowTemplatePopup = (props) => {
setIsClicked, setIsClicked,
inputWorkflowId, inputWorkflowId,
inputWorkflow, inputWorkflow,
onClose,
} = props; } = props;
const [isActive, setIsActive] = useState(workflowBuilt === true || (workflowBuilt !== undefined && workflowBuilt !== null && workflowBuilt?.length > 0) || (inputWorkflow !== undefined && inputWorkflow !== null && inputWorkflow.id !== undefined && inputWorkflow.id !== null && inputWorkflow.id !== "") ? true : false) const [isActive, setIsActive] = useState(workflowBuilt === true || (workflowBuilt !== undefined && workflowBuilt !== null && workflowBuilt?.length > 0) || (inputWorkflow !== undefined && inputWorkflow !== null && inputWorkflow.id !== undefined && inputWorkflow.id !== null && inputWorkflow.id !== "") ? true : false)
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true ? true : false) const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [workflowLoading, setWorkflowLoading] = useState(false) const [workflowLoading, setWorkflowLoading] = useState(false)
const [showLoginButton, setShowLoginButton] = useState(false); const [showLoginButton, setShowLoginButton] = useState(false);
@@ -173,6 +174,12 @@ const WorkflowTemplatePopup = (props) => {
} }
}, [configurationFinished, workflow]) }, [configurationFinished, workflow])
useEffect(() => {
if (isModalOpenDefault === true) {
setModalOpen(true);
}
}, [isModalOpenDefault]);
const imageSize = 32 const imageSize = 32
const defaultBorder = "1px solid rgba(255,255,255,0.6)" const defaultBorder = "1px solid rgba(255,255,255,0.6)"
const imagestyleWrapper = { const imagestyleWrapper = {
@@ -516,6 +523,18 @@ const WorkflowTemplatePopup = (props) => {
return false return false
} }
const handleClose = () => {
setModalOpen(false);
if (onClose) {
onClose();
}
if (setIsClicked !== undefined) {
setIsClicked(false);
}
}
const ModalView = () => { const ModalView = () => {
if (modalOpen === false) { if (modalOpen === false) {
return null return null
@@ -528,13 +547,7 @@ const WorkflowTemplatePopup = (props) => {
<Drawer <Drawer
anchor={"right"} anchor={"right"}
open={modalOpen} open={modalOpen}
onClose={() => { onClose={handleClose}
setModalOpen(false);
if (setIsClicked !== undefined) {
setIsClicked(false)
}
}}
PaperProps={{ PaperProps={{
style: { style: {
backgroundColor: "black", backgroundColor: "black",
@@ -550,17 +563,15 @@ const WorkflowTemplatePopup = (props) => {
style={{ style={{
zIndex: 5000, zIndex: 5000,
position: "absolute", position: "absolute",
top: 14, top: 110,
right: 14, right: 110,
color: "white", color: "white",
}} }}
onClick={() => { onClick={handleClose}
setModalOpen(false);
}}
> >
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<DialogContent style={{marginTop: 0, marginLeft: isHomePage ? null : isMobile ? null : 75, maxWidth: 470, }}> <DialogContent style={{marginTop: 0, marginLeft: isHomePage ? null : isMobile ? null : 75, maxWidth: 470, marginTop: 20 }}>
<Typography variant="h4" style={{ fontSize: isMobile ? 20 : null}}> <Typography variant="h4" style={{ fontSize: isMobile ? 20 : null}}>
<b>Configure Workflow</b> <b>Configure Workflow</b>
</Typography> </Typography>
+32
View File
@@ -157,6 +157,38 @@ const data = [
"background-gradient-stop-colors": "data(fillGradient)", "background-gradient-stop-colors": "data(fillGradient)",
}, },
}, },
{
selector: `node[app_id="shuffle_agent"]`,
css: {
"height": "74px",
"width": "222px",
"background-image": "data(large_image)",
"label": function(element) {
var elementname = element.data("label")
if (elementname === null || elementname === undefined) {
return ""
}
if (elementname.length > 15) {
elementname = elementname.substring(0, 15) + ".."
}
return elementname
},
"background-width": "65px",
"background-height": "65px",
"background-position-x": "20px",
//"background-position-x": "center", // Crashes
"background-repeat": "no-repeat",
"font-size": "14px",
"text-halign": "center",
"text-valign": "center",
"text-margin-x": "-140px",
"text-margin-y": "0px",
},
},
{ {
selector: `node[app_name="Testing"]`, selector: `node[app_name="Testing"]`,
css: { css: {
+44 -12
View File
@@ -83,6 +83,11 @@ const theme = createTheme(adaptV4Theme({
typography: { typography: {
fontFamily: `"Roboto", "Helvetica", "Arial", "inter", sans-serif`, fontFamily: `"Roboto", "Helvetica", "Arial", "inter", sans-serif`,
useNextVariants: true, useNextVariants: true,
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightSemiBold: 600,
fontWeightBold: 700,
h1: { h1: {
fontSize: 40, fontSize: 40,
}, },
@@ -104,18 +109,45 @@ const theme = createTheme(adaptV4Theme({
}, },
}, },
MuiCssBaseline: { MuiCssBaseline: {
MuiCssBaseline: { MuiCssBaseline: {
styleOverrides: ` styleOverrides: `
@font-face { @font-face {
font-family: 'roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 300; font-weight: 300;
src: local('roboto'), local('roboto'), format('truetype'); src: local('Roboto Light'), local('Roboto-Light');
unicodeRange: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF; }
} @font-face {
`, font-family: 'Roboto';
}, font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 600;
src: local('Roboto SemiBold'), local('Roboto-SemiBold');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold');
}
`,
},
}, },
}, },
})); }));
+103 -34
View File
@@ -1243,25 +1243,28 @@ const AngularWorkflow = (defaultprops) => {
useEffect(() => { useEffect(() => {
if (selectedTriggerIndex === undefined || selectedTriggerIndex === null || selectedTriggerIndex < 0) { if (selectedTriggerIndex === undefined || selectedTriggerIndex === null || selectedTriggerIndex < 0) {
console.log("Failed in trigger selection: ", selectedTrigger)
return return
} }
//console.log("Failed in trigger selection: ", selectedTriggerIndex, "Trigger: ", selectedTrigger)
var found = null var found = null
/*
try { try {
for (var key in workflows) { for (var key in workflows) {
const curworkflow = workflows[key] const curworkflow = workflows[key]
const curtrigger = curworkflow?.triggers[selectedTriggerIndex] const curtrigger = curworkflow?.triggers[selectedTriggerIndex]
if (curtrigger === undefined || curtrigger === null) { if (curtrigger === undefined || curtrigger === null) {
console.log("Failed in trigger selection (1): ", curworkflow)
continue continue
} }
if (curtrigger?.parameters === undefined || curtrigger?.parameters === null || curtrigger?.parameters.length === 0) { if (curtrigger?.parameters === undefined || curtrigger?.parameters === null || curtrigger?.parameters.length === 0) {
console.log("Failed in trigger selection (2): ", curworkflow)
continue continue
} }
if (curtrigger?.parameters[0] === undefined || curtrigger?.parameters[0] === null || curtrigger?.parameters[0].value === undefined || curtrigger?.parameters[0].value === null) { if (curtrigger?.parameters[0] === undefined || curtrigger?.parameters[0] === null || curtrigger?.parameters[0].value === undefined || curtrigger?.parameters[0].value === null) {
console.log("Failed in trigger selection (3): ", curworkflow)
continue continue
} }
@@ -1275,10 +1278,9 @@ const AngularWorkflow = (defaultprops) => {
setSubworkflow(found) setSubworkflow(found)
} }
} catch (e) { } catch (e) {
console.log("Failed in trigger selection: ", e) console.log("Failed in trigger selection (4): ", e)
return //return
} }
*/
if (found) { if (found) {
const startNode = found.actions?.find((action) => action.id === workflow?.triggers[selectedTriggerIndex]?.parameters[3]?.value) const startNode = found.actions?.find((action) => action.id === workflow?.triggers[selectedTriggerIndex]?.parameters[3]?.value)
@@ -2725,6 +2727,11 @@ const AngularWorkflow = (defaultprops) => {
} }
const monitorUpdates = () => { const monitorUpdates = () => {
if (cy === undefined || cy === null) {
console.log("No cy found to verify startnode.")
return true
}
var firstnode = cy.getElementById(workflow.start); var firstnode = cy.getElementById(workflow.start);
if (firstnode.length === 0) { if (firstnode.length === 0) {
var found = false; var found = false;
@@ -2732,7 +2739,7 @@ const AngularWorkflow = (defaultprops) => {
if (workflow.actions[actionkey].isStartNode) { if (workflow.actions[actionkey].isStartNode) {
console.log("Updating startnode"); console.log("Updating startnode");
workflow.start = workflow.actions[actionkey].id; workflow.start = workflow.actions[actionkey].id;
firstnode = cy.getElementById(workflow.actions[actionkey].id); firstnode = cy.getElementById(workflow.actions[actionkey].id);
found = true; found = true;
break; break;
} }
@@ -2822,10 +2829,12 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
var curelements = cy.elements(); if (cy !== undefined && cy !== null) {
for (let i = 0; i < curelements.length; i++) { var curelements = cy.elements();
curelements[i].addClass("not-executing-highlight"); for (let i = 0; i < curelements.length; i++) {
} curelements[i].addClass("not-executing-highlight");
}
}
var headers = { var headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -2836,6 +2845,11 @@ const AngularWorkflow = (defaultprops) => {
headers["Org-Id"] = workflow.org_id headers["Org-Id"] = workflow.org_id
} }
if (workflow?.id === undefined || workflow?.id === null || workflow?.id?.length === 0) {
console.log("No workflow id found during execution")
workflow.id = props.match.params.key
}
const data = { execution_argument: executionArgument, start: startNode }; const data = { execution_argument: executionArgument, start: startNode };
// fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`, // fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/execute`,
fetch(`${globalUrl}/api/v1/workflows/${workflow.id}/execute`, fetch(`${globalUrl}/api/v1/workflows/${workflow.id}/execute`,
@@ -4116,10 +4130,14 @@ const AngularWorkflow = (defaultprops) => {
var execFound = new URLSearchParams(cursearch).get("execution_id"); var execFound = new URLSearchParams(cursearch).get("execution_id");
var sessionToken = new URLSearchParams(cursearch).get("session_token"); var sessionToken = new URLSearchParams(cursearch).get("session_token");
if (execFound === null && sessionToken === null) { if (execFound === null && sessionToken === null) {
toast(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds..`)
toast.error(`You don't have access to this workflow or loading failed. Redirecting to workflows in a few seconds. If you recently deleted this workflow, speak with support@shuffler.io to recover it from a revision.`, {
autoClose: 10000,
})
setTimeout(() => { setTimeout(() => {
window.location.pathname = "/workflows"; window.location.pathname = "/workflows";
}, 2000); }, 2500);
} else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") { } else if (sessionToken !== null && workflow_id === "3abdfb21-b40f-4e50-b855-ac0d62f83cbe") {
toast(`Injecting session token and reloading workflow..`) toast(`Injecting session token and reloading workflow..`)
@@ -4396,7 +4414,11 @@ const AngularWorkflow = (defaultprops) => {
console.log("Node not found: ", target_id) console.log("Node not found: ", target_id)
} }
cy.fit(null, 400); try {
cy.fit(null, 400);
} catch (e) {
console.log("Error in fitting (1): ", e)
}
cy.on("add", "node", (e) => onNodeAdded(e)); cy.on("add", "node", (e) => onNodeAdded(e));
cy.on("add", "edge", (e) => onEdgeAdded(e)); cy.on("add", "edge", (e) => onEdgeAdded(e));
} else { } else {
@@ -4529,12 +4551,12 @@ const AngularWorkflow = (defaultprops) => {
setSelectedApp({}); setSelectedApp({});
setSelectedComment({}) setSelectedComment({})
setSelectedEdge({}) setSelectedEdge({})
//setSelectedActionEnvironment({})
setTriggerAuthentication({}) setTriggerAuthentication({})
setLocalFirstrequest(true) setLocalFirstrequest(true)
setSelectedTrigger({}); setSelectedTrigger({});
setSelectedTriggerIndex(-1) setSelectedTriggerIndex(-1)
setSubworkflow({})
setUpdate(Math.random()) setUpdate(Math.random())
// Can be used for right side view // Can be used for right side view
@@ -4635,7 +4657,12 @@ const AngularWorkflow = (defaultprops) => {
.then(() => { .then(() => {
console.log("DONE: ", workflow_id); console.log("DONE: ", workflow_id);
getWorkflow(workflow_id.value, nodedata); getWorkflow(workflow_id.value, nodedata);
cy.fit(null, 300);
try {
cy.fit(null, 300);
} catch (e) {
console.log("Error in fitting (2): ", e)
}
}); });
} }
}; };
@@ -6856,10 +6883,15 @@ const AngularWorkflow = (defaultprops) => {
var found = false; var found = false;
for (let branchkey in workflow.branches) { for (let branchkey in workflow.branches) {
if (workflow.branches[branchkey].destination_id === edge.source && workflow.branches[branchkey].source_id === edge.target) { if (workflow.branches[branchkey].destination_id === edge.source && workflow.branches[branchkey].source_id === edge.target) {
toast("A branch in the opposite direction already exists")
event.target.remove() // Find the branch as well
found = true const foundbranch = cy.getElementById(workflow.branches[branchkey].id)
break if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) {
toast("A branch in the opposite direction already exists")
event.target.remove()
found = true
break
}
} }
if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) {
@@ -7657,6 +7689,10 @@ const AngularWorkflow = (defaultprops) => {
//if (nodedata.id === selectedAction.id || nodedata.id === selectedTrigger.id) { //if (nodedata.id === selectedAction.id || nodedata.id === selectedTrigger.id) {
// return // return
//} //}
//
if (nodedata.name === "switch" || nodedata.app_id === "shuffle_agent") {
return
}
var parsedStyle = { var parsedStyle = {
"border-width": "1px", "border-width": "1px",
@@ -7883,8 +7919,14 @@ const AngularWorkflow = (defaultprops) => {
var parentNode = cy.$("#" + event.target.data("id")); var parentNode = cy.$("#" + event.target.data("id"));
if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (parentNode.data("isButton") || parentNode.data("buttonId")) return;
const px = parentNode.position("x") - 65; var xDiff = 0
const py = parentNode.position("y") - 5; var yDiff = 0
if (parentNode.data("app_id") === "shuffle_agent") {
xDiff = 70
}
const px = parentNode.position("x") - 65 - xDiff
const py = parentNode.position("y") - 5 - yDiff
const circleId = (newNodeId = uuidv4()); const circleId = (newNodeId = uuidv4());
parentNode.data("circleId", circleId); parentNode.data("circleId", circleId);
@@ -8205,8 +8247,14 @@ const AngularWorkflow = (defaultprops) => {
var parentNode = cy.$("#" + event.target.data("id")); var parentNode = cy.$("#" + event.target.data("id"));
if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (parentNode.data("isButton") || parentNode.data("buttonId")) return;
const px = parentNode.position("x") + 100; var xDiff = 0
const py = parentNode.position("y") + 35; var yDiff = 0
if (parentNode.data("app_id") === "shuffle_agent") {
xDiff = 70
}
const px = parentNode.position("x") + 100 - xDiff;
const py = parentNode.position("y") + 35 - yDiff;
const circleId = (newNodeId = uuidv4()); const circleId = (newNodeId = uuidv4());
parentNode.data("circleId", circleId); parentNode.data("circleId", circleId);
@@ -8240,8 +8288,14 @@ const AngularWorkflow = (defaultprops) => {
var parentNode = cy.$("#" + event.target.data("id")); var parentNode = cy.$("#" + event.target.data("id"));
if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (parentNode.data("isButton") || parentNode.data("buttonId")) return;
const px = parentNode.position("x") - 65; var xDiff = 0
const py = parentNode.position("y") + 35; var yDiff = 0
if (parentNode.data("app_id") === "shuffle_agent") {
xDiff = 70
}
const px = parentNode.position("x") - 65 - xDiff;
const py = parentNode.position("y") + 35 - yDiff;
const circleId = (newNodeId = uuidv4()); const circleId = (newNodeId = uuidv4());
parentNode.data("circleId", circleId); parentNode.data("circleId", circleId);
@@ -8400,7 +8454,10 @@ const AngularWorkflow = (defaultprops) => {
} else { } else {
addCopyButton(event); addCopyButton(event);
addStartnodeButton(event);
if (nodedata.app_id !== "shuffle_agent") {
addStartnodeButton(event);
}
} }
// autocomplete // autocomplete
@@ -8416,7 +8473,7 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
if (nodedata.name === "switch") { if (nodedata.name === "switch" || nodedata.app_id === "shuffle_agent") {
return return
} }
@@ -9105,7 +9162,12 @@ const AngularWorkflow = (defaultprops) => {
// Reset view for cytoscape // Reset view for cytoscape
if (cy !== undefined && cy !== null) { if (cy !== undefined && cy !== null) {
cy.add(insertedNodes) cy.add(insertedNodes)
cy.fit(null, 250)
try {
cy.fit(null, 250)
} catch (error) {
console.log("Error fitting cytoscape (3): ", error)
}
} else { } else {
setElements(insertedNodes) setElements(insertedNodes)
} }
@@ -9314,7 +9376,9 @@ const AngularWorkflow = (defaultprops) => {
.then((responseJson) => { .then((responseJson) => {
if (responseJson === null) { if (responseJson === null) {
//console.log("No revisions found") //console.log("No revisions found")
return
//toast.warning("No revisions found")
return
} }
if (responseJson.success === false) { if (responseJson.success === false) {
@@ -9502,7 +9566,11 @@ const AngularWorkflow = (defaultprops) => {
} }
// preview: true, // preview: true,
cy.fit(null, 400) try {
cy.fit(null, 400)
} catch (error) {
console.log("Error fitting cytoscape (4): ", error)
}
cy.on("boxselect", "node", (e) => { cy.on("boxselect", "node", (e) => {
if (e.target.data("isButton") || e.target.data("isDescriptor") || e.target.data("isSuggestion")) { if (e.target.data("isButton") || e.target.data("isDescriptor") || e.target.data("isSuggestion")) {
@@ -12210,7 +12278,7 @@ const AngularWorkflow = (defaultprops) => {
// Add Shuffle DB with cache keys if available // Add Shuffle DB with cache keys if available
let cacheKey = { let cacheKey = {
type: "Shuffle DB", type: "Shuffle DB",
name: "Shuffle DB", name: "Shuffle Datastore",
value: "$shuffle_cache", value: "$shuffle_cache",
highlight: "shuffle_cache", highlight: "shuffle_cache",
autocomplete: "shuffle_cache", autocomplete: "shuffle_cache",
@@ -17643,7 +17711,7 @@ const AngularWorkflow = (defaultprops) => {
id="suborg-changer" id="suborg-changer"
style={{ color: "rgba(255,255,255,0.7)", }} style={{ color: "rgba(255,255,255,0.7)", }}
> >
Select an Org Select an Org ({originalWorkflow?.suborg_distribution?.length})
</InputLabel> </InputLabel>
<Select <Select
style={{ style={{
@@ -17662,12 +17730,12 @@ const AngularWorkflow = (defaultprops) => {
}} }}
labelId="suborg-changer" labelId="suborg-changer"
value={workflow.org_id} value={workflow.org_id}
disabled={savingState !== 0 || suborgWorkflows?.length === 0} disabled={savingState !== 0 || suborgWorkflows?.length === 0 || allTriggers === undefined}
onChange={(e) => { onChange={(e) => {
if (lastSaved === false && originalWorkflow.id === workflow.id) { if (lastSaved === false && originalWorkflow.id === workflow.id) {
setSuborgWorkflows([]) setSuborgWorkflows([])
saveWorkflow(workflow, undefined, undefined, e.target.value) saveWorkflow(workflow, undefined, undefined, e.target.value)
toast.warn("Saving workflow first due to detected changes. Please try to change workflow again when it is finished.", { toast.warn(`Saving workflow first due to detected changes. If more than 10 auth`, {
autoClose: 2000, autoClose: 2000,
}) })
return return
@@ -17693,6 +17761,7 @@ const AngularWorkflow = (defaultprops) => {
} }
ReactDOM.unstable_batchedUpdates(() => { ReactDOM.unstable_batchedUpdates(() => {
setAllTriggers(undefined)
setSelectedTriggerIndex(-1) setSelectedTriggerIndex(-1)
getEnvironments(e.target.value) getEnvironments(e.target.value)
getAppAuthentication(undefined, undefined, undefined, e.target.value) getAppAuthentication(undefined, undefined, undefined, e.target.value)
+1 -1
View File
@@ -663,7 +663,7 @@ const ApiExplorerWrapper = (props) => {
} }
} }
} else if (validate.result.status === 404) { } else if (validate.result.status === 404) {
toast.error("Page not found. Please try a different URL.") //toast.error("Page not found. Please try a different URL.")
} else if (validate.result.error !== undefined && validate.result.error !== null && validate.result.error.length > 0) { } else if (validate.result.error !== undefined && validate.result.error !== null && validate.result.error.length > 0) {
if (validate.result.error.toLowerCase().includes("max retries")) { if (validate.result.error.toLowerCase().includes("max retries")) {
toast.error("Are you sure the URL is correct? It seems like the server is not responding.") toast.error("Are you sure the URL is correct? It seems like the server is not responding.")
+1 -1
View File
@@ -281,7 +281,7 @@ export const appCategories = [
"name": "AI", "name": "AI",
"color": "#FFC107", "color": "#FFC107",
"icon": "AI", "icon": "AI",
"action_labels": ["Answer Question", "Run Action"], "action_labels": ["Answer Question", "Run Action", "Run LLM",],
}, },
{ {
"name": "Other", "name": "Other",
+1 -1
View File
@@ -1215,7 +1215,7 @@ const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{
minWidth: isMobile ? null : (isLoggedIn && isLoaded) ? leftSideBarOpenByClick ? 800 : 900 : null, margin: "auto", minWidth: isMobile ? null : (isLoggedIn && isLoaded) ? leftSideBarOpenByClick ? 800 : 900 : null, margin: "auto",
position: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? "relative" : "static", position: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? "relative" : "static",
left: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? 120 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0, left: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? 120 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0,
marginLeft: windowWidth < 1920 ? leftSideBarOpenByClick && (isLoggedIn && isLoaded) ? 160 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0 : "auto", width: "100%", marginLeft: windowWidth < 1920 ? leftSideBarOpenByClick && (isLoggedIn && isLoaded) ? 90 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0 : "auto", width: "100%",
transition: "left 0.3s ease-in-out, min-width 0.3s ease-in-out, max-width 0.3s ease-in-out, position 0.3s ease-in-out, margin 0.3s ease-in-out, margin-left 0.3s ease" transition: "left 0.3s ease-in-out, min-width 0.3s ease-in-out, max-width 0.3s ease-in-out, position 0.3s ease-in-out, margin 0.3s ease-in-out, margin-left 0.3s ease"
}}> }}>
{children} {children}
+191 -71
View File
@@ -23,6 +23,7 @@ import {
Chip, Chip,
Checkbox, Checkbox,
Fade, Fade,
Skeleton,
} from "@mui/material"; } from "@mui/material";
import { import {
@@ -140,12 +141,166 @@ const UsecaseListComponent = (props) => {
const [firstLoad, setFirstLoad] = useState(true) const [firstLoad, setFirstLoad] = useState(true)
const [apps, setApps] = useState([]) const [apps, setApps] = useState([])
const [autoOpenUsecase, setAutoOpenUsecase] = useState(null);
const classes = useStyles(); const classes = useStyles();
let navigate = useNavigate(); let navigate = useNavigate();
const [mitreTags, setMitreTags] = useState([]); const [mitreTags, setMitreTags] = useState([]);
// Add loading state
const [isLoading, setIsLoading] = useState(true);
// Add this useEffect to handle URL parameters on load
useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const selectedUsecase = params["selected_object"];
if (selectedUsecase && keys.length > 0) {
const usecaseName = selectedUsecase.toLowerCase().replaceAll("_", " ");
// Find the matching usecase in the keys
for (const category of keys) {
const foundUsecase = category.list.find(
usecase => usecase.name.toLowerCase().replaceAll("_", " ") === usecaseName
);
if (foundUsecase) {
setAutoOpenUsecase(foundUsecase);
// Wait for render then scroll
setTimeout(() => {
const element = document.getElementById(usecaseName);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center"
});
}
}, 1000);
break;
}
}
}
}, [keys]);
// Add useEffect to handle auto-opening
useEffect(() => {
if (autoOpenUsecase) {
getUsecase(autoOpenUsecase, 0, 0);
setAutoOpenUsecase(null);
}
}, [autoOpenUsecase]);
// Loading skeleton component
const LoadingSkeleton = () => (
<div style={{paddingTop: 75, minHeight: 1000, textAlign: "left"}}>
{/* Header skeleton */}
<Skeleton variant="text" width={200} height={40} sx={{ bgcolor: 'grey.800' }} />
<Skeleton variant="text" width="60%" height={24} sx={{ marginTop: 3, bgcolor: 'grey.800' }} />
{/* Apps selection skeleton */}
<Skeleton variant="text" width={150} height={24} sx={{ marginTop: 5, marginBottom: 10 }} />
<Paper style={{
height: 60,
width: "97.5%",
backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette?.borderRadius || 5,
display: "flex",
padding: "0 25px",
}}>
{/* App icons skeleton */}
<div style={{flex: 10, display: "flex", gap: 25, alignItems: "center"}}>
{[1, 2, 3, 4, 5].map((app) => (
<Skeleton
key={app}
variant="circular"
width={40}
height={40}
sx={{ bgcolor: 'grey.800' }}
/>
))}
</div>
{/* Add more apps button skeleton */}
<Skeleton
variant="rectangular"
width={150}
height={40}
sx={{
marginTop: "10px",
borderRadius: 20,
bgcolor: 'grey.800'
}}
/>
</Paper>
{/* Usecase categories skeleton - 3 sections: Collect, Enrich, Detect */}
{[1, 2, 3,4,5].map((category) => (
<div key={category} style={{marginTop: category === 1 ? 20: 45}}>
{/* Category title with color indicator */}
<Typography variant="body1" style={{marginBottom: 15}}>
<Skeleton
variant="text"
width={200}
height={24}
sx={{
bgcolor: category === 1 ? '#f85a3e33' :
category === 2 ? '#ffb00d33' :
category === 3 ? '#2196f333' :
category === 4 ? '#4caf5033' :
category === 5 ? '#9c27b033' : '#00000033'
}}
/>
</Typography>
<Grid container spacing={1}>
{[1, 2, 3].map((item) => (
<Grid item xs={isMobile ? 12 : 4} key={item}>
<Paper
style={{
backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette?.borderRadius || 5,
padding: "10px 20px",
height: 80,
display: "flex",
alignItems: "center",
gap: 15
}}
>
{/* App icons container */}
<div style={{display: "flex", gap: 5}}>
<Skeleton
variant="circular"
width={30}
height={30}
sx={{ bgcolor: 'grey.800' }}
/>
<Skeleton
variant="circular"
width={30}
height={30}
sx={{ bgcolor: 'grey.800' }}
/>
</div>
{/* Usecase title */}
<Skeleton
variant="text"
width="70%"
height={24}
sx={{ bgcolor: 'grey.800' }}
/>
</Paper>
</Grid>
))}
</Grid>
</div>
))}
</div>
);
const parseUsecase = (subcase, inputFramework) => { const parseUsecase = (subcase, inputFramework) => {
var useFramework = frameworkData var useFramework = frameworkData
@@ -187,6 +342,7 @@ const UsecaseListComponent = (props) => {
}, [frameworkData]) }, [frameworkData])
const loadApps = () => { const loadApps = () => {
setIsLoading(true);
fetch(`${globalUrl}/api/v1/apps`, { fetch(`${globalUrl}/api/v1/apps`, {
method: "GET", method: "GET",
headers: { headers: {
@@ -220,9 +376,11 @@ const UsecaseListComponent = (props) => {
} }
setApps(responseJson); setApps(responseJson);
setIsLoading(false);
}) })
.catch((error) => { .catch((error) => {
console.log("App loading error: " + error.toString()); console.log("App loading error: " + error.toString());
setIsLoading(false);
}) })
} }
@@ -231,15 +389,23 @@ const UsecaseListComponent = (props) => {
}, []) }, [])
if (keys === undefined || keys === null || keys.length === 0) { if (keys === undefined || keys === null || keys.length === 0) {
return null return <LoadingSkeleton />;
} }
// Timeout 50ms to delay it slightly // Timeout 50ms to delay it slightly
const getUsecase = (subcase, index, subindex) => { const getUsecase = (subcase, index, subindex) => {
subcase = parseUsecase(subcase) // Update URL with selected usecase
setPrevSubcase(subcase) const usecaseName = subcase.name.toLowerCase().replaceAll(" ", "_")
const newUrl = `?selected_object=${usecaseName}`
// Force URL update even if it's the same usecase
navigate(newUrl, { replace: true })
// Parse and fetch usecase data
subcase = parseUsecase(subcase)
setPrevSubcase(subcase)
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, { fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, {
method: "GET", method: "GET",
@@ -415,6 +581,19 @@ const UsecaseListComponent = (props) => {
} }
} }
// Then, add a cleanup function for URL params when drawer closes
const handleUsecaseClose = () => {
// Remove the selected_object parameter from URL
navigate("/usecases", { replace: true })
// Reset relevant state
setInputUsecase({})
setExpandedIndex(-1)
setExpandedItem(-1)
setFirstLoad(false)
setSelectedWorkflows([])
}
return ( return (
<div style={{paddingTop: 75, minHeight: 1000, textAlign: "left",}}> <div style={{paddingTop: 75, minHeight: 1000, textAlign: "left",}}>
<Typography variant="h4" style={{color: "white", }}> <Typography variant="h4" style={{color: "white", }}>
@@ -645,10 +824,10 @@ const UsecaseListComponent = (props) => {
return ( return (
<Grid id={fixedName} item xs={isMobile ? 12 : 4} key={subindex} style={{}} onClick={() => { <Grid id={fixedName} item xs={isMobile ? 12 : 4} key={subindex} style={{}} onClick={() => {
if (fixedName === "reporting") { // if (fixedName === "reporting") {
getUsecase(subcase, index, subindex) // getUsecase(subcase, index, subindex)
return // return
} // }
//setSelectedWorkflows([]) //setSelectedWorkflows([])
if (selectedItem) { if (selectedItem) {
@@ -678,6 +857,8 @@ const UsecaseListComponent = (props) => {
workflowBuilt={workflowBuilt} workflowBuilt={workflowBuilt}
inputWorkflowId={workflowBuilt} inputWorkflowId={workflowBuilt}
usecaseDetails={usecaseDetails} usecaseDetails={usecaseDetails}
isModalOpenDefault={autoOpenUsecase?.name === subcase.name}
onClose={handleUsecaseClose}
/> />
</Grid> </Grid>
@@ -717,6 +898,7 @@ const Usecases2 = (props) => {
window.location.host === "localhost:3002" || window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io"; window.location.host === "shuffler.io";
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (selectedUsecaseCategory.length === 0) { if (selectedUsecaseCategory.length === 0) {
@@ -729,71 +911,8 @@ const Usecases2 = (props) => {
} }
}, [selectedUsecaseCategory]) }, [selectedUsecaseCategory])
const checkSelectedParams = () => {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname;
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const foundQuery = params["selected"]
if (foundQuery !== null && foundQuery !== undefined) {
setSelectedUsecaseCategory(foundQuery)
const newitem = removeParam("selected", cursearch);
navigate(curpath + newitem)
}
const baseItem = document.getElementById("reporting")
if (baseItem !== undefined && baseItem !== null) {
baseItem.click()
// Find close window button -> go to top
const foundButton = document.getElementById("close_selection")
if (foundButton !== undefined && foundButton !== null) {
foundButton.click()
}
// Scroll back to top
window.scrollTo(0, 0)
}
const foundQuery2 = params["selected_object"]
if (foundQuery2 !== null && foundQuery2 !== undefined) {
// Take a random object, quickly click it, then go to this one
// Something is weird with loading apps without it
const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
// Waiting a bit for it to render
setTimeout(() => {
const foundItem = document.getElementById(queryName)
if (foundItem !== undefined && foundItem !== null) {
foundItem.click()
// Scroll to it
setTimeout(() => {
foundItem.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center"
})
}, 100)
} else {
//console.log("Couldn't find item with name ", queryName)
}
}, 1000)
}
}
useEffect(() => {
if (usecases.length > 0) {
//console.log(usecases)
checkSelectedParams()
}
}, [usecases])
const getFramework = () => { const getFramework = () => {
setIsLoading(true);
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET", method: "GET",
headers: { headers: {
@@ -831,6 +950,7 @@ const Usecases2 = (props) => {
}) })
.catch((error) => { .catch((error) => {
toast(error.toString()); toast(error.toString());
setIsLoading(false);
}) })
} }
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/docker/docker v27.5.0+incompatible github.com/docker/docker v27.5.0+incompatible
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.7.92 github.com/shuffle/shuffle-shared v0.7.95
k8s.io/api v0.30.2 k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2 k8s.io/apimachinery v0.30.2
) )
+1 -1
View File
@@ -8,7 +8,7 @@ require (
github.com/docker/docker v27.5.0+incompatible github.com/docker/docker v27.5.0+incompatible
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.7.92 github.com/shuffle/shuffle-shared v0.7.95
k8s.io/api v0.30.2 k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2 k8s.io/apimachinery v0.30.2
k8s.io/client-go v0.30.2 k8s.io/client-go v0.30.2