@checkstack/anomaly-frontend 0.4.7 → 0.5.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 +105 -0
- package/package.json +16 -16
- package/src/components/AnomalyFieldMuteList.tsx +4 -1
- package/src/components/AnomalySignalsFiller.tsx +80 -0
- package/src/components/SystemAnomalyBadge.tsx +4 -20
- package/src/components/SystemAnomalyWidget.tsx +37 -0
- package/src/plugin.tsx +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
1
|
# @checkstack/anomaly-frontend
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9dcc848: Auto-resolve anomalies that settle at a new normal, and add global suppression.
|
|
8
|
+
|
|
9
|
+
Part A (bug fix): a confirmed anomaly used to stay stuck in `anomaly` indefinitely when the metric settled at a new stable level. Both detectors now carry a baseline-independent self-resolution path - spike: after `STABLE_RESOLUTION_RUN_COUNT` (5) consecutive healthy samples within `STABLE_RESOLUTION_RELATIVE_BAND` (10%) the row self-resolves to `recovered`; drift: when the projected change goes flat relative to the new mean for `STABLE_DRIFT_RESOLUTION_RUN_COUNT` (2) analyzer runs. The original baseline-relative recovery path is unchanged.
|
|
10
|
+
|
|
11
|
+
Part B (feature): global (per-row) suppression. New `suppressedAt` / `suppressedValue` / `suppressedBaseline` columns (Drizzle migration `0005`), `suppressAnomaly` / `unsuppressAnomaly` RPCs gated by `anomaly_feed.manage`, and a `suppression` filter on `getAnomalies` (default `active` hides suppressed rows). Suppressed rows drop out of the dashboard badge/widget active count; the widget exposes an eye-off suppress affordance. Suppression auto-clears once the observed value moves more than `SUPPRESSION_REACTIVATION_DELTA` (25%) from the value it was suppressed at. All suppression state lives on the shared `anomalies` row, so every pod reads the same active/suppressed set. Distinct from the existing per-user notification mute.
|
|
12
|
+
|
|
13
|
+
This is a beta minor.
|
|
14
|
+
|
|
15
|
+
- 9dcc848: Redesign the dashboard as an extensible "needs attention" overview, and normalize system state badges.
|
|
16
|
+
|
|
17
|
+
The dashboard now surfaces ONLY systems that need attention (degraded, unhealthy, breaching/at-risk SLO, under an incident or active maintenance, anomalous, or with a dependency problem) and hides everything healthy. A compact header summarises fleet health and filters by severity; each problem renders as an elevated card with one row per issue that deep-links to where the issue originates. A calm "all clear" state shows when nothing needs attention, a live "recent activity" feed sits below, and a "View catalog" link replaces the duplicated system list.
|
|
18
|
+
|
|
19
|
+
New platform contract `SystemSignalsSlot` (`@checkstack/catalog-common`): a headless, render-once slot where any plugin bulk-fetches and reports structured `SystemSignal[]` per system via `onSignals(sourceId, map)`. The dashboard aggregates every source agnostic to which plugins contribute; each core reliability plugin (healthcheck, incident, SLO, maintenance, anomaly, dependency) ships a filler, and third-party plugins add new per-system state the same way with no dashboard change. Signals carry an `iconName` rendered via `DynamicIcon` so the contract stays React-free. The dashboard's old summary tiles and overview sheets are removed, so it no longer depends on those plugins' packages. The group "subscribe" control moved onto the catalog browse page's group headers.
|
|
20
|
+
|
|
21
|
+
System state badges are normalized into one icon-only `@checkstack/ui` `StatusBadge` primitive - a small tinted icon chip with the full label on hover/focus (and via `aria-label`). Each signal uses its feature's navbar icon (health = Activity, incident = AlertTriangle, SLO = Target, maintenance = Wrench, dependency = GitBranch; anomaly = ChartSpline). Badges self-sort by severity via CSS `order` (error -> warn -> info), tooltips are scoped to a named group, and in catalog browse rows the cluster moved to the right edge.
|
|
22
|
+
|
|
23
|
+
This is a beta minor.
|
|
24
|
+
|
|
25
|
+
- 9dcc848: Cut initial-load JS: lazy plugin contributions, a hardened lazy-by-default contribution contract, on-demand Monaco, and a lighter icon/chart load.
|
|
26
|
+
|
|
27
|
+
- Lazy plugin route pages: each plugin's route `element` references a `React.lazy`-wrapped page rendered inside a shared `<Suspense>` boundary. Plugins still register synchronously, so nav, slots, commands, API factories, and `foreignSignals` are available on first paint. This moves ~37 route-page chunks (~600 KB) out of the entry; the entry chunk drops from ~2.4 MB to ~190 KB. Auth flow pages stay eager. The `@checkstack/scripts` scaffold template generates lazy route pages too.
|
|
28
|
+
- Hardened contribution contract (BREAKING, frontend plugin contract): plugins declare contributions lazily and let the framework own code-splitting, Suspense, and per-plugin error isolation. Routes use `load: () => import("./Page").then((m) => ({ default: m.Page }))` instead of `element: <Page />` (`element` is still accepted for the rare page that must paint without a chunk fetch; provide exactly one). Slot extensions accept either an eager `component` or a lazy `load`; new `getLazyContribution` + `ExtensionComponent` exports from `@checkstack/frontend-api` render either kind. This also fixes runtime-installed plugins: `ExtensionSlot` subscribes to the plugin registry, and the API registry rebuilds when the plugin set changes (`getPlugins()` returns an immutable snapshot via `useSyncExternalStore`). A per-plugin error boundary contains a bad contribution.
|
|
29
|
+
- On-demand Monaco: the `@checkstack/ui` barrel no longer pulls the `@codingame/*` / `monaco-languageclient` stack into the initial load. `CodeEditor` lazy-loads its Monaco-backed editor behind `React.lazy` + Suspense, `validateTypeScriptSources` imports the editor API via in-body `await import(...)`, and the "vscode services ready" signal moved to a Monaco-free module. The ~10 MB editor body loads only when a `CodeEditor` mounts. A `react-vendor` `manualChunks` split was added for stable vendor caching.
|
|
30
|
+
- lucide-react 1.x + lighter icons/charts (BREAKING for icon consumers): lucide-react unified from three drifting ranges to `^1.17.0`. lucide v1 removed brand icons, so the GitHub/GitLab marks are vendored in `@checkstack/ui` (`GithubIcon`, `GitlabIcon`, `brandIcons`); a new `IconName` type (`LucideIconName | BrandIconName`) in `@checkstack/common` is canonical, accepted by `AuthStrategy.icon` and the card components, so data-driven brand names keep working. `DynamicIcon` no longer eagerly imports lucide's ~1600-icon map (~1 MB) - it lives in a `React.lazy` `iconRegistry` chunk fetched on first data-driven render, while statically named-imported icons tree-shake normally. The recharts-backed health-check charts (~300 KB) and the `HealthCheckSystemOverview` drawer leave the initial load.
|
|
31
|
+
|
|
32
|
+
BREAKING CHANGES:
|
|
33
|
+
|
|
34
|
+
- Frontend plugin contract: routes/slot contributions are lazy-by-default (`load` instead of `element`/eager elements) as described above.
|
|
35
|
+
- Any external consumer importing a brand icon from `lucide-react` (e.g. `import { Github } from "lucide-react"`) must switch to the vendored `@checkstack/ui` brand icons or a custom SVG.
|
|
36
|
+
|
|
37
|
+
This is a beta minor.
|
|
38
|
+
|
|
39
|
+
- 9dcc848: Align workspace dependency versions and migrate React Router to v7.
|
|
40
|
+
|
|
41
|
+
BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
|
|
42
|
+
|
|
43
|
+
Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
|
|
44
|
+
|
|
45
|
+
Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
|
|
46
|
+
|
|
47
|
+
Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [9dcc848]
|
|
52
|
+
- Updated dependencies [9dcc848]
|
|
53
|
+
- Updated dependencies [9dcc848]
|
|
54
|
+
- Updated dependencies [9dcc848]
|
|
55
|
+
- Updated dependencies [9dcc848]
|
|
56
|
+
- Updated dependencies [9dcc848]
|
|
57
|
+
- Updated dependencies [9dcc848]
|
|
58
|
+
- Updated dependencies [9dcc848]
|
|
59
|
+
- Updated dependencies [9dcc848]
|
|
60
|
+
- Updated dependencies [9dcc848]
|
|
61
|
+
- Updated dependencies [9dcc848]
|
|
62
|
+
- Updated dependencies [9dcc848]
|
|
63
|
+
- Updated dependencies [9dcc848]
|
|
64
|
+
- Updated dependencies [9dcc848]
|
|
65
|
+
- Updated dependencies [9dcc848]
|
|
66
|
+
- Updated dependencies [9dcc848]
|
|
67
|
+
- Updated dependencies [9dcc848]
|
|
68
|
+
- @checkstack/ui@1.13.0
|
|
69
|
+
- @checkstack/healthcheck-common@1.5.0
|
|
70
|
+
- @checkstack/notification-common@1.3.0
|
|
71
|
+
- @checkstack/anomaly-common@1.3.0
|
|
72
|
+
- @checkstack/catalog-common@2.3.0
|
|
73
|
+
- @checkstack/healthcheck-frontend@0.23.0
|
|
74
|
+
- @checkstack/common@0.13.0
|
|
75
|
+
- @checkstack/frontend-api@0.7.0
|
|
76
|
+
- @checkstack/notification-frontend@0.5.0
|
|
77
|
+
- @checkstack/signal-frontend@0.2.0
|
|
78
|
+
|
|
79
|
+
## 0.4.8
|
|
80
|
+
|
|
81
|
+
### Patch Changes
|
|
82
|
+
|
|
83
|
+
- Updated dependencies [b995afb]
|
|
84
|
+
- Updated dependencies [270ef29]
|
|
85
|
+
- Updated dependencies [270ef29]
|
|
86
|
+
- Updated dependencies [270ef29]
|
|
87
|
+
- Updated dependencies [270ef29]
|
|
88
|
+
- Updated dependencies [b995afb]
|
|
89
|
+
- Updated dependencies [b995afb]
|
|
90
|
+
- Updated dependencies [b995afb]
|
|
91
|
+
- Updated dependencies [b995afb]
|
|
92
|
+
- Updated dependencies [b995afb]
|
|
93
|
+
- Updated dependencies [b995afb]
|
|
94
|
+
- Updated dependencies [270ef29]
|
|
95
|
+
- Updated dependencies [270ef29]
|
|
96
|
+
- Updated dependencies [b995afb]
|
|
97
|
+
- Updated dependencies [b995afb]
|
|
98
|
+
- Updated dependencies [270ef29]
|
|
99
|
+
- Updated dependencies [b995afb]
|
|
100
|
+
- Updated dependencies [270ef29]
|
|
101
|
+
- Updated dependencies [b995afb]
|
|
102
|
+
- Updated dependencies [270ef29]
|
|
103
|
+
- @checkstack/ui@1.12.0
|
|
104
|
+
- @checkstack/healthcheck-common@1.4.0
|
|
105
|
+
- @checkstack/healthcheck-frontend@0.22.0
|
|
106
|
+
- @checkstack/notification-frontend@0.4.7
|
|
107
|
+
|
|
3
108
|
## 0.4.7
|
|
4
109
|
|
|
5
110
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/anomaly-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.tsx",
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
"lint:code": "eslint . --max-warnings 0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@checkstack/anomaly-common": "1.2.
|
|
17
|
-
"@checkstack/catalog-common": "2.2.
|
|
18
|
-
"@checkstack/common": "0.
|
|
19
|
-
"@checkstack/frontend-api": "0.
|
|
20
|
-
"@checkstack/healthcheck-common": "1.
|
|
21
|
-
"@checkstack/healthcheck-frontend": "0.
|
|
22
|
-
"@checkstack/notification-common": "1.2.
|
|
23
|
-
"@checkstack/notification-frontend": "0.4.
|
|
24
|
-
"@checkstack/signal-frontend": "0.1.
|
|
25
|
-
"@checkstack/ui": "1.
|
|
26
|
-
"date-fns": "^4.
|
|
27
|
-
"lucide-react": "^
|
|
28
|
-
"react": "^18.
|
|
29
|
-
"react-router-dom": "^7.
|
|
16
|
+
"@checkstack/anomaly-common": "1.2.3",
|
|
17
|
+
"@checkstack/catalog-common": "2.2.3",
|
|
18
|
+
"@checkstack/common": "0.12.0",
|
|
19
|
+
"@checkstack/frontend-api": "0.6.0",
|
|
20
|
+
"@checkstack/healthcheck-common": "1.4.0",
|
|
21
|
+
"@checkstack/healthcheck-frontend": "0.22.0",
|
|
22
|
+
"@checkstack/notification-common": "1.2.1",
|
|
23
|
+
"@checkstack/notification-frontend": "0.4.7",
|
|
24
|
+
"@checkstack/signal-frontend": "0.1.5",
|
|
25
|
+
"@checkstack/ui": "1.12.0",
|
|
26
|
+
"date-fns": "^4.4.0",
|
|
27
|
+
"lucide-react": "^1.17.0",
|
|
28
|
+
"react": "^18.3.1",
|
|
29
|
+
"react-router-dom": "^7.16.0",
|
|
30
30
|
"zod": "^4.2.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@checkstack/scripts": "0.3.
|
|
33
|
+
"@checkstack/scripts": "0.3.4",
|
|
34
34
|
"@checkstack/tsconfig": "0.0.7",
|
|
35
35
|
"@types/react": "^18.2.0",
|
|
36
36
|
"typescript": "^5.0.0"
|
|
@@ -23,8 +23,11 @@ export const AnomalyFieldMuteList: React.FC<{
|
|
|
23
23
|
const anomalyClient = usePluginClient(AnomalyApi);
|
|
24
24
|
const toast = useToast();
|
|
25
25
|
|
|
26
|
+
// Notification mute is a per-user concern orthogonal to global suppression,
|
|
27
|
+
// so the mute-management list must include suppressed rows — otherwise a
|
|
28
|
+
// field that is both suppressed and muted would lose its unmute affordance.
|
|
26
29
|
const { data: anomalies = [] } = anomalyClient.getAnomalies.useQuery(
|
|
27
|
-
{ systemId, limit: 50 },
|
|
30
|
+
{ systemId, limit: 50, suppression: "all" },
|
|
28
31
|
{ staleTime: 30_000 },
|
|
29
32
|
);
|
|
30
33
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from "react";
|
|
2
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
|
+
import { resolveRoute } from "@checkstack/common";
|
|
4
|
+
import {
|
|
5
|
+
SystemSignalsSlot,
|
|
6
|
+
type SystemSignal,
|
|
7
|
+
type SystemSignalsMap,
|
|
8
|
+
} from "@checkstack/catalog-common";
|
|
9
|
+
import { AnomalyApi } from "@checkstack/anomaly-common";
|
|
10
|
+
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
11
|
+
|
|
12
|
+
type Props = SlotContext<typeof SystemSignalsSlot>;
|
|
13
|
+
|
|
14
|
+
const SOURCE_ID = "anomaly";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reports confirmed anomalies and suspicious states as dashboard signals.
|
|
18
|
+
* Reuses the two globally-deduped anomaly queries (the same ones the badge
|
|
19
|
+
* uses) and emits one signal per anomaly for systems in the overview, deep-
|
|
20
|
+
* linking to the affected check's history. Headless filler for
|
|
21
|
+
* {@link SystemSignalsSlot}.
|
|
22
|
+
*/
|
|
23
|
+
export const AnomalySignalsFiller: React.FC<Props> = ({
|
|
24
|
+
systemIds,
|
|
25
|
+
onSignals,
|
|
26
|
+
}) => {
|
|
27
|
+
const anomalyClient = usePluginClient(AnomalyApi);
|
|
28
|
+
|
|
29
|
+
const { data: confirmed = [] } = anomalyClient.getAnomalies.useQuery(
|
|
30
|
+
{ state: "anomaly", limit: 500 },
|
|
31
|
+
{ staleTime: 30_000 },
|
|
32
|
+
);
|
|
33
|
+
const { data: suspicious = [] } = anomalyClient.getAnomalies.useQuery(
|
|
34
|
+
{ state: "suspicious", limit: 500 },
|
|
35
|
+
{ staleTime: 30_000 },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const signals = useMemo<SystemSignalsMap>(() => {
|
|
39
|
+
const result: SystemSignalsMap = {};
|
|
40
|
+
const inOverview = new Set(systemIds);
|
|
41
|
+
|
|
42
|
+
const add = (
|
|
43
|
+
systemId: string,
|
|
44
|
+
configurationId: string,
|
|
45
|
+
fieldPath: string,
|
|
46
|
+
startedAt: string,
|
|
47
|
+
tone: SystemSignal["tone"],
|
|
48
|
+
label: string,
|
|
49
|
+
) => {
|
|
50
|
+
if (!inOverview.has(systemId)) return;
|
|
51
|
+
const signal: SystemSignal = {
|
|
52
|
+
source: SOURCE_ID,
|
|
53
|
+
tone,
|
|
54
|
+
label,
|
|
55
|
+
detail: fieldPath,
|
|
56
|
+
href: resolveRoute(healthcheckRoutes.routes.historyDetail, {
|
|
57
|
+
systemId,
|
|
58
|
+
configurationId,
|
|
59
|
+
}),
|
|
60
|
+
since: new Date(startedAt).toISOString(),
|
|
61
|
+
iconName: "ChartSpline",
|
|
62
|
+
};
|
|
63
|
+
(result[systemId] ??= []).push(signal);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const a of confirmed) {
|
|
67
|
+
add(a.systemId, a.configurationId, a.fieldPath, a.startedAt, "warn", "Anomaly detected");
|
|
68
|
+
}
|
|
69
|
+
for (const a of suspicious) {
|
|
70
|
+
add(a.systemId, a.configurationId, a.fieldPath, a.startedAt, "info", "Suspicious behaviour");
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}, [confirmed, suspicious, systemIds]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
onSignals(SOURCE_ID, signals);
|
|
77
|
+
}, [signals, onSignals]);
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
@@ -2,8 +2,8 @@ import React, { useMemo } from "react";
|
|
|
2
2
|
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
4
4
|
import { AnomalyApi, type AnomalyState } from "@checkstack/anomaly-common";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { StatusBadge } from "@checkstack/ui";
|
|
6
|
+
import { ChartSpline } from "lucide-react";
|
|
7
7
|
|
|
8
8
|
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
9
9
|
|
|
@@ -48,24 +48,8 @@ export const SystemAnomalyBadge: React.FC<Props> = ({ system }) => {
|
|
|
48
48
|
if (!systemState) return <></>;
|
|
49
49
|
|
|
50
50
|
if (systemState === "anomaly") {
|
|
51
|
-
return
|
|
52
|
-
<Badge
|
|
53
|
-
variant="warning"
|
|
54
|
-
className="flex items-center gap-1 shrink-0 cursor-default"
|
|
55
|
-
>
|
|
56
|
-
<AlertTriangle className="h-3 w-3" />
|
|
57
|
-
Anomaly
|
|
58
|
-
</Badge>
|
|
59
|
-
);
|
|
51
|
+
return <StatusBadge tone="warn" icon={ChartSpline} label="Anomaly detected" />;
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
return
|
|
63
|
-
<Badge
|
|
64
|
-
variant="outline"
|
|
65
|
-
className="flex items-center gap-1 shrink-0 cursor-default border-warning/50 text-warning"
|
|
66
|
-
>
|
|
67
|
-
<HelpCircle className="h-3 w-3" />
|
|
68
|
-
Suspicious
|
|
69
|
-
</Badge>
|
|
70
|
-
);
|
|
54
|
+
return <StatusBadge tone="info" icon={ChartSpline} label="Suspicious behaviour" />;
|
|
71
55
|
};
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
LineChart,
|
|
27
27
|
Bell,
|
|
28
28
|
BellOff,
|
|
29
|
+
EyeOff,
|
|
29
30
|
} from "lucide-react";
|
|
30
31
|
import { formatDistanceToNow } from "date-fns";
|
|
31
32
|
import { Link } from "react-router-dom";
|
|
@@ -114,12 +115,16 @@ function AnomalyRow({
|
|
|
114
115
|
isMuted,
|
|
115
116
|
onToggleMute,
|
|
116
117
|
isToggling,
|
|
118
|
+
onSuppress,
|
|
119
|
+
isSuppressing,
|
|
117
120
|
}: {
|
|
118
121
|
anomaly: AnomalyDto;
|
|
119
122
|
systemId: string;
|
|
120
123
|
isMuted: boolean;
|
|
121
124
|
onToggleMute: (fieldPath: string, isMuted: boolean) => void;
|
|
122
125
|
isToggling: boolean;
|
|
126
|
+
onSuppress: (anomalyId: string) => void;
|
|
127
|
+
isSuppressing: boolean;
|
|
123
128
|
}) {
|
|
124
129
|
const isSuspicious = anomaly.state === "suspicious";
|
|
125
130
|
const isDrift = anomaly.kind === "drift";
|
|
@@ -237,6 +242,23 @@ function AnomalyRow({
|
|
|
237
242
|
<Bell className="h-3.5 w-3.5" />
|
|
238
243
|
)}
|
|
239
244
|
</Button>
|
|
245
|
+
{!isSuspicious && (
|
|
246
|
+
<Button
|
|
247
|
+
type="button"
|
|
248
|
+
variant="ghost"
|
|
249
|
+
size="icon"
|
|
250
|
+
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
251
|
+
disabled={isSuppressing}
|
|
252
|
+
title="Suppress this anomaly until it changes again"
|
|
253
|
+
onClick={(event) => {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
event.stopPropagation();
|
|
256
|
+
onSuppress(anomaly.id);
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
<EyeOff className="h-3.5 w-3.5" />
|
|
260
|
+
</Button>
|
|
261
|
+
)}
|
|
240
262
|
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors" />
|
|
241
263
|
</div>
|
|
242
264
|
</Link>
|
|
@@ -303,6 +325,19 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
|
|
|
303
325
|
}
|
|
304
326
|
};
|
|
305
327
|
|
|
328
|
+
// Suppressing removes the row from the active feed. The mutation invalidates
|
|
329
|
+
// the anomaly plugin's own queries on success, so the active list (which
|
|
330
|
+
// defaults to the "active" suppression filter) refetches without the row.
|
|
331
|
+
const suppressMutation = anomalyClient.suppressAnomaly.useMutation({
|
|
332
|
+
onError: () => {
|
|
333
|
+
toast.error("Failed to suppress anomaly");
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const handleSuppress = (anomalyId: string) => {
|
|
338
|
+
suppressMutation.mutate({ systemId: system.id, anomalyId });
|
|
339
|
+
};
|
|
340
|
+
|
|
306
341
|
const isLoading = loadingConfirmed || loadingSuspicious;
|
|
307
342
|
|
|
308
343
|
// Confirmed anomalies first, then suspicious
|
|
@@ -397,6 +432,8 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
|
|
|
397
432
|
isMuted={isSystemMuted || mutedFields.has(anomaly.fieldPath)}
|
|
398
433
|
onToggleMute={handleToggleMute}
|
|
399
434
|
isToggling={isToggling}
|
|
435
|
+
onSuppress={handleSuppress}
|
|
436
|
+
isSuppressing={suppressMutation.isPending}
|
|
400
437
|
/>
|
|
401
438
|
))}
|
|
402
439
|
</div>
|
package/src/plugin.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lazy, Suspense } from "react";
|
|
1
2
|
import { definePluginMetadata } from "@checkstack/common";
|
|
2
3
|
import { FrontendPlugin, createSlotExtension } from "@checkstack/frontend-api";
|
|
3
4
|
import {
|
|
@@ -11,17 +12,30 @@ import {
|
|
|
11
12
|
import {
|
|
12
13
|
SystemStateBadgesSlot,
|
|
13
14
|
SystemDetailsSlot,
|
|
15
|
+
SystemSignalsSlot,
|
|
14
16
|
} from "@checkstack/catalog-common";
|
|
15
17
|
import { registerSubscriptionSubControls } from "@checkstack/notification-frontend";
|
|
16
18
|
import { anomalySystemSubscription } from "@checkstack/anomaly-common";
|
|
17
|
-
import { AnomalyConfigPanel } from "./components/AnomalyConfigPanel";
|
|
18
|
-
import { AnomalyTemplatePanel } from "./components/AnomalyTemplatePanel";
|
|
19
19
|
import { SystemAnomalyBadge } from "./components/SystemAnomalyBadge";
|
|
20
20
|
import { SystemAnomalyWidget } from "./components/SystemAnomalyWidget";
|
|
21
21
|
import { AnomalyFieldMuteList } from "./components/AnomalyFieldMuteList";
|
|
22
22
|
import { IDETreeNode } from "@checkstack/ui";
|
|
23
23
|
import { Activity } from "lucide-react";
|
|
24
24
|
|
|
25
|
+
// Heavy IDE panels — lazy-loaded so they stay out of the initial bundle and
|
|
26
|
+
// load only when their IDE node is actually selected (the eager guards below
|
|
27
|
+
// decide that), not whenever the IDE panel slot renders.
|
|
28
|
+
const AnomalyTemplatePanel = lazy(() =>
|
|
29
|
+
import("./components/AnomalyTemplatePanel").then((m) => ({
|
|
30
|
+
default: m.AnomalyTemplatePanel,
|
|
31
|
+
})),
|
|
32
|
+
);
|
|
33
|
+
const AnomalyConfigPanel = lazy(() =>
|
|
34
|
+
import("./components/AnomalyConfigPanel").then((m) => ({
|
|
35
|
+
default: m.AnomalyConfigPanel,
|
|
36
|
+
})),
|
|
37
|
+
);
|
|
38
|
+
|
|
25
39
|
const pluginMetadata = definePluginMetadata({
|
|
26
40
|
pluginId: "anomaly",
|
|
27
41
|
});
|
|
@@ -33,6 +47,13 @@ export const plugin: FrontendPlugin = {
|
|
|
33
47
|
id: "anomaly.system-badge",
|
|
34
48
|
component: SystemAnomalyBadge,
|
|
35
49
|
}),
|
|
50
|
+
createSlotExtension(SystemSignalsSlot, {
|
|
51
|
+
id: "anomaly.dashboard.signals",
|
|
52
|
+
load: () =>
|
|
53
|
+
import("./components/AnomalySignalsFiller").then((m) => ({
|
|
54
|
+
default: m.AnomalySignalsFiller,
|
|
55
|
+
})),
|
|
56
|
+
}),
|
|
36
57
|
createSlotExtension(SystemDetailsSlot, {
|
|
37
58
|
id: "anomaly.system-details.widget",
|
|
38
59
|
component: SystemAnomalyWidget,
|
|
@@ -55,7 +76,11 @@ export const plugin: FrontendPlugin = {
|
|
|
55
76
|
if (!context.selectedNode?.startsWith("anomaly-template:")) {
|
|
56
77
|
return <></>;
|
|
57
78
|
}
|
|
58
|
-
return
|
|
79
|
+
return (
|
|
80
|
+
<Suspense fallback={null}>
|
|
81
|
+
<AnomalyTemplatePanel context={context} />
|
|
82
|
+
</Suspense>
|
|
83
|
+
);
|
|
59
84
|
},
|
|
60
85
|
}),
|
|
61
86
|
createSlotExtension(AssignmentIDENodeSlot, {
|
|
@@ -77,7 +102,11 @@ export const plugin: FrontendPlugin = {
|
|
|
77
102
|
if (!context.selectedNode?.startsWith("anomaly:")) {
|
|
78
103
|
return <></>;
|
|
79
104
|
}
|
|
80
|
-
return
|
|
105
|
+
return (
|
|
106
|
+
<Suspense fallback={null}>
|
|
107
|
+
<AnomalyConfigPanel context={context} />
|
|
108
|
+
</Suspense>
|
|
109
|
+
);
|
|
81
110
|
},
|
|
82
111
|
}),
|
|
83
112
|
],
|