From 1bff92172b6dc9be0d4f0a5cd115d588e67a142c Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 20 Oct 2025 11:37:26 +0200 Subject: [PATCH] Sync + Added dashboard for stats API --- frontend/src/components/AppAuthTab.jsx | 11 +- frontend/src/components/AppCreationModal.jsx | 87 +- frontend/src/components/AppGrid.jsx | 8 +- frontend/src/components/BillingStats.jsx | 342 ++++---- frontend/src/components/CacheView.jsx | 56 +- .../src/components/CollectIngestModal.jsx | 130 ++- frontend/src/components/CreatorGrid.jsx | 17 +- .../src/components/DashboardOnboarding.jsx | 446 ++++++++++ frontend/src/components/Detection.jsx | 1 + frontend/src/components/DetectionExplorer.jsx | 251 +++++- frontend/src/components/DetectionRuleCard.jsx | 93 ++- frontend/src/components/DiscordChat.jsx | 15 +- frontend/src/components/DocsGrid.jsx | 5 +- frontend/src/components/EditWorkflow.jsx | 4 +- frontend/src/components/EnvironmentTab.jsx | 429 ++++++---- frontend/src/components/Files.jsx | 246 +++--- frontend/src/components/LeftSideBar.jsx | 6 +- frontend/src/components/Oauth2Auth.jsx | 184 ++--- .../src/components/OrgHeaderexpandedNew.jsx | 5 +- frontend/src/components/ParsedAction.jsx | 14 +- frontend/src/components/PartnerDetails.jsx | 13 +- frontend/src/components/Priorities.jsx | 8 +- frontend/src/components/RunDetectionTest.jsx | 223 +++++ frontend/src/components/SchedulesTab.jsx | 507 +++++++----- .../src/components/ShuffleCodeEditor1.jsx | 35 +- frontend/src/components/WorkflowGrid.jsx | 14 +- frontend/src/views/AgentUI.jsx | 635 ++++++++++++-- frontend/src/views/AngularWorkflow.jsx | 67 +- frontend/src/views/ApiExplorerWrapper.jsx | 14 +- frontend/src/views/AppCreator.jsx | 210 +++-- frontend/src/views/AppExplorer.jsx | 8 +- frontend/src/views/Apps2.jsx | 53 ++ frontend/src/views/DashboardViews.jsx | 13 +- frontend/src/views/Docs.jsx | 7 +- frontend/src/views/NewDashboard.jsx | 266 ++++++ frontend/src/views/RunWorkflow.jsx | 217 +++-- frontend/src/views/Workflows.jsx | 4 +- frontend/src/views/Workflows2.jsx | 773 ++++++++++-------- 38 files changed, 3985 insertions(+), 1432 deletions(-) create mode 100644 frontend/src/components/DashboardOnboarding.jsx create mode 100644 frontend/src/components/RunDetectionTest.jsx create mode 100644 frontend/src/views/NewDashboard.jsx diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index 8c65366e..c2544d1a 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -992,16 +992,16 @@ const AppAuthTab = memo((props) => { App Authentication
- 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 Protected Keys.   - Learn more about App Authentication + Learn more
@@ -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`; diff --git a/frontend/src/components/AppCreationModal.jsx b/frontend/src/components/AppCreationModal.jsx index 28dea461..6a66f0e0 100644 --- a/frontend/src/components/AppCreationModal.jsx +++ b/frontend/src/components/AppCreationModal.jsx @@ -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 }) => { } }} > + { Paste in the URI for the OpenAPI or find out - How to find URI for openAPI? @@ -568,31 +583,54 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { Must point to a version 2 or 3 OpenAPI specification. - + Or upload a YAML or JSON specification - - +
+ + Drag & drop your OpenAPI (YAML/JSON) anywhere + + + or click to browse files + +
+ + { Continue +
{/* Generate App Modal */} diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index e647085b..33998bfb 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -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 (
{ 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") { diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 8e011d42..0cd2a38e 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -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) => { } */} - {syncStats === true ? null : + {/* {syncStats === true ? null : */} App runs in the selected period }> + {syncStats === true ? + + {onpremAppRuns} + : {filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions} - + } App Runs - } + {/* } */} {syncStats === true || currentTab === 0 ? null : { 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} /> : - + {data.value} } @@ -1712,7 +1739,7 @@ const CacheView = memo((props) => { { e.preventDefault() e.stopPropagation() @@ -1911,7 +1938,7 @@ const CacheView = memo((props) => { {selectedCategory === "protected" ?
- 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 App Auth.
: null} @@ -2051,7 +2078,7 @@ const CacheView = memo((props) => {
: - + + + + + + ); +}; + +export default DashboardOnboarding; + + diff --git a/frontend/src/components/Detection.jsx b/frontend/src/components/Detection.jsx index bd75e99d..35c1a994 100644 --- a/frontend/src/components/Detection.jsx +++ b/frontend/src/components/Detection.jsx @@ -143,6 +143,7 @@ const Detection = (props) => { size="small" sx={{ mr: 2 }} value={searchQuery} + disabled onChange={(e) => setSearchQuery(e.target.value)} /> {/* + {/**/} {detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ? - + 0 ? green : red}} /> @@ -345,7 +553,7 @@ const DetectionExplorer = (props) => { - {filteredRules?.length > 0 ? + {ruleInfo?.length > 0 ? { size="small" sx={{ mr: 2 }} value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={(e) => { + setSearchQuery(e?.target?.value?.replaceAll(" ", "_")?.toLowerCase()) + }} /> @@ -386,7 +596,7 @@ const DetectionExplorer = (props) => { { folderDisabled={folderDisabled} isDetectionActive={isDetectionActive} + ruleDetails={rule} ruleMapping={ruleMapping} setRuleMapping={setRuleMapping} diff --git a/frontend/src/components/DetectionRuleCard.jsx b/frontend/src/components/DetectionRuleCard.jsx index 1a2e9760..a9fcb44b 100644 --- a/frontend/src/components/DetectionRuleCard.jsx +++ b/frontend/src/components/DetectionRuleCard.jsx @@ -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 ( - {ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total}) + + {parsedRulename} {/*({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})*/} +
- (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); + + + {/* */} + (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); - }} - /> - + }} + /> + + + {/*
*/} + {selectedCategory === "sigma" || selectedCategory === "yara" ? + + + + + + : null} + {fileCategories !== undefined && fileCategories !== null && fileCategories.length > 1 ? ( - + + + Category + { /> + {!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org || null || selectedOrganization?.creator_org?.length > 0 ? null : + />} Workflow Backup Repository diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index f86bc1d7..6af8691e 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -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")) + selectedApp.actions === undefined || selectedApp.actions === null ? [] : + 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, diff --git a/frontend/src/components/PartnerDetails.jsx b/frontend/src/components/PartnerDetails.jsx index 9ca0aeff..7b7c102c 100644 --- a/frontend/src/components/PartnerDetails.jsx +++ b/frontend/src/components/PartnerDetails.jsx @@ -223,7 +223,7 @@ const PartnerDetails = (props) => {
- Name + Company Name { />
*/}
-
+
Solutions
{ variant="text" style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }} > - Name + Company Name { 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" diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx index 2ff15cdf..96a80435 100644 --- a/frontend/src/components/Priorities.jsx +++ b/frontend/src/components/Priorities.jsx @@ -1362,10 +1362,10 @@ print('"' + encoded + '"')
- Notification Workflow + Error Workflow - 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. You can point child org notifications into the parent org notification by choosing it in the list. + 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. You can point child org errors to a parent org's error workflow by choosing it in the list. {modalView} @@ -1614,12 +1614,12 @@ print('"' + encoded + '"')
} - Notifications ({ + Errors ({ notifications?.filter((notification) => showRead === true || notification.read === false).length }) - Notifications help you find potential problems with your workflows and apps.  + Error help you find potential problems with your workflows and apps.  { + 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 ( + + ) +} + +export default RunDetectionTest diff --git a/frontend/src/components/SchedulesTab.jsx b/frontend/src/components/SchedulesTab.jsx index cc91db50..26734560 100644 --- a/frontend/src/components/SchedulesTab.jsx +++ b/frontend/src/components/SchedulesTab.jsx @@ -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) => { }, }} > - + Run a Tenzir pipeline - Alpha feature. Deploys to the first available Orborus location. Explore Tenzir Pipelines. 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. Explore Tenzir Pipelines. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook. - -
+ +
+ + { + setNewPipelineValue(`load_tcp "0.0.0.0:1514" { read_syslog } | import`) + }} + label={"Syslog Listener (TCP)"} + variant="outlined" + color="secondary" + style={{ + marginRight: 10, + }} + /> + + { + 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, + }} + /> + + { + setNewPipelineValue(`export live=true | sigma "/tmp/sigma_rules" | to "${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}"`) + }} + label={"Sigma Rules"} + variant="outlined" + color="secondary" + style={{ + marginRight: 10, + }} + /> + + { + 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, + }} + /> + { 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) => { />
- + @@ -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. Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length}) +
+ Pipelines + + + Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "} + + Learn more + + + +
+ + + +
+
+ + + {["Status", "Command", "Environment", "Total Runs", "Actions"].map((header, index) => ( + + ))} + + {showLoader ? ( + [...Array(6)].map((_, rowIndex) => { + return ( + + {Array(5) + .fill() + .map((_, colIndex) => { + return ( + + + + ) + })} + + ) + } + ) + + ) : ( + pipelines?.length === 0 ? ( +
+ No pipelines found. +
+ + ):( + pipelines.map((pipeline, index) => { + var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; + if (index % 2 === 0) { + bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; + } + + return ( + + + + + + + + { + 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") + } + }}> + + + + + { + changePipelineState(pipeline, "stop"); + }}> + + + + + )} + /> + + ); + }) + ) + )} +
+ +
+ +
+
+
- + Schedules @@ -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) => { )}
-
- Pipelines - - - Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "} - - Learn more - - - -
- - - -
-
- - - {["Command", "Environment", "Total Runs", "Actions"].map((header, index) => ( - - ))} - - {showLoader ? ( - [...Array(6)].map((_, rowIndex) => { - return ( - - {Array(5) - .fill() - .map((_, colIndex) => { - return ( - - - - ) - })} - - ) - } - ) - - ): ( - pipelines?.length === 0 ? ( -
- No pipeline trigger found -
- - ):( - pipelines.map((pipeline, index) => { - var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; - if (index % 2 === 0) { - bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; - } - - return ( - - - - - - - - )} - /> - - ); - }) - ) - )} -
-
+
@@ -1096,3 +1224,4 @@ const SchedulesTab = memo((props) => { }); export default SchedulesTab; + diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index 3424d3d2..8a26556c 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -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', }} > +
{ File Editor ({localcodedata.length})
+ + + { + 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" + > + + + +
:
{ 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) { diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index 218cc060..63ea3abb 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -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") { diff --git a/frontend/src/views/AgentUI.jsx b/frontend/src/views/AgentUI.jsx index fab6d012..2360a426 100644 --- a/frontend/src/views/AgentUI.jsx +++ b/frontend/src/views/AgentUI.jsx @@ -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) => { 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 + + // + + 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" ? @@ -273,38 +436,167 @@ const AgentUI = (props) => { :
- - 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 = + + + + + 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 = + + + { + 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 + } + } + } + }} + > + + + + + + + const rerunButton = + + + { + e.preventDefault() + e.stopPropagation() + + //toast.info("Attempting to rerun this decision by itself.") + setDisableButtons(true) + RerunDecision(item.details) + }} + > + + + + + + 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 (
{ }} onMouseEnter={() => { if (!hovered) { - console.log("HOVER") + //console.log("HOVER") setHovered(true) } }} @@ -364,41 +656,50 @@ const AgentUI = (props) => {
{parsedCategory}
+ {/*
- {/* 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 + } +
+ */}
{item.label}
- +
- {currentDuration !== 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ? + {currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
- : null} + minHeight: 10, + maxHeight: 10, + borderRadius: theme.palette.borderRadius, + }}> +
+ : + + + }
@@ -407,24 +708,68 @@ const AgentUI = (props) => { maxWidth: 100, display: "flex", }}> - - - { - e.preventDefault() - e.stopPropagation() + {item.category === "ask" ? + + {rerunButton} + {/* + + + { + e.preventDefault() + e.stopPropagation() - toast.info("Attempting to rerun this decision by itself.") - setDisableButtons(true) - RerunDecision(item.details) - }} - > - - + toast.info("Approving this step.") + }} + > + + + + + + + { + e.preventDefault() + e.stopPropagation() + + toast.info("Stopping on this step.") + }} + > + + + + + */} + + + + { + 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'); + }} + > + + + + -
+ : + item.category === "agent" ? + rerunAgentButton + : + rerunButton + } {
+ + {showAuthentication && selectedApp.id !== undefined ? +
+ +
+ : null} + + {questions?.length > 0 && item?.status === "RUNNING" ? +
+ {questions.map((q, questionIndex) => { + return ( +
+ + {`${q.question}`} + + + { + 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) + } + }} + + /> +
+ ) + })} + + +
+ : null} + {open ?
@@ -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 (
+ {showAgentStarter ? - { - e.preventDefault(); - submitInput(actionInput); - }}> + { + e.preventDefault(); + submitInput(actionInput); + }} + > +
@@ -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) => { + + + + + + {buttonState === "timeline" ? : diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index e1adeb09..88adac1b 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -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 [ .# ]{" "} + PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "} { const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
{ 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) }} > diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index d104d43d..81d3cd39 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -1494,13 +1494,13 @@ const ApiExplorerWrapper = (props) => { />
+ style={{ + backgroundColor: theme.palette.inputColor, + padding: 15, + borderRadius: theme.palette?.borderRadius, + marginBottom: 30, + }} + > 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! diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 414a7af0..26f1179b 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -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 -
+ {index === extraAuth.length - 1 ? ( -
+ ); })} @@ -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 ( - - { + return ( + + { if (chipRequired) { currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1) } else { @@ -3524,27 +3528,28 @@ const AppCreator = (defaultprops) => { } setCurrentAction(currentAction); - setChipRequired(!chipRequired); - }} - /> - - ); - }; - - const setActionField = (field, value) => { - currentAction[field] = value - setCurrentAction(currentAction) - - //setUrlPathQueries(currentAction.queries) + setChipRequired(!chipRequired); + }} + /> + + ); }; - 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 : (
{ return (
-
- - Click required to flip - - } - 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, - }, - }} - /> - { - urlPathQueries[queryIndex].example = e.target.value.replaceAll( - "=", - "" - ) - - setUrlPathQueries(urlPathQueries) - }} - style={{flex: 2}} - InputProps={{ - style: { - color: theme.palette.text.primary, - }, - }} - /> -
+
+ { + urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "") + setUrlPathQueries(urlPathQueries) + }} + style={{flex: 3}} + InputProps={{ + style: { + color: theme.palette.text.primary, + }, + }} + /> + { + // 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, + }, + }} + /> +
{ @@ -3654,7 +3651,7 @@ const AppCreator = (defaultprops) => { deletePathQuery(queryIndex); }} > - +
); @@ -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 ( - {newActionModal} + {newActionModal} {error} diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index 1f434a83..38e7cc4f 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -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)";
+
@@ -2365,6 +2417,7 @@ const Apps2 = (props) => {
+ ); }; diff --git a/frontend/src/views/DashboardViews.jsx b/frontend/src/views/DashboardViews.jsx index 0748e80a..98d03344 100644 --- a/frontend/src/views/DashboardViews.jsx +++ b/frontend/src/views/DashboardViews.jsx @@ -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) => { }
- diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 8b4772c6..ac1babe2 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -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(" ", "-"); } diff --git a/frontend/src/views/NewDashboard.jsx b/frontend/src/views/NewDashboard.jsx new file mode 100644 index 00000000..630e6dec --- /dev/null +++ b/frontend/src/views/NewDashboard.jsx @@ -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: , percentage: STATIC_TIME_PERCENT, color: '#5cc879' }, + { value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: , percentage: STATIC_MONEY_PERCENT, color: '#5cc879' }, + { value: String(unreadCount), label: 'Total errors', icon: , percentage: "", color: '#f87171' }, + { value: String(readCount), label: 'Errors resolved', icon: , 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 ( +
+ 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 && ( +
+
+ + Loading dashboard… +
+
+ )} + {/* Header / Greeting */} + + {`${getGreeting()}, ${displayName ?? 'User'}!`} + <> + {sfwControls} + + + + {/* KPI cards */} + + {kpis.map((kpi) => ( + + { + if (kpi.label.toLowerCase().includes('total errors')) { + // navigate to notifications page + navigate('/admin?admin_tab=notifications'); + } + }} + + > + + + {kpi.value} + {kpi.label} + + + {kpi.icon} + {kpi.percentage} + + + + + ))} + + + {/* Success/Failed widget uses its own internal sub-cards; make wrapper transparent */} + + + + + {/* Runs over time section */} + + + +
+ ); +}; + +export default NewDashboard; + + diff --git a/frontend/src/views/RunWorkflow.jsx b/frontend/src/views/RunWorkflow.jsx index 654646b7..cf8e9ad8 100644 --- a/frontend/src/views/RunWorkflow.jsx +++ b/frontend/src/views/RunWorkflow.jsx @@ -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 (
{workflowQuestion !== "" ? null : - +
} {workflowQuestion !== "" ? null : validate.valid === false ?
- + {validate?.result !== undefined && validate?.result !== null && validate?.result.length > 0 ? + + : null } { 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) => {
- Loading Form Details... + Loading Details...
:
- {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) ?
{ }} 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}
: null} {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 :
{/* { {organization} - + {organization?.length > 0 && + + } {disabledButtons && message.length > 0 ? null : - + {message} } @@ -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 (
@@ -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) => { : - {disabledButtons ? "Already answered. You may close this window." : ""} + {disabledButtons ? "Question answered. You may close this window." : ""} } @@ -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
+ + {handleValidateForm(executionArgument) === false && disabledButtons === false ? + + All required questions have not been answered yet. + + : null} :
diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 32859549..702092da 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -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) diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index 86c28e18..02df7908 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -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) => { } }} > - +
Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId)?.name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}?
- 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.
{ 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 ( -
+
- {selectedCategory !== "" ?
{ }} /> - : null} + : null} { {currTab === 2 ? null : -
{ @@ -3183,6 +3194,7 @@ const Workflows2 = (props) => { + {appGroup.length > 0 ?
@@ -3437,7 +3449,7 @@ const Workflows2 = (props) => { {showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 && -
+
{
-
+ {currTab === 4 ? null : +
- {currTab === 2 ? ( - - ) : ( - - // - // - // ), - 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 ? ( + + ) : + ( + + // + // + // ), + 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 && ( - selected.length ? selected.join(', ') : 'All Categories'} + > + + All Categories + + {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 ( - { - 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)" - }, - }} - > -
- -
- - {category} - - - {usecase?.matches.length}/{usecase?.list.length} - -
-
-
- ) - })} - - ) - } - { - currTab === 2 && ( - - ) - } + const category = usecase?.name.split(" ")[1] + return ( + { + 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)" + }, + }} + > +
+ +
+ + {category} + + + {usecase?.matches.length}/{usecase?.list.length} + +
+
+
+ ) + })} + + ) + } + { + currTab === 2 && ( + + ) + } -
-
+
+
- - { + + { - const newView = !showExecutionStats - localStorage.setItem("showExecutionStats", newView) - setShowExecutionStats(!showExecutionStats) - }} - disabled={currTab === 2} - > - - - + const newView = !showExecutionStats + localStorage.setItem("showExecutionStats", newView) + setShowExecutionStats(!showExecutionStats) + }} + disabled={currTab === 2} + > + + + - - navigate("/workflows/debug")} - disabled={currTab === 2} - > - - - + + navigate("/workflows/debug")} + disabled={currTab === 2} + > + + + - - { - const newView = view === "grid" ? "list" : "grid"; - localStorage.setItem("workflowView", newView); - setView(newView); + + { + const newView = view === "grid" ? "list" : "grid"; + localStorage.setItem("workflowView", newView); + setView(newView); - if (view === "grid") { - setCurrTab(0) - } - }} - disabled={currTab === 2} - > - {view === "grid" ? - : - - } - - + if (view === "grid") { + setCurrTab(0) + } + }} + disabled={currTab === 2} + > + {view === "grid" ? + : + + } + + - - upload.click()} - disabled={currTab === 2} - > - {submitLoading ? - : - - } - - + + upload.click()} + disabled={currTab === 2} + > + {submitLoading ? + : + + } + + - (upload = ref)} - onChange={importFiles} - /> + (upload = ref)} + onChange={importFiles} + /> - - exportAllWorkflows(workflows)} - > - - - -
- -
+ + exportAllWorkflows(workflows)} + > + + + +
+ +
+
+ } + - -
{ ) : ( view === "grid" && currTab !== 2 ? ( <> -
{ + 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 ( +
{ + // 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 + } + }} + > + + {webhookName} + + + + webhook + + + { + 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" + > + + + + + ), + style: { + color: theme.palette.textFieldStyle.color, + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + } + }} + /> +
+ ) + })} + +