Sync + Added dashboard for stats API
This commit is contained in:
@@ -992,16 +992,16 @@ const AppAuthTab = memo((props) => {
|
||||
<Typography variant='h5' style={{ marginBottom: 8, marginTop: 0, }}>App Authentication</Typography>
|
||||
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', }}>
|
||||
<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>
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1787,7 +1787,7 @@ const Hits = ({
|
||||
|
||||
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
|
||||
setAuthenticationType({
|
||||
type: "",
|
||||
type: "",
|
||||
})
|
||||
|
||||
selectedAppData.authentication = {
|
||||
@@ -1955,6 +1955,7 @@ const Hits = ({
|
||||
if (data === undefined || data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
|
||||
if (filteredData.length === 0) {
|
||||
setAppAuthentication([]);
|
||||
@@ -1965,7 +1966,7 @@ const Hits = ({
|
||||
}
|
||||
};
|
||||
|
||||
const HandleAppAuthentication = ()=>{
|
||||
const HandleAppAuthentication = () => {
|
||||
|
||||
const url = `${globalUrl}/api/v1/apps/authentication`;
|
||||
|
||||
|
||||
@@ -21,8 +21,9 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
||||
import CreateIcon from '@mui/icons-material/Create'
|
||||
import { toast } from 'react-toastify'
|
||||
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 [generateAppModal, setGenerateAppModal] = useState(false)
|
||||
const [openApi, setOpenApi] = useState("")
|
||||
@@ -35,6 +36,16 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
||||
const navigate = useNavigate()
|
||||
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
|
||||
const AppCreateButton = ({ text, func, icon }) => {
|
||||
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={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -504,12 +516,15 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
||||
<Typography sx={{ color: theme.palette.text.primary, fontSize: '16px' }}>
|
||||
Paste in the URI for the OpenAPI or find out
|
||||
</Typography>
|
||||
<Link style={{
|
||||
<Link
|
||||
to="https://shuffler.io/docs/apps#getting-started"
|
||||
style={{
|
||||
color: '#ff8544',
|
||||
textDecoration: 'none',
|
||||
textDecoration: 'underline',
|
||||
fontSize: '16px',
|
||||
fontFamily: theme?.typography?.fontFamily
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
textUnderlineOffset: "3px",
|
||||
}}>
|
||||
How to find URI for openAPI?
|
||||
</Link>
|
||||
@@ -568,31 +583,54 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
||||
Must point to a version 2 or 3 OpenAPI specification.
|
||||
</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
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
<div style={{
|
||||
width: '100%',
|
||||
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()}
|
||||
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
|
||||
</Button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<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
|
||||
hidden
|
||||
@@ -638,6 +676,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
|
||||
Continue
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dropzone>
|
||||
</Dialog>
|
||||
|
||||
{/* Generate App Modal */}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ClearRefinements,
|
||||
connectStateResults
|
||||
} from "react-instantsearch-dom";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
|
||||
import aa from "search-insights";
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -160,6 +161,8 @@ const AppGrid = (props) => {
|
||||
refine(searchQuery.trim());
|
||||
};
|
||||
|
||||
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
@@ -229,9 +232,10 @@ const AppGrid = (props) => {
|
||||
placeholder="Search more than 2500 Apps"
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.currentTarget.value);
|
||||
const value = event.currentTarget.value;
|
||||
setSearchQuery(value);
|
||||
removeQuery("q");
|
||||
refine(event.currentTarget.value);
|
||||
debouncedRefine(value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
|
||||
@@ -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 classNames from "classnames";
|
||||
@@ -73,6 +73,7 @@ const AppStats = (defaultprops) => {
|
||||
const [resultRows, setResultRows] = useState([])
|
||||
const [resultLoading, setResultLoading] = useState(true)
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const [onpremAppRuns, setOnpremAppRuns] = useState(0)
|
||||
const theme = getTheme(themeMode, brandColor)
|
||||
|
||||
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(() => {
|
||||
if (statistics && statistics?.org_id?.length > 0) {
|
||||
handleDataSetting(statistics, "day")
|
||||
}
|
||||
}, [statistics])
|
||||
|
||||
useEffect(() => {
|
||||
setStartTime("")
|
||||
setEndTime("")
|
||||
}, [currentTab])
|
||||
|
||||
const getWorkflowStats = async (workflow, startTime, endTime) => {
|
||||
|
||||
if (workflow.id === undefined || workflow.id === null || workflow.id === "") {
|
||||
@@ -227,7 +372,7 @@ const AppStats = (defaultprops) => {
|
||||
}
|
||||
|
||||
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)
|
||||
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
|
||||
return
|
||||
@@ -356,17 +501,55 @@ const AppStats = (defaultprops) => {
|
||||
workflowexecutions += item["workflow_executions"]
|
||||
appexecutions += item["app_executions"]
|
||||
|
||||
if (currentTab === 0) {
|
||||
if (currentTab === 0 || currentTab === 3) {
|
||||
appexecutions += (item["child_app_executions"] ?? 0)
|
||||
}
|
||||
|
||||
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_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
|
||||
if (isCloud) {
|
||||
// Exclude includedExecutions*month
|
||||
@@ -380,11 +563,11 @@ const AppStats = (defaultprops) => {
|
||||
handleDataSetting(tmpstats, "day")
|
||||
// if we have done monthly reset than only show monthly app runs as current month app run
|
||||
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"])
|
||||
}
|
||||
|
||||
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"])
|
||||
}
|
||||
|
||||
@@ -397,7 +580,7 @@ const AppStats = (defaultprops) => {
|
||||
loadWorkflowStats(foundWorkflows, startTime, endTime)
|
||||
}
|
||||
|
||||
}, [statistics, startTime, endTime])
|
||||
}, [statistics, startTime, endTime, syncStats, currentTab, handleDataSetting])
|
||||
|
||||
const handleStartTimeChange = (date) => {
|
||||
setStartTime(date)
|
||||
@@ -407,142 +590,7 @@ const AppStats = (defaultprops) => {
|
||||
setEndTime(date)
|
||||
}
|
||||
|
||||
const handleDataSetting = (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"]),
|
||||
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)
|
||||
}
|
||||
console.log("sync stats: ", syncStats, statistics)
|
||||
|
||||
const paperStyle = {
|
||||
textAlign: "center",
|
||||
@@ -708,22 +756,26 @@ const AppStats = (defaultprops) => {
|
||||
</Tooltip>
|
||||
} */}
|
||||
|
||||
{syncStats === true ? null :
|
||||
{/* {syncStats === true ? null : */}
|
||||
<Tooltip title={
|
||||
<Typography variant="body1" style={{padding: 10, }}>
|
||||
App runs in the selected period
|
||||
</Typography>
|
||||
}>
|
||||
<Box sx={paperStyle}>
|
||||
{syncStats === true ?
|
||||
<Typography variant="h4">
|
||||
{onpremAppRuns}
|
||||
</Typography>:
|
||||
<Typography variant="h4">
|
||||
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
|
||||
</Typography>
|
||||
</Typography>}
|
||||
<Typography variant="h6">
|
||||
App Runs
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
{/* } */}
|
||||
|
||||
{syncStats === true || currentTab === 0 ? null :
|
||||
<Tooltip title={
|
||||
|
||||
@@ -147,6 +147,20 @@ const CacheView = memo((props) => {
|
||||
const [showSettingsMenu, setShowSettingsMenu] = 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 = "";
|
||||
const defaultAutomation = [
|
||||
{
|
||||
@@ -299,7 +313,16 @@ const CacheView = memo((props) => {
|
||||
useEffect(() => {
|
||||
getWorkflows()
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
|
||||
|
||||
if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
|
||||
@@ -458,7 +480,12 @@ const CacheView = memo((props) => {
|
||||
}
|
||||
}
|
||||
} 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) => {
|
||||
@@ -513,8 +540,8 @@ const CacheView = memo((props) => {
|
||||
category: selectedCategory,
|
||||
}
|
||||
|
||||
if (dataValue?.category !== "" && dataValue?.category !== "default") {
|
||||
entry.category = dataValue.category.replaceAll(" ", "_");
|
||||
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
|
||||
entry.category = dataValue?.category?.replaceAll(" ", "_");
|
||||
|
||||
}
|
||||
|
||||
@@ -1513,7 +1540,7 @@ const CacheView = memo((props) => {
|
||||
name={null}
|
||||
/>
|
||||
:
|
||||
<Typography variant="body2" style={{maxHeight: 200, overflow: "hidden", }}>
|
||||
<Typography variant="body2" style={{maxWidth: 500, maxHeight: 200, overflow: "auto", }}>
|
||||
{data.value}
|
||||
</Typography>
|
||||
}
|
||||
@@ -1712,7 +1739,7 @@ const CacheView = memo((props) => {
|
||||
<span>
|
||||
<IconButton
|
||||
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) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -1911,7 +1938,7 @@ const CacheView = memo((props) => {
|
||||
|
||||
{selectedCategory === "protected" ?
|
||||
<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>
|
||||
: null}
|
||||
|
||||
@@ -2051,7 +2078,7 @@ const CacheView = memo((props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
|
||||
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
@@ -2077,8 +2104,14 @@ const CacheView = memo((props) => {
|
||||
{renderTextBox && <TextField
|
||||
onKeyPress={(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)
|
||||
|
||||
listOrgCache(orgId, event.target.value, 0, pageSize, 0)
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -2096,6 +2129,7 @@ const CacheView = memo((props) => {
|
||||
paddingTop: 0,
|
||||
},
|
||||
}}
|
||||
id=""
|
||||
color="primary"
|
||||
placeholder="Category name"
|
||||
required
|
||||
@@ -2416,7 +2450,7 @@ const CacheView = memo((props) => {
|
||||
</Typography>
|
||||
|
||||
<Pagination
|
||||
count={Number.parseInt(totalAmount/pageSize*100/2)}
|
||||
count={Number.parseInt(totalAmount/pageSize)}
|
||||
page={page+1}
|
||||
renderItem={(item) => {
|
||||
var disabled = false
|
||||
|
||||
@@ -18,14 +18,21 @@ import {
|
||||
Tooltip,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
Rocket as RocketIcon,
|
||||
FilterAlt as FilterAltIcon,
|
||||
Add as AddIcon,
|
||||
Check as CheckIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import {
|
||||
green,
|
||||
red,
|
||||
} from '../views/AngularWorkflow.jsx'
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
|
||||
@@ -97,6 +104,7 @@ const CollectIngestModal = (props) => {
|
||||
|
||||
const [showAppsearch, setShowAppsearch] = useState(false);
|
||||
const [algoliaOptions, setAlgoliaOptions] = useState([]);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
const appname = type
|
||||
const ingestedAmount = 20
|
||||
@@ -107,7 +115,7 @@ const CollectIngestModal = (props) => {
|
||||
})
|
||||
|
||||
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 foundWorkflow = workflows.find((workflow) => {
|
||||
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 (
|
||||
//<Grid item xs={hovering ? 12 : 5.9}
|
||||
<Grid item xs={12}
|
||||
style={{
|
||||
minHeight: hovering ? 200 : 200,
|
||||
maxHeight: hovering ? "auto" : 140,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
@@ -200,7 +232,7 @@ const CollectIngestModal = (props) => {
|
||||
<div style={{flex: 1, margin: "auto", marginTop: 50, }}>
|
||||
|
||||
<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
|
||||
return (
|
||||
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
|
||||
@@ -214,18 +246,29 @@ const CollectIngestModal = (props) => {
|
||||
)
|
||||
})}
|
||||
|
||||
<Tooltip title="Select Apps" placement="top">
|
||||
<IconButton
|
||||
style={{marginLeft: 10, marginRight: 50, }}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setShowAppsearch(!showAppsearch)
|
||||
}}
|
||||
>
|
||||
<AddIcon style={{color: theme.palette.primary.main, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!generating && appCategory !== undefined && appCategory !== null && appCategory.length > 0 ?
|
||||
<Tooltip title={showAppsearch ? "Done selecting apps" : "Select Apps"} placement="top">
|
||||
<IconButton
|
||||
style={{marginLeft: 10, marginRight: 50, }}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
|
||||
if (showAppsearch === true) {
|
||||
runIngestion()
|
||||
}
|
||||
|
||||
setShowAppsearch(!showAppsearch)
|
||||
}}
|
||||
>
|
||||
{showAppsearch ?
|
||||
<CheckIcon style={{color: green, }} />
|
||||
:
|
||||
<AddIcon style={{color: theme.palette.primary.main, }} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{showAppsearch ?
|
||||
@@ -237,20 +280,42 @@ const CollectIngestModal = (props) => {
|
||||
|
||||
value={selectedApps}
|
||||
onChange={(event, value) => {
|
||||
console.log("New value: ", value)
|
||||
|
||||
setSelectedApps(value)
|
||||
}}
|
||||
|
||||
getOptionLabel={(option) => {
|
||||
const parsedname = option.name.replaceAll("_", " ")
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
|
||||
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
|
||||
{parsedname}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
return parsedname
|
||||
//return (
|
||||
// <div>
|
||||
// <img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
|
||||
// <Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
|
||||
// {parsedname}
|
||||
// </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) => {
|
||||
return (
|
||||
@@ -267,24 +332,9 @@ const CollectIngestModal = (props) => {
|
||||
style={{width: 250, margin: 25, }}
|
||||
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
|
||||
onClick={() => {
|
||||
|
||||
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)
|
||||
}
|
||||
runIngestion()
|
||||
}}
|
||||
disabled={generating}
|
||||
>
|
||||
{foundMatchingWorkflow !== null ?
|
||||
"Re-Create Ingestion"
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const CreatorGrid = props => {
|
||||
@@ -109,6 +110,8 @@ const CreatorGrid = props => {
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
@@ -134,7 +137,7 @@ const CreatorGrid = props => {
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
debouncedRefine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -190,10 +193,10 @@ const CreatorGrid = props => {
|
||||
null
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<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
|
||||
</Typography>
|
||||
</div>
|
||||
<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
|
||||
</Typography>
|
||||
{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,}}>
|
||||
{data.specialized_apps.map((app, index) => {
|
||||
@@ -267,7 +270,7 @@ const CreatorGrid = props => {
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
@@ -285,7 +288,7 @@ const CreatorGrid = props => {
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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;
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ const Detection = (props) => {
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
disabled
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{/* <Button
|
||||
|
||||
@@ -16,11 +16,14 @@ import {
|
||||
import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
FmdGood as FmdGoodIcon,
|
||||
Check as CheckIcon,
|
||||
} from "@mui/icons-material"
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import RunDetectionTest from '../components/RunDetectionTest.jsx';
|
||||
import theme from '../theme.jsx';
|
||||
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
||||
import CollectIngestModal from "../components/CollectIngestModal.jsx";
|
||||
import {
|
||||
green,
|
||||
red,
|
||||
@@ -30,13 +33,12 @@ import {
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||
|
||||
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
|
||||
if (!isDetectionActive) {
|
||||
toast.warn("Connect to siem first for global enable/disable to work");
|
||||
return;
|
||||
}
|
||||
//if (!isDetectionActive) {
|
||||
// toast.warn("Connect first for global enable/disable to work");
|
||||
// return;
|
||||
//}
|
||||
|
||||
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}`;
|
||||
|
||||
fetch(url, {
|
||||
@@ -68,10 +70,117 @@ const DetectionExplorer = (props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [workflow, setWorkflow] = useState({})
|
||||
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
|
||||
const [isDetectionValid, setIsDetectionValid] = useState(false)
|
||||
const [availableDetection, setAvailableDetection] = 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 url = `${globalUrl}/api/v1/workflows/usecases`
|
||||
@@ -133,6 +242,11 @@ const DetectionExplorer = (props) => {
|
||||
}
|
||||
|
||||
const handleConnectClick = () => {
|
||||
|
||||
// NEW way to handle it
|
||||
setShowCollectIngestMenu(true)
|
||||
return
|
||||
|
||||
if (detectionWorkflowId !== "") {
|
||||
console.log("Already have a workflow ID for this detection")
|
||||
//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) {
|
||||
setIsDetectionValid(responseJson.workflow_valid)
|
||||
//setIsDetectionValid(responseJson.workflow_valid)
|
||||
}
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
@@ -238,8 +352,12 @@ const DetectionExplorer = (props) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getApps()
|
||||
getWorkflows()
|
||||
loadUsecases()
|
||||
loadEnvironments()
|
||||
|
||||
handleGetAllTriggers()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -251,18 +369,87 @@ const DetectionExplorer = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
handleConnectClick()
|
||||
console.log("Detection info: ", detectionInfo)
|
||||
//handleConnectClick()
|
||||
}, [detectionInfo])
|
||||
|
||||
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
|
||||
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
rule?.file_name?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
|
||||
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 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 (
|
||||
<Container>
|
||||
|
||||
<CollectIngestModal
|
||||
globalUrl={globalUrl}
|
||||
open={showCollectIngestMenu}
|
||||
setOpen={setShowCollectIngestMenu}
|
||||
|
||||
workflows={workflows}
|
||||
getWorkflows={getWorkflows}
|
||||
|
||||
apps={apps}
|
||||
/>
|
||||
|
||||
<Paper
|
||||
style={{
|
||||
marginTop: 50,
|
||||
@@ -316,8 +503,21 @@ const DetectionExplorer = (props) => {
|
||||
</div>
|
||||
|
||||
: */}
|
||||
<div style={{marginRight: 20, }}>
|
||||
<RunDetectionTest
|
||||
globalUrl={globalUrl}
|
||||
pipelines={pipelines}
|
||||
workflows={workflows}
|
||||
ticketWebhook={ticketWebhook}
|
||||
detectionWorkflowId={detectionWorkflowId}
|
||||
|
||||
changePipelineState={undefined}
|
||||
submitPipelineWrapper={submitPipeline}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
variant={detectionWorkflowId === "" ? "contained" : "outlined"}
|
||||
onClick={() => {
|
||||
handleConnectClick()
|
||||
}}
|
||||
@@ -326,18 +526,26 @@ const DetectionExplorer = (props) => {
|
||||
// Red = workflow exists, validation is false
|
||||
// Green = workflow exists, validation is true
|
||||
// Grey = workflow does not exist
|
||||
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
|
||||
}}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> :
|
||||
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
|
||||
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
|
||||
{loading ?
|
||||
<CircularProgress size={24} />
|
||||
:
|
||||
detectionWorkflowId !== "" ?
|
||||
<span>
|
||||
<CheckIcon style={{color: green, marginRight: 10, top: 5, }} />
|
||||
Connected
|
||||
</span>
|
||||
:
|
||||
`Connect to ${detectionInfo?.category}`
|
||||
}
|
||||
</Button>
|
||||
|
||||
{/**/}
|
||||
|
||||
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
|
||||
<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}} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
@@ -345,7 +553,7 @@ const DetectionExplorer = (props) => {
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
{filteredRules?.length > 0 ?
|
||||
{ruleInfo?.length > 0 ?
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -367,7 +575,9 @@ const DetectionExplorer = (props) => {
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e?.target?.value?.replaceAll(" ", "_")?.toLowerCase())
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
@@ -386,7 +596,7 @@ const DetectionExplorer = (props) => {
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
height: "500px",
|
||||
minHeight: 500,
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
@@ -410,6 +620,7 @@ const DetectionExplorer = (props) => {
|
||||
folderDisabled={folderDisabled}
|
||||
isDetectionActive={isDetectionActive}
|
||||
|
||||
ruleDetails={rule}
|
||||
ruleMapping={ruleMapping}
|
||||
setRuleMapping={setRuleMapping}
|
||||
|
||||
|
||||
@@ -12,16 +12,18 @@ import {
|
||||
FormLabel,
|
||||
} from "@mui/material";
|
||||
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
import LineChartWrapper, { LoadStats } from "../components/LineChartWrapper.jsx";
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { toast } from "react-toastify";
|
||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.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 [fileData, setFileData] = React.useState("");
|
||||
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 isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
|
||||
|
||||
console.log("Rulemapping: ", ruleMapping)
|
||||
useEffect(() => {
|
||||
|
||||
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
||||
//const resp = LoadStats(globalUrl, ruleName)
|
||||
//const resp = LoadStats(globalUrl, "app_executions_test2")
|
||||
const resp = LoadStats(globalUrl, "app_executions_cloud")
|
||||
resp.then((data) => {
|
||||
if (data === undefined) {
|
||||
setFilteredBarchart([])
|
||||
} else {
|
||||
setFilteredBarchart(data)
|
||||
if (key < 10) {
|
||||
console.log("RuleCard Key: ", key, ruleName, file_id, otherProps)
|
||||
}
|
||||
})
|
||||
|
||||
if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) {
|
||||
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping)
|
||||
}
|
||||
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) => {
|
||||
if (data === undefined) {
|
||||
setFilteredBarchart([])
|
||||
} else {
|
||||
setFilteredBarchart(data)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
console.log("Response Value: ", responseValue)
|
||||
|
||||
const handleSwitchChange = (event) => {
|
||||
if (folderDisabled) {
|
||||
toast.warn("Enable the directory to enable individual rules");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTenzirActive) {
|
||||
if (!isDetectionActive) {
|
||||
toast.warn("Connect to the siem first to enable/disable the rule");
|
||||
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 (
|
||||
<Card style={{
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
@@ -116,10 +117,13 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
||||
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' }}>
|
||||
|
||||
<Select
|
||||
{/*
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
@@ -148,6 +152,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
||||
color: "white",
|
||||
height: 40,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginRight: 20,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
@@ -178,6 +183,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
*/}
|
||||
|
||||
|
||||
<Tooltip title="Edit Rule" placement="top">
|
||||
@@ -204,12 +210,47 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
|
||||
|
||||
minHeight: 40,
|
||||
maxHeight: 40,
|
||||
display: "flex",
|
||||
}}>
|
||||
{filteredBarchart === null ? null :
|
||||
<DashboardBarchart
|
||||
timelineData={filteredBarchart}
|
||||
/>
|
||||
<Tooltip title="Refresh stats" placement="top">
|
||||
<IconButton
|
||||
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>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Search as SearchIcon } from '@mui/icons-material';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback.js';
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7");
|
||||
@@ -69,12 +70,22 @@ const DiscordChat = props => {
|
||||
}
|
||||
|
||||
const SearchBox = ({ currentRefinement, refine }) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(currentRefinement || "");
|
||||
}, [currentRefinement]);
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
value={currentRefinement}
|
||||
onChange={(event) => refine(event.currentTarget.value)}
|
||||
value={inputValue}
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
setInputValue(value);
|
||||
debouncedRefine(value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
|
||||
|
||||
|
||||
@@ -97,6 +98,8 @@ const DocsGrid = props => {
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
@@ -122,7 +125,7 @@ const DocsGrid = props => {
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
debouncedRefine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
|
||||
@@ -917,10 +917,10 @@ const EditWorkflow = (props) => {
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" style={{ marginBottom: 5 }}>
|
||||
Generate Workflow from Flowchart
|
||||
Generate Workflow from Flowchart (beta)
|
||||
</Typography>
|
||||
<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 variant="caption" color="textSecondary">
|
||||
PNG, JPG, JPEG • Max 5MB
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Help as HelpIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { toast } from 'react-toastify';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
@@ -53,7 +54,7 @@ const EnvironmentTab = memo((props) => {
|
||||
pipelines: false,
|
||||
proxies: false,
|
||||
})
|
||||
const [installationTab, setInstallationTab] = React.useState(0);
|
||||
const [installationTab, setInstallationTab] = React.useState(1);
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const [listItemExpanded, setListItemExpanded] = React.useState(-1);
|
||||
const [, setUpdate] = React.useState(0);
|
||||
@@ -61,6 +62,7 @@ const EnvironmentTab = memo((props) => {
|
||||
const [selectedEnvironment, setSelectedEnvironment] = React.useState(null);
|
||||
const [selectedSubOrg, setSelectedSubOrg] = React.useState([]);
|
||||
const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined)
|
||||
const [currentEnvQueue, setCurrentEnvQueue] = React.useState([])
|
||||
|
||||
const { themeMode, supportEmail, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
@@ -408,6 +410,11 @@ const EnvironmentTab = memo((props) => {
|
||||
skipPipeline = true
|
||||
}
|
||||
|
||||
var showDetection = false
|
||||
if (commandController.detection === true) {
|
||||
showDetection = true
|
||||
}
|
||||
|
||||
var addProxy = false
|
||||
if (commandController.proxies === true) {
|
||||
addProxy = true
|
||||
@@ -422,12 +429,11 @@ const EnvironmentTab = memo((props) => {
|
||||
-e AUTH="${auth}" \\
|
||||
-e ENVIRONMENT_NAME="${environment.Name}" \\
|
||||
-e ORG="${environment.org_id}" \\
|
||||
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\
|
||||
-e SHUFFLE_SWARM_CONFIG=run \\
|
||||
-e SHUFFLE_LOGS_DISABLED=true \\
|
||||
-e BASE_URL="${newUrl}" \\${addProxy ? `
|
||||
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
|
||||
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
|
||||
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
|
||||
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? `
|
||||
-v /tmp:/tmp \\` : ""}
|
||||
ghcr.io/shuffle/shuffle-orborus:latest
|
||||
`)
|
||||
} 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 data = {
|
||||
action: "suborg_distribute",
|
||||
@@ -881,14 +957,14 @@ const EnvironmentTab = memo((props) => {
|
||||
<ListItem
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
|
||||
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
|
||||
width: "100%",
|
||||
minWidth: 800,
|
||||
paddingBottom: 0,
|
||||
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 (
|
||||
<ListItemText
|
||||
@@ -912,7 +988,6 @@ const EnvironmentTab = memo((props) => {
|
||||
key={rowIndex}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
height: 40,
|
||||
width: "100%",
|
||||
@@ -1014,12 +1089,17 @@ const EnvironmentTab = memo((props) => {
|
||||
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", }}
|
||||
onClick={() => {
|
||||
if (environment.Type === "cloud") {
|
||||
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
|
||||
return
|
||||
}
|
||||
if (environment.Type === "cloud") {
|
||||
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
|
||||
return
|
||||
}
|
||||
|
||||
setListItemExpanded(listItemExpanded === index ? -1 : index)
|
||||
setListItemExpanded(listItemExpanded === index ? -1 : index)
|
||||
|
||||
if (listItemExpanded !== index) {
|
||||
getEnvQueue(environment)
|
||||
setCurrentEnvQueue([])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
@@ -1100,7 +1180,7 @@ const EnvironmentTab = memo((props) => {
|
||||
<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>
|
||||
} placement="top">
|
||||
<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
|
||||
style={{
|
||||
marginLeft: 30,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
display: "table-cell",
|
||||
}}
|
||||
primary={
|
||||
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">
|
||||
@@ -1231,40 +1283,32 @@ const EnvironmentTab = memo((props) => {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
|
||||
<CheckCircleIcon style={{ color: "#4caf50" }} />
|
||||
</Tooltip>
|
||||
</a>
|
||||
) : (
|
||||
<Tooltip
|
||||
title="Data Lake node disabled. Click to enable."
|
||||
placement="top"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
|
||||
<CheckCircleIcon style={{ color: "#4caf50" }} />
|
||||
</Tooltip>
|
||||
</a>
|
||||
) : (
|
||||
<Tooltip
|
||||
title="Data Lake node disabled. Click to enable."
|
||||
placement="top"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
window.open("/detections/Sigma", "_blank")
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="/detections/Sigma"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<CancelIcon style={{ color: "#f85a3e" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
style={{
|
||||
minWidth: 60,
|
||||
marginLeft: 40,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
display: "table-cell",
|
||||
}}
|
||||
/>
|
||||
window.open("/detections/Sigma", "_blank")
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="/detections/Sigma"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<CancelIcon style={{ color: "#f85a3e" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={(
|
||||
@@ -1274,12 +1318,12 @@ const EnvironmentTab = memo((props) => {
|
||||
)}
|
||||
primaryTypographyProps={{
|
||||
style:{
|
||||
maxWidth: 150,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: "hidden",
|
||||
textOverflow: 'ellipsis',
|
||||
wordWrap: "break-word",
|
||||
transition: "all 0.3s ease",
|
||||
textAlign: "center",
|
||||
}}}
|
||||
style={{
|
||||
minWidth: 120,
|
||||
@@ -1292,7 +1336,7 @@ const EnvironmentTab = memo((props) => {
|
||||
primary={environment.Type}
|
||||
primaryTypographyProps={{
|
||||
style:{
|
||||
minWidth: 70,
|
||||
minWidth: 50,
|
||||
overflow: "hidden",
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
@@ -1302,6 +1346,7 @@ const EnvironmentTab = memo((props) => {
|
||||
}}}
|
||||
style={{display: "table-cell",}}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
style:{
|
||||
@@ -1344,7 +1389,7 @@ const EnvironmentTab = memo((props) => {
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Make Default
|
||||
Default
|
||||
</Button>
|
||||
<Button
|
||||
variant={environment.archived ? "contained" : "outlined"}
|
||||
@@ -1418,34 +1463,38 @@ const EnvironmentTab = memo((props) => {
|
||||
|
||||
</ButtonGroup>
|
||||
|
||||
{/*
|
||||
<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}}/>}
|
||||
</IconButton>
|
||||
*/}
|
||||
</div>
|
||||
</ListItemText>
|
||||
|
||||
{selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
|
||||
<ListItemText
|
||||
primary={
|
||||
<Tooltip
|
||||
<ListItemText
|
||||
primary={
|
||||
<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."
|
||||
placement="top"
|
||||
>
|
||||
<Chip
|
||||
label={"Parent"}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
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."}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
sx={{":hover": {backgroundColor: "transparent"}}}
|
||||
>
|
||||
<Chip
|
||||
style={{marginLeft: 200, }}
|
||||
label={"Parent"}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
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."}
|
||||
placement="top"
|
||||
>
|
||||
<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}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -1489,35 +1538,41 @@ const EnvironmentTab = memo((props) => {
|
||||
aria-label="disabled tabs example"
|
||||
variant="scrollable"
|
||||
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
|
||||
value={1}
|
||||
label=<span style={{color: theme.palette.text.secondary, }}>
|
||||
<img
|
||||
src="/icons/docker.svg"
|
||||
style={{ width: 20, height: 20, marginRight: 10,}}
|
||||
/> Scale
|
||||
</span>
|
||||
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
|
||||
<img
|
||||
src="/icons/docker.svg"
|
||||
style={{ width: 20, height: 20, marginRight: 10,}}
|
||||
/> Docker (default)
|
||||
</span>
|
||||
/>
|
||||
<Tab
|
||||
value={2}
|
||||
label=<span style={{color: theme.palette.text.secondary, }}>
|
||||
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
|
||||
<img
|
||||
src="/icons/k8s.svg"
|
||||
style={{ width: 20, height: 20, marginRight: 10 }}
|
||||
/> k8s
|
||||
/> Kubernetes
|
||||
|
||||
</span>
|
||||
/>
|
||||
|
||||
<Tab
|
||||
value={0}
|
||||
style={{marginLeft: 300, }}
|
||||
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
|
||||
Verbose Mode
|
||||
</span>
|
||||
/>
|
||||
|
||||
</Tabs>
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||
{installationTab === 2 ?
|
||||
@@ -1563,6 +1618,7 @@ const EnvironmentTab = memo((props) => {
|
||||
fontFamily: "monospace",
|
||||
fontSize: 18,
|
||||
border: themeMode === "dark" ? "1px solid #555" : "1px solid #ddd",
|
||||
minHeight: 325,
|
||||
}}
|
||||
>
|
||||
{getOrborusCommand(environment)}
|
||||
@@ -1588,49 +1644,114 @@ const EnvironmentTab = memo((props) => {
|
||||
|
||||
<Divider style={{marginTop: 25, marginBottom: 10, }}/>
|
||||
<div style={{display: 'flex', alignItems: 'center', }}>
|
||||
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies:</Typography> <Checkbox
|
||||
id="shuffle_skip_proxies"
|
||||
onClick={() => {
|
||||
if (commandController.proxies === undefined) {
|
||||
commandController.proxies = true
|
||||
} else {
|
||||
commandController.proxies = !commandController.proxies
|
||||
}
|
||||
<Checkbox
|
||||
id="shuffle_skip_proxies"
|
||||
onClick={() => {
|
||||
if (commandController.proxies === undefined) {
|
||||
commandController.proxies = true
|
||||
} else {
|
||||
commandController.proxies = !commandController.proxies
|
||||
}
|
||||
|
||||
setCommandController(commandController)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
setCommandController(commandController)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies</Typography>
|
||||
</div>
|
||||
<div />
|
||||
<div style={{display: 'flex', alignItems: 'center', }}>
|
||||
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake:</Typography> <Checkbox
|
||||
id="shuffle_skip_pipelines"
|
||||
onClick={() => {
|
||||
if (commandController.pipelines === undefined) {
|
||||
commandController.pipelines = true
|
||||
} else {
|
||||
commandController.pipelines = !commandController.pipelines
|
||||
}
|
||||
setCommandController(commandController)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
id="shuffle_enable_detection"
|
||||
onClick={() => {
|
||||
if (commandController.detection === undefined) {
|
||||
commandController.detection = true
|
||||
} else {
|
||||
commandController.detection = !commandController.detection
|
||||
}
|
||||
|
||||
setCommandController(commandController)
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
|
||||
</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>
|
||||
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||
{installationTab === 2 ? null :
|
||||
<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>
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
|
||||
{currentEnvQueue.length === 0 ? null :
|
||||
<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 : (
|
||||
<ListItem
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { toast } from 'react-toastify';
|
||||
import { GetIconInfo, } from "../views/Workflows2.jsx";
|
||||
|
||||
import {
|
||||
IconButton,
|
||||
@@ -9,24 +10,25 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Menu,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Menu,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
} from "@mui/material";
|
||||
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
@@ -352,8 +354,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Delete file"}
|
||||
style={{marginLeft: isSelectedFiles?5:15, }}
|
||||
style={{}}
|
||||
aria-label={"Delete"}
|
||||
placement="right"
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
@@ -628,8 +631,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (folder === undefined || folder === null || folder.length < 2) {
|
||||
toast("Please enter a valid folder name")
|
||||
if (folder === undefined || folder === null || folder.length < 1) {
|
||||
toast("Please enter a valid folder name. For Root: /")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -637,6 +640,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
url: url,
|
||||
path: folder,
|
||||
field_3: downloadBranch || "master",
|
||||
|
||||
namespace: selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" ? selectedCategory : "",
|
||||
};
|
||||
|
||||
if (field1.length > 0) {
|
||||
@@ -1312,57 +1317,80 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
|
||||
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
upload.click();
|
||||
}}
|
||||
style={{ textTransform: 'none',fontSize: 16, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
|
||||
>
|
||||
Upload files
|
||||
</Button>
|
||||
{/* <FileCategoryInput
|
||||
isSet={renderTextBox} /> */}
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={(event) => {
|
||||
//const file = event.target.value
|
||||
//const fileObject = URL.createObjectURL(actualFile)
|
||||
//setFile(fileObject)
|
||||
//const files = event.target.files[0]
|
||||
uploadFiles(event.target.files);
|
||||
<ButtonGroup style={{top: -10, position: "relative", }}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
upload.click();
|
||||
}}
|
||||
style={{ textTransform: 'none',fontSize: 16, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
|
||||
>
|
||||
Upload files
|
||||
</Button>
|
||||
{/* <FileCategoryInput
|
||||
isSet={renderTextBox} /> */}
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={(event) => {
|
||||
//const file = event.target.value
|
||||
//const fileObject = URL.createObjectURL(actualFile)
|
||||
//setFile(fileObject)
|
||||
//const files = event.target.files[0]
|
||||
uploadFiles(event.target.files);
|
||||
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ marginLeft: 16, marginRight: 15, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => getFiles(selectedCategory)}
|
||||
>
|
||||
<CachedIcon />
|
||||
</Button>
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => getFiles(selectedCategory)}
|
||||
>
|
||||
<CachedIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup style={{marginLeft: 10, }}>
|
||||
{/* <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 !== null &&
|
||||
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
|
||||
labelId="input-namespace-select-label"
|
||||
labelId="category-choice"
|
||||
id="input-namespace-select-id"
|
||||
style={{
|
||||
minWidth: 122,
|
||||
maxWidth: 122,
|
||||
minWidth: 175,
|
||||
maxWidth: 175,
|
||||
height: 35,
|
||||
float: "right",
|
||||
position: 'relative',
|
||||
top: 8
|
||||
borderRadius: "5px 0px 0px 5px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
value={selectedCategory}
|
||||
onChange={(event) => {
|
||||
@@ -1389,12 +1417,31 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
}}
|
||||
>
|
||||
{fileCategories.map((data, index) => {
|
||||
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||
const iconDetails = GetIconInfo({
|
||||
"app_name": fixedname,
|
||||
"name": fixedname,
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
@@ -1430,36 +1477,51 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
<div style={{display: "inline-flex", position:"relative", top: 8}}>
|
||||
{renderTextBox ?
|
||||
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setRenderTextBox(false);
|
||||
console.log(" close clicked")
|
||||
|
||||
{/*<div style={{display: "inline-flex", position:"relative", top: 8}}>*/}
|
||||
{renderTextBox ?
|
||||
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{
|
||||
height: 35,
|
||||
borderRadius: 4,
|
||||
textTransform: 'none',
|
||||
fontSize: 16,
|
||||
borderRadius: "0px 5px 5px 0px",
|
||||
|
||||
marginRight: 10,
|
||||
}}
|
||||
>
|
||||
<ClearIcon/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
:
|
||||
<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);
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setRenderTextBox(false);
|
||||
console.log(" close clicked")
|
||||
}}
|
||||
>
|
||||
<AddIcon/>
|
||||
File Category
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<ClearIcon/>
|
||||
</Button>
|
||||
</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
|
||||
onKeyPress={(event)=>{
|
||||
@@ -1491,7 +1553,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
margin="dense"
|
||||
defaultValue={""}
|
||||
autoFocus
|
||||
/>}</div>
|
||||
/>}
|
||||
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openEditor}
|
||||
@@ -1673,3 +1736,4 @@ const DownloadFileIcon = memo(({ setLoadFileModalOpen, isSelectedFiles }) => {
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -751,6 +751,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
localStorage.setItem("getting_started_sidebar", "open");
|
||||
localStorage.removeItem("workflows");
|
||||
localStorage.removeItem("apps");
|
||||
localStorage.removeItem("dashboard_onboarding_complete")
|
||||
localStorage.removeItem("dashboard_onboarding_completed")
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
||||
mode: "cors",
|
||||
@@ -955,7 +957,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
if (!fetched && org) {
|
||||
setActiveOrgData(org);
|
||||
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);
|
||||
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||
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", }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/usecases"
|
||||
to={userdata?.support ? "/new-dashboard" : "/usecases"}
|
||||
onClick={(event) => {
|
||||
setOpenautomateTab(true);
|
||||
setOpenSecurityTab(false);
|
||||
|
||||
@@ -688,51 +688,51 @@ const AuthenticationOauth2 = (props) => {
|
||||
|
||||
const autoAuthButton =
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{buttonClicked ? (
|
||||
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
|
||||
) : (
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAction.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={selectedAction.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5, color: "#2f2f2f",}} variant="body1">
|
||||
One-click Login
|
||||
</Typography>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
fullWidth
|
||||
variant="contained"
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 275,
|
||||
maxWidth: 275,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{buttonClicked ? (
|
||||
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
|
||||
) : (
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAction.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={selectedAction.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
|
||||
One-click Login
|
||||
</Typography>
|
||||
</span>
|
||||
)}
|
||||
</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)) {
|
||||
return autoAuthButton
|
||||
@@ -747,7 +747,8 @@ const AuthenticationOauth2 = (props) => {
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<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> - </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
|
||||
target="_blank"
|
||||
rel="norefferer"
|
||||
@@ -760,51 +761,52 @@ const AuthenticationOauth2 = (props) => {
|
||||
<div />
|
||||
</span>
|
||||
|
||||
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
|
||||
<span>
|
||||
<span style={{display: "flex"}}>
|
||||
{autoAuthButton}
|
||||
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
|
||||
<span>
|
||||
<span style={{display: "flex"}}>
|
||||
{autoAuthButton}
|
||||
|
||||
{buttonClicked ?
|
||||
null
|
||||
:
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Force Admin Consent"}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
style={{
|
||||
maxWidth: 50,
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
maxHeight: 50,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
//startOauth2Request(true)
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
<SupervisorAccountIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
|
||||
OR
|
||||
</Typography>
|
||||
</span>
|
||||
: null}
|
||||
{buttonClicked ?
|
||||
null
|
||||
:
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Force Admin Consent"}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
style={{
|
||||
maxWidth: 40,
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
maxHeight: 50,
|
||||
marginLeft: 10,
|
||||
}}
|
||||
color="secondary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
//startOauth2Request(true)
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
<SupervisorAccountIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
|
||||
OR
|
||||
</Typography>
|
||||
</span>
|
||||
: null}
|
||||
{/*<TextField
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
|
||||
InputProps={{
|
||||
|
||||
@@ -499,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
|
||||
</div>
|
||||
{userdata?.support ? (
|
||||
<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 }}>
|
||||
<Select
|
||||
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
|
||||
@@ -711,11 +711,12 @@ const OrgHeaderexpandedNew = (props) => {
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
{!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org || null || selectedOrganization?.creator_org?.length > 0 ? null :
|
||||
<CloudSyncTab
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
serverside={false}
|
||||
/>
|
||||
/>}
|
||||
<Grid item xs={12} style={{ marginTop: 20, }}>
|
||||
<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 }}>
|
||||
|
||||
@@ -437,7 +437,8 @@ const ParsedAction = (props) => {
|
||||
];
|
||||
|
||||
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: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
@@ -447,7 +448,7 @@ const ParsedAction = (props) => {
|
||||
if (response.status === 200) {
|
||||
//toast("Successfully GOT app "+appId)
|
||||
} else {
|
||||
toast("Failed getting app");
|
||||
toast.error("Failed getting app. Please try again or contact support@shuffler.io");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
@@ -1711,6 +1712,7 @@ const ParsedAction = (props) => {
|
||||
}
|
||||
|
||||
const sortByCategoryLabel = (a, b) => {
|
||||
|
||||
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
|
||||
|
||||
@@ -1739,11 +1741,12 @@ const ParsedAction = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Gets the most important actions first
|
||||
const renderedActionOptions = deduplicateByName((
|
||||
selectedApp.actions === undefined || selectedApp.actions === null ? [] :
|
||||
selectedApp.actions.filter((a) =>
|
||||
a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
|
||||
isIntegration ? selectedApp.actions :
|
||||
selectedApp.actions.filter((a) => a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
|
||||
).sort(sortByCategoryLabel))
|
||||
|
||||
|
||||
@@ -2981,7 +2984,6 @@ const ParsedAction = (props) => {
|
||||
dataLPIgnore="true"
|
||||
autoComplete="off"
|
||||
|
||||
|
||||
id="checkbox-search"
|
||||
style={{
|
||||
...theme.palette.textFieldStyle,
|
||||
|
||||
@@ -223,7 +223,7 @@ const PartnerDetails = (props) => {
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
|
||||
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
|
||||
Name
|
||||
Company Name
|
||||
</Typography>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
|
||||
/>
|
||||
</div> */}
|
||||
<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
|
||||
</div>
|
||||
<Skeleton
|
||||
@@ -399,7 +399,7 @@ const PartnerDetails = (props) => {
|
||||
variant="text"
|
||||
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
|
||||
>
|
||||
Name
|
||||
Company Name
|
||||
</Typography>
|
||||
<TextField
|
||||
required
|
||||
@@ -419,7 +419,7 @@ const PartnerDetails = (props) => {
|
||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder="Name"
|
||||
placeholder="Company Name"
|
||||
type="name"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
@@ -544,7 +544,8 @@ const PartnerDetails = (props) => {
|
||||
style={{
|
||||
marginRight: "12px",
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: theme?.typography?.fontFamily
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
marginTop: 2.5,
|
||||
}}
|
||||
>
|
||||
Solutions
|
||||
@@ -895,7 +896,7 @@ const PartnerDetails = (props) => {
|
||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder="support@shuffler.io"
|
||||
placeholder="example@company.com"
|
||||
type="name"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
|
||||
@@ -1362,10 +1362,10 @@ print('"' + encoded + '"')
|
||||
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
|
||||
<div style={{ maxWidth: "calc(100% - 20px)" }}>
|
||||
<Typography variant="h5" style={{ fontSize: 24, fontWeight: 500, textAlign: "left" }}>
|
||||
Notification Workflow
|
||||
Error Workflow
|
||||
</Typography>
|
||||
<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>
|
||||
|
||||
{modalView}
|
||||
@@ -1614,12 +1614,12 @@ print('"' + encoded + '"')
|
||||
</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
|
||||
})</Typography>
|
||||
|
||||
<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.
|
||||
Error help you find potential problems with your workflows and apps.
|
||||
<a
|
||||
target="_blank"
|
||||
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
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
@@ -14,15 +15,21 @@ import {
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
FileCopy as FileCopyIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Padding,
|
||||
FileCopy as FileCopyIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Delete as DeleteIcon,
|
||||
Check as CheckIcon,
|
||||
} from "@mui/icons-material"
|
||||
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
|
||||
import { Box, Skeleton, Typography } from '@mui/material';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import RunDetectionTest from '../components/RunDetectionTest.jsx';
|
||||
|
||||
const SchedulesTab = memo((props) => {
|
||||
const {globalUrl, users, } = props;
|
||||
@@ -30,13 +37,58 @@ const SchedulesTab = memo((props) => {
|
||||
const [allSchedules, setAllSchedules] = React.useState([]);
|
||||
const [pipelines, setPipelines] = React.useState([]);
|
||||
const [showLoader, setShowLoader] = React.useState(true);
|
||||
const [workflows, setWorkflows] = React.useState([]);
|
||||
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 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(() => {
|
||||
handleGetWorkflows()
|
||||
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
|
||||
handleGetAllTriggers()
|
||||
}
|
||||
@@ -58,8 +110,11 @@ const SchedulesTab = memo((props) => {
|
||||
environment: pipeline.environment,
|
||||
};
|
||||
|
||||
if (state === "start") toast("starting the pipeline");
|
||||
else toast.info("Stopping the pipeline. This may take a few minutes to propagate.")
|
||||
if (state === "start") {
|
||||
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`;
|
||||
fetch(url, {
|
||||
@@ -144,16 +199,65 @@ const SchedulesTab = memo((props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<DialogTitle style={{padding: "50px 50px 25px 50px", }}>
|
||||
<Typography variant='h5' color="textPrimary" >
|
||||
Run a Tenzir pipeline
|
||||
</Typography>
|
||||
<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>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<div>
|
||||
<DialogContent style={{padding: "0px 50px 50px 50px", }}>
|
||||
<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
|
||||
color="primary"
|
||||
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
|
||||
@@ -163,8 +267,9 @@ const SchedulesTab = memo((props) => {
|
||||
minRows={4}
|
||||
required
|
||||
fullWidth={true}
|
||||
defaultValue="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"
|
||||
placeholder="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"
|
||||
defaultValue={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
|
||||
value={newPipelineValue}
|
||||
placeholder={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
|
||||
id="environment_name"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
@@ -174,7 +279,7 @@ const SchedulesTab = memo((props) => {
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogActions style={{padding: "0px 50px 50px 50px", }}>
|
||||
<Button
|
||||
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
|
||||
onClick={() => {
|
||||
@@ -191,7 +296,7 @@ const SchedulesTab = memo((props) => {
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
Create Pipeline
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -232,18 +337,18 @@ const SchedulesTab = memo((props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success && pipelineConfig.type !== "delete") {
|
||||
toast("Failed to set pipeline: " + responseJson.reason);
|
||||
toast.error("Failed to set pipeline: " + responseJson.reason);
|
||||
} else {
|
||||
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)
|
||||
|
||||
} else if (pipelineConfig.type === "stop") {
|
||||
toast("Pipeline will be stopped: " + responseJson.reason)
|
||||
toast.success("Pipeline will be stopped: " + responseJson.reason)
|
||||
setPipelineModalOpen(false)
|
||||
|
||||
} 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?
|
||||
const url =
|
||||
globalUrl +
|
||||
"/api/v1/workflows/" +
|
||||
data["workflow_id"] +
|
||||
"/schedule/" +
|
||||
data.id;
|
||||
const url = `${globalUrl}/api/v1/workflows/${data?.workflow_id}/schedule/${data.id}`;
|
||||
fetch(url, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
@@ -414,7 +514,7 @@ const SchedulesTab = memo((props) => {
|
||||
//toast(error.toString());
|
||||
console.log("Get schedule error: ", error.toString());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const startWebHook = (trigger) => {
|
||||
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>
|
||||
</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>
|
||||
<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
|
||||
</Typography>
|
||||
<Typography variant='body2' color="textSecondary">
|
||||
@@ -903,11 +1192,9 @@ const SchedulesTab = memo((props) => {
|
||||
style={{
|
||||
textTransform: 'none',
|
||||
fontSize: 16,
|
||||
color:webhook.status === "running" ? '#1a1a1a' : null,
|
||||
backgroundColor: webhook.status === "running" ? '#ff8544' : null,
|
||||
width: 150,
|
||||
}}
|
||||
color={webhook.status === "running" ? "secondary" : "primary"}
|
||||
color={"secondary"}
|
||||
variant={webhook.status === "running" ? "contained" : "outlined"}
|
||||
disabled={webhook.status === "uninitialized"}
|
||||
onClick={() => {
|
||||
@@ -929,166 +1216,7 @@ const SchedulesTab = memo((props) => {
|
||||
)}
|
||||
</List>
|
||||
</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>
|
||||
@@ -1096,3 +1224,4 @@ const SchedulesTab = memo((props) => {
|
||||
});
|
||||
|
||||
export default SchedulesTab;
|
||||
|
||||
|
||||
@@ -147,6 +147,8 @@ const CodeEditor = (props) => {
|
||||
|
||||
// Auto-indent JSON-like content (with safety hehe)
|
||||
const autoIndentContent = React.useCallback((content) => {
|
||||
return content
|
||||
|
||||
// Safety checks :)
|
||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||
return content;
|
||||
@@ -173,6 +175,7 @@ const CodeEditor = (props) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
|
||||
|
||||
// const {codelang, setcodelang} = props
|
||||
@@ -1832,6 +1835,7 @@ const CodeEditor = (props) => {
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<DialogTitle
|
||||
style={{
|
||||
@@ -1842,6 +1846,35 @@ const CodeEditor = (props) => {
|
||||
File Editor ({localcodedata.length})
|
||||
</DialogTitle>
|
||||
</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
|
||||
@@ -2270,7 +2303,7 @@ const CodeEditor = (props) => {
|
||||
width: 50,
|
||||
marginLeft: 100,
|
||||
}}
|
||||
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
|
||||
disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
|
||||
onClick={() => {
|
||||
const indentedText = IndentJsonLikeString(localcodedata, 2)
|
||||
if (indentedText !== undefined && indentedText !== null) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Zoom,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
||||
@@ -172,6 +173,7 @@ const AppGrid = props => {
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
const [inputValue, setInputValue] = useState("")
|
||||
useEffect(() => {
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
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) {
|
||||
//setLocalMessage(inputsearch)
|
||||
refine(inputsearch)
|
||||
@@ -217,12 +225,14 @@ const AppGrid = props => {
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
value={currentRefinement}
|
||||
value={inputValue}
|
||||
placeholder="Find Workflows..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
const value = event.currentTarget.value
|
||||
setInputValue(value)
|
||||
debouncedRefine(value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
|
||||
+556
-77
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import AuthenticationModal from "../components/AuthenticationModal.jsx";
|
||||
import { useNavigate, Link, useLocation } from "react-router-dom";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import { toast } from "react-toastify"
|
||||
@@ -21,12 +22,16 @@ import {
|
||||
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Check as CheckIcon,
|
||||
HourglassDisabled as HourglassDisabledIcon,
|
||||
RestartAlt as RestartAltIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
Send as SendIcon,
|
||||
Error as ErrorIcon,
|
||||
Close as CloseIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material'
|
||||
|
||||
import {
|
||||
@@ -43,21 +48,26 @@ const AgentUI = (props) => {
|
||||
const [data, setData] = useState({})
|
||||
const [openIndexes, setOpenIndexes] = useState([])
|
||||
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 [actionInput, setActionInput] = useState("")
|
||||
const [questionAnswers, setQuestionAnswers] = useState({})
|
||||
|
||||
const {themeMode} = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
const navigate = useNavigate();
|
||||
|
||||
document.title = "Shuffle AI Agents"
|
||||
|
||||
const agentWrapperStyle = {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
margin: "auto",
|
||||
paddingTop: 100,
|
||||
paddingBottom: 1000,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
}
|
||||
|
||||
if (data.input === undefined || data.input === null) {
|
||||
@@ -75,7 +85,22 @@ const AgentUI = (props) => {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -150,15 +175,22 @@ const AgentUI = (props) => {
|
||||
if (responseJson.success !== false) {
|
||||
if (responseJson.status === "EXECUTING") {
|
||||
// Recursively looking for updates until it's not executing anymore
|
||||
setTimeout(() => {
|
||||
GetExecution(execution_id, node_id, authorization)
|
||||
}, 3000)
|
||||
//setTimeout(() => {
|
||||
// GetExecution(execution_id, node_id, authorization)
|
||||
//}, 3000)
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
setDisableButtons(false)
|
||||
@@ -216,12 +248,53 @@ const AgentUI = (props) => {
|
||||
}
|
||||
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
setTimeout(() => {
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
}, 10000)
|
||||
})
|
||||
.catch((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(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const executionId = params.get("execution_id")
|
||||
@@ -233,9 +306,15 @@ const AgentUI = (props) => {
|
||||
setShowAgentStarter(true)
|
||||
//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 { item, index } = props;
|
||||
const [hovered, setHovered] = useState(false);
|
||||
@@ -258,12 +337,96 @@ const AgentUI = (props) => {
|
||||
</Tooltip>
|
||||
|
||||
const categoryStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
width: 25,
|
||||
height: 25,
|
||||
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">
|
||||
<img src="/images/logos/singul.svg" style={categoryStyle} />
|
||||
</Tooltip>
|
||||
@@ -274,37 +437,166 @@ const AgentUI = (props) => {
|
||||
:
|
||||
<div style={categoryStyle} />
|
||||
|
||||
const validate = validateJson(item.details)
|
||||
const itemStartTime = item.start_time
|
||||
var itemEndTime = item.end_time
|
||||
if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
||||
console.log("Rerender 1")
|
||||
//setOriginalStartTime(itemStartTime)
|
||||
var showAuthentication = false
|
||||
var selectedApp = {}
|
||||
if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) {
|
||||
|
||||
// Find the app and inject the image
|
||||
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) {
|
||||
console.log("Rerender 2")
|
||||
setLatestEndTime(itemEndTime)
|
||||
if (!showAuthentication) {
|
||||
if (item?.details?.run_details?.raw_response !== undefined && item?.details?.run_details?.raw_response !== null && item?.details?.run_details?.raw_response?.includes("app_authentication")) {
|
||||
showAuthentication = true
|
||||
}
|
||||
}
|
||||
|
||||
if (itemEndTime === undefined || itemEndTime === null) {
|
||||
// Set it to now
|
||||
itemEndTime = latestEndTime
|
||||
var questionSubmitDisabled = questions.length === 0 ? true : false
|
||||
for (var qKey in questions) {
|
||||
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 currentDuration = itemStartTime - itemEndTime
|
||||
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
||||
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
||||
const barColor = item.status === "FINISHED" ? green :
|
||||
item.status === "FAILURE" || item.status == "ABORTED" ? red :
|
||||
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
|
||||
theme.palette.surfaceColor
|
||||
|
||||
if (totalDuration === currentDuration) {
|
||||
timelineMarginLeft = 0
|
||||
timelineWidth = maxTimelineWidth
|
||||
const rerunAgentButton =
|
||||
<Tooltip title="Rerun from the start with the same input" placement="right">
|
||||
<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¬e=${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 (
|
||||
<div
|
||||
style={{
|
||||
@@ -315,7 +607,7 @@ const AgentUI = (props) => {
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!hovered) {
|
||||
console.log("HOVER")
|
||||
//console.log("HOVER")
|
||||
setHovered(true)
|
||||
}
|
||||
}}
|
||||
@@ -364,41 +656,50 @@ const AgentUI = (props) => {
|
||||
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
|
||||
{parsedCategory}
|
||||
</div>
|
||||
{/*
|
||||
<div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}>
|
||||
{/* To ISO string from unix time */}
|
||||
{new Date(item.start_time * 1000).toLocaleString()}
|
||||
{item?.start_time !== undefined && item?.start_time !== null && item?.start_time !== 0 ?
|
||||
new Date(item.start_time * 1000).toLocaleString()
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
*/}
|
||||
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
|
||||
<Chip
|
||||
label={item.type}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
paddingTop: defaultTopPadding,
|
||||
}}>
|
||||
{item.label}
|
||||
</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={{
|
||||
minWidth: 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={{
|
||||
backgroundColor: item.status === "FINISHED" ?
|
||||
green : item.status === "RUNNING" || item.status === "" ?
|
||||
theme.palette.main : theme.palette.surfaceColor,
|
||||
|
||||
backgroundColor: barColor,
|
||||
marginLeft: timelineMarginLeft,
|
||||
minWidth: timelineWidth,
|
||||
maxWidth: timelineWidth,
|
||||
height: 10,
|
||||
}} />
|
||||
: null}
|
||||
minHeight: 10,
|
||||
maxHeight: 10,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}>
|
||||
</div>
|
||||
:
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -407,24 +708,68 @@ const AgentUI = (props) => {
|
||||
maxWidth: 100,
|
||||
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">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item.type !== "decision" || disableButtons}
|
||||
style={{marginLeft: 20, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
{item.category === "ask" ?
|
||||
<span style={{display: "flex", }}>
|
||||
{rerunButton}
|
||||
{/*
|
||||
<Tooltip title="Approve" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={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>
|
||||
toast.info("Approving this step.")
|
||||
}}
|
||||
>
|
||||
<CheckIcon style={{color: green, }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</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>
|
||||
</Tooltip>
|
||||
:
|
||||
item.category === "agent" ?
|
||||
rerunAgentButton
|
||||
:
|
||||
rerunButton
|
||||
}
|
||||
<Tooltip title="Explore results" placement="right">
|
||||
<span>
|
||||
<IconButton
|
||||
@@ -443,6 +788,69 @@ const AgentUI = (props) => {
|
||||
</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 ?
|
||||
<div style={{marginTop: 10, marginBottom: 10, }}>
|
||||
{validate.valid === true ?
|
||||
@@ -479,7 +887,12 @@ const AgentUI = (props) => {
|
||||
const TimelineRender = (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 = [
|
||||
{
|
||||
"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
|
||||
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
|
||||
const verifiedInput = validateJson(actionResult?.result)
|
||||
@@ -500,6 +934,7 @@ const AgentUI = (props) => {
|
||||
agent_data.decisions = verifiedInput.result?.decisions
|
||||
|
||||
setAgentActionResult(actionResult)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,13 +951,13 @@ const AgentUI = (props) => {
|
||||
}
|
||||
|
||||
var newTimelineItem = {
|
||||
"label": item.action,
|
||||
"label": item?.action,
|
||||
"type": "decision",
|
||||
"category": item.category,
|
||||
"category": item?.category,
|
||||
|
||||
"status": item.run_details.status,
|
||||
"start_time": item.run_details.started_at,
|
||||
"end_time": item.run_details.completed_at,
|
||||
"status": item?.run_details?.status,
|
||||
"start_time": item?.run_details?.started_at,
|
||||
"end_time": item?.run_details?.completed_at,
|
||||
}
|
||||
|
||||
newTimelineItem.details = item
|
||||
@@ -577,6 +1012,14 @@ const AgentUI = (props) => {
|
||||
setAgentRequestLoading(true)
|
||||
//setShowAgentStarter(false);
|
||||
//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 === "") {
|
||||
toast.error("Please provide a valid input for the AI Agent.")
|
||||
@@ -606,7 +1049,7 @@ const AgentUI = (props) => {
|
||||
},
|
||||
{
|
||||
"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 (
|
||||
<div style={agentWrapperStyle}>
|
||||
<TextField
|
||||
id="copy_element_shuffle"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
{showAgentStarter ?
|
||||
<Box component="form" style={{textAlign: "center", }} onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitInput(actionInput);
|
||||
}}>
|
||||
<Box
|
||||
component="form"
|
||||
style={{textAlign: "center", }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitInput(actionInput);
|
||||
}}
|
||||
>
|
||||
<img src="/images/logos/agent.svg" style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}} />
|
||||
|
||||
<div />
|
||||
|
||||
<Typography variant="h5" style={{marginTop: 30, }}>
|
||||
@@ -661,7 +1124,7 @@ const AgentUI = (props) => {
|
||||
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
||||
multiline
|
||||
minRows={2}
|
||||
defaultValue={execution?.execution_id || ""}
|
||||
defaultValue={actionInput || ""}
|
||||
onChange={(e) => {
|
||||
setActionInput(e.target.value)
|
||||
}}
|
||||
@@ -704,6 +1167,22 @@ const AgentUI = (props) => {
|
||||
</Button>
|
||||
</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" ?
|
||||
<TimelineRender agent_data={data} />
|
||||
:
|
||||
|
||||
@@ -22,6 +22,7 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import useDebouncedCallback from "../utils/useDebouncedCallback.js";
|
||||
import {
|
||||
Zoom,
|
||||
Fade,
|
||||
@@ -275,7 +276,7 @@ export const triggers = [
|
||||
{
|
||||
"name": "alertinfo",
|
||||
"example": "",
|
||||
"value": "Do you want to continue the workflow? Start parameters: $exec",
|
||||
"value": "## Stop or continue?\n\nDetails: $exec",
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
@@ -1259,6 +1260,24 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"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",
|
||||
"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) => {
|
||||
if (cy === undefined || cy == null) {
|
||||
console.log("Cytoscape not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
var currentnode = cy.getElementById(newNodeId);
|
||||
|
||||
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) {
|
||||
@@ -12411,11 +12436,20 @@ const AngularWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
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) {
|
||||
const appsearchValue = document.getElementById("appsearch")
|
||||
if (appsearchValue !== undefined && appsearchValue !== null) {
|
||||
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) {
|
||||
// setSearchOpen(true)
|
||||
//}
|
||||
|
||||
refine(event.currentTarget.value)
|
||||
safeRefine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
@@ -14725,7 +14758,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
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
|
||||
rel="noopener noreferrer"
|
||||
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) ?
|
||||
<div
|
||||
style={{
|
||||
border: theme.palette.DialogStyle.border,
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
|
||||
style={{
|
||||
border: theme.palette.DialogStyle.border,
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
|
||||
width: "fit-content",
|
||||
maxWidth: "45vw",
|
||||
minWidth: 300,
|
||||
|
||||
color: theme.palette.DialogStyle.color,
|
||||
padding: 10,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
transition: "left 0.3s ease, top 0.3s ease",
|
||||
}}
|
||||
overflowWrap: "anywhere",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
|
||||
<Tooltip
|
||||
@@ -22726,12 +22765,12 @@ const AngularWorkflow = (defaultprops) => {
|
||||
style={{ float: "right", marginTop: 20, }}
|
||||
|
||||
// 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={() => {
|
||||
toast("Opening logs in a new tab")
|
||||
|
||||
setTimeout(() => {
|
||||
window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
|
||||
window.open(`${globalUrl}/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
|
||||
}, 250)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1494,13 +1494,13 @@ const ApiExplorerWrapper = (props) => {
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
}}
|
||||
>
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
}}
|
||||
>
|
||||
<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!
|
||||
</Typography>
|
||||
|
||||
+103
-107
@@ -8,6 +8,7 @@ import {
|
||||
Typography,
|
||||
FormControlLabel,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
@@ -2680,11 +2681,11 @@ const AppCreator = (defaultprops) => {
|
||||
setErrorCode(responseJson.reason);
|
||||
|
||||
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
|
||||
})
|
||||
} else {
|
||||
toast.error("Failed to build: " + responseJson.reason, {
|
||||
toast.error("Failed to build: \n\n" + responseJson?.reason, {
|
||||
autoClose: 10000
|
||||
})
|
||||
}
|
||||
@@ -2930,7 +2931,7 @@ const AppCreator = (defaultprops) => {
|
||||
Query
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<div style={{ display: "flex", width: 100 }}>
|
||||
<ButtonGroup style={{ display: "flex", width: 100 }}>
|
||||
{index === extraAuth.length - 1 ? (
|
||||
<Button
|
||||
color="primary"
|
||||
@@ -2963,7 +2964,7 @@ const AppCreator = (defaultprops) => {
|
||||
>
|
||||
<RemoveIcon style={{}} />
|
||||
</Button>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
@@ -3431,13 +3432,13 @@ const AppCreator = (defaultprops) => {
|
||||
const ActionPaper = (props) => {
|
||||
const { data, index } = props
|
||||
|
||||
const [updater, setUpdater] = useState("tmp");
|
||||
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
||||
const [urlPath, setUrlPath] = useState("");
|
||||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
||||
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
||||
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
||||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||||
const [updater, setUpdater] = useState("tmp");
|
||||
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
||||
const [urlPath, setUrlPath] = useState("");
|
||||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
||||
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
||||
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
||||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||||
const [currentAction, setCurrentAction] = useState({
|
||||
name: "",
|
||||
file_field: "",
|
||||
@@ -3454,6 +3455,10 @@ const AppCreator = (defaultprops) => {
|
||||
required_bodyfields: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Queries: ", urlPathQueries)
|
||||
}, [urlPathQueries])
|
||||
|
||||
const findBodyParams = (body) => {
|
||||
const regex = /\${(\w+)}/g;
|
||||
const found = body.match(regex);
|
||||
@@ -3462,7 +3467,7 @@ const AppCreator = (defaultprops) => {
|
||||
} else {
|
||||
setExtraBodyFields(found);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const UrlPathParameters = () => {
|
||||
const values = getCurrentPaths(urlPath);
|
||||
@@ -3495,28 +3500,27 @@ const AppCreator = (defaultprops) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
const HandleIndividualChip = (props) => {
|
||||
const { chipData, index } = props;
|
||||
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
|
||||
const { chipData, index } = props;
|
||||
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
|
||||
|
||||
return (
|
||||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||||
height: 30,
|
||||
margin: 3,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
cursor: "pointer",
|
||||
borderColor: theme.palette.chipStyle.borderColor,
|
||||
color: theme.palette.chipStyle.color,
|
||||
}}
|
||||
label={parsedChip}
|
||||
onClick={() => {
|
||||
return (
|
||||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||||
height: 30,
|
||||
margin: 3,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
cursor: "pointer",
|
||||
borderColor: theme.palette.chipStyle.borderColor,
|
||||
color: theme.palette.chipStyle.color,
|
||||
}}
|
||||
label={parsedChip}
|
||||
onClick={() => {
|
||||
if (chipRequired) {
|
||||
currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1)
|
||||
} else {
|
||||
@@ -3524,27 +3528,28 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
|
||||
setCurrentAction(currentAction);
|
||||
setChipRequired(!chipRequired);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const setActionField = (field, value) => {
|
||||
currentAction[field] = value
|
||||
setCurrentAction(currentAction)
|
||||
|
||||
//setUrlPathQueries(currentAction.queries)
|
||||
setChipRequired(!chipRequired);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const addPathQuery = () => {
|
||||
const setActionField = (field, value) => {
|
||||
currentAction[field] = value
|
||||
setCurrentAction(currentAction)
|
||||
|
||||
//setUrlPathQueries(currentAction.queries)
|
||||
};
|
||||
|
||||
const addPathQuery = () => {
|
||||
urlPathQueries.push({ name: "", required: true, example: "", });
|
||||
if (updater === "addupdater") {
|
||||
setUpdater("updater");
|
||||
} else {
|
||||
setUpdater("addupdater");
|
||||
}
|
||||
|
||||
setUrlPathQueries(urlPathQueries);
|
||||
};
|
||||
|
||||
@@ -3555,6 +3560,7 @@ const AppCreator = (defaultprops) => {
|
||||
} else {
|
||||
setUpdater("flipupdater");
|
||||
}
|
||||
|
||||
setUrlPathQueries(urlPathQueries);
|
||||
};
|
||||
|
||||
@@ -3573,7 +3579,7 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loopQueries = urlPathQueries.length === 0 ? null : (
|
||||
const loopQueries = urlPathQueries.length === 0 ? null : (
|
||||
<div>
|
||||
<Divider
|
||||
style={{
|
||||
@@ -3591,51 +3597,42 @@ const AppCreator = (defaultprops) => {
|
||||
return (
|
||||
<Paper key={queryIndex} style={actionListStyle}>
|
||||
<div style={{ marginLeft: "5px", width: "100%" }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<TextField
|
||||
required
|
||||
fullWidth={true}
|
||||
defaultValue={query.name}
|
||||
placeholder={"Query name (key)"}
|
||||
label={"Query Key"}
|
||||
helperText={
|
||||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||||
Click required to flip
|
||||
</span>
|
||||
}
|
||||
onBlur={(e) => {
|
||||
console.log("IN BLUR: ", e.target.value);
|
||||
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "");
|
||||
setUrlPathQueries(urlPathQueries);
|
||||
}}
|
||||
style={{flex: 3}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
defaultValue={query.example}
|
||||
placeholder={"Default value"}
|
||||
label={"Example"}
|
||||
onBlur={(e) => {
|
||||
urlPathQueries[queryIndex].example = e.target.value.replaceAll(
|
||||
"=",
|
||||
""
|
||||
)
|
||||
|
||||
setUrlPathQueries(urlPathQueries)
|
||||
}}
|
||||
style={{flex: 2}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{display: "flex"}}>
|
||||
<TextField
|
||||
required
|
||||
fullWidth={true}
|
||||
defaultValue={query.name}
|
||||
placeholder={"Query name (key)"}
|
||||
label={"Query Key"}
|
||||
onBlur={(e) => {
|
||||
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "")
|
||||
setUrlPathQueries(urlPathQueries)
|
||||
}}
|
||||
style={{flex: 3}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
defaultValue={query.example}
|
||||
placeholder={"Default value"}
|
||||
label={"Example"}
|
||||
onBlur={(e) => {
|
||||
// E.g. for Jira -> JQL -> requires = in param
|
||||
urlPathQueries[queryIndex].example = e.target.value.replaceAll("=","=")
|
||||
setUrlPathQueries(urlPathQueries)
|
||||
}}
|
||||
style={{flex: 2}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
@@ -3654,7 +3651,7 @@ const AppCreator = (defaultprops) => {
|
||||
deletePathQuery(queryIndex);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
@@ -4106,22 +4103,22 @@ const AppCreator = (defaultprops) => {
|
||||
if (request.header !== undefined && request.header !== null) {
|
||||
var headers = [];
|
||||
for (let [key, value] of Object.entries(request.header)) {
|
||||
if (value === undefined) {
|
||||
if (key.includes(":")) {
|
||||
const keysplit = key.split(":")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
if (value === undefined) {
|
||||
if (key.includes(":")) {
|
||||
const keysplit = key.split(":")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
|
||||
} else if (key.includes("=")) {
|
||||
const keysplit = key.split("=")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
} else if (key.includes("=")) {
|
||||
const keysplit = key.split("=")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
|
||||
} else {
|
||||
toast("Removed key: ", key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast("Removed key: ", key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
parameterName !== undefined &&
|
||||
@@ -4392,9 +4389,8 @@ const AppCreator = (defaultprops) => {
|
||||
variant={urlPath.length > 0 ? "contained" : "outlined"}
|
||||
style={{ }}
|
||||
onClick={() => {
|
||||
//console.log(urlPathQueries)
|
||||
//console.log(urlPath)
|
||||
console.log(currentAction);
|
||||
|
||||
const errors = getActionErrors();
|
||||
addActionToView(errors);
|
||||
setActionsModalOpen(false);
|
||||
@@ -4460,7 +4456,7 @@ const AppCreator = (defaultprops) => {
|
||||
|
||||
return (
|
||||
<Paper key={index} style={actionListStyle}>
|
||||
{newActionModal}
|
||||
{newActionModal}
|
||||
|
||||
{error}
|
||||
<Tooltip title="Edit action" placement="bottom">
|
||||
|
||||
@@ -3075,7 +3075,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
|
||||
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((
|
||||
@@ -3405,7 +3405,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
<div style={{ textAlign: "center", marginTop: 25 }}>
|
||||
<Link
|
||||
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" }}
|
||||
>
|
||||
<Button
|
||||
@@ -4300,8 +4300,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (!isLoggedIn) {
|
||||
//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.")
|
||||
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.")
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import { debounce } from "lodash";
|
||||
import AppSelection from "../components/AppSelection.jsx";
|
||||
import AppModal from "../components/AppModal.jsx";
|
||||
import AppCreationModal from "../components/AppCreationModal.jsx";
|
||||
import Dropzone from "../components/Dropzone.jsx";
|
||||
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
@@ -1136,6 +1137,7 @@ const Apps2 = (props) => {
|
||||
const [field2, setField2] = useState("");
|
||||
const [validation, setValidation] = useState(null);
|
||||
const [createAppModalOpen, setCreateAppModalOpen] = useState(false);
|
||||
const [openApiData, setOpenApiData] = useState("");
|
||||
|
||||
const {themeMode, brandColor} = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
@@ -1736,6 +1738,31 @@ const Apps2 = (props) => {
|
||||
// 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(() => {
|
||||
const apps = currTab === 1 ? userApps : orgApps;
|
||||
const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
|
||||
@@ -1853,6 +1880,10 @@ const Apps2 = (props) => {
|
||||
}
|
||||
|
||||
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, }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<AppModal
|
||||
@@ -1869,6 +1900,8 @@ const Apps2 = (props) => {
|
||||
theme={theme}
|
||||
globalUrl={globalUrl}
|
||||
isCloud={isCloud}
|
||||
startOpenApi={openApiData?.length > 0}
|
||||
prefillOpenApiData={openApiData}
|
||||
/>
|
||||
{appsModalLoad}
|
||||
<div style={boxStyle}>
|
||||
@@ -2198,6 +2231,24 @@ const Apps2 = (props) => {
|
||||
</>
|
||||
)}
|
||||
</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={{
|
||||
width: "25%",
|
||||
minWidth: "25%",
|
||||
@@ -2222,6 +2273,7 @@ const Apps2 = (props) => {
|
||||
Create an App
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -2365,6 +2417,7 @@ const Apps2 = (props) => {
|
||||
<Configure clickAnalytics />
|
||||
</InstantSearch>
|
||||
</div>
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
import Draggable from "react-draggable";
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
import { LoadStats } from '../components/LineChartWrapper.jsx';
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
@@ -828,9 +828,14 @@ const Dashboard = (props) => {
|
||||
}
|
||||
</div>
|
||||
|
||||
<DashboardBarchart
|
||||
timelineData={data}
|
||||
height={50}
|
||||
<LineChartWrapper
|
||||
inputname={"heyo"}
|
||||
keys={data}
|
||||
height={100}
|
||||
width={100}
|
||||
border={false}
|
||||
|
||||
color={"#808080"}
|
||||
/>
|
||||
|
||||
</Paper>
|
||||
|
||||
@@ -400,6 +400,10 @@ const Docs = (defaultprops) => {
|
||||
if (propkey === "app_creation") {
|
||||
navigate('/docs/apps#app-creation-introduction')
|
||||
}
|
||||
|
||||
if (propkey === "api") {
|
||||
navigate('/docs/API')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -690,7 +694,8 @@ const Docs = (defaultprops) => {
|
||||
|
||||
const Heading = (props) => {
|
||||
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) {
|
||||
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
const [executionLoading, setExecutionLoading] = useState(false);
|
||||
const [executionData, setExecutionData] = React.useState({});
|
||||
const [executionRunning, setExecutionRunning] = useState(false);
|
||||
const [disableButtons, setDisableButtons] = useState(false);
|
||||
const [workflowQuestion, setWorkflowQuestion] = useState("");
|
||||
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined);
|
||||
const [apps, setApps] = React.useState([]);
|
||||
@@ -84,12 +85,14 @@ const RunWorkflow = (defaultprops) => {
|
||||
const [workflows, setWorkflows] = React.useState([])
|
||||
const [boxWidth, setBoxWidth] = React.useState(500)
|
||||
const [inputQuestions, setInputQuestions] = React.useState([])
|
||||
const [agentic, setAgentic] = React.useState(false)
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const answer = searchParams.get("answer")
|
||||
const execution_id = searchParams.get("reference_execution")
|
||||
const authorization = searchParams.get("authorization")
|
||||
const sourceNode = searchParams.get("source_node")
|
||||
const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
|
||||
const backendUrl = searchParams.get("backend_url") || globalUrl
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,11 +165,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Used to swap from login to register. True = login, false = register
|
||||
|
||||
// Error messages etc
|
||||
const [executionInfo, setExecutionInfo] = useState("");
|
||||
|
||||
const handleValidateForm = (executionArgument) => {
|
||||
// Check if every field exists
|
||||
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) {
|
||||
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
|
||||
console.log("Unanswered, required question: ", key)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -334,17 +337,18 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
const validate = validateJson(executionData.result)
|
||||
|
||||
return (
|
||||
<div style={{marginTop: executionMargin, }}>
|
||||
{workflowQuestion !== "" ? null :
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
<div style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
}
|
||||
|
||||
{workflowQuestion !== "" ? null :
|
||||
validate.valid === false ?
|
||||
<div style={{marginTop: 20, }}>
|
||||
<Divider />
|
||||
{validate?.result !== undefined && validate?.result !== null && validate?.result.length > 0 ?
|
||||
<Divider />
|
||||
: null }
|
||||
<Markdown
|
||||
components={{
|
||||
img: Img,
|
||||
@@ -397,10 +401,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
stop()
|
||||
setMessage("")
|
||||
setExecutionLoading(true)
|
||||
setExecutionData({})
|
||||
setExecutionInfo("")
|
||||
|
||||
setTimeout(() => {
|
||||
setExecutionLoading(true)
|
||||
}, 2500)
|
||||
|
||||
var data = {
|
||||
"execution_argument": executionArgument,
|
||||
"execution_source": "form",
|
||||
@@ -462,6 +469,14 @@ const RunWorkflow = (defaultprops) => {
|
||||
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
|
||||
fetch(url, fetchBody)
|
||||
.then((response) => {
|
||||
@@ -480,25 +495,30 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
//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 (2)`)
|
||||
//}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then(responseJson => {
|
||||
//if (responseJson.success === true) {
|
||||
// setDisableButtons(true)
|
||||
//}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
if (responseJson.success === false) {
|
||||
|
||||
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")) {
|
||||
setMessage("Already answered. You may close this window (2).")
|
||||
setMessage("This form has been answered. You may close this window.")
|
||||
} else {
|
||||
toast.warn(responseJson.reason)
|
||||
toast.warn(responseJson?.reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,11 +540,17 @@ const RunWorkflow = (defaultprops) => {
|
||||
setExecutionRequest(responseJson)
|
||||
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 => {
|
||||
//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()
|
||||
setMessage("")
|
||||
@@ -597,8 +623,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
||||
const newmarkdown = realtimeMarkdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
} 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)
|
||||
} 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)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
}
|
||||
}
|
||||
@@ -608,10 +634,10 @@ const RunWorkflow = (defaultprops) => {
|
||||
console.log("Get workflow error: ", error.toString())
|
||||
|
||||
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)
|
||||
} 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)
|
||||
} 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)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
}
|
||||
})
|
||||
@@ -646,6 +672,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
trig.parameters = []
|
||||
}
|
||||
|
||||
newexec = {}
|
||||
for (var paramkey in trig.parameters) {
|
||||
const param = trig.parameters[paramkey]
|
||||
if (param.name !== "input_questions") {
|
||||
@@ -683,6 +710,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Setting exec arg: ", newexec)
|
||||
setExecutionArgument(newexec)
|
||||
}
|
||||
|
||||
@@ -733,10 +761,10 @@ const RunWorkflow = (defaultprops) => {
|
||||
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
|
||||
// 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 found = newmarkdown.match(uuidRegex)
|
||||
@@ -784,8 +812,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (workflow.status !== "WAITING") {
|
||||
setMessage("Already answered. You may close this window (3).")
|
||||
if (workflow.status === "EXECUTING" || workflow.status === "SUCCESS" || workflow.status === "ABORTED" || workflow.status === "STOPPED" || workflow.status === "FAILURE" || workflow.status === "FINISHED") {
|
||||
setMessage("Already handled. You may close this window.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,13 +834,17 @@ const RunWorkflow = (defaultprops) => {
|
||||
console.log("Status not 200 for workflows :O!");
|
||||
}
|
||||
|
||||
if ((response.status === 401 || response.status === 403) && 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.`)
|
||||
}
|
||||
//if (response.status >= 400 && authorization === undefined || authorization === null || authorization.length === 0) {
|
||||
// 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()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
return
|
||||
}
|
||||
|
||||
// Not sure why this is necessary.
|
||||
if (responseJson.isValid === undefined) {
|
||||
responseJson.isValid = true;
|
||||
@@ -1008,14 +1040,78 @@ const RunWorkflow = (defaultprops) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success == false) {
|
||||
if (responseJson?.success == false) {
|
||||
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)
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
|
||||
} 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)
|
||||
} 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)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
}
|
||||
|
||||
@@ -1072,7 +1168,6 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
getWorkflow(props.match.params.key, sourceNode)
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
|
||||
console.log("Get execution: ", execution_id)
|
||||
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"}
|
||||
|
||||
// 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) {
|
||||
// Check field values
|
||||
//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.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", }}>
|
||||
<CircularProgress />
|
||||
<Typography variant="body1" style={{marginTop: 20, }}>
|
||||
Loading Form Details...
|
||||
Loading Details...
|
||||
</Typography>
|
||||
</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, }}>
|
||||
<Markdown
|
||||
components={{
|
||||
@@ -1342,13 +1437,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<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>
|
||||
{/*
|
||||
<img
|
||||
@@ -1370,10 +1465,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
|
||||
{organization}
|
||||
</Typography>
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
{organization?.length > 0 &&
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
}
|
||||
|
||||
{disabledButtons && message.length > 0 ? null :
|
||||
<Typography color="textSecondary" style={{textAlign: "center", }}>
|
||||
<Typography color="textSecondary" style={{textAlign: "center", marginTop: 15, }}>
|
||||
{message}
|
||||
</Typography>
|
||||
}
|
||||
@@ -1412,6 +1509,11 @@ const RunWorkflow = (defaultprops) => {
|
||||
executionArgument[multiChoiceOptions[0]] = multiChoiceOptions[1]
|
||||
}
|
||||
|
||||
const parsedLabel = question?.value?.startsWith("question_") ?
|
||||
""
|
||||
:
|
||||
question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)
|
||||
|
||||
return (
|
||||
<div style={{marginBottom: 10}} key={index}>
|
||||
|
||||
@@ -1457,7 +1559,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginTop: 5,
|
||||
}}
|
||||
label={question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)}
|
||||
label={parsedLabel}
|
||||
required
|
||||
|
||||
disabled={disabledButtons}
|
||||
@@ -1542,7 +1644,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
:
|
||||
<Fade in={true} timeout={2500}>
|
||||
<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>
|
||||
</Fade>
|
||||
}
|
||||
@@ -1565,10 +1667,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
setButtonClicked("FINISHED")
|
||||
setExecutionData({
|
||||
status: "FINISHED",
|
||||
})
|
||||
// Timeout 2500 just in case
|
||||
setTimeout(() => {
|
||||
setButtonClicked("FINISHED")
|
||||
setExecutionData({
|
||||
status: "FINISHED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, true)
|
||||
}}>
|
||||
@@ -1586,16 +1691,24 @@ const RunWorkflow = (defaultprops) => {
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
}} onClick={() => {
|
||||
setButtonClicked("ABORTED")
|
||||
setExecutionData({
|
||||
status: "ABORTED",
|
||||
})
|
||||
setTimeout(() => {
|
||||
setButtonClicked("ABORTED")
|
||||
setExecutionData({
|
||||
status: "ABORTED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, false)
|
||||
}}>
|
||||
Stop
|
||||
</Button>
|
||||
</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>
|
||||
:
|
||||
<div style={{display: "flex", marginTop: "15px"}}>
|
||||
|
||||
@@ -477,7 +477,7 @@ export const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
//var newitem = JSON.parse(base);
|
||||
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) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
@@ -742,7 +742,7 @@ const DropzoneWrapper = memo(({ onDrop, WorkflowView }) => {
|
||||
const Workflows = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
|
||||
|
||||
document.title = "Shuffle - Workflows";
|
||||
document.title = "Workflows - Shuffle";
|
||||
let navigate = useNavigate();
|
||||
|
||||
const classes = useStyles(theme)
|
||||
|
||||
+442
-329
@@ -4,16 +4,6 @@ import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||
import ReactDOM from "react-dom"
|
||||
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
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { Navigate } from "react-router-dom";
|
||||
@@ -67,6 +57,7 @@ import {
|
||||
|
||||
// Material UI Icons
|
||||
import {
|
||||
ContentCopy as ContentCopyIcon,
|
||||
Close as CloseIcon,
|
||||
Compare as CompareIcon,
|
||||
Maximize as MaximizeIcon,
|
||||
@@ -105,6 +96,12 @@ import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
BarChart as BarChartIcon,
|
||||
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";
|
||||
|
||||
// Additional Components
|
||||
@@ -209,10 +206,10 @@ export const GetIconInfo = (action) => {
|
||||
key: "compare",
|
||||
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: "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: "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"] },
|
||||
@@ -235,6 +232,7 @@ export const GetIconInfo = (action) => {
|
||||
values: [
|
||||
"api",
|
||||
"password",
|
||||
"passwd",
|
||||
"protect",
|
||||
],
|
||||
}
|
||||
@@ -835,7 +833,9 @@ const Workflows2 = (props) => {
|
||||
setCurrTab(1);
|
||||
} else if (tabParam === 'all_workflows' && currTab !== 2) {
|
||||
setCurrTab(2);
|
||||
}
|
||||
} else if (tabParam === 'background_processes' && currTab !== 4) {
|
||||
setCurrTab(4);
|
||||
}
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
@@ -853,10 +853,15 @@ const Workflows2 = (props) => {
|
||||
1: 'my_workflows',
|
||||
2: 'all_workflows',
|
||||
3: 'backup_apps',
|
||||
4: 'background_processes',
|
||||
};
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
queryParams.set('tab', tabMapping[newValue]);
|
||||
|
||||
if (newValue === 4) {
|
||||
setShowExecutionStats(true)
|
||||
setView("grid")
|
||||
}
|
||||
|
||||
navigate(`${location.pathname}?${queryParams.toString()}`);
|
||||
};
|
||||
@@ -1553,7 +1558,7 @@ const Workflows2 = (props) => {
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
minWidth: '440px',
|
||||
minWidth: 440,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
@@ -1566,11 +1571,11 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<DialogTitle style={{padding: 50, }}>
|
||||
<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 />
|
||||
|
||||
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>
|
||||
</DialogTitle>
|
||||
<DialogContent
|
||||
@@ -1819,6 +1824,7 @@ const Workflows2 = (props) => {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setIsLoadingWorkflow(false)
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
|
||||
@@ -1956,6 +1962,7 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoadingWorkflow(false)
|
||||
toast(error.toString());
|
||||
});
|
||||
}
|
||||
@@ -2949,6 +2956,8 @@ const Workflows2 = (props) => {
|
||||
triggerfound = true
|
||||
image = wfTriggers[0].large_image
|
||||
|
||||
trigger.status = trigger?.status?.toLowerCase()
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
@@ -2962,6 +2971,8 @@ const Workflows2 = (props) => {
|
||||
triggerfound = true
|
||||
image = wfTriggers[1].large_image
|
||||
|
||||
trigger.status = trigger?.status?.toLowerCase()
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
@@ -3034,10 +3045,11 @@ const Workflows2 = (props) => {
|
||||
|
||||
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
|
||||
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}>
|
||||
|
||||
{selectedCategory !== "" ?
|
||||
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
|
||||
<div
|
||||
@@ -3058,7 +3070,7 @@ const Workflows2 = (props) => {
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
: null}
|
||||
: null}
|
||||
|
||||
<Grid
|
||||
item
|
||||
@@ -3067,7 +3079,6 @@ const Workflows2 = (props) => {
|
||||
<Grid item style={{ display: "flex", maxHeight: 34 }}>
|
||||
{currTab === 2 ? null :
|
||||
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
|
||||
|
||||
<div
|
||||
style={{ cursor: "" }}
|
||||
onClick={() => {
|
||||
@@ -3183,6 +3194,7 @@ const Workflows2 = (props) => {
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item style={workflowActionStyle}>
|
||||
{appGroup.length > 0 ?
|
||||
<div style={{ display: "flex", marginTop: 8, }}>
|
||||
@@ -3437,7 +3449,7 @@ const Workflows2 = (props) => {
|
||||
</Grid>
|
||||
|
||||
{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
|
||||
inputname={""}
|
||||
keys={foundTimeline?.timeline}
|
||||
@@ -4974,331 +4986,334 @@ const Workflows2 = (props) => {
|
||||
</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 ? (
|
||||
<CustomSearchBox
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
) : (
|
||||
<MuiChipsInput
|
||||
style={{
|
||||
width: "25%",
|
||||
maxWidth: "25%",
|
||||
minWidth: "25%",
|
||||
height: 43,
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
zIndex: 1000,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: "fit-content",
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
placeholder: "Filter Workflows",
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <SearchIcon style={{ color: 'white', paddingRight: 5 }} />
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
onKeyDown: (e) => {
|
||||
// Prevent default behavior for Enter and Backspace
|
||||
if (e.key === 'Enter' || e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}
|
||||
},
|
||||
}}
|
||||
clearInputOnBlur={false}
|
||||
sx={{
|
||||
// Container styling
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: "fit-content",
|
||||
borderRadius: '4px',
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
},
|
||||
{currTab === 2 ? (
|
||||
<CustomSearchBox
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
) :
|
||||
(
|
||||
<MuiChipsInput
|
||||
style={{
|
||||
width: "25%",
|
||||
maxWidth: "25%",
|
||||
minWidth: "25%",
|
||||
height: 43,
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
zIndex: 1000,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: "fit-content",
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
placeholder: "Filter Workflows",
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <SearchIcon style={{ color: 'white', paddingRight: 5 }} />
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
onKeyDown: (e) => {
|
||||
// Prevent default behavior for Enter and Backspace
|
||||
if (e.key === 'Enter' || e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}
|
||||
},
|
||||
}}
|
||||
clearInputOnBlur={false}
|
||||
sx={{
|
||||
// Container styling
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: "fit-content",
|
||||
borderRadius: '4px',
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
},
|
||||
|
||||
// Adjust chip container to center vertically
|
||||
'& .MuiInputBase-root': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
fontSize: 18,
|
||||
padding: '4px 8px',
|
||||
alignItems: 'center',
|
||||
height: "fit-content", // Match height
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
// Adjust chip container to center vertically
|
||||
'& .MuiInputBase-root': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
fontSize: 18,
|
||||
padding: '4px 8px',
|
||||
alignItems: 'center',
|
||||
height: "fit-content", // Match height
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
|
||||
// Rest of the styling remains the same...
|
||||
}}
|
||||
value={filters}
|
||||
onChange={(chips) => {
|
||||
setFilters(chips);
|
||||
const remainingCategories = chips.map(chip => {
|
||||
const match = chip.match(/\d+\.\s+(\w+)/i);
|
||||
return match ? match[1] : chip;
|
||||
}).filter(category => {
|
||||
return usecases.some(usecase =>
|
||||
usecase.name.toLowerCase().includes(category.toLowerCase())
|
||||
);
|
||||
});
|
||||
// Rest of the styling remains the same...
|
||||
}}
|
||||
value={filters}
|
||||
onChange={(chips) => {
|
||||
setFilters(chips);
|
||||
const remainingCategories = chips.map(chip => {
|
||||
const match = chip.match(/\d+\.\s+(\w+)/i);
|
||||
return match ? match[1] : chip;
|
||||
}).filter(category => {
|
||||
return usecases.some(usecase =>
|
||||
usecase.name.toLowerCase().includes(category.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
setSelectedCategory(remainingCategories);
|
||||
findWorkflow(chips);
|
||||
setSelectedCategory(remainingCategories);
|
||||
findWorkflow(chips);
|
||||
|
||||
}}
|
||||
//onAdd={(chip) => {
|
||||
// console.log("ADd: ", chip);
|
||||
// addFilter(chip);
|
||||
//}}
|
||||
//onDelete={(_, index) => {
|
||||
// console.log("Remove: ", index);
|
||||
// removeFilter(index);
|
||||
//}}
|
||||
/>
|
||||
)}
|
||||
}}
|
||||
//onAdd={(chip) => {
|
||||
// console.log("ADd: ", chip);
|
||||
// addFilter(chip);
|
||||
//}}
|
||||
//onDelete={(_, index) => {
|
||||
// console.log("Remove: ", index);
|
||||
// removeFilter(index);
|
||||
//}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
currTab !== 2 && (
|
||||
<Select
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
displayEmpty
|
||||
disabled={currTab === 2}
|
||||
multiple
|
||||
style={{
|
||||
width: "25%",
|
||||
minWidth: "25%",
|
||||
maxWidth: "25%",
|
||||
height: 47,
|
||||
borderRadius: 4,
|
||||
fontSize: 18,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(selected) => selected.length ? selected.join(', ') : 'All Categories'}
|
||||
>
|
||||
<MenuItem disabled value="">
|
||||
All Categories
|
||||
</MenuItem>
|
||||
{usecases.map((usecase, index) => {
|
||||
if (usecase?.name === "5. Verify") {
|
||||
return null;
|
||||
}
|
||||
{
|
||||
currTab !== 2 && (
|
||||
<Select
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
displayEmpty
|
||||
disabled={currTab === 2}
|
||||
multiple
|
||||
style={{
|
||||
width: "25%",
|
||||
minWidth: "25%",
|
||||
maxWidth: "25%",
|
||||
height: 47,
|
||||
borderRadius: 4,
|
||||
fontSize: 18,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(selected) => selected.length ? selected.join(', ') : 'All Categories'}
|
||||
>
|
||||
<MenuItem disabled value="">
|
||||
All Categories
|
||||
</MenuItem>
|
||||
{usecases.map((usecase, index) => {
|
||||
if (usecase?.name === "5. Verify") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentDone = usecase.matches.length > 0 ? parseInt(usecase.matches.length / usecase.list.length * 100) : 0
|
||||
if (percentDone === 0) {
|
||||
usecase = findMatches(usecase, workflows)
|
||||
}
|
||||
const percentDone = usecase.matches.length > 0 ? parseInt(usecase.matches.length / usecase.list.length * 100) : 0
|
||||
if (percentDone === 0) {
|
||||
usecase = findMatches(usecase, workflows)
|
||||
}
|
||||
|
||||
const category = usecase?.name.split(" ")[1]
|
||||
return (
|
||||
<MenuItem
|
||||
value={category}
|
||||
onClick={() => {
|
||||
if (!filters.includes(usecase?.name.toLowerCase())) {
|
||||
addFilter(usecase.name)
|
||||
} else {
|
||||
removeFilter(filters.indexOf(usecase?.name.toLowerCase()))
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
padding: "12px 16px",
|
||||
borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255,255,255,0.1)"
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: "12px"
|
||||
}}>
|
||||
<Checkbox
|
||||
checked={selectedCategory.includes(category)}
|
||||
style={{
|
||||
padding: 0,
|
||||
marginRight: 8,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
fontWeight: selectedCategory.includes(category) ? 500 : 400
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
padding: "2px 8px",
|
||||
borderRadius: "12px",
|
||||
fontSize: "0.75rem"
|
||||
}}
|
||||
>
|
||||
{usecase?.matches.length}/{usecase?.list.length}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 2 && (
|
||||
<CustomCategoryDropdown attribute="usecase_ids" limit={20} />
|
||||
)
|
||||
}
|
||||
const category = usecase?.name.split(" ")[1]
|
||||
return (
|
||||
<MenuItem
|
||||
value={category}
|
||||
onClick={() => {
|
||||
if (!filters.includes(usecase?.name.toLowerCase())) {
|
||||
addFilter(usecase.name)
|
||||
} else {
|
||||
removeFilter(filters.indexOf(usecase?.name.toLowerCase()))
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
padding: "12px 16px",
|
||||
borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255,255,255,0.1)"
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: "12px"
|
||||
}}>
|
||||
<Checkbox
|
||||
checked={selectedCategory.includes(category)}
|
||||
style={{
|
||||
padding: 0,
|
||||
marginRight: 8,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
fontWeight: selectedCategory.includes(category) ? 500 : 400
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
padding: "2px 8px",
|
||||
borderRadius: "12px",
|
||||
fontSize: "0.75rem"
|
||||
}}
|
||||
>
|
||||
{usecase?.matches.length}/{usecase?.list.length}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 2 && (
|
||||
<CustomCategoryDropdown attribute="usecase_ids" limit={20} />
|
||||
)
|
||||
}
|
||||
|
||||
<div style={{ width: "50%", minWidth: "50%", maxWidth: "50%", height: 47, display: "flex", gap: 5 }}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
justifyContent: "space-around",
|
||||
flex: 0.7,
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
gap: 4
|
||||
}}>
|
||||
<div style={{ width: "50%", minWidth: "50%", maxWidth: "50%", height: 47, display: "flex", gap: 5 }}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
justifyContent: "space-around",
|
||||
flex: 0.7,
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
gap: 4
|
||||
}}>
|
||||
|
||||
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}}
|
||||
onClick={() => {
|
||||
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}}
|
||||
onClick={() => {
|
||||
|
||||
const newView = !showExecutionStats
|
||||
localStorage.setItem("showExecutionStats", newView)
|
||||
setShowExecutionStats(!showExecutionStats)
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<BarChartIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
const newView = !showExecutionStats
|
||||
localStorage.setItem("showExecutionStats", newView)
|
||||
setShowExecutionStats(!showExecutionStats)
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<BarChartIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => navigate("/workflows/debug")}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<QueryStatsIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => navigate("/workflows/debug")}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<QueryStatsIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => {
|
||||
const newView = view === "grid" ? "list" : "grid";
|
||||
localStorage.setItem("workflowView", newView);
|
||||
setView(newView);
|
||||
<Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => {
|
||||
const newView = view === "grid" ? "list" : "grid";
|
||||
localStorage.setItem("workflowView", newView);
|
||||
setView(newView);
|
||||
|
||||
if (view === "grid") {
|
||||
setCurrTab(0)
|
||||
}
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{view === "grid" ?
|
||||
<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 }} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
if (view === "grid") {
|
||||
setCurrTab(0)
|
||||
}
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{view === "grid" ?
|
||||
<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 }} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Import workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => upload.click()}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{submitLoading ?
|
||||
<CircularProgress color="secondary" /> :
|
||||
<PublishIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Import workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => upload.click()}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{submitLoading ?
|
||||
<CircularProgress color="secondary" /> :
|
||||
<PublishIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple="multiple"
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={importFiles}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple="multiple"
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={importFiles}
|
||||
/>
|
||||
|
||||
<Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top">
|
||||
<IconButton
|
||||
style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
|
||||
disabled={isCloud || currTab === 2}
|
||||
onClick={() => exportAllWorkflows(workflows)}
|
||||
>
|
||||
<GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateWorkflow}
|
||||
id="create_workflow_button"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
flex: 0.8,
|
||||
textTransform: 'none',
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: 500
|
||||
}}
|
||||
startIcon={<Add/>}
|
||||
>
|
||||
Create Workflow
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top">
|
||||
<IconButton
|
||||
style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
|
||||
disabled={isCloud || currTab === 2}
|
||||
onClick={() => exportAllWorkflows(workflows)}
|
||||
>
|
||||
<GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateWorkflow}
|
||||
id="create_workflow_button"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
flex: 0.8,
|
||||
textTransform: 'none',
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: 500
|
||||
}}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Create Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
<div style={{
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
@@ -5310,8 +5325,106 @@ const Workflows2 = (props) => {
|
||||
) : (
|
||||
view === "grid" && currTab !== 2 ? (
|
||||
<>
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
{currTab === 4 && backgroundWorkflows.map((data, index) => {
|
||||
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%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(365px, 1fr))",
|
||||
|
||||
Reference in New Issue
Block a user