diff --git a/src/App.tsx b/src/App.tsx index f27ad02..ecef852 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,8 @@ import { NotFoundPage } from "./pages/NotFoundPage"; import { PlaceholderPage } from "./pages/PlaceholderPage"; import { MainPage } from "./pages/main-page/MainPage"; import { SettingsPage } from "./pages/main-page/SettingsPage"; +import { FinDetailPage } from "./pages/main-page/fins-page/fin-detail-page/FinDetailPage"; +import { FinsPage } from "./pages/main-page/fins-page/FinsPage"; import { ExecutionDetailPage } from "./pages/main-page/monitoring-page/ExecutionDetailPage"; import { MonitoringPage } from "./pages/main-page/monitoring-page/MonitoringPage"; import { PlaybookCreatePage } from "./pages/main-page/playbooks-page/PlaybookCreatePage"; @@ -39,6 +41,8 @@ function App() { path={PATHS.MONITORING.DETAIL} element={} /> + } /> + } /> } /> } /> diff --git a/src/api/fin.ts b/src/api/fin.ts new file mode 100644 index 0000000..c4b7d61 --- /dev/null +++ b/src/api/fin.ts @@ -0,0 +1,13 @@ +import { Fin, FinListResponse } from "@/types"; +import { deleteToApi, fetchFromApi } from "./utils"; + +export const getFins = async (): Promise => { + const response = await fetchFromApi(`/api/fin/`); + return response.fins; +}; + +export const getFinById = (finId: string) => + fetchFromApi(`/api/fin/${finId}`); + +export const deleteFin = (finId: string) => + deleteToApi(`/api/fin/${finId}`); diff --git a/src/api/manual.ts b/src/api/manual.ts index debefca..9e7ff9a 100644 --- a/src/api/manual.ts +++ b/src/api/manual.ts @@ -1,16 +1,20 @@ import { - Execution, + RunStarted, InteractionCommandData, ManualOutArgsUpdatePayload, } from "@/types"; import { fetchFromApi, HttpMutationMethod, mutationToApi } from "./utils"; -export const postStepActionResult = (data: ManualOutArgsUpdatePayload) => - mutationToApi( - HttpMutationMethod.POST, - `/api/manual/continue`, +export const putStepActionResult = ( + runId: string, + stepRunId: string, + data: ManualOutArgsUpdatePayload, +) => + mutationToApi( + HttpMutationMethod.PUT, + `/api/manual/${runId}/${stepRunId}`, data, ); -export const getStepManualData = (executionId: string, stepId: string) => - fetchFromApi(`/api/manual/${executionId}/${stepId}`); +export const getStepManualData = (runId: string, stepRunId: string) => + fetchFromApi(`/api/manual/${runId}/${stepRunId}`); diff --git a/src/api/reporter.ts b/src/api/reporter.ts index 763ad4b..f01179a 100644 --- a/src/api/reporter.ts +++ b/src/api/reporter.ts @@ -1,8 +1,8 @@ -import { PlaybookExecutionReport } from "@/types"; +import { PlaybookRunReport } from "@/types"; import { fetchFromApi } from "./utils"; export const getReporterState = () => - fetchFromApi(`/api/reporter/`); + fetchFromApi(`/api/reporter/`); -export const getReportOfExecutionById = (executionId: string) => - fetchFromApi(`/api/reporter/${executionId}`); +export const getReportOfExecutionById = (runId: string) => + fetchFromApi(`/api/reporter/${runId}`); diff --git a/src/api/trigger.ts b/src/api/trigger.ts index 7926811..ada3a9c 100644 --- a/src/api/trigger.ts +++ b/src/api/trigger.ts @@ -1,11 +1,11 @@ -import { Execution, Variables } from "@/types"; +import { RunStarted, Variables } from "@/types"; import { HttpMutationMethod, mutationToApi } from "./utils"; export const triggerPlaybookById = ( playbookId: string, variables?: Variables, ) => { - return mutationToApi( + return mutationToApi( HttpMutationMethod.POST, `/api/trigger/playbook/${playbookId}`, variables ?? {}, diff --git a/src/pages/main-page/MainPage.tsx b/src/pages/main-page/MainPage.tsx index 5d3346c..c1d9642 100644 --- a/src/pages/main-page/MainPage.tsx +++ b/src/pages/main-page/MainPage.tsx @@ -1,6 +1,7 @@ import { Activity, BookOpen, + Cable, FilePlusCorner, Home, LayoutDashboard, @@ -59,6 +60,7 @@ const NAV_ROUTES: NavRoute[] = [ icon: LayoutDashboard, }, { path: PATHS.MONITORING.BASE, label: "Monitoring", icon: Activity }, + { path: PATHS.FINS.BASE, label: "Fins", icon: Cable }, { path: PATHS.SETTINGS, label: "Settings (Beta)", icon: Settings }, ]; @@ -107,7 +109,9 @@ export const MainPage: React.FC = () => { (route.path === PATHS.MONITORING.BASE && location.pathname.includes(PATHS.MONITORING.BASE)) || (route.path === PATHS.PLAYBOOKS.BASE && - location.pathname.includes(PATHS.PLAYBOOKS.BASE)) + location.pathname.includes(PATHS.PLAYBOOKS.BASE)) || + (route.path === PATHS.FINS.BASE && + location.pathname.includes(PATHS.FINS.BASE)) } onClick={() => handleNavigation(route.path)} > diff --git a/src/pages/main-page/fins-page/FinsPage.tsx b/src/pages/main-page/fins-page/FinsPage.tsx new file mode 100644 index 0000000..151084a --- /dev/null +++ b/src/pages/main-page/fins-page/FinsPage.tsx @@ -0,0 +1,104 @@ +import { useQuery } from "@tanstack/react-query"; +import React from "react"; +import { useNavigate } from "react-router"; + +import { getFins } from "@/api/fin"; +import { getErrorFromApiResponse } from "@/api/utils"; +import { + Badge, + CardBody, + CardContainer, + CardHeader, + CardTitle, + Cell, + CenteredCardContent, + HeaderCell, + Row, + Spacer, + Spinner, + Table, + TableBody, + TableHead, + Text, +} from "@/components"; +import { ThemeSize, ThemeVariant } from "@/components/utils"; +import { Fin } from "@/types"; +import { formatDateTime, PATHS, sortByString } from "@/utils"; + +export const FinsPage: React.FC = () => { + const navigate = useNavigate(); + const { data, isLoading, isError, error } = useQuery({ + queryKey: ["fins"], + queryFn: getFins, + refetchInterval: 5000, + refetchOnWindowFocus: false, + }); + + const fins: Fin[] = sortByString( + [...(data || [])], + (fin) => fin.display_name || fin.fin_id, + ); + const parsedError = getErrorFromApiResponse(error); + + const navigateToDetail = (finId: string) => { + navigate(PATHS.FINS.DETAIL.replace(":finId", finId)); + }; + + const renderBody = () => { + if (isLoading) + return ( + + + + ); + if (isError) + return {parsedError?.message || "Could not load fins"}; + if (fins.length === 0) + return No fins are currently registered.; + return ( + + + + Display name + Capabilities + Protocol version + Registered + Last seen + + + + {fins.map((fin) => ( + navigateToDetail(fin.fin_id)} + > + {fin.display_name || fin.fin_id} + + + {fin.capabilities.map((capability) => ( + + {capability.type} + + ))} + + + {fin.protocol_version || "—"} + {formatDateTime(fin.registered_at)} + {formatDateTime(fin.last_seen)} + + ))} + +
+ ); + }; + + return ( + + + Registered fins ({fins.length}) + + {renderBody()} + + ); +}; diff --git a/src/pages/main-page/fins-page/fin-detail-page/FinDetailPage.styles.tsx b/src/pages/main-page/fins-page/fin-detail-page/FinDetailPage.styles.tsx new file mode 100644 index 0000000..5ef304c --- /dev/null +++ b/src/pages/main-page/fins-page/fin-detail-page/FinDetailPage.styles.tsx @@ -0,0 +1,55 @@ +import styled from "styled-components"; + +export const DetailsGrid = styled.div` + width: 100%; + box-sizing: border-box; + display: grid; + grid-template-columns: 1fr 1fr; + gap: ${({ theme }) => theme.spacing.lg}; +`; + +export const DetailsItem = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing.sm}; + align-items: flex-start; +`; + +export const CapabilityCard = styled.div` + width: 100%; + box-sizing: border-box; + padding: ${({ theme }) => theme.spacing.md}; + + border: 1px solid ${({ theme }) => theme.colors.border.light}; + border-radius: ${({ theme }) => theme.radius.md}; +`; + +export const CapabilityHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: ${({ theme }) => theme.spacing.sm}; +`; + +export const StepExamplesList = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing.md}; + margin-top: ${({ theme }) => theme.spacing.md}; +`; + +export const CodeBlock = styled.pre` + width: 100%; + box-sizing: border-box; + margin: 0; + padding: ${({ theme }) => theme.spacing.md}; + + border-radius: ${({ theme }) => theme.radius.md}; + background-color: ${({ theme }) => theme.colors.background.secondary}; + color: ${({ theme }) => theme.colors.text.primary}; + font: ${({ theme }) => theme.typography.code_small.font}; + + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +`; diff --git a/src/pages/main-page/fins-page/fin-detail-page/FinDetailPage.tsx b/src/pages/main-page/fins-page/fin-detail-page/FinDetailPage.tsx new file mode 100644 index 0000000..5f5e089 --- /dev/null +++ b/src/pages/main-page/fins-page/fin-detail-page/FinDetailPage.tsx @@ -0,0 +1,192 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { ArrowLeft, Trash } from "lucide-react"; +import React, { useState } from "react"; +import toast from "react-hot-toast"; +import { useNavigate, useParams } from "react-router"; + +import { deleteFin, getFinById } from "@/api/fin"; +import { formatErrorForToast, getErrorFromApiResponse } from "@/api/utils"; +import { + Badge, + Button, + ButtonWidth, + CardBody, + CardContainer, + CardHeader, + CardTitle, + CenteredCardContent, + ConfirmDialog, + CopyButton, + FormLabel, + Icon, + Link, + Spacer, + Spinner, + Text, + ThemeSize, + ThemeVariant, +} from "@/components"; +import { formatDateTime, PATHS } from "@/utils"; + +import { + CapabilityCard, + CapabilityHeader, + CodeBlock, + DetailsGrid, + DetailsItem, + StepExamplesList, +} from "./FinDetailPage.styles"; + +export const FinDetailPage: React.FC = () => { + const { finId } = useParams(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false); + + const { data, isLoading, isError, error } = useQuery({ + queryKey: ["fin", finId], + queryFn: () => getFinById(finId!), + enabled: !!finId, + refetchOnWindowFocus: false, + }); + + const deleteMutation = useMutation({ + mutationFn: () => deleteFin(finId!), + onSuccess: () => { + toast.success("Fin deleted"); + queryClient.invalidateQueries({ queryKey: ["fins"] }); + navigate(PATHS.FINS.BASE); + }, + onError: (err: Error) => { + toast.error(formatErrorForToast(err, "Failed to delete fin")); + }, + }); + + const fin = data; + const parsedError = getErrorFromApiResponse(error); + + const renderBody = () => { + if (isLoading) + return ( + + + + ); + if (isError || !fin) + return ( + + {isError + ? parsedError?.message || "Could not load fin" + : "Fin not found"} + + ); + + return ( + + + + Fin ID + + {fin.fin_id} + + + + + Protocol version + {fin.protocol_version || "—"} + + + Registered + {formatDateTime(fin.registered_at)} + + + Last seen + {formatDateTime(fin.last_seen)} + + + + + Capabilities ({fin.capabilities.length}) + {fin.capabilities.map((capability) => ( + + + + {capability.type} + {capability.version && v{capability.version}} + + + {capability.description && {capability.description}} + {capability.step_examples && + capability.step_examples.length > 0 && ( + + + Example step{capability.step_examples.length > 1 && "s"} + + {capability.step_examples.map((step, idx) => ( + + {JSON.stringify(step, null, 2)} + + ))} + + )} + + ))} + + + ); + }; + + return ( + + + + + Back to fins + + {fin && ( + + )} + + + + {fin?.display_name || fin?.fin_id || "Fin"} + + {renderBody()} + + {fin && ( + setOpenDeleteConfirm(false)} + $onConfirm={() => { + setOpenDeleteConfirm(false); + deleteMutation.mutate(); + }} + /> + )} + + ); +}; diff --git a/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx b/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx index 36572bb..798467f 100644 --- a/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx +++ b/src/pages/main-page/monitoring-page/ExecutionDetailPage.tsx @@ -29,7 +29,7 @@ import { getIconFromStatus, getPlaybookStatusFromSoarcaStatus, } from "@/pages/main-page/monitoring-page/utils"; -import { PlaybookExecutionReport, StepExecutionReport } from "@/types"; +import { PlaybookRunReport, StepRunReport } from "@/types"; import { DetailsGrid, DetailsItem, @@ -65,7 +65,7 @@ export const ExecutionDetailPage: React.FC = () => { refetchIntervalInBackground: true, }); - const report: PlaybookExecutionReport | undefined = data; + const report: PlaybookRunReport | undefined = data; const steps = Object.values(report?.step_results || {}); const status = getPlaybookStatusFromSoarcaStatus(report?.status); const parsedError = getErrorFromApiResponse(error); @@ -87,8 +87,8 @@ export const ExecutionDetailPage: React.FC = () => { - Execution ID - {report.execution_id} + Run ID + {report.run_id} Status @@ -151,7 +151,7 @@ export const ExecutionDetailPage: React.FC = () => { @@ -178,11 +178,11 @@ export const ExecutionDetailPage: React.FC = () => { }; interface TabContentRendererProps { - steps: StepExecutionReport[]; + steps: StepRunReport[]; playbookId?: string; executionId?: string; onRefetch?: () => Promise< - QueryObserverResult + QueryObserverResult >; } diff --git a/src/pages/main-page/monitoring-page/MonitoringPage.tsx b/src/pages/main-page/monitoring-page/MonitoringPage.tsx index 5588ca0..316591a 100644 --- a/src/pages/main-page/monitoring-page/MonitoringPage.tsx +++ b/src/pages/main-page/monitoring-page/MonitoringPage.tsx @@ -29,7 +29,7 @@ import { getBadgeVariantFromStatus, getPlaybookStatusFromSoarcaStatus, } from "@/pages/main-page/monitoring-page/utils"; -import { PlaybookExecutionReport } from "@/types"; +import { PlaybookRunReport } from "@/types"; import { computeDurationMs, formatDateTime, @@ -47,7 +47,7 @@ interface PlaybookRow { hasActionRequired: boolean; // this will indicate if there are ongoing manual actions that require user intervention } -const hasOngoingManualActions = (report: PlaybookExecutionReport): boolean => { +const hasOngoingManualActions = (report: PlaybookRunReport): boolean => { const steps = Object.values(report.step_results || {}); return steps.some( (step) => step.automated_execution === false && step.status === "ongoing", @@ -55,12 +55,12 @@ const hasOngoingManualActions = (report: PlaybookExecutionReport): boolean => { }; const parseReportToRows = ( - reports: PlaybookExecutionReport[], + reports: PlaybookRunReport[], ): PlaybookRow[] => { return reports.map((report) => { const status = getPlaybookStatusFromSoarcaStatus(report.status); return { - id: report.execution_id, + id: report.run_id, name: report.name, startTime: formatDateTime(report.started), durationMs: computeDurationMs(report.started, report.ended), diff --git a/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx b/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx index e5b101c..5cb9f31 100644 --- a/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx +++ b/src/pages/main-page/monitoring-page/details-tab-view/DetailsTabView.tsx @@ -22,7 +22,7 @@ import { getIconFromStepStatus, getStepStatusFromSoarcaStatus, } from "@/pages/main-page/monitoring-page/utils"; -import { StepExecutionReport } from "@/types"; +import { StepRunReport } from "@/types"; import { computeDurationMs, decodeBase64, @@ -65,7 +65,7 @@ const renderAutomatedIcon = (isAutomated: boolean | undefined) => { }; interface DetailsTabViewProps { - steps: StepExecutionReport[]; + steps: StepRunReport[]; } export const DetailsTabView: React.FC = ({ steps }) => { @@ -76,12 +76,12 @@ export const DetailsTabView: React.FC = ({ steps }) => { return ( {steps.map((step) => { - const { step_id, status: soarcaStatus } = step; + const { step_run_id, status: soarcaStatus } = step; const status = getStepStatusFromSoarcaStatus(soarcaStatus); return ( = ({ steps }) => { - Execution ID - {step.execution_id || "—"} + Run ID + {step.run_id || "—"} Status diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx index bec1a15..7630ccf 100644 --- a/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx +++ b/src/pages/main-page/monitoring-page/timeline-tab-view/ManualActionModal.tsx @@ -7,15 +7,15 @@ import { import React, { useState } from "react"; import toast from "react-hot-toast"; -import { getStepManualData, postStepActionResult } from "@/api/manual"; +import { getStepManualData, putStepActionResult } from "@/api/manual"; import { formatErrorForToast } from "@/api/utils"; import { Button, Modal, RadioGroup, Spinner, ThemeVariant } from "@/components"; import { - Execution, + RunStarted, ManualOutArgsUpdatePayload, ManualResponseStatus, - PlaybookExecutionReport, - StepExecutionReport, + PlaybookRunReport, + StepRunReport, Variables, } from "@/types"; import { @@ -37,12 +37,12 @@ import { import { VariableInput } from "./VariableInputs"; interface ManualActionModalProps { - activeStep: StepExecutionReport | null; + activeStep: StepRunReport | null; playbookId?: string; executionId?: string; onClose: () => void; onSuccess?: () => Promise< - QueryObserverResult + QueryObserverResult >; } @@ -74,14 +74,14 @@ export const ManualActionModal: React.FC = ({ >({}); const { data: interactionData, isLoading: isLoadingOutArgs } = useQuery({ - queryKey: ["manual", executionId, activeStep?.step_id], - queryFn: () => getStepManualData(executionId!, activeStep!.step_id), + queryKey: ["manual", executionId, activeStep?.step_run_id], + queryFn: () => getStepManualData(executionId!, activeStep!.step_run_id), enabled: !!activeStep && !!executionId, }); const mutation = useMutation({ mutationFn: (payload: ManualOutArgsUpdatePayload) => - postStepActionResult(payload), + putStepActionResult(executionId!, activeStep!.step_run_id, payload), onSuccess: async () => { toast.success("Action submitted"); if (onSuccess) { @@ -134,9 +134,6 @@ export const ManualActionModal: React.FC = ({ action === ActionType.CONFIRM ? "success" : "failure"; const payload: ManualOutArgsUpdatePayload = { - execution_id: executionId, - playbook_id: playbookId, - step_id: activeStep.step_id, type: "manual-command-info", response_status: responseStatus, response_out_args, @@ -198,11 +195,11 @@ export const ManualActionModal: React.FC = ({ }; interface ModalContentProps { - activeStep: StepExecutionReport | null; + activeStep: StepRunReport | null; outArgs: Variables; isLoadingOutArgs: boolean; mutation: UseMutationResult< - Execution, + RunStarted, Error, ManualOutArgsUpdatePayload, unknown diff --git a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx index 52f441c..8ecfc1a 100644 --- a/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx +++ b/src/pages/main-page/monitoring-page/timeline-tab-view/TimelineTabView.tsx @@ -1,4 +1,4 @@ -import { BadgeAlert, Check, Hand, X } from "lucide-react"; +import { BadgeAlert, Check, Flag, Hand, X } from "lucide-react"; import React, { useState } from "react"; import { @@ -16,7 +16,7 @@ import { getUserCompletedActionCardProperties, UserActionResponse, } from "@/pages/main-page/monitoring-page/utils"; -import { PlaybookExecutionReport, StepExecutionReport } from "@/types"; +import { PlaybookRunReport, StepRunReport } from "@/types"; import { computeDurationMs, formatDateTime, @@ -45,12 +45,12 @@ import { UserReplyItem, } from "./TimelineTabView.styles"; -const dateExtractor = (step: StepExecutionReport) => { +const dateExtractor = (step: StepRunReport) => { const date = parseDate(step.started); return date ? date.getTime() : undefined; }; -const getUserResponses = (step: StepExecutionReport): UserActionResponse[] => { +const getUserResponses = (step: StepRunReport): UserActionResponse[] => { const stepVariables = step.variables; if (!stepVariables) { @@ -67,11 +67,11 @@ const getUserResponses = (step: StepExecutionReport): UserActionResponse[] => { }; interface TimelineViewProps { - steps: StepExecutionReport[]; + steps: StepRunReport[]; playbookId?: string; executionId?: string; onRefetch?: () => Promise< - QueryObserverResult + QueryObserverResult >; } @@ -84,7 +84,7 @@ export const TimelineTabView: React.FC = ({ // We use this state to pass the right step to the manual action modal // when the user clicks on the "User action required" card const [selectedActionStep, setSelectedActionStep] = - useState(null); + useState(null); const resetSelectedActionStep = () => { setSelectedActionStep(null); @@ -97,8 +97,14 @@ export const TimelineTabView: React.FC = ({ {sortedSteps.map((step, stepIndex) => { - const { step_id, status: soarcaStatus, automated_execution } = step; + const { + step_run_id, + status: soarcaStatus, + automated_execution, + type: stepType, + } = step; const status = getStepStatusFromSoarcaStatus(soarcaStatus); + const isMarkerStep = stepType === "start" || stepType === "end"; const isStepTerminated = status !== StepExecutionStatus.Running; const isStepManualAndOngoing = @@ -117,13 +123,15 @@ export const TimelineTabView: React.FC = ({ let rowIndex = 0; return ( - + - {status === StepExecutionStatus.Running ? ( + {isMarkerStep ? ( + + ) : status === StepExecutionStatus.Running ? ( = ({ })} = ({ }; interface StepCardProps { - step: StepExecutionReport; + step: StepRunReport; } const StepCard: React.FC = ({ step }) => { diff --git a/src/pages/main-page/monitoring-page/utils.ts b/src/pages/main-page/monitoring-page/utils.ts index 111e0b7..29b2bed 100644 --- a/src/pages/main-page/monitoring-page/utils.ts +++ b/src/pages/main-page/monitoring-page/utils.ts @@ -6,13 +6,13 @@ import { StepExecutionStatus, } from "@/enums"; import { - SoarcaPlaybookExecutionStatus, - SoarcaStepExecutionStatus, + SoarcaPlaybookRunStatus, + SoarcaStepRunStatus, } from "@/types"; import { Check, LucideIcon, PlayIcon, X } from "lucide-react"; const SOARCA_STEP_STATUS_MAP: Record< - SoarcaStepExecutionStatus, + SoarcaStepRunStatus, StepExecutionStatus > = { [SoarcaApiStepExecutionStatus.SUCCESS]: StepExecutionStatus.Executed, @@ -28,7 +28,7 @@ const SOARCA_STEP_STATUS_MAP: Record< }; export function getStepStatusFromSoarcaStatus( - stepStatus?: SoarcaStepExecutionStatus, + stepStatus?: SoarcaStepRunStatus, ): StepExecutionStatus { return stepStatus ? SOARCA_STEP_STATUS_MAP[stepStatus] @@ -36,7 +36,7 @@ export function getStepStatusFromSoarcaStatus( } const SOARCA_PLAYBOOK_STATUS_MAP: Record< - SoarcaPlaybookExecutionStatus, + SoarcaPlaybookRunStatus, PlaybookExecutionStatus > = { [SoarcaApiPlaybookExecutionStatus.SUCCESS]: PlaybookExecutionStatus.Executed, @@ -45,7 +45,7 @@ const SOARCA_PLAYBOOK_STATUS_MAP: Record< }; export function getPlaybookStatusFromSoarcaStatus( - playbookStatus?: SoarcaPlaybookExecutionStatus, + playbookStatus?: SoarcaPlaybookRunStatus, ): PlaybookExecutionStatus { return playbookStatus ? SOARCA_PLAYBOOK_STATUS_MAP[playbookStatus] diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx index 0682aad..a225767 100644 --- a/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx +++ b/src/pages/main-page/playbooks-page/playbook-detail-page/PlaybookDetailPage.tsx @@ -25,7 +25,7 @@ import { ThemeSize, ThemeVariant, } from "@/components"; -import { Execution, Playbook } from "@/types"; +import { RunStarted, Playbook } from "@/types"; import { PATHS } from "@/utils"; import { generatePlaybookFilename, getOrderedSteps } from "../utils"; @@ -57,11 +57,9 @@ export const PlaybookDetailPage: React.FC = () => { const [openRunModal, setOpenRunModal] = useState(false); const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false); - const navigateToExecution = (data: Execution) => { - if (data?.execution_id) { - navigate( - PATHS.MONITORING.DETAIL.replace(":executionId", data.execution_id), - ); + const navigateToExecution = (data: RunStarted) => { + if (data?.run_id) { + navigate(PATHS.MONITORING.DETAIL.replace(":executionId", data.run_id)); } else { navigate(PATHS.MONITORING.BASE); } @@ -69,7 +67,7 @@ export const PlaybookDetailPage: React.FC = () => { const mutation = useMutation({ mutationFn: () => triggerPlaybookById(playbookId!), - onSuccess: (data: Execution) => { + onSuccess: (data: RunStarted) => { toast.success("Playbook started"); navigateToExecution(data); }, diff --git a/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx b/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx index c16c5ea..a3e3afc 100644 --- a/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx +++ b/src/pages/main-page/playbooks-page/playbook-detail-page/RunPlaybookModal.tsx @@ -6,7 +6,7 @@ import { triggerPlaybookById } from "@/api/trigger"; import { formatErrorForToast } from "@/api/utils"; import { Button, Modal, ThemeVariant } from "@/components"; import { VariableInput } from "@/pages/main-page/monitoring-page/timeline-tab-view/VariableInputs"; -import { Execution, Playbook, Variables } from "@/types"; +import { RunStarted, Playbook, Variables } from "@/types"; import { Description, @@ -22,7 +22,7 @@ interface RunPlaybookModalProps { playbook: Playbook; isOpen: boolean; onClose: () => void; - onSuccess: (execution: Execution) => void; + onSuccess: (execution: RunStarted) => void; } export const RunPlaybookModal: React.FC = ({ @@ -42,7 +42,7 @@ export const RunPlaybookModal: React.FC = ({ const mutation = useMutation({ mutationFn: (variables: Variables | undefined) => triggerPlaybookById(playbook.id, variables), - onSuccess: (data: Execution) => { + onSuccess: (data: RunStarted) => { toast.success("Playbook started"); onSuccess(data); onClose(); diff --git a/src/types/fin.d.ts b/src/types/fin.d.ts new file mode 100644 index 0000000..189950e --- /dev/null +++ b/src/types/fin.d.ts @@ -0,0 +1,26 @@ +import { Step } from "./cacao"; +import { ISODateString } from "./common"; + +// One executable capability a Fin offers, declared at registration time. +// A Fin registration may declare more than one Capability. +export type FinCapability = { + type: string; // routing key: agent_definitions[...].type in a playbook + description?: string; + version?: string; + step_examples?: Step[]; // illustrative example steps, never validated by SOARCA +}; + +// A currently-registered Fin, as returned by the read-only discovery API +// (GET /api/fin/, GET /api/fin/{fin_id}). The fin_token is never exposed. +export type Fin = { + fin_id: string; + display_name?: string; + protocol_version?: string; + capabilities: FinCapability[]; + registered_at: ISODateString; + last_seen: ISODateString; +}; + +export type FinListResponse = { + fins: Fin[]; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 550b47e..32e8978 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ export * from "./cacao"; export * from "./common"; +export * from "./fin"; export * from "./keymanagement"; export * from "./manual"; export * from "./reporter"; diff --git a/src/types/manual.d.ts b/src/types/manual.d.ts index 2feeee3..7d538a9 100644 --- a/src/types/manual.d.ts +++ b/src/types/manual.d.ts @@ -1,24 +1,36 @@ -import { AgentTarget, Variables } from "./cacao"; +import { AgentTarget, AuthenticationInformation, Variables } from "./cacao"; export type ManualResponseStatus = "success" | "failure"; -export type InteractionCommandData = { +export type ManualCommand = { command: string; commandb64?: boolean; description: string; - execution_id: string; +}; + +export type ResolvedTarget = { + target: AgentTarget; + authentication?: AuthenticationInformation; +}; + +// One pending manual command/interaction: a whole step (commands + targets), +// individually addressable by (run_id, step_run_id) since a +// step_id can recur (e.g. overlapping while-loop iterations). +export type InteractionCommandData = { + commands: ManualCommand[]; + run_id: string; out_args: Variables; playbook_id: string; + step_run_id: string; step_id: string; - target: AgentTarget; - type: string; // e.g. "execution-status" + targets: ResolvedTarget[]; + type: string; // "manual-command-info" }; +// Body for PUT /manual/{run_id}/{step_run_id} — the ids are +// resolved from the URL, not repeated in the payload. export type ManualOutArgsUpdatePayload = { - execution_id: string; - playbook_id: string; response_out_args: Variables; response_status: ManualResponseStatus; - step_id: string; type: string; }; diff --git a/src/types/reporter.d.ts b/src/types/reporter.d.ts index 04fe194..3256977 100644 --- a/src/types/reporter.d.ts +++ b/src/types/reporter.d.ts @@ -1,22 +1,24 @@ import { Variable } from "./cacao"; import { ISODateString } from "./common"; -export type StepExecutionReport = { +export type StepRunReport = { automated_execution?: boolean; commands_b64: string[]; description: string; ended: ISODateString; executed_by: string; - execution_id: string; + run_id: string; name: string; started: ISODateString; - status: SoarcaStepExecutionStatus; + status: SoarcaStepRunStatus; status_text: string; + step_run_id: string; step_id: string; + type: string; variables: Record; }; -export type SoarcaStepExecutionStatus = +export type SoarcaStepRunStatus = | "successfully_executed" | "ongoing" | "failed" @@ -25,21 +27,19 @@ export type SoarcaStepExecutionStatus = | "timeout_error" | "exception_condition_error"; -export type PlaybookExecutionReport = { +export type PlaybookRunReport = { description: string; ended: ISODateString; - execution_id: string; + run_id: string; name: string; playbook_id: string; request_interval: number; started: ISODateString; - status: SoarcaPlaybookExecutionStatus; + status: SoarcaPlaybookRunStatus; status_text: string; - step_results: Record; + step_results: Record; type: string; }; -export type SoarcaPlaybookExecutionStatus = - | "successfully_executed" - | "ongoing" - | "failed"; +export type SoarcaPlaybookRunStatus = + "successfully_executed" | "ongoing" | "failed"; diff --git a/src/types/trigger.d.ts b/src/types/trigger.d.ts index 4d5e1ae..b4e851f 100644 --- a/src/types/trigger.d.ts +++ b/src/types/trigger.d.ts @@ -1,4 +1,4 @@ -export type Execution = { - execution_id: string; - payload: string; // playbook id or reference +export type RunStarted = { + run_id: string; + playbook_id: string; }; diff --git a/src/utils.ts b/src/utils.ts index 8dd3d57..4b43f10 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,10 @@ export const PATHS = { BASE: "/monitoring", DETAIL: "/monitoring/:executionId", }, + FINS: { + BASE: "/fins", + DETAIL: "/fins/:finId", + }, SETTINGS: "/settings", LOGIN: "/login", NOT_FOUND: "/are-you-lost", @@ -38,7 +42,7 @@ export const PATHS = { * @example * sortByString(users, user => user.name, true, false); */ -export const sortByString = ( +export const sortByString = ( array: T[], keyExtractor: (item: T) => string, ascending: boolean = true, @@ -66,7 +70,7 @@ export const sortByString = ( * @example * sortByNumber(products, product => product.price, false); */ -export const sortByNumber = ( +export const sortByNumber = ( array: T[], keyExtractor: (item: T) => number | undefined, ascending: boolean = true,