Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20371,6 +20371,7 @@ export const $Role = {
"tracecat-cli",
"tracecat-executor",
"tracecat-agent-executor",
"tracecat-case-duration-sync",
"tracecat-case-triggers",
"tracecat-llm-gateway",
"tracecat-mcp",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/client/services.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10805,7 +10805,7 @@ export const caseDurationsDeleteCaseDurationDefinition = (

/**
* List Case Durations
* Sync and list case durations for the provided case.
* List materialized case durations for the provided case.
* @param data The data for the request.
* @param data.caseId
* @param data.workspaceId
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6158,6 +6158,7 @@ export type Role = {
| "tracecat-cli"
| "tracecat-executor"
| "tracecat-agent-executor"
| "tracecat-case-duration-sync"
| "tracecat-case-triggers"
| "tracecat-llm-gateway"
| "tracecat-mcp"
Expand All @@ -6178,6 +6179,7 @@ export type service_id =
| "tracecat-cli"
| "tracecat-executor"
| "tracecat-agent-executor"
| "tracecat-case-duration-sync"
| "tracecat-case-triggers"
| "tracecat-llm-gateway"
| "tracecat-mcp"
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/cases/case-duration-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
STATUSES,
} from "@/components/cases/case-categories"
import {
CASE_DURATION_EVENT_OPTIONS,
CASE_DURATION_EVENT_VALUES,
CASE_DURATION_SELECTION_OPTIONS,
CASE_EVENT_FILTER_OPTIONS,
CASE_EVENT_OPTIONS,
CASE_EVENT_VALUES,
isCaseDropdownEventType,
isCaseEventFilterType,
isCaseFieldEventType,
Expand Down Expand Up @@ -61,7 +61,7 @@ import { useWorkspaceId } from "@/providers/workspace-id"

const anchorSchema = z.object({
selection: z.enum(["first", "last"]),
eventType: z.enum(CASE_EVENT_VALUES),
eventType: z.enum(CASE_DURATION_EVENT_VALUES),
filterValues: z.array(z.string()).optional(),
dropdownDefinitionId: z.string().optional(),
dropdownOptionIds: z.array(z.string()).optional(),
Expand All @@ -73,7 +73,7 @@ const CATEGORY_OPTIONS = {
status_changed: STATUSES,
} as const

type CaseDurationEventTypeValue = (typeof CASE_EVENT_VALUES)[number]
type CaseDurationEventTypeValue = (typeof CASE_DURATION_EVENT_VALUES)[number]

const requiresFilterSelection = (
eventType: CaseDurationEventTypeValue
Expand Down Expand Up @@ -605,7 +605,7 @@ export function CaseDurationDialog({
</SelectTrigger>
</FormControl>
<SelectContent>
{CASE_EVENT_OPTIONS.map((option) => (
{CASE_DURATION_EVENT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="flex items-center gap-2">
<option.icon className="size-3.5 text-muted-foreground" />
Expand Down Expand Up @@ -755,7 +755,7 @@ export function CaseDurationDialog({
</SelectTrigger>
</FormControl>
<SelectContent>
{CASE_EVENT_OPTIONS.map((option) => (
{CASE_DURATION_EVENT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="flex items-center gap-2">
<option.icon className="size-3.5 text-muted-foreground" />
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/components/cases/case-duration-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface CaseEventOption {
description?: string
}

export type CaseDurationAnchorEventType = Exclude<CaseEventType, "case_viewed">

interface CaseDurationEventOption extends Omit<CaseEventOption, "value"> {
value: CaseDurationAnchorEventType
}

export const CASE_EVENT_OPTIONS: CaseEventOption[] = [
{
value: "case_created",
Expand Down Expand Up @@ -119,6 +125,14 @@ export const CASE_EVENT_VALUES = CASE_EVENT_OPTIONS.map(
(option) => option.value
) as [CaseEventType, ...CaseEventType[]]

export const CASE_DURATION_EVENT_OPTIONS = CASE_EVENT_OPTIONS.filter(
(option): option is CaseDurationEventOption => option.value !== "case_viewed"
)

export const CASE_DURATION_EVENT_VALUES = CASE_DURATION_EVENT_OPTIONS.map(
(option) => option.value
) as [CaseDurationAnchorEventType, ...CaseDurationAnchorEventType[]]

export const CASE_DURATION_SELECTION_OPTIONS: Array<{
value: CaseDurationAnchorSelection
label: string
Expand Down Expand Up @@ -173,6 +187,12 @@ export function isCaseEventFilterType(
return value in CASE_EVENT_FILTER_OPTIONS
}

export function isCaseDurationAnchorEventType(
value: CaseEventType
): value is CaseDurationAnchorEventType {
return value !== "case_viewed"
}

const CASE_TAG_EVENT_TYPES = [
"tag_added",
"tag_removed",
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/components/cases/update-case-duration-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
normalizeFilterValues,
} from "@/components/cases/case-duration-dialog"
import {
type CaseDurationAnchorEventType,
isCaseDropdownEventType,
isCaseDurationAnchorEventType,
isCaseEventFilterType,
isCaseFieldEventType,
isCaseTagEventType,
Expand All @@ -31,14 +33,19 @@ interface UpdateCaseDurationDialogProps {
}

function extractAnchorFormValues(
anchor: CaseDurationDefinitionRead["start_anchor"]
anchor: CaseDurationDefinitionRead["start_anchor"],
fallbackEventType: CaseDurationAnchorEventType
): CaseDurationFormValues["start"] {
if (isCaseDropdownEventType(anchor.event_type)) {
const eventType = isCaseDurationAnchorEventType(anchor.event_type)
Comment on lines +37 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid replacing legacy viewed anchors on save

When an existing duration definition still has a case_viewed anchor, this fallback initializes the edit form as case_created/case_closed. The submit handler always sends both anchors, so a user who only edits the name or description and saves will silently rewrite the legacy anchor and change the metric instead of preserving or explicitly migrating it. This affects workspaces with preexisting case_viewed duration definitions that are still returned by the backend compatibility path.

Useful? React with 👍 / 👎.

? anchor.event_type
: fallbackEventType

if (isCaseDropdownEventType(eventType)) {
const defId = anchor.filters?.dropdown_definition_id
const optionIds = normalizeFilterValues(anchor.filters?.dropdown_option_ids)
return {
selection: anchor.selection ?? "first",
eventType: anchor.event_type,
eventType,
filterValues: [],
dropdownDefinitionId: typeof defId === "string" ? defId : undefined,
dropdownOptionIds: optionIds,
Expand All @@ -48,7 +55,7 @@ function extractAnchorFormValues(
const filterValues = normalizeFilterValues(getAnchorFilterValues(anchor))
return {
selection: anchor.selection ?? "first",
eventType: anchor.event_type,
eventType,
filterValues,
dropdownDefinitionId: undefined,
dropdownOptionIds: [],
Expand Down Expand Up @@ -80,8 +87,8 @@ const getInitialValues = (
return {
name: duration.name,
description: duration.description ?? "",
start: extractAnchorFormValues(duration.start_anchor),
end: extractAnchorFormValues(duration.end_anchor),
start: extractAnchorFormValues(duration.start_anchor, "case_created"),
end: extractAnchorFormValues(duration.end_anchor, "case_closed"),
}
}

Expand Down
Loading
Loading