Added detection pages and fixed more form pages
This commit is contained in:
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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, }}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user