@checkstack/healthcheck-frontend 0.19.4 → 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 +81 -0
- package/package.json +10 -10
- package/src/components/HealthCheckList.tsx +249 -73
- package/src/pages/HealthCheckConfigPage.tsx +134 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
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
|
+
|
|
3
84
|
## 0.19.4
|
|
4
85
|
|
|
5
86
|
### 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
|
+
);
|
|
@@ -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}
|