Merge branch '2.0.0' into sso_2.0.0
This commit is contained in:
@@ -1129,7 +1129,7 @@ const AppFramework = (props) => {
|
||||
}, [newSelectedApp])
|
||||
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const imgSize = 50;
|
||||
var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ const AppSelection = props => {
|
||||
document.title = "Choose your apps"
|
||||
const ref = useRef()
|
||||
let navigate = useNavigate();
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
|
||||
useEffect(() => {
|
||||
if (newSelectedApp === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
|
||||
//const theme = useTheme();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import theme from "../theme.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
import { toast } from 'react-toastify';
|
||||
import theme from "../theme.jsx";
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||
|
||||
import {
|
||||
InputAdornment,
|
||||
@@ -79,7 +80,7 @@ const ConfigureWorkflow = (props) => {
|
||||
useEffect(() => {
|
||||
if (requiredActions.length === 0) {
|
||||
if (setConfigurationFinished !== undefined) {
|
||||
setConfigurationFinished(true)
|
||||
setConfigurationFinished(true)
|
||||
}
|
||||
}
|
||||
}, [requiredActions])
|
||||
@@ -141,17 +142,18 @@ const ConfigureWorkflow = (props) => {
|
||||
|
||||
// Where is this from?
|
||||
if (workflow === undefined || workflow === null || workflow.id === undefined) {
|
||||
return null;
|
||||
//console.log("Workflow is undefined or null: ", workflow)
|
||||
return null
|
||||
}
|
||||
|
||||
if (apps === undefined || apps === null) {
|
||||
console.log("Apps is undefined or null: ", apps)
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (appAuthentication === undefined || appAuthentication === null) {
|
||||
console.log("App authentication is undefined or null: ", appAuthentication)
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const getApp = (actionId, appId) => {
|
||||
@@ -1386,9 +1388,23 @@ const ConfigureWorkflow = (props) => {
|
||||
: null
|
||||
}
|
||||
|
||||
<div style={{marginTop: 10, }} />
|
||||
|
||||
{/*
|
||||
<WorkflowValidationTimeline
|
||||
workflow={workflow}
|
||||
|
||||
apps={apps}
|
||||
|
||||
getParents={undefined}
|
||||
execution={undefined}
|
||||
/>
|
||||
<div style={{marginBottom: 10, }} />
|
||||
*/}
|
||||
|
||||
{requiredActions.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body2" style={{}}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Please configure the following steps to help us complete your workflow. This can also be done later.
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1,22 +1,57 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Typography,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Select,
|
||||
MenuItem,
|
||||
Divider,
|
||||
FormLabel,
|
||||
} from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { toast } from "react-toastify";
|
||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
import theme from '../theme.jsx';
|
||||
|
||||
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, ...otherProps }) => {
|
||||
|
||||
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => {
|
||||
const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
|
||||
const [fileData, setFileData] = React.useState("");
|
||||
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
|
||||
const [filteredBarchart, setFilteredBarchart] = React.useState(null)
|
||||
|
||||
const [responseValue, setResponseValue] = React.useState("No response action")
|
||||
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
|
||||
|
||||
console.log("Rulemapping: ", ruleMapping)
|
||||
useEffect(() => {
|
||||
|
||||
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
||||
//const resp = LoadStats(globalUrl, ruleName)
|
||||
//const resp = LoadStats(globalUrl, "app_executions_test2")
|
||||
const resp = LoadStats(globalUrl, "app_executions_cloud")
|
||||
resp.then((data) => {
|
||||
if (data === undefined) {
|
||||
setFilteredBarchart([])
|
||||
} else {
|
||||
setFilteredBarchart(data)
|
||||
}
|
||||
})
|
||||
|
||||
if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) {
|
||||
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping)
|
||||
}
|
||||
}, [])
|
||||
|
||||
console.log("Response Value: ", responseValue)
|
||||
|
||||
const handleSwitchChange = (event) => {
|
||||
if (folderDisabled) {
|
||||
toast.warn("Enable the directory to enable individual rules");
|
||||
@@ -32,7 +67,8 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
||||
toggleRule(file_id, !newIsEnabled, globalUrl, () => {
|
||||
setIsEnabled(newIsEnabled);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const UpdateText = (text) => {
|
||||
fetch(`${globalUrl}/api/v1/files/${file_id}/edit`, {
|
||||
@@ -64,32 +100,121 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
||||
<Card style={{
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
minHeight: 100,
|
||||
marginBottom: 10,
|
||||
paddingBottom: 0,
|
||||
}}>
|
||||
<CardContent>
|
||||
<CardContent
|
||||
style={{
|
||||
padding: "10px 30px 0px 30px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
color: "white",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{ruleName}</Typography>
|
||||
<Typography variant="h6">{ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})</Typography>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<IconButton onClick={() => openEditBar(file_id, setOpenCodeEditor, setFileData, globalUrl)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<Switch
|
||||
checked={isEnabled && !folderDisabled}
|
||||
onChange={handleSwitchChange}
|
||||
disabled={false}
|
||||
/>
|
||||
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="Response Action"
|
||||
value={responseValue}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
toast("Changing response: " + e.target.value)
|
||||
console.log("Target: ", e.target.value)
|
||||
|
||||
setResponseValue(e.target.value)
|
||||
|
||||
// FIXME: Handle:
|
||||
// 1. Get the current cache for the detection
|
||||
// 2. Create a new mapping for Detection -> Response
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
height: 40,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value="No response action"
|
||||
>
|
||||
<em>No selected response</em>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{availableDetection === undefined || availableDetection === null ? null : availableDetection.map((data, index) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
overflowX: "auto",
|
||||
}}
|
||||
value={data.name}
|
||||
>
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
|
||||
|
||||
<Tooltip title="Edit Rule" placement="top">
|
||||
<IconButton onClick={() => openEditBar(file_id, setOpenCodeEditor, setFileData, globalUrl)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={isEnabled && !folderDisabled ? "Disable Rule" : "Enable Rule"} placement="top">
|
||||
<Switch
|
||||
checked={isEnabled && !folderDisabled}
|
||||
onChange={handleSwitchChange}
|
||||
disabled={false}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
overflow: 'visible',
|
||||
zIndex: 10,
|
||||
//border: "1px solid rgba(255,255,255,0.3)",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
marginTop: 5,
|
||||
|
||||
minHeight: 40,
|
||||
maxHeight: 40,
|
||||
}}>
|
||||
{filteredBarchart === null ? null :
|
||||
<DashboardBarchart
|
||||
timelineData={filteredBarchart}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Typography variant="body2" style={{ marginTop: '2%' }}>
|
||||
{description}
|
||||
</Typography>
|
||||
*/}
|
||||
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useContext } from "react";
|
||||
import theme from '../theme.jsx';
|
||||
import { isMobile } from "react-device-detect"
|
||||
import { MuiChipsInput } from "mui-chips-input";
|
||||
import { toast } from "react-toastify"
|
||||
import UsecaseSearch from "../components/UsecaseSearch.jsx"
|
||||
import WorkflowGrid from "../components/WorkflowGrid.jsx"
|
||||
import dayjs from 'dayjs';
|
||||
@@ -60,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, } = 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
|
||||
|
||||
@@ -79,15 +81,30 @@ const EditWorkflow = (props) => {
|
||||
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 [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 [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();
|
||||
|
||||
if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) {
|
||||
setTimeout(() => {
|
||||
const foundScroll = document.getElementById(scrollTo)
|
||||
if (foundScroll !== null) {
|
||||
// Smooth scroll
|
||||
foundScroll.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
|
||||
}, 200)
|
||||
setScrollDone(true)
|
||||
|
||||
}
|
||||
|
||||
// Gets the generated workflow
|
||||
const getGeneratedWorkflow = (workflow_id) => {
|
||||
fetch(globalUrl + "/api/v1/workflows/" + workflow_id, {
|
||||
const getGeneratedWorkflow = (workflow_id) => {
|
||||
const url = `${globalUrl}/api/v1/workflows/${workflow_id}`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -95,54 +112,55 @@ const EditWorkflow = (props) => {
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 when getting workflow");
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 when getting workflow");
|
||||
}
|
||||
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.id === workflow_id) {
|
||||
console.log("GOT WORKFLOW: ", responseJson)
|
||||
if (name === "") {
|
||||
innerWorkflow.name = responseJson.name
|
||||
setName(responseJson.name)
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.id === workflow_id) {
|
||||
console.log("GOT WORKFLOW: ", responseJson)
|
||||
if (name === "") {
|
||||
innerWorkflow.name = responseJson.name
|
||||
setName(responseJson.name)
|
||||
}
|
||||
|
||||
if (description === "") {
|
||||
innerWorkflow.description = responseJson.description
|
||||
setDescription(description)
|
||||
}
|
||||
|
||||
if (newWorkflowTags === []) {
|
||||
innerWorkflow.tags = responseJson.tags
|
||||
setNewWorkflowTags(responseJson.tags)
|
||||
}
|
||||
|
||||
if (selectedUsecases === []) {
|
||||
selectedUsecases = responseJson.usecase_ids
|
||||
}
|
||||
|
||||
innerWorkflow.id = responseJson.id
|
||||
innerWorkflow.blogpost = responseJson.blogpost
|
||||
innerWorkflow.actions = responseJson.actions
|
||||
innerWorkflow.triggers = responseJson.triggers
|
||||
innerWorkflow.branches = responseJson.branches
|
||||
innerWorkflow.comments = responseJson.comments
|
||||
innerWorkflow.workflow_variables = responseJson.workflow_variables
|
||||
innerWorkflow.execution_variables = responseJson.execution_variables
|
||||
|
||||
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
setUpdate(Math.random())
|
||||
if (description === "") {
|
||||
innerWorkflow.description = responseJson.description
|
||||
setDescription(description)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast(error.toString());
|
||||
console.log("Get workflow error: ", error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
if (newWorkflowTags === []) {
|
||||
innerWorkflow.tags = responseJson.tags
|
||||
setNewWorkflowTags(responseJson.tags)
|
||||
}
|
||||
|
||||
if (selectedUsecases === []) {
|
||||
selectedUsecases = responseJson.usecase_ids
|
||||
}
|
||||
|
||||
innerWorkflow.id = responseJson.id
|
||||
innerWorkflow.blogpost = responseJson.blogpost
|
||||
innerWorkflow.actions = responseJson.actions
|
||||
innerWorkflow.triggers = responseJson.triggers
|
||||
innerWorkflow.branches = responseJson.branches
|
||||
innerWorkflow.comments = responseJson.comments
|
||||
innerWorkflow.workflow_variables = responseJson.workflow_variables
|
||||
innerWorkflow.execution_variables = responseJson.execution_variables
|
||||
|
||||
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast(error.toString());
|
||||
console.log("Get workflow error: ", error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
if (foundWorkflowId.length > 0) {
|
||||
getGeneratedWorkflow(foundWorkflowId)
|
||||
@@ -162,6 +180,7 @@ const EditWorkflow = (props) => {
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor={"right"}
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
@@ -186,38 +205,42 @@ 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={`/workflows/${workflow.id}/run`}
|
||||
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="/usecases" 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>
|
||||
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>
|
||||
</Typography>
|
||||
|
||||
{/*
|
||||
<div style={{marginTop: 10, marginBottom: 10, marginRight: 50, }}>
|
||||
<WorkflowValidationTimeline
|
||||
originalWorkflow={workflow}
|
||||
|
||||
apps={apps}
|
||||
workflow={workflow}
|
||||
/>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
{showUpload === true ?
|
||||
<div style={{ float: "right" }}>
|
||||
@@ -247,7 +270,7 @@ const EditWorkflow = (props) => {
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<FormControl>
|
||||
<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, }}>
|
||||
<div style={{borderTop: "1px solid rgba(255,255,255,0.5)", width: 600, position: "fixed", right: 20, bottom: 0, zIndex: 1002, backgroundColor: "rgba(53,53,53,1)", height: 75, paddingTop: 20, paddingLeft: 75, }}>
|
||||
{/*
|
||||
<Button
|
||||
style={{}}
|
||||
@@ -287,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) {
|
||||
@@ -363,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>
|
||||
@@ -595,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>
|
||||
@@ -602,7 +632,7 @@ const EditWorkflow = (props) => {
|
||||
userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ?
|
||||
userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ?
|
||||
<Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}>
|
||||
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.
|
||||
Your organization does not have any suborgs yet, OR you may not have access to any suborgs directly.. Please <a href="/admin?tab=suborgs" style={{textDecoration: "none", color: "#f86a3e"}} target="_blank">make one</a> or get access to suborgs by admin, then try again.
|
||||
</Typography>
|
||||
:
|
||||
<Typography variant="body2" style={{marginTop: 10, color: "rgba(255,255,255,0.7)"}}>
|
||||
@@ -700,156 +730,11 @@ const EditWorkflow = (props) => {
|
||||
</Link>
|
||||
}
|
||||
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }} />
|
||||
|
||||
<Typography variant="h6" style={{marginTop: 50, }}>
|
||||
Input fields
|
||||
</Typography>
|
||||
<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={`/workflows/${workflow.id}/run`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>workflow run page</a>. If chosen in the User Input node, these will be required fields.
|
||||
</Typography>
|
||||
|
||||
|
||||
{inputQuestions.map((data, index) => {
|
||||
console.log("Inputfield: ", data)
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", }}>
|
||||
<TextField
|
||||
disabled={data.deleted === true}
|
||||
style={{
|
||||
height: 50,
|
||||
flex: 2,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginRight: 5,
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder="Question"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
defaultValue={data.name}
|
||||
onChange={(e) => {
|
||||
inputQuestions[index].name = e.target.value
|
||||
setInputQuestions(inputQuestions)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
minHeight: 50,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
disabled={data.deleted === true}
|
||||
style={{
|
||||
height: 50,
|
||||
flex: 2,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginRight: 5,
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder="JSON key"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
defaultValue={data.value}
|
||||
onChange={(e) => {
|
||||
inputQuestions[index].value = e.target.value
|
||||
setInputQuestions(inputQuestions)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
minHeight: 50,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
style={{ maxWidth: 50, marginLeft: 15 }}
|
||||
disabled={data.deleted === true}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
// Remove current index
|
||||
console.log("Removing index: ", index)
|
||||
inputQuestions[index].deleted = true
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
>
|
||||
<RemoveIcon style={{}} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
style={{ maxWidth: 50, marginLeft: 15, marginTop: 20, }}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
inputQuestions.push({
|
||||
"name": "",
|
||||
"value": "",
|
||||
"deleted": false,
|
||||
"required": false
|
||||
})
|
||||
setInputQuestions(inputQuestions)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
>
|
||||
<AddIcon style={{}} />
|
||||
</Button>
|
||||
{/*<Divider style={{marginTop: 20, marginBottom: 20, }} />*/}
|
||||
|
||||
|
||||
{inputQuestions.length === 0 ? null :
|
||||
<div>
|
||||
<Typography variant="h6" style={{marginTop: 50, }}>
|
||||
Input Markdown
|
||||
</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
color="primary"
|
||||
value={inputMarkdown}
|
||||
onChange={(e) => {
|
||||
setInputMarkdown(e.target.value)
|
||||
workflow.input_markdown = e.target.value
|
||||
setWorkflow(workflow)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
|
||||
{/*
|
||||
<Typography variant="h6" style={{marginTop: 50, }}>
|
||||
Output Markdown
|
||||
</Typography>
|
||||
<TextField
|
||||
multiLine
|
||||
rows={3}
|
||||
fullWidth
|
||||
color="primary"
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
}
|
||||
|
||||
<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">
|
||||
@@ -998,8 +883,240 @@ const EditWorkflow = (props) => {
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }} />
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
{inputQuestions.map((data, index) => {
|
||||
var showListinfo = false
|
||||
if (data.value !== undefined && data.value !== null && data.value.length > 0) {
|
||||
if (data.value.includes(";")) {
|
||||
showListinfo = true
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", }}>
|
||||
<TextField
|
||||
disabled={data.deleted === true}
|
||||
style={{
|
||||
flex: 2,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginRight: 5,
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder="Question"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
defaultValue={data.name}
|
||||
onChange={(e) => {
|
||||
inputQuestions[index].name = e.target.value
|
||||
setInputQuestions(inputQuestions)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
disabled={data.deleted === true}
|
||||
style={{
|
||||
flex: 2,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginRight: 5,
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder="$exec JSON key"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
helperText={showListinfo === true ? "Dropdown list" : null}
|
||||
defaultValue={data.value}
|
||||
onChange={(e) => {
|
||||
// Replace multiple semicolon with one
|
||||
e.target.value = e.target.value.replace(";;", ";")
|
||||
|
||||
inputQuestions[index].value = e.target.value
|
||||
setInputQuestions(inputQuestions)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
style={{ maxWidth: 50, marginLeft: 15 }}
|
||||
disabled={data.deleted === true}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
// Remove current index
|
||||
console.log("Removing index: ", index)
|
||||
inputQuestions[index].deleted = true
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
>
|
||||
<RemoveIcon style={{}} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
style={{ maxWidth: 50, marginLeft: 15, marginTop: 20, }}
|
||||
variant="outlined"
|
||||
|
||||
disabled={inputQuestions !== undefined && inputQuestions !== null && inputQuestions.length > 5}
|
||||
onClick={() => {
|
||||
inputQuestions.push({
|
||||
"name": "",
|
||||
"value": "",
|
||||
"deleted": false,
|
||||
"required": false
|
||||
})
|
||||
setInputQuestions(inputQuestions)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
>
|
||||
<AddIcon style={{}} />
|
||||
</Button>
|
||||
|
||||
<div id="input_markdown">
|
||||
<Typography variant="h6" style={{marginTop: 50, }}>
|
||||
Input Markdown
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginBottom: 20, }}>
|
||||
Markdown will be shown on the <a href={`/forms/${workflow.id}`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Form page</a>. Output for a Workflow is also shown in Markdown, and is controlled by the LAST action that runs.
|
||||
</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
minRows={3}
|
||||
fullWidth
|
||||
color="primary"
|
||||
value={inputMarkdown}
|
||||
onKeyDown={(e) => {
|
||||
//console.log("KEY: ", e.key)
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
|
||||
onChange={(e) => {
|
||||
if (setRealtimeMarkdown !== undefined) {
|
||||
setRealtimeMarkdown(e.target.value)
|
||||
}
|
||||
|
||||
setInputMarkdown(e.target.value)
|
||||
workflow.input_markdown = e.target.value
|
||||
setWorkflow(workflow)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
</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}
|
||||
|
||||
|
||||
|
||||
<Tooltip color="primary" title={"Add more details"} placement="top">
|
||||
@@ -1047,7 +1164,7 @@ const EditWorkflow = (props) => {
|
||||
</span>
|
||||
: null}
|
||||
|
||||
{newWorkflow === true && name.length > 2 ?
|
||||
{/*newWorkflow === true && name.length > 2 ?
|
||||
<div style={{marginLeft: 30, }}>
|
||||
<WorkflowGrid
|
||||
maxRows={1}
|
||||
@@ -1062,7 +1179,7 @@ const EditWorkflow = (props) => {
|
||||
onlyResults={true}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
: null*/}
|
||||
</FormControl>
|
||||
</Drawer>
|
||||
)
|
||||
|
||||
@@ -132,6 +132,8 @@ const Header = (props) => {
|
||||
isMobile,
|
||||
serverside,
|
||||
billingInfo,
|
||||
|
||||
notifications,
|
||||
} = props;
|
||||
const [isHeader, setIsHeader] = React.useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
@@ -315,7 +317,9 @@ const Header = (props) => {
|
||||
localStorage.setItem("globalUrl", responseJson.region_url);
|
||||
//globalUrl = responseJson.region_url
|
||||
}
|
||||
|
||||
if (responseJson["reason"] === "SSO_REDIRECT") {
|
||||
toast.info("Redirecting to SSO login page as SSO is required for this organization.")
|
||||
setTimeout(() => {
|
||||
toast.info(
|
||||
"Redirecting to SSO login page as SSO is required for this organization."
|
||||
@@ -428,26 +432,19 @@ const Header = (props) => {
|
||||
</MenuItem>
|
||||
</Link>
|
||||
|
||||
<Link to="/admin?admin_tab=priorities" style={hrefStyle}>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<NotificationsIcon style={{ marginRight: 5 }} /> Notifications
|
||||
</MenuItem>
|
||||
</Link>
|
||||
|
||||
<Divider style={{ marginTop: 10, marginBottom: 10, }} />
|
||||
<Link to="/docs" style={hrefStyle}>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<HelpOutlineIcon style={{ marginRight: 5 }} /> About
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/admin?admin_tab=priorities" style={hrefStyle}>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<NotificationsIcon style={{ marginRight: 5 }} /> Notifications ({
|
||||
notifications === undefined || notifications === null ? 0 :
|
||||
notifications?.filter((notification) => notification.read === false).length
|
||||
})
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{/*
|
||||
<Link to="/getting-started" style={hrefStyle}>
|
||||
<MenuItem
|
||||
@@ -469,7 +466,7 @@ const Header = (props) => {
|
||||
</MenuItem>
|
||||
</Link>
|
||||
|
||||
{userdata?.public_username === undefined || userdata?.public_username === null || userdata?.public_username.length <= 0 ? null :
|
||||
{/*userdata?.public_username === undefined || userdata?.public_username === null || userdata?.public_username.length <= 0 ? null :
|
||||
<Link to={`/creators/${userdata.public_username}`} style={hrefStyle}>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
@@ -479,9 +476,18 @@ const Header = (props) => {
|
||||
<EmojiObjectsIcon style={{ marginRight: 5 }} /> Creator page
|
||||
</MenuItem>
|
||||
</Link>
|
||||
}
|
||||
*/}
|
||||
|
||||
<Divider style={{ marginTop: 10, marginBottom: 10, }} />
|
||||
<Link to="/docs" style={hrefStyle}>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<HelpOutlineIcon style={{ marginRight: 5 }} /> About
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<MenuItem
|
||||
style={{ color: "white" }}
|
||||
onClick={(event) => {
|
||||
@@ -495,7 +501,7 @@ const Header = (props) => {
|
||||
<Divider style={{ marginBottom: 10, }} />
|
||||
|
||||
<Typography variant="body2" color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, }}>
|
||||
Version: 1.4.0
|
||||
Version: 1.4.5
|
||||
</Typography>
|
||||
</Menu>
|
||||
</span>
|
||||
@@ -922,7 +928,6 @@ const Header = (props) => {
|
||||
}}
|
||||
>
|
||||
{avatarMenu}
|
||||
{/*notificationMenu*/}
|
||||
{supportMenu}
|
||||
{logoCheck}
|
||||
</span>
|
||||
|
||||
@@ -167,7 +167,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
//console.log("APP: ", selectedApp)
|
||||
if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") {
|
||||
handleOauth2Request(
|
||||
"efe4c3fe-84a1-4821-a84f-23a6cfe8e72d",
|
||||
"fd55c175-aa30-4fa6-b303-09a29fb3f750",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["Mail.ReadWrite", "Mail.Send", "offline_access"],
|
||||
@@ -524,7 +524,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
//alert('"Secure Payment" window closed!');
|
||||
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication(true, true, true);
|
||||
getAppAuthentication(true, true, true, selectedAction.id)
|
||||
}
|
||||
|
||||
toast("Authentication successful!")
|
||||
@@ -538,7 +538,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
setFinalized(true)
|
||||
}
|
||||
} else {
|
||||
console.log("Not closed")
|
||||
//console.log("Not closed")
|
||||
}
|
||||
}, 1000);
|
||||
//do {
|
||||
@@ -739,7 +739,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<span style={{}}>
|
||||
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. {authenticationType.type === "oauth2-app" ? null : <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b> - </span>}
|
||||
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b> - </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="norefferer"
|
||||
|
||||
@@ -385,6 +385,44 @@ const OrgHeaderexpanded = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const HandleTestSSO = () => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||
const data = {
|
||||
org_id: selectedOrganization?.id,
|
||||
sso_test: true,
|
||||
}
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
toast.error("Failed to test sso. Please try again later or contact support@shuffler.io if issue persist.")
|
||||
return
|
||||
}
|
||||
return response.json();
|
||||
}).then((responjson) => {
|
||||
if (responjson["reason"] === "SSO_REDIRECT") {
|
||||
setTimeout(() => {
|
||||
toast.info("Redirecting to SSO login page as SSO is required for this organization.")
|
||||
window.location.href = responjson["url"];
|
||||
return
|
||||
}, 2000)
|
||||
} else {
|
||||
toast.error("No SSO found for this org. Please set up sso for this org.")
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log("error for sso test is: ", err)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Grid container spacing={3} style={{ textAlign: "left" }}>
|
||||
|
||||
@@ -90,9 +90,10 @@ import {
|
||||
Circle as CircleIcon,
|
||||
SquareFoot as SquareFootIcon,
|
||||
Storage as StorageIcon,
|
||||
Check as CheckIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
export const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
borderColor: "#f85a3e !important",
|
||||
},
|
||||
@@ -1500,92 +1501,7 @@ const ParsedAction = (props) => {
|
||||
<DescriptionIcon style={{ color: "rgba(255,255,255,0.7)" }} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
{/*
|
||||
<IconButton
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
height: 30,
|
||||
marginLeft: 15,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
onClick={() => {}}
|
||||
>
|
||||
<a
|
||||
href="https://shuffler.io/docs/workflows#nodes"
|
||||
rel="norefferer"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="What are actions?"
|
||||
placement="top"
|
||||
>
|
||||
<HelpOutlineIcon style={{ color: "rgba(255,255,255,0.7)" }} />
|
||||
</Tooltip>
|
||||
</a>
|
||||
</IconButton>
|
||||
*/}
|
||||
{/*
|
||||
<IconButton
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
height: 30,
|
||||
marginLeft: 15,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
onClick={() => {
|
||||
//setAuthenticationModalOpen(true);
|
||||
console.log("Should enable/disable magic!")
|
||||
console.log("Action: ", selectedAction)
|
||||
if (selectedAction.run_magic_output === undefined) {
|
||||
selectedAction.run_magic_output = true
|
||||
} else {
|
||||
if (selectedAction.run_magic_output === true) {
|
||||
selectedAction.run_magic_output = false
|
||||
} else {
|
||||
selectedAction.run_magic_output = true
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction)
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={selectedAction.run_magic_output === undefined || selectedAction.run_magic_output === null || selectedAction.run_magic_output === false ? "Click to enable magic parsing" : "Click to disable magic parsing"}
|
||||
placement="top"
|
||||
>
|
||||
<AutoFixHighIcon style={{ color: selectedAction.run_magic_output === undefined || selectedAction.run_magic_output === null || selectedAction.run_magic_output === false ? "rgba(255,255,255,0.7)" : "#f86a3e"}} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
*/}
|
||||
{/*
|
||||
<IconButton
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
height: 30,
|
||||
marginLeft: 15,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
onClick={() => {
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Find related tworkflows"}
|
||||
placement="top"
|
||||
>
|
||||
<a href={`https://shuffler.io/search?tab=workflows&q=${selectedAction.app_name}`} target="_blank">
|
||||
<SearchIcon style={{ color: "rgba(255,255,255,0.7)"}} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
*/}
|
||||
<IconButton
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
@@ -1602,6 +1518,10 @@ const ParsedAction = (props) => {
|
||||
aiSubmit("Fill based on previous values", undefined, undefined, selectedAction)
|
||||
//}
|
||||
setAutocompleting(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setAutocompleting(false)
|
||||
}, 3000)
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
@@ -1998,7 +1918,7 @@ const ParsedAction = (props) => {
|
||||
|
||||
for (let [key,keyval] in Object.entries(selectedAction.parameters)) {
|
||||
if (selectedAction.parameters[key].configuration === false) {
|
||||
console.log("FIELDSKIP: ", selectedAction.parameters[key].name)
|
||||
//console.log("FIELDSKIP: ", selectedAction.parameters[key].name)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2085,7 +2005,18 @@ const ParsedAction = (props) => {
|
||||
}}
|
||||
value={data}
|
||||
>
|
||||
{data.last_modified === true ?
|
||||
|
||||
{data?.validation?.valid === true ?
|
||||
<Tooltip title="Authentication has been validated" placement="top">
|
||||
<Chip
|
||||
style={{marginLeft: 0, padding: 0, marginRight: 10, cursor: "pointer", borderColor: green, }}
|
||||
label={"Valid"}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
/>
|
||||
</Tooltip>
|
||||
: null }
|
||||
{data?.last_modified === true ?
|
||||
<Chip
|
||||
style={{marginLeft: 0, padding: 0, marginRight: 10, cursor: "pointer",}}
|
||||
label={"Latest"}
|
||||
@@ -2093,14 +2024,14 @@ const ParsedAction = (props) => {
|
||||
color="secondary"
|
||||
/>
|
||||
: null}
|
||||
{data.app.app_version !== undefined && data.app.app_version !== null && data.app.app_version !== "" && data.app.app_version !== "undefined" ?
|
||||
{/*data.app.app_version !== undefined && data.app.app_version !== null && data.app.app_version !== "" && data.app.app_version !== "undefined" ?
|
||||
<Chip
|
||||
style={{marginLeft: 0, padding: 0, marginRight: 10, cursor: "pointer",}}
|
||||
label={data.app.app_version}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
/>
|
||||
: null}
|
||||
: null*/}
|
||||
{data.label}
|
||||
</MenuItem>
|
||||
);
|
||||
@@ -2342,6 +2273,7 @@ const ParsedAction = (props) => {
|
||||
value={selectedAction}
|
||||
classes={{ inputRoot: classes.inputRoot }}
|
||||
groupBy={(option) => {
|
||||
// FIXME: Sorting
|
||||
// Most popular
|
||||
// Is categorized
|
||||
// Uncategorized
|
||||
@@ -2364,7 +2296,6 @@ const ParsedAction = (props) => {
|
||||
},
|
||||
}}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
//console.log("Option contains?: ", inputValue, options)
|
||||
const lowercaseValue = inputValue.toLowerCase()
|
||||
options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue))
|
||||
|
||||
@@ -2449,22 +2380,6 @@ const ParsedAction = (props) => {
|
||||
extraUrl = descSplit[descSplit.length-1]
|
||||
}
|
||||
|
||||
//for (let [line,lineval] in Object.entries(descSplit)) {
|
||||
// if (descSplit[line].includes("http") && descSplit[line].includes("://")) {
|
||||
// const urlsplit = descSplit[line].split("/")
|
||||
// try {
|
||||
// extraUrl = "/"+urlsplit.slice(3, urlsplit.length).join("/")
|
||||
// } catch (e) {
|
||||
// //console.log("Failed - running with -1")
|
||||
// extraUrl = "/"+urlsplit.slice(3, urlsplit.length-1).join("/")
|
||||
// }
|
||||
|
||||
|
||||
// //console.log("NO BASEURL TOO!! Why missing last one in certain scenarios (sevco)?", extraUrl, urlsplit, descSplit[line])
|
||||
// //break
|
||||
// }
|
||||
//}
|
||||
|
||||
if (extraUrl.length > 0) {
|
||||
if (extraUrl.includes(" ")) {
|
||||
extraUrl = extraUrl.split(" ")[0]
|
||||
@@ -2492,6 +2407,7 @@ const ParsedAction = (props) => {
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => {
|
||||
|
||||
if (params.inputProps?.value) {
|
||||
const prefixes = ["Post", "Put", "Patch"];
|
||||
for (let prefix of prefixes) {
|
||||
@@ -2512,84 +2428,86 @@ const ParsedAction = (props) => {
|
||||
}
|
||||
|
||||
|
||||
const actionDescription = (
|
||||
<Box
|
||||
p={1.5}
|
||||
borderRadius={3}
|
||||
boxShadow={2}
|
||||
backgroundColor={theme.palette.textFieldStyle}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="body1" style={{ flexGrow: 1 }}>
|
||||
{params.inputProps.value}
|
||||
</Typography>
|
||||
<IconButton size="small"
|
||||
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
||||
onClick={() => {
|
||||
setHiddenDescription(true)
|
||||
const inputElement = document.getElementById(uiBox);
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Divider sx={{ backgroundColor: theme.palette.surfaceColor, marginTop: "5px", marginBottom : "10px", height: "3px" }}/>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography variant="body2" mb={0.5}>
|
||||
<strong>Description: </strong> {selectedAction?.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip title={actionDescription}
|
||||
placement="right"
|
||||
open={!hiddenDescription}
|
||||
PopperProps={{
|
||||
sx: {
|
||||
'& .MuiTooltip-tooltip': {
|
||||
backgroundColor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: 'transparent',
|
||||
},
|
||||
},
|
||||
}}
|
||||
const actionDescription = null
|
||||
/*(
|
||||
<Box
|
||||
p={1.5}
|
||||
borderRadius={3}
|
||||
boxShadow={2}
|
||||
backgroundColor={theme.palette.textFieldStyle}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<TextField
|
||||
{...params}
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="body1" style={{ flexGrow: 1 }}>
|
||||
{params.inputProps.value}
|
||||
</Typography>
|
||||
<IconButton size="small"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
||||
onClick={() => {
|
||||
setHiddenDescription(true)
|
||||
const inputElement = document.getElementById(uiBox);
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Divider sx={{ backgroundColor: theme.palette.surfaceColor, marginTop: "5px", marginBottom : "10px", height: "3px" }}/>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography variant="body2" mb={0.5}>
|
||||
<strong>Description: </strong> {selectedAction?.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
*/
|
||||
|
||||
data-lpignore="true"
|
||||
autocomplete="off"
|
||||
dataLPIgnore="true"
|
||||
autoComplete="off"
|
||||
|
||||
color="primary"
|
||||
id="checkbox-search"
|
||||
variant="body1"
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
return (
|
||||
<Tooltip title={actionDescription}
|
||||
placement="right"
|
||||
open={!hiddenDescription}
|
||||
PopperProps={{
|
||||
sx: {
|
||||
'& .MuiTooltip-tooltip': {
|
||||
backgroundColor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: 'transparent',
|
||||
},
|
||||
},
|
||||
}}
|
||||
label={isIntegration ? "Choose a category" : "Find Actions"}
|
||||
variant="outlined"
|
||||
name={`disable_autocomplete_${Math.random()}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<TextField
|
||||
{...params}
|
||||
|
||||
data-lpignore="true"
|
||||
autocomplete="off"
|
||||
dataLPIgnore="true"
|
||||
autoComplete="off"
|
||||
|
||||
color="primary"
|
||||
id="checkbox-search"
|
||||
variant="body1"
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
label={isIntegration ? "Choose a category" : "Find Actions"}
|
||||
variant="outlined"
|
||||
name={`disable_autocomplete_${Math.random()}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/*setNewSelectedAction !== undefined ?
|
||||
@@ -2939,58 +2857,24 @@ const ParsedAction = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (
|
||||
(selectedAction.auth_not_required !== undefined && !selectedAction.auth_not_required) &&
|
||||
selectedActionParameters[count] !== undefined &&
|
||||
selectedActionParameters[count] !== null &&
|
||||
selectedActionParameters[count].value !== undefined &&
|
||||
selectedAction.parameters[count] !== undefined &&
|
||||
selectedAction.parameters[count] !== null &&
|
||||
selectedAction.parameters[count].value !== undefined &&
|
||||
selectedAction.selectedAuthentication !== undefined &&
|
||||
selectedAction.selectedAuthentication.fields !== undefined &&
|
||||
selectedAction.selectedAuthentication.fields[data.name] !==
|
||||
undefined
|
||||
) {
|
||||
*/
|
||||
|
||||
/*
|
||||
if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) {
|
||||
|
||||
// This sets the placeholder in the frontend. (Replaced in backend)
|
||||
selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name];
|
||||
selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name];
|
||||
setSelectedAction(selectedAction);
|
||||
//setUpdate(Math.random())
|
||||
|
||||
if (authWritten) {
|
||||
return null
|
||||
}
|
||||
|
||||
authWritten = true
|
||||
return (
|
||||
<Typography
|
||||
key={count}
|
||||
id="skip_auth"
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 5 }}
|
||||
>
|
||||
Authentication fields are hidden
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
*/
|
||||
if (selectedAction.parameters === undefined || selectedAction.parameters === null || selectedAction.parameters.length !== selectedActionParameters.length) {
|
||||
|
||||
//selectedAction.parameters = selectedActionParameters
|
||||
console.log("PARAM BUG: ", selectedAction)
|
||||
}
|
||||
|
||||
//!selectedAction.auth_not_required &&
|
||||
if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) {
|
||||
|
||||
// This sets the placeholder in the frontend. (Replaced in backend)
|
||||
selectedActionParameters[count].value =
|
||||
selectedAction.selectedAuthentication.fields[data.name];
|
||||
selectedAction.parameters[count].value =
|
||||
selectedAction.selectedAuthentication.fields[data.name];
|
||||
if (selectedActionParameters[count] !== undefined) {
|
||||
selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name]
|
||||
}
|
||||
|
||||
if (selectedAction.parameters[count] !== undefined) {
|
||||
selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name]
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction);
|
||||
//setUpdate(Math.random())
|
||||
|
||||
@@ -3064,7 +2948,7 @@ const ParsedAction = (props) => {
|
||||
|
||||
if (data.value.length === 0) {
|
||||
if (data.name.toLowerCase() === "headers") {
|
||||
console.log("Should show headers field instead with + and -!")
|
||||
//console.log("Should show headers field instead with + and -!")
|
||||
|
||||
// Check if file ID exists
|
||||
//
|
||||
@@ -3360,13 +3244,6 @@ const ParsedAction = (props) => {
|
||||
<IconButton size="small"
|
||||
onClick={() => {
|
||||
setUiBox("closed")
|
||||
|
||||
/*
|
||||
const inputElement = document.getElementById(uiBox);
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
*/
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
|
||||
@@ -393,7 +393,10 @@ const Priorities = (props) => {
|
||||
|
||||
return (
|
||||
<div style={{width: clickedFromOrgTab ? 1030:1000, padding: clickedFromOrgTab ? 27:null, height: clickedFromOrgTab ? "auto":null, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
|
||||
<h2 style={{ display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, marginTop: clickedFromOrgTab?40:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications</h2>
|
||||
<h2 style={{ display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, marginTop: clickedFromOrgTab?40:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications ({
|
||||
notifications?.filter((notification) => showRead === true || notification.read === false).length
|
||||
})</h2>
|
||||
|
||||
<span style={{ marginLeft: clickedFromOrgTab?null:25, color: clickedFromOrgTab?"#9E9E9E":null, }}>
|
||||
Notifications help you find potential problems with your workflows and apps.
|
||||
<a
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
const Priority = (props) => {
|
||||
const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
let navigate = useNavigate();
|
||||
|
||||
if (window.location.pathname === "/workflows") {
|
||||
|
||||
@@ -71,7 +71,7 @@ const SearchData = props => {
|
||||
// return null
|
||||
//}
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
// if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) {
|
||||
// setModalOpen(false)
|
||||
// }
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
|
||||
|
||||
import { validateJson } from "../views/Workflows.jsx";
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
|
||||
import { padding, textAlign } from '@mui/system';
|
||||
|
||||
@@ -349,7 +349,7 @@ const UsecaseSearch = (props) => {
|
||||
const [selectedAction, setSelectedAction] = React.useState({});
|
||||
const [firstRequest, setFirstRequest] = React.useState(true);
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
//const alert = useAlert()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -161,7 +161,7 @@ const WelcomeForm = (props) => {
|
||||
const [clickdiff, setclickdiff] = useState(0);
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
//const alert = useAlert();
|
||||
let navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
|
||||
const AppGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
|
||||
@@ -47,7 +47,7 @@ const WorkflowTemplatePopup = (props) => {
|
||||
|
||||
const [requestSent, setRequestSent] = React.useState(false)
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
let navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (modalOpen !== true) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
grey,
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
|
||||
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import theme from "../theme.jsx";
|
||||
const itemHeight = 24
|
||||
@@ -63,7 +64,7 @@ export const getParentNodes = (workflow, action) => {
|
||||
currentnode = workflow.triggers.find((element) => element.id === allkeys[parentkey])
|
||||
|
||||
if (currentnode === undefined) {
|
||||
console.log("Could not find parent node for: ", allkeys[parentkey])
|
||||
//console.log("Could not find parent node for: ", allkeys[parentkey])
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -128,11 +129,13 @@ export const getParentNodes = (workflow, action) => {
|
||||
}
|
||||
|
||||
const WorkflowValidationTimeline = (props) => {
|
||||
const { workflow, originalWorkflow, apps, getParents, execution} = props
|
||||
const { globalUrl, userdata, workflow, originalWorkflow, apps, getParents, execution, showHoverColor, } = props
|
||||
|
||||
const [hovering, setHovering] = useState(false)
|
||||
const [decidedColor, setDecidedColor] = useState(grey)
|
||||
const [isClicked, setIsClicked] = useState(false)
|
||||
|
||||
const showMiddle = false
|
||||
|
||||
|
||||
if (workflow === undefined || workflow === null) {
|
||||
return null
|
||||
}
|
||||
@@ -146,13 +149,11 @@ const WorkflowValidationTimeline = (props) => {
|
||||
}
|
||||
|
||||
if (workflow.triggers === undefined || workflow.triggers === null) {
|
||||
workflow.triggers = []
|
||||
|
||||
workflow.triggers = []
|
||||
}
|
||||
|
||||
if (workflow.branches === undefined || workflow.branches === null) {
|
||||
workflow.branches = []
|
||||
|
||||
workflow.branches = []
|
||||
}
|
||||
|
||||
var results = []
|
||||
@@ -260,6 +261,12 @@ const WorkflowValidationTimeline = (props) => {
|
||||
relevantactions.push(...newactions)
|
||||
}
|
||||
|
||||
console.log("Relevant actions (return null if 0-1): ", relevantactions)
|
||||
|
||||
if (relevantactions.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Sort according to how many parents a node has. MAY be wrong~
|
||||
relevantactions.sort((a, b) => {
|
||||
if (a.order === undefined) {
|
||||
@@ -279,15 +286,81 @@ const WorkflowValidationTimeline = (props) => {
|
||||
var skipped = false
|
||||
|
||||
var previousTools = false
|
||||
var scheduleNotStarted = false
|
||||
|
||||
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.validation_ran === false) {
|
||||
console.log("Validation didn't run. Why?")
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null && workflow.validation.errors.length > 0) {
|
||||
var newErrors = []
|
||||
for (var key in workflow.validation.errors) {
|
||||
const error = workflow.validation.errors[key]
|
||||
if (error.type === "SCHEDULE") {
|
||||
scheduleNotStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
newErrors.push(error)
|
||||
}
|
||||
|
||||
workflow.validation.errors = newErrors
|
||||
}
|
||||
|
||||
// Use this variable to control visualization
|
||||
//const showMiddle = false
|
||||
// border: workflow.validation.valid ? `2px solid ${green}` : "1px solid rgba(255,255,255,0.4)",
|
||||
var middleError = ""
|
||||
var startBranchColor = ""
|
||||
var middleBranchColor = ""
|
||||
|
||||
|
||||
const showHoverForClick = showHoverColor === true ? true : false
|
||||
return (
|
||||
<div style={{ padding: "10px 5px 10px 5px", borderRadius: theme.palette.borderRadius, }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 5px 10px 5px",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
|
||||
border: hovering === true && showHoverForClick === true ? `1px solid ${decidedColor}` : "1px solid rgba(255,255,255,0.0)",
|
||||
cursor: hovering === true && showHoverForClick === true ? "pointer" : "default",
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (isClicked === false) {
|
||||
setHovering(true)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isClicked === false) {
|
||||
setHovering(false)
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (showHoverForClick === true) {
|
||||
setIsClicked(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
{isClicked === false ? null :
|
||||
<WorkflowTemplatePopup2
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
|
||||
isModalOpenDefault={isClicked}
|
||||
workflowBuilt={true}
|
||||
setIsClicked={setIsClicked}
|
||||
inputWorkflowId={workflow.id}
|
||||
/>
|
||||
}
|
||||
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||
|
||||
{scheduleNotStarted === true ?
|
||||
null
|
||||
: null}
|
||||
|
||||
{relevantactions.map((action, index) => {
|
||||
action.result = {}
|
||||
if (results !== undefined) {
|
||||
@@ -309,8 +382,10 @@ const WorkflowValidationTimeline = (props) => {
|
||||
const validate = validateJson(action.result.result)
|
||||
if (validate.valid) {
|
||||
if (validate.result.success === true) {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
} else {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
}
|
||||
}
|
||||
@@ -319,9 +394,12 @@ const WorkflowValidationTimeline = (props) => {
|
||||
} else if (action.status === "SKIPPED") {
|
||||
branchcolor = grey
|
||||
} else {
|
||||
// FIXME: How do we handle this?
|
||||
if (action.status === undefined) {
|
||||
branchcolor = green
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
} else {
|
||||
nodecolor = red
|
||||
branchcolor = red
|
||||
}
|
||||
}
|
||||
@@ -389,29 +467,64 @@ const WorkflowValidationTimeline = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
var appgroup = []
|
||||
if (action.app_name === "shuffle-subflow") {
|
||||
if (action.status === "SUCCESS") {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
}
|
||||
|
||||
if (workflow.validation.subflow_apps !== undefined && workflow.validation.subflow_apps !== null && workflow.validation.subflow_apps.length > 0) {
|
||||
nodecolor = red
|
||||
branchcolor = red
|
||||
|
||||
for (var subflowkey in workflow.validation.subflow_apps) {
|
||||
const subflowApp = workflow.validation.subflow_apps[subflowkey]
|
||||
founderror += "- " + subflowApp.error+"\n"
|
||||
|
||||
if (subflowApp.error === action.id) {
|
||||
appgroup.push(subflowApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!showMiddle && relevantactions.length > 2 && index > 0 && index === relevantactions.length - 2) {
|
||||
if (founderror.length > 0) {
|
||||
middleError += founderror+"\n"
|
||||
|
||||
middleBranchColor = branchcolor
|
||||
}
|
||||
|
||||
if (index === relevantactions.length-2 && relevantactions.length > 2) {
|
||||
|
||||
const selectedIcon = middleError.length > 0 ?
|
||||
<Tooltip title={middleError}>
|
||||
<Tooltip title={
|
||||
<Typography variant="body1" style={{margin: 5, whiteSpace: "pre-line", }}>
|
||||
{middleError}
|
||||
</Typography>
|
||||
}>
|
||||
<IconButton style={{width: 30, height: 30, backgroundColor: "rgba(255,255,255,0.0)", borderRadius: 30, marginTop: 2, }}>
|
||||
<ErrorOutlineIcon style={{color: "red", }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null
|
||||
|
||||
return (
|
||||
selectedIcon
|
||||
)
|
||||
return selectedIcon
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Returns for anything non-middle
|
||||
if (relevantactions.length > 2 && index >= 1 && index < relevantactions.length - 2) {
|
||||
if (founderror.length > 0) {
|
||||
middleError += founderror+"\n"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (skipped && !lastitem) {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
@@ -423,28 +536,14 @@ const WorkflowValidationTimeline = (props) => {
|
||||
branchcolor = nodecolor
|
||||
}
|
||||
|
||||
var appgroup = []
|
||||
if (action.trigger_type === "WEBHOOK") {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
} else if (action.app_name === "shuffle-subflow") {
|
||||
if (action.status === "SUCCESS") {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
}
|
||||
|
||||
for (var subflowkey in workflow.validation.subflow_apps) {
|
||||
const subflowApp = workflow.validation.subflow_apps[subflowkey]
|
||||
if (subflowApp.error === action.id) {
|
||||
appgroup.push(subflowApp)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3
|
||||
|
||||
if (nodecolor === green) {
|
||||
branchcolor = green
|
||||
} else if (nodecolor === yellow) {
|
||||
@@ -455,12 +554,29 @@ const WorkflowValidationTimeline = (props) => {
|
||||
|
||||
if (index === 0) {
|
||||
startBranchColor = branchcolor
|
||||
} else if (index !== 0 && index !== relevantactions.length - 1) {
|
||||
// FIXME: This doesn't work yet
|
||||
middleBranchColor = branchcolor
|
||||
}
|
||||
|
||||
if (lastitem && middleError.length === 0) {
|
||||
branchcolor = startBranchColor
|
||||
if (lastitem) {
|
||||
if (middleError.length === 0) {
|
||||
branchcolor = startBranchColor
|
||||
} else {
|
||||
//branchcolor = middleBranchColor
|
||||
}
|
||||
|
||||
if (founderror === "") {
|
||||
nodecolor = green
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This could mean the workflow hasn't ran yet
|
||||
if (workflow.validation.valid === false && (workflow.validation.errors === undefined || workflow.validation.errors === null || workflow.validation.errors.length == 0) && (workflow.validation.subflow_apps === undefined || workflow.validation.subflow_apps === null || workflow.validation.subflow_apps.length == 0)) {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
}
|
||||
|
||||
const branchTooltip = branchcolor === yellow ? "Check nodes for errors" : ""
|
||||
const appname = action.app_name.replaceAll('_', ' ').slice(0, 16)
|
||||
|
||||
@@ -488,6 +604,14 @@ const WorkflowValidationTimeline = (props) => {
|
||||
console.log("MISSING IMAGE: ", appname, image, action)
|
||||
}
|
||||
|
||||
if (decidedColor === grey && nodecolor === green) {
|
||||
setDecidedColor(red)
|
||||
}
|
||||
|
||||
if (decidedColor !== red && nodecolor === red) {
|
||||
setDecidedColor(red)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flex: flex, justifyContent: "right", }}>
|
||||
{lastitem ?
|
||||
@@ -528,7 +652,7 @@ const WorkflowValidationTimeline = (props) => {
|
||||
:
|
||||
<Tooltip title={
|
||||
<Typography variant="body1" style={{margin: 5, color: "white", }}>
|
||||
{founderror.length > 0 ? founderror : `App: ${appname}`}
|
||||
{founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`}
|
||||
</Typography>
|
||||
} placement="top">
|
||||
|
||||
|
||||
@@ -467,13 +467,66 @@ const data = [
|
||||
"font-size": "0px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node:selected",
|
||||
css: {
|
||||
"border-color": "#f86a3e",
|
||||
"border-width": "7px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node:selected",
|
||||
css: {
|
||||
"border-color": "#f86a3e",
|
||||
"border-width": "7px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[buttonType="condition-drag"]`,
|
||||
css: {
|
||||
"width": "5px",
|
||||
"height": "5px",
|
||||
"background-color": "#f85a3e",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[name="switch"]`,
|
||||
css: {
|
||||
label: function(element) {
|
||||
// Load from the actual element
|
||||
var nodeheight = 400
|
||||
var conditions = [{
|
||||
"name": "Condition 1",
|
||||
"check": "X equals Y",
|
||||
},
|
||||
{
|
||||
"name": "Condition 2",
|
||||
"check": "X2 equals Y2",
|
||||
},
|
||||
{
|
||||
"name": "Condition 3",
|
||||
"check": "X3 equals Y3",
|
||||
}]
|
||||
|
||||
conditions.push({
|
||||
"name": "Else",
|
||||
"check": "If all else fails",
|
||||
})
|
||||
|
||||
const newlines = nodeheight / conditions.length
|
||||
console.log("Newlines: ", newlines)
|
||||
|
||||
const label = conditions.map((condition) => {
|
||||
return `${condition.name}\n\n\n`
|
||||
}).join("\n")
|
||||
|
||||
return label
|
||||
},
|
||||
color: "white",
|
||||
"border-color": "#f85a3e",
|
||||
"background-color": "#1f1f1f",
|
||||
"font-size": "19px",
|
||||
"text-margin-x": "-110px",
|
||||
"text-wrap": "wrap",
|
||||
shape: "roundrectangle",
|
||||
width: "100",
|
||||
height: "300",
|
||||
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
//{
|
||||
|
||||
@@ -6274,7 +6274,7 @@ If you're interested, please let me know a time that works for you, or set up a
|
||||
style={{ minWidth: 50, maxWidth: 50 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="License"
|
||||
primary="Scale"
|
||||
style={{ minWidth: 85, maxWidth: 85 }}
|
||||
/>
|
||||
<ListItemText
|
||||
@@ -6406,7 +6406,7 @@ If you're interested, please let me know a time that works for you, or set up a
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title="Not licensed, and can't scale.. This may cause service disruption."
|
||||
title="In Verbose mode. Set SHUFFLE_SWARM_CONFIG=run to Scale. This will not be as verbose. Details: https://shuffler.io/docs/configuration#scaling-shuffle"
|
||||
placement="top"
|
||||
>
|
||||
<a
|
||||
@@ -6441,7 +6441,7 @@ If you're interested, please let me know a time that works for you, or set up a
|
||||
environment.running_ip.length === 0 ? (
|
||||
<div>Not running</div>
|
||||
) : (
|
||||
environment.running_ip.split(":")[0]
|
||||
environment.running_ip
|
||||
)
|
||||
) : (
|
||||
"N/A"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -544,7 +544,7 @@ const AppCreator = (defaultprops) => {
|
||||
};
|
||||
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
|
||||
useEffect(() => {
|
||||
if (window.location.pathname.includes("apps/edit")) {
|
||||
@@ -897,8 +897,31 @@ const AppCreator = (defaultprops) => {
|
||||
};
|
||||
|
||||
if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) {
|
||||
console.log("LABEL: ", methodvalue["x-label"])
|
||||
|
||||
var correctlabel = ""
|
||||
const labels = methodvalue["x-label"].split(",")
|
||||
for (let labelkey in labels) {
|
||||
var label = labels[labelkey].trim()
|
||||
if (label.toLowerCase() === "no label") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove quotes and escapes
|
||||
label = label.replace(/['"]+/g, '')
|
||||
label = label.replace(/\\/g, '')
|
||||
|
||||
//label = label.replace("_", " ", -1)
|
||||
//label = label.charAt(0).toUpperCase() + label.slice(1)
|
||||
|
||||
correctlabel = label
|
||||
break
|
||||
}
|
||||
|
||||
console.log("LABEL: ", correctlabel)
|
||||
// FIX: Map labels only if they're actually in the category list
|
||||
newaction.action_label = methodvalue["x-label"]
|
||||
//newaction.action_label = methodvalue["x-label"]
|
||||
newaction.action_label = correctlabel
|
||||
}
|
||||
|
||||
if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) {
|
||||
@@ -3124,15 +3147,14 @@ const AppCreator = (defaultprops) => {
|
||||
Scopes for Oauth2
|
||||
</Typography>
|
||||
<MuiChipsInput
|
||||
style={{border: "2px solid #f86a3e", borderRadius: theme.palette.borderRadius,}}
|
||||
required
|
||||
required
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
maxHeight: 50,
|
||||
maxHeight: 160,
|
||||
},
|
||||
}}
|
||||
style={{ maxHeight: 80, overflowX: "hidden", overflowY: "auto" }}
|
||||
style={{ minHeight: 160, maxHeight: 160, overflowX: "hidden", overflowY: "auto" }}
|
||||
placeholder="Available Oauth2 Scopes"
|
||||
color="primary"
|
||||
fullWidth
|
||||
@@ -3159,7 +3181,7 @@ const AppCreator = (defaultprops) => {
|
||||
required
|
||||
style={{ marginTop: 0, backgroundColor: inputColor }}
|
||||
fullWidth={true}
|
||||
placeholder="Field Name (key, NOT your actual API-key)"
|
||||
placeholder="The Key to use as the header/query - NOT your actual API-key"
|
||||
type="name"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
@@ -4441,7 +4463,7 @@ const AppCreator = (defaultprops) => {
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
}}
|
||||
value={data.action_label}
|
||||
value={data?.action_label?.replace(" ", "_").toLowerCase()}
|
||||
style={{
|
||||
border: data.action_label === undefined || data.action_label === "No Label" ? "" : `2px solid ${bgColor}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
@@ -4463,7 +4485,7 @@ const AppCreator = (defaultprops) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={labelindex}
|
||||
value={label}
|
||||
value={label.replace(" ", "_").toLowerCase()}
|
||||
style={{
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3035,7 +3035,7 @@ const Apps = (props) => {
|
||||
height: "50px",
|
||||
}}
|
||||
variant="contained"
|
||||
disabled={openApi.length === 0 || appValidation.length > 0}
|
||||
disabled={openApi.length === 0 || appValidation.length > 0 || validation}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpenApiError("");
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
//import { useAlert
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
import Draggable from "react-draggable";
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
TextField,
|
||||
IconButton,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
@@ -146,28 +149,8 @@ const inputdata = [
|
||||
const LineChartWrapper = ({keys, height, width}) => {
|
||||
const [hovered, setHovered] = useState("");
|
||||
|
||||
//console.log("Date: ", new Date("2019-11-14T08:00:00.000Z"))
|
||||
console.log("Keys: ", keys)
|
||||
var inputdata = keys.data
|
||||
|
||||
/*
|
||||
const inputdata = [{
|
||||
"key": "Intel",
|
||||
"data": [
|
||||
{ key: new Date('11/22/2019'), data: 3, metadata: {color: "orange", "name": "Intel"}},
|
||||
{ key: new Date('11/24/2019'), data: 8, metadata: {color: "orange", "name": "Intel"}},
|
||||
{ key: new Date('11/29/2019'), data: 2, metadata: {color: "orange", "name": "Intel"}},
|
||||
]},
|
||||
{
|
||||
"key": "Popper",
|
||||
"data": [
|
||||
{ key: new Date('11/24/2019'), data: 9, },
|
||||
{ key: new Date('11/29/2019'), data: 3, },
|
||||
]
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
<Typography variant="h6" style={{marginBotton: 15}}>
|
||||
@@ -211,7 +194,6 @@ const LineChartWrapper = ({keys, height, width}) => {
|
||||
offset: '5px, 5px'
|
||||
}}
|
||||
content={(data, color) => {
|
||||
console.log("DATA: ", data)
|
||||
const name = data.metadata !== undefined && data.metadata.name !== undefined ? data.metadata.name : "No"
|
||||
|
||||
return (
|
||||
@@ -350,12 +332,35 @@ const Dashboard = (props) => {
|
||||
const [frameworkData, setFrameworkData] = useState(undefined);
|
||||
|
||||
const [widgetData, setWidgetData] = useState([]);
|
||||
const [newWidgetData, setNewWidgetData] = useState([]);
|
||||
|
||||
const [, setUpdate] = useState(0);
|
||||
|
||||
let navigate = useNavigate();
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
useEffect(() => {
|
||||
const widgetnames = ["app_executions_cloud"]
|
||||
for (let widgetkey in widgetnames) {
|
||||
const widgetName = widgetnames[widgetkey]
|
||||
|
||||
console.log("NAME: ", widgetName)
|
||||
|
||||
const resp = LoadStats(globalUrl, widgetName)
|
||||
if (resp !== undefined) {
|
||||
resp.then((data) => {
|
||||
console.log("Got data in parent: ", data)
|
||||
if (data === undefined) {
|
||||
} else {
|
||||
newWidgetData.push(data)
|
||||
setNewWidgetData(newWidgetData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUsecaseCategory.length === 0) {
|
||||
@@ -368,6 +373,7 @@ const Dashboard = (props) => {
|
||||
}
|
||||
}, [selectedUsecaseCategory])
|
||||
|
||||
|
||||
const checkSelectedParams = () => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
@@ -409,23 +415,22 @@ const Dashboard = (props) => {
|
||||
}, [usecases])
|
||||
|
||||
const getWidget = (dashboard, widget) => {
|
||||
fetch(`${globalUrl}/api/v1/dashboards/${dashboard}/widgets/${widget}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for framework!");
|
||||
}
|
||||
fetch(`${globalUrl}/api/v1/dashboards/${dashboard}/widgets/${widget}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for framework!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
console.log("Resp: ", responseJson)
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
//toast("Failed loading: " + responseJson.reason)
|
||||
@@ -441,14 +446,12 @@ const Dashboard = (props) => {
|
||||
}
|
||||
|
||||
const foundWidget = widgetData.findIndex(data => data.title === widget)
|
||||
console.log("Found: ", foundWidget)
|
||||
if (foundWidget !== undefined && foundWidget !== null && foundWidget >= 0) {
|
||||
widgetData[foundWidget] = tmpdata
|
||||
} else {
|
||||
widgetData.push(tmpdata)
|
||||
}
|
||||
|
||||
console.log("Data: ", widgetData)
|
||||
setWidgetData(widgetData)
|
||||
}
|
||||
})
|
||||
@@ -480,7 +483,7 @@ const Dashboard = (props) => {
|
||||
useEffect(() => {
|
||||
getWidget("main", "Overall")
|
||||
getWidget("main", "Overall2")
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const fetchdata = (stats_id) => {
|
||||
fetch(globalUrl + "/api/v1/stats/" + stats_id, {
|
||||
@@ -646,7 +649,6 @@ const Dashboard = (props) => {
|
||||
stats["workflow_executions"].data !== undefined
|
||||
) {
|
||||
setStatsRan(true);
|
||||
//console.log("NEW DATA?: ", stats)
|
||||
console.log("SET WORKFLOW: ", stats["workflow_executions"]);
|
||||
//var curday = startDate.getDate()
|
||||
|
||||
@@ -729,6 +731,100 @@ const Dashboard = (props) => {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const WidgetController = (props) => {
|
||||
const { data, index, availableStats, } = props
|
||||
const [hovering, setHovering] = useState(false)
|
||||
|
||||
const newname = data.key !== undefined ? data.key.replaceAll("_", " ") : ""
|
||||
|
||||
console.log("KEYDATA: ", data)
|
||||
|
||||
const loadNewStats = (newkey) => {
|
||||
const resp = LoadStats(globalUrl, newkey)
|
||||
if (resp !== undefined) {
|
||||
resp.then((respdata) => {
|
||||
if (respdata === undefined || respdata === null) {
|
||||
toast("Failed to laod data. Please try again, or contact support@shuffler.io if this persists.")
|
||||
} else {
|
||||
newWidgetData[index] = respdata
|
||||
setNewWidgetData(newWidgetData)
|
||||
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable>
|
||||
<Paper
|
||||
style={{
|
||||
height: "100%", width: "100%", maxWidth: 500, margin: 15, padding: "15px 15px 15px 15px", textAlign: "left",
|
||||
backgroundColor: hovering ? theme.palette.inputColor : theme.palette.backgroundColor,
|
||||
cursor: hovering ? "pointer" : "default",
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHovering(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovering(false)
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", justifyContent: "space-between", alignItems: "center",}}>
|
||||
<Typography variant="h6">
|
||||
{newname}
|
||||
</Typography>
|
||||
|
||||
{data.available_keys === undefined || data.available_keys === null || data.available_keys.length === 0 ? null :
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="Response Action"
|
||||
value={data.key}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
loadNewStats(e.target.value)
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
height: 40,
|
||||
maxWidth: 150,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
>
|
||||
{data.available_keys.map((foundKey, index) => {
|
||||
const parsedKeyName = foundKey.replaceAll("_", " ")
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={foundKey}
|
||||
>
|
||||
<em>{parsedKeyName}</em>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
}
|
||||
</div>
|
||||
<DashboardBarchart
|
||||
timelineData={data}
|
||||
height={50}
|
||||
/>
|
||||
</Paper>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
const data = (
|
||||
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
|
||||
<div style={{width: 500, margin: "auto"}}>
|
||||
@@ -739,12 +835,25 @@ const Dashboard = (props) => {
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null :
|
||||
{/*widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null :
|
||||
<Draggable>
|
||||
<Paper style={{height: 350, width: 500, padding: "15px 15px 15px 15px", }}>
|
||||
<LineChartWrapper keys={widgetData[0]} height={280} width={470} />
|
||||
</Paper>
|
||||
</Draggable>
|
||||
*/}
|
||||
|
||||
{newWidgetData === undefined || newWidgetData === null || newWidgetData === [] ? null :
|
||||
newWidgetData.map((data, index) => {
|
||||
|
||||
return (
|
||||
<WidgetController
|
||||
key={index}
|
||||
index={index}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
import { toast } from 'react-toastify';
|
||||
import Markdown from 'react-markdown'
|
||||
import theme from '../theme.jsx';
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
@@ -152,9 +152,19 @@ export const OuterLink = (props) => {
|
||||
|
||||
|
||||
export const Img = (props) => {
|
||||
var height = "auto"
|
||||
var width = 750
|
||||
if (props.height !== undefined && props.height !== null) {
|
||||
height = props.height
|
||||
}
|
||||
|
||||
if (props.width !== undefined && props.width !== null) {
|
||||
width = props.width
|
||||
}
|
||||
|
||||
return(
|
||||
<img
|
||||
style={{border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }}
|
||||
style={{border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: width, maxWidth: width, margin: "auto", marginTop: 10, marginBottom: 10, }}
|
||||
alt={props.alt}
|
||||
src={props.src}
|
||||
/>
|
||||
|
||||
+486
-397
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ const SetAuthentication = (props) => {
|
||||
const [loadFail, setLoadFail] = useState("");
|
||||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
//const alert = useAlert();
|
||||
|
||||
const parseIncomingOpenapiData = (data) => {
|
||||
@@ -142,8 +142,8 @@ const SetAuthentication = (props) => {
|
||||
// 3. Help them set info for the app
|
||||
// Make sure to test both private and public apps
|
||||
|
||||
const appname = app.name !== undefined ? app.name : "";
|
||||
const appLink = "/apps/" + app.id || "";
|
||||
const appname = app.name !== undefined ? app.name : ""
|
||||
const appLink = "/apps/" + app.id || ""
|
||||
|
||||
console.log("App: ", app)
|
||||
|
||||
@@ -154,21 +154,16 @@ const SetAuthentication = (props) => {
|
||||
:
|
||||
<><div>
|
||||
<Typography variant="h4" style={{ marginBottom: 20, }}>
|
||||
A Shuffle Organization has invited you to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication
|
||||
You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication
|
||||
</Typography>
|
||||
|
||||
{/* What does this mean box */}
|
||||
<Typography variant="h6" style={{ marginBottom: 20, }}>
|
||||
What does this mean?
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)", }}>
|
||||
A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6">
|
||||
Authenticate Here:
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
{app.authentication === undefined || app.authentication === null || app.authentication.length === 0 ?
|
||||
null
|
||||
@@ -195,7 +190,7 @@ const SetAuthentication = (props) => {
|
||||
appAuthentication={appAuthentication} />}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={{ marginBottom: 20, }}>
|
||||
<Typography variant="h6" style={{ marginTop: 50, marginBottom: 20, }}>
|
||||
What can they do with this?
|
||||
</Typography>
|
||||
|
||||
@@ -203,12 +198,11 @@ const SetAuthentication = (props) => {
|
||||
You can check the actions they want to use <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>here</a>.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
{/* Add a box below */}
|
||||
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)",}}>
|
||||
<div className="collapsible-container">
|
||||
<div className="collapsible-list">
|
||||
{app.actions?.map((item, index) => (
|
||||
<div key={index} className="collapsible-item">
|
||||
<div key={index} className="collapsible-item" style={{cursor: "pointer", }}>
|
||||
<div className="collapsible-label" onClick={() => handleToggle(index)}>
|
||||
{item.label}
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ const Welcome = (props) => {
|
||||
}
|
||||
}, [activeStep])
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const [steps, setSteps] = useState([
|
||||
"Help us get to know you",
|
||||
"Find your Apps",
|
||||
|
||||
@@ -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>
|
||||
@@ -3528,7 +3559,7 @@ const Workflows = (props) => {
|
||||
var workflowDelay = -150
|
||||
var appDelay = -75
|
||||
|
||||
const foundPriority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true)
|
||||
const foundPriority = userdata === undefined || userdata === null || userdata.priorities === undefined || userdata.priorities === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true)
|
||||
return (
|
||||
<div style={viewStyle}>
|
||||
<div style={workflowViewStyle}>
|
||||
|
||||
Reference in New Issue
Block a user