@checkstack/anomaly-frontend 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/package.json +35 -0
- package/src/components/AnomalyConfigPanel.tsx +154 -0
- package/src/components/AnomalyFieldOverridesEditor.tsx +362 -0
- package/src/components/AnomalySettingsForm.tsx +211 -0
- package/src/components/AnomalyTemplatePanel.tsx +133 -0
- package/src/components/SystemAnomalyBadge.tsx +71 -0
- package/src/components/SystemAnomalyWidget.tsx +283 -0
- package/src/components/useAnomalyFields.ts +70 -0
- package/src/index.tsx +1 -0
- package/src/plugin.tsx +78 -0
- package/tsconfig.json +9 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @checkstack/anomaly-frontend
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8d1ef12: ## Anomaly Detection & UI Improvements
|
|
8
|
+
|
|
9
|
+
### Anomaly Detection Enhancements (Phase 2)
|
|
10
|
+
|
|
11
|
+
- **`@checkstack/anomaly-backend`**: Implemented background baseline analyzer jobs and anomaly trend deviation detection mechanics.
|
|
12
|
+
- **`@checkstack/anomaly-common`**: Added new baseline statistical logic and inference rules.
|
|
13
|
+
- **`@checkstack/anomaly-frontend`**: Added new Anomaly Widget and refactored system detail rendering to be more human-readable.
|
|
14
|
+
- **`@checkstack/dashboard-frontend`**: Refined the global anomaly widget and fixed hardcoded access gating to render appropriately.
|
|
15
|
+
- **`@checkstack/healthcheck-backend`**: Connected executor telemetry to the anomaly pipeline.
|
|
16
|
+
- **`@checkstack/healthcheck-frontend`**: Reconciled baseline display consistency in Drawer and charts.
|
|
17
|
+
|
|
18
|
+
### Notification Identifiers
|
|
19
|
+
|
|
20
|
+
- **`@checkstack/incident-backend`**: Resolved system IDs to human-readable System Names within Incident notifications to eliminate ID-only alert content.
|
|
21
|
+
- **`@checkstack/maintenance-backend`**: Adopted the same resolution strategy for Maintenance notifications to keep parity.
|
|
22
|
+
|
|
23
|
+
### UI Experience
|
|
24
|
+
|
|
25
|
+
- **`@checkstack/incident-frontend`**: Fixed the "Back to X" BackLink to properly use `react-router` hook `useNavigate` instead of doing a full application reload.
|
|
26
|
+
- **`@checkstack/healthcheck-frontend`**: Implemented `useNavigate` for seamless SPA back-linking.
|
|
27
|
+
- **`@checkstack/integration-frontend`**: Updated connections and delivery logs links to navigate without hard reloads.
|
|
28
|
+
|
|
29
|
+
- 8d1ef12: Phase 2 of anomaly detection: trend drift detection.
|
|
30
|
+
|
|
31
|
+
The background baseline analyzer now computes a linear regression slope across each field's chronologically-ordered history and runs a `detectDrift` evaluator that catches gradual "creeping degradation" never reaching the 3σ spike threshold. Drifts share the same `anomalies` table as spike anomalies via a new `kind` column (`spike` | `drift`, default `spike`); the existing suspicious → anomaly → recovered lifecycle is reused, ticking at the analyzer's hourly cadence with a default 2-run confirmation window.
|
|
32
|
+
|
|
33
|
+
User-facing additions: a Trend Drift toggle and threshold slider on both the template and assignment anomaly settings panels (with per-field overrides), drift rows in the System Anomaly widget, dashed regression-line overlays on the auto-generated line charts, and a new `ANOMALY_TREND_DETECTED` signal for live UI updates. Plugin authors can disable drift per chartable field via `x-anomaly-drift-enabled: false` or tighten/loosen it via `x-anomaly-drift-threshold`.
|
|
34
|
+
|
|
35
|
+
- 8d1ef12: Added Categorical Anomaly Detection (Dominance Drift) support for non-numeric healthcheck values, and introduced Slider UI components for sensitivity and confirmation window anomaly settings.
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [8d1ef12]
|
|
40
|
+
- Updated dependencies [8d1ef12]
|
|
41
|
+
- Updated dependencies [8d1ef12]
|
|
42
|
+
- Updated dependencies [8d1ef12]
|
|
43
|
+
- @checkstack/healthcheck-common@0.12.0
|
|
44
|
+
- @checkstack/anomaly-common@0.2.0
|
|
45
|
+
- @checkstack/healthcheck-frontend@0.17.0
|
|
46
|
+
- @checkstack/common@0.7.0
|
|
47
|
+
- @checkstack/ui@1.6.0
|
|
48
|
+
- @checkstack/catalog-common@1.5.2
|
|
49
|
+
- @checkstack/frontend-api@0.3.11
|
|
50
|
+
- @checkstack/signal-frontend@0.0.16
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/anomaly-frontend",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"checkstack": {
|
|
7
|
+
"type": "frontend"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "bun run lint:code",
|
|
12
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@checkstack/anomaly-common": "0.1.0",
|
|
16
|
+
"@checkstack/catalog-common": "1.5.1",
|
|
17
|
+
"@checkstack/common": "0.6.5",
|
|
18
|
+
"@checkstack/frontend-api": "0.3.10",
|
|
19
|
+
"@checkstack/healthcheck-common": "0.11.0",
|
|
20
|
+
"@checkstack/healthcheck-frontend": "0.16.5",
|
|
21
|
+
"@checkstack/signal-frontend": "0.0.15",
|
|
22
|
+
"@checkstack/ui": "1.5.1",
|
|
23
|
+
"date-fns": "^4.1.0",
|
|
24
|
+
"lucide-react": "^0.344.0",
|
|
25
|
+
"react": "^18.2.0",
|
|
26
|
+
"react-router-dom": "^7.14.2",
|
|
27
|
+
"zod": "^4.2.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@checkstack/scripts": "0.1.2",
|
|
31
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
32
|
+
"@types/react": "^18.2.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardHeader,
|
|
5
|
+
CardTitle,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardFooter,
|
|
9
|
+
Button,
|
|
10
|
+
useToast,
|
|
11
|
+
} from "@checkstack/ui";
|
|
12
|
+
import { Activity, Save } from "lucide-react";
|
|
13
|
+
import type { AssignmentIDEContext } from "@checkstack/healthcheck-frontend";
|
|
14
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
15
|
+
import {
|
|
16
|
+
AnomalyApi,
|
|
17
|
+
type AnomalyFieldConfig,
|
|
18
|
+
} from "@checkstack/anomaly-common";
|
|
19
|
+
import { useAnomalyFields } from "./useAnomalyFields";
|
|
20
|
+
import {
|
|
21
|
+
AnomalySettingsForm,
|
|
22
|
+
type AnomalySettingsFormValues,
|
|
23
|
+
} from "./AnomalySettingsForm";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_VALUES: AnomalySettingsFormValues = {
|
|
26
|
+
enabled: true,
|
|
27
|
+
sensitivity: 1,
|
|
28
|
+
confirmationWindow: 3,
|
|
29
|
+
driftEnabled: true,
|
|
30
|
+
driftThreshold: 2,
|
|
31
|
+
fieldOverrides: {},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function AnomalyConfigPanel({ context }: { context: AssignmentIDEContext }) {
|
|
35
|
+
const toast = useToast();
|
|
36
|
+
const anomalyClient = usePluginClient(AnomalyApi);
|
|
37
|
+
|
|
38
|
+
const [values, setValues] = useState<AnomalySettingsFormValues>(DEFAULT_VALUES);
|
|
39
|
+
|
|
40
|
+
const { data: configRecord, isLoading: isLoadingConfig } =
|
|
41
|
+
anomalyClient.getAnomalyAssignmentConfig.useQuery(
|
|
42
|
+
{ systemId: context.systemId, configurationId: context.configurationId },
|
|
43
|
+
{ enabled: !!context.systemId && !!context.configurationId },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const { data: templateRecord, isLoading: isLoadingTemplate } =
|
|
47
|
+
anomalyClient.getAnomalyConfig.useQuery(
|
|
48
|
+
{ configurationId: context.configurationId },
|
|
49
|
+
{ enabled: !!context.configurationId },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const baseAvailableFields = useAnomalyFields(context.configurationId);
|
|
53
|
+
|
|
54
|
+
// Cascade template-level field overrides into the available-fields metadata so
|
|
55
|
+
// each row's "default" reflects the template, not the engine fallback.
|
|
56
|
+
const availableFields = baseAvailableFields.map((field) => {
|
|
57
|
+
const templateOverride = templateRecord?.data?.fieldOverrides?.[field.path];
|
|
58
|
+
return {
|
|
59
|
+
...field,
|
|
60
|
+
defaultEnabled: templateOverride?.enabled ?? field.defaultEnabled,
|
|
61
|
+
defaultSensitivity: templateOverride?.sensitivity ?? field.defaultSensitivity,
|
|
62
|
+
defaultConfirmationWindow:
|
|
63
|
+
templateOverride?.confirmationWindow ?? field.defaultConfirmationWindow,
|
|
64
|
+
defaultDirection: templateOverride?.direction ?? field.defaultDirection,
|
|
65
|
+
defaultDriftEnabled:
|
|
66
|
+
templateOverride?.driftEnabled ?? field.defaultDriftEnabled,
|
|
67
|
+
defaultDriftThreshold:
|
|
68
|
+
templateOverride?.driftThreshold ?? field.defaultDriftThreshold,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (configRecord?.data) {
|
|
74
|
+
const tpl = templateRecord?.data;
|
|
75
|
+
setValues({
|
|
76
|
+
enabled: configRecord.data.enabled ?? true,
|
|
77
|
+
sensitivity: configRecord.data.sensitivity ?? tpl?.sensitivity ?? 1,
|
|
78
|
+
confirmationWindow:
|
|
79
|
+
configRecord.data.confirmationWindow ?? tpl?.confirmationWindow ?? 3,
|
|
80
|
+
driftEnabled:
|
|
81
|
+
configRecord.data.driftEnabled ?? tpl?.driftEnabled ?? true,
|
|
82
|
+
driftThreshold:
|
|
83
|
+
configRecord.data.driftThreshold ?? tpl?.driftThreshold ?? 2,
|
|
84
|
+
fieldOverrides:
|
|
85
|
+
(configRecord.data.fieldOverrides as Record<string, AnomalyFieldConfig>) ??
|
|
86
|
+
{},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}, [configRecord, templateRecord]);
|
|
90
|
+
|
|
91
|
+
const updateMutation = anomalyClient.updateAnomalyAssignmentConfig.useMutation({
|
|
92
|
+
onSuccess: () => toast.success("Assignment exceptions saved"),
|
|
93
|
+
onError: () => toast.error("Failed to save assignment exceptions"),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const handleChange = <K extends keyof AnomalySettingsFormValues>(
|
|
97
|
+
key: K,
|
|
98
|
+
value: AnomalySettingsFormValues[K],
|
|
99
|
+
) => {
|
|
100
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleSave = () => {
|
|
104
|
+
updateMutation.mutate({
|
|
105
|
+
systemId: context.systemId,
|
|
106
|
+
configurationId: context.configurationId,
|
|
107
|
+
config: values,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (isLoadingConfig || isLoadingTemplate) {
|
|
112
|
+
return <div className="p-6 text-muted-foreground">Loading anomaly exceptions...</div>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Card className="flex flex-col h-full border-0 rounded-none shadow-none">
|
|
117
|
+
<CardHeader className="pb-4">
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<Activity className="h-5 w-5 text-primary" />
|
|
120
|
+
<div>
|
|
121
|
+
<CardTitle className="text-lg">Assignment Exceptions</CardTitle>
|
|
122
|
+
<CardDescription>
|
|
123
|
+
Override the anomaly detection defaults for this specific system assignment.
|
|
124
|
+
</CardDescription>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
|
|
129
|
+
<CardContent className="flex-1 space-y-6 overflow-y-auto">
|
|
130
|
+
<AnomalySettingsForm
|
|
131
|
+
values={values}
|
|
132
|
+
onChange={handleChange}
|
|
133
|
+
availableFields={availableFields}
|
|
134
|
+
isLocked={context.isLocked}
|
|
135
|
+
variant="assignment"
|
|
136
|
+
/>
|
|
137
|
+
</CardContent>
|
|
138
|
+
|
|
139
|
+
<CardFooter className="justify-end border-t pt-4">
|
|
140
|
+
<Button
|
|
141
|
+
onClick={handleSave}
|
|
142
|
+
disabled={updateMutation.isPending || context.isLocked}
|
|
143
|
+
>
|
|
144
|
+
{updateMutation.isPending ? "Saving..." : (
|
|
145
|
+
<>
|
|
146
|
+
<Save className="mr-2 h-4 w-4" />
|
|
147
|
+
Save Exceptions
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</Button>
|
|
151
|
+
</CardFooter>
|
|
152
|
+
</Card>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Label,
|
|
4
|
+
Toggle,
|
|
5
|
+
Badge,
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
Accordion,
|
|
12
|
+
AccordionItem,
|
|
13
|
+
AccordionTrigger,
|
|
14
|
+
AccordionContent,
|
|
15
|
+
Slider,
|
|
16
|
+
} from "@checkstack/ui";
|
|
17
|
+
import type {
|
|
18
|
+
AnomalyFieldConfig,
|
|
19
|
+
AnomalyDirection,
|
|
20
|
+
} from "@checkstack/anomaly-common";
|
|
21
|
+
import type { AnomalyFieldMeta } from "./useAnomalyFields";
|
|
22
|
+
|
|
23
|
+
interface AnomalyFieldOverridesEditorProps {
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
availableFields: AnomalyFieldMeta[];
|
|
27
|
+
fieldOverrides: Record<string, AnomalyFieldConfig>;
|
|
28
|
+
onChange: (
|
|
29
|
+
field: string,
|
|
30
|
+
key: keyof AnomalyFieldConfig,
|
|
31
|
+
value: number | boolean | AnomalyDirection | undefined,
|
|
32
|
+
) => void;
|
|
33
|
+
parentEnabled: boolean;
|
|
34
|
+
isLocked?: boolean;
|
|
35
|
+
defaultSensitivity: number;
|
|
36
|
+
defaultConfirmationWindow: number;
|
|
37
|
+
defaultDriftEnabled?: boolean;
|
|
38
|
+
defaultDriftThreshold?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function AnomalyFieldOverridesEditor({
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
availableFields,
|
|
45
|
+
fieldOverrides,
|
|
46
|
+
onChange,
|
|
47
|
+
parentEnabled,
|
|
48
|
+
isLocked,
|
|
49
|
+
defaultSensitivity,
|
|
50
|
+
defaultConfirmationWindow,
|
|
51
|
+
defaultDriftEnabled = true,
|
|
52
|
+
defaultDriftThreshold = 2,
|
|
53
|
+
}: AnomalyFieldOverridesEditorProps) {
|
|
54
|
+
if (availableFields.length === 0) {
|
|
55
|
+
return <></>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A field is only considered "overridden" if its configured values differ from the parent defaults.
|
|
59
|
+
const isFieldOverridden = (fieldMeta: AnomalyFieldMeta) => {
|
|
60
|
+
const override = fieldOverrides[fieldMeta.path];
|
|
61
|
+
if (!override) return false;
|
|
62
|
+
|
|
63
|
+
if (override.enabled !== undefined && override.enabled !== fieldMeta.defaultEnabled)
|
|
64
|
+
return true;
|
|
65
|
+
if (
|
|
66
|
+
override.sensitivity !== undefined &&
|
|
67
|
+
override.sensitivity !== (fieldMeta.defaultSensitivity ?? defaultSensitivity)
|
|
68
|
+
)
|
|
69
|
+
return true;
|
|
70
|
+
if (
|
|
71
|
+
override.confirmationWindow !== undefined &&
|
|
72
|
+
override.confirmationWindow !== (fieldMeta.defaultConfirmationWindow ?? defaultConfirmationWindow)
|
|
73
|
+
)
|
|
74
|
+
return true;
|
|
75
|
+
if (override.direction !== undefined) return true;
|
|
76
|
+
if (
|
|
77
|
+
override.driftEnabled !== undefined &&
|
|
78
|
+
override.driftEnabled !== (fieldMeta.defaultDriftEnabled ?? defaultDriftEnabled)
|
|
79
|
+
)
|
|
80
|
+
return true;
|
|
81
|
+
if (
|
|
82
|
+
override.driftThreshold !== undefined &&
|
|
83
|
+
override.driftThreshold !== (fieldMeta.defaultDriftThreshold ?? defaultDriftThreshold)
|
|
84
|
+
)
|
|
85
|
+
return true;
|
|
86
|
+
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Auto-expand overridden fields
|
|
91
|
+
const overriddenFields = availableFields
|
|
92
|
+
.filter((fieldMeta) => isFieldOverridden(fieldMeta))
|
|
93
|
+
.map(meta => meta.path);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="mt-10 space-y-6">
|
|
97
|
+
<div>
|
|
98
|
+
<h3 className="text-lg font-medium tracking-tight">{title}</h3>
|
|
99
|
+
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<Accordion
|
|
103
|
+
type="multiple"
|
|
104
|
+
defaultValue={overriddenFields}
|
|
105
|
+
className="w-full space-y-3"
|
|
106
|
+
>
|
|
107
|
+
{availableFields.map((fieldMeta) => {
|
|
108
|
+
const field = fieldMeta.path;
|
|
109
|
+
const override = fieldOverrides[field];
|
|
110
|
+
const isOverridden = isFieldOverridden(fieldMeta);
|
|
111
|
+
const fieldEnabled = override?.enabled ?? fieldMeta.defaultEnabled;
|
|
112
|
+
|
|
113
|
+
// Split the field path into namespace breadcrumbs and the actual property name
|
|
114
|
+
const parts = field.split(".");
|
|
115
|
+
const fieldName = parts.pop() || field;
|
|
116
|
+
const pathParts = parts;
|
|
117
|
+
|
|
118
|
+
const isCategorical =
|
|
119
|
+
override?.direction === "dominance" ||
|
|
120
|
+
fieldMeta.defaultDirection === "dominance" ||
|
|
121
|
+
((!override?.direction) &&
|
|
122
|
+
(!fieldMeta.defaultDirection) &&
|
|
123
|
+
(fieldMeta.type === "string" || fieldMeta.type === "boolean"));
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<AccordionItem
|
|
127
|
+
key={field}
|
|
128
|
+
value={field}
|
|
129
|
+
className={`
|
|
130
|
+
rounded-xl border bg-card text-card-foreground shadow-sm transition-all duration-200 overflow-hidden
|
|
131
|
+
${isOverridden ? "border-primary/40 shadow-md" : "border-border/40 opacity-80 hover:opacity-100"}
|
|
132
|
+
`}
|
|
133
|
+
>
|
|
134
|
+
<AccordionTrigger className="hover:no-underline px-5 py-4">
|
|
135
|
+
<div className="flex flex-1 items-center justify-between gap-4 mr-4">
|
|
136
|
+
<div className="space-y-1 text-left">
|
|
137
|
+
{pathParts.length > 0 && (
|
|
138
|
+
<div className="flex flex-wrap items-center gap-1 text-[10px] text-muted-foreground font-mono">
|
|
139
|
+
{pathParts.map((part, idx) => (
|
|
140
|
+
<React.Fragment key={idx}>
|
|
141
|
+
{idx > 0 && <span className="opacity-40">▶</span>}
|
|
142
|
+
<span>{part}</span>
|
|
143
|
+
</React.Fragment>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
<div
|
|
148
|
+
className={`text-sm font-semibold tracking-tight font-mono ${isOverridden ? "text-primary" : ""}`}
|
|
149
|
+
>
|
|
150
|
+
{fieldName}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="flex items-center gap-3">
|
|
155
|
+
{isOverridden ? (
|
|
156
|
+
<Badge
|
|
157
|
+
variant={fieldEnabled ? "default" : "secondary"}
|
|
158
|
+
className="text-[10px] uppercase tracking-wider font-semibold"
|
|
159
|
+
>
|
|
160
|
+
{fieldEnabled ? "Custom Override" : "Ignored"}
|
|
161
|
+
</Badge>
|
|
162
|
+
) : (
|
|
163
|
+
<Badge
|
|
164
|
+
variant="outline"
|
|
165
|
+
className="text-[10px] uppercase tracking-wider font-semibold opacity-50"
|
|
166
|
+
>
|
|
167
|
+
Default Settings
|
|
168
|
+
</Badge>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</AccordionTrigger>
|
|
173
|
+
|
|
174
|
+
<AccordionContent className="px-5 pb-5 pt-2 border-t border-border/30">
|
|
175
|
+
<div className="space-y-6 mt-4">
|
|
176
|
+
<div className="flex items-center justify-between bg-muted/30 p-3 rounded-lg border">
|
|
177
|
+
<div className="space-y-0.5">
|
|
178
|
+
<Label className="text-sm font-medium">
|
|
179
|
+
Enable Custom Monitoring
|
|
180
|
+
</Label>
|
|
181
|
+
<div className="text-xs text-muted-foreground">
|
|
182
|
+
Toggle to ignore anomalies for this specific field.
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<Toggle
|
|
186
|
+
checked={fieldEnabled}
|
|
187
|
+
onCheckedChange={(val) => onChange(field, "enabled", val)}
|
|
188
|
+
disabled={!parentEnabled || isLocked}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{fieldEnabled && (
|
|
193
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
194
|
+
<div className="space-y-2 flex flex-col">
|
|
195
|
+
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
196
|
+
{isCategorical ? "Dominance Threshold" : "Sensitivity Multiplier"}
|
|
197
|
+
</Label>
|
|
198
|
+
<div className="pt-2 pb-1 px-1">
|
|
199
|
+
<Slider
|
|
200
|
+
value={[override?.sensitivity ?? fieldMeta.defaultSensitivity ?? defaultSensitivity]}
|
|
201
|
+
min={0.5}
|
|
202
|
+
max={3}
|
|
203
|
+
step={0.1}
|
|
204
|
+
onValueChange={(val) =>
|
|
205
|
+
onChange(
|
|
206
|
+
field,
|
|
207
|
+
"sensitivity",
|
|
208
|
+
val[0],
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
disabled={!parentEnabled || isLocked}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
|
|
215
|
+
<span>0.5 (More)</span>
|
|
216
|
+
<span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
217
|
+
{(override?.sensitivity ?? fieldMeta.defaultSensitivity ?? defaultSensitivity).toFixed(1)}x
|
|
218
|
+
</span>
|
|
219
|
+
<span>3.0 (Fewer)</span>
|
|
220
|
+
</div>
|
|
221
|
+
<p className="text-[10.5px] text-muted-foreground leading-relaxed pt-1">
|
|
222
|
+
{isCategorical
|
|
223
|
+
? "Controls the strictness for categorical drift. A lower threshold requires absolute stability (e.g., 99%) before trusting a deviation, while a higher threshold allows drifting even if the baseline is noisy (e.g., 45%)."
|
|
224
|
+
: "Controls the strictness of the baseline boundaries. A higher multiplier widens the acceptable range, resulting in fewer alerts (lower sensitivity)."}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="space-y-2 flex flex-col">
|
|
228
|
+
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
229
|
+
Confirmation Window
|
|
230
|
+
</Label>
|
|
231
|
+
<div className="pt-2 pb-1 px-1">
|
|
232
|
+
<Slider
|
|
233
|
+
value={[override?.confirmationWindow ?? fieldMeta.defaultConfirmationWindow ?? defaultConfirmationWindow]}
|
|
234
|
+
min={1}
|
|
235
|
+
max={10}
|
|
236
|
+
step={1}
|
|
237
|
+
onValueChange={(val) =>
|
|
238
|
+
onChange(
|
|
239
|
+
field,
|
|
240
|
+
"confirmationWindow",
|
|
241
|
+
val[0],
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
disabled={!parentEnabled || isLocked}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
|
|
248
|
+
<span>1 Run</span>
|
|
249
|
+
<span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
250
|
+
{override?.confirmationWindow ?? fieldMeta.defaultConfirmationWindow ?? defaultConfirmationWindow} Runs
|
|
251
|
+
</span>
|
|
252
|
+
<span>10 Runs</span>
|
|
253
|
+
</div>
|
|
254
|
+
<p className="text-[10.5px] text-muted-foreground leading-relaxed pt-1">
|
|
255
|
+
Consecutive anomalous data points required before an alert is officially raised, preventing alert fatigue from isolated network spikes.
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
<div className="space-y-2 flex flex-col">
|
|
259
|
+
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
260
|
+
Behavior
|
|
261
|
+
</Label>
|
|
262
|
+
<Select
|
|
263
|
+
value={override?.direction ?? "auto"}
|
|
264
|
+
onValueChange={(val) =>
|
|
265
|
+
onChange(
|
|
266
|
+
field,
|
|
267
|
+
"direction",
|
|
268
|
+
val === "auto"
|
|
269
|
+
? undefined
|
|
270
|
+
: (val as AnomalyDirection),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
disabled={!parentEnabled || isLocked}
|
|
274
|
+
>
|
|
275
|
+
<SelectTrigger className="h-10 bg-background/50 focus:bg-background transition-colors">
|
|
276
|
+
<SelectValue placeholder="Auto-inferred" />
|
|
277
|
+
</SelectTrigger>
|
|
278
|
+
<SelectContent>
|
|
279
|
+
<SelectItem value="auto">Auto-inferred</SelectItem>
|
|
280
|
+
<SelectItem value="deviation">
|
|
281
|
+
Any Deviation
|
|
282
|
+
</SelectItem>
|
|
283
|
+
<SelectItem value="higher-is-better">
|
|
284
|
+
Higher is Better
|
|
285
|
+
</SelectItem>
|
|
286
|
+
<SelectItem value="lower-is-better">
|
|
287
|
+
Lower is Better
|
|
288
|
+
</SelectItem>
|
|
289
|
+
<SelectItem value="dominance">
|
|
290
|
+
Dominance Drift
|
|
291
|
+
</SelectItem>
|
|
292
|
+
</SelectContent>
|
|
293
|
+
</Select>
|
|
294
|
+
<p className="text-[10.5px] text-muted-foreground leading-relaxed pt-1">
|
|
295
|
+
Override the auto-inferred behavior logic. "Higher is Better" ignores positive spikes, while "Lower is Better" ignores sudden drops.
|
|
296
|
+
</p>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{fieldEnabled && !isCategorical && (
|
|
302
|
+
<div className="grid gap-6 lg:grid-cols-3 border-t border-border/30 pt-4">
|
|
303
|
+
<div className="space-y-2 flex flex-col">
|
|
304
|
+
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
305
|
+
Trend Drift
|
|
306
|
+
</Label>
|
|
307
|
+
<div className="flex items-center justify-between bg-muted/30 p-2 rounded-md border">
|
|
308
|
+
<span className="text-xs text-muted-foreground">
|
|
309
|
+
{(override?.driftEnabled ?? fieldMeta.defaultDriftEnabled ?? defaultDriftEnabled)
|
|
310
|
+
? "Detecting drift"
|
|
311
|
+
: "Drift muted"}
|
|
312
|
+
</span>
|
|
313
|
+
<Toggle
|
|
314
|
+
checked={override?.driftEnabled ?? fieldMeta.defaultDriftEnabled ?? defaultDriftEnabled}
|
|
315
|
+
onCheckedChange={(val) => onChange(field, "driftEnabled", val)}
|
|
316
|
+
disabled={!parentEnabled || isLocked}
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
<p className="text-[10.5px] text-muted-foreground leading-relaxed pt-1">
|
|
320
|
+
Trend drift catches gradual degradation that never triggers a spike alert.
|
|
321
|
+
</p>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="space-y-2 flex flex-col lg:col-span-2">
|
|
324
|
+
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
325
|
+
Drift Threshold (σ)
|
|
326
|
+
</Label>
|
|
327
|
+
<div className="pt-2 pb-1 px-1">
|
|
328
|
+
<Slider
|
|
329
|
+
value={[override?.driftThreshold ?? fieldMeta.defaultDriftThreshold ?? defaultDriftThreshold]}
|
|
330
|
+
min={1}
|
|
331
|
+
max={4}
|
|
332
|
+
step={0.1}
|
|
333
|
+
onValueChange={(val) => onChange(field, "driftThreshold", val[0])}
|
|
334
|
+
disabled={
|
|
335
|
+
!parentEnabled ||
|
|
336
|
+
isLocked ||
|
|
337
|
+
!(override?.driftEnabled ?? fieldMeta.defaultDriftEnabled ?? defaultDriftEnabled)
|
|
338
|
+
}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
<div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
|
|
342
|
+
<span>1.0σ (More)</span>
|
|
343
|
+
<span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
344
|
+
{(override?.driftThreshold ?? fieldMeta.defaultDriftThreshold ?? defaultDriftThreshold).toFixed(1)}σ
|
|
345
|
+
</span>
|
|
346
|
+
<span>4.0σ (Fewer)</span>
|
|
347
|
+
</div>
|
|
348
|
+
<p className="text-[10.5px] text-muted-foreground leading-relaxed pt-1">
|
|
349
|
+
Drift fires when |slope × n| exceeds this many standard deviations across the baseline window.
|
|
350
|
+
</p>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
</AccordionContent>
|
|
356
|
+
</AccordionItem>
|
|
357
|
+
);
|
|
358
|
+
})}
|
|
359
|
+
</Accordion>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|