diff --git a/frontend/src/components/badges.tsx b/frontend/src/components/badges.tsx index 055beb42..da094ca5 100644 --- a/frontend/src/components/badges.tsx +++ b/frontend/src/components/badges.tsx @@ -9,24 +9,29 @@ interface StatusBadgeProps } const statusColors = { - low: "bg-cyan-100 border-cyan-600 text-cyan-700", - medium: "bg-yellow-100 border-yellow-600 text-yellow-700", - high: "bg-orange-100 border-orange-600 text-orange-700", - critical: "bg-red-100 border-red-400 text-red-700", + normal: "bg-blue-100/70 border-blue-600/70 text-blue-700/80", + low: "bg-yellow-100/80 border-yellow-600/70 text-yellow-700/80", + medium: "bg-orange-100 border-orange-600 text-orange-700", + high: "bg-red-100 border-red-400 text-red-700", + critical: "bg-fuchsia-100 border-fuchsia-400 text-fuchsia-700", malicious: "bg-red-100 border-red-400 text-red-700", success: "bg-green-100 border-green-600 text-green-700", benign: "bg-green-100 border-green-600 text-green-700", } -export function StatusBadge({ status, children }: StatusBadgeProps) { +const defaultStatusColor = "border-slate-400/70 bg-slate-50 text-slate-600/80" + +export function StatusBadge({ status, children, className }: StatusBadgeProps) { return ( {children} diff --git a/frontend/src/components/cases/columns.tsx b/frontend/src/components/cases/columns.tsx index 1afe2ba2..1e3870af 100644 --- a/frontend/src/components/cases/columns.tsx +++ b/frontend/src/components/cases/columns.tsx @@ -82,22 +82,16 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => { - const priority = priorities.find( - (priority) => - priority.value === row.getValue("priority") - ) - - if (!priority) { - return null - } + const priority = row.getValue("priority") + const { label, icon: Icon } = priorities.find( + (p) => p.value === priority + )! return ( -
- {priority.icon && ( - - )} - {priority.label} -
+ + + {label} + ) }, filterFn: (row, id, value) => { @@ -237,7 +231,7 @@ export const columns: ColumnDef[] = [ } return (
- + {tags.length > 0 ? tags.map(({ tag, value, is_ai_generated }, idx) => ( diff --git a/frontend/src/components/cases/data/categories.tsx b/frontend/src/components/cases/data/categories.tsx index 02878a08..a20c0bf2 100644 --- a/frontend/src/components/cases/data/categories.tsx +++ b/frontend/src/components/cases/data/categories.tsx @@ -1,17 +1,20 @@ import { AlertTriangleIcon, - ArrowDownIcon, - ArrowRightIcon, - ArrowUpIcon, CheckCircleIcon, CircleIcon, FlagTriangleRightIcon, InfoIcon, + LucideIcon, ShieldAlertIcon, ShieldOffIcon, + SignalHighIcon, + SignalIcon, + SignalMediumIcon, TrafficConeIcon, } from "lucide-react" +import { CasePriorityType } from "@/types/schemas" + export const statuses = [ { value: "open", @@ -41,21 +44,25 @@ export const statuses = [ ] export type Status = (typeof statuses)[number]["value"] -export const priorities = [ +export const priorities: { + label: string + value: CasePriorityType + icon: LucideIcon +}[] = [ { label: "Low", value: "low", - icon: ArrowDownIcon, + icon: SignalMediumIcon, }, { label: "Medium", value: "medium", - icon: ArrowRightIcon, + icon: SignalHighIcon, }, { label: "High", value: "high", - icon: ArrowUpIcon, + icon: SignalIcon, }, { label: "Critical", diff --git a/frontend/src/components/cases/panel-content.tsx b/frontend/src/components/cases/panel-content.tsx index 14c6fe3e..6b3de7ca 100644 --- a/frontend/src/components/cases/panel-content.tsx +++ b/frontend/src/components/cases/panel-content.tsx @@ -1,12 +1,16 @@ "use client" import React from "react" -import { TagsIcon } from "lucide-react" +import { useParams } from "next/navigation" +import { useSession } from "@/providers/session" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { Bell, ShieldQuestion, Smile, TagsIcon } from "lucide-react" import SyntaxHighlighter from "react-syntax-highlighter" import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs" -import { Case } from "@/types/schemas" -import { Card, CardContent } from "@/components/ui/card" +import { Case, CaseStatusType } from "@/types/schemas" +import { fetchCase, updateCase } from "@/lib/cases" +import { Card } from "@/components/ui/card" import { Select, SelectContent, @@ -24,66 +28,174 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" +import { toast } from "@/components/ui/use-toast" import { StatusBadge } from "@/components/badges" -import { statuses } from "@/components/cases/data/categories" +import { priorities, statuses } from "@/components/cases/data/categories" import { AIGeneratedFlair } from "@/components/flair" import { LabelsTable } from "@/components/labels-table" +import { CenteredSpinner } from "@/components/loading/spinner" +import { AlertNotification } from "@/components/notifications" type TStatus = (typeof statuses)[number] interface CasePanelContentProps { - currentCase: Case + caseId: string } -export function CasePanelContent({ - currentCase: { +export function CasePanelContent({ caseId }: CasePanelContentProps) { + const session = useSession() + const { workflowId } = useParams<{ + workflowId: string + }>() + const queryClient = useQueryClient() + const { + data: case_, + isLoading, + error, + } = useQuery({ + queryKey: ["case", caseId], + queryFn: async () => await fetchCase(session, workflowId, caseId), + }) + const { mutateAsync } = useMutation({ + mutationFn: (newCase: Case) => + updateCase(session, workflowId, caseId, newCase), + onSuccess: (data) => { + toast({ + title: "Updated case", + description: "Your case has been updated successfully.", + }) + queryClient.invalidateQueries({ + queryKey: ["case", caseId], + }) + queryClient.invalidateQueries({ + queryKey: ["cases"], + }) + }, + onError: (error) => { + console.error("Failed to update action:", error) + toast({ + title: "Failed to save action", + description: "Could not update your action. Please try again.", + }) + }, + }) + + if (isLoading) { + return + } + if (error || !case_) { + return ( + + ) + } + const { id, title, + status: caseStatus, priority, malice, + action, + tags, payload, context, - status: caseStatus, - action, suppression, - tags, - }, -}: CasePanelContentProps) { - const currentStatus = statuses.find((status) => status.value === caseStatus) + } = case_ + const handleStatusChange = async (newStatus: CaseStatusType) => { + console.log("Updating status to", newStatus) + await mutateAsync({ + ...case_, + status: newStatus, + }) + } + + const currentStatus = statuses.find((status) => status.value === caseStatus)! + const { label, icon: Icon } = priorities.find((p) => p.value === priority)! return ( - +
- Case #{id} + Case #{id}
- {title} - -
-
- priority: {priority} - {malice} + {title} +
-
- - - - - Tags - - {tags?.map((tag, idx) => ( - - - {tag.tag}:{tag.value} - +
+
+ + + + + + Priority + + + + + {label} - ))} +
+
+ + + + + + Malice + + + {malice} +
+
+ + + + + + Action + + + {action} +
+
+ + + + + + Tags + + + {tags.length > 0 ? ( + tags.map((tag, idx) => ( + + + {tag.tag}: {tag.value} + + + )) + ) : ( + No tags + )} +
-
-
+
+
Payload
+
+ +
+
+
+
Context
-
+
Suppressions
-
-
Actions
- - {action} - -
-
-
-
Payload
-
- -
@@ -124,9 +224,13 @@ export function CasePanelContent({ ) } -function CaseStatusSelect({ status }: { status?: TStatus }) { +interface CaseStatusSelectProps { + status: TStatus + onStatusChange: (status: CaseStatusType) => void +} +function CaseStatusSelect({ status, onStatusChange }: CaseStatusSelectProps) { return ( - diff --git a/frontend/src/components/flair.tsx b/frontend/src/components/flair.tsx index f216a60c..045bc261 100644 --- a/frontend/src/components/flair.tsx +++ b/frontend/src/components/flair.tsx @@ -15,7 +15,7 @@ export function AIGeneratedFlair({ return (
{flair && ( - + )} {children}
diff --git a/frontend/src/lib/cases.ts b/frontend/src/lib/cases.ts index ef769cb9..fc2faa0c 100644 --- a/frontend/src/lib/cases.ts +++ b/frontend/src/lib/cases.ts @@ -18,6 +18,13 @@ export async function getCases( } } +/** + * Use this for autocomplete + * + * @param session + * @param workflowId + * @param cases + */ export async function updateCases( session: Session | null, workflowId: string, // They should all have the same workflow ID @@ -40,3 +47,41 @@ export async function updateCases( throw error } } + +export async function fetchCase( + session: Session | null, + workflowId: string, + caseId: string +): Promise { + try { + const client = getAuthenticatedClient(session) + const response = await client.get( + `/workflows/${workflowId}/cases/${caseId}` + ) + return caseSchema.parse(response.data) + } catch (error) { + console.error("Error fetching case:", error) + throw error + } +} + +export async function updateCase( + session: Session | null, + workflowId: string, + caseId: string, + case_: Case +) { + try { + const client = getAuthenticatedClient(session) + const response = await client.post( + `/workflows/${workflowId}/cases/${caseId}`, + case_ + ) + if (response.status !== 200) { + throw new Error("Failed to update case") + } + } catch (error) { + console.error("Error updating case:", error) + throw error + } +} diff --git a/frontend/src/providers/case-panel.tsx b/frontend/src/providers/case-panel.tsx index da861d52..ca1229a9 100644 --- a/frontend/src/providers/case-panel.tsx +++ b/frontend/src/providers/case-panel.tsx @@ -49,7 +49,7 @@ export default function CasePanelProvider({ setIsOpen={setIsOpen} > {selectedCase && isOpen && ( - + )} diff --git a/tracecat/api/app.py b/tracecat/api/app.py index ff289a58..6c65aa0a 100644 --- a/tracecat/api/app.py +++ b/tracecat/api/app.py @@ -1060,7 +1060,11 @@ def get_case( .to_polars() .to_dicts() ) - return [Case.from_flattened(c) for c in result] + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found" + ) + return Case.from_flattened(result[0]) @app.post("/workflows/{workflow_id}/cases/{case_id}")