@checkstack/slo-frontend 0.2.0

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.
@@ -0,0 +1,283 @@
1
+ import React, { useMemo } from "react";
2
+ import { useParams } from "react-router-dom";
3
+ import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
4
+ import { useSignal } from "@checkstack/signal-frontend";
5
+ import { SloApi } from "../api";
6
+ import { SLO_STATUS_CHANGED } from "@checkstack/slo-common";
7
+ import { CatalogApi } from "@checkstack/catalog-common";
8
+ import { ErrorBudgetBar } from "../components/ErrorBudgetBar";
9
+ import { BurnRateIndicator } from "../components/BurnRateIndicator";
10
+ import { StreakCounter } from "../components/StreakCounter";
11
+ import { AttributionChart } from "../components/AttributionChart";
12
+ import { DowntimeTimeline } from "../components/DowntimeTimeline";
13
+ import { SloTrendChart } from "../components/SloTrendChart";
14
+ import {
15
+ AchievementBadge,
16
+ NoAchievements,
17
+ } from "../components/AchievementBadge";
18
+ import {
19
+ Card,
20
+ CardContent,
21
+ CardHeader,
22
+ CardTitle,
23
+ PageLayout,
24
+ LoadingSpinner,
25
+ Badge,
26
+ } from "@checkstack/ui";
27
+ import {
28
+ Target,
29
+ Clock,
30
+ Shield,
31
+ TrendingUp,
32
+ Flame,
33
+ Award,
34
+ BarChart3,
35
+ LineChart,
36
+ } from "lucide-react";
37
+ import { subDays } from "date-fns";
38
+
39
+ const SloDetailPageContent: React.FC = () => {
40
+ const { sloId } = useParams<{ sloId: string }>();
41
+ const sloClient = usePluginClient(SloApi);
42
+ const catalogClient = usePluginClient(CatalogApi);
43
+
44
+ const { data, isLoading, refetch } = sloClient.getObjective.useQuery(
45
+ { id: sloId ?? "" },
46
+ { enabled: !!sloId },
47
+ );
48
+
49
+ const { data: eventsData, refetch: refetchEvents } =
50
+ sloClient.getDowntimeEvents.useQuery(
51
+ { objectiveId: sloId ?? "", limit: 20 },
52
+ { enabled: !!sloId },
53
+ );
54
+
55
+ const { data: streaksData } = sloClient.getStreaks.useQuery({});
56
+
57
+ const { data: systemData } = catalogClient.getSystem.useQuery(
58
+ { systemId: data?.objective?.systemId ?? "" },
59
+ { enabled: !!data?.objective?.systemId },
60
+ );
61
+
62
+ const { data: achievementsData } = sloClient.getAchievements.useQuery(
63
+ { systemId: data?.objective?.systemId ?? "" },
64
+ { enabled: !!data?.objective?.systemId },
65
+ );
66
+
67
+ const snapshotWindowDays = data?.objective?.windowDays ?? 30;
68
+ const snapshotRange = useMemo(() => ({
69
+ startDate: subDays(new Date(), snapshotWindowDays),
70
+ endDate: new Date(),
71
+ }), [snapshotWindowDays]);
72
+
73
+ const { data: snapshotsData } = sloClient.getDailySnapshots.useQuery(
74
+ {
75
+ objectiveId: sloId ?? "",
76
+ ...snapshotRange,
77
+ },
78
+ { enabled: !!sloId },
79
+ );
80
+
81
+ const events = eventsData?.events;
82
+
83
+ useSignal(SLO_STATUS_CHANGED, ({ objectiveId }) => {
84
+ if (objectiveId === sloId) {
85
+ void refetch();
86
+ void refetchEvents();
87
+ }
88
+ });
89
+
90
+ if (isLoading || !data) {
91
+ return (
92
+ <PageLayout title="SLO Detail" icon={Target}>
93
+ <div className="p-12 flex justify-center">
94
+ <LoadingSpinner />
95
+ </div>
96
+ </PageLayout>
97
+ );
98
+ }
99
+
100
+ const { objective, status } = data;
101
+ const systemName = systemData?.name ?? objective.systemId;
102
+
103
+ // Find streak for this objective
104
+ const streak = streaksData?.streaks.find(
105
+ (s) => s.objectiveId === objective.id,
106
+ );
107
+
108
+ // Filter achievements for this system
109
+ const achievements = achievementsData?.achievements ?? [];
110
+
111
+ return (
112
+ <PageLayout
113
+ title={`${objective.target}% / ${objective.windowDays}d SLO`}
114
+ subtitle={`System: ${systemName}`}
115
+ icon={Target}
116
+ >
117
+ {/* Status Cards */}
118
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
119
+ <Card>
120
+ <CardHeader className="pb-2">
121
+ <CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
122
+ <Shield className="w-4 h-4" />
123
+ Current Availability
124
+ </CardTitle>
125
+ </CardHeader>
126
+ <CardContent>
127
+ <div className="text-3xl font-bold">
128
+ {status.currentAvailability?.toFixed(3) ?? "—"}%
129
+ </div>
130
+ <div className="text-sm text-muted-foreground mt-1">
131
+ Target: {objective.target}%
132
+ </div>
133
+ </CardContent>
134
+ </Card>
135
+
136
+ <Card>
137
+ <CardHeader className="pb-2">
138
+ <CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
139
+ <Clock className="w-4 h-4" />
140
+ Error Budget
141
+ </CardTitle>
142
+ </CardHeader>
143
+ <CardContent>
144
+ <div className="text-3xl font-bold">
145
+ {status.errorBudgetRemainingMinutes.toFixed(1)}
146
+ <span className="text-lg text-muted-foreground"> min</span>
147
+ </div>
148
+ <ErrorBudgetBar
149
+ consumedPercent={100 - status.errorBudgetRemainingPercent}
150
+ warningThreshold={
151
+ objective.burnRateThresholds.warningPercent
152
+ }
153
+ criticalThreshold={
154
+ objective.burnRateThresholds.criticalPercent
155
+ }
156
+ label="Budget consumption"
157
+ />
158
+ </CardContent>
159
+ </Card>
160
+
161
+ <Card>
162
+ <CardHeader className="pb-2">
163
+ <CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
164
+ <TrendingUp className="w-4 h-4" />
165
+ Burn Rate
166
+ </CardTitle>
167
+ </CardHeader>
168
+ <CardContent>
169
+ <div className="text-3xl font-bold">
170
+ <BurnRateIndicator burnRate={status.burnRate} />
171
+ </div>
172
+ <div className="text-sm text-muted-foreground mt-1">
173
+ {status.isBreaching ? (
174
+ <Badge variant="destructive">Breaching</Badge>
175
+ ) : status.hasOpenDowntime ? (
176
+ <Badge variant="warning">Degraded</Badge>
177
+ ) : (
178
+ <Badge variant="success">Within Budget</Badge>
179
+ )}
180
+ </div>
181
+ </CardContent>
182
+ </Card>
183
+ </div>
184
+
185
+ {/* Streak & Achievements Row */}
186
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
187
+ <Card>
188
+ <CardHeader className="pb-2">
189
+ <CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
190
+ <Flame className="w-4 h-4" />
191
+ Compliance Streak
192
+ </CardTitle>
193
+ </CardHeader>
194
+ <CardContent>
195
+ {streak ? (
196
+ <StreakCounter
197
+ currentStreak={streak.currentStreak}
198
+ bestStreak={streak.bestStreak}
199
+ />
200
+ ) : (
201
+ <div className="text-sm text-muted-foreground">
202
+ Streaks will be calculated after the first daily snapshot
203
+ </div>
204
+ )}
205
+ </CardContent>
206
+ </Card>
207
+
208
+ <Card>
209
+ <CardHeader className="pb-2">
210
+ <CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
211
+ <Award className="w-4 h-4" />
212
+ Achievements
213
+ </CardTitle>
214
+ </CardHeader>
215
+ <CardContent>
216
+ {achievements.length > 0 ? (
217
+ <div className="flex flex-wrap gap-2">
218
+ {achievements.map((a) => (
219
+ <AchievementBadge
220
+ key={a.id}
221
+ achievement={a.achievement}
222
+ unlockedAt={a.unlockedAt}
223
+ />
224
+ ))}
225
+ </div>
226
+ ) : (
227
+ <NoAchievements />
228
+ )}
229
+ </CardContent>
230
+ </Card>
231
+ </div>
232
+
233
+ {/* Attribution Chart */}
234
+ {status.attribution.length > 0 && (
235
+ <Card>
236
+ <CardHeader>
237
+ <CardTitle className="text-sm flex items-center gap-2">
238
+ <BarChart3 className="w-4 h-4" />
239
+ Budget Attribution
240
+ </CardTitle>
241
+ </CardHeader>
242
+ <CardContent>
243
+ <AttributionChart
244
+ attribution={status.attribution}
245
+ totalBudgetMinutes={status.errorBudgetTotalMinutes}
246
+ />
247
+ </CardContent>
248
+ </Card>
249
+ )}
250
+
251
+ {/* Availability Trend Chart */}
252
+ <Card>
253
+ <CardHeader>
254
+ <CardTitle className="text-sm flex items-center gap-2">
255
+ <LineChart className="w-4 h-4" />
256
+ Availability Trend
257
+ </CardTitle>
258
+ </CardHeader>
259
+ <CardContent>
260
+ <SloTrendChart
261
+ snapshots={snapshotsData?.snapshots ?? []}
262
+ target={objective.target}
263
+ />
264
+ </CardContent>
265
+ </Card>
266
+
267
+ {/* Downtime Events Timeline */}
268
+ <Card>
269
+ <CardHeader>
270
+ <CardTitle className="text-sm flex items-center gap-2">
271
+ <Clock className="w-4 h-4" />
272
+ Recent Downtime Events
273
+ </CardTitle>
274
+ </CardHeader>
275
+ <CardContent>
276
+ <DowntimeTimeline events={events ?? []} />
277
+ </CardContent>
278
+ </Card>
279
+ </PageLayout>
280
+ );
281
+ };
282
+
283
+ export const SloDetailPage = wrapInSuspense(SloDetailPageContent);
@@ -0,0 +1,140 @@
1
+ import React, { useMemo } from "react";
2
+ import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
3
+ import { useSignal } from "@checkstack/signal-frontend";
4
+ import { SloApi } from "../api";
5
+ import { SLO_STATUS_CHANGED, sloRoutes } from "@checkstack/slo-common";
6
+ import { CatalogApi } from "@checkstack/catalog-common";
7
+ import { ErrorBudgetBar } from "../components/ErrorBudgetBar";
8
+ import { BurnRateIndicator } from "../components/BurnRateIndicator";
9
+ import { MilestoneFeed } from "../components/MilestoneFeed";
10
+ import {
11
+ Card,
12
+ CardContent,
13
+ CardHeader,
14
+ CardTitle,
15
+ PageLayout,
16
+ EmptyState,
17
+ LoadingSpinner,
18
+ Badge,
19
+ } from "@checkstack/ui";
20
+ import { Target, ArrowRight } from "lucide-react";
21
+ import { Link } from "react-router-dom";
22
+ import { resolveRoute } from "@checkstack/common";
23
+
24
+ const SloOverviewPageContent: React.FC = () => {
25
+ const sloClient = usePluginClient(SloApi);
26
+ const catalogClient = usePluginClient(CatalogApi);
27
+
28
+ const {
29
+ data: objectivesData,
30
+ isLoading: objectivesLoading,
31
+ refetch,
32
+ } = sloClient.listObjectives.useQuery({});
33
+
34
+ const { data: systemsData, isLoading: systemsLoading } =
35
+ catalogClient.getSystems.useQuery({});
36
+
37
+ useSignal(SLO_STATUS_CHANGED, () => {
38
+ void refetch();
39
+ });
40
+
41
+ const objectives = objectivesData?.objectives ?? [];
42
+ const isLoading = objectivesLoading || systemsLoading;
43
+
44
+ const systemNameMap = useMemo(() => {
45
+ const map = new Map<string, string>();
46
+ for (const system of systemsData?.systems ?? []) {
47
+ map.set(system.id, system.name);
48
+ }
49
+ return map;
50
+ }, [systemsData]);
51
+
52
+ return (
53
+ <PageLayout
54
+ title="SLO Dashboard"
55
+ subtitle="Service Level Objective performance across all systems"
56
+ icon={Target}
57
+ actions={
58
+ <Link
59
+ to={resolveRoute(sloRoutes.routes.config)}
60
+ className="inline-flex items-center gap-2 text-sm text-primary hover:underline"
61
+ >
62
+ Manage SLOs
63
+ <ArrowRight className="w-4 h-4" />
64
+ </Link>
65
+ }
66
+ >
67
+ {isLoading ? (
68
+ <div className="p-12 flex justify-center">
69
+ <LoadingSpinner />
70
+ </div>
71
+ ) : objectives.length === 0 ? (
72
+ <EmptyState
73
+ title="No SLOs configured"
74
+ description="Define Service Level Objectives to track reliability."
75
+ />
76
+ ) : (
77
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-6">
78
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
79
+ {objectives.map((item) => (
80
+ <Link
81
+ key={item.objective.id}
82
+ to={resolveRoute(sloRoutes.routes.detail, {
83
+ sloId: item.objective.id,
84
+ })}
85
+ className="block no-underline"
86
+ >
87
+ <Card className="transition-colors hover:border-primary/50">
88
+ <CardHeader className="pb-3">
89
+ <div className="flex items-center justify-between">
90
+ <CardTitle className="text-sm font-medium">
91
+ {systemNameMap.get(item.objective.systemId) ??
92
+ item.objective.systemId}
93
+ </CardTitle>
94
+ {item.status.isBreaching ? (
95
+ <Badge variant="destructive">Breaching</Badge>
96
+ ) : item.status.hasOpenDowntime ? (
97
+ <Badge variant="warning">Degraded</Badge>
98
+ ) : item.status.errorBudgetRemainingPercent <= 20 ? (
99
+ <Badge variant="warning">At Risk</Badge>
100
+ ) : (
101
+ <Badge variant="success">Healthy</Badge>
102
+ )}
103
+ </div>
104
+ </CardHeader>
105
+ <CardContent>
106
+ <div className="text-lg font-semibold mb-2">
107
+ {item.objective.target}% / {item.objective.windowDays}d
108
+ </div>
109
+ <ErrorBudgetBar
110
+ consumedPercent={
111
+ 100 - item.status.errorBudgetRemainingPercent
112
+ }
113
+ warningThreshold={
114
+ item.objective.burnRateThresholds.warningPercent
115
+ }
116
+ criticalThreshold={
117
+ item.objective.burnRateThresholds.criticalPercent
118
+ }
119
+ />
120
+ <div className="flex items-center justify-between mt-2 text-xs text-muted-foreground">
121
+ <span>
122
+ {item.status.currentAvailability?.toFixed(3) ?? "—"}%
123
+ </span>
124
+ <BurnRateIndicator burnRate={item.status.burnRate} />
125
+ </div>
126
+ </CardContent>
127
+ </Card>
128
+ </Link>
129
+ ))}
130
+ </div>
131
+ <div className="lg:sticky lg:top-4 self-start">
132
+ <MilestoneFeed />
133
+ </div>
134
+ </div>
135
+ )}
136
+ </PageLayout>
137
+ );
138
+ };
139
+
140
+ export const SloOverviewPage = wrapInSuspense(SloOverviewPageContent);
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/frontend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }