@checkstack/anomaly-frontend 0.6.13 → 0.6.14
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 +123 -0
- package/package.json +12 -12
- package/src/components/AnomalyBadgeDataProvider.tsx +69 -0
- package/src/components/AnomalySignalsFiller.tsx +19 -8
- package/src/components/AnomalyTemplatePanel.tsx +13 -14
- package/src/components/CatalogBrowseAnomalyDataFiller.tsx +22 -0
- package/src/components/SystemAnomalyBadge.tsx +21 -17
- package/src/components/SystemAnomalyWidget.tsx +16 -7
- package/src/plugin.tsx +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,128 @@
|
|
|
1
1
|
# @checkstack/anomaly-frontend
|
|
2
2
|
|
|
3
|
+
## 0.6.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6c8b36b: The Logs, Metrics, and Traces cards on the system overview page now match the
|
|
8
|
+
other cards. They had drifted to a flat `bg-card` background with a
|
|
9
|
+
hairline-only shadow, so they rendered visibly flatter than their siblings
|
|
10
|
+
(health, dependency, SLO, incident, anomaly, maintenance), which all use the
|
|
11
|
+
detail-page gradient plus a soft two-layer elevation shadow.
|
|
12
|
+
|
|
13
|
+
The shared card surface is now a single primitive - `DetailCard` (and the
|
|
14
|
+
`detailCardSurface` / `detailCardSurfaceFlat` class constants) in
|
|
15
|
+
`@checkstack/ui` - instead of a className that was copy-pasted (and could
|
|
16
|
+
diverge) in every system-overview card. All of those cards now render from the
|
|
17
|
+
one primitive, so they cannot drift apart again. A new `error`-level ESLint
|
|
18
|
+
rule `checkstack/no-inline-detail-card-chrome` fails the build if a card in that
|
|
19
|
+
family re-declares the surface inline instead of using `DetailCard`.
|
|
20
|
+
|
|
21
|
+
- 6c8b36b: Smooth out loading states so surfaces no longer flash a wrong resolved state or
|
|
22
|
+
pop content in one piece at a time.
|
|
23
|
+
|
|
24
|
+
- **Dashboard no longer flashes "all systems healthy".** The overview aggregates
|
|
25
|
+
per-system signals from many plugins (health, incidents, SLOs, anomalies,
|
|
26
|
+
dependencies, log/metric/trace streams), each reporting asynchronously - so
|
|
27
|
+
before any had loaded, an empty problem list briefly read as an all-clear.
|
|
28
|
+
`SystemSignalsSlot` gains an additive `onLoadingChange` report; every source
|
|
29
|
+
filler reports its load state, and the dashboard holds its existing skeleton
|
|
30
|
+
until all mounted sources have settled (bounded by a grace period so a
|
|
31
|
+
non-reporting source cannot hang it).
|
|
32
|
+
- **System detail overview cards reveal together.** Each `SystemDetailsSlot` card
|
|
33
|
+
self-loads and several self-hide when empty, so they popped in one after
|
|
34
|
+
another. The slot gains an additive `onLoadingChange`; each card reports, and
|
|
35
|
+
the detail page keeps the cards mounted but behind a skeleton set until all
|
|
36
|
+
have settled, then reveals them at once - no stagger, no layout shift, and
|
|
37
|
+
cards with no content simply never appear.
|
|
38
|
+
- **Catalog manage "Health" column no longer pops in.** `CatalogBrowseHealthSlot`
|
|
39
|
+
gains an additive `onLoading` report (sourced from the health filler's bulk
|
|
40
|
+
fetch); the manage Systems tab shows a per-row placeholder until the health
|
|
41
|
+
data settles, so the status badges swap in instead of appearing onto an empty
|
|
42
|
+
cell. The same tab also keeps its state badges on one row (side by side)
|
|
43
|
+
instead of wrapping.
|
|
44
|
+
- The system detail **Dependencies** and **Logs / Metrics / Traces** cards are now
|
|
45
|
+
collapsed by default: each shows a compact "<title> N" summary and expands its
|
|
46
|
+
detail on click, so the overview column stays short. They render through a new
|
|
47
|
+
shared `CollapsibleDetailCard` (`@checkstack/ui`) that single-sources the header
|
|
48
|
+
layout (icon + title + count + rotating chevron) so every collapsible overview
|
|
49
|
+
card is vertically centred and behaves identically - the earlier per-card header
|
|
50
|
+
markup had drifted and left the Logs/Metrics/Traces titles off-centre when
|
|
51
|
+
collapsed.
|
|
52
|
+
- Moved the system detail **SLO card** from the full-width alert strip into the
|
|
53
|
+
left (monitoring) column, so it sits at the same width as the dependencies and
|
|
54
|
+
health cards; only maintenances and incidents stay full width. It now joins the
|
|
55
|
+
coordinated card reveal above.
|
|
56
|
+
- Removed a dead, unreferenced duplicate dashboard component
|
|
57
|
+
(`dashboard-frontend/src/Dashboard.tsx`); the live overview is
|
|
58
|
+
`DashboardSystemHealthSection`.
|
|
59
|
+
|
|
60
|
+
All slot-contract additions are optional/additive - existing fillers and
|
|
61
|
+
consumers keep working unchanged.
|
|
62
|
+
|
|
63
|
+
- 6c8b36b: Edit forms stay stable while you are typing. Previously, editing a system's
|
|
64
|
+
description (and many other edit dialogs/settings pages) would reset the field
|
|
65
|
+
mid-edit whenever a webhook update or realtime signal refetched the underlying
|
|
66
|
+
query: the form re-seeded its local state from the fresh query result on every
|
|
67
|
+
refetch. Forms now seed their local state ONCE - on the dialog's open
|
|
68
|
+
transition, or once per record via a stable key - and ignore background
|
|
69
|
+
refetches while you are editing.
|
|
70
|
+
|
|
71
|
+
New shared primitive `useSeedFormOnOpen(open, onInit)` in `@checkstack/ui`
|
|
72
|
+
(alongside the existing `useInitOnceForKey`) seeds a dialog form once per
|
|
73
|
+
open transition, StrictMode-safe. Fixed surfaces include the catalog
|
|
74
|
+
system/environment/group editors, the healthcheck platform-defaults dialog,
|
|
75
|
+
the SLO / gitops-provider / telemetry-source / satellite / announcement /
|
|
76
|
+
role edit dialogs, and the cache / queue / notification / secrets / anomaly /
|
|
77
|
+
profile / strategies settings pages (query-seeded pages also drop their loader
|
|
78
|
+
cache via `gcTime: 0` so a warm cache cannot race the one-shot seed).
|
|
79
|
+
|
|
80
|
+
- 6c8b36b: Speed up the catalog manage Systems tab and unify its per-row actions.
|
|
81
|
+
|
|
82
|
+
- The per-row `SystemHealthCheckAssignment` no longer runs two allocation-heavy
|
|
83
|
+
access hooks (`useCanAccessType` + `useResourceAccess`) plus a counts query
|
|
84
|
+
PER ROW - profiling showed this as the dominant, GC-bound cost of opening the
|
|
85
|
+
Systems tab. A new `CatalogSystemHealthCheckDataProvider`, folded around the
|
|
86
|
+
catalog tree via `CatalogBrowseDataBoundarySlot`, resolves the gate + counts
|
|
87
|
+
once for the whole visible list; the row action reads them from context (the
|
|
88
|
+
heavy standalone path is only rendered on surfaces without the provider, e.g.
|
|
89
|
+
the system detail page).
|
|
90
|
+
- The per-row `SystemAnomalyBadge` no longer instantiates two live query
|
|
91
|
+
observers (and scans up to 500-element arrays) per row. A new
|
|
92
|
+
`AnomalyBadgeDataProvider`, folded around the catalog browse/manage tree via
|
|
93
|
+
`CatalogBrowseDataBoundarySlot`, fetches the active + suspicious anomaly sets
|
|
94
|
+
once and exposes an O(1) per-system lookup - matching the SLO / incident /
|
|
95
|
+
health / dependency badges. Without the provider the badge falls back to its
|
|
96
|
+
own (deduped) queries, so the system detail page is unchanged.
|
|
97
|
+
- `ScopeSystemToTeamAction` and `SystemHealthCheckAssignment` now render through
|
|
98
|
+
the shared `RowAction`, so a system row's action cluster looks uniform.
|
|
99
|
+
`ScopeSystemToTeamAction` additionally defers mounting its Radix dialog until
|
|
100
|
+
first use, so a table of rows no longer mounts an idle dialog per row.
|
|
101
|
+
- `@checkstack/ui` `RowAction` gains an optional `badge` (e.g. an assigned-count
|
|
102
|
+
indicator) rendered next to the icon, so a count action stays a normal
|
|
103
|
+
`RowAction` instead of a bespoke button.
|
|
104
|
+
|
|
105
|
+
- Updated dependencies [6c8b36b]
|
|
106
|
+
- Updated dependencies [6c8b36b]
|
|
107
|
+
- Updated dependencies [6c8b36b]
|
|
108
|
+
- Updated dependencies [6c8b36b]
|
|
109
|
+
- Updated dependencies [6c8b36b]
|
|
110
|
+
- Updated dependencies [6c8b36b]
|
|
111
|
+
- Updated dependencies [6c8b36b]
|
|
112
|
+
- Updated dependencies [6c8b36b]
|
|
113
|
+
- Updated dependencies [6c8b36b]
|
|
114
|
+
- Updated dependencies [6c8b36b]
|
|
115
|
+
- @checkstack/ui@1.29.0
|
|
116
|
+
- @checkstack/healthcheck-frontend@0.37.0
|
|
117
|
+
- @checkstack/healthcheck-common@1.18.0
|
|
118
|
+
- @checkstack/catalog-common@2.8.0
|
|
119
|
+
- @checkstack/frontend-api@0.16.1
|
|
120
|
+
- @checkstack/notification-frontend@0.9.6
|
|
121
|
+
- @checkstack/common@0.23.0
|
|
122
|
+
- @checkstack/anomaly-common@1.8.2
|
|
123
|
+
- @checkstack/notification-common@1.7.2
|
|
124
|
+
- @checkstack/signal-frontend@0.3.7
|
|
125
|
+
|
|
3
126
|
## 0.6.13
|
|
4
127
|
|
|
5
128
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/anomaly-frontend",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
"lint:code": "eslint . --max-warnings 0"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@checkstack/anomaly-common": "1.8.
|
|
20
|
-
"@checkstack/catalog-common": "2.
|
|
21
|
-
"@checkstack/common": "0.
|
|
22
|
-
"@checkstack/frontend-api": "0.16.
|
|
23
|
-
"@checkstack/healthcheck-common": "1.
|
|
24
|
-
"@checkstack/healthcheck-frontend": "0.
|
|
25
|
-
"@checkstack/notification-common": "1.7.
|
|
26
|
-
"@checkstack/notification-frontend": "0.9.
|
|
27
|
-
"@checkstack/signal-frontend": "0.3.
|
|
28
|
-
"@checkstack/ui": "1.
|
|
19
|
+
"@checkstack/anomaly-common": "1.8.2",
|
|
20
|
+
"@checkstack/catalog-common": "2.8.0",
|
|
21
|
+
"@checkstack/common": "0.23.0",
|
|
22
|
+
"@checkstack/frontend-api": "0.16.1",
|
|
23
|
+
"@checkstack/healthcheck-common": "1.18.0",
|
|
24
|
+
"@checkstack/healthcheck-frontend": "0.37.0",
|
|
25
|
+
"@checkstack/notification-common": "1.7.2",
|
|
26
|
+
"@checkstack/notification-frontend": "0.9.6",
|
|
27
|
+
"@checkstack/signal-frontend": "0.3.7",
|
|
28
|
+
"@checkstack/ui": "1.29.0",
|
|
29
29
|
"date-fns": "^4.4.0",
|
|
30
30
|
"lucide-react": "^1.17.0",
|
|
31
31
|
"react": "19.2.7",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"zod": "^4.2.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@checkstack/scripts": "0.7.
|
|
36
|
+
"@checkstack/scripts": "0.7.6",
|
|
37
37
|
"@checkstack/tsconfig": "0.0.7",
|
|
38
38
|
"@types/react": "^19.0.0",
|
|
39
39
|
"typescript": "^5.0.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { AnomalyApi } from "@checkstack/anomaly-common";
|
|
4
|
+
|
|
5
|
+
interface AnomalyBadgeDataContextValue {
|
|
6
|
+
/**
|
|
7
|
+
* The worst anomaly state for a system from the two bulk fetches, or
|
|
8
|
+
* `undefined` when the system has no active/suspicious anomaly.
|
|
9
|
+
*/
|
|
10
|
+
getSystemState: (systemId: string) => "anomaly" | "suspicious" | undefined;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const AnomalyBadgeDataContext = createContext<
|
|
15
|
+
AnomalyBadgeDataContextValue | undefined
|
|
16
|
+
>(undefined);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provider that fetches the active + suspicious anomaly sets ONCE and derives an
|
|
20
|
+
* O(1) per-system lookup, so per-row `SystemAnomalyBadge`s read from context
|
|
21
|
+
* instead of each instantiating its own live query observers and scanning the
|
|
22
|
+
* (up to 500-element) arrays. Without this provider each badge falls back to its
|
|
23
|
+
* own (deduped) queries, so surfaces that don't install it still work.
|
|
24
|
+
*
|
|
25
|
+
* The `getAnomalies` queries are unscoped (they return every active/suspicious
|
|
26
|
+
* anomaly, capped at 500), so this provider ignores `systemIds` for fetching and
|
|
27
|
+
* only uses the results to build the lookup once.
|
|
28
|
+
*/
|
|
29
|
+
export const AnomalyBadgeDataProvider: React.FC<{
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}> = ({ children }) => {
|
|
32
|
+
const anomalyClient = usePluginClient(AnomalyApi);
|
|
33
|
+
|
|
34
|
+
const { data: confirmed = [], isLoading: confirmedLoading } =
|
|
35
|
+
anomalyClient.getAnomalies.useQuery(
|
|
36
|
+
{ state: "anomaly", limit: 500 },
|
|
37
|
+
{ staleTime: 30_000 },
|
|
38
|
+
);
|
|
39
|
+
const { data: suspicious = [], isLoading: suspiciousLoading } =
|
|
40
|
+
anomalyClient.getAnomalies.useQuery(
|
|
41
|
+
{ state: "suspicious", limit: 500 },
|
|
42
|
+
{ staleTime: 30_000 },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const contextValue = useMemo<AnomalyBadgeDataContextValue>(() => {
|
|
46
|
+
const confirmedIds = new Set(confirmed.map((a) => a.systemId));
|
|
47
|
+
const suspiciousIds = new Set(suspicious.map((a) => a.systemId));
|
|
48
|
+
return {
|
|
49
|
+
getSystemState: (systemId) => {
|
|
50
|
+
if (confirmedIds.has(systemId)) return "anomaly";
|
|
51
|
+
if (suspiciousIds.has(systemId)) return "suspicious";
|
|
52
|
+
return;
|
|
53
|
+
},
|
|
54
|
+
loading: confirmedLoading || suspiciousLoading,
|
|
55
|
+
};
|
|
56
|
+
}, [confirmed, suspicious, confirmedLoading, suspiciousLoading]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<AnomalyBadgeDataContext.Provider value={contextValue}>
|
|
60
|
+
{children}
|
|
61
|
+
</AnomalyBadgeDataContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function useAnomalyBadgeDataOptional():
|
|
66
|
+
| AnomalyBadgeDataContextValue
|
|
67
|
+
| undefined {
|
|
68
|
+
return useContext(AnomalyBadgeDataContext);
|
|
69
|
+
}
|
|
@@ -28,17 +28,20 @@ type Props = SlotContext<typeof SystemSignalsSlot>;
|
|
|
28
28
|
export const AnomalySignalsFiller: React.FC<Props> = ({
|
|
29
29
|
systemIds,
|
|
30
30
|
onSignals,
|
|
31
|
+
onLoadingChange,
|
|
31
32
|
}) => {
|
|
32
33
|
const anomalyClient = usePluginClient(AnomalyApi);
|
|
33
34
|
|
|
34
|
-
const { data: confirmed = [] } =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const { data: confirmed = [], isLoading: confirmedLoading } =
|
|
36
|
+
anomalyClient.getAnomalies.useQuery(
|
|
37
|
+
{ state: "anomaly", limit: 500 },
|
|
38
|
+
{ staleTime: 30_000 },
|
|
39
|
+
);
|
|
40
|
+
const { data: suspicious = [], isLoading: suspiciousLoading } =
|
|
41
|
+
anomalyClient.getAnomalies.useQuery(
|
|
42
|
+
{ state: "suspicious", limit: 500 },
|
|
43
|
+
{ staleTime: 30_000 },
|
|
44
|
+
);
|
|
42
45
|
|
|
43
46
|
const signals = useMemo<SystemSignalsMap>(() => {
|
|
44
47
|
const inOverview = new Set(systemIds);
|
|
@@ -71,5 +74,13 @@ export const AnomalySignalsFiller: React.FC<Props> = ({
|
|
|
71
74
|
onSignals(ANOMALY_SIGNAL_SOURCE_ID, signals);
|
|
72
75
|
}, [signals, onSignals]);
|
|
73
76
|
|
|
77
|
+
// Report load state so the dashboard holds its overview skeleton until this
|
|
78
|
+
// (and every other source) has settled, instead of flashing "all healthy".
|
|
79
|
+
const isLoading = confirmedLoading || suspiciousLoading;
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (systemIds.length === 0) return;
|
|
82
|
+
onLoadingChange(ANOMALY_SIGNAL_SOURCE_ID, isLoading);
|
|
83
|
+
}, [isLoading, systemIds.length, onLoadingChange]);
|
|
84
|
+
|
|
74
85
|
return null;
|
|
75
86
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Card,
|
|
4
4
|
CardHeader,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CardFooter,
|
|
9
9
|
Button,
|
|
10
10
|
useToast,
|
|
11
|
+
useInitOnceForKey,
|
|
11
12
|
} from "@checkstack/ui";
|
|
12
13
|
import { Activity, Save } from "lucide-react";
|
|
13
14
|
import type { HealthCheckConfigIDEContext } from "@checkstack/healthcheck-frontend";
|
|
@@ -37,7 +38,7 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
|
|
|
37
38
|
|
|
38
39
|
const { data: configRecord, isLoading } = anomalyClient.getAnomalyConfig.useQuery(
|
|
39
40
|
{ configurationId: context.configurationId },
|
|
40
|
-
{ enabled: !!context.configurationId },
|
|
41
|
+
{ enabled: !!context.configurationId, gcTime: 0 },
|
|
41
42
|
);
|
|
42
43
|
|
|
43
44
|
const availableFields = useAnomalyFields(context.configurationId);
|
|
@@ -47,18 +48,16 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
|
|
|
47
48
|
onError: () => toast.error("Failed to save settings"),
|
|
48
49
|
});
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}, [configRecord]);
|
|
51
|
+
// Seed the form once per configuration id; ignores background refetches so
|
|
52
|
+
// in-progress edits aren't wiped by a realtime-driven query update.
|
|
53
|
+
useInitOnceForKey(configRecord?.data, context.configurationId, (data) => {
|
|
54
|
+
setValues({
|
|
55
|
+
enabled: data.enabled ?? true,
|
|
56
|
+
baselineWindow: data.baselineWindow ?? "7d",
|
|
57
|
+
notify: data.notify ?? true,
|
|
58
|
+
fieldOverrides: (data.fieldOverrides as Record<string, AnomalyFieldConfig>) ?? {},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
62
61
|
|
|
63
62
|
const handleChange = <K extends keyof AnomalySettingsFormValues>(
|
|
64
63
|
key: K,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AnomalyBadgeDataProvider } from "./AnomalyBadgeDataProvider";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fills catalog's `CatalogBrowseDataBoundarySlot`. Wraps the boundary's
|
|
6
|
+
* `children` (the catalog browse / manage tree) in `AnomalyBadgeDataProvider`,
|
|
7
|
+
* so the per-row `SystemAnomalyBadge`s read the anomaly lookup from context
|
|
8
|
+
* (`useAnomalyBadgeDataOptional`) instead of each instantiating its own live
|
|
9
|
+
* query observers - eliminating the per-row observer + array-scan cost.
|
|
10
|
+
*
|
|
11
|
+
* `children` is typed OPTIONAL so the component stays assignable to the slot
|
|
12
|
+
* context (which declares only `systemIds` / `groupIds`); catalog-frontend
|
|
13
|
+
* supplies the children when it folds this filler around the tree. The anomaly
|
|
14
|
+
* fetch is unscoped, so `systemIds` / `groupIds` are unused here.
|
|
15
|
+
*/
|
|
16
|
+
export const CatalogBrowseAnomalyDataFiller = ({
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
systemIds: string[];
|
|
20
|
+
groupIds: string[];
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
}) => <AnomalyBadgeDataProvider>{children}</AnomalyBadgeDataProvider>;
|
|
@@ -1,49 +1,53 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
4
|
-
import { AnomalyApi
|
|
4
|
+
import { AnomalyApi } from "@checkstack/anomaly-common";
|
|
5
5
|
import { StatusBadge } from "@checkstack/ui";
|
|
6
6
|
import { ChartSpline } from "lucide-react";
|
|
7
|
+
import { useAnomalyBadgeDataOptional } from "./AnomalyBadgeDataProvider";
|
|
7
8
|
|
|
8
9
|
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Renders an anomaly status badge for a system.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* On the catalog browse / manage tree an `AnomalyBadgeDataProvider` (installed
|
|
15
|
+
* via `CatalogBrowseDataBoundarySlot`) fetches the active + suspicious anomaly
|
|
16
|
+
* sets ONCE and exposes an O(1) per-system lookup; when present this badge reads
|
|
17
|
+
* from it and disables its own queries, so a table of rows instantiates NO
|
|
18
|
+
* per-row query observers. Without the provider (e.g. the system detail page)
|
|
19
|
+
* the fallback queries run, deduped across all badge instances by React Query,
|
|
20
|
+
* so even 50+ badges issue only 2 requests total. Recovered anomalies are
|
|
21
|
+
* excluded to keep the payload bounded.
|
|
18
22
|
*/
|
|
19
23
|
export const SystemAnomalyBadge: React.FC<Props> = ({ system }) => {
|
|
20
24
|
const anomalyClient = usePluginClient(AnomalyApi);
|
|
25
|
+
const badgeCtx = useAnomalyBadgeDataOptional();
|
|
21
26
|
|
|
22
|
-
//
|
|
27
|
+
// Fallback queries only when no bulk provider is present. Disabled (no live
|
|
28
|
+
// observer created) on surfaces that install the provider.
|
|
23
29
|
const { data: confirmedAnomalies = [] } = anomalyClient.getAnomalies.useQuery(
|
|
24
30
|
{ state: "anomaly", limit: 500 },
|
|
25
|
-
{ staleTime: 30_000 },
|
|
31
|
+
{ staleTime: 30_000, enabled: !badgeCtx },
|
|
26
32
|
);
|
|
27
|
-
|
|
28
|
-
// Fetch suspicious states — deduped across all badge instances via React Query
|
|
29
33
|
const { data: suspiciousAnomalies = [] } =
|
|
30
34
|
anomalyClient.getAnomalies.useQuery(
|
|
31
35
|
{ state: "suspicious", limit: 500 },
|
|
32
|
-
{ staleTime: 30_000 },
|
|
36
|
+
{ staleTime: 30_000, enabled: !badgeCtx },
|
|
33
37
|
);
|
|
34
38
|
|
|
35
|
-
//
|
|
39
|
+
// Worst state for this system: from the provider's O(1) lookup when present,
|
|
40
|
+
// else scan the (deduped) fallback results.
|
|
36
41
|
const systemState = useMemo(() => {
|
|
37
|
-
|
|
42
|
+
if (badgeCtx) return badgeCtx.getSystemState(system?.id ?? "");
|
|
38
43
|
if (confirmedAnomalies.some((a) => a.systemId === system?.id)) {
|
|
39
|
-
return "anomaly" as
|
|
44
|
+
return "anomaly" as const;
|
|
40
45
|
}
|
|
41
|
-
// Check suspicious states
|
|
42
46
|
if (suspiciousAnomalies.some((a) => a.systemId === system?.id)) {
|
|
43
|
-
return "suspicious" as
|
|
47
|
+
return "suspicious" as const;
|
|
44
48
|
}
|
|
45
49
|
return;
|
|
46
|
-
}, [confirmedAnomalies, suspiciousAnomalies, system?.id]);
|
|
50
|
+
}, [badgeCtx, confirmedAnomalies, suspiciousAnomalies, system?.id]);
|
|
47
51
|
|
|
48
52
|
if (!systemState) return <></>;
|
|
49
53
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
usePluginClient,
|
|
4
4
|
useApi,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
15
15
|
import { resolveRoute } from "@checkstack/common";
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
DetailCard,
|
|
18
18
|
CardHeader,
|
|
19
19
|
CardTitle,
|
|
20
20
|
CardContent,
|
|
@@ -311,7 +311,10 @@ function AnomalyRow({
|
|
|
311
311
|
// Main Widget
|
|
312
312
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
313
313
|
|
|
314
|
-
export const SystemAnomalyWidget: React.FC<Props> = ({
|
|
314
|
+
export const SystemAnomalyWidget: React.FC<Props> = ({
|
|
315
|
+
system,
|
|
316
|
+
onLoadingChange,
|
|
317
|
+
}) => {
|
|
315
318
|
const anomalyClient = usePluginClient(AnomalyApi);
|
|
316
319
|
const catalogClient = usePluginClient(CatalogApi);
|
|
317
320
|
const toast = useToast();
|
|
@@ -404,12 +407,18 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
|
|
|
404
407
|
|
|
405
408
|
const isLoading = loadingConfirmed || loadingSuspicious;
|
|
406
409
|
|
|
410
|
+
// Report load state so the detail page reveals all overview cards together
|
|
411
|
+
// instead of each popping in as its own fetch settles.
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
onLoadingChange?.("anomaly.widget", isLoading);
|
|
414
|
+
}, [isLoading, onLoadingChange]);
|
|
415
|
+
|
|
407
416
|
// Confirmed anomalies first, then suspicious
|
|
408
417
|
const activeAnomalies = [...confirmedAnomalies, ...suspiciousAnomalies];
|
|
409
418
|
|
|
410
419
|
if (isLoading) {
|
|
411
420
|
return (
|
|
412
|
-
<
|
|
421
|
+
<DetailCard className="relative overflow-hidden">
|
|
413
422
|
<CardHeader>
|
|
414
423
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
415
424
|
<Activity className="h-4 w-4" />
|
|
@@ -421,7 +430,7 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
|
|
|
421
430
|
Loading anomalies...
|
|
422
431
|
</div>
|
|
423
432
|
</CardContent>
|
|
424
|
-
</
|
|
433
|
+
</DetailCard>
|
|
425
434
|
);
|
|
426
435
|
}
|
|
427
436
|
|
|
@@ -440,7 +449,7 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
|
|
|
440
449
|
const suspiciousStyles = anomalyToneStyles("unknown");
|
|
441
450
|
|
|
442
451
|
return (
|
|
443
|
-
<
|
|
452
|
+
<DetailCard className="relative overflow-hidden transition-all">
|
|
444
453
|
{/* Status accent stripe: worst active state encoded by hue + position. */}
|
|
445
454
|
<span
|
|
446
455
|
className={cn("absolute inset-y-0 left-0 w-1", accent.accent)}
|
|
@@ -547,6 +556,6 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
|
|
|
547
556
|
))}
|
|
548
557
|
</div>
|
|
549
558
|
</CardContent>
|
|
550
|
-
</
|
|
559
|
+
</DetailCard>
|
|
551
560
|
);
|
|
552
561
|
};
|
package/src/plugin.tsx
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
SystemStateBadgesSlot,
|
|
14
14
|
SystemDetailsSlot,
|
|
15
15
|
SystemSignalsSlot,
|
|
16
|
+
CatalogBrowseDataBoundarySlot,
|
|
16
17
|
} from "@checkstack/catalog-common";
|
|
17
18
|
import { registerSubscriptionSubControls } from "@checkstack/notification-frontend";
|
|
18
19
|
import { anomalySystemSubscription } from "@checkstack/anomaly-common";
|
|
19
20
|
import { SystemAnomalyBadge } from "./components/SystemAnomalyBadge";
|
|
21
|
+
import { CatalogBrowseAnomalyDataFiller } from "./components/CatalogBrowseAnomalyDataFiller";
|
|
20
22
|
import { SystemAnomalyWidget } from "./components/SystemAnomalyWidget";
|
|
21
23
|
import { AnomalyFieldMuteList } from "./components/AnomalyFieldMuteList";
|
|
22
24
|
import { IDETreeNode } from "@checkstack/ui";
|
|
@@ -47,6 +49,13 @@ export const plugin: FrontendPlugin = {
|
|
|
47
49
|
id: "anomaly.system-badge",
|
|
48
50
|
component: SystemAnomalyBadge,
|
|
49
51
|
}),
|
|
52
|
+
// Bulk-fills the catalog browse/manage tree so the per-row anomaly badges
|
|
53
|
+
// read an O(1) lookup from context instead of each instantiating two live
|
|
54
|
+
// query observers + scanning the anomaly arrays (the SystemsTab mount cost).
|
|
55
|
+
createSlotExtension(CatalogBrowseDataBoundarySlot, {
|
|
56
|
+
id: "anomaly.catalog.browse-anomaly-data",
|
|
57
|
+
component: CatalogBrowseAnomalyDataFiller,
|
|
58
|
+
}),
|
|
50
59
|
createSlotExtension(SystemSignalsSlot, {
|
|
51
60
|
id: "anomaly.dashboard.signals",
|
|
52
61
|
load: () =>
|