Bunch of fixes for MSSP feature buildout

This commit is contained in:
Frikky
2024-06-13 18:49:18 +02:00
parent 0455b59bd8
commit fe43b683b9
13 changed files with 2759 additions and 1372 deletions
+3
View File
@@ -134,6 +134,9 @@ const AppStats = (defaultprops) => {
Accept: "application/json", Accept: "application/json",
}, },
credentials: "include", credentials: "include",
}).catch((error) => {
console.log("Error getting workflow stats: " + error);
return workflow
}) })
if (response.status !== 200) { if (response.status !== 200) {
@@ -428,7 +428,6 @@ const ConfigureWorkflow = (props) => {
console.log("Found webhook: ", trigger) console.log("Found webhook: ", trigger)
if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") { if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") {
console.log("Actions: ", newactions)
const findapp = trigger.app_association.name.toLowerCase() const findapp = trigger.app_association.name.toLowerCase()
const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp) const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp)
@@ -449,9 +448,6 @@ const ConfigureWorkflow = (props) => {
newactions[foundindex].show_steps = true newactions[foundindex].show_steps = true
console.log("CHANGED ACTION: ", newactions[foundindex])
//console.log("Index: ", newactions[foundindex])
continue continue
} }
} }
@@ -1321,7 +1317,6 @@ const ConfigureWorkflow = (props) => {
} }
if (step.type === "authenticate") { if (step.type === "authenticate") {
console.log("AUTH STEP: ", step)
if (data.must_authenticate === true ) { if (data.must_authenticate === true ) {
filled = false filled = false
} else { } else {
+172 -13
View File
@@ -78,7 +78,6 @@ const EditWorkflow = (props) => {
const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") 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 [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day'))
console.log("WORKFLOW: ", workflow)
const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : [])
const classes = useStyles(); const classes = useStyles();
@@ -235,7 +234,7 @@ const EditWorkflow = (props) => {
</div> </div>
</DialogTitle> </DialogTitle>
<FormControl> <FormControl>
<div style={{width: 600, position: "fixed", left: 0, bottom: 0, zIndex: 1002, backgroundColor: "rgba(53,53,53,1)", height: 75, paddingTop: 20, paddingLeft: 75, }}> <div style={{borderTop: "1px solid rgba(255,255,255,0.5)", width: 600, position: "fixed", left: 0, bottom: 0, zIndex: 1002, backgroundColor: "rgba(53,53,53,1)", height: 75, paddingTop: 20, paddingLeft: 75, }}>
{/* {/*
<Button <Button
style={{}} style={{}}
@@ -315,7 +314,7 @@ const EditWorkflow = (props) => {
}} }}
color="primary" color="primary"
> >
{submitLoading ? <CircularProgress color="secondary" /> : "Done"} {submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"}
</Button> </Button>
</div> </div>
@@ -565,14 +564,22 @@ const EditWorkflow = (props) => {
fullWidth fullWidth
/> />
<Typography variant="body2" style={{marginTop: 50, }}> <Typography variant="body1" style={{marginTop: 50, }}>
MSSP Suborg Distribution (beta - contact support@shuffler.io) MSSP Suborg Distribution (beta - contact support@shuffler.io for more info)
</Typography> </Typography>
{userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ?
userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ?
<Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}> userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ?
You can only distribute to suborgs from a parent org. <Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}>
</Typography> Your organization does not have any suborgs yet. Please <a href="/admin?tab=suborgs" style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">make one</a>, then try again.
</Typography>
:
<Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}>
{innerWorkflow.parentorg_workflow !== undefined && innerWorkflow.parentorg_workflow !== null && innerWorkflow.parentorg_workflow.length > 0 ? <span>This workflow is distributed from <a href={`/workflows/${innerWorkflow.parentorg_workflow}`} style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">your parent workflow</a> (you may not have access).</span> : null}
<br />
<br />
You can only distribute to suborgs from a parent org.
</Typography>
: :
<Select <Select
multiple multiple
@@ -584,8 +591,6 @@ const EditWorkflow = (props) => {
newvalue = newvalue.filter(value => value !== "none") newvalue = newvalue.filter(value => value !== "none")
} }
console.log("NEWVALUE: ", newvalue)
if (newvalue.includes("none")) { if (newvalue.includes("none")) {
newvalue = ["none"] newvalue = ["none"]
} else if (newvalue.includes("all")) { } else if (newvalue.includes("all")) {
@@ -775,18 +780,172 @@ const EditWorkflow = (props) => {
> >
<AddIcon style={{}} /> <AddIcon style={{}} />
</Button> </Button>
<Typography variant="body1" style={{marginTop: 50, }}>
Git Backup Repository
</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. <b>The repository and branch must already have been initialized</b>. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides the <a href="/admin?admin_tab=organization" style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">default backup repository</a> your org has chosen. All fields must be filled in for the backup to work.
<br />
PS: This is a beta feature, and might not work as expected. Credentials are NOT encrypted.
</Typography>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography>Workflow Backup Repository</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
}}
fullWidth={true}
type="name"
multiline={true}
rows={1}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="github/com/shuffle/workflowbackup "
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_repo === undefined || innerWorkflow.backup_config.upload_repo === null || innerWorkflow.backup_config.upload_repo === "" ? "" : innerWorkflow.backup_config.upload_repo}
onChange={(e) => {
//setUploadRepo(e.target.value);
innerWorkflow.backup_config.upload_repo = e.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>Branch</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={1}
placeholder="The branch to use"
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_branch === undefined || innerWorkflow.backup_config.upload_branch === null || innerWorkflow.backup_config.upload_branch === "" ? "" : innerWorkflow.backup_config.upload_branch}
onChange={(e) => {
innerWorkflow.backup_config.upload_branch = e.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
</Grid>
<Grid container style={{ }} spacing={2}>
<Grid item xs={6} style={{}}>
<span>
<Typography>Username</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
}}
fullWidth={true}
type="name"
multiline={true}
rows={1}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The username to use"
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_username === undefined || innerWorkflow.backup_config.upload_username === null || innerWorkflow.backup_config.upload_username === "" ? "" : innerWorkflow.backup_config.upload_username}
onChange={(e) => {
innerWorkflow.backup_config.upload_username = e.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>Git token/password</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
}}
fullWidth={true}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={1}
placeholder="The API token to use"
defaultValue={innerWorkflow.backup_config === undefined || innerWorkflow.backup_config.upload_token === undefined || innerWorkflow.backup_config.upload_token === null || innerWorkflow.backup_config.upload_token === "" ? "" : innerWorkflow.backup_config.upload_token}
onChange={(e) => {
innerWorkflow.backup_config.upload_token = e.target.value
setInnerWorkflow(innerWorkflow)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
type="password"
/>
</span>
</Grid>
</Grid>
</span> </span>
: null} : null}
<Tooltip color="primary" title={"Add more details"} placement="top"> <Tooltip color="primary" title={"Add more details"} placement="top">
<IconButton <Button
style={{ color: "white", margin: "auto", textAlign: "center", width: 50, marginTop: 50, }} style={{ margin: "auto", marginTop: 50, textAlign: "center", }}
variant="outlined"
onClick={() => { onClick={() => {
setShowMoreClicked(!showMoreClicked); setShowMoreClicked(!showMoreClicked);
}} }}
> >
{showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon/>} {showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon/>}
</IconButton> {showMoreClicked ? "Collapse": "Expand"}
</Button>
</Tooltip> </Tooltip>
</div> </div>
+16 -7
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import theme from "../theme.jsx"; import theme from "../theme.jsx";
import { BrowserView, MobileView } from "react-device-detect"; import { BrowserView, MobileView } from "react-device-detect";
@@ -84,6 +84,15 @@ const Header = (props) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
useEffect(() => {
const topbar = localStorage.getItem("topbar_closed")
if (topbar === "true") {
setShowTopbar(false)
} else {
setShowTopbar(true)
}
}, [])
const handleClose = () => { const handleClose = () => {
setAnchorEl(null); setAnchorEl(null);
setAnchorElAvatar(null); setAnchorElAvatar(null);
@@ -1590,17 +1599,17 @@ const Header = (props) => {
navigate("/training") navigate("/training")
//if (window.drift !== undefined) {
// window.drift.api.startInteraction({ interactionId: 341911 })
//} else {
// console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift)
//}
}} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}> }} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}>
Public Training! Public Training!
</span> </span>
</u> </u>
</Typography> </Typography>
<IconButton color="secondary" style={{ position: "absolute", top: -3, right: 20, }} onClick={(event) => { setShowTopbar(false) }}> <IconButton color="secondary" style={{ position: "absolute", top: -3, right: 20, }} onClick={(event) => {
setShowTopbar(false)
// Set storage that it's clicked
localStorage.setItem("topbar_closed", "true")
}}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
</div> </div>
File diff suppressed because it is too large Load Diff
+105 -1
View File
@@ -167,6 +167,7 @@ const ParsedAction = (props) => {
listCache, listCache,
authGroups,
apps, apps,
setEditorData, setEditorData,
setcodedata, setcodedata,
@@ -1123,6 +1124,65 @@ const ParsedAction = (props) => {
return helperText return helperText
} }
const analyzeFields = () => {
if (selectedAction === undefined || selectedAction === null) {
return null
}
if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) {
return null
}
// Only shuffle tools for now
if (selectedAction.app_name !== "Shuffle Tools") {
return null
}
// Custom rules
if (selectedAction.name === "set_cache_value") {
var actionKey = ""
var actionValue = ""
for (let [key,keyval] in Object.entries(selectedActionParameters)) {
const param = selectedActionParameters[key]
if (param.name === "key") {
actionKey = param.value
}
if (param.name === "value") {
actionValue = param.value
}
}
if (actionKey === "" || actionValue === "") {
return null
}
if (!actionKey.includes(".#") && actionValue.includes(".#")) {
return <span>When the key ({actionKey}) is static, but the value is a list ({actionValue}), it will overwrite the list. You may be looking for the <span onClick={() => {}} style={{cursor: "pointer", color: "#f85a3e", }}>Check Cache Contains</span> action instead.</span>
}
}
return null
}
const suggestionInfo = () => {
const suggestionText = analyzeFields()
if (suggestionText === undefined || suggestionText === null) {
return null
}
if (selectedAction.errors === undefined || selectedAction.errors === null || selectedAction.errors.length === 0) {
selectedAction.errors = ["Suggestion: " + suggestionText]
}
return <Paper style={{padding: 10, backgroundColor: theme.palette.surfaceColor, border: "1px solid red",}}>
<Typography variant="body" style={{color: "white", }}>
<b>Tip:</b> {suggestionText}
</Typography>
</Paper>
}
// FIXME: Issue #40 - selectedActionParameters not reset // FIXME: Issue #40 - selectedActionParameters not reset
if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) {
@@ -1310,6 +1370,7 @@ const ParsedAction = (props) => {
</Button> </Button>
</Tooltip> </Tooltip>
} }
{selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ?
<div> <div>
<Typography variant="body1"> <Typography variant="body1">
@@ -1441,6 +1502,8 @@ const ParsedAction = (props) => {
</div> </div>
) : null} ) : null}
{suggestionInfo()}
{selectedActionParameters.map((data, count) => { {selectedActionParameters.map((data, count) => {
if (data.variant === "") { if (data.variant === "") {
data.variant = "STATIC_VALUE"; data.variant = "STATIC_VALUE";
@@ -3606,6 +3669,7 @@ const ParsedAction = (props) => {
}} }}
labelId="select-app-auth" labelId="select-app-auth"
value={ value={
selectedAction.authentication_id === "authgroups" ? "authgroups" :
Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length === 0 Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length === 0
? "No selection" ? "No selection"
: selectedAction.selectedAuthentication : selectedAction.selectedAuthentication
@@ -3617,6 +3681,8 @@ const ParsedAction = (props) => {
}} }}
fullWidth fullWidth
onChange={(e) => { onChange={(e) => {
console.log("AUTH CHANGE: ", e.target.value)
if (e.target.value === "No selection") { if (e.target.value === "No selection") {
selectedAction.selectedAuthentication = {}; selectedAction.selectedAuthentication = {};
selectedAction.authentication_id = ""; selectedAction.authentication_id = "";
@@ -3628,13 +3694,36 @@ const ParsedAction = (props) => {
} }
} }
setSelectedAction(selectedAction); setSelectedAction(selectedAction);
setUpdate(Math.random()); setUpdate(Math.random())
} else if (e.target.value === "authgroups") {
if (authGroups !== undefined && authGroups !== null && authGroups.length === 0) {
toast("No auth groups created. Opening window to create one")
setTimeout(() => {
window.open("/admin?tab=app_auth", "_blank")
}, 2500)
} else {
selectedAction.selectedAuthentication = {};
selectedAction.authentication_id = "authgroups"
for (let [key,keyval] in Object.entries(selectedAction.parameters)) {
//console.log(selectedAction.parameters[key])
if (selectedAction.parameters[key].configuration) {
selectedAction.parameters[key].value = "authgroup controlled"
}
}
setSelectedAction(selectedAction)
setUpdate(Math.random())
}
} else { } else {
selectedAction.selectedAuthentication = e.target.value; selectedAction.selectedAuthentication = e.target.value;
selectedAction.authentication_id = e.target.value.id; selectedAction.authentication_id = e.target.value.id;
setSelectedAction(selectedAction); setSelectedAction(selectedAction);
setUpdate(Math.random()); setUpdate(Math.random());
} }
}} }}
style={{ style={{
backgroundColor: theme.palette.inputColor, backgroundColor: theme.palette.inputColor,
@@ -3653,6 +3742,7 @@ const ParsedAction = (props) => {
> >
<em>No selection</em> <em>No selection</em>
</MenuItem> </MenuItem>
{selectedAction.authentication.map((data) => { {selectedAction.authentication.map((data) => {
if (data.last_modified === true) { if (data.last_modified === true) {
//console.log("LAST MODIFIED: ", data.label) //console.log("LAST MODIFIED: ", data.label)
@@ -3689,6 +3779,20 @@ const ParsedAction = (props) => {
</MenuItem> </MenuItem>
); );
})} })}
<Divider style={{marginTop: 10, marginBottom: 10, }}/>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value="authgroups"
>
<em>Auth Groups</em>
</MenuItem>
</Select> </Select>
{/* {/*
@@ -627,8 +627,8 @@ const CodeEditor = (props) => {
newMarkers.push({ newMarkers.push({
startRow: i, startRow: i,
startCol: startCh, startCol: startCh,
endRow: i+1, endRow: i,
endCol: endCh+1, endCol: endCh,
className: correctVariable ? "good-marker" : "bad-marker", className: correctVariable ? "good-marker" : "bad-marker",
type: "text", type: "text",
}) })
+5
View File
@@ -133,6 +133,11 @@ const data = [
"background-gradient-stop-colors": "data(fillGradient)", "background-gradient-stop-colors": "data(fillGradient)",
}, },
}, },
{
selector: `node[?parent_controlled]`,
css: {
},
},
{ {
selector: `node[app_name="Testing"]`, selector: `node[app_name="Testing"]`,
css: { css: {
+296 -163
View File
@@ -166,7 +166,9 @@ const Admin = (props) => {
//console.log("Selected: ", selectedOrganization) //console.log("Selected: ", selectedOrganization)
const [appAuthenticationGroupModalOpen , setAppAuthenticationGroupModalOpen] = React.useState(false); const [appAuthenticationGroupModalOpen , setAppAuthenticationGroupModalOpen] = React.useState(false);
const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]);
const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState("");
const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState("");
const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState("");
const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState("");
const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]);
const [organizationFeatures, setOrganizationFeatures] = React.useState({}); const [organizationFeatures, setOrganizationFeatures] = React.useState({});
@@ -431,23 +433,62 @@ const Admin = (props) => {
}); });
}; };
const createAppAuthenticationGroup = (name, description, appAuthIds) => { const deleteAppAuthenticationGroup = (appAuthGroupId) => {
const url = `${globalUrl}/api/v1/authentication/group/${appAuthGroupId}`
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for deleting app auth group");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
toast("Failed to delete app authentication group");
} else {
toast("App authentication group deleted")
getAppAuthenticationGroups()
}
})
.catch((error) => {
toast(error.toString())
})
}
const createAppAuthenticationGroup = (name, environment, description, appAuthIds) => {
// Makes list of ids into a full-on list of auth, but just with the ID
// The backend fills in the rest
console.log("INput auth: ", appAuthIds)
let app_auths = appAuthIds.map((appAuthId) => { let app_auths = appAuthIds.map((appAuthId) => {
return { id: appAuthId }; return { id: appAuthId };
}) })
fetch(globalUrl + "/api/v1/apps/authentication/group", { var parsedAppGroup = {
label: name,
environment: environment,
description: description,
app_auths: app_auths
}
if (appAuthenticationGroupId !== undefined && appAuthenticationGroupId !== null && appAuthenticationGroupId !== "") {
parsedAppGroup.id = appAuthenticationGroupId
}
fetch(globalUrl + "/api/v1/authentication/group", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
credentials: "include", credentials: "include",
body: JSON.stringify({ body: JSON.stringify(parsedAppGroup),
label: name,
description: description,
app_auths: app_auths
}),
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
@@ -457,8 +498,15 @@ const Admin = (props) => {
return response.json(); return response.json();
}) })
.then((responseJson) => { .then((responseJson) => {
// getAppAuthenticationGroups(); if (responseJson.success === false) {
toast("App authentication group created"); toast("Failed to create. Please try again, or contact support@shuffler.io")
} else {
// Close the modal
setAppAuthenticationGroupModalOpen(false)
toast("App authentication group created")
getAppAuthenticationGroups()
}
}) })
.catch((error) => { .catch((error) => {
toast(error.toString()); toast(error.toString());
@@ -805,7 +853,7 @@ If you're interested, please let me know a time that works for you, or set up a
.then((responseJson) => { .then((responseJson) => {
setWebHooks(responseJson.webhooks || []); // Handling the case where the result is null or undefined setWebHooks(responseJson.webhooks || []); // Handling the case where the result is null or undefined
setAllSchedules(responseJson.schedules || []); setAllSchedules(responseJson.schedules || []);
setPipelines(responseJson.pipelines || []); // setPipelines(responseJson.pipelines || []);
}) })
.catch((error) => { .catch((error) => {
// toast(error.toString()); // toast(error.toString());
@@ -990,13 +1038,11 @@ If you're interested, please let me know a time that works for you, or set up a
} }
const data = { const data = {
command: pipeline.command,
name: pipeline.name, name: pipeline.name,
type: state, type: state,
environment: pipeline.environment, environment: pipeline.environment,
workflow_id: pipeline.workflow_id, workflow_id: pipeline.workflow_id,
trigger_id: pipeline.trigger_id, trigger_id: pipeline.trigger_id,
start_node: pipeline.start_node,
}; };
if (state === "start") toast("starting the pipeline"); if (state === "start") toast("starting the pipeline");
@@ -1027,7 +1073,6 @@ If you're interested, please let me know a time that works for you, or set up a
if (state === "start") toast("Successfully created pipeline"); if (state === "start") toast("Successfully created pipeline");
else toast("Sucessfully stopped the pipeline"); else toast("Sucessfully stopped the pipeline");
} }
setTimeout(handleGetAllTriggers, 1000);
}) })
.catch((error) => { .catch((error) => {
//toast(error.toString()); //toast(error.toString());
@@ -2093,10 +2138,10 @@ If you're interested, please let me know a time that works for you, or set up a
}; };
const getAppAuthenticationGroups = () => { const getAppAuthenticationGroups = () => {
console.log("DEBUG: Skipping app auth group loading") //console.log("DEBUG: Skipping app auth group loading")
return //return
fetch(globalUrl + "/api/v1/apps/authentication/group", { fetch(globalUrl + "/api/v1/authentication/group", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -4941,7 +4986,7 @@ If you're interested, please let me know a time that works for you, or set up a
<ListItemText> <ListItemText>
<Button <Button
style={{ marginLeft: "140px" }} style={{ marginLeft: "18%" }}
variant={ variant={
webhook.status === "running" ? "contained" : "outlined" webhook.status === "running" ? "contained" : "outlined"
} }
@@ -4963,10 +5008,10 @@ If you're interested, please let me know a time that works for you, or set up a
</List> </List>
)} )}
<div style={{ marginTop: 20, marginBottom: 20 }}> {/* <div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Tenzir Pipelines</h2> <h2 style={{ display: "inline" }}>Tenzir Pipelines</h2>
<span style={{ marginLeft: 25 }}> <span style={{ marginLeft: 25 }}>
Controls the Tenzir pipeline operations.{" "} Controls a pipeline to run things.{" "}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -5013,7 +5058,7 @@ If you're interested, please let me know a time that works for you, or set up a
primary="Workflow" primary="Workflow"
style={{ maxWidth: 315, minWidth: 315 }} style={{ maxWidth: 315, minWidth: 315 }}
/> />
<ListItemText primary="Actions" style={{ marginLeft: '120px' }} /> <ListItemText primary="Actions" />
</ListItem> </ListItem>
{pipelines.map((pipeline, index) => { {pipelines.map((pipeline, index) => {
var bgColor = "#27292d"; var bgColor = "#27292d";
@@ -5066,7 +5111,7 @@ If you're interested, please let me know a time that works for you, or set up a
); );
})} })}
</List> </List>
)} )}*/}
</div> </div>
) : null; ) : null;
@@ -5170,31 +5215,63 @@ If you're interested, please let me know a time that works for you, or set up a
const handleAppAuthGroupCheckbox = (data) => { const handleAppAuthGroupCheckbox = (data) => {
let appOrginal = data.app //let groupApp = data.app.id
if (appsForAppAuthGroup.includes(appOrginal.id)) { var newappauth = appsForAppAuthGroup
return; if (appsForAppAuthGroup.includes(data.app.id)) {
} newappauth = newappauth.filter((item) => item !== data.app.id)
}
setAppsForAppAuthGroup([...appsForAppAuthGroup, data.id]); if (appsForAppAuthGroup.includes(data.id)) {
console.log("Apps for app auth group: ", appsForAppAuthGroup); // Remove app from app auth group
}; newappauth = newappauth.filter((item) => item !== data.id)
setAppsForAppAuthGroup(newappauth)
return
}
for (var i = 0; i < authentication.length; i++) {
if (authentication[i].id === data.id) {
continue
}
if (!appsForAppAuthGroup.includes(authentication[i].id)) {
continue
}
if (authentication[i].app.id === data.app.id) {
// Remove app from app auth group
newappauth = newappauth.filter((item) => item !== authentication[i].id)
toast(`App ${data.app.name} is already in this group`)
}
}
setAppsForAppAuthGroup(newappauth.concat(data.id))
}
const authenticationView = const authenticationView =
curTab === 2 ? ( curTab === 2 ? (
<>
<div>
{/* (appAuthenticationGroupModalOpen : { */} {/* (appAuthenticationGroupModalOpen : { */}
{appAuthenticationGroupModalOpen && ( {appAuthenticationGroupModalOpen && (
<Dialog <Dialog
open={appAuthenticationGroupModalOpen} open={appAuthenticationGroupModalOpen}
onClose={() => { onClose={() => {
setAppAuthenticationGroupModalOpen(false); setAppAuthenticationGroupModalOpen(false);
setAppAuthenticationGroupId("")
setAppAuthenticationGroupName("")
setAppAuthenticationGroupEnvironment("")
setAppAuthenticationGroupDescription("")
setAppsForAppAuthGroup([])
}} }}
PaperProps={{ PaperProps={{
style: { style: {
backgroundColor: theme.palette.surfaceColor, backgroundColor: theme.palette.surfaceColor,
color: "white", color: "white",
minWidth: "1200px", minWidth: "1000px",
minHeight: "320px", minHeight: "320px",
padding: 25,
paddingLeft: 50,
}, },
}} }}
> >
@@ -5202,106 +5279,130 @@ If you're interested, please let me know a time that works for you, or set up a
<span style={{ color: "white" }}>App Authentication Groups</span> <span style={{ color: "white" }}>App Authentication Groups</span>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent style={{marginLeft: 0, paddingLeft: 0, }}>
<div> <div style={{display: "flex", position: "sticky", top: 0, zIndex: 1, backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette.borderRadius, padding: 20, marginBottom: 10, }}>
<TextField <div style={{marginRight: 50, minWidth: 250, }}>
color="primary" <Typography style={{marginTop: 10, }}>
style={{ backgroundColor: theme.palette.inputColor }} Name
autoFocus </Typography>
InputProps={{ <TextField
style: { color="primary"
height: "50px", label="Name"
color: "white", style={{ backgroundColor: theme.palette.inputColor }}
fontSize: "1em", autoFocus
}, InputProps={{
}} style: {
required height: "50px",
fullWidth={true} color: "white",
placeholder="Name" fontSize: "1em",
id="namefield" },
margin="normal" }}
variant="outlined" required
onChange={(event) => { fullWidth={true}
setAppAuthenticationGroupName(event.target.value); placeholder="Name"
}} id="namefield"
/> margin="normal"
variant="outlined"
defaultValue={appAuthenticationGroupName}
onChange={(event) => {
setAppAuthenticationGroupName(event.target.value);
}}
/>
</div>
<div style={{marginRight: 50, }}>
<Typography style={{marginTop: 10, marginBottom: 10}}>
Evironment
</Typography>
{environments !== undefined && environments !== null && environments.length > 0 ?
<Select
defaultValue={appAuthenticationGroupEnvironment === "" ? environments[0].Name : appAuthenticationGroupEnvironment}
onChange={(e) => {
setAppAuthenticationGroupEnvironment(e.target.value);
}}
>
{environments.map((env, index) => {
return (
<MenuItem key={index} value={env.Name}>
{env.Name}
</MenuItem>
)
})}
</Select>
:
<Typography>
Environments failed to load. Please try again
</Typography>
}
</div> </div>
<div> <div style={{marginTop: 65, }}>
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
placeholder="Description"
id="descriptionfield"
margin="normal"
variant="outlined"
onChange={(event) => {
setAppAuthenticationGroupDescription(event.target.value);
}}
/>
</div>
<div>
{/* Show a check box list of all app authentications to add to the auth group */}
<div>
{authentication.map((data, index) => (
<div key={index}>
<FormControlLabel
control={
<Tooltip
title={data.app.name}
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '10px', marginLeft: '5px' }}>
<img
src={data.app.large_image ? data.app.large_image : '/images/no_image.png'}
alt=""
style={{ width: '50px', height: '50px', marginRight: '10px' }}
/>
<Checkbox
checked={data.checked}
onChange={(event) => {
handleAppAuthGroupCheckbox(data)
}}
name={data.label}
disabled={data.app.id in appsForAppAuthGroup}
/>
</div>
</Tooltip>
}
label={data.label}
/>
</div>
))}
</div>
</div>
<div>
<Button <Button
style={{}} style={{}}
disabled={appAuthenticationGroupName === "" || appAuthenticationGroupEnvironment === "" || appsForAppAuthGroup.length === 0}
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => { onClick={() => {
createAppAuthenticationGroup( createAppAuthenticationGroup(
appAuthenticationGroupName, appAuthenticationGroupName,
appAuthenticationGroupEnvironment,
appAuthenticationGroupDescription, appAuthenticationGroupDescription,
appsForAppAuthGroup appsForAppAuthGroup,
); );
}} }}
> >
Create Set Group
</Button> </Button>
</div> </div>
</div>
<Divider style={{marginTop: 10, marginBottom: 10, }}/>
<div style={{marginLeft: 25, }}>
{/* Show a check box list of all app authentications to add to the auth group */}
<div>
{authentication.map((data, index) => {
var checked = data.checked
if (checked === undefined || checked === null) {
checked = false
}
if (appsForAppAuthGroup.includes(data.id)) {
checked = true
}
return (
<div key={index}>
<FormControlLabel
control={
<Tooltip
title={data.app.name}
placement="left"
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '10px', marginLeft: '5px', }}>
<img
src={data.app.large_image ? data.app.large_image : '/images/no_image.png'}
alt=""
style={{ borderRadius: theme.palette.borderRadius, width: 50, height: 50, marginRight: 10 }}
/>
<Checkbox
checked={checked}
onChange={(event) => {
handleAppAuthGroupCheckbox(data)
}}
name={data.label}
disabled={data.app.id in appsForAppAuthGroup}
/>
</div>
</Tooltip>
}
label={data.label}
/>
</div>
)
})}
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
@@ -5311,7 +5412,7 @@ If you're interested, please let me know a time that works for you, or set up a
<div style={{ marginTop: 20, marginBottom: 20 }}> <div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>App Authentication</h2> <h2 style={{ display: "inline" }}>App Authentication</h2>
<span style={{ marginLeft: 25 }}> <span style={{ marginLeft: 25 }}>
Control the authentication options for individual apps. Control the authentication options for individual apps. App Groups are farther down on this page.
</span> </span>
&nbsp; &nbsp;
<a <a
@@ -5587,10 +5688,18 @@ If you're interested, please let me know a time that works for you, or set up a
</List> </List>
</div> </div>
{/* <div> <Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<div style={{marginTop: 50, }}>
<div style={{ marginTop: 20, marginBottom: 20 }}> <div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>App Authentication Groups</h2> <h2 style={{ }}>App Authentication Groups</h2>
<span style={{ marginLeft: 25 }}> <span style={{ marginLeft: 0 }}>
Groups of authentication options for subflows.{" "} Groups of authentication options for subflows.{" "}
<a <a
target="_blank" target="_blank"
@@ -5602,29 +5711,39 @@ If you're interested, please let me know a time that works for you, or set up a
</a> </a>
</span> </span>
<Divider <br />
style={{ <Button
marginTop: 20, style={{ marginTop: 20 }}
marginBottom: 20, variant="contained"
backgroundColor: theme.palette.inputColor, color="primary"
onClick={() => {
if (environments !== undefined && environments !== null && environments.length > 0) {
setAppAuthenticationGroupEnvironment(environments[0].Name)
}
setAppAuthenticationGroupModalOpen(true)
}} }}
/> >
<List> Add Group
</Button>
<List style={{marginTop: 25, }}>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary="Label" primary="Label"
style={{ minWidth: 250, maxWidth: 250}}
/>
<ListItemText
primary="Environment"
style={{ minWidth: 150, maxWidth: 150 }} style={{ minWidth: 150, maxWidth: 150 }}
/> />
<ListItemText <ListItemText
primary="Description" primary="App Auth"
style={{ minWidth: 250, maxWidth: 250 }} style={{ minWidth: 250, maxWidth: 250 }}
/> />
<ListItemText <ListItemText
primary="Apps" primary="Created At"
style={{ minWidth: 250, maxWidth: 250 }}
/>
<ListItemText
primary="CreatedAt"
style={{ minWidth: 150, maxWidth: 150 }} style={{ minWidth: 150, maxWidth: 150 }}
/> />
<ListItemText <ListItemText
@@ -5638,31 +5757,49 @@ If you're interested, please let me know a time that works for you, or set up a
if (index % 2 === 0) { if (index % 2 === 0) {
bgColor = "#1f2023"; bgColor = "#1f2023";
} }
if (data.app_auths === undefined || data.app_auths === null) {
data.app_auths = []
}
return ( return (
<ListItem key={index} style={{ backgroundColor: bgColor }}> <ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText <ListItemText
primary={data.label} primary={data.label}
style={{ minWidth: 150, maxWidth: 150 }} style={{ minWidth: 250, maxWidth: 250, }}
/> />
<ListItemText <ListItemText
primary={data.description} primary={data.environment}
style={{ minWidth: 250, maxWidth: 250 }} style={{ minWidth: 150, maxWidth: 150, }}
/> />
<ListItemText <ListItemText
primary={ primary={
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
{data.app_auths.map((appAuth, index) => ( {data.app_auths.map((appAuth, index) => {
<Tooltip if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") {
title={appAuth.app.name} const foundImage = authentication.find((auth) => auth.app.id === appAuth.app.id)
> if (foundImage !== undefined) {
<img appAuth.app.large_image = foundImage.app.large_image
key={index}
src={appAuth.app.large_image} appAuth.app.name = foundImage.app.name
alt={appAuth.app.name} }
style={{ width: '24px', height: '24px', marginRight: '5px' }} }
/>
</Tooltip> const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})`
))}
return (
<Tooltip
title={tooltip}
>
<img
key={index}
src={appAuth.app.large_image}
alt={appAuth.app.name}
style={{ width: 30, height: 30, marginRight: 5 }}
/>
</Tooltip>
)
})}
</div> </div>
} }
style={{ minWidth: 250, maxWidth: 250 }} style={{ minWidth: 250, maxWidth: 250 }}
@@ -5676,16 +5813,22 @@ If you're interested, please let me know a time that works for you, or set up a
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<IconButton <IconButton
onClick={() => { onClick={() => {
setAppAuthenticationGroupId(data.id)
setAppAuthenticationGroupName(data.label)
setAppAuthenticationGroupDescription(data.description)
setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id))
setAppAuthenticationGroupEnvironment(data.environment)
setAppAuthenticationGroupModalOpen(true)
}} }}
disabled={true}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => { onClick={() => {
// deleteAppAuthenticationGroup(data); deleteAppAuthenticationGroup(data.id)
}} }}
disabled={true}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@@ -5700,20 +5843,10 @@ If you're interested, please let me know a time that works for you, or set up a
)} )}
</List> </List>
<Button
style={{ marginLeft: 10 }}
variant="contained"
color="primary"
onClick={() => {
setAppAuthenticationGroupModalOpen(true);
}}
>
Add Group
</Button>
</div> </div>
</div> */} </div>
</> </div>
) : null; ) : null;
const getLogs = async (ip, userId) => { const getLogs = async (ip, userId) => {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -46,6 +46,7 @@ const Body = {
height: "100%", height: "100%",
color: "white", color: "white",
position: "relative", position: "relative",
paddingTop: 40,
//textAlign: "center", //textAlign: "center",
}; };
+154 -70
View File
@@ -74,6 +74,7 @@ import {
ArrowLeft as ArrowLeftIcon, ArrowLeft as ArrowLeftIcon,
ArrowRight as ArrowRightIcon, ArrowRight as ArrowRightIcon,
QueryStats as QueryStatsIcon, QueryStats as QueryStatsIcon,
Visibility as VisibilityIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { DataGrid, GridToolbar } from "@mui/x-data-grid"; import { DataGrid, GridToolbar } from "@mui/x-data-grid";
@@ -614,6 +615,7 @@ const Workflows = (props) => {
const [videoViewOpen, setVideoViewOpen] = React.useState(false) const [videoViewOpen, setVideoViewOpen] = React.useState(false)
const [gettingStartedItems, setGettingStartedItems] = React.useState([]) const [gettingStartedItems, setGettingStartedItems] = React.useState([])
const [selectedWorkflowIndexes, setSelectedWorkflowIndexes] = React.useState([]) const [selectedWorkflowIndexes, setSelectedWorkflowIndexes] = React.useState([])
const [highlightIds, setHighlightIds] = React.useState([])
const [apps, setApps] = React.useState([]); const [apps, setApps] = React.useState([]);
@@ -1213,7 +1215,24 @@ const Workflows = (props) => {
} }
setFirstLoad(false) setFirstLoad(false)
}, 100) }, 250)
/*
setTimeout(() => {
var timeout = 0
for (var key in newarray) {
const wf = newarray[key]
if (wf.actions === undefined || wf.actions === null || wf.actions.length === 0) {
setTimeout(() => {
sideloadWorkflow(wf.id, false)
}, timeout)
timeout += 1000
}
}
}, 1000)
*/
} else { } else {
if (isLoggedIn) { if (isLoggedIn) {
@@ -1637,20 +1656,24 @@ const Workflows = (props) => {
}); });
}; };
const copyWorkflow = (data) => { const duplicateWorkflow = (data) => {
data = JSON.parse(JSON.stringify(data)); //data = JSON.parse(JSON.stringify(data));
toast("Copying workflow " + data.name); toast("Copying workflow '" + data.name + "'. The new workflow will load in and be highlighted.");
data.id = ""; //data.id = "";
data.name = data.name + "_copy"; //data.name = data.name + "_copy";
data = deduplicateIds(data, true); //data = deduplicateIds(data, true);
fetch(globalUrl + "/api/v1/workflows", { const duplicateData = {
name: data.name + "_copy",
}
fetch(`${globalUrl}/api/v1/workflows/${data.id}/duplicate`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
}, },
body: JSON.stringify(data), body: JSON.stringify(duplicateData),
credentials: "include", credentials: "include",
}) })
.then((response) => { .then((response) => {
@@ -1660,7 +1683,20 @@ const Workflows = (props) => {
} }
return response.json(); return response.json();
}) })
.then(() => { .then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason !== undefined) {
toast("Failed copying workflow: " + responseJson.reason)
} else {
toast("Failed copying workflow")
}
return
}
if (responseJson.id !== undefined) {
setHighlightIds([responseJson.id])
}
setTimeout(() => { setTimeout(() => {
getAvailableWorkflows(); getAvailableWorkflows();
}, 1000); }, 1000);
@@ -1689,15 +1725,68 @@ const Workflows = (props) => {
}) })
} }
const sideloadWorkflow = (id, openEdit) => { const exportSingleWorkflow = (data, setOpen) => {
setExportModalOpen(true)
if (data.triggers !== null && data.triggers !== undefined) {
var newSubflows = [];
for (var key in data.triggers) {
const trigger = data.triggers[key];
if (
trigger.parameters !== null &&
trigger.parameters !== undefined
) {
for (var subkey in trigger.parameters) {
const param = trigger.parameters[subkey];
if (
param.name === "workflow" &&
param.value !== data.id &&
!newSubflows.includes(param.value)
) {
newSubflows.push(param.value);
}
}
}
}
var parsedworkflows = [];
for (var key in newSubflows) {
const foundWorkflow = workflows.find(
(workflow) => workflow.id === newSubflows[key]
);
if (foundWorkflow !== undefined && foundWorkflow !== null) {
parsedworkflows.push(foundWorkflow);
}
}
if (parsedworkflows.length > 0) {
console.log(
"Appending subflows during export: ",
parsedworkflows.length
);
data.subflows = parsedworkflows;
}
}
setExportData(data)
setOpen(false)
}
const sideloadWorkflow = (id, action, setOpen) => {
const storagewf = localStorage.getItem("workflows") const storagewf = localStorage.getItem("workflows")
const storageWorkflows = JSON.parse(storagewf) const storageWorkflows = JSON.parse(storagewf)
if (storageWorkflows === null || storageWorkflows === undefined || storageWorkflows.length === 0) { if (storageWorkflows === null || storageWorkflows === undefined || storageWorkflows.length === 0) {
} else { } else {
for (var i = 0; i < storageWorkflows.length; i++) { for (var i = 0; i < storageWorkflows.length; i++) {
if (storageWorkflows[i].id === id) { if (storageWorkflows[i].id === id) {
if (storageWorkflows[i].image !== "") { if (storageWorkflows[i].image !== "" && storageWorkflows[i].image !== undefined && storageWorkflows[i].image !== null) {
return
if (action === undefined || action === null || action === "") {
console.log("RETURNING")
return
}
} }
} }
} }
@@ -1718,8 +1807,16 @@ const Workflows = (props) => {
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
if (openEdit) { if (responseJson.success !== false && responseJson.id !== undefined) {
setEditing(responseJson) if (action === "edit") {
setEditing(responseJson)
} else if (action === "publish") {
setPublishModalOpen(true)
setSelectedWorkflow(responseJson)
} else if (action === "export") {
exportSingleWorkflow(responseJson, setOpen)
}
} }
for (var i = 0; i < storageWorkflows.length; i++) { for (var i = 0; i < storageWorkflows.length; i++) {
@@ -1730,8 +1827,9 @@ const Workflows = (props) => {
} }
} }
setWorkflows(storageWorkflows) //setWorkflows(storageWorkflows)
//setFilteredWorkflows(storageWorkflows) setFilteredWorkflows(storageWorkflows)
//setUpdate(Math.random())
}) })
.catch((error) => { .catch((error) => {
console.log(error.toString()) console.log(error.toString())
@@ -1878,7 +1976,9 @@ const Workflows = (props) => {
const appGroup = getWorkflowAppgroup(data) const appGroup = getWorkflowAppgroup(data)
const [triggers, subflows] = getWorkflowMeta(data) const [triggers, subflows] = getWorkflowMeta(data)
const isDistributed = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.includes(userdata.active_org.id) const hasSuborgs = data.suborg_distribution !== undefined && data.suborg_distribution !== null && data.suborg_distribution.length > 0
const isDistributed = (data.parentorg_workflow !== undefined && data.parentorg_workflow !== null && data.parentorg_workflow.length > 0) //|| (data.org_id !== userdata.active_org.id && data.org_id !== undefined && data.org_id !== null && data.org_id.length > 0)
const workflowMenuButtons = ( const workflowMenuButtons = (
<Menu <Menu
id="long-menu" id="long-menu"
@@ -1890,8 +1990,20 @@ const Workflows = (props) => {
setAnchorEl(null); setAnchorEl(null);
}} }}
> >
{isDistributed ?
<MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
onClick={() => {
navigate(`/workflows/${data.id}`)
}}
>
<VisibilityIcon style={{ marginLeft: 0, marginRight: 8 }} />
Explore Workflow
</MenuItem>
: null}
<MenuItem <MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }} style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") { if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") {
@@ -1899,7 +2011,9 @@ const Workflows = (props) => {
} else { } else {
//toast("Need to side-load workflow to be edited properly") //toast("Need to side-load workflow to be edited properly")
sideloadWorkflow(data.id, true) sideloadWorkflow(data.id, "edit")
toast.info("Loading full workflow for editing. Please wait...")
} }
}} }}
key={"change"} key={"change"}
@@ -1909,9 +2023,11 @@ const Workflows = (props) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }} style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={() => { onClick={() => {
setSelectedWorkflow(data); sideloadWorkflow(data.id, "publish")
setPublishModalOpen(true);
toast.info("Loading full workflow for publishing. Please wait...")
}} }}
key={"publish"} key={"publish"}
> >
@@ -1920,9 +2036,10 @@ const Workflows = (props) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }} style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={() => { onClick={() => {
copyWorkflow(data); duplicateWorkflow(data)
setOpen(false); setOpen(false)
}} }}
key={"duplicate"} key={"duplicate"}
> >
@@ -1931,52 +2048,11 @@ const Workflows = (props) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }} style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={() => { onClick={() => {
setExportModalOpen(true); sideloadWorkflow(data.id, "export", setOpen)
if (data.triggers !== null && data.triggers !== undefined) { toast.info("Loading full workflow to be exported. Please wait...")
var newSubflows = [];
for (var key in data.triggers) {
const trigger = data.triggers[key];
if (
trigger.parameters !== null &&
trigger.parameters !== undefined
) {
for (var subkey in trigger.parameters) {
const param = trigger.parameters[subkey];
if (
param.name === "workflow" &&
param.value !== data.id &&
!newSubflows.includes(param.value)
) {
newSubflows.push(param.value);
}
}
}
}
var parsedworkflows = [];
for (var key in newSubflows) {
const foundWorkflow = workflows.find(
(workflow) => workflow.id === newSubflows[key]
);
if (foundWorkflow !== undefined && foundWorkflow !== null) {
parsedworkflows.push(foundWorkflow);
}
}
if (parsedworkflows.length > 0) {
console.log(
"Appending subflows during export: ",
parsedworkflows.length
);
data.subflows = parsedworkflows;
}
}
setExportData(data);
setOpen(false);
}} }}
key={"export"} key={"export"}
> >
@@ -1985,6 +2061,7 @@ const Workflows = (props) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }} style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
onClick={() => { onClick={() => {
setDeleteModalOpen(true); setDeleteModalOpen(true);
setSelectedWorkflowId(data.id); setSelectedWorkflowId(data.id);
@@ -2076,7 +2153,7 @@ const Workflows = (props) => {
} }
return ( return (
<div style={{width: "100%", minWidth: 321, position: "relative", border: isDistributed ? "2px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}> <div style={{width: "100%", minWidth: 321, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "2px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}>
<Paper square style={paperAppStyle}> <Paper square style={paperAppStyle}>
{selectedCategory !== "" ? {selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom"> <Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
@@ -2462,6 +2539,8 @@ const Workflows = (props) => {
} }
const reader = new FileReader(); const reader = new FileReader();
var workflowids = []
// Waits for the read // Waits for the read
reader.addEventListener("load", (event) => { reader.addEventListener("load", (event) => {
var data = reader.result; var data = reader.result;
@@ -2497,7 +2576,8 @@ const Workflows = (props) => {
data.org_id = userdata.active_org.id data.org_id = userdata.active_org.id
data.org = [] data.org = []
data.execution_org = {} data.execution_org = {}
workflowids.push(data.id)
// Actually create it // Actually create it
setNewWorkflow( setNewWorkflow(
@@ -2528,6 +2608,10 @@ const Workflows = (props) => {
} }
setLoadWorkflowsModalOpen(false); setLoadWorkflowsModalOpen(false);
if (workflowids.length > 0) {
setHighlightIds(workflowids)
}
}; };
const getWorkflowMeta = (data) => { const getWorkflowMeta = (data) => {
@@ -3682,7 +3766,7 @@ const Workflows = (props) => {
workflowDelay += 75 workflowDelay += 75
} else { } else {
return ( return (
<Grid key={index} item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}> <Grid key={index} item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px", }}>
<WorkflowPaper key={index} data={data} /> <WorkflowPaper key={index} data={data} />
</Grid> </Grid>
) )
+79 -79
View File
@@ -1897,87 +1897,87 @@ func buildEnvVars(envMap map[string]string) []corev1.EnvVar {
} }
func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
if request.Body == nil { if request.Body == nil {
log.Printf("[WARNING] (2) No body in request for workflowqueue") log.Printf("[WARNING] (2) No body in request for workflowqueue")
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
return return
}
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[WARNING] (3) Failed reading body for workflowqueue")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
var actionResult shuffle.ActionResult
err = json.Unmarshal(body, &actionResult)
if err != nil {
log.Printf("[ERROR] Failed shuffle.ActionResult unmarshaling (2): %s", err)
//resp.WriteHeader(401)
//resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
//return
}
if len(actionResult.ExecutionId) == 0 {
log.Printf("[ERROR] No workflow execution id in action result. Data: %s", string(body))
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflow execution id in action result"}`)))
return
}
// 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database
// 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit
// 3. Add to and update actionResult in workflowExecution
// 4. Push to db
// IF FAIL: Set executionstatus: abort or cancel
ctx := context.Background()
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId)
if err != nil {
log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId)))
return
}
if workflowExecution.Authorization != actionResult.Authorization {
log.Printf("[ERROR][%s] Bad authorization key when updating node (workflowQueue). Want: %s, Have: %s", actionResult.ExecutionId, workflowExecution.Authorization, actionResult.Authorization)
resp.WriteHeader(403)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key"}`)))
return
}
if workflowExecution.Status == "FINISHED" {
log.Printf("[DEBUG][%s] Workflowexecution is already FINISHED. No further action can be taken", workflowExecution.ExecutionId)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because it has status %s. Lastnode: %s"}`, workflowExecution.Status, workflowExecution.LastNode)))
return
}
if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
log.Printf("[WARNING][%s] Workflowexecution already has status %s. No further action can be taken", workflowExecution.ExecutionId, workflowExecution.Status)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status)))
return
}
retries := 0
retry, retriesok := request.URL.Query()["retries"]
if retriesok && len(retry) > 0 {
val, err := strconv.Atoi(retry[0])
if err == nil {
retries = val
} }
}
log.Printf("[DEBUG][%s] Action: Received, Label: '%s', Action: '%s', Status: %s, Run status: %s, Extra=Retry:%d", workflowExecution.ExecutionId, actionResult.Action.Label, actionResult.Action.AppName, actionResult.Status, workflowExecution.Status, retries) defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[WARNING] (3) Failed reading body for workflowqueue")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
//results = append(results, actionResult) var actionResult shuffle.ActionResult
//log.Printf("[INFO][%s] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", workflowExecution.ExecutionId, action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters)) err = json.Unmarshal(body, &actionResult)
//log.Printf("[DEBUG][%s] In workflowQueue with transaction", workflowExecution.ExecutionId) if err != nil {
runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp) log.Printf("[ERROR] Failed shuffle.ActionResult unmarshaling (2): %s", err)
//resp.WriteHeader(401)
//resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
//return
}
if len(actionResult.ExecutionId) == 0 {
log.Printf("[ERROR] No workflow execution id in action result. Data: %s", string(body))
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflow execution id in action result"}`)))
return
}
// 1. Get the shuffle.WorkflowExecution(ExecutionId) from the database
// 2. if shuffle.ActionResult.Authentication != shuffle.WorkflowExecution.Authentication -> exit
// 3. Add to and update actionResult in workflowExecution
// 4. Push to db
// IF FAIL: Set executionstatus: abort or cancel
ctx := context.Background()
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId)
if err != nil {
log.Printf("[ERROR][%s] Failed getting execution (workflowqueue) %s: %s", actionResult.ExecutionId, actionResult.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist locally."}`, actionResult.ExecutionId)))
return
}
if workflowExecution.Authorization != actionResult.Authorization {
log.Printf("[ERROR][%s] Bad authorization key when updating node (workflowQueue). Want: %s, Have: %s", actionResult.ExecutionId, workflowExecution.Authorization, actionResult.Authorization)
resp.WriteHeader(403)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key"}`)))
return
}
if workflowExecution.Status == "FINISHED" {
log.Printf("[DEBUG][%s] Workflowexecution is already FINISHED. No further action can be taken", workflowExecution.ExecutionId)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is already finished because it has status %s. Lastnode: %s"}`, workflowExecution.Status, workflowExecution.LastNode)))
return
}
if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
log.Printf("[WARNING][%s] Workflowexecution already has status %s. No further action can be taken", workflowExecution.ExecutionId, workflowExecution.Status)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status)))
return
}
retries := 0
retry, retriesok := request.URL.Query()["retries"]
if retriesok && len(retry) > 0 {
val, err := strconv.Atoi(retry[0])
if err == nil {
retries = val
}
}
log.Printf("[DEBUG][%s] Action: Received, Label: '%s', Action: '%s', Status: %s, Run status: %s, Extra=Retry:%d", workflowExecution.ExecutionId, actionResult.Action.Label, actionResult.Action.AppName, actionResult.Status, workflowExecution.Status, retries)
//results = append(results, actionResult)
//log.Printf("[INFO][%s] Time to execute %s (%s) with app %s:%s, function %s, env %s with %d parameters.", workflowExecution.ExecutionId, action.ID, action.Label, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters))
//log.Printf("[DEBUG][%s] In workflowQueue with transaction", workflowExecution.ExecutionId)
runWorkflowExecutionTransaction(ctx, 0, workflowExecution.ExecutionId, actionResult, resp)
} }
// Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times // Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times
@@ -2090,7 +2090,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
*/ */
} }
} }
if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" { if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) log.Printf("[DEBUG][%s] Running setexec with status %s and %d/%d results", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions))
//result(s)", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results)) //result(s)", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results))