@checkstack/healthcheck-frontend 0.19.3 → 0.19.5
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,166 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.19.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f23f3c9: Retrofit the highest-traffic configuration list tables
|
|
8
|
+
(`HealthCheckList`, `SloConfigPage`, and the integration
|
|
9
|
+
`DeliveryLogsPage`) onto the `ResponsiveTable` + `MobileCardList`
|
|
10
|
+
primitives from `@checkstack/ui`. On `sm` and up each page still
|
|
11
|
+
renders the unchanged 5- to 7-column table; below that breakpoint a
|
|
12
|
+
sibling stacked-card layout surfaces the same data with the resource
|
|
13
|
+
name + status badge at the top, secondary columns in a muted line, and
|
|
14
|
+
the existing action buttons in a right-aligned footer. The
|
|
15
|
+
`HealthCheckListSkeleton` placeholder mirrors both branches so the page
|
|
16
|
+
no longer jumps when data resolves. No business logic, column order,
|
|
17
|
+
or query inputs changed.
|
|
18
|
+
- f23f3c9: Establish the canonical optimistic-UI pattern for oRPC mutations
|
|
19
|
+
(`onMutate` snapshot / patch, `onError` rollback, `onSettled`
|
|
20
|
+
invalidate) and apply it to the two highest-frequency toggles where
|
|
21
|
+
perceived latency was most visible:
|
|
22
|
+
|
|
23
|
+
- `markAsRead` on the Notifications page — clicking the check on a
|
|
24
|
+
notification card now flips the read state immediately instead of
|
|
25
|
+
waiting for the round-trip.
|
|
26
|
+
- `pauseConfiguration` / `resumeConfiguration` on the Health Check
|
|
27
|
+
Config page — pause/resume now flip the row's badge instantly,
|
|
28
|
+
rolling back on server error.
|
|
29
|
+
|
|
30
|
+
The wrapper type for `useMutation` on each plugin client gained an
|
|
31
|
+
optional `TContext` generic so optimistic sites can return a snapshot
|
|
32
|
+
from `onMutate` and consume it in `onError` without `unknown` casts.
|
|
33
|
+
The runtime behaviour and the auto-invalidation on success are
|
|
34
|
+
unchanged; the change is additive on the type surface only.
|
|
35
|
+
|
|
36
|
+
Full pattern and "when NOT to use it" guidance live in
|
|
37
|
+
`docs/frontend/optimistic-updates.md`.
|
|
38
|
+
|
|
39
|
+
- f23f3c9: Gate decorative motion and blur effects behind
|
|
40
|
+
`usePerformance().isLowPower` on a focused set of high-traffic plugin
|
|
41
|
+
pages (Dashboard, Dependency map, System node, Notification bell,
|
|
42
|
+
Announcement banner / cards, Anomaly field overrides editor, SLO
|
|
43
|
+
attribution chart, Catalog droppable group). Hover scales, backdrop
|
|
44
|
+
blurs, `animate-pulse`/`animate-ping` accents, and entry transitions
|
|
45
|
+
now drop to static states on low-power devices; functional UX
|
|
46
|
+
transitions (Drawer/Dialog open-close, colour transitions) are left
|
|
47
|
+
alone.
|
|
48
|
+
|
|
49
|
+
Standardise the post-mutation error-toast voice on plugin pages by
|
|
50
|
+
migrating multi-clause `toast.error(extractErrorMessage(error, "Failed
|
|
51
|
+
to X"))` call sites onto the `toastError(toast, "Failed to X", error)`
|
|
52
|
+
helper from `@checkstack/ui`. The helper applies the canonical
|
|
53
|
+
`"action: message"` prefix and 100-character truncation in one place,
|
|
54
|
+
and the now-orphaned `extractErrorMessage` imports are dropped from
|
|
55
|
+
the affected files. No business logic or component APIs changed.
|
|
56
|
+
|
|
57
|
+
- f23f3c9: Standardise the empty / loading / error story on key list pages using
|
|
58
|
+
the shared `ListEmptyState`, `QueryErrorState`, and `Skeleton`
|
|
59
|
+
primitives from `@checkstack/ui`. Each affected page now branches
|
|
60
|
+
through the same `isLoading -> isError -> empty -> data` ladder, so
|
|
61
|
+
failed queries surface a retry-able inline error instead of silently
|
|
62
|
+
rendering an empty table, and loading states match the final layout
|
|
63
|
+
rather than flashing a generic spinner. No layout, business logic, or
|
|
64
|
+
query input shapes changed.
|
|
65
|
+
- Updated dependencies [f23f3c9]
|
|
66
|
+
- Updated dependencies [f23f3c9]
|
|
67
|
+
- Updated dependencies [f23f3c9]
|
|
68
|
+
- Updated dependencies [f23f3c9]
|
|
69
|
+
- Updated dependencies [f23f3c9]
|
|
70
|
+
- Updated dependencies [f23f3c9]
|
|
71
|
+
- @checkstack/common@0.11.0
|
|
72
|
+
- @checkstack/auth-frontend@0.6.5
|
|
73
|
+
- @checkstack/frontend-api@0.5.2
|
|
74
|
+
- @checkstack/dashboard-frontend@0.7.5
|
|
75
|
+
- @checkstack/gitops-frontend@0.4.5
|
|
76
|
+
- @checkstack/ui@1.10.0
|
|
77
|
+
- @checkstack/anomaly-common@1.2.2
|
|
78
|
+
- @checkstack/catalog-common@2.2.2
|
|
79
|
+
- @checkstack/healthcheck-common@1.1.2
|
|
80
|
+
- @checkstack/satellite-common@0.5.2
|
|
81
|
+
- @checkstack/tips-frontend@0.2.5
|
|
82
|
+
- @checkstack/signal-frontend@0.1.4
|
|
83
|
+
|
|
84
|
+
## 0.19.4
|
|
85
|
+
|
|
86
|
+
### Patch Changes
|
|
87
|
+
|
|
88
|
+
- a06b899: Fix stale healthcheck editor on reopen after save.
|
|
89
|
+
|
|
90
|
+
Deleting a collector from a healthcheck, saving, then reopening the
|
|
91
|
+
editor used to show the deleted collector reappear — only a full page
|
|
92
|
+
refresh cleared it. The editor's `getConfiguration` query was being
|
|
93
|
+
served stale-while-revalidate on remount, and `useInitOnceForKey`
|
|
94
|
+
fired with that stale value before the background refetch landed.
|
|
95
|
+
|
|
96
|
+
Setting `gcTime: 0` on the loader query drops the cached entry on
|
|
97
|
+
unmount, so the next mount has nothing stale to serve and the form
|
|
98
|
+
seeds from fresh data.
|
|
99
|
+
|
|
100
|
+
The wider rule has been written up at
|
|
101
|
+
`docs/src/content/docs/frontend/query-invalidation.md` (Pillar 3) and
|
|
102
|
+
a pointer added to `.agent/rules/code-style-guide.md`. tl;dr:
|
|
103
|
+
within-plugin mutations are auto-invalidated by the oRPC client (no
|
|
104
|
+
manual `refetch()` needed); cross-plugin mutations must invalidate
|
|
105
|
+
explicitly; one-shot editor forms must use `gcTime: 0` on their loader.
|
|
106
|
+
|
|
107
|
+
- a06b899: Overhaul shell + inline-script health checks with real shell semantics, real ESM execution, and upstream Node/Bun IntelliSense.
|
|
108
|
+
|
|
109
|
+
**BREAKING CHANGES**
|
|
110
|
+
|
|
111
|
+
- **Shell collector** — the `Execute Script` collector now takes a single `script` string instead of `{ command, args }`. Existing configs are auto-migrated to v2: `command` + `args` are joined with POSIX single-quote escaping into the new `script` field, so behaviour is preserved. Custom UIs that hard-coded `command`/`args` field names need to switch to `script`.
|
|
112
|
+
- **Inline collector** — scripts are now executed as real ES modules in a Bun subprocess (was: `new Function()` inside a Web Worker). The legacy `return X;` style still works (it's auto-wrapped in an async IIFE), but mixed scripts that `import` _and_ `return` at the top level need to use `export default` for their result.
|
|
113
|
+
|
|
114
|
+
**FIXES**
|
|
115
|
+
|
|
116
|
+
- Shell scripts containing pipes, redirects, `awk`, command substitution, conditionals etc. no longer fail with `ENOENT`. The collector now runs through `sh -c <script>` instead of passing the full expression as `Bun.spawn`'s argv[0]. This was the original `awk … failed with ENOENT` regression.
|
|
117
|
+
- Inline scripts can now `import { loadavg } from "node:os"` (and any other `node:*` or `bun` import). They could not before, because the executor wrapped user code inside `new Function(...)` and ran it in a Web Worker that had no Node module access; the wrapper also made top-level `import` syntactically invalid (`Unexpected token '{'`).
|
|
118
|
+
- Healthcheck editor fields no longer reset while you're editing. The page was re-running its form-state init `useEffect` on every refetch of the configuration query — and that query is invalidated on every realtime `HEALTH_CHECK_RUN_COMPLETED` signal across the platform, so in-progress edits got wiped within seconds. Replaced the naive `useEffect([existingConfig])` with a new `useInitOnceForKey` hook from `@checkstack/ui` that initialises the form only on first load per healthcheck id and ignores background refetches. The hook's decision logic is a pure function (`shouldInitForKey`) and is unit-tested in `useInitOnceForKey.test.ts`.
|
|
119
|
+
- Switching between healthcheck collectors no longer mis-applies the previous collector's tokenizer / language service to the new editor. `MultiTypeEditorField` was reusing the same React instance across collector switches (same `key="script"` in both `DynamicForm` renders) and `selectedType` was initialised from `useState` only once on first mount. After a shell→typescript switch the new collector's TS content rendered through the shell branch (no TS highlighting, no IntelliSense); the reverse direction tokenised shell content through TS and surfaced nonsense errors like `2304 "Cannot find name 'and'"` on shell comments. Now a `useEffect` re-derives `selectedType` whenever `editorTypes` changes to a set that doesn't contain the current selection.
|
|
120
|
+
- Monaco workers are now bundled locally via Vite `?worker` imports and wired up through `MonacoEnvironment.getWorker` in a new `monacoWorkers.ts` module. The default `@monaco-editor/loader` CDN path silently failed CORS on worker scripts in some browsers, leaving Monaco's TS service with only the generic `editorWorkerService` — which is enough for tokenizer-only languages like shell but breaks TypeScript's semantic features entirely. Same module configures the TS service singleton (compiler options, eager-model-sync, diagnostics-options-ignore-1108) at module load instead of inside per-editor `onMount`, so the service starts pre-configured regardless of which language opens first. Migrated from the deprecated `monaco.languages.typescript.*` path to `monaco.typescript.*` (the old path is marked `{ deprecated: true }` in monaco-editor 0.55).
|
|
121
|
+
- `defineIntegration` / `defineHealthCheck` callback parameters are now typed against the schema. Previously the virtual module declared them as `(ctx: unknown) => …`, so writing `defineIntegration(async (context) => { console.log(context.event.eventId) })` produced `'context' is of type 'unknown'. (18046)`. The result type and the shared `IntegrationScriptContext` / `HealthCheckScriptContext` interfaces are now generated together in `scriptContext.ts`, so both the function-arg form and the ambient `declare const context` reference the same schema-typed shape.
|
|
122
|
+
- The shell starter template no longer uses Linux-only `/proc/loadavg` (which fails on macOS satellites with `awk: can't open file /proc/loadavg`). It now reads the 1-minute load average via `uptime` and parses both the Linux (`load average: 0.00, 0.01, 0.05`) and macOS (`load averages: 0.45 0.55 0.65`) output formats with a portable `sed`/`awk`/`tr` pipeline.
|
|
123
|
+
- Starter-template seeding is now self-healing. `DynamicForm`'s schema-defaults `useEffect` fires AFTER child seed effects in React's child-before-parent order, so the previous one-shot seed got clobbered back to `""` by the defaults call on first mount and never re-fired. Replaced the `[]`-deps effect with a two-effect pattern: an observer that latches `hasSeededRef = true` the first time `value` is observed non-empty, and a seed effect that keeps re-installing the starter while the latch is open. Once the seed sticks the latch closes; subsequent edits and realtime refetches don't re-trigger.
|
|
124
|
+
|
|
125
|
+
**NEW**
|
|
126
|
+
|
|
127
|
+
- The Monaco editor for inline scripts now mounts the real upstream `@types/node` + `bun-types` declarations as a virtual filesystem (lazy-loaded as its own JS chunk), so IntelliSense covers the full Node/Bun stdlib, the `Bun` global, `process.env`, `Buffer`, etc. DOM types are deliberately excluded so suggestions stay focused on the backend surface. `context.config` is typed from the collector's own JSON Schema.
|
|
128
|
+
- New `healthcheckScriptContext` / `integrationScriptContext` helpers (exported from `@checkstack/ui`) build a complete editor bundle in one call: TS declarations (`context.config` / `context.event.payload` + the virtual `@checkstack/healthcheck` / `@checkstack/integration` result-type modules), starter templates per language, and the shell env-var list (with platform-injected `EVENT_ID` / `DELIVERY_ID` / `PAYLOAD_*` for integrations). Both call sites — `CollectorSection.tsx` and `CreateSubscriptionDialog.tsx` — were rewired to use them, fixing a long-standing wiring gap where IntelliSense for injected values silently never reached the editor.
|
|
129
|
+
- Inline scripts can now `import { defineHealthCheck } from "@checkstack/healthcheck"` (or `defineIntegration` for integrations) for a typed return-shape assertion. The editor catches `{ success: "yes" }` as a type error against `HealthCheckScriptResult`. The runtime is just an identity function — the collector rewrites the import to a sibling helper file in the temp dir before executing.
|
|
130
|
+
- Shell editors now autocomplete env-vars after `$` and `${`. The completion list is supplied by `healthcheckScriptContext` (safe-vars whitelist) and `integrationScriptContext` (whitelist + `EVENT_ID` etc. + `PAYLOAD_*` flattened from the event's payload schema). The matcher is pure and unit-tested in `shellEnvVarMatcher.test.ts` so regex regressions are caught locally.
|
|
131
|
+
- Empty editor fields are now seeded with a working starter template per language (inline TS uses `defineHealthCheck`, inline shell does the `awk` load-average check, integration TS shows `defineIntegration` with `context.event`, integration shell lists the `$EVENT_ID` / `$PAYLOAD_*` env vars). Users see a runnable example instead of a blank canvas; once they edit, we leave their content alone.
|
|
132
|
+
- Hardened concurrency + cleanup model documented and tested: each invocation gets its own `mkdtemp` directory + UUID result marker; the `finally` block clears the timeout handle, kills any surviving subprocess (idempotent), and removes the temp directory on success, throw, _and_ timeout. New `concurrency.test.ts` proves 20 parallel inline scripts don't cross wires and that the temp-dir count returns to baseline after throws and timeouts.
|
|
133
|
+
|
|
134
|
+
**TESTING**
|
|
135
|
+
|
|
136
|
+
Tight unit tests added so changes to the editor surface don't need smoke testing:
|
|
137
|
+
|
|
138
|
+
- `scriptContext.test.ts` — 18 tests covering the generated type declarations (including explicit regression guards for `defineIntegration` / `defineHealthCheck` callback params being typed against the shared context interface rather than `unknown`), starter templates (including a guard that the shell starter doesn't depend on Linux-only `/proc/loadavg`), shell env-vars for both healthcheck + integration flavours, plus the schema-flattening utility.
|
|
139
|
+
- `shellEnvVarMatcher.test.ts` — 12 tests covering the bare `$` / braced `${` / partial-name / case-insensitive matching logic that powers Monaco's shell completion.
|
|
140
|
+
- `inline-script-normaliser.test.ts` — 13 tests covering the legacy `return X;` → IIFE wrap path, the ESM-passthrough path, and the `@checkstack/healthcheck` import rewriter.
|
|
141
|
+
- `inline-script-collector.test.ts` — 18 tests including ones that actually execute a script importing `defineHealthCheck` (named-import form) AND using the global `defineHealthCheck` (no import) to prove both code paths resolve at runtime.
|
|
142
|
+
- `concurrency.test.ts` — 4 tests proving 20 parallel runs don't collide and that the temp-dir count returns to baseline after success, throw, and timeout.
|
|
143
|
+
- `useInitOnceForKey.test.ts` — 10 tests proving the healthcheck-editor form state isn't reset when react-query refetches in the background (the original "fields reset while I'm typing" regression).
|
|
144
|
+
- `starterTemplateSelector.test.ts` — 7 tests for the pure decision function powering empty-field seeding.
|
|
145
|
+
- `security.test.ts` — added an integration test that actually executes the portable load-average pipeline through `Bun.spawn` on the current OS, catching `/proc/loadavg`-style regressions at CI time on macOS runners.
|
|
146
|
+
|
|
147
|
+
**SECURITY**
|
|
148
|
+
|
|
149
|
+
- Same env-var whitelist as before (`PATH`, `HOME`, `USER`, `LANG`, `LC_ALL`, `LC_CTYPE`, `TZ`, `TMPDIR`, `HOSTNAME`, `SHELL`). Backend secrets in the satellite process's environment remain invisible to user scripts.
|
|
150
|
+
|
|
151
|
+
See `docs/src/content/docs/backend/script-healthchecks.md` for the full user-facing guide.
|
|
152
|
+
|
|
153
|
+
- Updated dependencies [a06b899]
|
|
154
|
+
- @checkstack/ui@1.9.0
|
|
155
|
+
- @checkstack/anomaly-common@1.2.1
|
|
156
|
+
- @checkstack/catalog-common@2.2.1
|
|
157
|
+
- @checkstack/dashboard-frontend@0.7.4
|
|
158
|
+
- @checkstack/healthcheck-common@1.1.1
|
|
159
|
+
- @checkstack/auth-frontend@0.6.4
|
|
160
|
+
- @checkstack/gitops-frontend@0.4.4
|
|
161
|
+
- @checkstack/tips-frontend@0.2.4
|
|
162
|
+
- @checkstack/satellite-common@0.5.1
|
|
163
|
+
|
|
3
164
|
## 0.19.3
|
|
4
165
|
|
|
5
166
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
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.
|
|
17
|
-
"@checkstack/auth-frontend": "0.6.
|
|
18
|
-
"@checkstack/catalog-common": "2.2.
|
|
16
|
+
"@checkstack/anomaly-common": "1.2.1",
|
|
17
|
+
"@checkstack/auth-frontend": "0.6.4",
|
|
18
|
+
"@checkstack/catalog-common": "2.2.1",
|
|
19
19
|
"@checkstack/common": "0.10.0",
|
|
20
|
-
"@checkstack/dashboard-frontend": "0.7.
|
|
20
|
+
"@checkstack/dashboard-frontend": "0.7.4",
|
|
21
21
|
"@checkstack/frontend-api": "0.5.1",
|
|
22
|
-
"@checkstack/gitops-frontend": "0.4.
|
|
23
|
-
"@checkstack/healthcheck-common": "1.1.
|
|
24
|
-
"@checkstack/satellite-common": "0.5.
|
|
22
|
+
"@checkstack/gitops-frontend": "0.4.4",
|
|
23
|
+
"@checkstack/healthcheck-common": "1.1.1",
|
|
24
|
+
"@checkstack/satellite-common": "0.5.1",
|
|
25
25
|
"@checkstack/signal-frontend": "0.1.3",
|
|
26
|
-
"@checkstack/tips-frontend": "0.2.
|
|
27
|
-
"@checkstack/ui": "1.
|
|
26
|
+
"@checkstack/tips-frontend": "0.2.4",
|
|
27
|
+
"@checkstack/ui": "1.9.0",
|
|
28
28
|
"ajv": "^8.18.0",
|
|
29
29
|
"ajv-formats": "^3.0.1",
|
|
30
30
|
"date-fns": "^4.1.0",
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
TableRow,
|
|
13
13
|
Button,
|
|
14
14
|
Badge,
|
|
15
|
+
Skeleton,
|
|
16
|
+
ResponsiveTable,
|
|
17
|
+
MobileCardList,
|
|
18
|
+
Card,
|
|
15
19
|
} from "@checkstack/ui";
|
|
16
20
|
import { Trash2, Edit, Pause, Play } from "lucide-react";
|
|
17
21
|
import { useProvenanceLock } from "@checkstack/gitops-frontend";
|
|
@@ -40,26 +44,20 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
return (
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<TableHead>Name</TableHead>
|
|
48
|
-
<TableHead>Strategy</TableHead>
|
|
49
|
-
<TableHead>Interval (s)</TableHead>
|
|
50
|
-
<TableHead>Status</TableHead>
|
|
51
|
-
<TableHead className="text-right">Actions</TableHead>
|
|
52
|
-
</TableRow>
|
|
53
|
-
</TableHeader>
|
|
54
|
-
<TableBody>
|
|
55
|
-
{configurations.length === 0 ? (
|
|
47
|
+
<>
|
|
48
|
+
<ResponsiveTable className="rounded-md border bg-card">
|
|
49
|
+
<Table>
|
|
50
|
+
<TableHeader>
|
|
56
51
|
<TableRow>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
</
|
|
52
|
+
<TableHead>Name</TableHead>
|
|
53
|
+
<TableHead>Strategy</TableHead>
|
|
54
|
+
<TableHead>Interval (s)</TableHead>
|
|
55
|
+
<TableHead>Status</TableHead>
|
|
56
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
60
57
|
</TableRow>
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
</TableHeader>
|
|
59
|
+
<TableBody>
|
|
60
|
+
{configurations.map((config) => (
|
|
63
61
|
<HealthCheckRow
|
|
64
62
|
key={config.id}
|
|
65
63
|
config={config}
|
|
@@ -70,11 +68,107 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
70
68
|
onResume={onResume}
|
|
71
69
|
canManage={canManage}
|
|
72
70
|
/>
|
|
73
|
-
))
|
|
74
|
-
|
|
75
|
-
</
|
|
76
|
-
</
|
|
77
|
-
|
|
71
|
+
))}
|
|
72
|
+
</TableBody>
|
|
73
|
+
</Table>
|
|
74
|
+
</ResponsiveTable>
|
|
75
|
+
|
|
76
|
+
<MobileCardList>
|
|
77
|
+
{configurations.map((config) => (
|
|
78
|
+
<HealthCheckMobileCard
|
|
79
|
+
key={config.id}
|
|
80
|
+
config={config}
|
|
81
|
+
strategyName={getStrategyName(config.strategyId)}
|
|
82
|
+
onEdit={onEdit}
|
|
83
|
+
onDelete={onDelete}
|
|
84
|
+
onPause={onPause}
|
|
85
|
+
onResume={onResume}
|
|
86
|
+
canManage={canManage}
|
|
87
|
+
/>
|
|
88
|
+
))}
|
|
89
|
+
</MobileCardList>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
interface HealthCheckListSkeletonProps {
|
|
95
|
+
/**
|
|
96
|
+
* Number of placeholder rows to render. Defaults to 4 so the skeleton
|
|
97
|
+
* roughly matches a typical first-page configuration list.
|
|
98
|
+
*/
|
|
99
|
+
rows?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* HealthCheckListSkeleton mirrors the shape of {@link HealthCheckList} so
|
|
104
|
+
* the page doesn't jump on load. Renders the same table chrome with
|
|
105
|
+
* `Skeleton` placeholders in each cell on desktop, and a stacked card
|
|
106
|
+
* skeleton on mobile to mirror the {@link MobileCardList} layout.
|
|
107
|
+
*/
|
|
108
|
+
export const HealthCheckListSkeleton: React.FC<
|
|
109
|
+
HealthCheckListSkeletonProps
|
|
110
|
+
> = ({ rows = 4 }) => {
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
<ResponsiveTable className="rounded-md border bg-card">
|
|
114
|
+
<Table>
|
|
115
|
+
<TableHeader>
|
|
116
|
+
<TableRow>
|
|
117
|
+
<TableHead>Name</TableHead>
|
|
118
|
+
<TableHead>Strategy</TableHead>
|
|
119
|
+
<TableHead>Interval (s)</TableHead>
|
|
120
|
+
<TableHead>Status</TableHead>
|
|
121
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
122
|
+
</TableRow>
|
|
123
|
+
</TableHeader>
|
|
124
|
+
<TableBody>
|
|
125
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
126
|
+
<TableRow key={index}>
|
|
127
|
+
<TableCell>
|
|
128
|
+
<Skeleton className="h-4 w-32" />
|
|
129
|
+
</TableCell>
|
|
130
|
+
<TableCell>
|
|
131
|
+
<Skeleton className="h-4 w-24" />
|
|
132
|
+
</TableCell>
|
|
133
|
+
<TableCell>
|
|
134
|
+
<Skeleton className="h-4 w-12" />
|
|
135
|
+
</TableCell>
|
|
136
|
+
<TableCell>
|
|
137
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
138
|
+
</TableCell>
|
|
139
|
+
<TableCell className="text-right">
|
|
140
|
+
<div className="flex justify-end gap-2">
|
|
141
|
+
<Skeleton className="h-8 w-8" />
|
|
142
|
+
<Skeleton className="h-8 w-8" />
|
|
143
|
+
<Skeleton className="h-8 w-8" />
|
|
144
|
+
</div>
|
|
145
|
+
</TableCell>
|
|
146
|
+
</TableRow>
|
|
147
|
+
))}
|
|
148
|
+
</TableBody>
|
|
149
|
+
</Table>
|
|
150
|
+
</ResponsiveTable>
|
|
151
|
+
|
|
152
|
+
<MobileCardList>
|
|
153
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
154
|
+
<Card key={index} className="p-3">
|
|
155
|
+
<div className="flex items-center justify-between gap-2">
|
|
156
|
+
<Skeleton className="h-4 w-32" />
|
|
157
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
158
|
+
</div>
|
|
159
|
+
<div className="mt-2 flex items-center gap-2">
|
|
160
|
+
<Skeleton className="h-3 w-24" />
|
|
161
|
+
<Skeleton className="h-3 w-12" />
|
|
162
|
+
</div>
|
|
163
|
+
<div className="mt-3 flex justify-end gap-2">
|
|
164
|
+
<Skeleton className="h-8 w-8" />
|
|
165
|
+
<Skeleton className="h-8 w-8" />
|
|
166
|
+
<Skeleton className="h-8 w-8" />
|
|
167
|
+
</div>
|
|
168
|
+
</Card>
|
|
169
|
+
))}
|
|
170
|
+
</MobileCardList>
|
|
171
|
+
</>
|
|
78
172
|
);
|
|
79
173
|
};
|
|
80
174
|
|
|
@@ -115,57 +209,139 @@ const HealthCheckRow: React.FC<HealthCheckRowProps> = ({
|
|
|
115
209
|
)}
|
|
116
210
|
</TableCell>
|
|
117
211
|
<TableCell className="text-right">
|
|
118
|
-
<
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
title={isLocked ? "Managed by GitOps" : "Resume health check"}
|
|
128
|
-
disabled={isLocked}
|
|
129
|
-
>
|
|
130
|
-
<Play className="h-4 w-4" />
|
|
131
|
-
</Button>
|
|
132
|
-
) : (
|
|
133
|
-
<Button
|
|
134
|
-
variant="ghost"
|
|
135
|
-
size="icon"
|
|
136
|
-
onClick={() => onPause(config.id)}
|
|
137
|
-
title={isLocked ? "Managed by GitOps" : "Pause health check"}
|
|
138
|
-
disabled={isLocked}
|
|
139
|
-
>
|
|
140
|
-
<Pause className="h-4 w-4" />
|
|
141
|
-
</Button>
|
|
142
|
-
))}
|
|
143
|
-
<Button
|
|
144
|
-
variant="ghost"
|
|
145
|
-
size="icon"
|
|
146
|
-
onClick={() => onEdit(config)}
|
|
147
|
-
title={
|
|
148
|
-
isLocked
|
|
149
|
-
? "View configuration (Managed by GitOps)"
|
|
150
|
-
: "Edit configuration"
|
|
151
|
-
}
|
|
152
|
-
>
|
|
153
|
-
<Edit className="h-4 w-4" />
|
|
154
|
-
</Button>
|
|
155
|
-
{canManage && (
|
|
156
|
-
<Button
|
|
157
|
-
variant="ghost"
|
|
158
|
-
size="icon"
|
|
159
|
-
className="text-destructive hover:text-destructive"
|
|
160
|
-
onClick={() => onDelete(config.id)}
|
|
161
|
-
disabled={isLocked}
|
|
162
|
-
title={isLocked ? "Managed by GitOps" : "Delete configuration"}
|
|
163
|
-
>
|
|
164
|
-
<Trash2 className="h-4 w-4" />
|
|
165
|
-
</Button>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
212
|
+
<HealthCheckActionButtons
|
|
213
|
+
config={config}
|
|
214
|
+
isLocked={isLocked}
|
|
215
|
+
onEdit={onEdit}
|
|
216
|
+
onDelete={onDelete}
|
|
217
|
+
onPause={onPause}
|
|
218
|
+
onResume={onResume}
|
|
219
|
+
canManage={canManage}
|
|
220
|
+
/>
|
|
168
221
|
</TableCell>
|
|
169
222
|
</TableRow>
|
|
170
223
|
);
|
|
171
224
|
};
|
|
225
|
+
|
|
226
|
+
interface HealthCheckMobileCardProps {
|
|
227
|
+
config: HealthCheckConfiguration;
|
|
228
|
+
strategyName: string;
|
|
229
|
+
onEdit: (config: HealthCheckConfiguration) => void;
|
|
230
|
+
onDelete: (id: string) => void;
|
|
231
|
+
onPause?: (id: string) => void;
|
|
232
|
+
onResume?: (id: string) => void;
|
|
233
|
+
canManage: boolean;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const HealthCheckMobileCard: React.FC<HealthCheckMobileCardProps> = ({
|
|
237
|
+
config,
|
|
238
|
+
strategyName,
|
|
239
|
+
onEdit,
|
|
240
|
+
onDelete,
|
|
241
|
+
onPause,
|
|
242
|
+
onResume,
|
|
243
|
+
canManage,
|
|
244
|
+
}) => {
|
|
245
|
+
const { isLocked } = useProvenanceLock({
|
|
246
|
+
kind: "Healthcheck",
|
|
247
|
+
entityId: config.id,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<Card className={`p-3 ${config.paused ? "opacity-60" : ""}`}>
|
|
252
|
+
<div className="flex items-start justify-between gap-2">
|
|
253
|
+
<span className="font-medium truncate">{config.name}</span>
|
|
254
|
+
{config.paused ? (
|
|
255
|
+
<Badge variant="secondary">Paused</Badge>
|
|
256
|
+
) : (
|
|
257
|
+
<Badge variant="default">Active</Badge>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
261
|
+
{strategyName} · every {config.intervalSeconds}s
|
|
262
|
+
</div>
|
|
263
|
+
<div className="mt-3 flex justify-end gap-2">
|
|
264
|
+
<HealthCheckActionButtons
|
|
265
|
+
config={config}
|
|
266
|
+
isLocked={isLocked}
|
|
267
|
+
onEdit={onEdit}
|
|
268
|
+
onDelete={onDelete}
|
|
269
|
+
onPause={onPause}
|
|
270
|
+
onResume={onResume}
|
|
271
|
+
canManage={canManage}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
</Card>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
interface HealthCheckActionButtonsProps {
|
|
279
|
+
config: HealthCheckConfiguration;
|
|
280
|
+
isLocked: boolean;
|
|
281
|
+
onEdit: (config: HealthCheckConfiguration) => void;
|
|
282
|
+
onDelete: (id: string) => void;
|
|
283
|
+
onPause?: (id: string) => void;
|
|
284
|
+
onResume?: (id: string) => void;
|
|
285
|
+
canManage: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const HealthCheckActionButtons: React.FC<HealthCheckActionButtonsProps> = ({
|
|
289
|
+
config,
|
|
290
|
+
isLocked,
|
|
291
|
+
onEdit,
|
|
292
|
+
onDelete,
|
|
293
|
+
onPause,
|
|
294
|
+
onResume,
|
|
295
|
+
canManage,
|
|
296
|
+
}) => (
|
|
297
|
+
<div className="flex justify-end gap-2">
|
|
298
|
+
{canManage &&
|
|
299
|
+
onPause &&
|
|
300
|
+
onResume &&
|
|
301
|
+
(config.paused ? (
|
|
302
|
+
<Button
|
|
303
|
+
variant="ghost"
|
|
304
|
+
size="icon"
|
|
305
|
+
onClick={() => onResume(config.id)}
|
|
306
|
+
title={isLocked ? "Managed by GitOps" : "Resume health check"}
|
|
307
|
+
disabled={isLocked}
|
|
308
|
+
>
|
|
309
|
+
<Play className="h-4 w-4" />
|
|
310
|
+
</Button>
|
|
311
|
+
) : (
|
|
312
|
+
<Button
|
|
313
|
+
variant="ghost"
|
|
314
|
+
size="icon"
|
|
315
|
+
onClick={() => onPause(config.id)}
|
|
316
|
+
title={isLocked ? "Managed by GitOps" : "Pause health check"}
|
|
317
|
+
disabled={isLocked}
|
|
318
|
+
>
|
|
319
|
+
<Pause className="h-4 w-4" />
|
|
320
|
+
</Button>
|
|
321
|
+
))}
|
|
322
|
+
<Button
|
|
323
|
+
variant="ghost"
|
|
324
|
+
size="icon"
|
|
325
|
+
onClick={() => onEdit(config)}
|
|
326
|
+
title={
|
|
327
|
+
isLocked
|
|
328
|
+
? "View configuration (Managed by GitOps)"
|
|
329
|
+
: "Edit configuration"
|
|
330
|
+
}
|
|
331
|
+
>
|
|
332
|
+
<Edit className="h-4 w-4" />
|
|
333
|
+
</Button>
|
|
334
|
+
{canManage && (
|
|
335
|
+
<Button
|
|
336
|
+
variant="ghost"
|
|
337
|
+
size="icon"
|
|
338
|
+
className="text-destructive hover:text-destructive"
|
|
339
|
+
onClick={() => onDelete(config.id)}
|
|
340
|
+
disabled={isLocked}
|
|
341
|
+
title={isLocked ? "Managed by GitOps" : "Delete configuration"}
|
|
342
|
+
>
|
|
343
|
+
<Trash2 className="h-4 w-4" />
|
|
344
|
+
</Button>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
@@ -3,7 +3,12 @@ import type {
|
|
|
3
3
|
CollectorConfigEntry,
|
|
4
4
|
CollectorDto,
|
|
5
5
|
} from "@checkstack/healthcheck-common";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
DynamicForm,
|
|
9
|
+
Label,
|
|
10
|
+
healthcheckScriptContext,
|
|
11
|
+
} from "@checkstack/ui";
|
|
7
12
|
import { Trash2 } from "lucide-react";
|
|
8
13
|
import { AssertionBuilder, type Assertion } from "../AssertionBuilder";
|
|
9
14
|
|
|
@@ -63,6 +68,9 @@ export const CollectorSection: React.FC<CollectorSectionProps> = ({
|
|
|
63
68
|
value={entry.config}
|
|
64
69
|
onChange={onConfigChange}
|
|
65
70
|
onValidChange={onValidChange}
|
|
71
|
+
{...healthcheckScriptContext({
|
|
72
|
+
collectorConfigSchema: collectorDef.configSchema,
|
|
73
|
+
})}
|
|
66
74
|
/>
|
|
67
75
|
</div>
|
|
68
76
|
)}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import { useSearchParams, useNavigate } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
4
|
usePluginClient,
|
|
5
|
+
useQueryClient,
|
|
5
6
|
wrapInSuspense,
|
|
6
7
|
accessApiRef,
|
|
7
8
|
useApi,
|
|
@@ -14,20 +15,36 @@ import {
|
|
|
14
15
|
pluginMetadata as healthcheckPluginMetadata,
|
|
15
16
|
} from "@checkstack/healthcheck-common";
|
|
16
17
|
import { Tip } from "@checkstack/tips-frontend";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
HealthCheckList,
|
|
20
|
+
HealthCheckListSkeleton,
|
|
21
|
+
} from "../components/HealthCheckList";
|
|
18
22
|
import {
|
|
19
23
|
Button,
|
|
20
24
|
ConfirmationModal,
|
|
25
|
+
ListEmptyState,
|
|
21
26
|
PageLayout,
|
|
27
|
+
QueryErrorState,
|
|
22
28
|
useToast,
|
|
29
|
+
toastError,
|
|
23
30
|
} from "@checkstack/ui";
|
|
24
31
|
import { Plus, History, Activity } from "lucide-react";
|
|
25
32
|
import { Link } from "react-router-dom";
|
|
26
|
-
import { resolveRoute
|
|
33
|
+
import { resolveRoute } from "@checkstack/common";
|
|
27
34
|
import { useState } from "react";
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Shape of the `healthcheck.getConfigurations` query output. Threaded
|
|
38
|
+
* through the optimistic pause/resume patches so cache reads/writes
|
|
39
|
+
* match the loader's surface.
|
|
40
|
+
*/
|
|
41
|
+
type ConfigurationsQueryData = {
|
|
42
|
+
configurations: HealthCheckConfiguration[];
|
|
43
|
+
};
|
|
44
|
+
|
|
29
45
|
const HealthCheckConfigPageContent = () => {
|
|
30
46
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
47
|
+
const queryClient = useQueryClient();
|
|
31
48
|
const accessApi = useApi(accessApiRef);
|
|
32
49
|
const toast = useToast();
|
|
33
50
|
const navigate = useNavigate();
|
|
@@ -43,9 +60,26 @@ const HealthCheckConfigPageContent = () => {
|
|
|
43
60
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
44
61
|
const [idToDelete, setIdToDelete] = useState<string | undefined>();
|
|
45
62
|
|
|
63
|
+
// Mirrors oRPC's `generateOperationKey([path], { type, input })` for
|
|
64
|
+
// the parameterless `getConfigurations` loader. Captured in a memo so
|
|
65
|
+
// the pause/resume optimistic patches address the exact same cache
|
|
66
|
+
// entry the loader writes. See `docs/frontend/optimistic-updates.md`
|
|
67
|
+
// for the query-key contract.
|
|
68
|
+
const configurationsQueryKey = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
[
|
|
71
|
+
["healthcheck", "getConfigurations"],
|
|
72
|
+
{ input: {}, type: "query" },
|
|
73
|
+
] as const,
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
46
77
|
// Fetch configurations with useQuery
|
|
47
|
-
const
|
|
48
|
-
|
|
78
|
+
const configurationsQuery = healthCheckClient.getConfigurations.useQuery({});
|
|
79
|
+
const {
|
|
80
|
+
data: configurationsData,
|
|
81
|
+
refetch: refetchConfigurations,
|
|
82
|
+
} = configurationsQuery;
|
|
49
83
|
|
|
50
84
|
// Fetch strategies with useQuery
|
|
51
85
|
const { data: strategies = [] } = healthCheckClient.getStrategies.useQuery(
|
|
@@ -72,25 +106,85 @@ const HealthCheckConfigPageContent = () => {
|
|
|
72
106
|
void refetchConfigurations();
|
|
73
107
|
},
|
|
74
108
|
onError: (error) => {
|
|
75
|
-
toast
|
|
109
|
+
toastError(toast, "Failed to delete health check", error);
|
|
76
110
|
},
|
|
77
111
|
});
|
|
78
112
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
// Mutation: Pause configuration — optimistic.
|
|
114
|
+
//
|
|
115
|
+
// Toggle, low risk; same four-step pattern as `markAsRead` on the
|
|
116
|
+
// notifications page (see `docs/frontend/optimistic-updates.md`):
|
|
117
|
+
// 1. onMutate flips `paused: true` on the matching row in the cache.
|
|
118
|
+
// 2. onError rolls back from the snapshot, then surfaces a toast.
|
|
119
|
+
// 3. onSettled invalidates so server truth settles in either branch.
|
|
120
|
+
// 4. No success toast — the row's pause badge IS the feedback.
|
|
121
|
+
const pauseMutation = healthCheckClient.pauseConfiguration.useMutation<{
|
|
122
|
+
previous: ConfigurationsQueryData | undefined;
|
|
123
|
+
}>({
|
|
124
|
+
onMutate: async (configId) => {
|
|
125
|
+
await queryClient.cancelQueries({ queryKey: configurationsQueryKey });
|
|
126
|
+
const previous = queryClient.getQueryData<ConfigurationsQueryData>(
|
|
127
|
+
configurationsQueryKey,
|
|
128
|
+
);
|
|
129
|
+
if (previous) {
|
|
130
|
+
queryClient.setQueryData<ConfigurationsQueryData>(
|
|
131
|
+
configurationsQueryKey,
|
|
132
|
+
{
|
|
133
|
+
...previous,
|
|
134
|
+
configurations: previous.configurations.map((c) =>
|
|
135
|
+
c.id === configId ? { ...c, paused: true } : c,
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return { previous };
|
|
82
141
|
},
|
|
83
|
-
onError: (error) => {
|
|
84
|
-
|
|
142
|
+
onError: (error, _vars, ctx) => {
|
|
143
|
+
if (ctx?.previous) {
|
|
144
|
+
queryClient.setQueryData(configurationsQueryKey, ctx.previous);
|
|
145
|
+
}
|
|
146
|
+
toastError(toast, "Failed to pause health check", error);
|
|
147
|
+
},
|
|
148
|
+
onSettled: () => {
|
|
149
|
+
void queryClient.invalidateQueries({
|
|
150
|
+
queryKey: configurationsQueryKey,
|
|
151
|
+
});
|
|
85
152
|
},
|
|
86
153
|
});
|
|
87
154
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
155
|
+
// Mutation: Resume configuration — optimistic. Mirror of `pause`
|
|
156
|
+
// with `paused: false`. See `pauseMutation` above for the contract.
|
|
157
|
+
const resumeMutation = healthCheckClient.resumeConfiguration.useMutation<{
|
|
158
|
+
previous: ConfigurationsQueryData | undefined;
|
|
159
|
+
}>({
|
|
160
|
+
onMutate: async (configId) => {
|
|
161
|
+
await queryClient.cancelQueries({ queryKey: configurationsQueryKey });
|
|
162
|
+
const previous = queryClient.getQueryData<ConfigurationsQueryData>(
|
|
163
|
+
configurationsQueryKey,
|
|
164
|
+
);
|
|
165
|
+
if (previous) {
|
|
166
|
+
queryClient.setQueryData<ConfigurationsQueryData>(
|
|
167
|
+
configurationsQueryKey,
|
|
168
|
+
{
|
|
169
|
+
...previous,
|
|
170
|
+
configurations: previous.configurations.map((c) =>
|
|
171
|
+
c.id === configId ? { ...c, paused: false } : c,
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return { previous };
|
|
91
177
|
},
|
|
92
|
-
onError: (error) => {
|
|
93
|
-
|
|
178
|
+
onError: (error, _vars, ctx) => {
|
|
179
|
+
if (ctx?.previous) {
|
|
180
|
+
queryClient.setQueryData(configurationsQueryKey, ctx.previous);
|
|
181
|
+
}
|
|
182
|
+
toastError(toast, "Failed to resume health check", error);
|
|
183
|
+
},
|
|
184
|
+
onSettled: () => {
|
|
185
|
+
void queryClient.invalidateQueries({
|
|
186
|
+
queryKey: configurationsQueryKey,
|
|
187
|
+
});
|
|
94
188
|
},
|
|
95
189
|
});
|
|
96
190
|
|
|
@@ -145,15 +239,30 @@ const HealthCheckConfigPageContent = () => {
|
|
|
145
239
|
</div>
|
|
146
240
|
}
|
|
147
241
|
>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
242
|
+
{configurationsQuery.isLoading ? (
|
|
243
|
+
<HealthCheckListSkeleton />
|
|
244
|
+
) : configurationsQuery.isError ? (
|
|
245
|
+
<QueryErrorState
|
|
246
|
+
error={configurationsQuery.error}
|
|
247
|
+
onRetry={() => void configurationsQuery.refetch()}
|
|
248
|
+
resource="health checks"
|
|
249
|
+
/>
|
|
250
|
+
) : configurations.length === 0 ? (
|
|
251
|
+
<ListEmptyState
|
|
252
|
+
resource="health checks"
|
|
253
|
+
description="No health checks have been configured yet. Create one to start monitoring a system."
|
|
254
|
+
/>
|
|
255
|
+
) : (
|
|
256
|
+
<HealthCheckList
|
|
257
|
+
configurations={configurations}
|
|
258
|
+
strategies={strategies}
|
|
259
|
+
onEdit={handleEdit}
|
|
260
|
+
onDelete={handleDelete}
|
|
261
|
+
onPause={(id) => pauseMutation.mutate(id)}
|
|
262
|
+
onResume={(id) => resumeMutation.mutate(id)}
|
|
263
|
+
canManage={canManage}
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
157
266
|
|
|
158
267
|
<ConfirmationModal
|
|
159
268
|
isOpen={isDeleteModalOpen}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
DEFAULT_STATE_THRESHOLDS,
|
|
14
14
|
} from "@checkstack/healthcheck-common";
|
|
15
15
|
import { CatalogApi } from "@checkstack/catalog-common";
|
|
16
|
-
import { PageLayout, Button, useToast, IDELayout, type ValidationIssue } from "@checkstack/ui";
|
|
16
|
+
import { PageLayout, Button, useToast, IDELayout, useInitOnceForKey, type ValidationIssue } from "@checkstack/ui";
|
|
17
17
|
import { Save, Settings } from "lucide-react";
|
|
18
18
|
import { resolveRoute, extractErrorMessage} from "@checkstack/common";
|
|
19
19
|
import { useCollectors } from "../hooks/useCollectors";
|
|
@@ -62,11 +62,19 @@ const HealthCheckIDEPageContent = () => {
|
|
|
62
62
|
{},
|
|
63
63
|
);
|
|
64
64
|
|
|
65
|
-
// Fetch single configuration for edit mode
|
|
65
|
+
// Fetch single configuration for edit mode.
|
|
66
|
+
//
|
|
67
|
+
// `gcTime: 0` is load-bearing: the form is seeded from this query
|
|
68
|
+
// exactly once via `useInitOnceForKey` below. Without it, reopening
|
|
69
|
+
// the editor after a save would synchronously serve the pre-save
|
|
70
|
+
// cached value (stale-while-revalidate) and the one-shot init would
|
|
71
|
+
// race the background refetch — deleted collectors would reappear
|
|
72
|
+
// until a hard refresh. See
|
|
73
|
+
// `docs/src/content/docs/frontend/query-invalidation.md` (Pillar 3).
|
|
66
74
|
const { data: existingConfig, isLoading: configLoading } =
|
|
67
75
|
healthCheckClient.getConfiguration.useQuery(
|
|
68
76
|
{ id: configId ?? "" },
|
|
69
|
-
{ enabled: isEditMode },
|
|
77
|
+
{ enabled: isEditMode, gcTime: 0 },
|
|
70
78
|
);
|
|
71
79
|
|
|
72
80
|
// Determine the active strategy ID
|
|
@@ -115,17 +123,23 @@ const HealthCheckIDEPageContent = () => {
|
|
|
115
123
|
systemIdFromUrl ? [systemIdFromUrl] : [],
|
|
116
124
|
);
|
|
117
125
|
|
|
118
|
-
// Initialize form from existing configuration (edit mode)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
// Initialize form from existing configuration (edit mode).
|
|
127
|
+
//
|
|
128
|
+
// CRITICAL: only ever runs once per healthcheck id. The configuration
|
|
129
|
+
// query is invalidated on every realtime `HEALTH_CHECK_RUN_COMPLETED`
|
|
130
|
+
// signal (see `SignalAutoInvalidator`), which would otherwise refetch
|
|
131
|
+
// and wipe the user's in-progress edits via a naive `[existingConfig]`
|
|
132
|
+
// dependency. Keying by `existingConfig.id` means the form only
|
|
133
|
+
// re-initialises when the user actually navigates to a different
|
|
134
|
+
// healthcheck — not on a background refetch of the same one.
|
|
135
|
+
useInitOnceForKey(existingConfig, existingConfig?.id, (config) => {
|
|
136
|
+
setFormState({
|
|
137
|
+
name: config.name,
|
|
138
|
+
intervalSeconds: config.intervalSeconds,
|
|
139
|
+
strategyConfig: config.config,
|
|
140
|
+
collectors: config.collectors ?? [],
|
|
141
|
+
});
|
|
142
|
+
});
|
|
129
143
|
|
|
130
144
|
// Unsaved changes guard
|
|
131
145
|
useEffect(() => {
|