@checkstack/healthcheck-frontend 0.19.5 → 0.21.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 CHANGED
@@ -1,5 +1,111 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 35bc682: feat(healthcheck): expose check + system run-context to script collectors
8
+
9
+ Script health checks can now read which check and system a run is for.
10
+ Previously shell scripts got only a curated env whitelist and inline
11
+ scripts only `context.config`, so a script had no built-in way to know
12
+ its own check name or the system it was checking.
13
+
14
+ - `@checkstack/backend-api`: new `CollectorRunContext` type
15
+ (`{ check: { id, name, intervalSeconds }, system: { id, name } }`) and
16
+ an optional `runContext` param on `CollectorStrategy.execute`. Optional,
17
+ so existing collector implementations are unaffected.
18
+ - Shell-script collector: injects reserved `CHECKSTACK_CHECK_ID`,
19
+ `CHECKSTACK_CHECK_NAME`, `CHECKSTACK_CHECK_INTERVAL_SECONDS`,
20
+ `CHECKSTACK_SYSTEM_ID`, `CHECKSTACK_SYSTEM_NAME` env vars (user-supplied
21
+ `env` still wins on collision).
22
+ - Inline-script collector: exposes `context.check` and `context.system`
23
+ alongside `context.config`; the inline-script editor now types them for
24
+ autocomplete.
25
+ - Shell editors (health-check collectors and automation shell actions) now
26
+ also suggest the user's own `env` (JSON) keys as `$NAME` completions, via
27
+ the new exported `customShellEnvVars` helper. Keys that aren't valid shell
28
+ identifiers are omitted.
29
+ - Fix: the Typefox `CodeEditor` captured a stale `onChange` at editor start,
30
+ so editing one `DynamicForm` field reverted sibling fields changed since
31
+ mount (e.g. typing in a shell `script` field wiped an unsaved `env` value,
32
+ or deleted a sibling automation action added after mount). The change
33
+ handler now routes through a ref to the current `onChange`.
34
+ - Fix: focusing a JSON editor threw "LanguageStatusService.addStatus is not
35
+ supported" because the standalone service set omitted `ILanguageStatusService`.
36
+ That one service is now registered via `serviceOverrides`.
37
+ - Fix: the automation trigger card nested a `<Badge>` (a `<div>`) inside a
38
+ `<p>`, producing a `validateDOMNesting` warning. Switched the wrapper to a
39
+ `<div>`.
40
+ - Local runs (`queue-executor`) and satellite runs both populate the
41
+ context. `SatelliteAssignment` (and the `getAssignmentsForSatellite`
42
+ RPC output) gained optional `configName` / `systemName` so the metadata
43
+ reaches satellite-side execution; `HealthCheckService` resolves the
44
+ system name via the catalog client.
45
+
46
+ BREAKING CHANGE: `createHealthCheckRouter` now requires a `catalogClient`
47
+ option (used to resolve system names for satellite assignments). Update
48
+ call sites to pass the catalog RPC client.
49
+
50
+ ### Patch Changes
51
+
52
+ - Updated dependencies [e2d6f25]
53
+ - Updated dependencies [41c77f4]
54
+ - Updated dependencies [41c77f4]
55
+ - Updated dependencies [41c77f4]
56
+ - Updated dependencies [41c77f4]
57
+ - Updated dependencies [4832e33]
58
+ - Updated dependencies [6d52276]
59
+ - Updated dependencies [35bc682]
60
+ - Updated dependencies [c39ee69]
61
+ - @checkstack/frontend-api@0.6.0
62
+ - @checkstack/ui@1.11.0
63
+ - @checkstack/common@0.12.0
64
+ - @checkstack/healthcheck-common@1.3.0
65
+ - @checkstack/satellite-common@0.6.0
66
+ - @checkstack/auth-frontend@0.6.6
67
+ - @checkstack/catalog-common@2.2.3
68
+ - @checkstack/dashboard-frontend@0.7.7
69
+ - @checkstack/gitops-frontend@0.4.6
70
+ - @checkstack/tips-frontend@0.2.6
71
+ - @checkstack/anomaly-common@1.2.3
72
+ - @checkstack/signal-frontend@0.1.5
73
+
74
+ ## 0.20.0
75
+
76
+ ### Minor Changes
77
+
78
+ - ba07ae2: Quiet down notification spam on flapping systems, auto-open incidents when a check goes critical, and let operators land directly on the broken checks.
79
+
80
+ Notification policy lives **per healthcheck assignment** (one row per `system × configuration`). Different checks on the same system are fully independent — disabling a setting on one check does not affect the others. Defaults preserve existing behaviour for `suppressDeEscalations`; **auto-incident defaults to on** for new and existing assignments.
81
+
82
+ - **`suppressDeEscalations`** (off by default). When on, transitions from a worse state to a better-but-still-failing state (e.g. `unhealthy → degraded`) no longer fire a notification. Escalations and full recoveries to `healthy` are unaffected. Resolved per assignment (the just-ran check is the one driving any aggregate transition).
83
+ - **`autoOpenIncidentOnUnhealthy`** (on by default). Either of two independent triggers can open the auto-incident:
84
+ - **`sustainedUnhealthyTrigger`** (default 30 min) — opens when the check stays continuously unhealthy for the configured duration. Catches real outages.
85
+ - **`flappingTrigger`** (default 3 transitions in 60 min) — opens when the check flips to unhealthy that many times in the window. Catches persistent flapping where each unhealthy phase is too brief for the sustained trigger.
86
+ Each trigger can be individually disabled. One incident per system: triggering checks attach to an existing active auto-incident.
87
+ - **`useNotificationSuppression`** (on by default, only meaningful when auto-open is on). Controls whether the auto-opened incident is created with `suppressNotifications: true` — leaving this off opens the incident but still pings operators on each transition.
88
+ - **`skipDuringMaintenance`** (on by default). No auto-incident is opened while the system has an active maintenance window with suppression. The system is intentionally down and shouldn't trip the on-call.
89
+ - **`autoCloseAfterMinutes`** (default 30). Auto-close cooldown is now per-assignment and snapshotted per-incident at open time — later policy edits don't alter in-flight incidents. Setting `null` ("Never auto-close") leaves the incident for manual resolution.
90
+ - **Require-recovery rule.** After any auto-incident closes (manual or auto), no new auto-incident can open until the check has logged at least one healthy run. Prevents a "operator dismissed but it's still broken" loop.
91
+ - **Auto-close worker** ticks every 60s and resolves auto-opened incidents whose systems have been healthy for their per-row `cooldownMinutes`. Rows with `null` cooldown are skipped entirely. Per-incident: failed close attempts are logged but never abort the sweep.
92
+ - **`incidentResolved` hook subscriber** syncs the auto-incident mapping when an operator manually resolves the incident, so the require-recovery rule sees the close immediately.
93
+ - **Platform-wide defaults.** New admin RPCs `getPlatformNotificationDefaults` / `setPlatformNotificationDefaults` (under the existing `healthcheck.configuration.{read,manage}` access rules) let operators set notification policy once for the whole instance. Per-assignment rows with `notificationPolicy: null` inherit the platform defaults at read time. UI: a "Notification defaults" button in the Assignment IDE opens a modal editor. The per-assignment Notifications panel shows an inheritance banner — "Using platform defaults" (read-only) with an "Override" button, or "Custom override" with a "Use platform defaults" button to revert. The all-or-nothing model keeps the mental model simple: each assignment is either fully inherited or fully overridden.
94
+ - **New service-level RPCs** on the incident plugin (`createAutoIncident`, `resolveAutoIncident`) let other plugins open/close incidents without a user context. Reused by the healthcheck auto-incident flow.
95
+ - **Health-state notification CTA** now deep-links to `?filter=failing` on the system detail page for non-recovery transitions (label changes to "View failing checks"). The system overview gains an `All / Failing / Healthy` segmented filter wired to the same `?filter=…` param.
96
+ - **Notification bell badge** now counts collapse groups instead of raw rows, so the number matches what the user sees in the notifications list. Built on `COUNT(DISTINCT COALESCE(collapse_key, id))` — notifications without a collapse key still each count as one.
97
+ - **`statusFilter` on `getHistory` / `getDetailedHistory`** lets the run-history page and the drawer's Recent Runs panel filter to `All / Healthy / Failing` via shared pills, with the page resetting to the first page on filter change.
98
+ - **Pagination defaults aligned with selector options.** Several pages defaulted to a page size (5 or 20) that wasn't in the dropdown's options (`[10, 25, 50, 100]`), so the page-size `<Select>` rendered empty. The drawer's Recent Runs now defaults to 10; the Run History, History List, and Delivery Logs pages now default to 25.
99
+
100
+ Includes Drizzle migrations adding the `notification_policy` jsonb column to `system_health_checks`, plus two new tables: `health_check_unhealthy_transitions` (for threshold counting) and `health_check_auto_incidents` (for mapping back to incident ids during auto-close).
101
+
102
+ ### Patch Changes
103
+
104
+ - Updated dependencies [ba07ae2]
105
+ - @checkstack/healthcheck-common@1.2.0
106
+ - @checkstack/dashboard-frontend@0.7.6
107
+ - @checkstack/satellite-common@0.5.3
108
+
3
109
  ## 0.19.5
