@checkstack/maintenance-frontend 0.0.2

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,295 @@
1
+ import React, { useEffect, useState, useMemo, useCallback } from "react";
2
+ import {
3
+ useParams,
4
+ Link,
5
+ useNavigate,
6
+ useSearchParams,
7
+ } from "react-router-dom";
8
+ import {
9
+ useApi,
10
+ rpcApiRef,
11
+ wrapInSuspense,
12
+ permissionApiRef,
13
+ } from "@checkstack/frontend-api";
14
+ import { resolveRoute } from "@checkstack/common";
15
+ import { maintenanceApiRef } from "../api";
16
+ import { maintenanceRoutes } from "@checkstack/maintenance-common";
17
+ import type { MaintenanceDetail } from "@checkstack/maintenance-common";
18
+ import {
19
+ catalogRoutes,
20
+ CatalogApi,
21
+ type System,
22
+ } from "@checkstack/catalog-common";
23
+ import {
24
+ Card,
25
+ CardHeader,
26
+ CardTitle,
27
+ CardContent,
28
+ Badge,
29
+ LoadingSpinner,
30
+ EmptyState,
31
+ PageLayout,
32
+ BackLink,
33
+ Button,
34
+ StatusUpdateTimeline,
35
+ useToast,
36
+ } from "@checkstack/ui";
37
+ import {
38
+ Calendar,
39
+ Clock,
40
+ Wrench,
41
+ Server,
42
+ Plus,
43
+ MessageSquare,
44
+ CheckCircle2,
45
+ } from "lucide-react";
46
+ import { format } from "date-fns";
47
+ import { MaintenanceUpdateForm } from "../components/MaintenanceUpdateForm";
48
+ import { getMaintenanceStatusBadge } from "../utils/badges";
49
+
50
+ const MaintenanceDetailPageContent: React.FC = () => {
51
+ const { maintenanceId } = useParams<{ maintenanceId: string }>();
52
+ const navigate = useNavigate();
53
+ const [searchParams] = useSearchParams();
54
+ const api = useApi(maintenanceApiRef);
55
+ const rpcApi = useApi(rpcApiRef);
56
+ const permissionApi = useApi(permissionApiRef);
57
+ const toast = useToast();
58
+
59
+ const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
60
+
61
+ const { allowed: canManage } = permissionApi.useResourcePermission(
62
+ "maintenance",
63
+ "manage"
64
+ );
65
+
66
+ const [maintenance, setMaintenance] = useState<MaintenanceDetail>();
67
+ const [systems, setSystems] = useState<System[]>([]);
68
+ const [loading, setLoading] = useState(true);
69
+ const [showUpdateForm, setShowUpdateForm] = useState(false);
70
+
71
+ const loadData = useCallback(async () => {
72
+ if (!maintenanceId) return;
73
+
74
+ setLoading(true);
75
+ try {
76
+ const [maintenanceData, systemList] = await Promise.all([
77
+ api.getMaintenance({ id: maintenanceId }),
78
+ catalogApi.getSystems(),
79
+ ]);
80
+ setMaintenance(maintenanceData ?? undefined);
81
+ setSystems(systemList);
82
+ } catch (error) {
83
+ console.error("Failed to load maintenance details:", error);
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ }, [maintenanceId, api, catalogApi]);
88
+
89
+ useEffect(() => {
90
+ loadData();
91
+ }, [loadData]);
92
+
93
+ const handleUpdateSuccess = () => {
94
+ setShowUpdateForm(false);
95
+ loadData();
96
+ };
97
+
98
+ const handleComplete = async () => {
99
+ if (!maintenanceId) return;
100
+
101
+ try {
102
+ await api.closeMaintenance({ id: maintenanceId });
103
+ toast.success("Maintenance completed");
104
+ await loadData();
105
+ } catch (error) {
106
+ const message =
107
+ error instanceof Error ? error.message : "Failed to complete";
108
+ toast.error(message);
109
+ }
110
+ };
111
+
112
+ const getSystemName = (systemId: string): string => {
113
+ return systems.find((s) => s.id === systemId)?.name ?? systemId;
114
+ };
115
+
116
+ if (!maintenanceId) {
117
+ return (
118
+ <EmptyState
119
+ title="Maintenance not found"
120
+ description="No maintenance ID was provided."
121
+ />
122
+ );
123
+ }
124
+
125
+ if (loading) {
126
+ return (
127
+ <div className="p-12 flex justify-center">
128
+ <LoadingSpinner />
129
+ </div>
130
+ );
131
+ }
132
+
133
+ if (!maintenance) {
134
+ return (
135
+ <EmptyState
136
+ title="Maintenance not found"
137
+ description="The requested maintenance could not be found."
138
+ />
139
+ );
140
+ }
141
+
142
+ // Use 'from' query param for back navigation, fallback to first affected system
143
+ const sourceSystemId = searchParams.get("from") ?? maintenance.systemIds[0];
144
+ const canComplete =
145
+ canManage &&
146
+ maintenance.status !== "completed" &&
147
+ maintenance.status !== "cancelled";
148
+
149
+ return (
150
+ <PageLayout
151
+ title={maintenance.title}
152
+ subtitle="Maintenance details and status history"
153
+ loading={false}
154
+ allowed={true}
155
+ actions={
156
+ sourceSystemId ? (
157
+ <BackLink
158
+ onClick={() =>
159
+ navigate(
160
+ resolveRoute(maintenanceRoutes.routes.systemHistory, {
161
+ systemId: sourceSystemId,
162
+ })
163
+ )
164
+ }
165
+ >
166
+ Back to History
167
+ </BackLink>
168
+ ) : undefined
169
+ }
170
+ >
171
+ <div className="space-y-6">
172
+ {/* Maintenance Info Card */}
173
+ <Card>
174
+ <CardHeader className="border-b border-border">
175
+ <div className="flex items-center justify-between">
176
+ <div className="flex items-center gap-2">
177
+ <Wrench className="h-5 w-5 text-muted-foreground" />
178
+ <CardTitle>Maintenance Details</CardTitle>
179
+ </div>
180
+ <div className="flex items-center gap-2">
181
+ {getMaintenanceStatusBadge(maintenance.status)}
182
+ {canComplete && (
183
+ <Button variant="outline" size="sm" onClick={handleComplete}>
184
+ <CheckCircle2 className="h-4 w-4 mr-1" />
185
+ Complete
186
+ </Button>
187
+ )}
188
+ </div>
189
+ </div>
190
+ </CardHeader>
191
+ <CardContent className="p-6 space-y-4">
192
+ {maintenance.description && (
193
+ <div>
194
+ <h4 className="text-sm font-medium text-muted-foreground mb-1">
195
+ Description
196
+ </h4>
197
+ <p className="text-foreground">{maintenance.description}</p>
198
+ </div>
199
+ )}
200
+
201
+ <div className="grid grid-cols-2 gap-4">
202
+ <div>
203
+ <h4 className="text-sm font-medium text-muted-foreground mb-1">
204
+ Start Time
205
+ </h4>
206
+ <div className="flex items-center gap-2 text-foreground">
207
+ <Calendar className="h-4 w-4" />
208
+ <span>{format(new Date(maintenance.startAt), "PPpp")}</span>
209
+ </div>
210
+ </div>
211
+ <div>
212
+ <h4 className="text-sm font-medium text-muted-foreground mb-1">
213
+ End Time
214
+ </h4>
215
+ <div className="flex items-center gap-2 text-foreground">
216
+ <Clock className="h-4 w-4" />
217
+ <span>{format(new Date(maintenance.endAt), "PPpp")}</span>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <div>
223
+ <h4 className="text-sm font-medium text-muted-foreground mb-2">
224
+ Affected Systems
225
+ </h4>
226
+ <div className="flex flex-wrap gap-2">
227
+ {maintenance.systemIds.map((systemId) => (
228
+ <Link
229
+ key={systemId}
230
+ to={resolveRoute(catalogRoutes.routes.systemDetail, {
231
+ systemId,
232
+ })}
233
+ >
234
+ <Badge
235
+ variant="outline"
236
+ className="cursor-pointer hover:bg-muted"
237
+ >
238
+ <Server className="h-3 w-3 mr-1" />
239
+ {getSystemName(systemId)}
240
+ </Badge>
241
+ </Link>
242
+ ))}
243
+ </div>
244
+ </div>
245
+ </CardContent>
246
+ </Card>
247
+
248
+ {/* Status Updates Timeline */}
249
+ <Card>
250
+ <CardHeader className="border-b border-border">
251
+ <div className="flex items-center justify-between">
252
+ <div className="flex items-center gap-2">
253
+ <MessageSquare className="h-5 w-5 text-muted-foreground" />
254
+ <CardTitle>Status Updates</CardTitle>
255
+ </div>
256
+ {canManage && !showUpdateForm && (
257
+ <Button
258
+ variant="outline"
259
+ size="sm"
260
+ onClick={() => setShowUpdateForm(true)}
261
+ >
262
+ <Plus className="h-4 w-4 mr-1" />
263
+ Add Update
264
+ </Button>
265
+ )}
266
+ </div>
267
+ </CardHeader>
268
+ <CardContent className="p-6">
269
+ {/* Add Update Form */}
270
+ {showUpdateForm && (
271
+ <div className="mb-6">
272
+ <MaintenanceUpdateForm
273
+ maintenanceId={maintenanceId}
274
+ onSuccess={handleUpdateSuccess}
275
+ onCancel={() => setShowUpdateForm(false)}
276
+ />
277
+ </div>
278
+ )}
279
+
280
+ <StatusUpdateTimeline
281
+ updates={maintenance.updates}
282
+ renderStatusBadge={getMaintenanceStatusBadge}
283
+ emptyTitle="No status updates"
284
+ emptyDescription="No status updates have been posted for this maintenance."
285
+ />
286
+ </CardContent>
287
+ </Card>
288
+ </div>
289
+ </PageLayout>
290
+ );
291
+ };
292
+
293
+ export const MaintenanceDetailPage = wrapInSuspense(
294
+ MaintenanceDetailPageContent
295
+ );
@@ -0,0 +1,198 @@
1
+ import React, { useEffect, useState, useMemo } from "react";
2
+ import { useParams, useNavigate } from "react-router-dom";
3
+ import {
4
+ useApi,
5
+ rpcApiRef,
6
+ wrapInSuspense,
7
+ } from "@checkstack/frontend-api";
8
+ import { resolveRoute } from "@checkstack/common";
9
+ import { maintenanceApiRef } from "../api";
10
+ import { maintenanceRoutes } from "@checkstack/maintenance-common";
11
+ import type {
12
+ MaintenanceWithSystems,
13
+ MaintenanceStatus,
14
+ } from "@checkstack/maintenance-common";
15
+ import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
16
+ import {
17
+ Card,
18
+ CardHeader,
19
+ CardTitle,
20
+ CardContent,
21
+ Badge,
22
+ LoadingSpinner,
23
+ EmptyState,
24
+ Table,
25
+ TableHeader,
26
+ TableRow,
27
+ TableHead,
28
+ TableBody,
29
+ TableCell,
30
+ PageLayout,
31
+ BackLink,
32
+ } from "@checkstack/ui";
33
+ import { Calendar, Clock, History } from "lucide-react";
34
+ import { format } from "date-fns";
35
+
36
+ const SystemMaintenanceHistoryPageContent: React.FC = () => {
37
+ const { systemId } = useParams<{ systemId: string }>();
38
+ const navigate = useNavigate();
39
+ const api = useApi(maintenanceApiRef);
40
+ const rpcApi = useApi(rpcApiRef);
41
+
42
+ const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
43
+
44
+ const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
45
+ []
46
+ );
47
+ const [systemName, setSystemName] = useState<string>("");
48
+ const [loading, setLoading] = useState(true);
49
+
50
+ useEffect(() => {
51
+ if (!systemId) return;
52
+
53
+ const loadData = async () => {
54
+ setLoading(true);
55
+ try {
56
+ const [maintenanceList, systemList] = await Promise.all([
57
+ api.listMaintenances({ systemId }),
58
+ catalogApi.getSystems(),
59
+ ]);
60
+ setMaintenances(maintenanceList);
61
+ const system = systemList.find((s) => s.id === systemId);
62
+ setSystemName(system?.name ?? "Unknown System");
63
+ } catch (error) {
64
+ console.error("Failed to load maintenance history:", error);
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
69
+
70
+ loadData();
71
+ }, [systemId, api, catalogApi]);
72
+
73
+ const getStatusBadge = (status: MaintenanceStatus) => {
74
+ switch (status) {
75
+ case "in_progress": {
76
+ return <Badge variant="warning">In Progress</Badge>;
77
+ }
78
+ case "scheduled": {
79
+ return <Badge variant="info">Scheduled</Badge>;
80
+ }
81
+ case "completed": {
82
+ return <Badge variant="success">Completed</Badge>;
83
+ }
84
+ case "cancelled": {
85
+ return <Badge variant="secondary">Cancelled</Badge>;
86
+ }
87
+ default: {
88
+ return <Badge>{status}</Badge>;
89
+ }
90
+ }
91
+ };
92
+
93
+ if (!systemId) {
94
+ return (
95
+ <EmptyState
96
+ title="System not found"
97
+ description="No system ID was provided."
98
+ />
99
+ );
100
+ }
101
+
102
+ return (
103
+ <PageLayout
104
+ title={`Maintenance History: ${systemName}`}
105
+ subtitle="All past and scheduled maintenances for this system"
106
+ loading={loading}
107
+ allowed={true}
108
+ actions={
109
+ <BackLink
110
+ onClick={() =>
111
+ navigate(
112
+ resolveRoute(catalogRoutes.routes.systemDetail, { systemId })
113
+ )
114
+ }
115
+ >
116
+ Back to System
117
+ </BackLink>
118
+ }
119
+ >
120
+ <Card>
121
+ <CardHeader className="border-b border-border">
122
+ <div className="flex items-center gap-2">
123
+ <History className="h-5 w-5 text-muted-foreground" />
124
+ <CardTitle>Maintenance History</CardTitle>
125
+ </div>
126
+ </CardHeader>
127
+ <CardContent className="p-0">
128
+ {loading ? (
129
+ <div className="p-12 flex justify-center">
130
+ <LoadingSpinner />
131
+ </div>
132
+ ) : maintenances.length === 0 ? (
133
+ <EmptyState
134
+ title="No maintenances found"
135
+ description="There are no recorded maintenances for this system."
136
+ />
137
+ ) : (
138
+ <Table>
139
+ <TableHeader>
140
+ <TableRow>
141
+ <TableHead>Title</TableHead>
142
+ <TableHead>Status</TableHead>
143
+ <TableHead>Start</TableHead>
144
+ <TableHead>End</TableHead>
145
+ </TableRow>
146
+ </TableHeader>
147
+ <TableBody>
148
+ {maintenances.map((m) => (
149
+ <TableRow
150
+ key={m.id}
151
+ className="cursor-pointer hover:bg-muted/50"
152
+ onClick={() =>
153
+ navigate(
154
+ `${resolveRoute(maintenanceRoutes.routes.detail, {
155
+ maintenanceId: m.id,
156
+ })}?from=${systemId}`
157
+ )
158
+ }
159
+ >
160
+ <TableCell>
161
+ <p className="font-medium text-foreground">{m.title}</p>
162
+ {m.description && (
163
+ <p className="text-sm text-muted-foreground truncate max-w-xs">
164
+ {m.description}
165
+ </p>
166
+ )}
167
+ </TableCell>
168
+ <TableCell>{getStatusBadge(m.status)}</TableCell>
169
+ <TableCell>
170
+ <div className="flex items-center gap-1 text-sm text-muted-foreground">
171
+ <Calendar className="h-3 w-3" />
172
+ <span>
173
+ {format(new Date(m.startAt), "MMM d, yyyy HH:mm")}
174
+ </span>
175
+ </div>
176
+ </TableCell>
177
+ <TableCell>
178
+ <div className="flex items-center gap-1 text-sm text-muted-foreground">
179
+ <Clock className="h-3 w-3" />
180
+ <span>
181
+ {format(new Date(m.endAt), "MMM d, yyyy HH:mm")}
182
+ </span>
183
+ </div>
184
+ </TableCell>
185
+ </TableRow>
186
+ ))}
187
+ </TableBody>
188
+ </Table>
189
+ )}
190
+ </CardContent>
191
+ </Card>
192
+ </PageLayout>
193
+ );
194
+ };
195
+
196
+ export const SystemMaintenanceHistoryPage = wrapInSuspense(
197
+ SystemMaintenanceHistoryPageContent
198
+ );
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { Badge } from "@checkstack/ui";
3
+ import type { MaintenanceStatus } from "@checkstack/maintenance-common";
4
+
5
+ /**
6
+ * Returns a styled badge for the given maintenance status.
7
+ * Use this utility to ensure consistent status badge styling across the plugin.
8
+ */
9
+ export function getMaintenanceStatusBadge(
10
+ status: MaintenanceStatus
11
+ ): React.ReactNode {
12
+ switch (status) {
13
+ case "in_progress": {
14
+ return <Badge variant="warning">In Progress</Badge>;
15
+ }
16
+ case "scheduled": {
17
+ return <Badge variant="info">Scheduled</Badge>;
18
+ }
19
+ case "completed": {
20
+ return <Badge variant="success">Completed</Badge>;
21
+ }
22
+ case "cancelled": {
23
+ return <Badge variant="secondary">Cancelled</Badge>;
24
+ }
25
+ default: {
26
+ return <Badge>{status}</Badge>;
27
+ }
28
+ }
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/frontend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }