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,