@checkstack/slo-frontend 0.4.4 → 0.4.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,62 @@
1
1
  # @checkstack/slo-frontend
2
2
 
3
+ ## 0.4.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: Gate decorative motion and blur effects behind
19
+ `usePerformance().isLowPower` on a focused set of high-traffic plugin
20
+ pages (Dashboard, Dependency map, System node, Notification bell,
21
+ Announcement banner / cards, Anomaly field overrides editor, SLO
22
+ attribution chart, Catalog droppable group). Hover scales, backdrop
23
+ blurs, `animate-pulse`/`animate-ping` accents, and entry transitions
24
+ now drop to static states on low-power devices; functional UX
25
+ transitions (Drawer/Dialog open-close, colour transitions) are left
26
+ alone.
27
+
28
+ Standardise the post-mutation error-toast voice on plugin pages by
29
+ migrating multi-clause `toast.error(extractErrorMessage(error, "Failed
30
+ to X"))` call sites onto the `toastError(toast, "Failed to X", error)`
31
+ helper from `@checkstack/ui`. The helper applies the canonical
32
+ `"action: message"` prefix and 100-character truncation in one place,
33
+ and the now-orphaned `extractErrorMessage` imports are dropped from
34
+ the affected files. No business logic or component APIs changed.
35
+
36
+ - f23f3c9: Standardise the empty / loading / error story on key list pages using
37
+ the shared `ListEmptyState`, `QueryErrorState`, and `Skeleton`
38
+ primitives from `@checkstack/ui`. Each affected page now branches
39
+ through the same `isLoading -> isError -> empty -> data` ladder, so
40
+ failed queries surface a retry-able inline error instead of silently
41
+ rendering an empty table, and loading states match the final layout
42
+ rather than flashing a generic spinner. No layout, business logic, or
43
+ query input shapes changed.
44
+ - Updated dependencies [f23f3c9]
45
+ - Updated dependencies [f23f3c9]
46
+ - Updated dependencies [f23f3c9]
47
+ - Updated dependencies [f23f3c9]
48
+ - Updated dependencies [f23f3c9]
49
+ - @checkstack/common@0.11.0
50
+ - @checkstack/frontend-api@0.5.2
51
+ - @checkstack/dashboard-frontend@0.7.5
52
+ - @checkstack/ui@1.10.0
53
+ - @checkstack/catalog-common@2.2.2
54
+ - @checkstack/dependency-common@1.1.2
55
+ - @checkstack/healthcheck-common@1.1.2
56
+ - @checkstack/slo-common@0.4.1
57
+ - @checkstack/tips-frontend@0.2.5
58
+ - @checkstack/signal-frontend@0.1.4
59
+
3
60
  ## 0.4.4
4
61
 
5
62
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/slo-frontend",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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.0",
16
+ "@checkstack/catalog-common": "2.2.1",
17
17
  "@checkstack/common": "0.10.0",
18
- "@checkstack/dashboard-frontend": "0.7.3",
19
- "@checkstack/dependency-common": "1.1.0",
18
+ "@checkstack/dashboard-frontend": "0.7.4",
19
+ "@checkstack/dependency-common": "1.1.1",
20
20
  "@checkstack/frontend-api": "0.5.1",
21
- "@checkstack/healthcheck-common": "1.1.0",
21
+ "@checkstack/healthcheck-common": "1.1.1",
22
22
  "@checkstack/signal-frontend": "0.1.3",
23
23
  "@checkstack/slo-common": "0.4.0",
24
- "@checkstack/tips-frontend": "0.2.3",
25
- "@checkstack/ui": "1.8.3",
24
+ "@checkstack/tips-frontend": "0.2.4",
25
+ "@checkstack/ui": "1.9.0",
26
26
  "date-fns": "^4.1.0",
27
27
  "lucide-react": "^0.344.0",
28
28
  "react": "^18.2.0",
@@ -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="bg-destructive/80 transition-all duration-500"
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="bg-amber-500/80 transition-all duration-500"
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="bg-emerald-500/30 transition-all duration-500"
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
- } = sloClient.listObjectives.useQuery({});
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
- <Table>
188
- <TableHeader>
189
- <TableRow>
190
- <TableHead>System</TableHead>
191
- <TableHead>Target</TableHead>
192
- <TableHead>Window</TableHead>
193
- <TableHead>Exclusion Mode</TableHead>
194
- <TableHead>Status</TableHead>
195
- <TableHead className="w-24">Actions</TableHead>
196
- </TableRow>
197
- </TableHeader>
198
- <TableBody>
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
- <TableRow key={item.objective.id}>
201
- <TableCell className="font-medium">
202
- {getSystemName(item.objective.systemId)}
203
- </TableCell>
204
- <TableCell>{item.objective.target}%</TableCell>
205
- <TableCell>{item.objective.windowDays}d</TableCell>
206
- <TableCell>
207
- {getExclusionBadge(
208
- item.objective.dependencyExclusion,
209
- )}
210
- </TableCell>
211
- <TableCell>
212
- {item.status.isBreaching ? (
213
- <Badge variant="destructive">Breaching</Badge>
214
- ) : item.status.hasOpenDowntime ? (
215
- <Badge variant="warning">Degraded</Badge>
216
- ) : item.status.errorBudgetRemainingPercent <= 20 ? (
217
- <Badge variant="warning">At Risk</Badge>
218
- ) : (
219
- <Badge variant="success">Healthy</Badge>
220
- )}
221
- </TableCell>
222
- <TableCell>
223
- <div className="flex gap-2">
224
- <Button
225
- variant="ghost"
226
- size="sm"
227
- onClick={() => handleEdit(item.objective)}
228
- >
229
- <Edit2 className="h-4 w-4" />
230
- </Button>
231
- <Button
232
- variant="ghost"
233
- size="sm"
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}% &middot;{" "}
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
- </TableBody>
243
- </Table>
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 { data, isLoading } = sloClient.getObjective.useQuery(
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 || !data) {
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