4
110
 
5
111
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.19.5",
3
+ "version": "0.21.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,18 +13,18 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/anomaly-common": "1.2.1",
17
- "@checkstack/auth-frontend": "0.6.4",
18
- "@checkstack/catalog-common": "2.2.1",
19
- "@checkstack/common": "0.10.0",
20
- "@checkstack/dashboard-frontend": "0.7.4",
21
- "@checkstack/frontend-api": "0.5.1",
22
- "@checkstack/gitops-frontend": "0.4.4",
23
- "@checkstack/healthcheck-common": "1.1.1",
24
- "@checkstack/satellite-common": "0.5.1",
25
- "@checkstack/signal-frontend": "0.1.3",
26
- "@checkstack/tips-frontend": "0.2.4",
27
- "@checkstack/ui": "1.9.0",
16
+ "@checkstack/anomaly-common": "1.2.2",
17
+ "@checkstack/auth-frontend": "0.6.5",
18
+ "@checkstack/catalog-common": "2.2.2",
19
+ "@checkstack/common": "0.11.0",
20
+ "@checkstack/dashboard-frontend": "0.7.6",
21
+ "@checkstack/frontend-api": "0.5.2",
22
+ "@checkstack/gitops-frontend": "0.4.5",
23
+ "@checkstack/healthcheck-common": "1.2.0",
24
+ "@checkstack/satellite-common": "0.5.3",
25
+ "@checkstack/signal-frontend": "0.1.4",
26
+ "@checkstack/tips-frontend": "0.2.5",
27
+ "@checkstack/ui": "1.10.0",
28
28
  "ajv": "^8.18.0",
29
29
  "ajv-formats": "^3.0.1",
30
30
  "date-fns": "^4.1.0",
@@ -36,7 +36,7 @@
36
36
  "zod": "^4.2.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@checkstack/scripts": "0.3.2",
39
+ "@checkstack/scripts": "0.3.3",
40
40
  "@checkstack/tsconfig": "0.0.7",
41
41
  "@types/react": "^18.2.0",
42
42
  "typescript": "^5.0.0"
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { TableRow, TableCell } from "@checkstack/ui";
3
+
4
+ interface EmptyRunsTableRowProps {
5
+ /** Number of columns in the parent table (drives `colSpan`). */
6
+ colSpan: number;
7
+ /** Message to display in the empty cell. Plain strings or rich nodes. */
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ /**
12
+ * Single-row empty state rendered inside a runs table body. Centralises
13
+ * the look so the drawer's Recent Runs and the Run History page can
14
+ * share the same in-table empty state.
15
+ */
16
+ export function EmptyRunsTableRow({ colSpan, children }: EmptyRunsTableRowProps) {
17
+ return (
18
+ <TableRow>
19
+ <TableCell
20
+ colSpan={colSpan}
21
+ className="text-center text-xs text-muted-foreground py-6"
22
+ >
23
+ {children}
24
+ </TableCell>
25
+ </TableRow>
26
+ );
27
+ }
@@ -56,6 +56,12 @@ import { HealthCheckLatencyChart } from "./HealthCheckLatencyChart";
56
56
  import { useHealthCheckData } from "../hooks/useHealthCheckData";
57
57
  import { AggregatedDataBanner } from "./AggregatedDataBanner";
58
58
  import { HealthCheckDiagramSlot } from "../slots";
59
+ import {
60
+ StatusFilterPills,
61
+ STATUS_FILTER_TO_STATUSES,
62
+ type StatusFilter,
63
+ } from "./StatusFilterPills";
64
+ import { EmptyRunsTableRow } from "./EmptyRunsTableRow";
59
65
  import { Heart, Clock, CheckCircle, AlertTriangle } from "lucide-react";
60
66
 
61
67
  import type {
@@ -130,6 +136,8 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
130
136
  );
131
137
  const [isRollingPreset, setIsRollingPreset] = useState(true);
132
138
  const [sourceFilter, setSourceFilter] = useState<string | undefined>();
139
+ const [runsStatusFilter, setRunsStatusFilter] =
140
+ useState<StatusFilter>("all");
133
141
 
134
142
  const activePreset = detectPreset(dateRange);
135
143
  const activePresetLabel = PRESETS.find((p) => p.id === activePreset)?.shortLabel ?? "Custom";
@@ -195,7 +203,7 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
195
203
  );
