Added detection pages and fixed more form pages

This commit is contained in:
Frikky
2024-10-11 17:10:24 +02:00
parent 38e9d66cfd
commit 79acdbb012
6 changed files with 917 additions and 75 deletions
+210
View File
@@ -0,0 +1,210 @@
import React, { useState } from "react";
import {
Container,
Box,
TextField,
Switch,
Typography,
Button,
CircularProgress,
Paper,
} from "@mui/material";
import { toast } from "react-toastify";
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) => {
if (!isTenzirActive) {
toast("connect to siem first for global enable/disable to work");
return;
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
if (action === "enable_folder") setFolderDisabled(false);
else setFolderDisabled(true);
} else {
//toast(`failed to disable rule`);
}
})
)
.catch((error) => {
console.log(`Error in ${action} the rule: `, error);
toast(`An error occurred while ${action} the rule`);
});
};
const Detection = (props) => {
const { globalUrl, ruleInfo, folderDisabled, setFolderDisabled, isTenzirActive } = props;
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const handleConnectClick = () => {
if (!isTenzirActive) {
setLoading(true);
const url = `${globalUrl}/api/v1/detections/siem/connect`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
setTimeout(() => {
setLoading(false);
window.location.reload();
}, 15000);
} else {
setLoading(false);
toast("Failed to connect to SIEM");
}
})
)
.catch((error) => {
setLoading(false);
console.log(`Error in connecting to SIEM: `, error);
toast("An error occurred while connecting to SIEM");
});
} else {
console.log("Already connected to SIEM");
}
};
const filteredRules = ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<Container>
<Paper
style={{
marginTop: 50,
width: "100%",
padding: 50,
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette.borderRadius,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h6" component="div">
Sigma Detection Rules
</Typography>
<Button
variant="contained"
onClick={handleConnectClick}
disabled={loading} // Disable the button while loading
color={isTenzirActive ? "primary" : "secondary"}
style={{ }}
>
{loading ? <CircularProgress size={24} /> : isTenzirActive ? "Connected to siem" : "Connect to siem"}
</Button>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Box
sx={{
display: "flex",
}}
>
<TextField
label="Search rules"
variant="outlined"
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{/* <Button
color="primary"
variant="contained"
onClick={() => uploadRef.current.click()}
>
<PublishIcon /> Upload sigma file
</Button>
<input
hidden
type="file"
multiple
ref={uploadRef}
onChange={(event) => {
uploadFiles(event.target.files);
}}
/> */}
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Global disable/enable
</Typography>
<Switch
checked={!folderDisabled}
onChange={() =>
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive)
}
disabled={!isTenzirActive}
/>
</Box>
</Box>
<Box
sx={{
height: "500px",
width: "100%",
overflowY: "auto",
p: 1,
}}
>
{filteredRules?.length > 0 ?
filteredRules.map((card) => {
console.log("RULE CARD: ", card);
return (
<DetectionRuleCard
key={card.file_id}
ruleName={card.title}
description={card.description}
file_id={card.file_id}
globalUrl={globalUrl}
folderDisabled={folderDisabled}
isTenzirActive={isTenzirActive}
{...card}
/>
)
})
: null }
</Box>
</Paper>
</Container>
);
};
export default Detection;
@@ -0,0 +1,399 @@
import React, { useState, useEffect, } from "react";
import {
Container,
Box,
TextField,
Switch,
Typography,
Button,
CircularProgress,
Paper,
Divider,
IconButton,
} from "@mui/material";
import {
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material"
import { toast } from "react-toastify";
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
import {
green,
red,
grey,
} from "../views/AngularWorkflow.jsx"
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
if (!isDetectionActive) {
toast.warn("Connect to siem first for global enable/disable to work");
return;
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
if (action === "enable_folder") setFolderDisabled(false);
else setFolderDisabled(true);
} else {
//toast(`failed to disable rule`);
}
})
)
.catch((error) => {
console.log(`Error in ${action} the rule: `, error);
toast(`An error occurred while ${action} the rule`);
});
};
const DetectionExplorer = (props) => {
const { globalUrl, userdata, ruleInfo, folderDisabled, setFolderDisabled, detectionInfo, importDetectionFromUrl, rulesLoading, isDetectionActive, setIsDetectionActive, ruleMapping, setRuleMapping, } = props;
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const [workflow, setWorkflow] = useState({})
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
const [isDetectionValid, setIsDetectionValid] = useState(false)
const [availableDetection, setAvailableDetection] = React.useState([]);
const loadUsecases = () => {
const url = `${globalUrl}/api/v1/workflows/usecases`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.success === false) {
return
}
if (responseJson.length == 0) {
return
}
for (var usecaseCategory in responseJson) {
const category = responseJson[usecaseCategory]
if (!category.name.toLowerCase().includes("respond") && !category.name.toLowerCase().includes("response")) {
continue
}
setAvailableDetection(category.list)
break
}
})
)
.catch((error) => {
console.log(`Error in loading usecases: `, error);
//toast(`An error occurred while loading usecases`);
})
}
const loadWorkflow = (workflowId) => {
const url = `${globalUrl}/api/v1/workflows/${workflowId}`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.id === workflowId) {
setWorkflow(responseJson)
} else {
toast(`Failed to load workflow ${workflowId}`);
}
}))
.catch((error) => {
console.log(`Error in loading workflow ${workflowId}: `, error);
toast(`An error occurred while loading workflow ${workflowId}`);
})
}
const handleConnectClick = () => {
if (detectionWorkflowId !== "") {
// FIXME: Show the Usecase UI for how to fix the workflow(s)
// Instead loading full workflow and showing it directly? Hmm
//toast.warn("Please reload the UI to load the detection status")
return
}
if (isDetectionActive) {
return
}
if (detectionInfo.category === undefined || detectionInfo.category === null) {
toast.warn("Detection category not found. Please try again or contact support@shuffler.io if you think this is a bug.")
return
}
setLoading(true);
const url = `${globalUrl}/api/v1/detections/${detectionInfo?.category}/connect`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
setLoading(false)
if (setIsDetectionActive !== undefined) {
setIsDetectionActive(true)
}
if (responseJson.workflow_id !== undefined && responseJson.workflow_id !== null) {
setDetectionWorkflowId(responseJson.workflow_id)
loadWorkflow(responseJson.workflow_id)
}
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
setIsDetectionValid(responseJson.workflow_valid)
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
toast(responseJson.reason)
} else {
toast(`Failed to connect to ${detectionInfo?.category}`);
}
if (responseJson.action !== undefined && responseJson.actio !== null && responseJson.action.length > 0) {
//if (responseJson.action === "environment_create") {
// navigate("/admin?tab=environments")
//}
}
setLoading(false);
}
})
)
.catch((error) => {
setLoading(false);
console.log(`Error in connecting to ${detectionInfo?.category}: `, error);
toast(`An error occurred while connecting to ${detectionInfo?.category}`);
});
}
useEffect(() => {
loadUsecases()
}, [])
useEffect(() => {
handleConnectClick()
}, [detectionInfo])
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<Container>
<Paper
style={{
marginTop: 50,
width: "100%",
padding: 50,
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette.borderRadius,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h6" component="div">
{detectionInfo?.title} {filteredRules === undefined || filteredRules === null ? null : `(${filteredRules?.length} rules)`}
</Typography>
{workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
<div style={{display: "flex", }}>
<div style={{minWidth: 400, maxWidth: 400, }}>
<WorkflowValidationTimeline
originalWorkflow={workflow}
apps={[]}
getParents={undefined}
execution={undefined}
workflow={workflow}
showHoverColor={true}
globalUrl={globalUrl}
userdata={userdata}
/>
</div>
<IconButton
variant="contained"
color="secondary"
onClick={() => {
window.open(`/workflows/${workflow.id}`, "_blank")
}}
>
<OpenInNewIcon />
</IconButton>
</div>
:
<Button
variant="contained"
onClick={() => {
handleConnectClick()
}}
disabled={loading} // Disable the button while loading
style={{
// Red = workflow exists, validation is false
// Green = workflow exists, validation is true
// Grey = workflow does not exist
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
}}
>
{loading ? <CircularProgress size={24} /> :
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
</Button>
}
</Box>
{filteredRules?.length > 0 ?
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Box
sx={{
display: "flex",
minHeight: 50,
maxHeight: 50,
}}
>
<TextField
label="Search rules"
variant="outlined"
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Global disable/enable
</Typography>
<Switch
checked={!folderDisabled}
onChange={() =>
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isDetectionActive)
}
/>
</Box>
</Box>
: null}
<Divider />
<Box
sx={{
height: "500px",
width: "100%",
overflowY: "auto",
p: 1,
}}
>
{filteredRules?.length > 0 ?
ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null ?
filteredRules.map((rule, index) => {
return (
<div style={{marginTop: 5, }}>
<DetectionRuleCard
globalUrl={globalUrl}
key={index}
ruleName={rule.file_name}
description={rule.description}
file_id={rule.file_id}
globalUrl={globalUrl}
folderDisabled={folderDisabled}
isDetectionActive={isDetectionActive}
ruleMapping={ruleMapping}
setRuleMapping={setRuleMapping}
availableDetection={availableDetection}
{...rule}
/>
</div>
)
})
: null
:
<div style={{textAlign: "center", }}>
{rulesLoading === true ?
<Container style={{ display: "flex", justifyContent: "center", alignItems: "center", marginTop: 25, }}>
<div>
<CircularProgress />
<Typography variant="h6" style={{ marginTop: 20 }}>Downloading rules, please wait...</Typography>
</div>
</Container>
:
<div>
<Typography variant="h6" color="textSecondary" style={{marginTop: 50, }}>
No rules loaded yet
</Typography>
<Button
style={{marginTop: 20, }}
variant="contained"
color="primary"
onClick={() => {
if (importDetectionFromUrl !== undefined) {
importDetectionFromUrl(true, detectionInfo.download_repo)
} else {
toast("Import function not found. Please contact support@shuffler.io")
}
}}
>
Load Default Rules
</Button>
</div>
}
</div>
}
</Box>
</Paper>
</Container>
);
};
export default DetectionExplorer;
+130 -31
View File
@@ -61,10 +61,11 @@ import {
OpenInNew as OpenInNewIcon,
Add as AddIcon,
Remove as RemoveIcon,
EditNote as EditNoteIcon,
} from "@mui/icons-material";
const EditWorkflow = (props) => {
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, } = props
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, setRealtimeMarkdown, } = props
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
@@ -82,8 +83,8 @@ const EditWorkflow = (props) => {
const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : [])
const [inputMarkdown, setInputMarkdown] = React.useState(workflow.input_markdown !== undefined && workflow.input_markdown !== null ? workflow.input_markdown : "")
const [outputMarkdown, setOutputMarkdown] = React.useState(workflow.output_markdown !== undefined && workflow.output_markdown !== null ? workflow.output_markdown : "")
const [scrollDone, setScrollDone] = React.useState(false)
const [selectedYieldActions, setSelectedYieldActions] = React.useState(workflow.output_yields !== undefined && workflow.output_yields !== null ? JSON.parse(JSON.stringify(workflow.output_yields)) : [])
const classes = useStyles();
@@ -204,25 +205,28 @@ const EditWorkflow = (props) => {
<Typography variant="h4" style={{flex: 9, }}>
{newWorkflow ? "New" : "Editing"} workflow
</Typography>
{newWorkflow === true ? null :
<div style={{ marginLeft: 5, flex: 1 }}>
<Tooltip title="Open Workflow Form for 'normal' users">
<a
rel="noopener noreferrer"
href={`/forms/${workflow.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
marginTop: 10,
}}
>
<OpenInNewIcon />
</a>
<Tooltip title="Go to Public Form page">
<IconButton>
<a
rel="noopener noreferrer"
href={`/forms/${workflow.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
}}
>
<EditNoteIcon />
</a>
</IconButton>
</Tooltip>
</div>
}
</div>
<Typography variant="body2" color="textSecondary" style={{marginTop: 20, maxWidth: 440,}}>
Workflows can be built from scratch, or from templates. <a href="/usecases2" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
@@ -306,6 +310,8 @@ const EditWorkflow = (props) => {
innerWorkflow.input_questions = validfields
innerWorkflow.input_markdown = inputMarkdown
innerWorkflow.output_yields = selectedYieldActions
innerWorkflow.name = name
innerWorkflow.description = description
if (newWorkflowTags.length > 0) {
@@ -382,16 +388,16 @@ const EditWorkflow = (props) => {
<FormControl style={{flex: 1, marginRight: 5,}}>
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
<Select
defaultValue=""
id="grouped-select"
label="Matching Usecase"
multiple
value={selectedUsecases}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Changed: ", event)
}}
>
defaultValue=""
id="grouped-select"
label="Matching Usecase"
multiple
value={selectedUsecases}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Changed: ", event)
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
@@ -614,6 +620,11 @@ const EditWorkflow = (props) => {
<Divider style={{marginTop: 20, marginBottom: 20, }} />
<Typography variant="h4" style={{marginTop: 50, }}>
MSSP controls
</Typography>
<Typography variant="body1" style={{marginTop: 50, }}>
MSSP Suborg Distribution (beta - contact support@shuffler.io for more info)
</Typography>
@@ -719,11 +730,11 @@ const EditWorkflow = (props) => {
</Link>
}
<Divider style={{marginTop: 20, marginBottom: 20, }} />
{/*<Divider style={{marginTop: 20, marginBottom: 20, }} />*/}
<Typography variant="body1" style={{marginTop: 50, }}>
<Typography variant="body1" style={{marginTop: 100, }}>
Git Backup Repository
</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
@@ -875,9 +886,38 @@ const EditWorkflow = (props) => {
<Divider style={{marginTop: 20, marginBottom: 20, }} />
<Typography variant="h6" style={{marginTop: 50, }}>
Input fields
</Typography>
<div id="form_fill" style={{position: "relative", }}>
<Typography variant="h4" style={{marginTop: 100, }}>
Form Control
</Typography>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
Form Control is used to control how the Form for the workflow is shown to users. You can add input fields, markdown, and more. This is the first step in the workflow, and is required for all workflows.
</Typography>
<Typography variant="h6" style={{marginTop: 50, }}>
Input fields
</Typography>
<Tooltip title="Go to Public Form page">
<IconButton style={{position: "absolute", top: 0, right: 10, }}>
<a
rel="noopener noreferrer"
href={`/forms/${workflow.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
}}
>
<EditNoteIcon />
</a>
</IconButton>
</Tooltip>
</div>
<Typography variant="body2" color="textSecondary" style={{marginBottom: 20, }}>
Input fields are fields that will be used during the startup of the workflow. These will be formatted in JSON and is most commonly used from the <a href={`/forms/${workflow.id}`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Form page</a> for this workflow. If chosen in the User Input node, these will be required fields. Use Semi-Colon ";" to create dropdown options. The first key will be the name shown, and subsequent keys will be the available values.
</Typography>
@@ -1008,6 +1048,10 @@ const EditWorkflow = (props) => {
}}
onChange={(e) => {
if (setRealtimeMarkdown !== undefined) {
setRealtimeMarkdown(e.target.value)
}
setInputMarkdown(e.target.value)
workflow.input_markdown = e.target.value
setWorkflow(workflow)
@@ -1015,6 +1059,61 @@ const EditWorkflow = (props) => {
}}
/>
</div>
<div id="output_control">
<Typography variant="h6" style={{marginTop: 50, }}>
Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`})
</Typography>
<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.
</Typography>
<FormControl style={{marginTop: 15, }}>
<Select
defaultValue=""
id="output-yield-control"
label="Yielding nodes"
multiple
fullWidth
style={{width: 500, }}
value={selectedYieldActions === [] ? ["none"] : selectedYieldActions}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Value: ", event.target.value)
if (event.target.value.length > 0) {
if (event.target.value.includes("none")) {
setSelectedYieldActions([])
return
}
}
const newvalue = event?.target?.value
if (newvalue === undefined || newvalue === null) {
} else {
setSelectedYieldActions(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>
</div>
: null}
+8 -9
View File
@@ -8602,7 +8602,6 @@ const releaseToConnectLabel = "Release to Connect"
if (alledges !== undefined && alledges !== null && alledges.length > 0) {
for (let edgekey in alledges) {
const tmp = alledges[edgekey]
console.log("TMP: ", tmp, tmp.data.source)
if (tmp.data.source === trigger.id) {
mappedStartnode = tmp.data.target
break
@@ -16079,7 +16078,7 @@ const releaseToConnectLabel = "Release to Connect"
}}
/>
<div style={{ flex: "10" }}>
<b>Interval (UTC) </b>
<b>When to start: {isCloud || selectedTrigger?.environment === "cloud" ? <a href="https://crontab.guru" target="_blank" style={{color: "#f85a3e", }}>Cron formatting</a> : "every X second"}</b>
</div>
</div>
<TextField
@@ -16129,7 +16128,7 @@ const releaseToConnectLabel = "Release to Connect"
}}
/>
<div style={{ flex: "10" }}>
<b>Execution argument: </b>
<b>Runtime Argument: </b>
</div>
</div>
<TextField
@@ -16145,7 +16144,7 @@ const releaseToConnectLabel = "Release to Connect"
workflow.triggers[selectedTriggerIndex] === null || workflow.triggers[selectedTriggerIndex] === undefined ? false : workflow.triggers[selectedTriggerIndex].status === "running"
}
fullWidth
rows="6"
rows="3"
multiline
color="primary"
defaultValue={
@@ -17556,7 +17555,7 @@ const releaseToConnectLabel = "Release to Connect"
</Tooltip>
<Tooltip
color="secondary"
title="Show Workflow Revision History (Beta) (Ctrl + ])"
title="Show Workflow Revision History (Ctrl + ])"
placement="top"
>
<span>
@@ -19303,11 +19302,11 @@ const releaseToConnectLabel = "Release to Connect"
executionData.execution_source === "questions" || executionData.execution_source === "web" || executionData.execution_source === "form" || executionData.execution_source === "forms" ?
<a
rel="noopener noreferrer"
href={`/workflows/${executionData.workflow.id}/run`}
href={`/forms/${executionData.workflow.id}`}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Questions
Form
</a>
:
executionData.execution_source
@@ -22178,9 +22177,9 @@ const releaseToConnectLabel = "Release to Connect"
const drawerData = originalWorkflow !== undefined && originalWorkflow !== null ?
<div style={{ height: "100%"}}>
<Typography variant="h5" style={{ paddingLeft: 25, paddingTop:25, backgroundColor: theme.palette.surfaceColor, height: "8%" }}>
Version History (Beta)
Version History
</Typography>
<div style={{height: "92%" }}>
<div style={{height: "92%" }}>
<div style={{paddingLeft: "25px", paddingRight: "25px", paddingTop: "10px"}}>
<div style={{marginBottom: "20px", }}>
<Typography variant="h6" style={{marginTop: 10, marginBottom: 5, }}>
+125 -21
View File
@@ -2,6 +2,7 @@
import React, {useState, useEffect} from 'react';
import ReactDOM from "react-dom"
import ReactJson from "react-json-view-ssr";
import { green, yellow, red, grey} from "./AngularWorkflow.jsx";
import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
import { useNavigate, Link, useParams } from "react-router-dom";
@@ -36,6 +37,8 @@ import {
import {
Preview as PreviewIcon,
ContentCopy as ContentCopyIcon,
ArrowBack as ArrowBackIcon,
ArrowForward as ArrowForwardIcon,
} from '@mui/icons-material';
const hrefStyle = {
@@ -71,6 +74,7 @@ const RunWorkflow = (defaultprops) => {
const [foundSourcenode, setFoundSourcenode] = React.useState(undefined);
const [editWorkflowModalOpen, setEditWorkflowModalOpen] = React.useState(false)
const [sharingOpen, setSharingOpen] = React.useState(false)
const [realtimeMarkdown, setRealtimeMarkdown] = React.useState("")
const boxStyle = {
color: "white",
@@ -225,10 +229,13 @@ const RunWorkflow = (defaultprops) => {
id="markdown_wrapper"
escapeHtml={false}
style={{
maxWidth: "100%", minWidth: "100%",
maxWidth: "100%",
minWidth: "100%",
overflowX: "hidden",
overflowY: "auto",
}}
>
{executionData.result}
{executionData.result}
</Markdown>
</div>
: null}
@@ -376,6 +383,7 @@ const RunWorkflow = (defaultprops) => {
})
}
const getWorkflow = (workflow_id, selectedNode) => {
const url = `${globalUrl}/api/v1/workflows/${workflow_id}`
@@ -477,6 +485,23 @@ const RunWorkflow = (defaultprops) => {
}
}
if (responseJson.input_markdown !== undefined && responseJson.input_markdown !== null && responseJson.input_markdown.length > 0) {
// Look for {{ uuid }} format, and try to run that workflow with their account
// This is a hack, but a fun one.
const uuidRegex = /{{\s*[a-f0-9-]+\s*}}/g
const found = responseJson.input_markdown.match(uuidRegex)
if (found !== undefined && found !== null && found.length > 0) {
for (var foundkey in found) {
const uuid = found[foundkey].replace("{{", "").replace("}}", "").trim()
// Run the workflow, then replace it.
// Only run if logged in
console.log("Found UUID: " + uuid)
responseJson.input_markdown = responseJson.input_markdown.replace(found[foundkey], "")
}
}
}
handleGetOrg(responseJson.org_id)
setWorkflow(responseJson);
})
@@ -651,6 +676,10 @@ const RunWorkflow = (defaultprops) => {
const sourceNode = searchParams.get("source_node")
useEffect(() => {
if (!isLoaded) {
return
}
getWorkflow(props.match.params.key, sourceNode)
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
console.log("Get execution: ", execution_id)
@@ -660,7 +689,7 @@ const RunWorkflow = (defaultprops) => {
if (answer !== undefined && answer !== null) {
console.log("Got answer: ", answer)
}
}, [])
}, [isLoaded])
useEffect(() => {
@@ -744,6 +773,7 @@ const RunWorkflow = (defaultprops) => {
}
}
var validResults = 0
const basedata =
<div style={bodyDivStyle}>
<Paper style={boxStyle}>
@@ -772,7 +802,7 @@ const RunWorkflow = (defaultprops) => {
}}
rehypePlugins={[rehypeRaw]}
>
{workflow.input_markdown}
{realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow.input_markdown}
</Markdown>
</div>
: null}
@@ -858,7 +888,7 @@ const RunWorkflow = (defaultprops) => {
>
{multiChoiceOptions.map((option, menuIndex) => {
if (index === 0) {
if (menuIndex === 0) {
return null
}
@@ -928,15 +958,17 @@ const RunWorkflow = (defaultprops) => {
</span>
}
{executionRunning ?
<span style={{width: 50, height: 50, margin: "auto", alignItems: "center", justifyContent: "center", textAlign: "center", }}>
<CircularProgress style={{marginTop: 20, marginBottom: 20, marginLeft: 185, }}/>
{executionData.status !== undefined && executionData.status !== null && executionData.status !== "" ?
{/*executionData.status !== undefined && executionData.status !== null && executionData.status !== "" ?
<Typography variant="body2" style={{margin: "auto", marginTop: 20, marginBottom: 20, textAlign: "center", alignItem: "center", }} color="textSecondary">
Status: {executionData.status}
</Typography>
: null}
: null*/}
</span>
:
((answer !== undefined && answer !== null) || (foundSourcenode !== undefined && foundSourcenode !== null)) ?
@@ -994,19 +1026,62 @@ const RunWorkflow = (defaultprops) => {
</div>
}
{/*buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ?
<img id="finalize_gif" src="/images/finalize.gif" alt="finalize workflow animation" style={{width: 150, marginLeft: 125, borderRadius: theme.palette.borderRadius, }}
onLoad={() => {
console.log("Img loaded.")
setTimeout(() => {
console.log("Img closing.")
setButtonClicked("finished")
}, 1250)
{workflow.output_yields !== undefined && workflow.output_yields !== null && workflow.output_yields.length > 0 ?
<div style={{marginTop: 20, }}>
{workflow.output_yields.map((yieldItem, index) => {
if (executionData.results === undefined || executionData.results === null || executionData.results.length === 0) {
return null
}
}}
/>
: null*/}
const foundresult = executionData.results.find((result) => {
return result.action.id === yieldItem
})
if (foundresult === undefined || foundresult === null) {
return null
}
if (foundresult.status === "SKIPPED") {
return null
}
const validate = validateJson(foundresult.result)
validResults += 1
console.log("VAlid: ", validate)
var appendedDetails = foundresult.result
if (validate.valid) {
appendedDetails = <ReactJson
src={validate.result}
theme={theme.palette.jsonTheme}
style={theme.palette.reactJsonStyle}
collapsed={true}
iconStyle={theme.palette.jsonIconStyle}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
displayArrayKey={false}
enableClipboard={(copy) => {
//handleReactJsonClipboard(copy);
}}
displayDataTypes={false}
onSelect={(select) => {
//HandleJsonCopy(validate.result, select, "exec");
}}
name={false}
/>
}
return (
<div style={{marginBottom: 10, }}>
{foundresult?.action?.label?.replaceAll("_", " ")} - {foundresult.status}:
<br />
{appendedDetails}
</div>
)
})}
</div>
: null}
<div style={{marginTop: "10px"}}>
{executionInfo}
@@ -1015,6 +1090,8 @@ const RunWorkflow = (defaultprops) => {
{answer !== undefined && answer !== null ? null :
<ShowExecutionResults executionData={executionData} />
}
</form>
</div>
}
@@ -1037,7 +1114,8 @@ const RunWorkflow = (defaultprops) => {
usecases={undefined}
expanded={true}
scrollTo={"input_markdown"}
setRealtimeMarkdown={setRealtimeMarkdown}
scrollTo={"form_fill"}
/>
: null}
@@ -1112,20 +1190,46 @@ const RunWorkflow = (defaultprops) => {
color={"secondary"}
style={{marginRight: 10, }}
onClick={() => {
setEditWorkflowModalOpen(true)
navigate(`/workflows`)
}}
>
Edit Details
<ArrowBackIcon style={{marginRight: 5, }} />
Workflows
</Button>
<Button
variant={"outlined"}
color={"secondary"}
style={{marginRight: 10, }}
onClick={() => {
window.open(`/workflow/${workflow.id}`, "_blank")
}}
>
<ArrowForwardIcon style={{marginRight: 5, }} />
Workflow
</Button>
<Button
variant={"outlined"}
color={"secondary"}
style={{marginRight: 10, }}
onClick={() => {
setSharingOpen(true)
}}
>
Share Form
</Button>
<Button
variant={"contained"}
color={"primary"}
style={{}}
onClick={() => {
setEditWorkflowModalOpen(true)
}}
>
Edit Details
</Button>
</div>
: null}
+45 -14
View File
@@ -75,6 +75,7 @@ import {
ArrowRight as ArrowRightIcon,
QueryStats as QueryStatsIcon,
Visibility as VisibilityIcon,
EditNote as EditNoteIcon,
} from "@mui/icons-material";
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
@@ -2050,6 +2051,18 @@ const Workflows = (props) => {
<EditIcon style={{ marginLeft: 0, marginRight: 8 }} />
{"Edit details"}
</MenuItem>
<MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
onClick={(event) => {
window.open(`/forms/${data.id}`, "_blank")
}}
key={"explore forms"}
>
<EditNoteIcon style={{ marginLeft: 0, marginRight: 8 }} />
{"Create Form"}
</MenuItem>
<MenuItem
style={{ backgroundColor: theme.palette.inputColor, color: "white" }}
disabled={isDistributed}
@@ -2432,20 +2445,38 @@ const Workflows = (props) => {
})
: null}
</Grid>
{data.actions !== undefined && data.actions !== null ? (
<div style={{position: "absolute", top: 10, right: 10, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={menuClick}
style={{ padding: "0px", color: "#979797" }}
>
<MoreVertIcon />
</IconButton>
{workflowMenuButtons}
</div>
) : null}
{data.actions !== undefined && data.actions !== null ? (
<div style={{position: "absolute", top: 10, right: 10, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={menuClick}
style={{ padding: "0px", color: "#979797" }}
>
<MoreVertIcon />
</IconButton>
{workflowMenuButtons}
</div>
) : null}
{(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data.input_markdown !== undefined && data.input_markdown !== null && data.input_markdown !== "") ?
<Tooltip title="Edit Form" placement="top">
<div style={{position: "absolute", top: 45, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={() => {
navigate(`/forms/${data.id}`)
}}
style={{ padding: "0px", color: "#979797" }}
>
<EditNoteIcon />
</IconButton>
{workflowMenuButtons}
</div>
</Tooltip>
: null}
</Grid>
</Paper>
</div>