@checkstack/anomaly-frontend 0.3.0 → 0.4.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 +185 -0
- package/package.json +15 -14
- package/src/components/AnomalyConfigPanel.tsx +5 -11
- package/src/components/AnomalyFieldOverridesEditor.tsx +881 -283
- package/src/components/AnomalySettingsForm.tsx +90 -120
- package/src/components/AnomalyTemplatePanel.tsx +5 -18
- package/src/components/useAnomalyFields.ts +6 -0
- package/tsconfig.json +32 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Label,
|
|
3
|
+
Toggle,
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@checkstack/ui";
|
|
2
10
|
import type {
|
|
3
11
|
AnomalyDirection,
|
|
4
12
|
AnomalyFieldConfig,
|
|
@@ -8,10 +16,8 @@ import type { AnomalyFieldMeta } from "./useAnomalyFields";
|
|
|
8
16
|
|
|
9
17
|
export interface AnomalySettingsFormValues {
|
|
10
18
|
enabled: boolean;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
driftEnabled: boolean;
|
|
14
|
-
driftThreshold: number;
|
|
19
|
+
baselineWindow: string;
|
|
20
|
+
notify: boolean;
|
|
15
21
|
fieldOverrides: Record<string, AnomalyFieldConfig>;
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -29,26 +35,37 @@ export interface AnomalySettingsFormProps {
|
|
|
29
35
|
|
|
30
36
|
const COPY = {
|
|
31
37
|
template: {
|
|
32
|
-
enabledLabel: "Enable
|
|
38
|
+
enabledLabel: "Enable anomaly detection by default",
|
|
33
39
|
enabledDescription:
|
|
34
40
|
"Run background analysis to detect deviations from expected behavior across all systems using this template.",
|
|
35
|
-
|
|
36
|
-
confirmationLabel: "Confirmation Window",
|
|
37
|
-
fieldOverridesTitle: "Global Field-Level Defaults",
|
|
41
|
+
fieldOverridesTitle: "Field-level defaults",
|
|
38
42
|
fieldOverridesDescription:
|
|
39
|
-
"
|
|
43
|
+
"Tune anomaly detection for specific metrics. Each field uses the plugin's tuned defaults until you override it here.",
|
|
44
|
+
notifyLabel: "Send notifications on confirmed anomalies",
|
|
45
|
+
notifyDescription:
|
|
46
|
+
"Page the assigned subscribers when an anomaly transitions to confirmed.",
|
|
40
47
|
},
|
|
41
48
|
assignment: {
|
|
42
|
-
enabledLabel: "Enable
|
|
43
|
-
enabledDescription:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fieldOverridesTitle: "Field-Level Overrides",
|
|
49
|
+
enabledLabel: "Enable assignment exceptions",
|
|
50
|
+
enabledDescription:
|
|
51
|
+
"Run background analysis for this specific system assignment.",
|
|
52
|
+
fieldOverridesTitle: "Field-level overrides",
|
|
47
53
|
fieldOverridesDescription:
|
|
48
|
-
"Override anomaly settings for specific metrics
|
|
54
|
+
"Override anomaly settings for specific metrics on this system. Other systems using the same template are unaffected.",
|
|
55
|
+
notifyLabel: "Send notifications on confirmed anomalies",
|
|
56
|
+
notifyDescription:
|
|
57
|
+
"Page the assigned subscribers for this system when an anomaly transitions to confirmed.",
|
|
49
58
|
},
|
|
50
59
|
} as const;
|
|
51
60
|
|
|
61
|
+
const BASELINE_WINDOW_OPTIONS: { value: string; label: string }[] = [
|
|
62
|
+
{ value: "1d", label: "1 day" },
|
|
63
|
+
{ value: "3d", label: "3 days" },
|
|
64
|
+
{ value: "7d", label: "7 days (recommended)" },
|
|
65
|
+
{ value: "14d", label: "14 days" },
|
|
66
|
+
{ value: "30d", label: "30 days" },
|
|
67
|
+
];
|
|
68
|
+
|
|
52
69
|
export function AnomalySettingsForm({
|
|
53
70
|
values,
|
|
54
71
|
onChange,
|
|
@@ -57,14 +74,7 @@ export function AnomalySettingsForm({
|
|
|
57
74
|
variant,
|
|
58
75
|
}: AnomalySettingsFormProps) {
|
|
59
76
|
const copy = COPY[variant];
|
|
60
|
-
const {
|
|
61
|
-
enabled,
|
|
62
|
-
sensitivity,
|
|
63
|
-
confirmationWindow,
|
|
64
|
-
driftEnabled,
|
|
65
|
-
driftThreshold,
|
|
66
|
-
fieldOverrides,
|
|
67
|
-
} = values;
|
|
77
|
+
const { enabled, baselineWindow, notify, fieldOverrides } = values;
|
|
68
78
|
|
|
69
79
|
const handleFieldOverrideChange = (
|
|
70
80
|
field: string,
|
|
@@ -76,15 +86,34 @@ export function AnomalySettingsForm({
|
|
|
76
86
|
onChange("fieldOverrides", next);
|
|
77
87
|
};
|
|
78
88
|
|
|
89
|
+
const handleFieldPatch = (
|
|
90
|
+
field: string,
|
|
91
|
+
patch: Partial<AnomalyFieldConfig>,
|
|
92
|
+
) => {
|
|
93
|
+
const next = { ...fieldOverrides };
|
|
94
|
+
next[field] = { ...next[field], ...patch };
|
|
95
|
+
onChange("fieldOverrides", next);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleFieldReset = (field: string) => {
|
|
99
|
+
const next = { ...fieldOverrides };
|
|
100
|
+
delete next[field];
|
|
101
|
+
onChange("fieldOverrides", next);
|
|
102
|
+
};
|
|
103
|
+
|
|
79
104
|
return (
|
|
80
105
|
<div className="space-y-4">
|
|
81
106
|
<div className="flex items-center justify-between p-4 border rounded-md">
|
|
82
107
|
<div className="space-y-0.5">
|
|
83
108
|
<Label className="text-base font-medium">{copy.enabledLabel}</Label>
|
|
84
|
-
<div className="text-sm text-muted-foreground">
|
|
109
|
+
<div className="text-sm text-muted-foreground">
|
|
110
|
+
{copy.enabledDescription}
|
|
111
|
+
</div>
|
|
85
112
|
</div>
|
|
86
113
|
<div className="flex items-center gap-3">
|
|
87
|
-
<span className="text-sm font-medium">
|
|
114
|
+
<span className="text-sm font-medium">
|
|
115
|
+
{enabled ? "Enabled" : "Disabled"}
|
|
116
|
+
</span>
|
|
88
117
|
<Toggle
|
|
89
118
|
checked={enabled}
|
|
90
119
|
onCheckedChange={(val) => onChange("enabled", val)}
|
|
@@ -95,101 +124,44 @@ export function AnomalySettingsForm({
|
|
|
95
124
|
|
|
96
125
|
<div className="grid gap-6 md:grid-cols-2 p-4 border rounded-md">
|
|
97
126
|
<div className="space-y-2">
|
|
98
|
-
<Label htmlFor="
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div className="space-y-2">
|
|
125
|
-
<Label htmlFor="confirmationWindow">{copy.confirmationLabel}</Label>
|
|
126
|
-
<div className="pt-4 pb-2 px-1">
|
|
127
|
-
<Slider
|
|
128
|
-
id="confirmationWindow"
|
|
129
|
-
value={[confirmationWindow]}
|
|
130
|
-
min={1}
|
|
131
|
-
max={10}
|
|
132
|
-
step={1}
|
|
133
|
-
onValueChange={(val) => onChange("confirmationWindow", val[0])}
|
|
134
|
-
disabled={!enabled || isLocked}
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
<div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
|
|
138
|
-
<span>1 Run</span>
|
|
139
|
-
<span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
140
|
-
{confirmationWindow} Runs
|
|
141
|
-
</span>
|
|
142
|
-
<span>10 Runs</span>
|
|
143
|
-
</div>
|
|
144
|
-
{variant === "template" && (
|
|
145
|
-
<p className="text-xs text-muted-foreground pt-2">
|
|
146
|
-
Number of consecutive anomalous runs required before an alert is triggered.
|
|
147
|
-
</p>
|
|
148
|
-
)}
|
|
127
|
+
<Label htmlFor="baselineWindow" className="text-sm font-medium">
|
|
128
|
+
Baseline window
|
|
129
|
+
</Label>
|
|
130
|
+
<Select
|
|
131
|
+
value={baselineWindow}
|
|
132
|
+
onValueChange={(val) => onChange("baselineWindow", val)}
|
|
133
|
+
disabled={!enabled || isLocked}
|
|
134
|
+
>
|
|
135
|
+
<SelectTrigger id="baselineWindow" className="h-10">
|
|
136
|
+
<SelectValue />
|
|
137
|
+
</SelectTrigger>
|
|
138
|
+
<SelectContent>
|
|
139
|
+
{BASELINE_WINDOW_OPTIONS.map((opt) => (
|
|
140
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
141
|
+
{opt.label}
|
|
142
|
+
</SelectItem>
|
|
143
|
+
))}
|
|
144
|
+
</SelectContent>
|
|
145
|
+
</Select>
|
|
146
|
+
<p className="text-xs text-muted-foreground">
|
|
147
|
+
How much history to use when computing each metric's baseline.
|
|
148
|
+
Longer windows are smoother but slower to react to legitimate
|
|
149
|
+
changes.
|
|
150
|
+
</p>
|
|
149
151
|
</div>
|
|
150
|
-
</div>
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
<div className="flex items-center justify-between md:col-span-2">
|
|
153
|
+
<div className="flex items-center justify-between md:col-span-1">
|
|
154
154
|
<div className="space-y-0.5">
|
|
155
|
-
<Label className="text-
|
|
156
|
-
<div className="text-
|
|
157
|
-
|
|
155
|
+
<Label className="text-sm font-medium">{copy.notifyLabel}</Label>
|
|
156
|
+
<div className="text-xs text-muted-foreground">
|
|
157
|
+
{copy.notifyDescription}
|
|
158
158
|
</div>
|
|
159
159
|
</div>
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
disabled={!enabled || isLocked}
|
|
166
|
-
/>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<div className="space-y-2 md:col-span-2">
|
|
171
|
-
<Label htmlFor="driftThreshold">Drift Threshold (σ)</Label>
|
|
172
|
-
<div className="pt-4 pb-2 px-1">
|
|
173
|
-
<Slider
|
|
174
|
-
id="driftThreshold"
|
|
175
|
-
value={[driftThreshold]}
|
|
176
|
-
min={1}
|
|
177
|
-
max={4}
|
|
178
|
-
step={0.1}
|
|
179
|
-
onValueChange={(val) => onChange("driftThreshold", val[0])}
|
|
180
|
-
disabled={!enabled || !driftEnabled || isLocked}
|
|
181
|
-
/>
|
|
182
|
-
</div>
|
|
183
|
-
<div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
|
|
184
|
-
<span>1.0σ (More)</span>
|
|
185
|
-
<span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
186
|
-
{driftThreshold.toFixed(1)}σ
|
|
187
|
-
</span>
|
|
188
|
-
<span>4.0σ (Fewer)</span>
|
|
189
|
-
</div>
|
|
190
|
-
<p className="text-xs text-muted-foreground">
|
|
191
|
-
Drift fires when the projected change over the baseline window exceeds this many standard deviations.
|
|
192
|
-
</p>
|
|
160
|
+
<Toggle
|
|
161
|
+
checked={notify}
|
|
162
|
+
onCheckedChange={(val) => onChange("notify", val)}
|
|
163
|
+
disabled={!enabled || isLocked}
|
|
164
|
+
/>
|
|
193
165
|
</div>
|
|
194
166
|
</div>
|
|
195
167
|
|
|
@@ -199,12 +171,10 @@ export function AnomalySettingsForm({
|
|
|
199
171
|
availableFields={availableFields}
|
|
200
172
|
fieldOverrides={fieldOverrides}
|
|
201
173
|
onChange={handleFieldOverrideChange}
|
|
174
|
+
onPatchField={handleFieldPatch}
|
|
175
|
+
onResetField={handleFieldReset}
|
|
202
176
|
parentEnabled={enabled}
|
|
203
177
|
isLocked={isLocked}
|
|
204
|
-
defaultSensitivity={sensitivity}
|
|
205
|
-
defaultConfirmationWindow={confirmationWindow}
|
|
206
|
-
defaultDriftEnabled={driftEnabled}
|
|
207
|
-
defaultDriftThreshold={driftThreshold}
|
|
208
178
|
/>
|
|
209
179
|
</div>
|
|
210
180
|
);
|
|
@@ -24,10 +24,8 @@ import {
|
|
|
24
24
|
|
|
25
25
|
const DEFAULT_VALUES: AnomalySettingsFormValues = {
|
|
26
26
|
enabled: true,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
driftEnabled: true,
|
|
30
|
-
driftThreshold: 2,
|
|
27
|
+
baselineWindow: "7d",
|
|
28
|
+
notify: true,
|
|
31
29
|
fieldOverrides: {},
|
|
32
30
|
};
|
|
33
31
|
|
|
@@ -36,9 +34,6 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
|
|
|
36
34
|
const anomalyClient = usePluginClient(AnomalyApi);
|
|
37
35
|
|
|
38
36
|
const [values, setValues] = useState<AnomalySettingsFormValues>(DEFAULT_VALUES);
|
|
39
|
-
// Template-only settings — preserved on save but not surfaced as form controls.
|
|
40
|
-
const [baselineWindow, setBaselineWindow] = useState("7d");
|
|
41
|
-
const [notify, setNotify] = useState(true);
|
|
42
37
|
|
|
43
38
|
const { data: configRecord, isLoading } = anomalyClient.getAnomalyConfig.useQuery(
|
|
44
39
|
{ configurationId: context.configurationId },
|
|
@@ -56,16 +51,12 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
|
|
|
56
51
|
if (configRecord?.data) {
|
|
57
52
|
setValues({
|
|
58
53
|
enabled: configRecord.data.enabled ?? true,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
driftEnabled: configRecord.data.driftEnabled ?? true,
|
|
62
|
-
driftThreshold: configRecord.data.driftThreshold ?? 2,
|
|
54
|
+
baselineWindow: configRecord.data.baselineWindow ?? "7d",
|
|
55
|
+
notify: configRecord.data.notify ?? true,
|
|
63
56
|
fieldOverrides:
|
|
64
57
|
(configRecord.data.fieldOverrides as Record<string, AnomalyFieldConfig>) ??
|
|
65
58
|
{},
|
|
66
59
|
});
|
|
67
|
-
setBaselineWindow(configRecord.data.baselineWindow ?? "7d");
|
|
68
|
-
setNotify(configRecord.data.notify ?? true);
|
|
69
60
|
}
|
|
70
61
|
}, [configRecord]);
|
|
71
62
|
|
|
@@ -79,11 +70,7 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
|
|
|
79
70
|
const handleSave = () => {
|
|
80
71
|
updateMutation.mutate({
|
|
81
72
|
configurationId: context.configurationId,
|
|
82
|
-
config:
|
|
83
|
-
...values,
|
|
84
|
-
baselineWindow,
|
|
85
|
-
notify,
|
|
86
|
-
},
|
|
73
|
+
config: values,
|
|
87
74
|
});
|
|
88
75
|
};
|
|
89
76
|
|
|
@@ -8,12 +8,15 @@ import type { AnomalyDirection } from "@checkstack/anomaly-common";
|
|
|
8
8
|
export type AnomalyFieldMeta = {
|
|
9
9
|
path: string;
|
|
10
10
|
type: string;
|
|
11
|
+
unit?: string;
|
|
11
12
|
defaultEnabled: boolean;
|
|
12
13
|
defaultDirection?: AnomalyDirection;
|
|
13
14
|
defaultSensitivity?: number;
|
|
14
15
|
defaultConfirmationWindow?: number;
|
|
15
16
|
defaultDriftEnabled?: boolean;
|
|
16
17
|
defaultDriftThreshold?: number;
|
|
18
|
+
defaultMinAbsoluteDelta?: number;
|
|
19
|
+
defaultMinRelativeDelta?: number;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
export function useAnomalyFields(configurationId: string | undefined) {
|
|
@@ -50,12 +53,15 @@ export function useAnomalyFields(configurationId: string | undefined) {
|
|
|
50
53
|
keys.push({
|
|
51
54
|
path,
|
|
52
55
|
type: value.type || "string",
|
|
56
|
+
unit: typeof value["x-chart-unit"] === "string" ? value["x-chart-unit"] : undefined,
|
|
53
57
|
defaultEnabled,
|
|
54
58
|
defaultDirection: value["x-anomaly-direction"] as AnomalyDirection | undefined,
|
|
55
59
|
defaultSensitivity: typeof value["x-anomaly-sensitivity"] === "number" ? value["x-anomaly-sensitivity"] : undefined,
|
|
56
60
|
defaultConfirmationWindow: typeof value["x-anomaly-confirmation-window"] === "number" ? value["x-anomaly-confirmation-window"] : undefined,
|
|
57
61
|
defaultDriftEnabled: typeof value["x-anomaly-drift-enabled"] === "boolean" ? value["x-anomaly-drift-enabled"] : undefined,
|
|
58
62
|
defaultDriftThreshold: typeof value["x-anomaly-drift-threshold"] === "number" ? value["x-anomaly-drift-threshold"] : undefined,
|
|
63
|
+
defaultMinAbsoluteDelta: typeof value["x-anomaly-min-absolute-delta"] === "number" ? value["x-anomaly-min-absolute-delta"] : undefined,
|
|
64
|
+
defaultMinRelativeDelta: typeof value["x-anomaly-min-relative-delta"] === "number" ? value["x-anomaly-min-relative-delta"] : undefined,
|
|
59
65
|
});
|
|
60
66
|
}
|
|
61
67
|
}
|
package/tsconfig.json
CHANGED
|
@@ -5,5 +5,37 @@
|
|
|
5
5
|
},
|
|
6
6
|
"include": [
|
|
7
7
|
"src"
|
|
8
|
+
],
|
|
9
|
+
"references": [
|
|
10
|
+
{
|
|
11
|
+
"path": "../anomaly-common"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "../catalog-common"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "../common"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "../frontend-api"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "../healthcheck-common"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "../healthcheck-frontend"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "../notification-common"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "../notification-frontend"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "../signal-frontend"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "../ui"
|
|
39
|
+
}
|
|
8
40
|
]
|
|
9
41
|
}
|