196
204
 
197
205
  // Pagination for history table
198
- const pagination = usePagination({ defaultLimit: 5 });
206
+ const pagination = usePagination({ defaultLimit: 10 });
199
207
 
200
208
  const { data: historyData, isLoading: historyLoading } =
201
209
  healthCheckClient.getHistory.useQuery({
@@ -205,6 +213,7 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
205
213
  offset: pagination.offset,
206
214
  startDate: dateRange.startDate,
207
215
  sourceFilter,
216
+ statusFilter: STATUS_FILTER_TO_STATUSES[runsStatusFilter],
208
217
  sortOrder: "desc",
209
218
  });
210
219
 
@@ -437,7 +446,7 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
437
446
  </div>
438
447
 
439
448
  {/* Zone 3 — Recent Runs */}
440
- {runs.length > 0 && (
449
+ {(runs.length > 0 || runsStatusFilter !== "all") && (
441
450
  <div className="space-y-3">
442
451
  <div className="flex items-center gap-4">
443
452
  <div className="flex-1 h-px bg-border" />
@@ -450,6 +459,16 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
450
459
  <div className="flex-1 h-px bg-border" />
451
460
  </div>
452
461
 
462
+ <div className="flex justify-end">
463
+ <StatusFilterPills
464
+ value={runsStatusFilter}
465
+ onChange={(next) => {
466
+ setRunsStatusFilter(next);
467
+ pagination.setPage(1);
468
+ }}
469
+ />
470
+ </div>
471
+
453
472
  <div className="rounded-md border">
454
473
  <Table>
455
474
  <TableHeader>
@@ -460,6 +479,13 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
460
479
  </TableRow>
461
480
  </TableHeader>
462
481
  <TableBody>
482
+ {runs.length === 0 && !historyLoading && (
483
+ <EmptyRunsTableRow colSpan={3}>
484
+ No runs match the{" "}
485
+ <span className="font-medium">{runsStatusFilter}</span>{" "}
486
+ filter.
487
+ </EmptyRunsTableRow>
488
+ )}
463
489
  {runs.map((run) => (
464
490
  <TableRow
465
491
  key={run.id}
@@ -14,6 +14,7 @@ import { ExternalLink, Loader2, Satellite, Server } from "lucide-react";
14
14
  import { useNavigate } from "react-router-dom";
15
15
  import { healthcheckRoutes } from "@checkstack/healthcheck-common";
16
16
  import { resolveRoute } from "@checkstack/common";
17
+ import { EmptyRunsTableRow } from "./EmptyRunsTableRow";
17
18
 
18
19
  export interface HealthCheckRunDetailed {
19
20
  id: string;
@@ -64,10 +65,6 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
64
65
  prevRunsRef.current = runs;
65
66
  }
66
67
 
67
- if (!loading && runs.length === 0 && prevRunsRef.current.length === 0) {
68
- return <p className="text-muted-foreground text-sm">{emptyMessage}</p>;
69
- }
70
-
71
68
  const handleRowClick = (run: HealthCheckRunDetailed) => {
72
69
  navigate(
73
70
  resolveRoute(healthcheckRoutes.routes.historyRun, {
@@ -78,6 +75,11 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
78
75
  );
79
76
  };
80
77
 
78
+ // 3 base columns (Status, Timestamp, Source) + 3 extras when
79
+ // showFilterColumns is on (System ID, Configuration ID, link icon).
80
+ const columnCount = showFilterColumns ? 6 : 3;
81
+ const showEmptyRow = !loading && displayRuns.length === 0;
82
+
81
83
  return (
82
84
  <>
83
85
  <div className="rounded-md border">
@@ -102,6 +104,11 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
102
104
  </TableRow>
103
105
  </TableHeader>
104
106
  <TableBody>
107
+ {showEmptyRow && (
108
+ <EmptyRunsTableRow colSpan={columnCount}>
109
+ {emptyMessage}
110
+ </EmptyRunsTableRow>
111
+ )}
105
112
  {displayRuns.map((run) => (
106
113
  <TableRow
107
114
  key={run.id}
@@ -1,4 +1,5 @@
1
1
  import React, { useState } from "react";
2
+ import { useSearchParams } from "react-router-dom";
2
3
  import {
3
4
  usePluginClient,
4
5
  type SlotContext,
@@ -12,6 +13,7 @@ import {
12
13
  CardContent,
13
14
  CardHeader,
14
15
  CardTitle,
16
+ Button,
15
17
  } from "@checkstack/ui";
16
18
  import { Heart } from "lucide-react";
17
19
  import { HealthCheckSparkline } from "./HealthCheckSparkline";
@@ -22,6 +24,24 @@ import type {
22
24
  HealthCheckStatus,
23
25
  } from "@checkstack/healthcheck-common";
24
26
 
27
+ type HealthFilter = "all" | "failing" | "healthy";
28
+
29
+ const FILTERS: { value: HealthFilter; label: string }[] = [
30
+ { value: "all", label: "All" },
31
+ { value: "failing", label: "Failing" },
32
+ { value: "healthy", label: "Healthy" },
33
+ ];
34
+
35
+ function parseFilter(raw: string | null): HealthFilter {
36
+ return raw === "failing" || raw === "healthy" ? raw : "all";
37
+ }
38
+
39
+ function matchesFilter(state: HealthCheckStatus, filter: HealthFilter) {
40
+ if (filter === "all") return true;
41
+ if (filter === "healthy") return state === "healthy";
42
+ return state !== "healthy";
43
+ }
44
+
25
45
  type SlotProps = SlotContext<typeof SystemDetailsSlot>;
26
46
 
27
47
  /**
@@ -54,6 +74,8 @@ interface HealthCheckOverviewItem {
54
74
  export function HealthCheckSystemOverview(props: SlotProps) {
55
75
  const systemId = props.system.id;
56
76
  const healthCheckClient = usePluginClient(HealthCheckApi);
77
+ const [searchParams, setSearchParams] = useSearchParams();
78
+ const filter = parseFilter(searchParams.get("filter"));
57
79
 
58
80
  const [selectedCheck, setSelectedCheck] = useState<
59
81
  HealthCheckOverviewItem | undefined
@@ -82,6 +104,21 @@ export function HealthCheckSystemOverview(props: SlotProps) {
82
104
  }));
83
105
  }, [overviewData]);
84
106
 
107
+ const visible = React.useMemo(
108
+ () => overview.filter((item) => matchesFilter(item.state, filter)),
109
+ [overview, filter],
110
+ );
111
+
112
+ const setFilter = (next: HealthFilter) => {
113
+ const params = new URLSearchParams(searchParams);
114
+ if (next === "all") {
115
+ params.delete("filter");
116
+ } else {
117
+ params.set("filter", next);
118
+ }
119
+ setSearchParams(params, { replace: true });
120
+ };
121
+
85
122
  if (initialLoading) {
86
123
  return <LoadingSpinner />;
87
124
  }
@@ -94,16 +131,46 @@ export function HealthCheckSystemOverview(props: SlotProps) {
94
131
  <>
95
132
  <Card>
96
133
  <CardHeader className="pb-3">
97
- <div className="flex items-center gap-2">
98
- <Heart className="h-4 w-4 text-muted-foreground" />
99
- <CardTitle className="text-base font-semibold">
100
- Health Checks
101
- </CardTitle>
134
+ <div className="flex items-center justify-between gap-2">
135
+ <div className="flex items-center gap-2">
136
+ <Heart className="h-4 w-4 text-muted-foreground" />
137
+ <CardTitle className="text-base font-semibold">
138
+ Health Checks
139
+ </CardTitle>
140
+ </div>
141
+ <div
142
+ className="flex items-center gap-1 rounded-md border bg-muted/30 p-0.5"
143
+ role="tablist"
144
+ aria-label="Filter health checks"
145
+ >
146
+ {FILTERS.map((f) => {
147
+ const active = filter === f.value;
148
+ return (
149
+ <Button
150
+ key={f.value}
151
+ size="sm"
152
+ variant={active ? "secondary" : "ghost"}
153
+ className="h-7 px-2 text-xs"
154
+ onClick={() => setFilter(f.value)}
155
+ role="tab"
156
+ aria-selected={active}
157
+ >
158
+ {f.label}
159
+ </Button>
160
+ );
161
+ })}
162
+ </div>
102
163
  </div>
103
164
  </CardHeader>
104
165
  <CardContent className="p-0">
105
- <div className="divide-y divide-border">
106
- {overview.map((item) => (
166
+ {visible.length === 0 ? (
167
+ <div className="px-4 py-6 text-center text-xs text-muted-foreground">
168
+ No health checks match the{" "}
169
+ <span className="font-medium">{filter}</span> filter.
170
+ </div>
171
+ ) : (
172
+ <div className="divide-y divide-border">
173
+ {visible.map((item) => (
107
174
  <button
108
175
  key={item.configurationId}
109
176
  className="w-full px-4 py-3 text-left hover:bg-muted/50 transition-colors flex items-center gap-3"
@@ -133,8 +200,9 @@ export function HealthCheckSystemOverview(props: SlotProps) {
133
200
  {formatCompactTime(item.lastRunAt)}
134
201
  </span>
135
202
  </button>
136
- ))}
137
- </div>
203
+ ))}
204
+ </div>
205
+ )}
138
206
  </CardContent>
139
207
  </Card>
140
208
 
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ import { CheckCircle, AlertTriangle } from "lucide-react";
3
+ import type { HealthCheckStatus } from "@checkstack/healthcheck-common";
4
+
5
+ /**
6
+ * Coarse status filter buckets for run-history surfaces. "Failing"
7
+ * collapses degraded and unhealthy because operators investigating a
8
+ * problem usually care about "anything not green" rather than the
9
+ * degraded/unhealthy distinction.
10
+ */
11
+ export type StatusFilter = "all" | "healthy" | "failing";
12
+
13
+ export const STATUS_FILTER_TO_STATUSES: Record<
14
+ StatusFilter,
15
+ HealthCheckStatus[] | undefined
16
+ > = {
17
+ all: undefined,
18
+ healthy: ["healthy"],
19
+ failing: ["degraded", "unhealthy"],
20
+ };
21
+
22
+ interface StatusFilterPillsProps {
23
+ value: StatusFilter;
24
+ onChange: (next: StatusFilter) => void;
25
+ }
26
+
27
+ const OPTIONS: {
28
+ value: StatusFilter;
29
+ label: string;
30
+ icon?: React.ComponentType<{ className?: string }>;
31
+ activeClass: string;
32
+ }[] = [
33
+ {
34
+ value: "all",
35
+ label: "All",
36
+ activeClass: "bg-primary text-primary-foreground",
37
+ },
38
+ {
39
+ value: "healthy",
40
+ label: "Healthy",
41
+ icon: CheckCircle,
42
+ activeClass: "bg-success text-white",
43
+ },
44
+ {
45
+ value: "failing",
46
+ label: "Failing",
47
+ icon: AlertTriangle,
48
+ activeClass: "bg-destructive text-destructive-foreground",
49
+ },
50
+ ];
51
+
52
+ export function StatusFilterPills({ value, onChange }: StatusFilterPillsProps) {
53
+ return (
54
+ <div className="flex items-center gap-2">
55
+ <span className="text-sm text-muted-foreground">Status:</span>
56
+ <div className="flex items-center gap-1">
57
+ {OPTIONS.map((opt) => {
58
+ const active = value === opt.value;
59
+ const Icon = opt.icon;
60
+ return (
61
+ <button
62
+ key={opt.value}
63
+ type="button"
64
+ onClick={() => onChange(opt.value)}
65
+ className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
66
+ active
67
+ ? opt.activeClass
68
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
69
+ }`}
70
+ >
71
+ {Icon && <Icon className="h-3 w-3" />}
72
+ {opt.label}
73
+ </button>
74
+ );
75
+ })}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -1,5 +1,13 @@
1
1
  import React from "react";
2
- import { Settings, Gauge, Database, Radio, Plus, Check } from "lucide-react";
2
+ import {
3
+ Settings,
4
+ Gauge,
5
+ Database,
6
+ Radio,
7
+ Plus,
8
+ Check,
9
+ Bell,
10
+ } from "lucide-react";
3
11
  import { IDETreeNode, IDETreeSection } from "@checkstack/ui";
4
12
  import { ExtensionSlot } from "@checkstack/frontend-api";
5
13
  import { AssignmentIDENodeSlot } from "../../slots";
@@ -97,6 +105,18 @@ export const AssignmentTree: React.FC<AssignmentTreeProps> = ({
97
105
  indent
98
106
  badge={assoc.satelliteCount > 0 ? `${assoc.satelliteCount}` : undefined}
99
107
  />
108
+ <IDETreeNode
109
+ nodeId={`notifications:${assoc.configurationId}`}
110
+ label="Notifications"
111
+ icon={Bell}
112
+ selected={
113
+ selectedNode === `notifications:${assoc.configurationId}`
114
+ }
115
+ onClick={() =>
116
+ onSelectNode(`notifications:${assoc.configurationId}`)
117
+ }
118
+ indent
119
+ />
100
120
  <ExtensionSlot
101
121
  slot={AssignmentIDENodeSlot}
102
122
  context={{