@checkstack/slo-frontend 0.4.4 → 0.4.6
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 +65 -0
- package/package.json +12 -12
- package/src/components/AttributionChart.tsx +14 -3
- package/src/pages/SloConfigPage.tsx +121 -56
- package/src/pages/SloDetailPage.tsx +29 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# @checkstack/slo-frontend
|
|
2
2
|
|
|
3
|
+
## 0.4.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ba07ae2]
|
|
8
|
+
- @checkstack/healthcheck-common@1.2.0
|
|
9
|
+
- @checkstack/dashboard-frontend@0.7.6
|
|
10
|
+
|
|
11
|
+
## 0.4.5
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- f23f3c9: Retrofit the highest-traffic configuration list tables
|
|
16
|
+
(`HealthCheckList`, `SloConfigPage`, and the integration
|
|
17
|
+
`DeliveryLogsPage`) onto the `ResponsiveTable` + `MobileCardList`
|
|
18
|
+
primitives from `@checkstack/ui`. On `sm` and up each page still
|
|
19
|
+
renders the unchanged 5- to 7-column table; below that breakpoint a
|
|
20
|
+
sibling stacked-card layout surfaces the same data with the resource
|
|
21
|
+
name + status badge at the top, secondary columns in a muted line, and
|
|
22
|
+
the existing action buttons in a right-aligned footer. The
|
|
23
|
+
`HealthCheckListSkeleton` placeholder mirrors both branches so the page
|
|
24
|
+
no longer jumps when data resolves. No business logic, column order,
|
|
25
|
+
or query inputs changed.
|
|
26
|
+
- f23f3c9: Gate decorative motion and blur effects behind
|
|
27
|
+
`usePerformance().isLowPower` on a focused set of high-traffic plugin
|
|
28
|
+
pages (Dashboard, Dependency map, System node, Notification bell,
|
|
29
|
+
Announcement banner / cards, Anomaly field overrides editor, SLO
|
|
30
|
+
attribution chart, Catalog droppable group). Hover scales, backdrop
|
|
31
|
+
blurs, `animate-pulse`/`animate-ping` accents, and entry transitions
|
|
32
|
+
now drop to static states on low-power devices; functional UX
|
|
33
|
+
transitions (Drawer/Dialog open-close, colour transitions) are left
|
|
34
|
+
alone.
|
|
35
|
+
|
|
36
|
+
Standardise the post-mutation error-toast voice on plugin pages by
|
|
37
|
+
migrating multi-clause `toast.error(extractErrorMessage(error, "Failed
|
|
38
|
+
to X"))` call sites onto the `toastError(toast, "Failed to X", error)`
|
|
39
|
+
helper from `@checkstack/ui`. The helper applies the canonical
|
|
40
|
+
`"action: message"` prefix and 100-character truncation in one place,
|
|
41
|
+
and the now-orphaned `extractErrorMessage` imports are dropped from
|
|
42
|
+
the affected files. No business logic or component APIs changed.
|
|
43
|
+
|
|
44
|
+
- f23f3c9: Standardise the empty / loading / error story on key list pages using
|
|
45
|
+
the shared `ListEmptyState`, `QueryErrorState`, and `Skeleton`
|
|
46
|
+
primitives from `@checkstack/ui`. Each affected page now branches
|
|
47
|
+
through the same `isLoading -> isError -> empty -> data` ladder, so
|
|
48
|
+
failed queries surface a retry-able inline error instead of silently
|
|
49
|
+
rendering an empty table, and loading states match the final layout
|
|
50
|
+
rather than flashing a generic spinner. No layout, business logic, or
|
|
51
|
+
query input shapes changed.
|
|
52
|
+
- Updated dependencies [f23f3c9]
|
|
53
|
+
- Updated dependencies [f23f3c9]
|
|
54
|
+
- Updated dependencies [f23f3c9]
|
|
55
|
+
- Updated dependencies [f23f3c9]
|
|
56
|
+
- Updated dependencies [f23f3c9]
|
|
57
|
+
- @checkstack/common@0.11.0
|
|
58
|
+
- @checkstack/frontend-api@0.5.2
|
|
59
|
+
- @checkstack/dashboard-frontend@0.7.5
|
|
60
|
+
- @checkstack/ui@1.10.0
|
|
61
|
+
- @checkstack/catalog-common@2.2.2
|
|
62
|
+
- @checkstack/dependency-common@1.1.2
|
|
63
|
+
- @checkstack/healthcheck-common@1.1.2
|
|
64
|
+
- @checkstack/slo-common@0.4.1
|
|
65
|
+
- @checkstack/tips-frontend@0.2.5
|
|
66
|
+
- @checkstack/signal-frontend@0.1.4
|
|
67
|
+
|
|
3
68
|
## 0.4.4
|
|
4
69
|
|
|
5
70
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/slo-frontend",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.tsx",
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"lint:code": "eslint . --max-warnings 0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@checkstack/catalog-common": "2.2.
|
|
17
|
-
"@checkstack/common": "0.
|
|
18
|
-
"@checkstack/dashboard-frontend": "0.7.
|
|
19
|
-
"@checkstack/dependency-common": "1.1.
|
|
20
|
-
"@checkstack/frontend-api": "0.5.
|
|
21
|
-
"@checkstack/healthcheck-common": "1.1.
|
|
22
|
-
"@checkstack/signal-frontend": "0.1.
|
|
23
|
-
"@checkstack/slo-common": "0.4.
|
|
24
|
-
"@checkstack/tips-frontend": "0.2.
|
|
25
|
-
"@checkstack/ui": "1.
|
|
16
|
+
"@checkstack/catalog-common": "2.2.2",
|
|
17
|
+
"@checkstack/common": "0.11.0",
|
|
18
|
+
"@checkstack/dashboard-frontend": "0.7.5",
|
|
19
|
+
"@checkstack/dependency-common": "1.1.2",
|
|
20
|
+
"@checkstack/frontend-api": "0.5.2",
|
|
21
|
+
"@checkstack/healthcheck-common": "1.1.2",
|
|
22
|
+
"@checkstack/signal-frontend": "0.1.4",
|
|
23
|
+
"@checkstack/slo-common": "0.4.1",
|
|
24
|
+
"@checkstack/tips-frontend": "0.2.5",
|
|
25
|
+
"@checkstack/ui": "1.10.0",
|
|
26
26
|
"date-fns": "^4.1.0",
|
|
27
27
|
"lucide-react": "^0.344.0",
|
|
28
28
|
"react": "^18.2.0",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"typescript": "^5.0.0",
|
|
34
34
|
"@types/react": "^18.2.0",
|
|
35
35
|
"@checkstack/tsconfig": "0.0.7",
|
|
36
|
-
"@checkstack/scripts": "0.3.
|
|
36
|
+
"@checkstack/scripts": "0.3.3"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { cn, usePerformance } from "@checkstack/ui";
|
|
2
3
|
|
|
3
4
|
interface AttributionChartProps {
|
|
4
5
|
attribution: Array<{
|
|
@@ -18,6 +19,7 @@ export const AttributionChart: React.FC<AttributionChartProps> = ({
|
|
|
18
19
|
attribution,
|
|
19
20
|
totalBudgetMinutes,
|
|
20
21
|
}) => {
|
|
22
|
+
const { isLowPower } = usePerformance();
|
|
21
23
|
if (totalBudgetMinutes <= 0) return;
|
|
22
24
|
|
|
23
25
|
const selfMinutes = attribution
|
|
@@ -47,21 +49,30 @@ export const AttributionChart: React.FC<AttributionChartProps> = ({
|
|
|
47
49
|
<div className="h-6 rounded-full overflow-hidden flex bg-muted/30 border border-border">
|
|
48
50
|
{selfPercent > 0 && (
|
|
49
51
|
<div
|
|
50
|
-
className=
|
|
52
|
+
className={cn(
|
|
53
|
+
"bg-destructive/80",
|
|
54
|
+
!isLowPower && "transition-all duration-500",
|
|
55
|
+
)}
|
|
51
56
|
style={{ width: `${selfPercent}%` }}
|
|
52
57
|
title={`Self: ${selfMinutes.toFixed(1)} min`}
|
|
53
58
|
/>
|
|
54
59
|
)}
|
|
55
60
|
{upstreamPercent > 0 && (
|
|
56
61
|
<div
|
|
57
|
-
className=
|
|
62
|
+
className={cn(
|
|
63
|
+
"bg-amber-500/80",
|
|
64
|
+
!isLowPower && "transition-all duration-500",
|
|
65
|
+
)}
|
|
58
66
|
style={{ width: `${upstreamPercent}%` }}
|
|
59
67
|
title={`Upstream: ${upstreamMinutes.toFixed(1)} min`}
|
|
60
68
|
/>
|
|
61
69
|
)}
|
|
62
70
|
{remainingPercent > 0 && (
|
|
63
71
|
<div
|
|
64
|
-
className=
|
|
72
|
+
className={cn(
|
|
73
|
+
"bg-emerald-500/30",
|
|
74
|
+
!isLowPower && "transition-all duration-500",
|
|
75
|
+
)}
|
|
65
76
|
style={{ width: `${remainingPercent}%` }}
|
|
66
77
|
title={`Remaining: ${(totalBudgetMinutes - selfMinutes - upstreamMinutes).toFixed(1)} min`}
|
|
67
78
|
/>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Badge,
|
|
24
24
|
LoadingSpinner,
|
|
25
25
|
EmptyState,
|
|
26
|
+
QueryErrorState,
|
|
26
27
|
Table,
|
|
27
28
|
TableHeader,
|
|
28
29
|
TableRow,
|
|
@@ -32,6 +33,8 @@ import {
|
|
|
32
33
|
useToast,
|
|
33
34
|
ConfirmationModal,
|
|
34
35
|
PageLayout,
|
|
36
|
+
ResponsiveTable,
|
|
37
|
+
MobileCardList,
|
|
35
38
|
} from "@checkstack/ui";
|
|
36
39
|
import { Plus, Target, Trash2, Edit2 } from "lucide-react";
|
|
37
40
|
import { extractErrorMessage } from "@checkstack/common";
|
|
@@ -48,11 +51,12 @@ const SloConfigPageContent: React.FC = () => {
|
|
|
48
51
|
sloAccess.slo.manage,
|
|
49
52
|
);
|
|
50
53
|
|
|
54
|
+
const objectivesQuery = sloClient.listObjectives.useQuery({});
|
|
51
55
|
const {
|
|
52
56
|
data: objectivesData,
|
|
53
57
|
isLoading: objectivesLoading,
|
|
54
58
|
refetch: refetchObjectives,
|
|
55
|
-
} =
|
|
59
|
+
} = objectivesQuery;
|
|
56
60
|
|
|
57
61
|
const { data: systemsData, isLoading: systemsLoading } =
|
|
58
62
|
catalogClient.getSystems.useQuery({});
|
|
@@ -60,6 +64,7 @@ const SloConfigPageContent: React.FC = () => {
|
|
|
60
64
|
const objectives = objectivesData?.objectives ?? [];
|
|
61
65
|
const systems = systemsData?.systems ?? [];
|
|
62
66
|
const loading = objectivesLoading || systemsLoading;
|
|
67
|
+
const isError = objectivesQuery.isError;
|
|
63
68
|
|
|
64
69
|
// Editor state
|
|
65
70
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
@@ -129,6 +134,21 @@ const SloConfigPageContent: React.FC = () => {
|
|
|
129
134
|
}
|
|
130
135
|
};
|
|
131
136
|
|
|
137
|
+
const renderStatusBadge = (
|
|
138
|
+
status: (typeof objectives)[number]["status"],
|
|
139
|
+
) => {
|
|
140
|
+
if (status.isBreaching) {
|
|
141
|
+
return <Badge variant="destructive">Breaching</Badge>;
|
|
142
|
+
}
|
|
143
|
+
if (status.hasOpenDowntime) {
|
|
144
|
+
return <Badge variant="warning">Degraded</Badge>;
|
|
145
|
+
}
|
|
146
|
+
if (status.errorBudgetRemainingPercent <= 20) {
|
|
147
|
+
return <Badge variant="warning">At Risk</Badge>;
|
|
148
|
+
}
|
|
149
|
+
return <Badge variant="success">Healthy</Badge>;
|
|
150
|
+
};
|
|
151
|
+
|
|
132
152
|
return (
|
|
133
153
|
<PageLayout
|
|
134
154
|
title="SLO Management"
|
|
@@ -164,6 +184,14 @@ const SloConfigPageContent: React.FC = () => {
|
|
|
164
184
|
<div className="p-12 flex justify-center">
|
|
165
185
|
<LoadingSpinner />
|
|
166
186
|
</div>
|
|
187
|
+
) : isError ? (
|
|
188
|
+
<div className="p-4">
|
|
189
|
+
<QueryErrorState
|
|
190
|
+
error={objectivesQuery.error}
|
|
191
|
+
onRetry={() => void objectivesQuery.refetch()}
|
|
192
|
+
resource="SLO objectives"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
167
195
|
) : objectives.length === 0 ? (
|
|
168
196
|
<EmptyState
|
|
169
197
|
icon={<Target className="size-10" />}
|
|
@@ -184,63 +212,100 @@ const SloConfigPageContent: React.FC = () => {
|
|
|
184
212
|
}
|
|
185
213
|
/>
|
|
186
214
|
) : (
|
|
187
|
-
|
|
188
|
-
<
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
<>
|
|
216
|
+
<ResponsiveTable>
|
|
217
|
+
<Table>
|
|
218
|
+
<TableHeader>
|
|
219
|
+
<TableRow>
|
|
220
|
+
<TableHead>System</TableHead>
|
|
221
|
+
<TableHead>Target</TableHead>
|
|
222
|
+
<TableHead>Window</TableHead>
|
|
223
|
+
<TableHead>Exclusion Mode</TableHead>
|
|
224
|
+
<TableHead>Status</TableHead>
|
|
225
|
+
<TableHead className="w-24">Actions</TableHead>
|
|
226
|
+
</TableRow>
|
|
227
|
+
</TableHeader>
|
|
228
|
+
<TableBody>
|
|
229
|
+
{objectives.map((item) => (
|
|
230
|
+
<TableRow key={item.objective.id}>
|
|
231
|
+
<TableCell className="font-medium">
|
|
232
|
+
{getSystemName(item.objective.systemId)}
|
|
233
|
+
</TableCell>
|
|
234
|
+
<TableCell>{item.objective.target}%</TableCell>
|
|
235
|
+
<TableCell>{item.objective.windowDays}d</TableCell>
|
|
236
|
+
<TableCell>
|
|
237
|
+
{getExclusionBadge(
|
|
238
|
+
item.objective.dependencyExclusion,
|
|
239
|
+
)}
|
|
240
|
+
</TableCell>
|
|
241
|
+
<TableCell>
|
|
242
|
+
{renderStatusBadge(item.status)}
|
|
243
|
+
</TableCell>
|
|
244
|
+
<TableCell>
|
|
245
|
+
<div className="flex gap-2">
|
|
246
|
+
<Button
|
|
247
|
+
variant="ghost"
|
|
248
|
+
size="sm"
|
|
249
|
+
onClick={() => handleEdit(item.objective)}
|
|
250
|
+
>
|
|
251
|
+
<Edit2 className="h-4 w-4" />
|
|
252
|
+
</Button>
|
|
253
|
+
<Button
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="sm"
|
|
256
|
+
onClick={() =>
|
|
257
|
+
setDeleteId(item.objective.id)
|
|
258
|
+
}
|
|
259
|
+
>
|
|
260
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
263
|
+
</TableCell>
|
|
264
|
+
</TableRow>
|
|
265
|
+
))}
|
|
266
|
+
</TableBody>
|
|
267
|
+
</Table>
|
|
268
|
+
</ResponsiveTable>
|
|
269
|
+
|
|
270
|
+
<MobileCardList className="p-3">
|
|
199
271
|
{objectives.map((item) => (
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{item.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
onClick={() => setDeleteId(item.objective.id)}
|
|
235
|
-
>
|
|
236
|
-
<Trash2 className="h-4 w-4 text-destructive" />
|
|
237
|
-
</Button>
|
|
238
|
-
</div>
|
|
239
|
-
</TableCell>
|
|
240
|
-
</TableRow>
|
|
272
|
+
<div
|
|
273
|
+
key={item.objective.id}
|
|
274
|
+
className="rounded-md border border-border bg-card p-3"
|
|
275
|
+
>
|
|
276
|
+
<div className="flex items-start justify-between gap-2">
|
|
277
|
+
<span className="font-medium truncate">
|
|
278
|
+
{getSystemName(item.objective.systemId)}
|
|
279
|
+
</span>
|
|
280
|
+
{renderStatusBadge(item.status)}
|
|
281
|
+
</div>
|
|
282
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
283
|
+
{item.objective.target}% ·{" "}
|
|
284
|
+
{item.objective.windowDays}d window
|
|
285
|
+
</div>
|
|
286
|
+
<div className="mt-2">
|
|
287
|
+
{getExclusionBadge(item.objective.dependencyExclusion)}
|
|
288
|
+
</div>
|
|
289
|
+
<div className="mt-3 flex justify-end gap-2">
|
|
290
|
+
<Button
|
|
291
|
+
variant="ghost"
|
|
292
|
+
size="sm"
|
|
293
|
+
onClick={() => handleEdit(item.objective)}
|
|
294
|
+
>
|
|
295
|
+
<Edit2 className="h-4 w-4" />
|
|
296
|
+
</Button>
|
|
297
|
+
<Button
|
|
298
|
+
variant="ghost"
|
|
299
|
+
size="sm"
|
|
300
|
+
onClick={() => setDeleteId(item.objective.id)}
|
|
301
|
+
>
|
|
302
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
303
|
+
</Button>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
241
306
|
))}
|
|
242
|
-
</
|
|
243
|
-
|
|
307
|
+
</MobileCardList>
|
|
308
|
+
</>
|
|
244
309
|
)}
|
|
245
310
|
</CardContent>
|
|
246
311
|
</Card>
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
PageLayout,
|
|
22
22
|
LoadingSpinner,
|
|
23
23
|
Badge,
|
|
24
|
+
ListEmptyState,
|
|
25
|
+
QueryErrorState,
|
|
24
26
|
} from "@checkstack/ui";
|
|
25
27
|
import {
|
|
26
28
|
Target,
|
|
@@ -39,10 +41,11 @@ const SloDetailPageContent: React.FC = () => {
|
|
|
39
41
|
const sloClient = usePluginClient(SloApi);
|
|
40
42
|
const catalogClient = usePluginClient(CatalogApi);
|
|
41
43
|
|
|
42
|
-
const
|
|
44
|
+
const objectiveQuery = sloClient.getObjective.useQuery(
|
|
43
45
|
{ id: sloId ?? "" },
|
|
44
46
|
{ enabled: !!sloId },
|
|
45
47
|
);
|
|
48
|
+
const { data, isLoading, isError } = objectiveQuery;
|
|
46
49
|
|
|
47
50
|
const { data: eventsData } = sloClient.getDowntimeEvents.useQuery(
|
|
48
51
|
{ objectiveId: sloId ?? "", limit: 20 },
|
|
@@ -77,7 +80,7 @@ const SloDetailPageContent: React.FC = () => {
|
|
|
77
80
|
|
|
78
81
|
const events = eventsData?.events;
|
|
79
82
|
|
|
80
|
-
if (isLoading
|
|
83
|
+
if (isLoading) {
|
|
81
84
|
return (
|
|
82
85
|
<PageLayout title="SLO Detail" icon={Target}>
|
|
83
86
|
<div className="p-12 flex justify-center">
|
|
@@ -87,6 +90,30 @@ const SloDetailPageContent: React.FC = () => {
|
|
|
87
90
|
);
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
if (isError) {
|
|
94
|
+
return (
|
|
95
|
+
<PageLayout title="SLO Detail" icon={Target}>
|
|
96
|
+
<QueryErrorState
|
|
97
|
+
error={objectiveQuery.error}
|
|
98
|
+
onRetry={() => void objectiveQuery.refetch()}
|
|
99
|
+
resource="SLO"
|
|
100
|
+
/>
|
|
101
|
+
</PageLayout>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!data) {
|
|
106
|
+
return (
|
|
107
|
+
<PageLayout title="SLO Detail" icon={Target}>
|
|
108
|
+
<ListEmptyState
|
|
109
|
+
resource="SLO"
|
|
110
|
+
description="This SLO does not exist or has been deleted. It may have been removed by another user or via GitOps."
|
|
111
|
+
icon={<Target className="h-10 w-10" />}
|
|
112
|
+
/>
|
|
113
|
+
</PageLayout>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
const { objective, status } = data;
|
|
91
118
|
const systemName = systemData?.name ?? objective.systemId;
|
|
92
119
|
|