Sync + Added dashboard for stats API

This commit is contained in:
Frikky
2025-10-20 11:37:26 +02:00
parent 9a20dcd426
commit 1bff92172b
38 changed files with 3985 additions and 1432 deletions
+6 -5
View File
@@ -992,16 +992,16 @@ const AppAuthTab = memo((props) => {
<Typography variant='h5' style={{ marginBottom: 8, marginTop: 0, }}>App Authentication</Typography> <Typography variant='h5' style={{ marginBottom: 8, marginTop: 0, }}>App Authentication</Typography>
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', }}> <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', }}>
<Typography variant='body2' color="textSecondary"> <Typography variant='body2' color="textSecondary">
Control the authentication options for individual apps. Control the authentication options for individual apps. These keys are write-only, and cannot be viewed after creation. If you want editable secrets (e.g. for use in code), use <a href="admin?tab=datastore&category=protected" style={{ color: theme.palette.linkColor }}>Protected Keys</a>.
</Typography> </Typography>
&nbsp; &nbsp;
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href="/docs/organizations#app_authentication" href="/docs/organizations#app_authentication"
style={{ color: theme.palette.linkColor }} style={{ minWidth: 200, marginleft: 25, color: theme.palette.linkColor }}
> >
Learn more about App Authentication Learn more
</a> </a>
</div> </div>
</div> </div>
@@ -1787,7 +1787,7 @@ const Hits = ({
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) { if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
setAuthenticationType({ setAuthenticationType({
type: "", type: "",
}) })
selectedAppData.authentication = { selectedAppData.authentication = {
@@ -1955,6 +1955,7 @@ const Hits = ({
if (data === undefined || data === null) { if (data === undefined || data === null) {
return; return;
} }
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid); const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
if (filteredData.length === 0) { if (filteredData.length === 0) {
setAppAuthentication([]); setAppAuthentication([]);
@@ -1965,7 +1966,7 @@ const Hits = ({
} }
}; };
const HandleAppAuthentication = ()=>{ const HandleAppAuthentication = () => {
const url = `${globalUrl}/api/v1/apps/authentication`; const url = `${globalUrl}/api/v1/apps/authentication`;
+63 -24
View File
@@ -21,8 +21,9 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
import CreateIcon from '@mui/icons-material/Create' import CreateIcon from '@mui/icons-material/Create'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import YAML from "yaml"; import YAML from "yaml";
import Dropzone from "./Dropzone.jsx";
const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenApi = false, prefillOpenApiData = "" }) => {
const [openApiModal, setOpenApiModal] = useState(false) const [openApiModal, setOpenApiModal] = useState(false)
const [generateAppModal, setGenerateAppModal] = useState(false) const [generateAppModal, setGenerateAppModal] = useState(false)
const [openApi, setOpenApi] = useState("") const [openApi, setOpenApi] = useState("")
@@ -35,6 +36,16 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
const navigate = useNavigate() const navigate = useNavigate()
const upload = useRef() const upload = useRef()
useEffect(() => {
if (open && (startOpenApi || (prefillOpenApiData && prefillOpenApiData.length > 0))) {
if (prefillOpenApiData && prefillOpenApiData.length > 0) {
setOpenApiData(prefillOpenApiData)
setIsDropzone(true)
}
setOpenApiModal(true)
}
}, [open, startOpenApi, prefillOpenApiData])
// Style for the create options // Style for the create options
const AppCreateButton = ({ text, func, icon }) => { const AppCreateButton = ({ text, func, icon }) => {
const [hover, setHover] = React.useState(false) const [hover, setHover] = React.useState(false)
@@ -467,6 +478,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
} }
}} }}
> >
<Dropzone onDrop={uploadFile} style={{ width: '100%' }}>
<DialogTitle sx={{ <DialogTitle sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
@@ -504,12 +516,15 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
<Typography sx={{ color: theme.palette.text.primary, fontSize: '16px' }}> <Typography sx={{ color: theme.palette.text.primary, fontSize: '16px' }}>
Paste in the URI for the OpenAPI or find out Paste in the URI for the OpenAPI or find out
</Typography> </Typography>
<Link style={{ <Link
to="https://shuffler.io/docs/apps#getting-started"
style={{
color: '#ff8544', color: '#ff8544',
textDecoration: 'none', textDecoration: 'none',
textDecoration: 'underline', textDecoration: 'underline',
fontSize: '16px', fontSize: '16px',
fontFamily: theme?.typography?.fontFamily fontFamily: theme?.typography?.fontFamily,
textUnderlineOffset: "3px",
}}> }}>
How to find URI for openAPI? How to find URI for openAPI?
</Link> </Link>
@@ -568,31 +583,54 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
Must point to a version 2 or 3 OpenAPI specification. Must point to a version 2 or 3 OpenAPI specification.
</Typography> </Typography>
<Typography sx={{ mb: 2, color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontFamily: theme?.typography?.fontFamily }}> <Typography sx={{ mb: 1.5, mt: 2, color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontFamily: theme?.typography?.fontFamily }}>
Or upload a YAML or JSON specification Or upload a YAML or JSON specification
</Typography> </Typography>
<div style={{
<Button width: '100%',
variant="outlined" boxSizing: 'border-box',
border: '1px dashed rgba(255,255,255,0.35)',
borderRadius: 4,
padding: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#1a1a1a',
marginTop: '12px',
cursor: 'pointer'
}}
onClick={() => upload.current.click()} onClick={() => upload.current.click()}
sx={{
color: '#FF8544',
borderColor: '#FF8544',
px: 5,
py: 1,
'&:hover': {
borderColor: '#FF8544',
color: '#FF8544',
bgcolor: 'rgba(255,133,68,0.1)'
},
textTransform: 'none',
fontSize: '14px',
fontFamily: theme?.typography?.fontFamily,
height: '40px'
}}
> >
Upload <div style={{ display: 'flex', flexDirection: 'column' }}>
</Button> <Typography sx={{ color: 'rgba(255,255,255,0.9)', fontSize: '15px', fontFamily: theme?.typography?.fontFamily }}>
Drag & drop your OpenAPI (YAML/JSON) anywhere
</Typography>
<Typography sx={{ color: 'rgba(255,255,255,0.6)', fontSize: '13px', mt: 0.5, fontFamily: theme?.typography?.fontFamily }}>
or click to browse files
</Typography>
</div>
<Button
variant="outlined"
onClick={(e) => { e.stopPropagation(); upload.current.click(); }}
sx={{
color: '#FF8544',
borderColor: '#FF8544',
px: 3,
py: 0.75,
'&:hover': {
borderColor: '#FF8544',
color: '#FF8544',
bgcolor: 'rgba(255,133,68,0.1)'
},
textTransform: 'none',
fontSize: '14px',
fontFamily: theme?.typography?.fontFamily,
height: '36px'
}}
>
Upload
</Button>
</div>
<input <input
hidden hidden
@@ -638,6 +676,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
Continue Continue
</Button> </Button>
</DialogActions> </DialogActions>
</Dropzone>
</Dialog> </Dialog>
{/* Generate App Modal */} {/* Generate App Modal */}
+6 -2
View File
@@ -33,6 +33,7 @@ import {
ClearRefinements, ClearRefinements,
connectStateResults connectStateResults
} from "react-instantsearch-dom"; } from "react-instantsearch-dom";
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import aa from "search-insights"; import aa from "search-insights";
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -160,6 +161,8 @@ const AppGrid = (props) => {
refine(searchQuery.trim()); refine(searchQuery.trim());
}; };
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
return ( return (
<form noValidate action="" role="search"> <form noValidate action="" role="search">
<TextField <TextField
@@ -229,9 +232,10 @@ const AppGrid = (props) => {
placeholder="Search more than 2500 Apps" placeholder="Search more than 2500 Apps"
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
setSearchQuery(event.currentTarget.value); const value = event.currentTarget.value;
setSearchQuery(value);
removeQuery("q"); removeQuery("q");
refine(event.currentTarget.value); debouncedRefine(value);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
+197 -145
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext, memo, useMemo } from 'react'; import React, { useState, useEffect, useContext, useCallback } from 'react';
import {getTheme} from '../theme.jsx'; import {getTheme} from '../theme.jsx';
import classNames from "classnames"; import classNames from "classnames";
@@ -73,6 +73,7 @@ const AppStats = (defaultprops) => {
const [resultRows, setResultRows] = useState([]) const [resultRows, setResultRows] = useState([])
const [resultLoading, setResultLoading] = useState(true) const [resultLoading, setResultLoading] = useState(true)
const { themeMode, brandColor } = useContext(Context); const { themeMode, brandColor } = useContext(Context);
const [onpremAppRuns, setOnpremAppRuns] = useState(0)
const theme = getTheme(themeMode, brandColor) const theme = getTheme(themeMode, brandColor)
const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0 const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0
@@ -83,12 +84,156 @@ const AppStats = (defaultprops) => {
} }
}, []) }, [])
const handleDataSetting = useCallback((inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) {
return
}
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
const dailyStats = inputdata[statKey]
if (dailyStats === undefined || dailyStats === null) {
return
}
var appRuns = {
"key": "App Runs",
"data": []
}
var childorgappRuns = {
"key": "Child Org App Runs",
"data": []
}
var workflowRuns = {
"key": "Workflow Runs (includes subflows)",
"data": []
}
var subflowRuns = {
"key": "Subflow Runs",
"data": []
}
var appcostRuns = {
"key": "Cost of App Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
continue
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
}
// Check if app_executions key in item
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
appRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["app_executions"]
})
// Add number
appcostRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: (item["app_executions"] * invocationCost).toFixed(2)
})
}
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["child_app_executions"]
})
}
// Check if workflow_executions key in item
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["workflow_executions"]
})
}
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["subflow_executions"]
})
}
}
// Only add today's data if endTime is not set or if today falls within the selected date range
const today = new Date()
const todayStartOfDay = new Date(today)
todayStartOfDay.setHours(0, 0, 0, 0)
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
(new Date(endTime) >= todayStartOfDay)
if (!syncStats && shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_app_executions"]
})
appcostRuns["data"].push({
key: new Date().toISOString(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
if (childorgappRuns["data"].length > 0) {
setChildOrgsAppRuns(childorgappRuns)
}
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
setApprunCosts(appcostRuns)
}, [syncStats, endTime, startTime])
useEffect(() => { useEffect(() => {
if (statistics && statistics?.org_id?.length > 0) { if (statistics && statistics?.org_id?.length > 0) {
handleDataSetting(statistics, "day") handleDataSetting(statistics, "day")
} }
}, [statistics]) }, [statistics])
useEffect(() => {
setStartTime("")
setEndTime("")
}, [currentTab])
const getWorkflowStats = async (workflow, startTime, endTime) => { const getWorkflowStats = async (workflow, startTime, endTime) => {
if (workflow.id === undefined || workflow.id === null || workflow.id === "") { if (workflow.id === undefined || workflow.id === null || workflow.id === "") {
@@ -227,7 +372,7 @@ const AppStats = (defaultprops) => {
} }
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
if (statistics[statKey] === undefined || statistics[statKey] === null) { if (!syncStats && (statistics[statKey] === undefined || statistics[statKey] === null)) {
setFilteredStatistics(statistics) setFilteredStatistics(statistics)
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0) setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
return return
@@ -356,17 +501,55 @@ const AppStats = (defaultprops) => {
workflowexecutions += item["workflow_executions"] workflowexecutions += item["workflow_executions"]
appexecutions += item["app_executions"] appexecutions += item["app_executions"]
if (currentTab === 0) { if (currentTab === 0 || currentTab === 3) {
appexecutions += (item["child_app_executions"] ?? 0) appexecutions += (item["child_app_executions"] ?? 0)
} }
estimatedcost += (item["app_executions"] * invocationCost) estimatedcost += (item["app_executions"] * invocationCost)
} }
const today = new Date();
const isCurrentMonthSelected =
(startTime === "" && endTime === "") ||
(
new Date(foundstarttime).getMonth() === today.getMonth() &&
new Date(foundstarttime).getFullYear() === today.getFullYear() &&
new Date(foundendtime).getMonth() === today.getMonth() &&
new Date(foundendtime).getFullYear() === today.getFullYear()
);
if (!syncStats && isCurrentMonthSelected) {
if (statistics["daily_app_executions"] !== undefined && statistics["daily_app_executions"] !== null) {
appexecutions += statistics["daily_app_executions"] + (statistics["daily_child_app_executions"] ?? 0)
}
}
tmpstats["monthly_workflow_executions"] = workflowexecutions tmpstats["monthly_workflow_executions"] = workflowexecutions
tmpstats["monthly_app_executions"] = appexecutions tmpstats["monthly_app_executions"] = appexecutions
if (syncStats) {
setOnpremAppRuns(appexecutions)
}
} else {
const today = new Date();
const isCurrentMonthSelected =
(startTime === "" && endTime === "") ||
(
new Date(foundstarttime).getMonth() === today.getMonth() &&
new Date(foundstarttime).getFullYear() === today.getFullYear() &&
new Date(foundendtime).getMonth() === today.getMonth() &&
new Date(foundendtime).getFullYear() === today.getFullYear()
);
if (!syncStats && isCurrentMonthSelected) {
if (statistics["daily_app_executions"] !== undefined && statistics["daily_app_executions"] !== null) {
appexecutions += statistics["daily_app_executions"] + (statistics["daily_child_app_executions"] ?? 0)
}
}
tmpstats["monthly_app_executions"] = appexecutions
} }
// Make estimatedcost have max 2 decimals // Make estimatedcost have max 2 decimals
if (isCloud) { if (isCloud) {
// Exclude includedExecutions*month // Exclude includedExecutions*month
@@ -380,11 +563,11 @@ const AppStats = (defaultprops) => {
handleDataSetting(tmpstats, "day") handleDataSetting(tmpstats, "day")
// if we have done monthly reset than only show monthly app runs as current month app run // if we have done monthly reset than only show monthly app runs as current month app run
const currentMonth = new Date().getMonth() + 1 const currentMonth = new Date().getMonth() + 1
if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) { if (!syncStats && !monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAppRunsParent(statistics["monthly_app_executions"]) setMonthlyAppRunsParent(statistics["monthly_app_executions"])
} }
if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) { if (!syncStats && !monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"]) setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
} }
@@ -397,7 +580,7 @@ const AppStats = (defaultprops) => {
loadWorkflowStats(foundWorkflows, startTime, endTime) loadWorkflowStats(foundWorkflows, startTime, endTime)
} }
}, [statistics, startTime, endTime]) }, [statistics, startTime, endTime, syncStats, currentTab, handleDataSetting])
const handleStartTimeChange = (date) => { const handleStartTimeChange = (date) => {
setStartTime(date) setStartTime(date)
@@ -407,142 +590,7 @@ const AppStats = (defaultprops) => {
setEndTime(date) setEndTime(date)
} }
const handleDataSetting = (inputdata, grouping) => { console.log("sync stats: ", syncStats, statistics)
if (inputdata === undefined || inputdata === null) {
return
}
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
const dailyStats = inputdata[statKey]
if (dailyStats === undefined || dailyStats === null) {
return
}
var appRuns = {
"key": "App Runs",
"data": []
}
var childorgappRuns = {
"key": "Child Org App Runs",
"data": []
}
var workflowRuns = {
"key": "Workflow Runs (includes subflows)",
"data": []
}
var subflowRuns = {
"key": "Subflow Runs",
"data": []
}
var appcostRuns = {
"key": "Cost of App Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
continue
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
}
// Check if app_executions key in item
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
appRuns["data"].push({
key: new Date(item["date"]),
data: item["app_executions"]
})
// Add number
appcostRuns["data"].push({
key: new Date(item["date"]),
data: (item["app_executions"] * invocationCost).toFixed(2)
})
}
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(item["date"]),
data: item["child_app_executions"]
})
}
// Check if workflow_executions key in item
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(item["date"]),
data: item["workflow_executions"]
})
}
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(item["date"]),
data: item["subflow_executions"]
})
}
}
// Only add today's data if endTime is not set or if today falls within the selected date range
const today = new Date()
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
(new Date(endTime) >= today.setHours(0, 0, 0, 0))
if (shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
if (childorgappRuns["data"].length > 0) {
setChildOrgsAppRuns(childorgappRuns)
}
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
setApprunCosts(appcostRuns)
}
const paperStyle = { const paperStyle = {
textAlign: "center", textAlign: "center",
@@ -708,22 +756,26 @@ const AppStats = (defaultprops) => {
</Tooltip> </Tooltip>
} */} } */}
{syncStats === true ? null : {/* {syncStats === true ? null : */}
<Tooltip title={ <Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
App runs in the selected period App runs in the selected period
</Typography> </Typography>
}> }>
<Box sx={paperStyle}> <Box sx={paperStyle}>
{syncStats === true ?
<Typography variant="h4">
{onpremAppRuns}
</Typography>:
<Typography variant="h4"> <Typography variant="h4">
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
</Typography> </Typography>}
<Typography variant="h6"> <Typography variant="h6">
App Runs App Runs
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
} {/* } */}
{syncStats === true || currentTab === 0 ? null : {syncStats === true || currentTab === 0 ? null :
<Tooltip title={ <Tooltip title={
+45 -11
View File
@@ -147,6 +147,20 @@ const CacheView = memo((props) => {
const [showSettingsMenu, setShowSettingsMenu] = useState(false); const [showSettingsMenu, setShowSettingsMenu] = useState(false);
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false); const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
useEffect(() => {
if (selectedCategory === "" || selectedCategory === null || selectedCategory === undefined || selectedCategory === "default") {
return
}
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length === 0) {
return
}
if (!datastoreCategories.includes(selectedCategory)) {
setDatastoreCategories([...datastoreCategories, selectedCategory])
}
}, [datastoreCategories, selectedCategory])
var to_be_copied = ""; var to_be_copied = "";
const defaultAutomation = [ const defaultAutomation = [
{ {
@@ -299,7 +313,16 @@ const CacheView = memo((props) => {
useEffect(() => { useEffect(() => {
getWorkflows() getWorkflows()
getApps() getApps()
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
var chosenCategory = selectedCategory
const urlParams = new URLSearchParams(window.location.search)
const categoryParam = urlParams.get("category")
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
chosenCategory = categoryParam
setSelectedCategory(categoryParam)
}
listOrgCache(orgId, chosenCategory, 0, pageSize, page)
}, []) }, [])
@@ -423,7 +446,6 @@ const CacheView = memo((props) => {
setDatastoreCategories(newcategories) setDatastoreCategories(newcategories)
} }
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) { if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") { if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
@@ -458,7 +480,12 @@ const CacheView = memo((props) => {
} }
} }
} else { } else {
toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.") //toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.")
if (category !== undefined && category !== null && category !== "" && category !== "default") {
toast.info(`No keys to load in category ${category}`)
setSelectedCategory(category)
}
} }
}) })
.catch((error) => { .catch((error) => {
@@ -513,8 +540,8 @@ const CacheView = memo((props) => {
category: selectedCategory, category: selectedCategory,
} }
if (dataValue?.category !== "" && dataValue?.category !== "default") { if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
entry.category = dataValue.category.replaceAll(" ", "_"); entry.category = dataValue?.category?.replaceAll(" ", "_");
} }
@@ -1513,7 +1540,7 @@ const CacheView = memo((props) => {
name={null} name={null}
/> />
: :
<Typography variant="body2" style={{maxHeight: 200, overflow: "hidden", }}> <Typography variant="body2" style={{maxWidth: 500, maxHeight: 200, overflow: "auto", }}>
{data.value} {data.value}
</Typography> </Typography>
} }
@@ -1712,7 +1739,7 @@ const CacheView = memo((props) => {
<span> <span>
<IconButton <IconButton
style={{ padding: "6px" }} style={{ padding: "6px" }}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false} disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false || data.category === "protected"}
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@@ -1911,7 +1938,7 @@ const CacheView = memo((props) => {
{selectedCategory === "protected" ? {selectedCategory === "protected" ?
<div style={{ color: red, }}> <div style={{ color: red, }}>
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. This is a basic protection, and is NOT bulletproof. Protected keys are encrypted, only available to admins, and will be masked when used in workflows. If you want unreadable secrets, use <a href="/admin?tab=app_auth" style={{ color: theme.palette.linkColor }}>App Auth</a>.
</div> </div>
: null} : null}
@@ -2051,7 +2078,7 @@ const CacheView = memo((props) => {
</Button> </Button>
</Tooltip> </Tooltip>
: :
<Tooltip title={"Add new file category"} style={{}} aria-label={""}> <Tooltip title={"Add or find category"} style={{}} aria-label={""}>
<Button <Button
style={{ style={{
whiteSpace: "nowrap", whiteSpace: "nowrap",
@@ -2077,8 +2104,14 @@ const CacheView = memo((props) => {
{renderTextBox && <TextField {renderTextBox && <TextField
onKeyPress={(event)=>{ onKeyPress={(event)=>{
handleKeyDown(event); handleKeyDown(event);
if(event.key === 'Enter' && selectedFileId.length > 0){
// Check value of the field
const foundValue = event.target.value.trim();
if(event.key === 'Enter' && foundValue?.length > 0){
setUpdateToThisCategory(event.target.value) setUpdateToThisCategory(event.target.value)
listOrgCache(orgId, event.target.value, 0, pageSize, 0)
setPage(0)
} }
}} }}
@@ -2096,6 +2129,7 @@ const CacheView = memo((props) => {
paddingTop: 0, paddingTop: 0,
}, },
}} }}
id=""
color="primary" color="primary"
placeholder="Category name" placeholder="Category name"
required required
@@ -2416,7 +2450,7 @@ const CacheView = memo((props) => {
</Typography> </Typography>
<Pagination <Pagination
count={Number.parseInt(totalAmount/pageSize*100/2)} count={Number.parseInt(totalAmount/pageSize)}
page={page+1} page={page+1}
renderItem={(item) => { renderItem={(item) => {
var disabled = false var disabled = false
+90 -40
View File
@@ -18,14 +18,21 @@ import {
Tooltip, Tooltip,
Autocomplete, Autocomplete,
TextField, TextField,
Box,
} from '@mui/material'; } from '@mui/material';
import { import {
Rocket as RocketIcon, Rocket as RocketIcon,
FilterAlt as FilterAltIcon, FilterAlt as FilterAltIcon,
Add as AddIcon, Add as AddIcon,
Check as CheckIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import {
green,
red,
} from '../views/AngularWorkflow.jsx'
import algoliasearch from 'algoliasearch/lite'; import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
@@ -97,6 +104,7 @@ const CollectIngestModal = (props) => {
const [showAppsearch, setShowAppsearch] = useState(false); const [showAppsearch, setShowAppsearch] = useState(false);
const [algoliaOptions, setAlgoliaOptions] = useState([]); const [algoliaOptions, setAlgoliaOptions] = useState([]);
const [generating, setGenerating] = useState(false);
const appname = type const appname = type
const ingestedAmount = 20 const ingestedAmount = 20
@@ -107,7 +115,7 @@ const CollectIngestModal = (props) => {
}) })
var foundMatchingWorkflow = null var foundMatchingWorkflow = null
if (workflows !== undefined && workflows !== null && workflows.length > 0) { if (!showAppsearch && workflows !== undefined && workflows !== null && workflows.length > 0) {
const parsedName = type.toLowerCase().replaceAll(" ", "_"); const parsedName = type.toLowerCase().replaceAll(" ", "_");
const foundWorkflow = workflows.find((workflow) => { const foundWorkflow = workflows.find((workflow) => {
return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName
@@ -169,12 +177,36 @@ const CollectIngestModal = (props) => {
} }
} }
const runIngestion = () => {
setGenerating(true)
setTimeout(() => {
setGenerating(false)
}, 5000)
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
}
startIngestion(appname, newapps, appCategory, index)
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
}
return ( return (
//<Grid item xs={hovering ? 12 : 5.9} //<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12} <Grid item xs={12}
style={{ style={{
minHeight: hovering ? 200 : 200, minHeight: hovering ? 200 : 200,
maxHeight: hovering ? "auto" : 140,
cursor: "pointer", cursor: "pointer",
position: "relative", position: "relative",
transition: "all 0.3s ease-in-out", transition: "all 0.3s ease-in-out",
@@ -200,7 +232,7 @@ const CollectIngestModal = (props) => {
<div style={{flex: 1, margin: "auto", marginTop: 50, }}> <div style={{flex: 1, margin: "auto", marginTop: 50, }}>
<div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}> <div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
{selectedApps.map((app, index) => { {generating ? null : selectedApps.map((app, index) => {
// Show image of each one // Show image of each one
return ( return (
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}> <div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
@@ -214,18 +246,29 @@ const CollectIngestModal = (props) => {
) )
})} })}
<Tooltip title="Select Apps" placement="top"> {!generating && appCategory !== undefined && appCategory !== null && appCategory.length > 0 ?
<IconButton <Tooltip title={showAppsearch ? "Done selecting apps" : "Select Apps"} placement="top">
style={{marginLeft: 10, marginRight: 50, }} <IconButton
variant="outlined" style={{marginLeft: 10, marginRight: 50, }}
color="secondary" variant="outlined"
onClick={() => { color="secondary"
setShowAppsearch(!showAppsearch) onClick={() => {
}}
> if (showAppsearch === true) {
<AddIcon style={{color: theme.palette.primary.main, }} /> runIngestion()
</IconButton> }
</Tooltip>
setShowAppsearch(!showAppsearch)
}}
>
{showAppsearch ?
<CheckIcon style={{color: green, }} />
:
<AddIcon style={{color: theme.palette.primary.main, }} />
}
</IconButton>
</Tooltip>
: null}
</div> </div>
{showAppsearch ? {showAppsearch ?
@@ -237,20 +280,42 @@ const CollectIngestModal = (props) => {
value={selectedApps} value={selectedApps}
onChange={(event, value) => { onChange={(event, value) => {
console.log("New value: ", value)
setSelectedApps(value) setSelectedApps(value)
}} }}
getOptionLabel={(option) => { getOptionLabel={(option) => {
const parsedname = option.name.replaceAll("_", " ") const parsedname = option.name.replaceAll("_", " ")
return ( return parsedname
<div> //return (
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} /> // <div>
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}> // <img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
{parsedname} // <Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
</Typography> // {parsedname}
</div> // </Typography>
) // </div>
//)
}}
renderOption={(props, option, state, ownerState) => {
const { key, ...optionProps } = props;
return (
<Box
key={key}
sx={{
borderRadius: '8px',
margin: '5px',
padding: '8px',
}}
component="li"
{...optionProps}
>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
{ownerState.getOptionLabel(option)}
</Box>
);
}} }}
renderInput={(params) => { renderInput={(params) => {
return ( return (
@@ -267,24 +332,9 @@ const CollectIngestModal = (props) => {
style={{width: 250, margin: 25, }} style={{width: 250, margin: 25, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"} variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => { onClick={() => {
runIngestion()
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
}
startIngestion(appname, newapps, appCategory, index)
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
}} }}
disabled={generating}
> >
{foundMatchingWorkflow !== null ? {foundMatchingWorkflow !== null ?
"Re-Create Ingestion" "Re-Create Ingestion"
+10 -7
View File
@@ -36,6 +36,7 @@ import {
Avatar, Avatar,
AvatarGroup, AvatarGroup,
} from "@mui/material" } from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CreatorGrid = props => { const CreatorGrid = props => {
@@ -109,6 +110,8 @@ const CreatorGrid = props => {
} }
} }
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
return ( return (
<form noValidate action="" role="search"> <form noValidate action="" role="search">
<TextField <TextField
@@ -134,7 +137,7 @@ const CreatorGrid = props => {
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
removeQuery("q") removeQuery("q")
refine(event.currentTarget.value) debouncedRefine(event.currentTarget.value)
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
@@ -190,10 +193,10 @@ const CreatorGrid = props => {
null null
} }
</span> </span>
</div> </div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}> <Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows <b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
</Typography> </Typography>
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ? {data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}> <AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
{data.specialized_apps.map((app, index) => { {data.specialized_apps.map((app, index) => {
@@ -267,7 +270,7 @@ const CreatorGrid = props => {
autoComplete="email" autoComplete="email"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
onChange={e => setFormMail(e.target.value)} onChange={e => setFormMail(e.target.value)}
/> />
<TextField <TextField
required required
@@ -285,7 +288,7 @@ const CreatorGrid = props => {
margin="normal" margin="normal"
variant="outlined" variant="outlined"
autoComplete="off" autoComplete="off"
onChange={e => setMessage(e.target.value)} onChange={e => setMessage(e.target.value)}
/> />
</div> </div>
<Button <Button
@@ -0,0 +1,446 @@
import React from "react";
import {
Box,
Button,
Typography,
Stack,
styled,
} from "@mui/material";
import theme from "../theme.jsx";
// Simple icon placeholders; replace with proper assets if desired
const StepIcon = styled("div")(({ completed }) => ({
width: 28,
height: 28,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
fontSize: 14,
color: completed ? "#0C111D" : "#ffffff",
background: completed ? "#43D17C" : "#2F2F2F",
border: completed ? "1px solid #43D17C" : "1px solid rgba(255,255,255,0.15)",
flexShrink: 0,
}));
// Single continuous rail will be drawn once for the entire steps list
const StepCard = styled(Box)(({ completed, flash }) => ({
display: "flex",
gap: "14px",
minHeight: 60,
backgroundColor: "#212121",
border: `1px solid ${flash ? "#f87171" : completed ? "#43D17C" : "rgba(255, 255, 255, 0.1)"}`,
borderRadius: "12px",
padding: "16px",
width: "100%",
flex: 1,
minWidth: 0,
}));
const PrimaryButton = styled(Button)({
background: "linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
color: "#fff",
borderRadius: 6,
textTransform: "none",
fontWeight: 600,
fontSize: 14,
padding: "8px 20px",
"&:hover": {
opacity: 0.95,
background: "linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
},
});
const SecondaryButton = styled(Button)({
color: "#FF8544",
borderRadius: 6,
textTransform: "none",
fontWeight: 600,
fontSize: 14,
padding: "8px 12px",
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.08)",
},
"&.Mui-disabled": {
color: "rgba(255, 255, 255, 0.5)",
backgroundColor: "rgba(255, 255, 255, 0.06)",
},
});
const StepItem = React.forwardRef(({ step, iconRef }, ref) => (
<Box ref={ref} sx={{ display: "flex", gap: 1.2, alignItems: "flex-start" }}>
<Box ref={iconRef} sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: 40, height: 28, zIndex: 1, mt: 1.5 }}>
<StepIcon completed={step.completed}>{step.index}</StepIcon>
</Box>
<StepCard completed={step.completed} flash={Array.isArray(step.flashKeys) && step.flashKeys.includes(step.key)}>
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between", gap: 1, width: "100%" }}>
<Stack direction="column" spacing={1} sx={{ width: "70%" }}>
<Typography
sx={{
color: "#ffffff",
fontWeight: 600,
fontSize: 16,
fontFamily: theme.typography.fontFamily,
}}
>
{step.title}
</Typography>
{step.description && (
<Typography
sx={{
color: "#c5c5c5",
fontSize: 12,
fontFamily: theme.typography.fontFamily,
}}
>
{step.description}
</Typography>
)}
</Stack>
<Stack direction="row" spacing={1}>
{step.secondaryCta && (
<SecondaryButton onClick={step.secondaryCta.onClick}>
{step.secondaryCta.label}
</SecondaryButton>
)}
{step.primaryCta && (
<PrimaryButton onClick={step.primaryCta.onClick}>
{step.primaryCta.label}
</PrimaryButton>
)}
</Stack>
</Box>
</StepCard>
</Box>
));
const DashboardOnboarding = ({
open,
onClose,
headerTitle = "Get started with your Dashboard",
headerSubtitle = "Follow these steps to unlock insights.",
footer,
globalUrl,
onExplore,
}) => {
// Internal completion state only; handlers are defined separately
const [completed, setCompleted] = React.useState({
docs: false,
apps: false,
workflow: false,
wait: false,
invite: false,
});
const [checkingApps, setCheckingApps] = React.useState(false);
const [checkingWait, setCheckingWait] = React.useState(false);
const [flashKeys, setFlashKeys] = React.useState([]);
const [waitProgress, setWaitProgress] = React.useState(0);
// Load persisted completion state
React.useEffect(() => {
try {
const raw = localStorage.getItem("dashboard_onboarding_completed");
if (!raw) return;
const data = JSON.parse(raw);
if (data && typeof data === "object") {
setCompleted((prev) => ({ ...prev, ...data }));
}
} catch {}
}, []);
// Persist completion state
React.useEffect(() => {
try {
localStorage.setItem("dashboard_onboarding_completed", JSON.stringify(completed));
} catch {}
}, [completed]);
// Handlers
const handleDocsClick = React.useCallback(() => {
window.open('/docs', '_blank');
setCompleted((prev) => ({ ...prev, docs: true }));
}, []);
const handleDiscoverApps = React.useCallback(() => {
window.open('/apps?tab=discover_apps', '_blank');
}, []);
const handleCheckAppsStatus = React.useCallback(async () => {
if (checkingApps) return;
setCheckingApps(true);
try {
const resp = await fetch(`${globalUrl}/api/v1/apps`, {
method: 'GET',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
credentials: 'include',
});
if (resp.status !== 200) return;
const data = await resp.json();
let count = 0;
if (Array.isArray(data)) {
count = data.length;
} else if (data && typeof data === 'object') {
count = Object.keys(data).length;
}
if (count >= 3) {
setCompleted((prev) => ({ ...prev, apps: true }));
}
} catch (_) {
// ignore
} finally {
setCheckingApps(false);
}
}, [checkingApps]);
const handleOpenWorkflow = React.useCallback(() => {
window.open('/workflows/b658f2a0-7316-40d9-97ed-350a54fe3adc', '_blank');
setCompleted((prev) => ({ ...prev, workflow: true }));
}, []);
const handleWaitCheck = React.useCallback(async () => {
if (checkingWait) return;
setCheckingWait(true);
try {
const days = 5;
const url = `${globalUrl}/api/v1/stats/workflow_executions_finished?days=${days}`;
const resp = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
credentials: 'include',
});
if (resp.status !== 200) return;
const data = await resp.json();
const entries = Array.isArray(data?.entries) ? data.entries : [];
const now = new Date();
let successDays = 0;
for (let i = 0; i < entries.length; i++) {
const e = entries[i];
const d = new Date(e?.Date || e?.date || e?.time || e?.timestamp);
const diffDays = Math.floor((now - d) / (24 * 60 * 60 * 1000));
const value = Number(e?.Value ?? e?.value ?? 0);
if (!isNaN(diffDays) && diffDays <= 4 && value > 0) {
successDays += 1;
}
}
const simulated = Math.min(5, successDays);
if (simulated < 5) {
setWaitProgress(simulated);
setFlashKeys(["wait"]);
setTimeout(() => setFlashKeys([]), 800);
setCheckingWait(false);
} else {
setCompleted((prev) => ({ ...prev, wait: true }));
setCheckingWait(false);
}
} catch (_) {
// ignore
} finally {
setCheckingWait(false);
}
setCheckingWait(false);
}, [checkingWait, globalUrl]);
const handleOpenUsers = React.useCallback(() => {
window.open('/admin?tab=users', '_blank');
setCompleted((prev) => ({ ...prev, invite: true }));
}, []);
const steps = [
{
index: 1,
key: 'docs',
title: 'Read our docs to understand Shuffle',
description: 'Explore the basics of Shuffle in our documentation. It will help you understand the platform and how to use it.',
primaryCta: { label: 'Read docs', onClick: handleDocsClick },
completed: completed.docs,
},
{
index: 2,
key: 'apps',
title: 'Activate at least 3 apps',
description: 'Go to Discover Apps and enable your first three integrations.',
primaryCta: { label: 'Discover apps', onClick: handleDiscoverApps },
secondaryCta: { label: checkingApps ? 'Checking…' : 'Check status', onClick: handleCheckAppsStatus, disabled: checkingApps },
completed: completed.apps,
},
{
index: 3,
key: 'workflow',
title: 'Save a public workflow and start its scheduler',
description: 'Open the public workflow, save it to your org, and start a daily scheduler.',
primaryCta: { label: 'Open public workflow', onClick: handleOpenWorkflow },
completed: completed.workflow,
},
{
index: 4,
key: 'wait',
title: `Wait for 5 days of runs${completed.wait ? '' : waitProgress > 0 ? ` (${waitProgress}/5)` : ''}`,
description: 'We will show daily stats after 5 runs. Come back to check again.',
primaryCta: { label: checkingWait ? 'Checking…' : 'Check status', onClick: handleWaitCheck, disabled: checkingWait },
completed: completed.wait,
},
{
index: 5,
key: 'invite',
title: 'Invite more team members (optional)',
description: 'Add teammates to collaborate in your org.',
primaryCta: { label: 'Open users page', onClick: handleOpenUsers },
completed: completed.invite,
},
];
const mandatoryKeys = ['docs', 'apps', 'workflow', 'wait'];
const handleFinalDone = React.useCallback(() => {
const missing = mandatoryKeys.filter((k) => !completed[k]);
if (missing.length === 0) {
try { localStorage.setItem("dashboard_onboarding_complete", "true"); } catch {}
if (typeof onExplore === 'function') {
try { onExplore(); } catch {}
}
if (onClose) onClose();
return;
}
setFlashKeys(missing);
setTimeout(() => setFlashKeys([]), 800);
}, [completed, onClose, onExplore]);
if (!open) return null;
return (
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000 }}>
{/* Blur overlay with visible background */}
<Box
onClick={onClose}
sx={{
position: "absolute",
inset: 0,
backdropFilter: "blur(6px)",
background: "rgba(0,0,0,0.05)",
}}
/>
{/* Modal container */}
<Box
sx={{
position: "relative",
zIndex: 2001,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
p: 2,
}}
>
<Box
sx={{
width: "100%",
maxWidth: 780,
backgroundColor: "#1A1A1A",
borderRadius: "16px",
border: "1px solid rgba(255,255,255,0.08)",
p: 3,
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
}}
>
{/* Header */}
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2 }}>
<Box>
<Typography
sx={{
color: "#f1f1f1",
fontWeight: 700,
fontSize: 22,
letterSpacing: "-0.2px",
fontFamily: theme.typography.fontFamily,
}}
>
{headerTitle}
</Typography>
<Typography
sx={{
color: "#c5c5c5",
mt: 0.5,
fontSize: 14,
fontFamily: theme.typography.fontFamily,
}}
>
{headerSubtitle}
</Typography>
</Box>
</Box>
{/* Steps list with a single continuous rail */}
<Box sx={{ position: "relative", display: "flex", flexDirection: "column", gap: 3, marginLeft: -1.5, marginTop: 4 }}>
{/* Base grey rail */}
<Box
sx={{
position: "absolute",
left: 20,
top: 30,
bottom: 60,
width: 2,
background: "rgba(255,255,255,0.2)",
borderRadius: 2,
}}
/>
{/* Green segments between consecutive completed steps */}
{steps.map((s, i) => ({ s, i }))
.filter(({ i }) => i < steps.length - 1)
.filter(({ i }) => steps[i].completed && steps[i + 1].completed)
.map(({ i }) => (
<Box
key={`seg-${i}`}
sx={{
position: "absolute",
left: 20,
top: 30 + i * 110, // approximate segment height per step
height: 130, // matches gap+card combined height; tuned visually
width: 2,
background: "#43D17C",
borderRadius: 2,
}}
/>
))}
{steps.map((step, idx) => (
<StepItem key={step.key || idx} step={{...step, flashKeys}} />
))}
</Box>
{/* Footer */}
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 1.5 }}>
{footer}
<Button variant="contained" color="primary" onClick={handleFinalDone}
sx={{
fontSize: 14,
padding: "8px 60px",
}}
>
Explore Now
</Button>
</Box>
</Box>
</Box>
</Box>
);
};
export default DashboardOnboarding;
+1
View File
@@ -143,6 +143,7 @@ const Detection = (props) => {
size="small" size="small"
sx={{ mr: 2 }} sx={{ mr: 2 }}
value={searchQuery} value={searchQuery}
disabled
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
{/* <Button {/* <Button
+231 -20
View File
@@ -16,11 +16,14 @@ import {
import { import {
OpenInNew as OpenInNewIcon, OpenInNew as OpenInNewIcon,
FmdGood as FmdGoodIcon, FmdGood as FmdGoodIcon,
Check as CheckIcon,
} from "@mui/icons-material" } from "@mui/icons-material"
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import RunDetectionTest from '../components/RunDetectionTest.jsx';
import theme from '../theme.jsx'; import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx"; import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
import CollectIngestModal from "../components/CollectIngestModal.jsx";
import { import {
green, green,
red, red,
@@ -30,13 +33,12 @@ import {
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => { const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
if (!isDetectionActive) { //if (!isDetectionActive) {
toast.warn("Connect to siem first for global enable/disable to work"); // toast.warn("Connect first for global enable/disable to work");
return; // return;
} //}
const action = folderDisabled ? "enable_folder" : "disable_folder"; const action = folderDisabled ? "enable_folder" : "disable_folder";
//const url = `${globalUrl}/api/v1/detections/${detectionType}/selected_rules/${action}`;
const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`; const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`;
fetch(url, { fetch(url, {
@@ -68,10 +70,117 @@ const DetectionExplorer = (props) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [workflow, setWorkflow] = useState({}) const [workflow, setWorkflow] = useState({})
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
const [isDetectionValid, setIsDetectionValid] = useState(false)
const [availableDetection, setAvailableDetection] = React.useState([]); const [availableDetection, setAvailableDetection] = React.useState([]);
const [environmentList, setEnvironmentList] = React.useState([]) const [environmentList, setEnvironmentList] = React.useState([])
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
const [workflows, setWorkflows] = React.useState([])
const [apps, setApps] = React.useState([])
const [pipelines, setPipelines] = React.useState([])
const [ticketWebhook, setTicketWebhook] = React.useState("");
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const handleGetAllTriggers = () => {
fetch(globalUrl + "/api/v1/triggers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all triggers");
}
return response.json();
})
.then((responseJson) => {
//setWebHooks(responseJson.webhooks || []);
//setAllSchedules(responseJson.schedules || []);
setPipelines(responseJson.pipelines || []);
//setShowLoader(false);
})
.catch((error) => {
// toast(error.toString());
});
};
const getWorkflows = () => {
const url = `${globalUrl}/api/v1/workflows`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success !== false) {
setWorkflows(responseJson || []);
for (var i = 0; i < responseJson?.length; i++) {
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
for (var triggerkey in responseJson[i].triggers) {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
//setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
}
}
}
}
} else {
toast.warn("Failed to load workflows. Please try again or contact support@shuffler if this persists.")
}
})
.catch((error) => {
toast(error.toString());
});
}
const getApps = () => {
const url = `${globalUrl}/api/v1/apps`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success === false) {
toast.warn("Failed to load apps. Please try again or contact support@shuffler if this persists.")
} else {
setApps(responseJson)
}
})
.catch((error) => {
toast(error.toString());
});
}
const loadUsecases = () => { const loadUsecases = () => {
const url = `${globalUrl}/api/v1/workflows/usecases` const url = `${globalUrl}/api/v1/workflows/usecases`
@@ -133,6 +242,11 @@ const DetectionExplorer = (props) => {
} }
const handleConnectClick = () => { const handleConnectClick = () => {
// NEW way to handle it
setShowCollectIngestMenu(true)
return
if (detectionWorkflowId !== "") { if (detectionWorkflowId !== "") {
console.log("Already have a workflow ID for this detection") console.log("Already have a workflow ID for this detection")
//toast.info(`Already have a detection workflow for ${detectionInfo?.category}`) //toast.info(`Already have a detection workflow for ${detectionInfo?.category}`)
@@ -179,7 +293,7 @@ const DetectionExplorer = (props) => {
} }
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) { if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
setIsDetectionValid(responseJson.workflow_valid) //setIsDetectionValid(responseJson.workflow_valid)
} }
} else { } else {
if (responseJson.reason !== undefined && responseJson.reason !== null) { if (responseJson.reason !== undefined && responseJson.reason !== null) {
@@ -238,8 +352,12 @@ const DetectionExplorer = (props) => {
} }
useEffect(() => { useEffect(() => {
getApps()
getWorkflows()
loadUsecases() loadUsecases()
loadEnvironments() loadEnvironments()
handleGetAllTriggers()
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -251,18 +369,87 @@ const DetectionExplorer = (props) => {
return return
} }
handleConnectClick() console.log("Detection info: ", detectionInfo)
//handleConnectClick()
}, [detectionInfo]) }, [detectionInfo])
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) => const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) || rule?.file_name?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase()) rule?.title?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
rule?.description?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery)
) )
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0 const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0
const submitPipeline = (pipeline, environment) => {
var pipelineConfig = {
command: pipeline,
name: pipeline,
type: "create",
environment: "",
workflow_id: "",
trigger_id: "",
start_node: "",
}
if (environment !== undefined && environment !== "") {
pipelineConfig.environment = environment
}
const url = `${globalUrl}/api/v1/triggers/pipeline`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(pipelineConfig),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success && pipelineConfig.type !== "delete") {
toast.error("Failed to set pipeline: " + responseJson.reason);
} else {
if (pipelineConfig.type === "create") {
toast.success("Pipeline will be created: " + responseJson.reason)
//setPipelineModalOpen(false)
} else if (pipelineConfig.type === "stop") {
toast.success("Pipeline will be stopped: " + responseJson.reason)
//setPipelineModalOpen(false)
} else {
toast.info("Unknown pipeline type: " + pipelineConfig.type)
}
}
})
.catch((error) => {
console.log("Get pipeline error: ", error.toString());
});
}
return ( return (
<Container> <Container>
<CollectIngestModal
globalUrl={globalUrl}
open={showCollectIngestMenu}
setOpen={setShowCollectIngestMenu}
workflows={workflows}
getWorkflows={getWorkflows}
apps={apps}
/>
<Paper <Paper
style={{ style={{
marginTop: 50, marginTop: 50,
@@ -316,8 +503,21 @@ const DetectionExplorer = (props) => {
</div> </div>
: */} : */}
<div style={{marginRight: 20, }}>
<RunDetectionTest
globalUrl={globalUrl}
pipelines={pipelines}
workflows={workflows}
ticketWebhook={ticketWebhook}
detectionWorkflowId={detectionWorkflowId}
changePipelineState={undefined}
submitPipelineWrapper={submitPipeline}
/>
</div>
<Button <Button
variant="contained" variant={detectionWorkflowId === "" ? "contained" : "outlined"}
onClick={() => { onClick={() => {
handleConnectClick() handleConnectClick()
}} }}
@@ -326,18 +526,26 @@ const DetectionExplorer = (props) => {
// Red = workflow exists, validation is false // Red = workflow exists, validation is false
// Green = workflow exists, validation is true // Green = workflow exists, validation is true
// Grey = workflow does not exist // Grey = workflow does not exist
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
}} }}
> >
{loading ? <CircularProgress size={24} /> : {loading ?
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` : <CircularProgress size={24} />
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`} :
detectionWorkflowId !== "" ?
<span>
<CheckIcon style={{color: green, marginRight: 10, top: 5, }} />
Connected
</span>
:
`Connect to ${detectionInfo?.category}`
}
</Button> </Button>
{/**/} {/**/}
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ? {detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}> <Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
<a href="/admin?tab=Locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer"> <a href="/admin?tab=locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} /> <FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
</a> </a>
</Tooltip> </Tooltip>
@@ -345,7 +553,7 @@ const DetectionExplorer = (props) => {
</div> </div>
</Box> </Box>
{filteredRules?.length > 0 ? {ruleInfo?.length > 0 ?
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -367,7 +575,9 @@ const DetectionExplorer = (props) => {
size="small" size="small"
sx={{ mr: 2 }} sx={{ mr: 2 }}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => {
setSearchQuery(e?.target?.value?.replaceAll(" ", "_")?.toLowerCase())
}}
/> />
</Box> </Box>
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
@@ -386,7 +596,7 @@ const DetectionExplorer = (props) => {
<Divider /> <Divider />
<Box <Box
sx={{ sx={{
height: "500px", minHeight: 500,
width: "100%", width: "100%",
overflowY: "auto", overflowY: "auto",
p: 1, p: 1,
@@ -410,6 +620,7 @@ const DetectionExplorer = (props) => {
folderDisabled={folderDisabled} folderDisabled={folderDisabled}
isDetectionActive={isDetectionActive} isDetectionActive={isDetectionActive}
ruleDetails={rule}
ruleMapping={ruleMapping} ruleMapping={ruleMapping}
setRuleMapping={setRuleMapping} setRuleMapping={setRuleMapping}
+67 -26
View File
@@ -12,16 +12,18 @@ import {
FormLabel, FormLabel,
} from "@mui/material"; } from "@mui/material";
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx'; import LineChartWrapper, { LoadStats } from "../components/LineChartWrapper.jsx";
import { import {
Edit as EditIcon, Edit as EditIcon,
Refresh as RefreshIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
import theme from '../theme.jsx'; import theme from '../theme.jsx';
const RuleCard = (props) => {
const { ruleName, description, file_id, globalUrl, folderDisabled, isDetectionActive, availableDetection, ruleMapping, setRuleMapping, ruleDetails, key, ...otherProps } = props
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => {
const [openCodeEditor, setOpenCodeEditor] = React.useState(false); const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
const [fileData, setFileData] = React.useState(""); const [fileData, setFileData] = React.useState("");
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled); const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
@@ -30,35 +32,33 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
const [responseValue, setResponseValue] = React.useState("No response action") const [responseValue, setResponseValue] = React.useState("No response action")
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host); const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
console.log("Rulemapping: ", ruleMapping)
useEffect(() => { useEffect(() => {
if (key < 10) {
//const url = `${globalUrl}/api/v1/stats/app_executions_test2` console.log("RuleCard Key: ", key, ruleName, file_id, otherProps)
//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) { if (ruleDetails?.title === undefined || ruleDetails?.title === null || ruleDetails?.title.length === 0) {
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping) //toast.error("Can't load stats for this rule. Contact support@shuffler.io if this persists.")
} return
}
const resp = LoadStats(globalUrl, `detection_rule_${ruleDetails?.title.replaceAll(" ", "_").toLowerCase()}`)
resp.then((data) => {
if (data === undefined) {
setFilteredBarchart([])
} else {
setFilteredBarchart(data)
}
})
}, []) }, [])
console.log("Response Value: ", responseValue)
const handleSwitchChange = (event) => { const handleSwitchChange = (event) => {
if (folderDisabled) { if (folderDisabled) {
toast.warn("Enable the directory to enable individual rules"); toast.warn("Enable the directory to enable individual rules");
return; return;
} }
if (!isTenzirActive) { if (!isDetectionActive) {
toast.warn("Connect to the siem first to enable/disable the rule"); toast.warn("Connect to the siem first to enable/disable the rule");
return; return;
} }
@@ -96,6 +96,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
}); });
}; };
var parsedRulename = ruleName.charAt(0).toUpperCase() + ruleName.slice(1).replaceAll("_", " ")
return ( return (
<Card style={{ <Card style={{
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
@@ -116,10 +117,13 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
color: "white", color: "white",
}} }}
> >
<Typography variant="h6">{ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})</Typography> <Typography variant="h6">
{parsedRulename} {/*({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})*/}
</Typography>
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Select {/*
<Select
MenuProps={{ MenuProps={{
disableScrollLock: true, disableScrollLock: true,
}} }}
@@ -148,6 +152,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
color: "white", color: "white",
height: 40, height: 40,
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
marginRight: 20,
}} }}
> >
<MenuItem <MenuItem
@@ -178,6 +183,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
) )
})} })}
</Select> </Select>
*/}
<Tooltip title="Edit Rule" placement="top"> <Tooltip title="Edit Rule" placement="top">
@@ -204,12 +210,47 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
minHeight: 40, minHeight: 40,
maxHeight: 40, maxHeight: 40,
display: "flex",
}}> }}>
{filteredBarchart === null ? null : <Tooltip title="Refresh stats" placement="top">
<DashboardBarchart <IconButton
timelineData={filteredBarchart} onClick={() => {
/> if (ruleDetails?.title === undefined || ruleDetails?.title === null || ruleDetails?.title.length === 0) {
toast.error("Can't load stats for this rule. Contact support@shuffler.io if this persists.")
return
}
const resp = LoadStats(globalUrl, `detection_rule_${ruleDetails?.title.replaceAll(" ", "_").toLowerCase()}`)
resp.then((data) => {
console.log("DATA: ", data)
if (data === undefined) {
setFilteredBarchart([])
} else {
setFilteredBarchart(data)
}
})
}}
>
<RefreshIcon
color="secondary"
/>
</IconButton>
</Tooltip>
{filteredBarchart === null ? <Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginLeft: 18, }}>No stats yet</Typography> :
<div style={{minWidth: "90%", }}>
<LineChartWrapper
inputname={""}
keys={filteredBarchart}
height={100}
width={"100%"}
border={false}
color={"#808080"}
/>
</div>
} }
</div> </div>
{/* {/*
+13 -2
View File
@@ -16,6 +16,7 @@ import {
ListItemText, ListItemText,
} from '@mui/material'; } from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material'; import { Search as SearchIcon } from '@mui/icons-material';
import useDebouncedCallback from '../utils/useDebouncedCallback.js';
const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7"); const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7");
@@ -69,12 +70,22 @@ const DiscordChat = props => {
} }
const SearchBox = ({ currentRefinement, refine }) => { const SearchBox = ({ currentRefinement, refine }) => {
const [inputValue, setInputValue] = useState("");
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
useEffect(() => {
setInputValue(currentRefinement || "");
}, [currentRefinement]);
return ( return (
<form noValidate action="" role="search"> <form noValidate action="" role="search">
<TextField <TextField
fullWidth fullWidth
value={currentRefinement} value={inputValue}
onChange={(event) => refine(event.currentTarget.value)} onChange={(event) => {
const value = event.currentTarget.value;
setInputValue(value);
debouncedRefine(value);
}}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
event.preventDefault(); event.preventDefault();
+4 -1
View File
@@ -26,6 +26,7 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
} from '@mui/material'; } from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
@@ -97,6 +98,8 @@ const DocsGrid = props => {
} }
} }
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
return ( return (
<form noValidate action="" role="search"> <form noValidate action="" role="search">
<TextField <TextField
@@ -122,7 +125,7 @@ const DocsGrid = props => {
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
removeQuery("q") removeQuery("q")
refine(event.currentTarget.value) debouncedRefine(event.currentTarget.value)
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
+2 -2
View File
@@ -917,10 +917,10 @@ const EditWorkflow = (props) => {
}} }}
/> />
<Typography variant="h6" style={{ marginBottom: 5 }}> <Typography variant="h6" style={{ marginBottom: 5 }}>
Generate Workflow from Flowchart Generate Workflow from Flowchart (beta)
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}> <Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}>
Click to upload your flowchart - AI will convert it to a workflow Click to upload your flowchart - Your LLM will convert it to a workflow
</Typography> </Typography>
<Typography variant="caption" color="textSecondary"> <Typography variant="caption" color="textSecondary">
PNG, JPG, JPEG Max 5MB PNG, JPG, JPEG Max 5MB
+275 -154
View File
@@ -35,6 +35,7 @@ import {
Help as HelpIcon, Help as HelpIcon,
ExpandLess as ExpandLessIcon, ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon, ExpandMore as ExpandMoreIcon,
Delete as DeleteIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
@@ -53,7 +54,7 @@ const EnvironmentTab = memo((props) => {
pipelines: false, pipelines: false,
proxies: false, proxies: false,
}) })
const [installationTab, setInstallationTab] = React.useState(0); const [installationTab, setInstallationTab] = React.useState(1);
const [isExpanded, setIsExpanded] = React.useState(false); const [isExpanded, setIsExpanded] = React.useState(false);
const [listItemExpanded, setListItemExpanded] = React.useState(-1); const [listItemExpanded, setListItemExpanded] = React.useState(-1);
const [, setUpdate] = React.useState(0); const [, setUpdate] = React.useState(0);
@@ -61,6 +62,7 @@ const EnvironmentTab = memo((props) => {
const [selectedEnvironment, setSelectedEnvironment] = React.useState(null); const [selectedEnvironment, setSelectedEnvironment] = React.useState(null);
const [selectedSubOrg, setSelectedSubOrg] = React.useState([]); const [selectedSubOrg, setSelectedSubOrg] = React.useState([]);
const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined) const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined)
const [currentEnvQueue, setCurrentEnvQueue] = React.useState([])
const { themeMode, supportEmail, brandColor } = useContext(Context); const { themeMode, supportEmail, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
@@ -408,6 +410,11 @@ const EnvironmentTab = memo((props) => {
skipPipeline = true skipPipeline = true
} }
var showDetection = false
if (commandController.detection === true) {
showDetection = true
}
var addProxy = false var addProxy = false
if (commandController.proxies === true) { if (commandController.proxies === true) {
addProxy = true addProxy = true
@@ -422,12 +429,11 @@ const EnvironmentTab = memo((props) => {
-e AUTH="${auth}" \\ -e AUTH="${auth}" \\
-e ENVIRONMENT_NAME="${environment.Name}" \\ -e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${environment.org_id}" \\ -e ORG="${environment.org_id}" \\
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\
-e SHUFFLE_SWARM_CONFIG=run \\ -e SHUFFLE_SWARM_CONFIG=run \\
-e SHUFFLE_LOGS_DISABLED=true \\
-e BASE_URL="${newUrl}" \\${addProxy ? ` -e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""} -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? `
-v /tmp:/tmp \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest ghcr.io/shuffle/shuffle-orborus:latest
`) `)
} else if (installationTab === 2) { } else if (installationTab === 2) {
@@ -676,6 +682,76 @@ const EnvironmentTab = memo((props) => {
) )
} }
const removeEnvQueueItem = (environment, queueItem) => {
const url = `${globalUrl}/api/v1/workflows/queue/confirm`
const headers = {
"Org-Id": environment.Name,
"Org": environment.org_id,
"Authorization": environment.auth,
}
const items = {
"data": [queueItem],
}
fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(items),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
toast("Failed removing queue item")
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success !== false) {
toast("Successfully removed queue item")
getEnvQueue(environment)
} else {
toast("Failed removing queue item")
}
})
.catch((error) => {
toast(error.toString());
})
}
const getEnvQueue = (environment) => {
const url = `${globalUrl}/api/v1/workflows/queue`
const headers = {
"Org-Id": environment.Name,
"Org": environment.org_id,
"Authorization": environment.auth,
}
fetch(url, {
method: "POST",
headers: headers,
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success !== false && responseJson?.data?.length > 0) {
setCurrentEnvQueue(responseJson.data)
}
})
.catch((error) => {
toast(error.toString());
})
}
const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => { const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => {
const data = { const data = {
action: "suborg_distribute", action: "suborg_distribute",
@@ -881,14 +957,14 @@ const EnvironmentTab = memo((props) => {
<ListItem <ListItem
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px", gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
width: "100%", width: "100%",
minWidth: 800, minWidth: 800,
paddingBottom: 0, paddingBottom: 0,
borderBottom: theme.palette.defaultBorder, borderBottom: theme.palette.defaultBorder,
}} }}
> >
{["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => { {["Type", "Status", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => {
return ( return (
<ListItemText <ListItemText
@@ -912,7 +988,6 @@ const EnvironmentTab = memo((props) => {
key={rowIndex} key={rowIndex}
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
backgroundColor: theme.palette.platformColor, backgroundColor: theme.palette.platformColor,
height: 40, height: 40,
width: "100%", width: "100%",
@@ -1014,12 +1089,17 @@ const EnvironmentTab = memo((props) => {
key={index} key={index}
style={{ cursor: "pointer", backgroundColor: bgColor, marginLeft: 0, borderBottomLeftRadius: environments?.length - 1 === index ? 8 : 0, borderBottomRightRadius: environments?.length - 1 === index ? 8 : 0, display: 'grid', gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 405px 125px", }} style={{ cursor: "pointer", backgroundColor: bgColor, marginLeft: 0, borderBottomLeftRadius: environments?.length - 1 === index ? 8 : 0, borderBottomRightRadius: environments?.length - 1 === index ? 8 : 0, display: 'grid', gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 405px 125px", }}
onClick={() => { onClick={() => {
if (environment.Type === "cloud") { if (environment.Type === "cloud") {
toast("Cloud environments are not configurable. To see what is possible, create a new environment.") toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
return return
} }
setListItemExpanded(listItemExpanded === index ? -1 : index) setListItemExpanded(listItemExpanded === index ? -1 : index)
if (listItemExpanded !== index) {
getEnvQueue(environment)
setCurrentEnvQueue([])
}
}} }}
> >
<ListItemText <ListItemText
@@ -1100,7 +1180,7 @@ const EnvironmentTab = memo((props) => {
<br /> <br />
<br /> <br />
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"} {environment?.Type === "cloud" ? "" : "Timeout: 180 seconds"} Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"}. {environment?.Type === "cloud" ? "" : "Timeout: 180 seconds"}
</Typography> </Typography>
} placement="top"> } placement="top">
<Typography <Typography
@@ -1182,42 +1262,14 @@ const EnvironmentTab = memo((props) => {
} }
/> />
<ListItemText
primary={
selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
"N/A"
:
environment.licensed ? (
<Tooltip title="Scale configured (auto on cloud)" placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip>
) : (
<Tooltip
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
href="/docs/configuration#scaling-shuffle"
target="_blank"
rel="noopener noreferrer"
>
<CancelIcon style={{ color: "#f85a3e" }} />
</a>
</Tooltip>
)
}
style={{
minWidth: 60,
marginLeft: 20,
overflow: "hidden",
whiteSpace: "normal",
wordWrap: "break-word",
padding: 8,
display: "table-cell",
}}
/>
<ListItemText <ListItemText
style={{
marginLeft: 30,
overflow: "hidden",
whiteSpace: "normal",
wordWrap: "break-word",
display: "table-cell",
}}
primary={ primary={
environment.Type === "cloud" ? environment.Type === "cloud" ?
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top"> <Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
@@ -1231,40 +1283,32 @@ const EnvironmentTab = memo((props) => {
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top"> <Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} /> <CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip> </Tooltip>
</a> </a>
) : ( ) : (
<Tooltip <Tooltip
title="Data Lake node disabled. Click to enable." title="Data Lake node disabled. Click to enable."
placement="top" placement="top"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
window.open("/detections/Sigma", "_blank") window.open("/detections/Sigma", "_blank")
}} }}
> >
<a <a
href="/detections/Sigma" href="/detections/Sigma"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<CancelIcon style={{ color: "#f85a3e" }} /> <CancelIcon style={{ color: "#f85a3e" }} />
</a> </a>
</Tooltip> </Tooltip>
) )
} }
style={{ />
minWidth: 60,
marginLeft: 40,
overflow: "hidden",
whiteSpace: "normal",
wordWrap: "break-word",
display: "table-cell",
}}
/>
<ListItemText <ListItemText
primary={( primary={(
@@ -1274,12 +1318,12 @@ const EnvironmentTab = memo((props) => {
)} )}
primaryTypographyProps={{ primaryTypographyProps={{
style:{ style:{
maxWidth: 150,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: "hidden", overflow: "hidden",
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
wordWrap: "break-word", wordWrap: "break-word",
transition: "all 0.3s ease", transition: "all 0.3s ease",
textAlign: "center",
}}} }}}
style={{ style={{
minWidth: 120, minWidth: 120,
@@ -1292,7 +1336,7 @@ const EnvironmentTab = memo((props) => {
primary={environment.Type} primary={environment.Type}
primaryTypographyProps={{ primaryTypographyProps={{
style:{ style:{
minWidth: 70, minWidth: 50,
overflow: "hidden", overflow: "hidden",
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@@ -1302,6 +1346,7 @@ const EnvironmentTab = memo((props) => {
}}} }}}
style={{display: "table-cell",}} style={{display: "table-cell",}}
/> />
<ListItemText <ListItemText
primaryTypographyProps={{ primaryTypographyProps={{
style:{ style:{
@@ -1344,7 +1389,7 @@ const EnvironmentTab = memo((props) => {
}} }}
color="primary" color="primary"
> >
Make Default Default
</Button> </Button>
<Button <Button
variant={environment.archived ? "contained" : "outlined"} variant={environment.archived ? "contained" : "outlined"}
@@ -1418,34 +1463,38 @@ const EnvironmentTab = memo((props) => {
</ButtonGroup> </ButtonGroup>
{/*
<IconButton disabled={environment.Type === "cloud"} onClick={()=> {setIsExpanded(prev => !prev)}}> <IconButton disabled={environment.Type === "cloud"} onClick={()=> {setIsExpanded(prev => !prev)}}>
{listItemExpanded === index ? <ExpandLessIcon sx={{color: theme.palette.text.primary}} /> : <ExpandMoreIcon sx={{color: theme.palette.text.primary}}/>} {listItemExpanded === index ? <ExpandLessIcon sx={{color: theme.palette.text.primary}} /> : <ExpandMoreIcon sx={{color: theme.palette.text.primary}}/>}
</IconButton> </IconButton>
*/}
</div> </div>
</ListItemText> </ListItemText>
{selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ? {selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
<ListItemText <ListItemText
primary={ primary={
<Tooltip <Tooltip
title="Parent organization controlled environments. You can use, but not modify this environments. Contact an admin of your parent organization if you need changes to this." title="Parent organization controlled environments. You can use, but not modify this environments. Contact an admin of your parent organization if you need changes to this."
placement="top" placement="top"
> >
<Chip <Chip
label={"Parent"} style={{marginLeft: 200, }}
variant="contained" label={"Parent"}
color="secondary" variant="contained"
/> color="secondary"
</Tooltip> />
} </Tooltip>
style={{ textAlign: 'center', verticalAlign: 'middle', }} }
/> style={{ textAlign: 'center', verticalAlign: 'middle', }}
: />
<Tooltip :
title={environment.Name === "Cloud" ? "Cloud environments cannot be distributed" : "Distributed to sub-organizations. This means the sub organizations can use this environment, but can not modify it."} <Tooltip
placement="top" title={environment.Name === "Cloud" ? "Cloud environments cannot be distributed" : "Distributed to sub-organizations. This means the sub organizations can use this environment, but can not modify it."}
> placement="top"
<IconButton >
sx={{":hover": {backgroundColor: "transparent"}}} <IconButton
style={{marginLeft: 200, }}
disabled={ environment.Name === "Cloud" || userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false} disabled={ environment.Name === "Cloud" || userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@@ -1489,35 +1538,41 @@ const EnvironmentTab = memo((props) => {
aria-label="disabled tabs example" aria-label="disabled tabs example"
variant="scrollable" variant="scrollable"
scrollButtons="auto" scrollButtons="auto"
style={{textAlign: "center", marginTop: 25, }} style={{
textAlign: "center",
marginTop: 25,
marginBottom: 25,
borderBottom: "1px solid rgba(255,255,255,0.3)",
}}
> >
<Tab
value={0}
label=<span style={{color: theme.palette.text.secondary, }}>
<img
src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10, }}
/> Verbose (default)
</span>
/>
<Tab <Tab
value={1} value={1}
label=<span style={{color: theme.palette.text.secondary, }}> label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
<img <img
src="/icons/docker.svg" src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10,}} style={{ width: 20, height: 20, marginRight: 10,}}
/> Scale /> Docker (default)
</span> </span>
/> />
<Tab <Tab
value={2} value={2}
label=<span style={{color: theme.palette.text.secondary, }}> label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
<img <img
src="/icons/k8s.svg" src="/icons/k8s.svg"
style={{ width: 20, height: 20, marginRight: 10 }} style={{ width: 20, height: 20, marginRight: 10 }}
/> k8s /> Kubernetes
</span> </span>
/> />
<Tab
value={0}
style={{marginLeft: 300, }}
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
Verbose Mode
</span>
/>
</Tabs> </Tabs>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}> <Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? {installationTab === 2 ?
@@ -1563,6 +1618,7 @@ const EnvironmentTab = memo((props) => {
fontFamily: "monospace", fontFamily: "monospace",
fontSize: 18, fontSize: 18,
border: themeMode === "dark" ? "1px solid #555" : "1px solid #ddd", border: themeMode === "dark" ? "1px solid #555" : "1px solid #ddd",
minHeight: 325,
}} }}
> >
{getOrborusCommand(environment)} {getOrborusCommand(environment)}
@@ -1588,49 +1644,114 @@ const EnvironmentTab = memo((props) => {
<Divider style={{marginTop: 25, marginBottom: 10, }}/> <Divider style={{marginTop: 25, marginBottom: 10, }}/>
<div style={{display: 'flex', alignItems: 'center', }}> <div style={{display: 'flex', alignItems: 'center', }}>
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies:</Typography> <Checkbox <Checkbox
id="shuffle_skip_proxies" id="shuffle_skip_proxies"
onClick={() => { onClick={() => {
if (commandController.proxies === undefined) { if (commandController.proxies === undefined) {
commandController.proxies = true commandController.proxies = true
} else { } else {
commandController.proxies = !commandController.proxies commandController.proxies = !commandController.proxies
} }
setCommandController(commandController) setCommandController(commandController)
setUpdate(Math.random()) setUpdate(Math.random())
}} }}
/> />
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies</Typography>
</div> </div>
<div /> <div />
<div style={{display: 'flex', alignItems: 'center', }}> <div style={{display: 'flex', alignItems: 'center', }}>
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake:</Typography> <Checkbox <Checkbox
id="shuffle_skip_pipelines" id="shuffle_enable_detection"
onClick={() => { onClick={() => {
if (commandController.pipelines === undefined) { if (commandController.detection === undefined) {
commandController.pipelines = true commandController.detection = true
} else { } else {
commandController.pipelines = !commandController.pipelines commandController.detection = !commandController.detection
} }
setCommandController(commandController)
setUpdate(Math.random()) setCommandController(commandController)
}} setUpdate(Math.random())
/> }}
/>
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
</div> </div>
{/*
<div style={{display: 'flex', alignItems: 'center', }}>
<Checkbox
id="shuffle_skip_pipelines"
onClick={() => {
if (commandController.pipelines === undefined) {
commandController.pipelines = true
} else {
commandController.pipelines = !commandController.pipelines
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography>
</div>
*/}
</div> </div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}> <Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? null : {installationTab === 2 ? null :
<span> <span>
3. Verify if the node is running. Try to refresh the page a little while after running the command. 3. Verify if the Runtime Location is running. Refresh the page 2 minutes after running the command.
</span> </span>
} }
</Typography> </Typography>
</div> </div>
</div> </div>
</Grid>
</Grid> {currentEnvQueue.length === 0 ? null :
</Collapse> <List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
{currentEnvQueue.map((queueItem, queueIndex) => {
return (
<ListItem
style={{
backgroundColor: theme.palette.surfaceColor,
borderBottom: theme.palette.defaultBorder,
maxHeight: 50,
}}
>
<ListItemText style={{minWidth: 50, maxWidth: 50, }}>
{queueItem.priority}
</ListItemText>
<ListItemText style={{minWidth: 125, maxWidth: 150, marginLeft: 25, }}>
{queueItem.type}
</ListItemText>
<ListItemText style={{minWidth: 325, maxWidth: 350, marginLeft: 25, overflow: "auto", }}>
{queueItem.execution_argument}
</ListItemText>
<ListItemText style={{minWidth: 50, maxWidth: 50, marginLeft: 25, overflow: "auto", }}>
<Tooltip title="Remove job from queue">
<IconButton
onClick={()=>{
removeEnvQueueItem(
environment,
queueItem,
)
}}
>
<DeleteIcon style={{
color: red,
}} />
</IconButton>
</Tooltip>
</ListItemText>
</ListItem>
)
})}
</List>
}
</Grid>
</Grid>
</Collapse>
{showCPUAlert === false ? null : ( {showCPUAlert === false ? null : (
<ListItem <ListItem
+155 -91
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useContext, memo } from "react"; import React, { useState, useEffect, useContext, memo } from "react";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { GetIconInfo, } from "../views/Workflows2.jsx";
import { import {
IconButton, IconButton,
@@ -9,24 +10,25 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemSecondaryAction, ListItemSecondaryAction,
Tooltip, Tooltip,
Button, Button,
FormControl, ButtonGroup,
InputLabel, FormControl,
TextField, InputLabel,
Divider, TextField,
Select, Divider,
MenuItem, Select,
Dialog, MenuItem,
DialogTitle, Dialog,
DialogContent, DialogTitle,
DialogActions, DialogContent,
Typography, DialogActions,
Skeleton, Typography,
Checkbox, Skeleton,
Chip, Checkbox,
Menu, Chip,
Pagination, Menu,
PaginationItem, Pagination,
PaginationItem,
} from "@mui/material"; } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid"; import { DataGrid } from "@mui/x-data-grid";
@@ -352,8 +354,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={"Delete file"} title={"Delete file"}
style={{marginLeft: isSelectedFiles?5:15, }} style={{}}
aria-label={"Delete"} aria-label={"Delete"}
placement="right"
> >
<span> <span>
<IconButton <IconButton
@@ -628,8 +631,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
return; return;
} }
if (folder === undefined || folder === null || folder.length < 2) { if (folder === undefined || folder === null || folder.length < 1) {
toast("Please enter a valid folder name") toast("Please enter a valid folder name. For Root: /")
return return
} }
@@ -637,6 +640,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
url: url, url: url,
path: folder, path: folder,
field_3: downloadBranch || "master", field_3: downloadBranch || "master",
namespace: selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" ? selectedCategory : "",
}; };
if (field1.length > 0) { if (field1.length > 0) {
@@ -1312,57 +1317,80 @@ const [filesLoaded, setFilesLoaded] = useState(false);
<Button <ButtonGroup style={{top: -10, position: "relative", }}>
color="primary" <Button
variant="contained" color="primary"
onClick={() => { variant="contained"
upload.click(); onClick={() => {
}} upload.click();
style={{ textTransform: 'none',fontSize: 16, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}} }}
> style={{ textTransform: 'none',fontSize: 16, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
Upload files >
</Button> Upload files
{/* <FileCategoryInput </Button>
isSet={renderTextBox} /> */} {/* <FileCategoryInput
<input isSet={renderTextBox} /> */}
hidden <input
type="file" hidden
multiple type="file"
ref={(ref) => (upload = ref)} multiple
onChange={(event) => { ref={(ref) => (upload = ref)}
//const file = event.target.value onChange={(event) => {
//const fileObject = URL.createObjectURL(actualFile) //const file = event.target.value
//setFile(fileObject) //const fileObject = URL.createObjectURL(actualFile)
//const files = event.target.files[0] //setFile(fileObject)
uploadFiles(event.target.files); //const files = event.target.files[0]
uploadFiles(event.target.files);
}} }}
/> />
<Button <Button
style={{ marginLeft: 16, marginRight: 15, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }} style={{ width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
variant="contained" variant="contained"
color="secondary" color="secondary"
onClick={() => getFiles(selectedCategory)} onClick={() => getFiles(selectedCategory)}
> >
<CachedIcon /> <CachedIcon />
</Button> </Button>
</ButtonGroup>
<ButtonGroup style={{marginLeft: 10, }}>
{/* <div style={{height: 35, width: 1, color: "#494949"}}></div> */} {/* <div style={{height: 35, width: 1, color: "#494949"}}></div> */}
{selectedCategory === "sigma" || selectedCategory === "yara" ?
<Tooltip title={"Open Detection Tab"} style={{}} aria-label={""}>
<a href={`/detections/${selectedCategory}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon
color="primary"
style={{
marginLeft: 10,
marginRight: 10,
top: 7,
position: 'relative',
}}
/>
</a>
</Tooltip>
: null}
{fileCategories !== undefined && {fileCategories !== undefined &&
fileCategories !== null && fileCategories !== null &&
fileCategories.length > 1 ? ( fileCategories.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150 }}> <FormControl style={{ minWidth: 175, maxWidth: 175, }}>
<InputLabel id="category-choice" style={{
color: "rgba(255, 255, 255, 0.65)",
}}>
Category
</InputLabel>
<Select <Select
labelId="input-namespace-select-label" labelId="category-choice"
id="input-namespace-select-id" id="input-namespace-select-id"
style={{ style={{
minWidth: 122, minWidth: 175,
maxWidth: 122, maxWidth: 175,
height: 35, height: 35,
float: "right", borderRadius: "5px 0px 0px 5px",
position: 'relative', overflow: "hidden",
top: 8
}} }}
value={selectedCategory} value={selectedCategory}
onChange={(event) => { onChange={(event) => {
@@ -1389,12 +1417,31 @@ const [filesLoaded, setFilesLoaded] = useState(false);
}} }}
> >
{fileCategories.map((data, index) => { {fileCategories.map((data, index) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
"name": fixedname,
})
return ( return (
<MenuItem <MenuItem
key={index} key={index}
value={data} value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
> >
{data.replaceAll("_", " ")} <Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem> </MenuItem>
); );
})} })}
@@ -1430,36 +1477,51 @@ const [filesLoaded, setFilesLoaded] = useState(false);
</FormControl> </FormControl>
) : null} ) : null}
<div style={{display: "inline-flex", position:"relative", top: 8}}>
{renderTextBox ? {/*<div style={{display: "inline-flex", position:"relative", top: 8}}>*/}
<Tooltip title={"Close"} style={{}} aria-label={""}> {renderTextBox ?
<Button <Tooltip title={"Close"} style={{}} aria-label={""}>
style={{ marginLeft: 5, marginRight: 15, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }} <Button
variant="contained" style={{
color="secondary" height: 35,
onClick={() => { borderRadius: 4,
setRenderTextBox(false); textTransform: 'none',
console.log(" close clicked") fontSize: 16,
borderRadius: "0px 5px 5px 0px",
marginRight: 10,
}} }}
> color="secondary"
<ClearIcon/> variant="contained"
</Button> onClick={() => {
</Tooltip> setRenderTextBox(false);
: console.log(" close clicked")
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{whiteSpace: 'nowrap', textWrap: 'nowrap', marginLeft: 5, marginRight: 15, width: 169, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }}
variant="contained"
color="secondary"
onClick={() => {
setRenderTextBox(true);
}} }}
> >
<AddIcon/> <ClearIcon/>
File Category </Button>
</Button> </Tooltip>
</Tooltip> :
} <Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{
whiteSpace: "nowrap",
width: fileCategories !== undefined && fileCategories !== null && fileCategories.length > 1 ? 50 : 169,
height: 35,
textTransform: 'none',
fontSize: 16,
}}
variant="outlined"
color="secondary"
onClick={() => {
setRenderTextBox(true);
}}
>
<AddIcon/>
</Button>
</Tooltip>
}
</ButtonGroup>
{renderTextBox && <TextField {renderTextBox && <TextField
onKeyPress={(event)=>{ onKeyPress={(event)=>{
@@ -1491,7 +1553,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
margin="dense" margin="dense"
defaultValue={""} defaultValue={""}
autoFocus autoFocus
/>}</div> />}
<ShuffleCodeEditor <ShuffleCodeEditor
isCloud={isCloud} isCloud={isCloud}
expansionModalOpen={openEditor} expansionModalOpen={openEditor}
@@ -1673,3 +1736,4 @@ const DownloadFileIcon = memo(({ setLoadFileModalOpen, isSelectedFiles }) => {
</Tooltip> </Tooltip>
); );
}); });
+4 -2
View File
@@ -751,6 +751,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
localStorage.setItem("getting_started_sidebar", "open"); localStorage.setItem("getting_started_sidebar", "open");
localStorage.removeItem("workflows"); localStorage.removeItem("workflows");
localStorage.removeItem("apps"); localStorage.removeItem("apps");
localStorage.removeItem("dashboard_onboarding_complete")
localStorage.removeItem("dashboard_onboarding_completed")
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
mode: "cors", mode: "cors",
@@ -955,7 +957,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
if (!fetched && org) { if (!fetched && org) {
setActiveOrgData(org); setActiveOrgData(org);
if (!isCloud) { if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) { if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true); setIsProdStatusOn(true);
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) { } else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true); setIsProdStatusOn(true);
@@ -1224,7 +1226,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}> <Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
<Button <Button
component={Link} component={Link}
to="/usecases" to={userdata?.support ? "/new-dashboard" : "/usecases"}
onClick={(event) => { onClick={(event) => {
setOpenautomateTab(true); setOpenautomateTab(true);
setOpenSecurityTab(false); setOpenSecurityTab(false);
+93 -91
View File
@@ -688,51 +688,51 @@ const AuthenticationOauth2 = (props) => {
const autoAuthButton = const autoAuthButton =
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
style={{ style={{
marginBottom: 20, marginBottom: 20,
marginTop: 20, marginTop: 20,
flex: 1, flex: 1,
textTransform: "none", textTransform: "none",
textAlign: "left", textAlign: "left",
justifyContent: "flex-start", justifyContent: "flex-start",
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
color: "#2f2f2f", color: "#2f2f2f",
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
minWidth: 300, minWidth: 275,
maxWidth: 300, maxWidth: 275,
maxHeight: 50, maxHeight: 50,
overflow: "hidden", overflow: "hidden",
border: `1px solid ${theme.palette.inputColor}`, border: `1px solid ${theme.palette.inputColor}`,
}} }}
color="primary" color="primary"
disabled={ disabled={
clientSecret.length > 0 || clientId.length > 0 clientSecret.length > 0 || clientId.length > 0
} }
fullWidth fullWidth
onClick={() => { onClick={() => {
// Hardcode some stuff? // Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID // This could prolly be added to the app itself with a "default" client ID
startOauth2Request() startOauth2Request()
}} }}
color="primary" color="primary"
> >
{buttonClicked ? ( {buttonClicked ? (
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} /> <CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
) : ( ) : (
<span style={{display: "flex"}}> <span style={{display: "flex"}}>
<img <img
alt={selectedAction.app_name} alt={selectedAction.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }} style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={selectedAction.large_image} src={selectedAction.large_image}
/> />
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5, color: "#2f2f2f",}} variant="body1"> <Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
One-click Login One-click Login
</Typography> </Typography>
</span> </span>
)} )}
</Button> </Button>
if (authButtonOnly === true && (authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) { if (authButtonOnly === true && (authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) {
return autoAuthButton return autoAuthButton
@@ -747,7 +747,8 @@ const AuthenticationOauth2 = (props) => {
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<span style={{}}> <span style={{}}>
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>&nbsp;-&nbsp;</span> Oauth2 requires a Client ID and Client Secret to authenticate, defined in your apps' remote website. <span>Your redirect URL: <br/><b>{window.location.origin}/set_authentication</b><br/>
</span>
<a <a
target="_blank" target="_blank"
rel="norefferer" rel="norefferer"
@@ -760,51 +761,52 @@ const AuthenticationOauth2 = (props) => {
<div /> <div />
</span> </span>
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ? {isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
<span> <span>
<span style={{display: "flex"}}> <span style={{display: "flex"}}>
{autoAuthButton} {autoAuthButton}
{buttonClicked ? {buttonClicked ?
null null
: :
<Tooltip <Tooltip
color="primary" color="primary"
title={"Force Admin Consent"} title={"Force Admin Consent"}
placement="top" placement="top"
> >
<Button <Button
fullWidth fullWidth
variant="outlined" variant="outlined"
style={{ style={{
maxWidth: 50, maxWidth: 40,
marginBottom: 20, marginBottom: 20,
marginTop: 20, marginTop: 20,
maxHeight: 50, maxHeight: 50,
}} marginLeft: 10,
color="primary" }}
disabled={ color="secondary"
clientSecret.length > 0 || clientId.length > 0 disabled={
} clientSecret.length > 0 || clientId.length > 0
fullWidth }
onClick={() => { fullWidth
// Hardcode some stuff? onClick={() => {
// This could prolly be added to the app itself with a "default" client ID // Hardcode some stuff?
//startOauth2Request(true) // This could prolly be added to the app itself with a "default" client ID
startOauth2Request() //startOauth2Request(true)
}} startOauth2Request()
color="primary" }}
> color="primary"
<SupervisorAccountIcon /> >
</Button> <SupervisorAccountIcon />
</Tooltip> </Button>
} </Tooltip>
</span> }
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}> </span>
OR <Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
</Typography> OR
</span> </Typography>
: null} </span>
: null}
{/*<TextField {/*<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}} style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
InputProps={{ InputProps={{
@@ -499,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
</div> </div>
{userdata?.support ? ( {userdata?.support ? (
<div style={{ alignItems: 'center' }}> <div style={{ alignItems: 'center' }}>
<div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}>Status</div> <div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily, marginTop: 2.5 }}>Status</div>
<FormControl style={{ width: 220, height: 35 }}> <FormControl style={{ width: 220, height: 35 }}>
<Select <Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}} style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
@@ -711,11 +711,12 @@ const OrgHeaderexpandedNew = (props) => {
/> />
</span> </span>
</Grid> </Grid>
{!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org || null || selectedOrganization?.creator_org?.length > 0 ? null :
<CloudSyncTab <CloudSyncTab
globalUrl={globalUrl} globalUrl={globalUrl}
userdata={userdata} userdata={userdata}
serverside={false} serverside={false}
/> />}
<Grid item xs={12} style={{ marginTop: 20, }}> <Grid item xs={12} style={{ marginTop: 20, }}>
<Typography variant="h5" style={{ textAlign: "left", fontWeight: 500, }}>Workflow Backup Repository</Typography> <Typography variant="h5" style={{ textAlign: "left", fontWeight: 500, }}>Workflow Backup Repository</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400 }}> <Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400 }}>
+8 -6
View File
@@ -437,7 +437,8 @@ const ParsedAction = (props) => {
]; ];
const getApp = (appId, setApp) => { const getApp = (appId, setApp) => {
fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", { const url = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false`;
fetch(url, {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
}, },
@@ -447,7 +448,7 @@ const ParsedAction = (props) => {
if (response.status === 200) { if (response.status === 200) {
//toast("Successfully GOT app "+appId) //toast("Successfully GOT app "+appId)
} else { } else {
toast("Failed getting app"); toast.error("Failed getting app. Please try again or contact support@shuffler.io");
} }
return response.json(); return response.json();
@@ -1711,6 +1712,7 @@ const ParsedAction = (props) => {
} }
const sortByCategoryLabel = (a, b) => { const sortByCategoryLabel = (a, b) => {
const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0 const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0
const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0 const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0
@@ -1739,11 +1741,12 @@ const ParsedAction = (props) => {
}) })
} }
// Gets the most important actions first // Gets the most important actions first
const renderedActionOptions = deduplicateByName(( const renderedActionOptions = deduplicateByName((
selectedApp.actions === undefined || selectedApp.actions === null ? [] : selectedApp.actions === undefined || selectedApp.actions === null ? [] :
selectedApp.actions.filter((a) => isIntegration ? selectedApp.actions :
a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label")) selectedApp.actions.filter((a) => a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
).sort(sortByCategoryLabel)) ).sort(sortByCategoryLabel))
@@ -2981,7 +2984,6 @@ const ParsedAction = (props) => {
dataLPIgnore="true" dataLPIgnore="true"
autoComplete="off" autoComplete="off"
id="checkbox-search" id="checkbox-search"
style={{ style={{
...theme.palette.textFieldStyle, ...theme.palette.textFieldStyle,
+7 -6
View File
@@ -223,7 +223,7 @@ const PartnerDetails = (props) => {
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}> <div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Name Company Name
</Typography> </Typography>
<Skeleton <Skeleton
variant="rounded" variant="rounded"
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
/> />
</div> */} </div> */}
<div style={{ alignItems: "center" }}> <div style={{ alignItems: "center" }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}> <div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily, marginTop: 2.5}}>
Solutions Solutions
</div> </div>
<Skeleton <Skeleton
@@ -399,7 +399,7 @@ const PartnerDetails = (props) => {
variant="text" variant="text"
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Name Company Name
</Typography> </Typography>
<TextField <TextField
required required
@@ -419,7 +419,7 @@ const PartnerDetails = (props) => {
cursor: isDisabled ? "not-allowed" : "pointer", cursor: isDisabled ? "not-allowed" : "pointer",
}} }}
fullWidth={true} fullWidth={true}
placeholder="Name" placeholder="Company Name"
type="name" type="name"
id="standard-required" id="standard-required"
margin="normal" margin="normal"
@@ -544,7 +544,8 @@ const PartnerDetails = (props) => {
style={{ style={{
marginRight: "12px", marginRight: "12px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontFamily: theme?.typography?.fontFamily fontFamily: theme?.typography?.fontFamily,
marginTop: 2.5,
}} }}
> >
Solutions Solutions
@@ -895,7 +896,7 @@ const PartnerDetails = (props) => {
cursor: isDisabled ? "not-allowed" : "pointer", cursor: isDisabled ? "not-allowed" : "pointer",
}} }}
fullWidth={true} fullWidth={true}
placeholder="support@shuffler.io" placeholder="example@company.com"
type="name" type="name"
id="standard-required" id="standard-required"
margin="normal" margin="normal"
+4 -4
View File
@@ -1362,10 +1362,10 @@ print('"' + encoded + '"')
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}> <div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
<div style={{ maxWidth: "calc(100% - 20px)" }}> <div style={{ maxWidth: "calc(100% - 20px)" }}>
<Typography variant="h5" style={{ fontSize: 24, fontWeight: 500, textAlign: "left" }}> <Typography variant="h5" style={{ fontSize: 24, fontWeight: 500, textAlign: "left" }}>
Notification Workflow Error Workflow
</Typography> </Typography>
<Typography color="textSecondary" style={{ fontSize: 16, fontWeight: 400, marginTop: 5, }}> <Typography color="textSecondary" style={{ fontSize: 16, fontWeight: 400, marginTop: 5, }}>
The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org notifications into the parent org notification by choosing it in the list.</b> The error workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org errors to a parent org's error workflow by choosing it in the list.</b>
</Typography> </Typography>
{modalView} {modalView}
@@ -1614,12 +1614,12 @@ print('"' + encoded + '"')
</div> </div>
} }
<Typography variant="h5" style={{ marginTop: 50, fontSize: 24, display: clickedFromOrgTab ? null : "inline", marginBottom: clickedFromOrgTab ? 8 : null, }}>Notifications ({ <Typography variant="h5" style={{ marginTop: 50, fontSize: 24, display: clickedFromOrgTab ? null : "inline", marginBottom: clickedFromOrgTab ? 8 : null, }}>Errors ({
notifications?.filter((notification) => showRead === true || notification.read === false).length notifications?.filter((notification) => showRead === true || notification.read === false).length
})</Typography> })</Typography>
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16, marginLeft: clickedFromOrgTab ? null : 25, color: clickedFromOrgTab ? "#9E9E9E" : null, }}> <Typography variant="body2" color="textSecondary" style={{ fontSize: 16, marginLeft: clickedFromOrgTab ? null : 25, color: clickedFromOrgTab ? "#9E9E9E" : null, }}>
Notifications help you find potential problems with your workflows and apps.&nbsp; Error help you find potential problems with your workflows and apps.&nbsp;
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -0,0 +1,223 @@
import React, { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import {
Button,
ButtonGroup,
CircularProgress,
Tooltip,
} from "@mui/material";
import {
Check as CheckIcon,
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material";
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
const RunDetectionTest = (props) => {
const {
globalUrl,
pipelines,
workflows,
ticketWebhook,
detectionWorkflowId,
changePipelineState,
submitPipelineWrapper,
} = props
const [executions, setExecutions] = React.useState([]);
const [detectionTestRunning, setDetectionTestRunning] = React.useState(false);
const [detectionTestExecutionId, setDetectionTestExecutionId] = React.useState("");
useEffect(() => {
if (detectionWorkflowId !== "") {
handleLoadExecutions(detectionWorkflowId)
}
}, [detectionWorkflowId])
if (workflows === undefined || workflows === null || workflows.length === 0) {
return null
}
const handleLoadExecutions = (workflowId, detectionTestRunning) => {
const url = `${globalUrl}/api/v2/workflows/${workflowId}/executions`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all executions");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false && responseJson?.executions?.length > 0) {
if (detectionTestRunning === true) {
console.log("Checking executions in workflow: ", workflowId, responseJson.executions)
for (var executionKey in responseJson.executions) {
const curExec = responseJson.executions[executionKey]
if (curExec.execution_id === detectionTestExecutionId) {
continue
}
// started_at = unix timestamp
// check within the last 60 seconds
const datecomparison = (Date.now() / 1000) - 60
if (curExec.started_at >= datecomparison) {
if (curExec?.execution_argument?.includes("rule") && curExec?.execution_argument?.includes("Test Notepad Event")) {
setDetectionTestRunning(false)
setDetectionTestExecutionId(curExec.execution_id)
}
break;
}
}
} else {
setExecutions(responseJson.executions || [])
}
}
})
.catch((error) => {
toast(error.toString());
})
}
const runDetectionTest = () => {
setDetectionTestRunning(true)
if (ticketWebhook === "") {
setDetectionTestRunning(false)
toast.error("No ticketing webhook found. Please enable the ticketing workflow first.")
return
}
if (detectionWorkflowId === "") {
setDetectionTestRunning(false)
toast.error("No ticketing workflow found. Please enable the ticketing workflow first.")
return
}
if (haveDetectionPipelines() === false) {
setDetectionTestRunning(false)
toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.")
return
}
// 1. Run a new pipeline which exits.
const detectionTest = `from {message: "<165>1 2025-10-06T12:34:56.789Z myhost.example.com myapp 1234 ID47 [huh eventSource=\\\"App\\\" EventID=\\\"4688\\\" NewProcessName=\\\"notepad.exe\\\" Context=\\\"Testing\\\"] This is a test log message"} | this = message.parse_syslog() | import`
for (var pipelineKey in pipelines) {
const curPipeline = pipelines[pipelineKey]
if (curPipeline.definition === detectionTest && changePipelineState !== undefined) {
changePipelineState(curPipeline, "stop");
}
}
// 1. Submit it to run
// 2. Check executions if they happened recently~
if (submitPipelineWrapper !== undefined) {
submitPipelineWrapper(detectionTest)
}
for (var i = 0; i < 10; i++) {
setTimeout(() => {
handleLoadExecutions(detectionWorkflowId, true)
}, i * 5000)
}
setTimeout(() => {
setDetectionTestRunning(false)
}, 60000)
}
const haveDetectionPipelines = () => {
if (pipelines === undefined) {
toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.")
return false
}
var foundCorrect = 0
for (var pipelineKey in pipelines) {
const curPipeline = pipelines[pipelineKey]
//if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) {
// foundCorrect += 1
//}
if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) {
foundCorrect += 1
}
}
if (foundCorrect >= 1) {
return true
}
return false
}
return (
<div style={{display: "flex", }}>
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
<Tooltip title={"Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline"} style={{}} aria-label={"Run detection test"}>
<div>
<Button
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
variant="outlined"
color="secondary"
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
onClick={() => {
//setPipelineModalOpen(true)
runDetectionTest()
}}
>
{
detectionTestRunning === true ? <CircularProgress size={20} style={{marginRight: 10, }} /> : "Run Detection Test"
}
</Button>
</div>
</Tooltip>
{detectionTestRunning === false && detectionTestExecutionId !== "" && detectionTestExecutionId !== undefined ?
<Tooltip title={`Go to detection test: ${detectionTestExecutionId}`} style={{}} aria-label={"Go to logs"}>
<a href={`/workflows/${detectionWorkflowId}?execution_id=${detectionTestExecutionId}`} target="_blank" rel="noopener noreferrer">
<Button
color="secondary"
style={{
minWidth: 75, maxWidth: 75,
minHeight: 40, maxHeight: 40,
}}
>
<CheckIcon style={{color: green}}/>
</Button>
</a>
</Tooltip>
: null}
</ButtonGroup>
{window?.location?.href?.includes("/detections/") === true ? null :
<Tooltip title={"Open Detection Tab"} style={{}} aria-label={""}>
<a href={`/detections/sigma`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon
color="secondary"
style={{
marginLeft: 20,
top: 8,
position: 'relative',
}}
/>
</a>
</Tooltip>
}
</div>
)
}
export default RunDetectionTest
+318 -189
View File
@@ -7,6 +7,7 @@ import {
ListItem, ListItem,
ListItemText, ListItemText,
Button, Button,
ButtonGroup,
Tooltip, Tooltip,
IconButton, IconButton,
Dialog, Dialog,
@@ -14,15 +15,21 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
TextField, TextField,
Chip,
CircularProgress,
} from '@mui/material'; } from '@mui/material';
import { import {
FileCopy as FileCopyIcon, FileCopy as FileCopyIcon,
OpenInNew as OpenInNewIcon, OpenInNew as OpenInNewIcon,
Padding, Refresh as RefreshIcon,
Delete as DeleteIcon,
Check as CheckIcon,
} from "@mui/icons-material" } from "@mui/icons-material"
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
import { Box, Skeleton, Typography } from '@mui/material'; import { Box, Skeleton, Typography } from '@mui/material';
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
import RunDetectionTest from '../components/RunDetectionTest.jsx';
const SchedulesTab = memo((props) => { const SchedulesTab = memo((props) => {
const {globalUrl, users, } = props; const {globalUrl, users, } = props;
@@ -30,13 +37,58 @@ const SchedulesTab = memo((props) => {
const [allSchedules, setAllSchedules] = React.useState([]); const [allSchedules, setAllSchedules] = React.useState([]);
const [pipelines, setPipelines] = React.useState([]); const [pipelines, setPipelines] = React.useState([]);
const [showLoader, setShowLoader] = React.useState(true); const [showLoader, setShowLoader] = React.useState(true);
const [workflows, setWorkflows] = React.useState([]);
const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false); const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
const [newPipelineValue, setNewPipelineValue] = React.useState("export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"); const [newPipelineValue, setNewPipelineValue] = React.useState(`export | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
const [ticketWebhook, setTicketWebhook] = React.useState("");
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const { themeMode, brandColor } = useContext(Context); const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
const handleGetWorkflows = () => {
const url = `${globalUrl}/api/v1/workflows`;
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all workflows");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
setWorkflows(responseJson || []);
for (var i = 0; i < responseJson?.length; i++) {
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
for (var triggerkey in responseJson[i].triggers) {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
}
}
}
}
}
})
.catch((error) => {
toast(error.toString());
})
}
useEffect(() => { useEffect(() => {
handleGetWorkflows()
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) { if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
handleGetAllTriggers() handleGetAllTriggers()
} }
@@ -58,8 +110,11 @@ const SchedulesTab = memo((props) => {
environment: pipeline.environment, environment: pipeline.environment,
}; };
if (state === "start") toast("starting the pipeline"); if (state === "start") {
else toast.info("Stopping the pipeline. This may take a few minutes to propagate.") toast("starting the pipeline")
} else {
toast.info("Stopping a pipeline. This may take a few minutes to propagate.")
}
const url = `${globalUrl}/api/v1/triggers/pipeline`; const url = `${globalUrl}/api/v1/triggers/pipeline`;
fetch(url, { fetch(url, {
@@ -144,16 +199,65 @@ const SchedulesTab = memo((props) => {
}, },
}} }}
> >
<DialogTitle> <DialogTitle style={{padding: "50px 50px 25px 50px", }}>
<Typography variant='h5' color="textPrimary" > <Typography variant='h5' color="textPrimary" >
Run a Tenzir pipeline Run a Tenzir pipeline
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}> <Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
Alpha feature. Deploys to the first available Orborus location. <a href="https://docs.tenzir.com/pipelines" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main }}>Explore Tenzir Pipelines</a>. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook. Alpha feature. Deploys to the first available Orborus location. <a href="https://docs.tenzir.com/explanations/architecture/pipeline/" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main }}>Explore Tenzir Pipelines</a>. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook.
</Typography> </Typography>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent style={{padding: "0px 50px 50px 50px", }}>
<div> <div style={{marginTop: 10, }}>
<Chip
onClick={() => {
setNewPipelineValue(`load_tcp "0.0.0.0:1514" { read_syslog } | import`)
}}
label={"Syslog Listener (TCP)"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<Chip
onClick={() => {
setNewPipelineValue(`load_udp "0.0.0.0:1514", insert_newlines=true | read_syslog | import`)
}}
label={"Syslog Listener (UDP)"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<Chip
onClick={() => {
setNewPipelineValue(`export live=true | sigma "/tmp/sigma_rules" | to "${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}"`)
}}
label={"Sigma Rules"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<Chip
onClick={() => {
setNewPipelineValue(`export live=true | to_opensearch "localhost:9200", action="create", index="shuffle_logs", user="admin", passwd="PASSWORD"`)
}}
label={"Opensearch Ingest"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<TextField <TextField
color="primary" color="primary"
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}} style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
@@ -163,8 +267,9 @@ const SchedulesTab = memo((props) => {
minRows={4} minRows={4}
required required
fullWidth={true} fullWidth={true}
defaultValue="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK" defaultValue={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
placeholder="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK" value={newPipelineValue}
placeholder={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
id="environment_name" id="environment_name"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
@@ -174,7 +279,7 @@ const SchedulesTab = memo((props) => {
/> />
</div> </div>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions style={{padding: "0px 50px 50px 50px", }}>
<Button <Button
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }} style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
onClick={() => { onClick={() => {
@@ -191,7 +296,7 @@ const SchedulesTab = memo((props) => {
}} }}
color="primary" color="primary"
> >
Submit Create Pipeline
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -232,18 +337,18 @@ const SchedulesTab = memo((props) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (!responseJson.success && pipelineConfig.type !== "delete") { if (!responseJson.success && pipelineConfig.type !== "delete") {
toast("Failed to set pipeline: " + responseJson.reason); toast.error("Failed to set pipeline: " + responseJson.reason);
} else { } else {
if (pipelineConfig.type === "create") { if (pipelineConfig.type === "create") {
toast("Pipeline will be created: " + responseJson.reason) toast.success("Pipeline will be created. Page will autorefresh in a bit: " + responseJson.reason)
setPipelineModalOpen(false) setPipelineModalOpen(false)
} else if (pipelineConfig.type === "stop") { } else if (pipelineConfig.type === "stop") {
toast("Pipeline will be stopped: " + responseJson.reason) toast.success("Pipeline will be stopped: " + responseJson.reason)
setPipelineModalOpen(false) setPipelineModalOpen(false)
} else { } else {
toast("Unknown pipeline type: " + pipelineConfig.type) toast.info("Unknown pipeline type: " + pipelineConfig.type)
} }
} }
@@ -274,12 +379,7 @@ const SchedulesTab = memo((props) => {
// Just use this one? // Just use this one?
const url = const url = `${globalUrl}/api/v1/workflows/${data?.workflow_id}/schedule/${data.id}`;
globalUrl +
"/api/v1/workflows/" +
data["workflow_id"] +
"/schedule/" +
data.id;
fetch(url, { fetch(url, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
@@ -414,7 +514,7 @@ const SchedulesTab = memo((props) => {
//toast(error.toString()); //toast(error.toString());
console.log("Get schedule error: ", error.toString()); console.log("Get schedule error: ", error.toString());
}); });
}; }
const startWebHook = (trigger) => { const startWebHook = (trigger) => {
const hookname = trigger.info.name; const hookname = trigger.info.name;
@@ -490,8 +590,197 @@ const SchedulesTab = memo((props) => {
Triggers are Automatic Workflow starters. <b>Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length})</b> Triggers are Automatic Workflow starters. <b>Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length})</b>
</Typography> </Typography>
<div style={{ marginTop: 50, marginBottom: 20 }}>
<Typography variant='h6' color="textPrimary" >Pipelines</Typography>
<Typography variant='body2' color="textSecondary" >
Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#pipelines"
style={{ color: theme.palette.primary.main }}
>
Learn more
</a>
</Typography>
<div style={{marginBottom: 10, marginTop: 10, }}/>
<Button
style={{}}
variant="contained"
color="primary"
onClick={() => setPipelineModalOpen(true)}
>
Deploy New Pipeline
</Button>
<Button
style={{marginLeft: 10, }}
variant="outlined"
color="primary"
onClick={() => {
handleGetAllTriggers()
}}
>
<RefreshIcon style={{}}/>
</Button>
</div>
<div
style={{
borderRadius: 4,
marginTop: 24,
border: theme.palette.defaultBorder,
width: "100%",
overflowX: pipelines?.length === 0 ? "hidden" : "auto",
paddingBottom: 0,
}}
>
<List
style={{
borderRadius: 4,
width: '100%',
tableLayout: "auto",
display: "table",
minWidth: pipelines?.length === 0 ? "auto" : 800,
overflowX: "auto",
paddingBottom: 0
}}>
<ListItem style={{width:"100%", borderBottom:theme.palette.defaultBorder, display: "table-row"}}>
{["Status", "Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
<ListItemText
key={index}
primary={header}
style={{
display: "table-cell",
padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: theme.palette.defaultBorder,
position: "sticky",
}}
/>
))}
</ListItem>
{showLoader ? (
[...Array(6)].map((_, rowIndex) => {
return (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: theme.palette.platformColor,
}}
>
{Array(5)
.fill()
.map((_, colIndex) => {
return (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: theme.palette.loaderColor,
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
)
})}
</ListItem>
)
}
)
) : (
pipelines?.length === 0 ? (
<div style={{width: "100%", textAlign: "center", }}>
<Typography style={{color: theme.palette.text.primary, padding: 20,width: "100%", fontSize: 16, textAlign: 'center'}}>No pipelines found.</Typography>
</div>
):(
pipelines.map((pipeline, index) => {
var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF";
if (index % 2 === 0) {
bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor, borderRadius: index === pipelines.length - 1 ? 8 : 0, display: 'table-row', }} >
<ListItemText
style={{ minWidth: 75, maxWidth: 75, overflow: "auto", display:'table-cell', padding: "8px 8px 8px 15px" }}
primary={pipeline.state}
/>
<ListItemText
style={{ maxWidth: 350, overflow: "auto", display:'table-cell', padding: "8px 8px 8px 15px" }}
primary={pipeline.definition}
/>
<ListItemText
style={{ display:'table-cell', padding: 8 }}
primary={pipeline.environment}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={pipeline.total_runs}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={(
<Box display="table-cell">
<Tooltip title={"Copy deletion command"} style={{}} aria-label={"Go to logs"}>
<IconButton style={{marginRight: 10, }} onClick={() => {
const copyContent = `curl -XPOST http://localhost:5160/api/v0/pipeline/delete -H "Content-Type: application/json" -d '{"id":"${pipeline.id}"}' -v`
const copyText = navigator?.clipboard?.writeText(copyContent)
if (copyText) {
toast.success("Pipeline copied to clipboard")
} else {
toast.error("Failed to copy pipeline")
}
}}>
<FileCopyIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Delete Pipeline"} style={{}} aria-label={"Go to logs"}>
<IconButton style={{marginRight: 10, }} onClick={() => {
changePipelineState(pipeline, "stop");
}}>
<DeleteIcon style={{color: red, }} />
</IconButton>
</Tooltip>
</Box>
)}
/>
</ListItem>
);
})
)
)}
</List>
<div style={{margin: 25, }}>
<RunDetectionTest
globalUrl={globalUrl}
pipelines={pipelines}
workflows={workflows}
ticketWebhook={ticketWebhook}
detectionWorkflowId={detectionWorkflowId}
changePipelineState={changePipelineState}
submitPipelineWrapper={submitPipelineWrapper}
/>
</div>
</div>
<div> <div>
<Typography variant='h6' color="textPrimary" style={{ marginBottom: 8, marginTop: 0, fontWeight: 500}}> <Typography variant='h6' color="textPrimary" style={{ marginBottom: 8, marginTop: 50, fontWeight: 500}}>
Schedules Schedules
</Typography> </Typography>
<Typography variant='body2' color="textSecondary"> <Typography variant='body2' color="textSecondary">
@@ -903,11 +1192,9 @@ const SchedulesTab = memo((props) => {
style={{ style={{
textTransform: 'none', textTransform: 'none',
fontSize: 16, fontSize: 16,
color:webhook.status === "running" ? '#1a1a1a' : null,
backgroundColor: webhook.status === "running" ? '#ff8544' : null,
width: 150, width: 150,
}} }}
color={webhook.status === "running" ? "secondary" : "primary"} color={"secondary"}
variant={webhook.status === "running" ? "contained" : "outlined"} variant={webhook.status === "running" ? "contained" : "outlined"}
disabled={webhook.status === "uninitialized"} disabled={webhook.status === "uninitialized"}
onClick={() => { onClick={() => {
@@ -929,166 +1216,7 @@ const SchedulesTab = memo((props) => {
)} )}
</List> </List>
</div> </div>
<div style={{ marginTop: 50, marginBottom: 20 }}>
<Typography variant='h6' color="textPrimary" >Pipelines</Typography>
<Typography variant='body2' color="textSecondary" >
Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#pipelines"
style={{ color: theme.palette.primary.main }}
>
Learn more
</a>
</Typography>
<div style={{marginBottom: 10, marginTop: 10, }}/>
<Button
style={{ borderRadius: 4, textTransform: "capitalize", fontSize: 16, }}
variant="contained"
color="primary"
onClick={() => setPipelineModalOpen(true)}
>
Deploy New Pipeline
</Button>
</div>
<div
style={{
borderRadius: 4,
marginTop: 24,
border: theme.palette.defaultBorder,
width: "100%",
overflowX: pipelines?.length === 0 ? "hidden" : "auto",
paddingBottom: 0,
}}
>
<List
style={{
borderRadius: 4,
width: '100%',
tableLayout: "auto",
display: "table",
minWidth: pipelines?.length === 0 ? "auto" : 800,
overflowX: "auto",
paddingBottom: 0
}}>
<ListItem style={{width:"100%", borderBottom:theme.palette.defaultBorder, display: "table-row"}}>
{["Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
<ListItemText
key={index}
primary={header}
style={{
display: "table-cell",
padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: theme.palette.defaultBorder,
position: "sticky",
}}
/>
))}
</ListItem>
{showLoader ? (
[...Array(6)].map((_, rowIndex) => {
return (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: theme.palette.platformColor,
}}
>
{Array(5)
.fill()
.map((_, colIndex) => {
return (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: theme.palette.loaderColor,
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
)
})}
</ListItem>
)
}
)
): (
pipelines?.length === 0 ? (
<div style={{width: "100%", textAlign: "center", }}>
<Typography style={{color: theme.palette.text.primary, padding: 20,width: "100%", fontSize: 16, textAlign: 'center'}}>No pipeline trigger found</Typography>
</div>
):(
pipelines.map((pipeline, index) => {
var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF";
if (index % 2 === 0) {
bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor, borderRadius: index === pipelines.length - 1 ? 8 : 0, display: 'table-row', }} >
<ListItemText
style={{ display:'table-cell', padding: "8px 8px 8px 15px" }}
primary={pipeline.definition}
/>
<ListItemText
style={{ display:'table-cell', padding: 8 }}
primary={pipeline.environment}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={pipeline.total_runs}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={(
<Box display="table-cell">
<Button
style={{
textTransform: 'none',
fontSize: 16,
}}
variant={"outlined"}
disabled={pipeline.status === "uninitialized"}
onClick={() => {
changePipelineState(pipeline, "stop");
/*
if (pipeline.status === "running") {
changePipelineState(pipeline, "stop");
} else changePipelineState(pipeline, "start");
*/
}}
>
Stop Pipeline
</Button>
</Box>
)}
/>
</ListItem>
);
})
)
)}
</List>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1096,3 +1224,4 @@ const SchedulesTab = memo((props) => {
}); });
export default SchedulesTab; export default SchedulesTab;
+34 -1
View File
@@ -147,6 +147,8 @@ const CodeEditor = (props) => {
// Auto-indent JSON-like content (with safety hehe) // Auto-indent JSON-like content (with safety hehe)
const autoIndentContent = React.useCallback((content) => { const autoIndentContent = React.useCallback((content) => {
return content
// Safety checks :) // Safety checks :)
if (!content || typeof content !== 'string' || content.trim().length === 0) { if (!content || typeof content !== 'string' || content.trim().length === 0) {
return content; return content;
@@ -173,6 +175,7 @@ const CodeEditor = (props) => {
} }
}, []); }, []);
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata); const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
// const {codelang, setcodelang} = props // const {codelang, setcodelang} = props
@@ -1832,6 +1835,7 @@ const CodeEditor = (props) => {
display: 'flex', display: 'flex',
}} }}
> >
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<DialogTitle <DialogTitle
style={{ style={{
@@ -1842,6 +1846,35 @@ const CodeEditor = (props) => {
File Editor ({localcodedata.length}) File Editor ({localcodedata.length})
</DialogTitle> </DialogTitle>
</div> </div>
<IconButton
style={{
position: "absolute",
height: 50,
width: 50,
right: 25,
top: 90,
zIndex: 5000,
}}
disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
onClick={() => {
const indentedText = IndentJsonLikeString(localcodedata, 2)
if (indentedText !== undefined && indentedText !== null) {
setlocalcodedata(indentedText)
} else {
toast.warn("Could not indent the text. Please check the input format.", { autoClose: 5000 })
}
}}
color="secondary"
>
<Tooltip
title={"Indent Text"}
placement="top"
>
<FormatIndentIncreaseIcon />
</Tooltip>
</IconButton>
</div> </div>
: :
<div <div
@@ -2270,7 +2303,7 @@ const CodeEditor = (props) => {
width: 50, width: 50,
marginLeft: 100, marginLeft: 100,
}} }}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0} disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
onClick={() => { onClick={() => {
const indentedText = IndentJsonLikeString(localcodedata, 2) const indentedText = IndentJsonLikeString(localcodedata, 2)
if (indentedText !== undefined && indentedText !== null) { if (indentedText !== undefined && indentedText !== null) {
+12 -2
View File
@@ -20,6 +20,7 @@ import {
Zoom, Zoom,
Chip, Chip,
} from '@mui/material'; } from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import WorkflowPaper from "../components/WorkflowPaper.jsx" import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx" import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
@@ -172,6 +173,7 @@ const AppGrid = props => {
// value={currentRefinement} // value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = "" var defaultSearch = ""
const [inputValue, setInputValue] = useState("")
useEffect(() => { useEffect(() => {
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) { if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search) const urlSearchParams = new URLSearchParams(window.location.search)
@@ -185,6 +187,12 @@ const AppGrid = props => {
} }
}, []) }, [])
useEffect(() => {
setInputValue(currentRefinement || defaultSearch || "")
}, [currentRefinement])
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) { if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) {
//setLocalMessage(inputsearch) //setLocalMessage(inputsearch)
refine(inputsearch) refine(inputsearch)
@@ -217,12 +225,14 @@ const AppGrid = props => {
autoComplete='off' autoComplete='off'
type="search" type="search"
color="primary" color="primary"
value={currentRefinement} value={inputValue}
placeholder="Find Workflows..." placeholder="Find Workflows..."
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
removeQuery("q") removeQuery("q")
refine(event.currentTarget.value) const value = event.currentTarget.value
setInputValue(value)
debouncedRefine(value)
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
+557 -78
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useContext, memo } from "react"; import React, { useState, useEffect, useContext, memo } from "react";
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
import AuthenticationModal from "../components/AuthenticationModal.jsx";
import { useNavigate, Link, useLocation } from "react-router-dom"; import { useNavigate, Link, useLocation } from "react-router-dom";
import { getTheme } from "../theme.jsx"; import { getTheme } from "../theme.jsx";
import { toast } from "react-toastify" import { toast } from "react-toastify"
@@ -21,12 +22,16 @@ import {
import { import {
CheckCircle as CheckCircleIcon, CheckCircle as CheckCircleIcon,
Check as CheckIcon,
HourglassDisabled as HourglassDisabledIcon, HourglassDisabled as HourglassDisabledIcon,
RestartAlt as RestartAltIcon, RestartAlt as RestartAltIcon,
ExpandMore as ExpandMoreIcon, ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon, ExpandLess as ExpandLessIcon,
Send as SendIcon, Send as SendIcon,
Error as ErrorIcon, Error as ErrorIcon,
Close as CloseIcon,
OpenInNew as OpenInNewIcon,
Refresh as RefreshIcon,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
@@ -43,21 +48,26 @@ const AgentUI = (props) => {
const [data, setData] = useState({}) const [data, setData] = useState({})
const [openIndexes, setOpenIndexes] = useState([]) const [openIndexes, setOpenIndexes] = useState([])
const [disableButtons, setDisableButtons] = useState(false) const [disableButtons, setDisableButtons] = useState(false)
const [apps, setApps] = useState([])
const [appAuth, setAppAuth] = useState([])
const [originalStartTime, setOriginalStartTime] = useState(0)
const [latestEndTime, setLatestEndTime] = useState(0)
const [showAgentStarter, setShowAgentStarter] = useState(false) const [showAgentStarter, setShowAgentStarter] = useState(false)
const [actionInput, setActionInput] = useState("") const [actionInput, setActionInput] = useState("")
const [questionAnswers, setQuestionAnswers] = useState({})
const {themeMode} = useContext(Context) const {themeMode} = useContext(Context)
const theme = getTheme(themeMode) const theme = getTheme(themeMode)
const navigate = useNavigate(); const navigate = useNavigate();
document.title = "Shuffle AI Agents"
const agentWrapperStyle = { const agentWrapperStyle = {
width: 1000, width: 1000,
height: 1000, height: 1000,
margin: "auto", margin: "auto",
paddingTop: 100, paddingTop: 100,
paddingBottom: 1000,
backgroundColor: theme.palette.backgroundColor,
} }
if (data.input === undefined || data.input === null) { if (data.input === undefined || data.input === null) {
@@ -75,7 +85,22 @@ const AgentUI = (props) => {
} }
if (node_id === undefined || node_id === null || node_id === "") { if (node_id === undefined || node_id === null || node_id === "") {
return // Look for AI agent
/*
for (var key in execution_data.results) {
const item = execution_data.results[key]
if (item?.action?.app_name !== "AI Agent") {
continue
}
node_id = item?.action?.id
break
}
*/
if (node_id === undefined || node_id === null || node_id === "") {
return
}
} }
var found = false var found = false
@@ -150,15 +175,22 @@ const AgentUI = (props) => {
if (responseJson.success !== false) { if (responseJson.success !== false) {
if (responseJson.status === "EXECUTING") { if (responseJson.status === "EXECUTING") {
// Recursively looking for updates until it's not executing anymore // Recursively looking for updates until it's not executing anymore
setTimeout(() => { //setTimeout(() => {
GetExecution(execution_id, node_id, authorization) // GetExecution(execution_id, node_id, authorization)
}, 3000) //}, 3000)
} else { } else {
setDisableButtons(false) setDisableButtons(false)
setDisableButtons(false)
} }
setExecution(responseJson) try {
if (JSON.stringify(responseJson) !== JSON.stringify(execution)) {
setExecution(responseJson)
}
} catch(e) {
console.log("Error comparing executions: ", e)
setExecution(responseJson)
}
findNodeData(responseJson, node_id) findNodeData(responseJson, node_id)
} else { } else {
setDisableButtons(false) setDisableButtons(false)
@@ -216,12 +248,53 @@ const AgentUI = (props) => {
} }
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization) GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
setTimeout(() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}, 10000)
}) })
.catch((error) => { .catch((error) => {
toast.error("Error: " + error) toast.error("Error: " + error)
}) })
} }
const getAppAuth = () => {
const url = `${globalUrl}/api/v1/apps/authentication`
fetch(url, {
method: "GET",
credentials: "include",
})
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson.success !== false) {
setAppAuth(responseJson)
}
})
.catch((error) => {
toast.error("Error in auth load: " + error)
})
}
const getApps = () => {
const url = `${globalUrl}/api/v1/apps`
fetch(url, {
method: "GET",
credentials: "include",
})
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson.success !== false) {
setApps(responseJson)
}
})
.catch((error) => {
toast.error("Error in app load: " + error)
})
}
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
const executionId = params.get("execution_id") const executionId = params.get("execution_id")
@@ -233,9 +306,15 @@ const AgentUI = (props) => {
setShowAgentStarter(true) setShowAgentStarter(true)
//toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.") //toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
} }
getApps()
getAppAuth()
}, []) }, [])
const maxTimelineWidth = 150 const maxTimelineWidth = 300
var latestEndTime = 0
var originalStartTime = 0
const TimelineItem = (props) => { const TimelineItem = (props) => {
const { item, index } = props; const { item, index } = props;
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@@ -258,12 +337,96 @@ const AgentUI = (props) => {
</Tooltip> </Tooltip>
const categoryStyle = { const categoryStyle = {
width: 20, width: 25,
height: 20, height: 25,
marginRight: 10, marginRight: 10,
borderRadius: 5,
} }
const parsedCategory = item.category === "singul" ?
const validate = validateJson(item.details)
const itemStartTime = item.start_time
var itemEndTime = item.end_time
if (item.category === "agent" && itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
console.log("Rerender 1: ", itemStartTime, originalStartTime)
originalStartTime = itemStartTime
}
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
console.log("Rerender 2")
latestEndTime = itemEndTime
}
if (itemEndTime === undefined || itemEndTime === null) {
// Set it to now
itemEndTime = latestEndTime
}
if (item.category == "agent" && itemEndTime === 0) {
// Right now -> .toLocaleString() support
itemEndTime = Date.now() / 1000
//<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
if (itemEndTime > latestEndTime) {
latestEndTime = itemEndTime
}
}
const totalDuration = latestEndTime - originalStartTime
var currentDuration = itemStartTime - itemEndTime
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
//var timelineMarginLeft = 0
// Calculate how long the div should be
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
//console.log("CURRENT DURATION (1): ", currentDuration, itemStartTime, itemEndTime, originalStartTime, latestEndTime, totalDuration, timelineMarginLeft, timelineWidth)
if (totalDuration === currentDuration) {
timelineMarginLeft = 0
timelineWidth = maxTimelineWidth
}
// Just for simplicity's sake
if (currentDuration < -1000000 || currentDuration > 1000000) {
currentDuration = 0
}
if (currentDuration < 0) {
currentDuration = currentDuration * -1
}
const defaultTopPadding = 10
const open = openIndexes.includes(index)
var questions = []
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
item.type = "finalise"
item.category = "finalise"
item.label = item?.details?.reason || item.label
} else if (item?.category === "ask" || item?.details?.action === "ask") {
item.type = "question"
item.category = "ask"
item.label = item?.details?.reason || item.label
for (var fieldKey in item?.details?.fields) {
const field = item?.details?.fields[fieldKey]
if (field?.key !== "question") {
continue
}
questions.push({
"question": field?.value,
"index": questions.length + 1,
})
}
} else if (item?.details?.action === "api" && item?.details?.tool?.length > 0) {
item.label = item?.details?.reason || item.label
}
var parsedCategory = item.category === "singul" ?
<Tooltip title="Singul" placement="top"> <Tooltip title="Singul" placement="top">
<img src="/images/logos/singul.svg" style={categoryStyle} /> <img src="/images/logos/singul.svg" style={categoryStyle} />
</Tooltip> </Tooltip>
@@ -273,38 +436,167 @@ const AgentUI = (props) => {
</Tooltip> </Tooltip>
: :
<div style={categoryStyle} /> <div style={categoryStyle} />
const validate = validateJson(item.details) var showAuthentication = false
const itemStartTime = item.start_time var selectedApp = {}
var itemEndTime = item.end_time if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) {
if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
console.log("Rerender 1") // Find the app and inject the image
//setOriginalStartTime(itemStartTime) const toolName = item.details.tool.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
for (var appKey in apps) {
const app = apps[appKey]
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
if (appname !== toolName) {
continue
}
if (app.large_image === undefined || app.large_image === null || app.large_image.length === 0) {
break
}
selectedApp = app
// Override the category
//item.category = app.name
//item.label = item?.details?.reason || item.label
parsedCategory =
<Tooltip title={app.name} placement="top">
<img src={app.large_image} style={categoryStyle} />
</Tooltip>
break
}
} }
if (itemEndTime !== undefined && itemEndTime > latestEndTime) { if (!showAuthentication) {
console.log("Rerender 2") if (item?.details?.run_details?.raw_response !== undefined && item?.details?.run_details?.raw_response !== null && item?.details?.run_details?.raw_response?.includes("app_authentication")) {
setLatestEndTime(itemEndTime) showAuthentication = true
}
} }
if (itemEndTime === undefined || itemEndTime === null) { var questionSubmitDisabled = questions.length === 0 ? true : false
// Set it to now for (var qKey in questions) {
itemEndTime = latestEndTime const q = questions[qKey]
if (questionAnswers[q.question] === undefined || questionAnswers[q.question] === null || questionAnswers[q.question] === "") {
//console.log("EMPTY QUESTION: ", q)
questionSubmitDisabled = true
break
} else {
questionSubmitDisabled = false
}
} }
const totalDuration = latestEndTime - originalStartTime const barColor = item.status === "FINISHED" ? green :
const currentDuration = itemStartTime - itemEndTime item.status === "FAILURE" || item.status == "ABORTED" ? red :
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth item.status === "RUNNING" || item.status === "" ? theme.palette.main :
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth theme.palette.surfaceColor
if (totalDuration === currentDuration) { const rerunAgentButton =
timelineMarginLeft = 0 <Tooltip title="Rerun from the start with the same input" placement="right">
timelineWidth = maxTimelineWidth <span>
<IconButton
style={{marginLeft: 20, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toast.info("Attempting to rerun everything.")
setDisableButtons(true)
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
toast.error("No decision details found to rerun. Cannot proceed. Please go back to your workflow or /agents to start over.")
} else {
//console.log("DETAILS: ", item?.details)
for (var messagekey in item?.details?.input?.messages) {
const message = item?.details?.input?.messages[messagekey]
if (message.role === "user") {
setActionInput(message.content)
setDisableButtons(true)
submitInput(message.content)
//toast.info("Rerun started. Please wait a few seconds and this page should refresh automatically.")
break
}
}
}
}}
>
<RestartAltIcon />
</IconButton>
</span>
</Tooltip>
const rerunButton =
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="right">
<span>
<IconButton
disabled={item.type !== "decision" || disableButtons}
style={{marginLeft: 20, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
//toast.info("Attempting to rerun this decision by itself.")
setDisableButtons(true)
RerunDecision(item.details)
}}
>
<RestartAltIcon />
</IconButton>
</span>
</Tooltip>
const submitQuestions = (decisionId, questionAnswers) => {
console.log("Submitting questions: ", decisionId, questionAnswers)
if (decisionId === undefined || decisionId === null || decisionId === "") {
toast.error("No decision ID provided. Cannot submit answers.")
return
}
if (Object.keys(questionAnswers).length === 0) {
toast.error("No answers provided. Cannot submit empty answers.")
return
}
// Loop qu
var newArgument = {}
for (var key in questionAnswers) {
const answer = questionAnswers[key]
newArgument["question_"+(answer.index)] = answer.value
}
const params = new URLSearchParams(window.location.search)
const executionId = params.get("execution_id")
const nodeId = params.get("node_id")
const authorization = params.get("authorization")
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
console.log("PARSED URL: ", url)
fetch(url, {
method: "GET",
credentials: "include",
})
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson.success !== false) {
setTimeout(() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}, 500)
toast.success("Successfully submitted answers! The agent should continue shortly.")
} else {
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
}
})
.catch((error) => {
toast.error("Problem with submitting: " + error)
})
} }
const defaultTopPadding = 10
const open = openIndexes.includes(index)
return ( return (
<div <div
style={{ style={{
@@ -315,7 +607,7 @@ const AgentUI = (props) => {
}} }}
onMouseEnter={() => { onMouseEnter={() => {
if (!hovered) { if (!hovered) {
console.log("HOVER") //console.log("HOVER")
setHovered(true) setHovered(true)
} }
}} }}
@@ -364,41 +656,50 @@ const AgentUI = (props) => {
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}> <div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
{parsedCategory} {parsedCategory}
</div> </div>
{/*
<div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}> <div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}>
{/* To ISO string from unix time */} {item?.start_time !== undefined && item?.start_time !== null && item?.start_time !== 0 ?
{new Date(item.start_time * 1000).toLocaleString()} new Date(item.start_time * 1000).toLocaleString()
:
null
}
</div> </div>
*/}
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}> <div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
<Chip <Chip
label={item.type} label={item.type}
/> />
</div> </div>
<div style={{ <div style={{
minWidth: 200, minWidth: 300,
maxWidth: 200, maxWidth: 300,
paddingTop: defaultTopPadding, paddingTop: defaultTopPadding,
}}> }}>
{item.label} {item.label}
</div> </div>
<Tooltip title={`Time taken: ${currentDuration*-1} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right"> <Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right">
<div style={{ <div style={{
minWidth: maxTimelineWidth, minWidth: maxTimelineWidth,
maxWidth: maxTimelineWidth, maxWidth: maxTimelineWidth,
paddingTop: defaultTopPadding*1.3, paddingTop: defaultTopPadding*1.5,
}}> }}>
{currentDuration !== 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ? {currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
<div style={{ <div style={{
backgroundColor: item.status === "FINISHED" ? backgroundColor: barColor,
green : item.status === "RUNNING" || item.status === "" ?
theme.palette.main : theme.palette.surfaceColor,
marginLeft: timelineMarginLeft, marginLeft: timelineMarginLeft,
minWidth: timelineWidth, minWidth: timelineWidth,
maxWidth: timelineWidth, maxWidth: timelineWidth,
height: 10, minHeight: 10,
}} /> maxHeight: 10,
: null} borderRadius: theme.palette.borderRadius,
}}>
</div>
:
<Typography variant="body2" color="textSecondary">
</Typography>
}
</div> </div>
</Tooltip> </Tooltip>
@@ -407,24 +708,68 @@ const AgentUI = (props) => {
maxWidth: 100, maxWidth: 100,
display: "flex", display: "flex",
}}> }}>
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="left"> {item.category === "ask" ?
<span> <span style={{display: "flex", }}>
<IconButton {rerunButton}
disabled={item.type !== "decision" || disableButtons} {/*
style={{marginLeft: 20, }} <Tooltip title="Approve" placement="left">
onClick={(e) => { <span>
e.preventDefault() <IconButton
e.stopPropagation() disabled={disableButtons}
style={{marginLeft: 20, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toast.info("Attempting to rerun this decision by itself.") toast.info("Approving this step.")
setDisableButtons(true) }}
RerunDecision(item.details) >
}} <CheckIcon style={{color: green, }} />
> </IconButton>
<RestartAltIcon /> </span>
</IconButton> </Tooltip>
<Tooltip title="Deny" placement="left">
<span>
<IconButton
disabled={item.type !== "decision" || disableButtons}
style={{marginLeft: 0, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toast.info("Stopping on this step.")
}}
>
<CloseIcon style={{color: red, }} />
</IconButton>
</span>
</Tooltip>
*/}
<Tooltip title="See in another window" placement="left">
<span>
<IconButton
style={{marginLeft: 0, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
//http://localhost:3002/forms/aadfe022-fe93-431c-8634-de42dd7440ac?authorization=9357f6a6-7d59-44be-ad66-be27657369ac&reference_execution=0726378d-b501-470f-b850-f7fb48cd8ca4&source_node=de446bcf-ad37-4337-9f72-e069c7425fac&backend_url=https://ec4245cd2941.ngrok-free.app
const newurl = `/forms/${execution?.workflow?.id}?authorization=${execution.authorization}&reference_execution=${execution.execution_id}&source_node=${agentActionResult?.action?.id}&decision_id=${item.details.run_details.id}&backend_url=${globalUrl}`
window.open(newurl, '_blank', 'noopener,noreferrer');
}}
>
<OpenInNewIcon />
</IconButton>
</span>
</Tooltip>
</span> </span>
</Tooltip> :
item.category === "agent" ?
rerunAgentButton
:
rerunButton
}
<Tooltip title="Explore results" placement="right"> <Tooltip title="Explore results" placement="right">
<span> <span>
<IconButton <IconButton
@@ -442,6 +787,69 @@ const AgentUI = (props) => {
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
{showAuthentication && selectedApp.id !== undefined ?
<div style={{minWidth: 300, maxWidth: 300, margin: "auto", marginTop: 25, }}>
<AuthenticationModal
globalUrl={globalUrl}
userdata={userdata}
setAppAuthentication={setAppAuth}
selectedAppData={selectedApp}
/>
</div>
: null}
{questions?.length > 0 && item?.status === "RUNNING" ?
<div>
{questions.map((q, questionIndex) => {
return (
<div style={{marginTop: 25, }}>
<Typography variant="body2">
{`${q.question}`}
</Typography>
<TextField
label={`Question ${q.index}`}
placeholder="No question found"
variant="outlined"
style={{width: 800, marginTop: 20, }}
multiline
minRows={2}
defaultValue={questionAnswers[q.question]?.value || ""}
onBlur={(e) => {
console.log("Change: ", e.target.value)
try {
questionAnswers[q.question] = {
"index": questionIndex,
"value": e.target.value,
}
setQuestionAnswers({...questionAnswers, })
} catch (e) {
toast.warn("Something went wrong. Please contact support@shuffler.io. Details: " + e)
}
}}
/>
</div>
)
})}
<Button
variant="contained"
style={{marginTop: 10, }}
disabled={questionSubmitDisabled}
onClick={() => {
submitQuestions(item?.details?.run_details?.id, questionAnswers)
}}
>
Submit
</Button>
</div>
: null}
{open ? {open ?
<div style={{marginTop: 10, marginBottom: 10, }}> <div style={{marginTop: 10, marginBottom: 10, }}>
@@ -479,7 +887,12 @@ const AgentUI = (props) => {
const TimelineRender = (props) => { const TimelineRender = (props) => {
const { agent_data } = props; const { agent_data } = props;
const actionResult = execution?.results?.length > 0 ? execution.results[0] : execution var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
const validate = validateJson(actionResult?.result)
if (validate.valid === true) {
actionResult.result = validate.result
}
var timelineItems = [ var timelineItems = [
{ {
"label": "AI Agent 2", "label": "AI Agent 2",
@@ -493,6 +906,27 @@ const AgentUI = (props) => {
}, },
] ]
// Setting up the initial item
if (agent_data?.started_at === undefined && execution?.started_at !== undefined) {
timelineItems[0].start_time = execution?.started_at
}
if (agent_data?.completed_at === undefined && execution?.completed_at !== undefined) {
timelineItems[0].end_time = execution?.completed_at
}
// Always prioritise the execution status first
// agent (RUNNING) = workflow (EXECUTING)
if (execution?.status !== undefined) {
timelineItems[0].status = execution?.status
}
if (actionResult?.result?.status !== undefined && actionResult?.result?.status !== null && actionResult?.result?.status?.length > 0) {
if (timelineItems[0].status !== "FINISHED" && timelineItems[0].status !== "ABORTED" && timelineItems[0].status !== "FAILURE") {
timelineItems[0].status = actionResult?.result?.status
}
}
// Autofixer for result lol // Autofixer for result lol
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) { if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
const verifiedInput = validateJson(actionResult?.result) const verifiedInput = validateJson(actionResult?.result)
@@ -500,6 +934,7 @@ const AgentUI = (props) => {
agent_data.decisions = verifiedInput.result?.decisions agent_data.decisions = verifiedInput.result?.decisions
setAgentActionResult(actionResult) setAgentActionResult(actionResult)
} }
} }
@@ -516,13 +951,13 @@ const AgentUI = (props) => {
} }
var newTimelineItem = { var newTimelineItem = {
"label": item.action, "label": item?.action,
"type": "decision", "type": "decision",
"category": item.category, "category": item?.category,
"status": item.run_details.status, "status": item?.run_details?.status,
"start_time": item.run_details.started_at, "start_time": item?.run_details?.started_at,
"end_time": item.run_details.completed_at, "end_time": item?.run_details?.completed_at,
} }
newTimelineItem.details = item newTimelineItem.details = item
@@ -577,6 +1012,14 @@ const AgentUI = (props) => {
setAgentRequestLoading(true) setAgentRequestLoading(true)
//setShowAgentStarter(false); //setShowAgentStarter(false);
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization); //GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
//
setData({})
setExecution(null)
setAgentRequestLoading(true)
setShowAgentStarter(true)
setActionInput(inputText)
setAgentActionResult(null)
if (inputText === undefined || inputText === null || inputText === "") { if (inputText === undefined || inputText === null || inputText === "") {
toast.error("Please provide a valid input for the AI Agent.") toast.error("Please provide a valid input for the AI Agent.")
@@ -606,7 +1049,7 @@ const AgentUI = (props) => {
}, },
{ {
"name":"action", "name":"action",
"value":"list_tickets" "value":"list_tickets,API"
} }
]} ]}
@@ -637,18 +1080,38 @@ const AgentUI = (props) => {
} }
const handleKeyDown = (e) => {
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
if (isCmdEnter || isCtrlEnter) {
e.preventDefault()
submitInput(actionInput)
}
}
return ( return (
<div style={agentWrapperStyle}> <div style={agentWrapperStyle}>
<TextField
id="copy_element_shuffle"
style={{ display: "none" }}
/>
{showAgentStarter ? {showAgentStarter ?
<Box component="form" style={{textAlign: "center", }} onSubmit={(e) => { <Box
e.preventDefault(); component="form"
submitInput(actionInput); style={{textAlign: "center", }}
}}> onKeyDown={handleKeyDown}
onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
}}
>
<img src="/images/logos/agent.svg" style={{ <img src="/images/logos/agent.svg" style={{
width: 200, width: 200,
height: 200, height: 200,
borderRadius: theme.palette.borderRadius,
}} /> }} />
<div /> <div />
<Typography variant="h5" style={{marginTop: 30, }}> <Typography variant="h5" style={{marginTop: 30, }}>
@@ -661,7 +1124,7 @@ const AgentUI = (props) => {
style={{width: 450, marginRight: 20, marginTop: 30, }} style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline multiline
minRows={2} minRows={2}
defaultValue={execution?.execution_id || ""} defaultValue={actionInput || ""}
onChange={(e) => { onChange={(e) => {
setActionInput(e.target.value) setActionInput(e.target.value)
}} }}
@@ -704,6 +1167,22 @@ const AgentUI = (props) => {
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<Tooltip title="Reload the agent data" placement="top">
<span>
<Button
disabled={execution === null || Object.keys(execution).length === 0}
style={{marginLeft: 25, }}
variant={"outlined"}
color="secondary"
onClick={() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}}
>
<RefreshIcon />
</Button>
</span>
</Tooltip>
{buttonState === "timeline" ? {buttonState === "timeline" ?
<TimelineRender agent_data={data} /> <TimelineRender agent_data={data} />
: :
+53 -14
View File
@@ -22,6 +22,7 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
import algoliasearch from 'algoliasearch/lite'; import algoliasearch from 'algoliasearch/lite';
import useDebouncedCallback from "../utils/useDebouncedCallback.js";
import { import {
Zoom, Zoom,
Fade, Fade,
@@ -275,7 +276,7 @@ export const triggers = [
{ {
"name": "alertinfo", "name": "alertinfo",
"example": "", "example": "",
"value": "Do you want to continue the workflow? Start parameters: $exec", "value": "## Stop or continue?\n\nDetails: $exec",
}, },
{ {
"name": "options", "name": "options",
@@ -1259,6 +1260,24 @@ const AngularWorkflow = (defaultprops) => {
"multiline": true, "multiline": true,
}] }]
}, },
/*
// An attempt at handling APIs directly. This ~kind of works
{
"name": "API",
"description": "Attempts to take your fields and run an API call with them, whatever they are",
"label": "Custom Action",
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
"parameters": [
{
"name": "fields",
"value": "",
"description": "A JSON object with the fields to send to the API. Example: {\"url\": \"hello\", \"key2\": \"value2\"}",
"required": true,
"multiline": true,
}
]
},
*/
{ {
"name": "Translate standard", "name": "Translate standard",
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore", "description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
@@ -11589,10 +11608,16 @@ const AngularWorkflow = (defaultprops) => {
}; };
const handleDragStop = (e, app) => { const handleDragStop = (e, app) => {
if (cy === undefined || cy == null) {
console.log("Cytoscape not initialized")
return
}
var currentnode = cy.getElementById(newNodeId); var currentnode = cy.getElementById(newNodeId);
if (currentnode === undefined || currentnode === null || currentnode.length === 0) { if (currentnode === undefined || currentnode === null || currentnode.length === 0) {
return; console.log("No current node found")
return
} }
if (parsedApp === undefined || parsedApp === null || parsedApp.data === undefined || parsedApp.data === null) { if (parsedApp === undefined || parsedApp === null || parsedApp.data === undefined || parsedApp.data === null) {
@@ -12411,11 +12436,20 @@ const AngularWorkflow = (defaultprops) => {
}; };
const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => { const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => {
const debouncedRefine = useDebouncedCallback(refine, 500)
const lastRefinedRef = useRef(currentRefinement)
const safeRefine = (value) => {
if (value === lastRefinedRef.current) return
lastRefinedRef.current = value
debouncedRefine(value)
}
if (document !== undefined) { if (document !== undefined) {
const appsearchValue = document.getElementById("appsearch") const appsearchValue = document.getElementById("appsearch")
if (appsearchValue !== undefined && appsearchValue !== null) { if (appsearchValue !== undefined && appsearchValue !== null) {
if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) { if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) {
refine(appsearchValue.value) safeRefine(appsearchValue.value)
} }
} }
} }
@@ -12448,8 +12482,7 @@ const AngularWorkflow = (defaultprops) => {
//if (event.currentTarget.value.length > 0 && !searchOpen) { //if (event.currentTarget.value.length > 0 && !searchOpen) {
// setSearchOpen(true) // setSearchOpen(true)
//} //}
safeRefine(event.currentTarget.value)
refine(event.currentTarget.value)
}} }}
limit={5} limit={5}
/> />
@@ -14725,7 +14758,7 @@ const AngularWorkflow = (defaultprops) => {
zIndex: 10000, zIndex: 10000,
}} }}
> >
Conditions can't be used for loops [ .# ]{" "} <b>PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "}</b>
<a <a
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
@@ -20236,17 +20269,23 @@ const AngularWorkflow = (defaultprops) => {
const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ? const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
<div <div
style={{ style={{
border: theme.palette.DialogStyle.border, border: theme.palette.DialogStyle.border,
position: "absolute", position: "absolute",
bottom: 100, bottom: 100,
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115, left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
width: "fit-content",
maxWidth: "45vw",
minWidth: 300,
color: theme.palette.DialogStyle.color, color: theme.palette.DialogStyle.color,
padding: 10, padding: 10,
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
transition: "left 0.3s ease, top 0.3s ease", transition: "left 0.3s ease, top 0.3s ease",
}} overflowWrap: "anywhere",
wordBreak: "break-word",
whiteSpace: "pre-wrap",
}}
> >
<Tooltip <Tooltip
@@ -22726,12 +22765,12 @@ const AngularWorkflow = (defaultprops) => {
style={{ float: "right", marginTop: 20, }} style={{ float: "right", marginTop: 20, }}
// Max 5 days in the past // Max 5 days in the past
disabled={userdata.region_url !== "https://shuffler.io" || executionData.started_at < (Math.floor(Date.now() / 1000) - 432000)} disabled={executionData.started_at < (Math.floor(Date.now() / 1000) - 432000)}
onClick={() => { onClick={() => {
toast("Opening logs in a new tab") toast("Opening logs in a new tab")
setTimeout(() => { setTimeout(() => {
window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank") window.open(`${globalUrl}/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
}, 250) }, 250)
}} }}
> >
+7 -7
View File
@@ -1494,13 +1494,13 @@ const ApiExplorerWrapper = (props) => {
/> />
<div <div
style={{ style={{
backgroundColor: theme.palette.inputColor, backgroundColor: theme.palette.inputColor,
padding: 15, padding: 15,
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
marginBottom: 30, marginBottom: 30,
}} }}
> >
<Typography variant="h6" style={{marginBottom: 25, }}> <Typography variant="h6" style={{marginBottom: 25, }}>
There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
</Typography> </Typography>
+103 -107
View File
@@ -8,6 +8,7 @@ import {
Typography, Typography,
FormControlLabel, FormControlLabel,
Button, Button,
ButtonGroup,
Divider, Divider,
Select, Select,
MenuItem, MenuItem,
@@ -2680,11 +2681,11 @@ const AppCreator = (defaultprops) => {
setErrorCode(responseJson.reason); setErrorCode(responseJson.reason);
if (responseJson?.details !== undefined && responseJson?.details !== null) { if (responseJson?.details !== undefined && responseJson?.details !== null) {
toast.error("Failed to build - contact support@shuffler.io: " + responseJson.details, { toast.error("Failed to build - contact support@shuffler.io:\n\n" + responseJson.details, {
autoClose: 60000 autoClose: 60000
}) })
} else { } else {
toast.error("Failed to build: " + responseJson.reason, { toast.error("Failed to build: \n\n" + responseJson?.reason, {
autoClose: 10000 autoClose: 10000
}) })
} }
@@ -2930,7 +2931,7 @@ const AppCreator = (defaultprops) => {
Query Query
</MenuItem> </MenuItem>
</Select> </Select>
<div style={{ display: "flex", width: 100 }}> <ButtonGroup style={{ display: "flex", width: 100 }}>
{index === extraAuth.length - 1 ? ( {index === extraAuth.length - 1 ? (
<Button <Button
color="primary" color="primary"
@@ -2963,7 +2964,7 @@ const AppCreator = (defaultprops) => {
> >
<RemoveIcon style={{}} /> <RemoveIcon style={{}} />
</Button> </Button>
</div> </ButtonGroup>
</span> </span>
); );
})} })}
@@ -3431,13 +3432,13 @@ const AppCreator = (defaultprops) => {
const ActionPaper = (props) => { const ActionPaper = (props) => {
const { data, index } = props const { data, index } = props
const [updater, setUpdater] = useState("tmp"); const [updater, setUpdater] = useState("tmp");
const [actionsModalOpen, setActionsModalOpen] = useState(false); const [actionsModalOpen, setActionsModalOpen] = useState(false);
const [urlPath, setUrlPath] = useState(""); const [urlPath, setUrlPath] = useState("");
const [fileUploadEnabled, setFileUploadEnabled] = useState(false); const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0]) const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
const [extraBodyFields, setExtraBodyFields] = useState([]); const [extraBodyFields, setExtraBodyFields] = useState([]);
const [urlPathQueries, setUrlPathQueries] = useState([]); const [urlPathQueries, setUrlPathQueries] = useState([]);
const [currentAction, setCurrentAction] = useState({ const [currentAction, setCurrentAction] = useState({
name: "", name: "",
file_field: "", file_field: "",
@@ -3454,6 +3455,10 @@ const AppCreator = (defaultprops) => {
required_bodyfields: [], required_bodyfields: [],
}); });
useEffect(() => {
console.log("Queries: ", urlPathQueries)
}, [urlPathQueries])
const findBodyParams = (body) => { const findBodyParams = (body) => {
const regex = /\${(\w+)}/g; const regex = /\${(\w+)}/g;
const found = body.match(regex); const found = body.match(regex);
@@ -3462,7 +3467,7 @@ const AppCreator = (defaultprops) => {
} else { } else {
setExtraBodyFields(found); setExtraBodyFields(found);
} }
}; };
const UrlPathParameters = () => { const UrlPathParameters = () => {
const values = getCurrentPaths(urlPath); const values = getCurrentPaths(urlPath);
@@ -3495,28 +3500,27 @@ const AppCreator = (defaultprops) => {
) : null; ) : null;
}; };
const HandleIndividualChip = (props) => { const HandleIndividualChip = (props) => {
const { chipData, index } = props; const { chipData, index } = props;
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false); const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
const parsedChip = chipData.startsWith("${") && chipData.endsWith("}") ? chipData.substring(2, chipData.length - 1) : chipData const parsedChip = chipData.startsWith("${") && chipData.endsWith("}") ? chipData.substring(2, chipData.length - 1) : chipData
return ( return (
<Tooltip title={chipRequired ? "Make not required" : "Make required"}> <Tooltip title={chipRequired ? "Make not required" : "Make required"}>
<Chip <Chip
style={{ style={{
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor, backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
height: 30, height: 30,
margin: 3, margin: 3,
paddingLeft: 5, paddingLeft: 5,
paddingRight: 5, paddingRight: 5,
cursor: "pointer", cursor: "pointer",
borderColor: theme.palette.chipStyle.borderColor, borderColor: theme.palette.chipStyle.borderColor,
color: theme.palette.chipStyle.color, color: theme.palette.chipStyle.color,
}} }}
label={parsedChip} label={parsedChip}
onClick={() => { onClick={() => {
if (chipRequired) { if (chipRequired) {
currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1) currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1)
} else { } else {
@@ -3524,27 +3528,28 @@ const AppCreator = (defaultprops) => {
} }
setCurrentAction(currentAction); setCurrentAction(currentAction);
setChipRequired(!chipRequired); setChipRequired(!chipRequired);
}} }}
/> />
</Tooltip> </Tooltip>
); );
};
const setActionField = (field, value) => {
currentAction[field] = value
setCurrentAction(currentAction)
//setUrlPathQueries(currentAction.queries)
}; };
const addPathQuery = () => { const setActionField = (field, value) => {
currentAction[field] = value
setCurrentAction(currentAction)
//setUrlPathQueries(currentAction.queries)
};
const addPathQuery = () => {
urlPathQueries.push({ name: "", required: true, example: "", }); urlPathQueries.push({ name: "", required: true, example: "", });
if (updater === "addupdater") { if (updater === "addupdater") {
setUpdater("updater"); setUpdater("updater");
} else { } else {
setUpdater("addupdater"); setUpdater("addupdater");
} }
setUrlPathQueries(urlPathQueries); setUrlPathQueries(urlPathQueries);
}; };
@@ -3555,6 +3560,7 @@ const AppCreator = (defaultprops) => {
} else { } else {
setUpdater("flipupdater"); setUpdater("flipupdater");
} }
setUrlPathQueries(urlPathQueries); setUrlPathQueries(urlPathQueries);
}; };
@@ -3573,7 +3579,7 @@ const AppCreator = (defaultprops) => {
} }
}; };
const loopQueries = urlPathQueries.length === 0 ? null : ( const loopQueries = urlPathQueries.length === 0 ? null : (
<div> <div>
<Divider <Divider
style={{ style={{
@@ -3591,51 +3597,42 @@ const AppCreator = (defaultprops) => {
return ( return (
<Paper key={queryIndex} style={actionListStyle}> <Paper key={queryIndex} style={actionListStyle}>
<div style={{ marginLeft: "5px", width: "100%" }}> <div style={{ marginLeft: "5px", width: "100%" }}>
<div style={{display: "flex"}}> <div style={{display: "flex"}}>
<TextField <TextField
required required
fullWidth={true} fullWidth={true}
defaultValue={query.name} defaultValue={query.name}
placeholder={"Query name (key)"} placeholder={"Query name (key)"}
label={"Query Key"} label={"Query Key"}
helperText={ onBlur={(e) => {
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}> urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "")
Click required to flip setUrlPathQueries(urlPathQueries)
</span> }}
} style={{flex: 3}}
onBlur={(e) => { InputProps={{
console.log("IN BLUR: ", e.target.value); style: {
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", ""); color: theme.palette.text.primary,
setUrlPathQueries(urlPathQueries); },
}} }}
style={{flex: 3}} />
InputProps={{ <TextField
style: { fullWidth={true}
color: theme.palette.text.primary, defaultValue={query.example}
}, placeholder={"Default value"}
}} label={"Example"}
/> onBlur={(e) => {
<TextField // E.g. for Jira -> JQL -> requires = in param
fullWidth={true} urlPathQueries[queryIndex].example = e.target.value.replaceAll("=","=")
defaultValue={query.example} setUrlPathQueries(urlPathQueries)
placeholder={"Default value"} }}
label={"Example"} style={{flex: 2}}
onBlur={(e) => { InputProps={{
urlPathQueries[queryIndex].example = e.target.value.replaceAll( style: {
"=", color: theme.palette.text.primary,
"" },
) }}
/>
setUrlPathQueries(urlPathQueries) </div>
}}
style={{flex: 2}}
InputProps={{
style: {
color: theme.palette.text.primary,
},
}}
/>
</div>
<div <div
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => { onClick={() => {
@@ -3654,7 +3651,7 @@ const AppCreator = (defaultprops) => {
deletePathQuery(queryIndex); deletePathQuery(queryIndex);
}} }}
> >
<DeleteIcon /> <DeleteIcon />
</div> </div>
</Paper> </Paper>
); );
@@ -4106,22 +4103,22 @@ const AppCreator = (defaultprops) => {
if (request.header !== undefined && request.header !== null) { if (request.header !== undefined && request.header !== null) {
var headers = []; var headers = [];
for (let [key, value] of Object.entries(request.header)) { for (let [key, value] of Object.entries(request.header)) {
if (value === undefined) { if (value === undefined) {
if (key.includes(":")) { if (key.includes(":")) {
const keysplit = key.split(":") const keysplit = key.split(":")
key = keysplit[0].trim() key = keysplit[0].trim()
value = keysplit[1].trim() value = keysplit[1].trim()
} else if (key.includes("=")) { } else if (key.includes("=")) {
const keysplit = key.split("=") const keysplit = key.split("=")
key = keysplit[0].trim() key = keysplit[0].trim()
value = keysplit[1].trim() value = keysplit[1].trim()
} else { } else {
toast("Removed key: ", key) toast("Removed key: ", key)
continue continue
} }
} }
if ( if (
parameterName !== undefined && parameterName !== undefined &&
@@ -4392,9 +4389,8 @@ const AppCreator = (defaultprops) => {
variant={urlPath.length > 0 ? "contained" : "outlined"} variant={urlPath.length > 0 ? "contained" : "outlined"}
style={{ }} style={{ }}
onClick={() => { onClick={() => {
//console.log(urlPathQueries)
//console.log(urlPath)
console.log(currentAction); console.log(currentAction);
const errors = getActionErrors(); const errors = getActionErrors();
addActionToView(errors); addActionToView(errors);
setActionsModalOpen(false); setActionsModalOpen(false);
@@ -4460,7 +4456,7 @@ const AppCreator = (defaultprops) => {
return ( return (
<Paper key={index} style={actionListStyle}> <Paper key={index} style={actionListStyle}>
{newActionModal} {newActionModal}
{error} {error}
<Tooltip title="Edit action" placement="bottom"> <Tooltip title="Edit action" placement="bottom">
+4 -4
View File
@@ -3075,7 +3075,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const appEnding = app?.public === true ? app?.app_version : app?.id const appEnding = app?.public === true ? app?.app_version : app?.id
return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip` return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip${!app?.public ? ` \\\n docker tag frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding} frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${app.app_version}` : ``}`
} }
const renderedActionOptions = deduplicateByName(( const renderedActionOptions = deduplicateByName((
@@ -3405,7 +3405,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
<div style={{ textAlign: "center", marginTop: 25 }}> <div style={{ textAlign: "center", marginTop: 25 }}>
<Link <Link
rel="noopener noreferrer" rel="noopener noreferrer"
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}`} to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}&view=/apps/${params.appid}/integrations/${secondaryApp.name}`}
style={{ textDecoration: "none" }} style={{ textDecoration: "none" }}
> >
<Button <Button
@@ -4300,8 +4300,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
color="primary" color="primary"
onClick={() => { onClick={() => {
if (!isLoggedIn) { if (!isLoggedIn) {
//navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid); navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
toast("You must be logged in to activate apps! Go to /login first.") // toast("You must be logged in to activate apps! Go to /login first.")
return; return;
} }
+53
View File
@@ -42,6 +42,7 @@ import { debounce } from "lodash";
import AppSelection from "../components/AppSelection.jsx"; import AppSelection from "../components/AppSelection.jsx";
import AppModal from "../components/AppModal.jsx"; import AppModal from "../components/AppModal.jsx";
import AppCreationModal from "../components/AppCreationModal.jsx"; import AppCreationModal from "../components/AppCreationModal.jsx";
import Dropzone from "../components/Dropzone.jsx";
const searchClient = algoliasearch( const searchClient = algoliasearch(
@@ -1136,6 +1137,7 @@ const Apps2 = (props) => {
const [field2, setField2] = useState(""); const [field2, setField2] = useState("");
const [validation, setValidation] = useState(null); const [validation, setValidation] = useState(null);
const [createAppModalOpen, setCreateAppModalOpen] = useState(false); const [createAppModalOpen, setCreateAppModalOpen] = useState(false);
const [openApiData, setOpenApiData] = useState("");
const {themeMode, brandColor} = useContext(Context); const {themeMode, brandColor} = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
@@ -1736,6 +1738,31 @@ const Apps2 = (props) => {
// setOpenModal(true); // setOpenModal(true);
}; };
const uploadFile = (e) => {
const isFromDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
const files = isFromDropzone ? e.dataTransfer.files : e.target.files;
const reader = new FileReader();
try {
reader.addEventListener("load", (ev) => {
const content = ev.target.result;
setOpenApiData(content);
setCreateAppModalOpen(true);
});
} catch (err) {
console.log("Error in dropzone: ", err);
}
try {
reader.readAsText(files[0]);
} catch (error) {
toast("Failed to read file");
}
};
// Validation and redirect are handled inside AppCreationModal
useEffect(() => { useEffect(() => {
const apps = currTab === 1 ? userApps : orgApps; const apps = currTab === 1 ? userApps : orgApps;
const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel); const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
@@ -1853,6 +1880,10 @@ const Apps2 = (props) => {
} }
return ( return (
<Dropzone
style={{ width: "100%", height: "100vh" }}
onDrop={uploadFile}
>
<div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}> <div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}>
<InstantSearch searchClient={searchClient} indexName="appsearch"> <InstantSearch searchClient={searchClient} indexName="appsearch">
<AppModal <AppModal
@@ -1869,6 +1900,8 @@ const Apps2 = (props) => {
theme={theme} theme={theme}
globalUrl={globalUrl} globalUrl={globalUrl}
isCloud={isCloud} isCloud={isCloud}
startOpenApi={openApiData?.length > 0}
prefillOpenApiData={openApiData}
/> />
{appsModalLoad} {appsModalLoad}
<div style={boxStyle}> <div style={boxStyle}>
@@ -2198,6 +2231,24 @@ const Apps2 = (props) => {
</> </>
)} )}
</div> </div>
<Tooltip
title="Create an app with different options or Just drop a YAML/JSON file here"
placement="top"
componentsProps={{
tooltip: {
sx: {
backgroundColor: "rgba(33, 33, 33, 1)",
color: "rgba(241, 241, 241, 1)",
fontSize: 12,
width: 240,
lineHeight: 1.5,
border: "1px solid rgba(73, 73, 73, 1)",
fontFamily: theme?.typography?.fontFamily,
}
},
}}
arrow
>
<div style={{ <div style={{
width: "25%", width: "25%",
minWidth: "25%", minWidth: "25%",
@@ -2222,6 +2273,7 @@ const Apps2 = (props) => {
Create an App Create an App
</Button> </Button>
</div> </div>
</Tooltip>
</div> </div>
<div> <div>
@@ -2365,6 +2417,7 @@ const Apps2 = (props) => {
<Configure clickAnalytics /> <Configure clickAnalytics />
</InstantSearch> </InstantSearch>
</div> </div>
</Dropzone>
); );
}; };
+9 -4
View File
@@ -8,7 +8,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify" import { ToastContainer, toast } from "react-toastify"
import Draggable from "react-draggable"; import Draggable from "react-draggable";
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx'; import { LoadStats } from '../components/LineChartWrapper.jsx';
import { import {
Autocomplete, Autocomplete,
@@ -828,9 +828,14 @@ const Dashboard = (props) => {
} }
</div> </div>
<DashboardBarchart <LineChartWrapper
timelineData={data} inputname={"heyo"}
height={50} keys={data}
height={100}
width={100}
border={false}
color={"#808080"}
/> />
</Paper> </Paper>
+6 -1
View File
@@ -400,6 +400,10 @@ const Docs = (defaultprops) => {
if (propkey === "app_creation") { if (propkey === "app_creation") {
navigate('/docs/apps#app-creation-introduction') navigate('/docs/apps#app-creation-introduction')
} }
if (propkey === "api") {
navigate('/docs/API')
}
} }
@@ -690,7 +694,8 @@ const Docs = (defaultprops) => {
const Heading = (props) => { const Heading = (props) => {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
var id = props.children[0].toLowerCase().toString()
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase();
if (props.level <= 3) { if (props.level <= 3) {
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-"); id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
} }
+266
View File
@@ -0,0 +1,266 @@
import React, { useEffect, useState, useContext, useRef, useCallback } from 'react';
import {
Typography,
Grid,
Paper,
Box,
Stack,
Chip,
Avatar,
Divider,
Select,
MenuItem,
} from '@mui/material';
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import TaskAltIcon from '@mui/icons-material/TaskAlt';
import SuccessFailedRunsWidget from '../components/SuccessFailedRunsWidget.jsx';
import RunsOverTimeWidget from '../components/RunsOverTimeWidget.jsx';
import { Context } from '../context/ContextApi.jsx';
import CircularProgress from '@mui/material/CircularProgress';
import { useNavigate } from 'react-router-dom';
import DashboardOnboarding from '../components/DashboardOnboarding.jsx';
const NewDashboard = (props) => {
const { globalUrl, userdata } = props;
// const [workflows, setWorkflows] = useState([]);
const { leftSideBarOpenByClick } = useContext(Context);
const [sfwControls, setSfwControls] = useState(null);
const [loadingSfw, setLoadingSfw] = useState(true);
const [loadingRot, setLoadingRot] = useState(true);
const [loadingNoti, setLoadingNoti] = useState(true);
const [showOverlay, setShowOverlay] = useState(true);
const [totals, setTotals] = useState({ days: 30, mode: 'workflows', totalRuns: 0, successRuns: 0, failedRuns: 0, activeDays: 0, timeSavedMinutes: 0, moneySavedDollars: 0 });
const [notifications, setNotifications] = useState([]);
const [onboardingOpen, setOnboardingOpen] = useState(() => {
try {
return localStorage.getItem("dashboard_onboarding_complete") === "true" ? false : true;
} catch {
return true;
}
});
const [overrideDays, setOverrideDays] = useState(undefined);
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
const navigate = useNavigate();
const handleSfwControls = useCallback((node) => {
setSfwControls(node);
}, []);
const formatCurrencyCompact = (value) => {
const n = Math.max(0, Number(value) || 0);
const abs = Math.abs(n);
const fmt = (x, suffix) => `${(Math.round(x * 10) / 10).toString().replace(/\.0$/, '')}${suffix}`;
if (abs >= 1e9) return `$${fmt(n / 1e9, 'B')}`;
if (abs >= 1e6) return `$${fmt(n / 1e6, 'M')}`;
if (abs >= 1e3) return `$${fmt(n / 1e3, 'k')}`;
return `$${Math.round(n).toLocaleString()}`;
};
const formatTimeDisplay = (mins) => {
const totalMins = Math.max(0, Math.round(mins || 0));
if (totalMins < 60) return { display: `${totalMins}m`, title: `${totalMins} minutes` };
const totalHours = Math.floor(totalMins / 60);
if (totalHours >= 24) {
const days = Math.floor(totalHours / 24);
return { display: `${days}d`, title: `${totalHours} hours` };
}
return { display: `${totalHours}h`, title: `${totalHours} hours` };
};
const timeFmt = formatTimeDisplay(totals.timeSavedMinutes);
const STATIC_TIME_PERCENT = '62%';
const STATIC_MONEY_PERCENT = '46%';
const unreadCount = notifications.filter(n => n && n.read === false).length;
const readCount = notifications.filter(n => n && n.read === true).length;
// Current values
// 1 Workflow run = 15 minutes
// 1 Workflow run = $25
const kpis = [
{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879' },
{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879' },
{ value: String(unreadCount), label: 'Total errors', icon: <ErrorOutlineIcon sx={{ color: '#f87171', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#f87171' },
{ value: String(readCount), label: 'Errors resolved', icon: <TaskAltIcon sx={{ color: '#5cc879', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#5cc879' },
];
const getGreeting = () => {
try {
const hour = new Date().getHours();
if (hour < 5) return 'Good night';
if (hour < 12) return 'Good morning';
if (hour < 18) return 'Good afternoon';
return 'Good evening';
} catch {
return 'Hey';
}
};
const displayName = userdata !== undefined && userdata?.username !== undefined ? userdata?.username?.split('@')[0]?.charAt(0)?.toUpperCase() + userdata?.username?.split('@')[0]?.slice(1) : 'User';
useEffect(() => {
let t;
const anyLoading = loadingSfw || loadingRot || loadingNoti;
if (anyLoading) {
t = setShowOverlay(true);
} else {
setShowOverlay(false);
}
return () => { if (t) clearTimeout(t); };
}, [loadingSfw, loadingRot, loadingNoti]);
// Auto-open onboarding when there aren't enough active days of stats
useEffect(() => {
try {
const alreadyDone = localStorage.getItem("dashboard_onboarding_complete") === "true";
if (alreadyDone) {
setOnboardingOpen(false);
return;
}
const active = Number(totals?.activeDays || 0);
setOnboardingOpen(active < 5);
} catch {
setOnboardingOpen(true);
}
}, [totals?.activeDays]);
// Load notifications
useEffect(() => {
const loadNotifications = async () => {
try {
const resp = await fetch(`${globalUrl}/api/v1/notifications`, {
method: 'GET',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
if (resp.status !== 200) {
setNotifications([]);
return;
}
const data = await resp.json();
const list = Array.isArray(data?.notifications) ? data.notifications : (Array.isArray(data) ? data : []);
setNotifications(list.filter(Boolean));
} catch (e) {
setNotifications([]);
} finally {
setLoadingNoti(false);
}
};
loadNotifications();
}, [globalUrl]);
// useEffect(() => {
// // Lightweight workflows list for selector in success/failed widget
// const loadWorkflows = async () => {
// try {
// const resp = await fetch(`${globalUrl}/api/v1/workflows`, {
// method: 'GET',
// credentials: 'include',
// headers: { 'Content-Type': 'application/json' },
// });
// if (resp.status !== 200) {
// return;
// }
// const data = await resp.json();
// const list = Array.isArray(data?.workflows) ? data.workflows : (Array.isArray(data) ? data : []);
// const normalized = list.filter(Boolean).map((w, idx) => ({ id: w?.id || w?.ID || `${idx}`, name: w?.name || w?.Name || `Workflow ${idx+1}` }));
// setWorkflows(normalized);
// } catch (e) {
// // ignore
// }
// };
// loadWorkflows();
// }, [globalUrl]);
return (
<div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}>
<DashboardOnboarding
open={onboardingOpen}
globalUrl={globalUrl}
onClose={() => setOnboardingOpen(false)}
onExplore={() => {
// Ensure overrides are set before closing modal
setOverrideDays(5);
setRotMonthOverride(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
// Close modal immediately to trigger data fetching
setOnboardingOpen(false);
}}
headerTitle="Unlock your Dashboard"
headerSubtitle="Complete these steps to start seeing insights."
/>
{showOverlay && (
<div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
<CircularProgress size={36} thickness={4} />
<Typography variant="body2" color="textSecondary">Loading dashboard</Typography>
</div>
</div>
)}
{/* Header / Greeting */}
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
<>
{sfwControls}
</>
</Box>
{/* KPI cards */}
<Grid container spacing={2}>
{kpis.map((kpi) => (
<Grid item xs={12} sm={6} md={3} key={kpi.label}>
<Paper style={{ padding: 16, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12
,cursor: kpi.label.toLowerCase().includes('total errors') ? 'pointer' : 'default'
}}
onClick={() => {
if (kpi.label.toLowerCase().includes('total errors')) {
// navigate to notifications page
navigate('/admin?admin_tab=notifications');
}
}}
>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack sx={{py:2, paddingLeft: 1}}>
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
</Stack>
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
{kpi.icon}
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
</Stack>
</Stack>
</Paper>
</Grid>
))}
</Grid>
{/* Success/Failed widget uses its own internal sub-cards; make wrapper transparent */}
<Paper elevation={0} style={{ padding: 0, marginTop: 5, background: 'transparent', boxShadow: 'none', border: 'none' }}>
<SuccessFailedRunsWidget
globalUrl={globalUrl}
overrideDays={overrideDays}
dummyMode={onboardingOpen}
onControlsChange={handleSfwControls}
onLoadingChange={setLoadingSfw}
onTotalsChange={setTotals}
/>
</Paper>
{/* Runs over time section */}
<Paper style={{ padding: 16, marginTop: 19, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12 }}>
<RunsOverTimeWidget globalUrl={globalUrl} onLoadingChange={setLoadingRot} monthOverride={rotMonthOverride} dummyMode={onboardingOpen} />
</Paper>
</div>
);
};
export default NewDashboard;
+165 -52
View File
@@ -72,6 +72,7 @@ const RunWorkflow = (defaultprops) => {
const [executionLoading, setExecutionLoading] = useState(false); const [executionLoading, setExecutionLoading] = useState(false);
const [executionData, setExecutionData] = React.useState({}); const [executionData, setExecutionData] = React.useState({});
const [executionRunning, setExecutionRunning] = useState(false); const [executionRunning, setExecutionRunning] = useState(false);
const [disableButtons, setDisableButtons] = useState(false);
const [workflowQuestion, setWorkflowQuestion] = useState(""); const [workflowQuestion, setWorkflowQuestion] = useState("");
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined); const [selectedOrganization, setSelectedOrganization] = React.useState(undefined);
const [apps, setApps] = React.useState([]); const [apps, setApps] = React.useState([]);
@@ -84,12 +85,14 @@ const RunWorkflow = (defaultprops) => {
const [workflows, setWorkflows] = React.useState([]) const [workflows, setWorkflows] = React.useState([])
const [boxWidth, setBoxWidth] = React.useState(500) const [boxWidth, setBoxWidth] = React.useState(500)
const [inputQuestions, setInputQuestions] = React.useState([]) const [inputQuestions, setInputQuestions] = React.useState([])
const [agentic, setAgentic] = React.useState(false)
const searchParams = new URLSearchParams(window.location.search) const searchParams = new URLSearchParams(window.location.search)
const answer = searchParams.get("answer") const answer = searchParams.get("answer")
const execution_id = searchParams.get("reference_execution") const execution_id = searchParams.get("reference_execution")
const authorization = searchParams.get("authorization") const authorization = searchParams.get("authorization")
const sourceNode = searchParams.get("source_node") const sourceNode = searchParams.get("source_node")
const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
const backendUrl = searchParams.get("backend_url") || globalUrl const backendUrl = searchParams.get("backend_url") || globalUrl
useEffect(() => { useEffect(() => {
@@ -162,11 +165,8 @@ const RunWorkflow = (defaultprops) => {
} }
} }
// Used to swap from login to register. True = login, false = register
// Error messages etc // Error messages etc
const [executionInfo, setExecutionInfo] = useState(""); const [executionInfo, setExecutionInfo] = useState("");
const handleValidateForm = (executionArgument) => { const handleValidateForm = (executionArgument) => {
// Check if every field exists // Check if every field exists
if (executionArgument === undefined || executionArgument === null) { if (executionArgument === undefined || executionArgument === null) {
@@ -184,9 +184,12 @@ const RunWorkflow = (defaultprops) => {
} }
} }
//console.log("EXEC: ", executionArgument) // FIXME: Error with User Input + Required arg (?)
// Somehow validation is not happening as it should, and it just checks all
// questions if none are selected
for (var key in executionArgument) { for (var key in executionArgument) {
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") { if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
console.log("Unanswered, required question: ", key)
return false return false
} }
} }
@@ -334,17 +337,18 @@ const RunWorkflow = (defaultprops) => {
} }
const validate = validateJson(executionData.result) const validate = validateJson(executionData.result)
return ( return (
<div style={{marginTop: executionMargin, }}> <div style={{marginTop: executionMargin, }}>
{workflowQuestion !== "" ? null : {workflowQuestion !== "" ? null :
<Divider style={{marginTop: 20, marginBottom: 20, }}/> <div style={{marginTop: 20, marginBottom: 20, }}/>
} }
{workflowQuestion !== "" ? null : {workflowQuestion !== "" ? null :
validate.valid === false ? validate.valid === false ?
<div style={{marginTop: 20, }}> <div style={{marginTop: 20, }}>
<Divider /> {validate?.result !== undefined && validate?.result !== null && validate?.result.length > 0 ?
<Divider />
: null }
<Markdown <Markdown
components={{ components={{
img: Img, img: Img,
@@ -397,10 +401,13 @@ const RunWorkflow = (defaultprops) => {
stop() stop()
setMessage("") setMessage("")
setExecutionLoading(true)
setExecutionData({}) setExecutionData({})
setExecutionInfo("") setExecutionInfo("")
setTimeout(() => {
setExecutionLoading(true)
}, 2500)
var data = { var data = {
"execution_argument": executionArgument, "execution_argument": executionArgument,
"execution_source": "form", "execution_source": "form",
@@ -462,6 +469,14 @@ const RunWorkflow = (defaultprops) => {
fetchBody.body = JSON.stringify(data) fetchBody.body = JSON.stringify(data)
} }
if (agentic === true) {
if (url.includes("?")) {
url += `&agentic=true&decision_id=${decisionId}`
} else {
url += `?agentic=true&decision_id=${decisionId}`
}
}
// IF there is an execution argument, we should use it // IF there is an execution argument, we should use it
fetch(url, fetchBody) fetch(url, fetchBody)
.then((response) => { .then((response) => {
@@ -480,25 +495,30 @@ const RunWorkflow = (defaultprops) => {
} }
} }
if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization.length === 0) { //if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization?.length === 0) {
toast(`This form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form`) // toast(`This form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form (2)`)
} //}
return response.json() return response.json()
}) })
.then(responseJson => { .then(responseJson => {
//if (responseJson.success === true) {
// setDisableButtons(true)
//}
setExecutionLoading(false) setExecutionLoading(false)
if (responseJson.execution_id !== undefined && responseJson.execution_id !== null && responseJson.execution_id.length > 0) { if (responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id?.length > 0) {
navigate(`?execution_id=${responseJson.execution_id}`) navigate(`?execution_id=${responseJson.execution_id}`)
} }
if (responseJson.success === false) { if (responseJson.success === false) {
console.log("Failed sending execution request") console.log("Failed sending execution request")
if (responseJson.reason !== undefined && responseJson.reason !== null) { if (responseJson?.reason !== undefined && responseJson?.reason !== null) {
if (responseJson?.reason?.toLowerCase().includes("already clicked")) { if (responseJson?.reason?.toLowerCase().includes("already clicked")) {
setMessage("Already answered. You may close this window (2).") setMessage("This form has been answered. You may close this window.")
} else { } else {
toast.warn(responseJson.reason) toast.warn(responseJson?.reason)
} }
} }
@@ -520,11 +540,17 @@ const RunWorkflow = (defaultprops) => {
setExecutionRequest(responseJson) setExecutionRequest(responseJson)
start() start()
} }
// If execution_id or authorization, add them to the URL
if (responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id?.length > 0 && responseJson?.authorization !== undefined && responseJson?.authorization !== null && responseJson?.authorization?.length > 0) {
navigate(`?execution_id=${responseJson.execution_id}&authorization=${responseJson.authorization}`)
}
} }
}) })
.catch(error => { .catch(error => {
//setExecutionInfo("Error in workflow startup: " + error) //setExecutionInfo("Error in workflow startup: " + error)
toast.warn("Error submitting form. Please try again.") console.log("Error starting workflow: ", error)
toast.warn(`Error submitting form. Please try again: ${error}`)
stop() stop()
setMessage("") setMessage("")
@@ -597,8 +623,8 @@ const RunWorkflow = (defaultprops) => {
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) { if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
const newmarkdown = realtimeMarkdown.replace(`{{ ${workflow_id} }}`, "", -1) const newmarkdown = realtimeMarkdown.replace(`{{ ${workflow_id} }}`, "", -1)
setRealtimeMarkdown(newmarkdown) setRealtimeMarkdown(newmarkdown)
} else if (inputWorkflow.form_control.input_markdown !== undefined && inputWorkflow.form_control.input_markdown !== null && inputWorkflow.form_control.input_markdown.length > 0) { } else if (inputWorkflow?.form_control?.input_markdown !== undefined && inputWorkflow?.form_control?.input_markdown !== null && inputWorkflow?.form_control?.input_markdown.length > 0) {
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1) const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
setRealtimeMarkdown(newmarkdown) setRealtimeMarkdown(newmarkdown)
} }
} }
@@ -608,10 +634,10 @@ const RunWorkflow = (defaultprops) => {
console.log("Get workflow error: ", error.toString()) console.log("Get workflow error: ", error.toString())
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) { if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1) const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
setRealtimeMarkdown(newmarkdown) setRealtimeMarkdown(newmarkdown)
} else if (inputWorkflow.form_control.input_markdown !== undefined && inputWorkflow.form_control.input_markdown !== null && inputWorkflow.form_control.input_markdown.length > 0) { } else if (inputWorkflow?.form_control?.input_markdown !== undefined && inputWorkflow?.form_control?.input_markdown !== null && inputWorkflow?.form_control?.input_markdown.length > 0) {
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1) const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
setRealtimeMarkdown(newmarkdown) setRealtimeMarkdown(newmarkdown)
} }
}) })
@@ -646,6 +672,7 @@ const RunWorkflow = (defaultprops) => {
trig.parameters = [] trig.parameters = []
} }
newexec = {}
for (var paramkey in trig.parameters) { for (var paramkey in trig.parameters) {
const param = trig.parameters[paramkey] const param = trig.parameters[paramkey]
if (param.name !== "input_questions") { if (param.name !== "input_questions") {
@@ -683,6 +710,7 @@ const RunWorkflow = (defaultprops) => {
} }
} }
console.log("Setting exec arg: ", newexec)
setExecutionArgument(newexec) setExecutionArgument(newexec)
} }
@@ -733,10 +761,10 @@ const RunWorkflow = (defaultprops) => {
setInputQuestions(workflow.input_questions) setInputQuestions(workflow.input_questions)
} }
if (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) { if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) {
// Look for {{ uuid }} format, and try to run that workflow with their account // Look for {{ uuid }} format, and try to run that workflow with their account
// This is a hack, but a fun one. // This is a hack, but a fun one.
var newmarkdown = workflow.form_control.input_markdown.replace("", "") var newmarkdown = workflow?.form_control?.input_markdown.replace("", "")
const uuidRegex = /{{\s[a-f0-9-]+\s}}/g const uuidRegex = /{{\s[a-f0-9-]+\s}}/g
const found = newmarkdown.match(uuidRegex) const found = newmarkdown.match(uuidRegex)
@@ -784,8 +812,8 @@ const RunWorkflow = (defaultprops) => {
} }
} }
if (workflow.status !== "WAITING") { if (workflow.status === "EXECUTING" || workflow.status === "SUCCESS" || workflow.status === "ABORTED" || workflow.status === "STOPPED" || workflow.status === "FAILURE" || workflow.status === "FINISHED") {
setMessage("Already answered. You may close this window (3).") setMessage("Already handled. You may close this window.")
} }
} }
@@ -806,13 +834,17 @@ const RunWorkflow = (defaultprops) => {
console.log("Status not 200 for workflows :O!"); console.log("Status not 200 for workflows :O!");
} }
if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization.length === 0) { //if (response.status >= 400 && authorization === undefined || authorization === null || authorization.length === 0) {
toast(`This form is not available to you. If you think this is an error, please contact ${supportEmail} with the URL.`) // toast.warn(`This form may not be available to you. If you think this is an error, please contact ${supportEmail} with the URL.`)
} //}
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success === false) {
return
}
// Not sure why this is necessary. // Not sure why this is necessary.
if (responseJson.isValid === undefined) { if (responseJson.isValid === undefined) {
responseJson.isValid = true; responseJson.isValid = true;
@@ -1008,14 +1040,78 @@ const RunWorkflow = (defaultprops) => {
return response.json(); return response.json();
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success == false) { if (responseJson?.success == false) {
return return
} }
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && (workflow.id === undefined || workflow.id === null || workflow.id.length === 0) && responseJson.workflow !== undefined && responseJson.workflow !== null) { if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && disableButtons === false && responseJson?.status !== "" && responseJson?.status !== "WAITING") {
setDisableButtons(true)
}
//if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && (workflow.id === undefined || workflow.id === null || workflow.id.length === 0) && responseJson.workflow !== undefined && responseJson.workflow !== null) {
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && responseJson.workflow !== undefined && responseJson.workflow !== null) {
setupSourcenode(responseJson.workflow, sourceNode) setupSourcenode(responseJson.workflow, sourceNode)
setWorkflow(responseJson.workflow) setWorkflow(responseJson.workflow)
//const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
// Check for decision_id in url
if (decisionId?.length > 0 && responseJson?.workflow?.actions?.length > 0 && sourceNode?.length > 0 && responseJson?.results?.length > 0) {
console.log("Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
setAgentic(true)
for (var resultkey in responseJson.results) {
const result = responseJson.results[resultkey]
if (result.action.id !== sourceNode) {
continue
}
const validated = validateJson(result.result)
if (!validated.valid) {
console.log("Error parsing result: ", validated.error)
continue
}
var parsedresult = validated.result
console.log("PARSED RES: ", parsedresult)
if (parsedresult?.decisions?.length > 0) {
var newexec = executionArgument
if (newexec === undefined || newexec === null || Object.keys(newexec).length === 0) {
newexec = {}
}
for (var decisionkey in parsedresult?.decisions) {
const decision = parsedresult.decisions[decisionkey]
if (decision?.run_details?.id !== decisionId) {
continue
}
for (var fieldkey in decision?.fields) {
const field = decision.fields[fieldkey]
if (field.key === "question" && !inputQuestions.find(q => q.name=== field.value)) {
console.log("QUESTION: ", field)
const newquestion = {
"name": field.value,
"value": field.key+"_"+fieldkey,
}
inputQuestions.push(newquestion)
newexec[newquestion.value] = ""
}
}
}
setInputQuestions([...inputQuestions] )
console.log("EXEC: ", newexec)
setExecutionArgument(newexec)
responseJson.workflow.input_questions = inputQuestions
setWorkflow(responseJson?.workflow)
setDisableButtons(false)
}
}
}
} }
@@ -1031,12 +1127,12 @@ const RunWorkflow = (defaultprops) => {
localStorage.setItem(storageKey, JSON.stringify(value)) localStorage.setItem(storageKey, JSON.stringify(value))
} }
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) { if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown?.length > 0) {
const newmarkdown = realtimeMarkdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1) const newmarkdown = realtimeMarkdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
setRealtimeMarkdown(newmarkdown) setRealtimeMarkdown(newmarkdown)
} else if (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) { } else if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) {
const newmarkdown = workflow.form_control.input_markdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1) const newmarkdown = workflow?.form_control?.input_markdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
setRealtimeMarkdown(newmarkdown) setRealtimeMarkdown(newmarkdown)
} }
@@ -1072,7 +1168,6 @@ const RunWorkflow = (defaultprops) => {
getWorkflow(props.match.params.key, sourceNode) getWorkflow(props.match.params.key, sourceNode)
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) { if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
console.log("Get execution: ", execution_id)
fetchUpdates(execution_id, authorization, true) fetchUpdates(execution_id, authorization, true)
} }
@@ -1136,13 +1231,13 @@ const RunWorkflow = (defaultprops) => {
const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"} const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"}
// Check if all fields are filled in? // Check if all fields are filled in?
var disabledButtons = executionLoading || executionRunning || message.length > 0 var disabledButtons = executionLoading || executionRunning || message.length > 0 || disableButtons
if (disabledButtons === false && workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) { if (disabledButtons === false && workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) {
// Check field values // Check field values
//disabledButtons = handleValidateForm(executionArgument) //disabledButtons = handleValidateForm(executionArgument)
} }
const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : "Unknown" const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : ""
const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io" const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io"
//const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io" //const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io"
@@ -1321,12 +1416,12 @@ const RunWorkflow = (defaultprops) => {
<div style={{paddingTop: 150, marginTop: 150, width: 250, itemAlign: "center", textAlign: "center", margin: "auto", }}> <div style={{paddingTop: 150, marginTop: 150, width: 250, itemAlign: "center", textAlign: "center", margin: "auto", }}>
<CircularProgress /> <CircularProgress />
<Typography variant="body1" style={{marginTop: 20, }}> <Typography variant="body1" style={{marginTop: 20, }}>
Loading Form Details... Loading Details...
</Typography> </Typography>
</div> </div>
: :
<div> <div>
{workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ? {workflowQuestion !== "" || (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) ?
<div style={{marginBottom: 20, }}> <div style={{marginBottom: 20, }}>
<Markdown <Markdown
components={{ components={{
@@ -1342,13 +1437,13 @@ const RunWorkflow = (defaultprops) => {
}} }}
rehypePlugins={[rehypeRaw]} rehypePlugins={[rehypeRaw]}
> >
{workflowQuestion !== "" ? workflowQuestion : realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow.form_control.input_markdown} {workflowQuestion !== "" ? workflowQuestion : realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow?.form_control?.input_markdown}
</Markdown> </Markdown>
</div> </div>
: null} : null}
<form onSubmit={(e) => {onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}> <form onSubmit={(e) => {onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}>
{workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ? null : {workflowQuestion !== "" || (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) ? null :
<div> <div>
{/* {/*
<img <img
@@ -1370,10 +1465,12 @@ const RunWorkflow = (defaultprops) => {
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}> <Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
{organization} {organization}
</Typography> </Typography>
<Divider style={{marginTop: 20, marginBottom: 20, }}/> {organization?.length > 0 &&
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
}
{disabledButtons && message.length > 0 ? null : {disabledButtons && message.length > 0 ? null :
<Typography color="textSecondary" style={{textAlign: "center", }}> <Typography color="textSecondary" style={{textAlign: "center", marginTop: 15, }}>
{message} {message}
</Typography> </Typography>
} }
@@ -1412,6 +1509,11 @@ const RunWorkflow = (defaultprops) => {
executionArgument[multiChoiceOptions[0]] = multiChoiceOptions[1] executionArgument[multiChoiceOptions[0]] = multiChoiceOptions[1]
} }
const parsedLabel = question?.value?.startsWith("question_") ?
""
:
question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)
return ( return (
<div style={{marginBottom: 10}} key={index}> <div style={{marginBottom: 10}} key={index}>
@@ -1457,7 +1559,7 @@ const RunWorkflow = (defaultprops) => {
backgroundColor: theme.palette.inputColor, backgroundColor: theme.palette.inputColor,
marginTop: 5, marginTop: 5,
}} }}
label={question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)} label={parsedLabel}
required required
disabled={disabledButtons} disabled={disabledButtons}
@@ -1542,7 +1644,7 @@ const RunWorkflow = (defaultprops) => {
: :
<Fade in={true} timeout={2500}> <Fade in={true} timeout={2500}>
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}> <Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
{disabledButtons ? "Already answered. You may close this window." : ""} {disabledButtons ? "Question answered. You may close this window." : ""}
</Typography> </Typography>
</Fade> </Fade>
} }
@@ -1565,10 +1667,13 @@ const RunWorkflow = (defaultprops) => {
textTransform: "none", textTransform: "none",
}} }}
onClick={() => { onClick={() => {
setButtonClicked("FINISHED") // Timeout 2500 just in case
setExecutionData({ setTimeout(() => {
status: "FINISHED", setButtonClicked("FINISHED")
}) setExecutionData({
status: "FINISHED",
})
}, 2500)
onSubmit(null, execution_id, authorization, true) onSubmit(null, execution_id, authorization, true)
}}> }}>
@@ -1586,16 +1691,24 @@ const RunWorkflow = (defaultprops) => {
flex: 1, flex: 1,
textTransform: "none", textTransform: "none",
}} onClick={() => { }} onClick={() => {
setButtonClicked("ABORTED") setTimeout(() => {
setExecutionData({ setButtonClicked("ABORTED")
status: "ABORTED", setExecutionData({
}) status: "ABORTED",
})
}, 2500)
onSubmit(null, execution_id, authorization, false) onSubmit(null, execution_id, authorization, false)
}}> }}>
Stop Stop
</Button> </Button>
</div> </div>
{handleValidateForm(executionArgument) === false && disabledButtons === false ?
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, underline: "1px solid grey", }}>
All required questions have not been answered yet.
</Typography>
: null}
</span> </span>
: :
<div style={{display: "flex", marginTop: "15px"}}> <div style={{display: "flex", marginTop: "15px"}}>
+2 -2
View File
@@ -477,7 +477,7 @@ export const HandleJsonCopy = (base, copy, base_node_name) => {
//var newitem = JSON.parse(base); //var newitem = JSON.parse(base);
var newitem = validateJson(base).result var newitem = validateJson(base).result
var to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_"); var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
for (let copykey in copy.namespace) { for (let copykey in copy.namespace) {
if (copy.namespace[copykey].includes("Results for")) { if (copy.namespace[copykey].includes("Results for")) {
continue; continue;
@@ -742,7 +742,7 @@ const DropzoneWrapper = memo(({ onDrop, WorkflowView }) => {
const Workflows = (props) => { const Workflows = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props; const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
document.title = "Shuffle - Workflows"; document.title = "Workflows - Shuffle";
let navigate = useNavigate(); let navigate = useNavigate();
const classes = useStyles(theme) const classes = useStyles(theme)
+443 -330
View File
@@ -4,16 +4,6 @@ import { useLocation, useNavigate, Link } from "react-router-dom";
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import { getTheme } from "../theme.jsx"; import { getTheme } from "../theme.jsx";
// Material UI Icons
import Add from '@mui/icons-material/Add';
import Search from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import QueryStatsIcon from '@mui/icons-material/QueryStats';
import GridOnIcon from '@mui/icons-material/GridOn';
import ListIcon from '@mui/icons-material/List';
import PublishIcon from '@mui/icons-material/Publish';
import GetAppIcon from '@mui/icons-material/GetApp';
// Material UI & Components // Material UI & Components
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
@@ -67,6 +57,7 @@ import {
// Material UI Icons // Material UI Icons
import { import {
ContentCopy as ContentCopyIcon,
Close as CloseIcon, Close as CloseIcon,
Compare as CompareIcon, Compare as CompareIcon,
Maximize as MaximizeIcon, Maximize as MaximizeIcon,
@@ -105,6 +96,12 @@ import {
AutoAwesome as AutoAwesomeIcon, AutoAwesome as AutoAwesomeIcon,
BarChart as BarChartIcon, BarChart as BarChartIcon,
Lock as LockIcon, Lock as LockIcon,
Clear as ClearIcon,
QueryStats as QueryStatsIcon,
GridOn as GridOnIcon,
List as ListIcon,
Publish as PublishIcon,
GetApp as GetAppIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
// Additional Components // Additional Components
@@ -209,10 +206,10 @@ export const GetIconInfo = (action) => {
key: "compare", key: "compare",
values: ["compare", "convert", "to", "filter", "translate", "parse"], values: ["compare", "convert", "to", "filter", "translate", "parse"],
}, },
{ key: "assets", values: ["cmdb", "assets", "asset", "cmdb", "inventory", "host", "hosts", "device", "devices"] }, { key: "assets", values: ["cmdb", "assets", "asset", "cmdb", "inventory", "host", "hosts", "device", "devices", "app",] },
{ key: "close", values: ["close", "stop", "cancel", "block"] }, { key: "close", values: ["close", "stop", "cancel", "block"] },
{ key: "communication", values: ["communication", "comms", "email", "mail",] }, { key: "communication", values: ["communication", "comms", "email", "mail",] },
{ key: "eradication", values: ["eradication", "edr", "xdr"] }, { key: "eradication", values: ["eradication", "edr", "xdr", "sigma", "yara",] },
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] }, { key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] },
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] }, { key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] },
{ key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] }, { key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
@@ -235,6 +232,7 @@ export const GetIconInfo = (action) => {
values: [ values: [
"api", "api",
"password", "password",
"passwd",
"protect", "protect",
], ],
} }
@@ -835,7 +833,9 @@ const Workflows2 = (props) => {
setCurrTab(1); setCurrTab(1);
} else if (tabParam === 'all_workflows' && currTab !== 2) { } else if (tabParam === 'all_workflows' && currTab !== 2) {
setCurrTab(2); setCurrTab(2);
} } else if (tabParam === 'background_processes' && currTab !== 4) {
setCurrTab(4);
}
} }
}, [location.search]); }, [location.search]);
@@ -853,10 +853,15 @@ const Workflows2 = (props) => {
1: 'my_workflows', 1: 'my_workflows',
2: 'all_workflows', 2: 'all_workflows',
3: 'backup_apps', 3: 'backup_apps',
4: 'background_processes',
}; };
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
queryParams.set('tab', tabMapping[newValue]); queryParams.set('tab', tabMapping[newValue]);
if (newValue === 4) {
setShowExecutionStats(true)
setView("grid")
}
navigate(`${location.pathname}?${queryParams.toString()}`); navigate(`${location.pathname}?${queryParams.toString()}`);
}; };
@@ -1553,7 +1558,7 @@ const Workflows2 = (props) => {
sx: { sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius, borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border, border: theme?.palette?.DialogStyle?.border,
minWidth: '440px', minWidth: 440,
fontFamily: theme?.typography?.fontFamily, fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000, zIndex: 1000,
@@ -1566,11 +1571,11 @@ const Workflows2 = (props) => {
} }
}} }}
> >
<DialogTitle> <DialogTitle style={{padding: 50, }}>
<div style={{ textAlign: "center", color: theme.palette.DialogStyle?.color }}> <div style={{ textAlign: "center", color: theme.palette.DialogStyle?.color }}>
Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId)?.name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}? <div /> Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId)?.name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}? <div />
Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working.
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent <DialogContent
@@ -1819,6 +1824,7 @@ const Workflows2 = (props) => {
credentials: "include", credentials: "include",
}) })
.then((response) => { .then((response) => {
setIsLoadingWorkflow(false)
if (response.status !== 200) { if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status); console.log("Status not 200 for workflows :O!: ", response.status);
@@ -1956,6 +1962,7 @@ const Workflows2 = (props) => {
} }
}) })
.catch((error) => { .catch((error) => {
setIsLoadingWorkflow(false)
toast(error.toString()); toast(error.toString());
}); });
} }
@@ -2949,6 +2956,8 @@ const Workflows2 = (props) => {
triggerfound = true triggerfound = true
image = wfTriggers[0].large_image image = wfTriggers[0].large_image
trigger.status = trigger?.status?.toLowerCase()
relevantTrigger = trigger relevantTrigger = trigger
if (trigger?.status === "running") { if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}` imageStyle.border = `3px solid ${green}`
@@ -2962,6 +2971,8 @@ const Workflows2 = (props) => {
triggerfound = true triggerfound = true
image = wfTriggers[1].large_image image = wfTriggers[1].large_image
trigger.status = trigger?.status?.toLowerCase()
relevantTrigger = trigger relevantTrigger = trigger
if (trigger?.status === "running") { if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}` imageStyle.border = `3px solid ${green}`
@@ -3034,10 +3045,11 @@ const Workflows2 = (props) => {
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id) const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
return ( return (
<div style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily }}> <div
id={`workflowbox-${data.id}`}
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily }}>
<Paper square style={paperAppStyle}> <Paper square style={paperAppStyle}>
{selectedCategory !== "" ? {selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom"> <Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
<div <div
@@ -3058,7 +3070,7 @@ const Workflows2 = (props) => {
}} }}
/> />
</Tooltip> </Tooltip>
: null} : null}
<Grid <Grid
item item
@@ -3067,7 +3079,6 @@ const Workflows2 = (props) => {
<Grid item style={{ display: "flex", maxHeight: 34 }}> <Grid item style={{ display: "flex", maxHeight: 34 }}>
{currTab === 2 ? null : {currTab === 2 ? null :
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom"> <Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
<div <div
style={{ cursor: "" }} style={{ cursor: "" }}
onClick={() => { onClick={() => {
@@ -3183,6 +3194,7 @@ const Workflows2 = (props) => {
</Typography> </Typography>
</Tooltip> </Tooltip>
</Grid> </Grid>
<Grid item style={workflowActionStyle}> <Grid item style={workflowActionStyle}>
{appGroup.length > 0 ? {appGroup.length > 0 ?
<div style={{ display: "flex", marginTop: 8, }}> <div style={{ display: "flex", marginTop: 8, }}>
@@ -3437,7 +3449,7 @@ const Workflows2 = (props) => {
</Grid> </Grid>
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 && {showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
<div style={{ margin: "40px 10px 0px 10px", paddingTop: 0, borderTop: "1px solid rgba(255,255,255,0.3)", }}> <div style={{ margin: "40px 10px 0px 10px", paddingTop: 0, borderTop: `1px solid ${theme.palette.text.secondary}`, zoom: 1.4 }}>
<LineChartWrapper <LineChartWrapper
inputname={""} inputname={""}
keys={foundTimeline?.timeline} keys={foundTimeline?.timeline}
@@ -4974,331 +4986,334 @@ const Workflows2 = (props) => {
</div> </div>
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, marginBottom: 20, paddingRight: 25, minHeight: 47 }}> {currTab === 4 ? null :
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, marginBottom: 20, paddingRight: 25, minHeight: 47 }}>
{currTab === 2 ? ( {currTab === 2 ? (
<CustomSearchBox <CustomSearchBox
searchQuery={searchQuery} searchQuery={searchQuery}
setSearchQuery={setSearchQuery} setSearchQuery={setSearchQuery}
/> />
) : ( ) :
<MuiChipsInput (
style={{ <MuiChipsInput
width: "25%", style={{
maxWidth: "25%", width: "25%",
minWidth: "25%", maxWidth: "25%",
height: 43, minWidth: "25%",
maxHeight: "fit-content", height: 43,
backgroundColor: theme.palette.textFieldStyle.backgroundColor, maxHeight: "fit-content",
zIndex: 1000, backgroundColor: theme.palette.textFieldStyle.backgroundColor,
color: theme.palette.textFieldStyle.color zIndex: 1000,
}} color: theme.palette.textFieldStyle.color
disabled={currTab === 2} }}
InputProps={{ disabled={currTab === 2}
style: { InputProps={{
height: "fit-content", style: {
maxHeight: "fit-content", height: "fit-content",
backgroundColor: theme.palette.textFieldStyle.backgroundColor, maxHeight: "fit-content",
color: theme.palette.textFieldStyle.color backgroundColor: theme.palette.textFieldStyle.backgroundColor,
}, color: theme.palette.textFieldStyle.color
placeholder: "Filter Workflows", },
// endAdornment: ( placeholder: "Filter Workflows",
// <InputAdornment position="end"> // endAdornment: (
// <SearchIcon style={{ color: 'white', paddingRight: 5 }} /> // <InputAdornment position="end">
// </InputAdornment> // <SearchIcon style={{ color: 'white', paddingRight: 5 }} />
// ), // </InputAdornment>
onKeyDown: (e) => { // ),
// Prevent default behavior for Enter and Backspace onKeyDown: (e) => {
if (e.key === 'Enter' || e.key === 'Backspace') { // Prevent default behavior for Enter and Backspace
e.preventDefault(); if (e.key === 'Enter' || e.key === 'Backspace') {
e.stopPropagation(); e.preventDefault();
e.target.focus(); e.stopPropagation();
} e.target.focus();
}, }
}} },
clearInputOnBlur={false} }}
sx={{ clearInputOnBlur={false}
// Container styling sx={{
'& .MuiOutlinedInput-root': { // Container styling
height: "fit-content", '& .MuiOutlinedInput-root': {
borderRadius: '4px', height: "fit-content",
color: theme.palette.textFieldStyle.color, borderRadius: '4px',
backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.textFieldStyle.color,
'& fieldset': { backgroundColor: theme.palette.textFieldStyle.backgroundColor,
borderColor: 'rgba(255, 255, 255, 0.23)', '& fieldset': {
}, borderColor: 'rgba(255, 255, 255, 0.23)',
'&:hover fieldset': { },
borderColor: 'rgba(255, 255, 255, 0.4)', '&:hover fieldset': {
}, borderColor: 'rgba(255, 255, 255, 0.4)',
}, },
},
// Adjust chip container to center vertically // Adjust chip container to center vertically
'& .MuiInputBase-root': { '& .MuiInputBase-root': {
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '4px', gap: '4px',
fontSize: 18, fontSize: 18,
padding: '4px 8px', padding: '4px 8px',
alignItems: 'center', alignItems: 'center',
height: "fit-content", // Match height height: "fit-content", // Match height
backgroundColor: theme.palette.textFieldStyle.backgroundColor, backgroundColor: theme.palette.textFieldStyle.backgroundColor,
color: theme.palette.textFieldStyle.color color: theme.palette.textFieldStyle.color
}, },
// Rest of the styling remains the same... // Rest of the styling remains the same...
}} }}
value={filters} value={filters}
onChange={(chips) => { onChange={(chips) => {
setFilters(chips); setFilters(chips);
const remainingCategories = chips.map(chip => { const remainingCategories = chips.map(chip => {
const match = chip.match(/\d+\.\s+(\w+)/i); const match = chip.match(/\d+\.\s+(\w+)/i);
return match ? match[1] : chip; return match ? match[1] : chip;
}).filter(category => { }).filter(category => {
return usecases.some(usecase => return usecases.some(usecase =>
usecase.name.toLowerCase().includes(category.toLowerCase()) usecase.name.toLowerCase().includes(category.toLowerCase())
); );
}); });
setSelectedCategory(remainingCategories); setSelectedCategory(remainingCategories);
findWorkflow(chips); findWorkflow(chips);
}} }}
//onAdd={(chip) => { //onAdd={(chip) => {
// console.log("ADd: ", chip); // console.log("ADd: ", chip);
// addFilter(chip); // addFilter(chip);
//}} //}}
//onDelete={(_, index) => { //onDelete={(_, index) => {
// console.log("Remove: ", index); // console.log("Remove: ", index);
// removeFilter(index); // removeFilter(index);
//}} //}}
/> />
)} )}
{ {
currTab !== 2 && ( currTab !== 2 && (
<Select <Select
fullWidth fullWidth
variant="outlined" variant="outlined"
value={selectedCategory} value={selectedCategory}
onChange={handleCategoryChange} onChange={handleCategoryChange}
displayEmpty displayEmpty
disabled={currTab === 2} disabled={currTab === 2}
multiple multiple
style={{ style={{
width: "25%", width: "25%",
minWidth: "25%", minWidth: "25%",
maxWidth: "25%", maxWidth: "25%",
height: 47, height: 47,
borderRadius: 4, borderRadius: 4,
fontSize: 18, fontSize: 18,
backgroundColor: theme.palette.textFieldStyle.backgroundColor, backgroundColor: theme.palette.textFieldStyle.backgroundColor,
fontFamily: theme.typography?.fontFamily, fontFamily: theme.typography?.fontFamily,
}} }}
sx={{ sx={{
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
'& fieldset': { '& fieldset': {
borderColor: 'rgba(255, 255, 255, 0.23)', borderColor: 'rgba(255, 255, 255, 0.23)',
}, },
}, },
}} }}
renderValue={(selected) => selected.length ? selected.join(', ') : 'All Categories'} renderValue={(selected) => selected.length ? selected.join(', ') : 'All Categories'}
> >
<MenuItem disabled value=""> <MenuItem disabled value="">
All Categories All Categories
</MenuItem> </MenuItem>
{usecases.map((usecase, index) => { {usecases.map((usecase, index) => {
if (usecase?.name === "5. Verify") { if (usecase?.name === "5. Verify") {
return null; return null;
} }
const percentDone = usecase.matches.length > 0 ? parseInt(usecase.matches.length / usecase.list.length * 100) : 0 const percentDone = usecase.matches.length > 0 ? parseInt(usecase.matches.length / usecase.list.length * 100) : 0
if (percentDone === 0) { if (percentDone === 0) {
usecase = findMatches(usecase, workflows) usecase = findMatches(usecase, workflows)
} }
const category = usecase?.name.split(" ")[1] const category = usecase?.name.split(" ")[1]
return ( return (
<MenuItem <MenuItem
value={category} value={category}
onClick={() => { onClick={() => {
if (!filters.includes(usecase?.name.toLowerCase())) { if (!filters.includes(usecase?.name.toLowerCase())) {
addFilter(usecase.name) addFilter(usecase.name)
} else { } else {
removeFilter(filters.indexOf(usecase?.name.toLowerCase())) removeFilter(filters.indexOf(usecase?.name.toLowerCase()))
} }
}} }}
sx={{ sx={{
padding: "12px 16px", padding: "12px 16px",
borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)", borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)",
"&:hover": { "&:hover": {
backgroundColor: "rgba(255,255,255,0.1)" backgroundColor: "rgba(255,255,255,0.1)"
}, },
}} }}
> >
<div style={{ <div style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
gap: "12px" gap: "12px"
}}> }}>
<Checkbox <Checkbox
checked={selectedCategory.includes(category)} checked={selectedCategory.includes(category)}
style={{ style={{
padding: 0, padding: 0,
marginRight: 8, marginRight: 8,
color: theme.palette.textFieldStyle.color, color: theme.palette.textFieldStyle.color,
}} }}
/> />
<div style={{ <div style={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
width: "100%" width: "100%"
}}> }}>
<Typography <Typography
variant="body1" variant="body1"
style={{ style={{
color: theme.palette.textFieldStyle.color, color: theme.palette.textFieldStyle.color,
fontWeight: selectedCategory.includes(category) ? 500 : 400 fontWeight: selectedCategory.includes(category) ? 500 : 400
}} }}
> >
{category} {category}
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
style={{ style={{
color: theme.palette.textFieldStyle.color, color: theme.palette.textFieldStyle.color,
backgroundColor: theme.palette.textFieldStyle.backgroundColor, backgroundColor: theme.palette.textFieldStyle.backgroundColor,
padding: "2px 8px", padding: "2px 8px",
borderRadius: "12px", borderRadius: "12px",
fontSize: "0.75rem" fontSize: "0.75rem"
}} }}
> >
{usecase?.matches.length}/{usecase?.list.length} {usecase?.matches.length}/{usecase?.list.length}
</Typography> </Typography>
</div> </div>
</div> </div>
</MenuItem> </MenuItem>
) )
})} })}
</Select> </Select>
) )
} }
{ {
currTab === 2 && ( currTab === 2 && (
<CustomCategoryDropdown attribute="usecase_ids" limit={20} /> <CustomCategoryDropdown attribute="usecase_ids" limit={20} />
) )
} }
<div style={{ width: "50%", minWidth: "50%", maxWidth: "50%", height: 47, display: "flex", gap: 5 }}> <div style={{ width: "50%", minWidth: "50%", maxWidth: "50%", height: 47, display: "flex", gap: 5 }}>
<div style={{ <div style={{
display: "flex", display: "flex",
height: "100%", height: "100%",
justifyContent: "space-around", justifyContent: "space-around",
flex: 0.7, flex: 0.7,
paddingLeft: 1, paddingLeft: 1,
paddingRight: 1, paddingRight: 1,
gap: 4 gap: 4
}}> }}>
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top"> <Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}} style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}}
onClick={() => { onClick={() => {
const newView = !showExecutionStats const newView = !showExecutionStats
localStorage.setItem("showExecutionStats", newView) localStorage.setItem("showExecutionStats", newView)
setShowExecutionStats(!showExecutionStats) setShowExecutionStats(!showExecutionStats)
}} }}
disabled={currTab === 2} disabled={currTab === 2}
> >
<BarChartIcon /> <BarChartIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Explore Workflow Runs (debugger)" placement="top"> <Tooltip title="Explore Workflow Runs (debugger)" placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle} style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => navigate("/workflows/debug")} onClick={() => navigate("/workflows/debug")}
disabled={currTab === 2} disabled={currTab === 2}
> >
<QueryStatsIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} /> <QueryStatsIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top"> <Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle} style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => { onClick={() => {
const newView = view === "grid" ? "list" : "grid"; const newView = view === "grid" ? "list" : "grid";
localStorage.setItem("workflowView", newView); localStorage.setItem("workflowView", newView);
setView(newView); setView(newView);
if (view === "grid") { if (view === "grid") {
setCurrTab(0) setCurrTab(0)
} }
}} }}
disabled={currTab === 2} disabled={currTab === 2}
> >
{view === "grid" ? {view === "grid" ?
<ListIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} /> : <ListIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} /> :
<GridOnIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} /> <GridOnIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
} }
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Import workflows" placement="top"> <Tooltip title="Import workflows" placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle} style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => upload.click()} onClick={() => upload.click()}
disabled={currTab === 2} disabled={currTab === 2}
> >
{submitLoading ? {submitLoading ?
<CircularProgress color="secondary" /> : <CircularProgress color="secondary" /> :
<PublishIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} /> <PublishIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
} }
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<input <input
hidden hidden
type="file" type="file"
multiple="multiple" multiple="multiple"
ref={(ref) => (upload = ref)} ref={(ref) => (upload = ref)}
onChange={importFiles} onChange={importFiles}
/> />
<Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top"> <Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top">
<IconButton <IconButton
style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }} style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
disabled={isCloud || currTab === 2} disabled={isCloud || currTab === 2}
onClick={() => exportAllWorkflows(workflows)} onClick={() => exportAllWorkflows(workflows)}
> >
<GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} /> <GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleCreateWorkflow} onClick={handleCreateWorkflow}
id="create_workflow_button" id="create_workflow_button"
style={{ style={{
borderRadius: 4, borderRadius: 4,
flex: 0.8, flex: 0.8,
textTransform: 'none', textTransform: 'none',
fontFamily: theme.typography?.fontFamily, fontFamily: theme.typography?.fontFamily,
fontSize: 16, fontSize: 16,
fontWeight: 500 fontWeight: 500
}} }}
startIcon={<Add/>} startIcon={<AddIcon />}
> >
Create Workflow Create Workflow
</Button> </Button>
</div> </div>
</div>
}
</div>
<div style={{ <div style={{
width: "100%", width: "100%",
position: "relative", position: "relative",
@@ -5310,8 +5325,106 @@ const Workflows2 = (props) => {
) : ( ) : (
view === "grid" && currTab !== 2 ? ( view === "grid" && currTab !== 2 ? (
<> <>
<div style={{ {currTab === 4 && backgroundWorkflows.map((data, index) => {
marginTop: 16, if (data.triggers.length === 0) {
return null
}
var foundWebhook = ""
var foundtrigger = {}
for (var triggerKey in data.triggers) {
if (data.triggers[triggerKey].trigger_type === "WEBHOOK") {
foundWebhook = `${globalUrl}/api/v1/hooks/webhook_${data.triggers[triggerKey].id}`
foundtrigger = data.triggers[triggerKey]
break
}
}
if (foundWebhook === "") {
return null
}
var webhookName = ``
if (data?.name?.toLowerCase().includes("ingest tickets")) {
webhookName = "Send your Tickets, Alerts, Cases and Detections here. This will ingest them into Shuffle."
}
return (
<div style={{padding: 50, }}
onMouseEnter={() => {
// Find the relevant workflow paper and highlight it
const foundElement = document.getElementById(`workflowbox-${data.id}`)
if (foundElement) {
foundElement.style.border = `3px solid ${theme.palette.primary.main}`
}
}}
onMouseLeave={() => {
const foundElement = document.getElementById(`workflowbox-${data.id}`)
if (foundElement) {
foundElement.style.border = null
}
}}
>
<Typography variant="body1" style={{ marginBottom: 10, fontFamily: theme.typography?.fontFamily, display: "flex", alignItems: "center", gap: 10 }}>
{webhookName}
</Typography>
<TextField
value={foundWebhook}
readOnly
fullWidth
disabled
// Add start adornment with webhook icon
InputProps={{
startAdornment: (
<InputAdornment position="start">
<img
alt="webhook"
src={wfTriggers[0].large_image}
style={{
width: 40,
height: 40,
marginRight: 20,
border: foundtrigger.status === "Running" || foundtrigger.status === "running" ? `2px solid ${green}` : `2px solid ${red}`,
borderRadius: theme.palette.borderRadius,
}}
/>
<Tooltip title={"Copy to clipboard"} placement="top">
<IconButton
onClick={() => {
if (navigator.clipboard === undefined) {
toast("Your browser doesn't support clipboard copying, please copy manually.", { type: "error" });
} else {
navigator.clipboard.writeText(foundWebhook);
}
}}
style={{
color: theme.palette.textFieldStyle.color,
backgroundColor: theme.palette.platformColor,
marginRight: 10,
borderRadius: 4,
}}
id="copy_webhook_url_button"
>
<ContentCopyIcon style={{ color: theme.palette.textFieldStyle.color }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
style: {
color: theme.palette.textFieldStyle.color,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
}
}}
/>
</div>
)
})}
<div style={{
marginTop: 32,
width: "100%", width: "100%",
display: "grid", display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(365px, 1fr))", gridTemplateColumns: "repeat(auto-fill, minmax(365px, 1fr))",