@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 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.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.0",
17
- "@checkstack/common": "0.10.0",
18
- "@checkstack/dashboard-frontend": "0.7.3",
19
- "@checkstack/dependency-common": "1.1.0",
20
- "@checkstack/frontend-api": "0.5.1",
21
- "@checkstack/healthcheck-common": "1.1.0",
22
- "@checkstack/signal-frontend": "0.1.3",
23
- "@checkstack/slo-common": "0.4.0",
24
- "@checkstack/tips-frontend": "0.2.3",
25
- "@checkstack/ui": "1.8.3",
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.2"
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="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