@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.
- package/CHANGELOG.md +96 -0
- package/package.json +29 -0
- package/src/api.ts +10 -0
- package/src/components/MaintenanceEditor.tsx +317 -0
- package/src/components/MaintenanceMenuItems.tsx +33 -0
- package/src/components/MaintenanceUpdateForm.tsx +123 -0
- package/src/components/SystemMaintenanceBadge.tsx +49 -0
- package/src/components/SystemMaintenancePanel.tsx +176 -0
- package/src/index.tsx +71 -0
- package/src/pages/MaintenanceConfigPage.tsx +347 -0
- package/src/pages/MaintenanceDetailPage.tsx +295 -0
- package/src/pages/SystemMaintenanceHistoryPage.tsx +198 -0
- package/src/utils/badges.tsx +29 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @checkstack/maintenance-frontend
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
|
|
8
|
+
- Updated dependencies [d20d274]
|
|
9
|
+
- @checkstack/catalog-common@0.0.2
|
|
10
|
+
- @checkstack/common@0.0.2
|
|
11
|
+
- @checkstack/frontend-api@0.0.2
|
|
12
|
+
- @checkstack/maintenance-common@0.0.2
|
|
13
|
+
- @checkstack/signal-frontend@0.0.2
|
|
14
|
+
- @checkstack/ui@0.0.2
|
|
15
|
+
|
|
16
|
+
## 0.1.2
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 97a6a23: Improve incident and maintenance detail page layout consistency and navigation
|
|
21
|
+
|
|
22
|
+
**Layout consistency:**
|
|
23
|
+
|
|
24
|
+
- Incident detail page now matches maintenance detail page structure
|
|
25
|
+
- Both use PageLayout wrapper with consistent card layout
|
|
26
|
+
- Affected systems moved into main details card with server icons
|
|
27
|
+
- Standardized padding, spacing, and description/date formatting
|
|
28
|
+
|
|
29
|
+
**Back navigation with system context:**
|
|
30
|
+
|
|
31
|
+
- Detail pages now track source system via `?from=systemId` query parameter
|
|
32
|
+
- "Back to History" navigates to the correct system's history page
|
|
33
|
+
- Works when navigating from system panels, history pages, or system detail page
|
|
34
|
+
- Falls back to first affected system if no query param present
|
|
35
|
+
|
|
36
|
+
- 32ea706: ### User Menu Loading State Fix
|
|
37
|
+
|
|
38
|
+
Fixed user menu items "popping in" one after another due to independent async permission checks.
|
|
39
|
+
|
|
40
|
+
**Changes:**
|
|
41
|
+
|
|
42
|
+
- Added `UserMenuItemsContext` interface with `permissions` and `hasCredentialAccount` to `@checkstack/frontend-api`
|
|
43
|
+
- `LoginNavbarAction` now pre-fetches all permissions and credential account info before rendering the menu
|
|
44
|
+
- All user menu item components now use the passed context for synchronous permission checks instead of async hooks
|
|
45
|
+
- Uses `qualifyPermissionId` helper for fully-qualified permission IDs
|
|
46
|
+
|
|
47
|
+
**Result:** All menu items appear simultaneously when the user menu opens.
|
|
48
|
+
|
|
49
|
+
- Updated dependencies [52231ef]
|
|
50
|
+
- Updated dependencies [b0124ef]
|
|
51
|
+
- Updated dependencies [54cc787]
|
|
52
|
+
- Updated dependencies [a65e002]
|
|
53
|
+
- Updated dependencies [ae33df2]
|
|
54
|
+
- Updated dependencies [32ea706]
|
|
55
|
+
- @checkstack/ui@0.1.2
|
|
56
|
+
- @checkstack/common@0.2.0
|
|
57
|
+
- @checkstack/frontend-api@0.1.0
|
|
58
|
+
- @checkstack/catalog-common@0.1.2
|
|
59
|
+
- @checkstack/maintenance-common@0.1.2
|
|
60
|
+
- @checkstack/signal-frontend@0.1.1
|
|
61
|
+
|
|
62
|
+
## 0.1.1
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [0f8cc7d]
|
|
67
|
+
- @checkstack/frontend-api@0.0.3
|
|
68
|
+
- @checkstack/catalog-common@0.1.1
|
|
69
|
+
- @checkstack/maintenance-common@0.1.1
|
|
70
|
+
- @checkstack/ui@0.1.1
|
|
71
|
+
|
|
72
|
+
## 0.1.0
|
|
73
|
+
|
|
74
|
+
### Minor Changes
|
|
75
|
+
|
|
76
|
+
- eff5b4e: Add standalone maintenance scheduling plugin
|
|
77
|
+
|
|
78
|
+
- New `@checkstack/maintenance-common` package with Zod schemas, permissions, oRPC contract, and extension slots
|
|
79
|
+
- New `@checkstack/maintenance-backend` package with Drizzle schema, service, and oRPC router
|
|
80
|
+
- New `@checkstack/maintenance-frontend` package with admin page and system detail panel
|
|
81
|
+
- Shared `DateTimePicker` component added to `@checkstack/ui`
|
|
82
|
+
- Database migrations for maintenances, maintenance_systems, and maintenance_updates tables
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- Updated dependencies [eff5b4e]
|
|
87
|
+
- Updated dependencies [ffc28f6]
|
|
88
|
+
- Updated dependencies [4dd644d]
|
|
89
|
+
- Updated dependencies [b55fae6]
|
|
90
|
+
- Updated dependencies [b354ab3]
|
|
91
|
+
- @checkstack/maintenance-common@0.1.0
|
|
92
|
+
- @checkstack/ui@0.1.0
|
|
93
|
+
- @checkstack/common@0.1.0
|
|
94
|
+
- @checkstack/catalog-common@0.1.0
|
|
95
|
+
- @checkstack/signal-frontend@0.1.0
|
|
96
|
+
- @checkstack/frontend-api@0.0.2
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/maintenance-frontend",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "bun run lint:code",
|
|
9
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@checkstack/catalog-common": "workspace:*",
|
|
13
|
+
"@checkstack/common": "workspace:*",
|
|
14
|
+
"@checkstack/frontend-api": "workspace:*",
|
|
15
|
+
"@checkstack/maintenance-common": "workspace:*",
|
|
16
|
+
"@checkstack/signal-frontend": "workspace:*",
|
|
17
|
+
"@checkstack/ui": "workspace:*",
|
|
18
|
+
"date-fns": "^4.1.0",
|
|
19
|
+
"lucide-react": "^0.344.0",
|
|
20
|
+
"react": "^18.2.0",
|
|
21
|
+
"react-router-dom": "^6.20.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.0.0",
|
|
25
|
+
"@types/react": "^18.2.0",
|
|
26
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
27
|
+
"@checkstack/scripts": "workspace:*"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createApiRef } from "@checkstack/frontend-api";
|
|
2
|
+
import { MaintenanceApi } from "@checkstack/maintenance-common";
|
|
3
|
+
import type { InferClient } from "@checkstack/common";
|
|
4
|
+
|
|
5
|
+
// MaintenanceApiClient type inferred from the client definition
|
|
6
|
+
export type MaintenanceApiClient = InferClient<typeof MaintenanceApi>;
|
|
7
|
+
|
|
8
|
+
export const maintenanceApiRef = createApiRef<MaintenanceApiClient>(
|
|
9
|
+
"plugin.maintenance.api"
|
|
10
|
+
);
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { useApi } from "@checkstack/frontend-api";
|
|
3
|
+
import { maintenanceApiRef } from "../api";
|
|
4
|
+
import type {
|
|
5
|
+
MaintenanceWithSystems,
|
|
6
|
+
MaintenanceUpdate,
|
|
7
|
+
} from "@checkstack/maintenance-common";
|
|
8
|
+
import type { System } from "@checkstack/catalog-common";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
Button,
|
|
16
|
+
Input,
|
|
17
|
+
Label,
|
|
18
|
+
Textarea,
|
|
19
|
+
Checkbox,
|
|
20
|
+
useToast,
|
|
21
|
+
DateTimePicker,
|
|
22
|
+
StatusUpdateTimeline,
|
|
23
|
+
} from "@checkstack/ui";
|
|
24
|
+
import { Plus, MessageSquare, Loader2, AlertCircle } from "lucide-react";
|
|
25
|
+
import { MaintenanceUpdateForm } from "./MaintenanceUpdateForm";
|
|
26
|
+
import { getMaintenanceStatusBadge } from "../utils/badges";
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
open: boolean;
|
|
30
|
+
onOpenChange: (open: boolean) => void;
|
|
31
|
+
maintenance?: MaintenanceWithSystems;
|
|
32
|
+
systems: System[];
|
|
33
|
+
onSave: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const MaintenanceEditor: React.FC<Props> = ({
|
|
37
|
+
open,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
maintenance,
|
|
40
|
+
systems,
|
|
41
|
+
onSave,
|
|
42
|
+
}) => {
|
|
43
|
+
const api = useApi(maintenanceApiRef);
|
|
44
|
+
const toast = useToast();
|
|
45
|
+
|
|
46
|
+
// Maintenance fields
|
|
47
|
+
const [title, setTitle] = useState("");
|
|
48
|
+
const [description, setDescription] = useState("");
|
|
49
|
+
const [startAt, setStartAt] = useState<Date>(new Date());
|
|
50
|
+
const [endAt, setEndAt] = useState<Date>(new Date());
|
|
51
|
+
const [selectedSystemIds, setSelectedSystemIds] = useState<Set<string>>(
|
|
52
|
+
new Set()
|
|
53
|
+
);
|
|
54
|
+
const [saving, setSaving] = useState(false);
|
|
55
|
+
|
|
56
|
+
// Status update fields
|
|
57
|
+
const [updates, setUpdates] = useState<MaintenanceUpdate[]>([]);
|
|
58
|
+
const [loadingUpdates, setLoadingUpdates] = useState(false);
|
|
59
|
+
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
60
|
+
|
|
61
|
+
const loadMaintenanceDetails = useCallback(
|
|
62
|
+
async (id: string) => {
|
|
63
|
+
setLoadingUpdates(true);
|
|
64
|
+
try {
|
|
65
|
+
const detail = await api.getMaintenance({ id });
|
|
66
|
+
if (detail) {
|
|
67
|
+
setUpdates(detail.updates);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Failed to load maintenance details:", error);
|
|
71
|
+
} finally {
|
|
72
|
+
setLoadingUpdates(false);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
[api]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Reset form when maintenance changes
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (maintenance) {
|
|
81
|
+
setTitle(maintenance.title);
|
|
82
|
+
setDescription(maintenance.description ?? "");
|
|
83
|
+
setStartAt(new Date(maintenance.startAt));
|
|
84
|
+
setEndAt(new Date(maintenance.endAt));
|
|
85
|
+
setSelectedSystemIds(new Set(maintenance.systemIds));
|
|
86
|
+
// Load full maintenance with updates
|
|
87
|
+
loadMaintenanceDetails(maintenance.id);
|
|
88
|
+
} else {
|
|
89
|
+
// Default to 1 hour from now to 2 hours from now
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const start = new Date(now.getTime() + 60 * 60 * 1000);
|
|
92
|
+
const end = new Date(now.getTime() + 2 * 60 * 60 * 1000);
|
|
93
|
+
setTitle("");
|
|
94
|
+
setDescription("");
|
|
95
|
+
setStartAt(start);
|
|
96
|
+
setEndAt(end);
|
|
97
|
+
setSelectedSystemIds(new Set());
|
|
98
|
+
setUpdates([]);
|
|
99
|
+
setShowUpdateForm(false);
|
|
100
|
+
}
|
|
101
|
+
}, [maintenance, open, loadMaintenanceDetails]);
|
|
102
|
+
|
|
103
|
+
const handleSystemToggle = (systemId: string) => {
|
|
104
|
+
setSelectedSystemIds((prev) => {
|
|
105
|
+
const next = new Set(prev);
|
|
106
|
+
if (next.has(systemId)) {
|
|
107
|
+
next.delete(systemId);
|
|
108
|
+
} else {
|
|
109
|
+
next.add(systemId);
|
|
110
|
+
}
|
|
111
|
+
return next;
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSubmit = async () => {
|
|
116
|
+
if (!title.trim()) {
|
|
117
|
+
toast.error("Title is required");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (selectedSystemIds.size === 0) {
|
|
121
|
+
toast.error("At least one system must be selected");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (endAt <= startAt) {
|
|
125
|
+
toast.error("End date must be after start date");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setSaving(true);
|
|
130
|
+
try {
|
|
131
|
+
if (maintenance) {
|
|
132
|
+
await api.updateMaintenance({
|
|
133
|
+
id: maintenance.id,
|
|
134
|
+
title,
|
|
135
|
+
description: description || undefined,
|
|
136
|
+
startAt,
|
|
137
|
+
endAt,
|
|
138
|
+
systemIds: [...selectedSystemIds],
|
|
139
|
+
});
|
|
140
|
+
toast.success("Maintenance updated");
|
|
141
|
+
} else {
|
|
142
|
+
await api.createMaintenance({
|
|
143
|
+
title,
|
|
144
|
+
description,
|
|
145
|
+
startAt,
|
|
146
|
+
endAt,
|
|
147
|
+
systemIds: [...selectedSystemIds],
|
|
148
|
+
});
|
|
149
|
+
toast.success("Maintenance created");
|
|
150
|
+
}
|
|
151
|
+
onSave();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : "Failed to save";
|
|
154
|
+
toast.error(message);
|
|
155
|
+
} finally {
|
|
156
|
+
setSaving(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleUpdateSuccess = () => {
|
|
161
|
+
if (maintenance) {
|
|
162
|
+
loadMaintenanceDetails(maintenance.id);
|
|
163
|
+
}
|
|
164
|
+
setShowUpdateForm(false);
|
|
165
|
+
// Notify parent to refresh list (status may have changed)
|
|
166
|
+
onSave();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
171
|
+
<DialogContent size="xl">
|
|
172
|
+
<DialogHeader>
|
|
173
|
+
<DialogTitle>
|
|
174
|
+
{maintenance ? "Edit Maintenance" : "Create Maintenance"}
|
|
175
|
+
</DialogTitle>
|
|
176
|
+
</DialogHeader>
|
|
177
|
+
|
|
178
|
+
<div className="grid gap-6 py-4 max-h-[70vh] overflow-y-auto">
|
|
179
|
+
{/* Basic Info Section */}
|
|
180
|
+
<div className="grid gap-4">
|
|
181
|
+
<div className="grid gap-2">
|
|
182
|
+
<Label htmlFor="title">Title</Label>
|
|
183
|
+
<Input
|
|
184
|
+
id="title"
|
|
185
|
+
value={title}
|
|
186
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
187
|
+
placeholder="Database maintenance"
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="grid gap-2">
|
|
192
|
+
<Label htmlFor="description">Description</Label>
|
|
193
|
+
<Textarea
|
|
194
|
+
id="description"
|
|
195
|
+
value={description}
|
|
196
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
197
|
+
placeholder="Details about the maintenance..."
|
|
198
|
+
rows={3}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div className="grid grid-cols-2 gap-4">
|
|
203
|
+
<div className="grid gap-2">
|
|
204
|
+
<Label>Start Date & Time</Label>
|
|
205
|
+
<DateTimePicker value={startAt} onChange={setStartAt} />
|
|
206
|
+
</div>
|
|
207
|
+
<div className="grid gap-2">
|
|
208
|
+
<Label>End Date & Time</Label>
|
|
209
|
+
<DateTimePicker
|
|
210
|
+
value={endAt}
|
|
211
|
+
onChange={setEndAt}
|
|
212
|
+
minDate={startAt}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="grid gap-2">
|
|
218
|
+
<Label>Affected Systems</Label>
|
|
219
|
+
<div className="max-h-36 overflow-y-auto border rounded-md p-3 space-y-2">
|
|
220
|
+
{systems.length === 0 ? (
|
|
221
|
+
<p className="text-sm text-muted-foreground">
|
|
222
|
+
No systems available
|
|
223
|
+
</p>
|
|
224
|
+
) : (
|
|
225
|
+
systems.map((system) => (
|
|
226
|
+
<div
|
|
227
|
+
key={system.id}
|
|
228
|
+
className="flex items-center space-x-2 p-2 rounded hover:bg-accent cursor-pointer"
|
|
229
|
+
onClick={() => handleSystemToggle(system.id)}
|
|
230
|
+
>
|
|
231
|
+
<Checkbox
|
|
232
|
+
id={`system-${system.id}`}
|
|
233
|
+
checked={selectedSystemIds.has(system.id)}
|
|
234
|
+
/>
|
|
235
|
+
<Label
|
|
236
|
+
htmlFor={`system-${system.id}`}
|
|
237
|
+
className="cursor-pointer flex-1"
|
|
238
|
+
>
|
|
239
|
+
{system.name}
|
|
240
|
+
</Label>
|
|
241
|
+
</div>
|
|
242
|
+
))
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
<p className="text-xs text-muted-foreground">
|
|
246
|
+
{selectedSystemIds.size} system(s) selected
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Status Updates Section - Only show when editing */}
|
|
252
|
+
{maintenance && (
|
|
253
|
+
<div className="border-t pt-4">
|
|
254
|
+
<div className="flex items-center justify-between mb-4">
|
|
255
|
+
<div className="flex items-center gap-2">
|
|
256
|
+
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
|
257
|
+
<Label className="text-base font-medium">
|
|
258
|
+
Status Updates
|
|
259
|
+
</Label>
|
|
260
|
+
</div>
|
|
261
|
+
{!showUpdateForm && (
|
|
262
|
+
<Button
|
|
263
|
+
variant="outline"
|
|
264
|
+
size="sm"
|
|
265
|
+
onClick={() => setShowUpdateForm(true)}
|
|
266
|
+
>
|
|
267
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
268
|
+
Add Update
|
|
269
|
+
</Button>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Add Update Form */}
|
|
274
|
+
{showUpdateForm && (
|
|
275
|
+
<div className="mb-4">
|
|
276
|
+
<MaintenanceUpdateForm
|
|
277
|
+
maintenanceId={maintenance.id}
|
|
278
|
+
onSuccess={handleUpdateSuccess}
|
|
279
|
+
onCancel={() => setShowUpdateForm(false)}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* Updates List */}
|
|
285
|
+
{loadingUpdates ? (
|
|
286
|
+
<div className="flex justify-center py-4">
|
|
287
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
288
|
+
</div>
|
|
289
|
+
) : updates.length === 0 ? (
|
|
290
|
+
<div className="flex flex-col items-center py-6 text-muted-foreground">
|
|
291
|
+
<AlertCircle className="h-8 w-8 mb-2" />
|
|
292
|
+
<p className="text-sm">No status updates yet</p>
|
|
293
|
+
</div>
|
|
294
|
+
) : (
|
|
295
|
+
<StatusUpdateTimeline
|
|
296
|
+
updates={updates}
|
|
297
|
+
renderStatusBadge={getMaintenanceStatusBadge}
|
|
298
|
+
showTimeline={false}
|
|
299
|
+
maxHeight="max-h-48"
|
|
300
|
+
/>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<DialogFooter>
|
|
307
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
308
|
+
Cancel
|
|
309
|
+
</Button>
|
|
310
|
+
<Button onClick={handleSubmit} disabled={saving}>
|
|
311
|
+
{saving ? "Saving..." : maintenance ? "Update" : "Create"}
|
|
312
|
+
</Button>
|
|
313
|
+
</DialogFooter>
|
|
314
|
+
</DialogContent>
|
|
315
|
+
</Dialog>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { Wrench } from "lucide-react";
|
|
4
|
+
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
|
+
import { DropdownMenuItem } from "@checkstack/ui";
|
|
6
|
+
import { qualifyPermissionId, resolveRoute } from "@checkstack/common";
|
|
7
|
+
import {
|
|
8
|
+
maintenanceRoutes,
|
|
9
|
+
permissions,
|
|
10
|
+
pluginMetadata,
|
|
11
|
+
} from "@checkstack/maintenance-common";
|
|
12
|
+
|
|
13
|
+
export const MaintenanceMenuItems = ({
|
|
14
|
+
permissions: userPerms,
|
|
15
|
+
}: UserMenuItemsContext) => {
|
|
16
|
+
const qualifiedId = qualifyPermissionId(
|
|
17
|
+
pluginMetadata,
|
|
18
|
+
permissions.maintenanceManage
|
|
19
|
+
);
|
|
20
|
+
const canManage = userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
21
|
+
|
|
22
|
+
if (!canManage) {
|
|
23
|
+
return <React.Fragment />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Link to={resolveRoute(maintenanceRoutes.routes.config)}>
|
|
28
|
+
<DropdownMenuItem icon={<Wrench className="w-4 h-4" />}>
|
|
29
|
+
Maintenances
|
|
30
|
+
</DropdownMenuItem>
|
|
31
|
+
</Link>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useApi } from "@checkstack/frontend-api";
|
|
3
|
+
import { maintenanceApiRef } from "../api";
|
|
4
|
+
import type { MaintenanceStatus } from "@checkstack/maintenance-common";
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Textarea,
|
|
8
|
+
Label,
|
|
9
|
+
Select,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
useToast,
|
|
15
|
+
} from "@checkstack/ui";
|
|
16
|
+
import { Loader2 } from "lucide-react";
|
|
17
|
+
|
|
18
|
+
interface MaintenanceUpdateFormProps {
|
|
19
|
+
maintenanceId: string;
|
|
20
|
+
onSuccess: () => void;
|
|
21
|
+
onCancel?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reusable form for adding status updates to a maintenance.
|
|
26
|
+
* Used in both MaintenanceDetailPage and MaintenanceEditor.
|
|
27
|
+
*/
|
|
28
|
+
export const MaintenanceUpdateForm: React.FC<MaintenanceUpdateFormProps> = ({
|
|
29
|
+
maintenanceId,
|
|
30
|
+
onSuccess,
|
|
31
|
+
onCancel,
|
|
32
|
+
}) => {
|
|
33
|
+
const api = useApi(maintenanceApiRef);
|
|
34
|
+
const toast = useToast();
|
|
35
|
+
|
|
36
|
+
const [message, setMessage] = useState("");
|
|
37
|
+
const [statusChange, setStatusChange] = useState<MaintenanceStatus | "">("");
|
|
38
|
+
const [isPosting, setIsPosting] = useState(false);
|
|
39
|
+
|
|
40
|
+
const handleSubmit = async () => {
|
|
41
|
+
if (!message.trim()) {
|
|
42
|
+
toast.error("Update message is required");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setIsPosting(true);
|
|
47
|
+
try {
|
|
48
|
+
await api.addUpdate({
|
|
49
|
+
maintenanceId,
|
|
50
|
+
message,
|
|
51
|
+
statusChange: statusChange || undefined,
|
|
52
|
+
});
|
|
53
|
+
toast.success("Update posted");
|
|
54
|
+
setMessage("");
|
|
55
|
+
setStatusChange("");
|
|
56
|
+
onSuccess();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const errorMessage =
|
|
59
|
+
error instanceof Error ? error.message : "Failed to post update";
|
|
60
|
+
toast.error(errorMessage);
|
|
61
|
+
} finally {
|
|
62
|
+
setIsPosting(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="p-4 bg-muted/30 rounded-lg border space-y-3">
|
|
68
|
+
<div className="grid gap-2">
|
|
69
|
+
<Label htmlFor="updateMessage">Update Message</Label>
|
|
70
|
+
<Textarea
|
|
71
|
+
id="updateMessage"
|
|
72
|
+
value={message}
|
|
73
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
74
|
+
placeholder="Describe the status update..."
|
|
75
|
+
rows={2}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="grid gap-2">
|
|
79
|
+
<Label>Change Status (Optional)</Label>
|
|
80
|
+
<Select
|
|
81
|
+
value={statusChange || "__keep_current__"}
|
|
82
|
+
onValueChange={(v) =>
|
|
83
|
+
setStatusChange(
|
|
84
|
+
v === "__keep_current__" ? "" : (v as MaintenanceStatus)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<SelectTrigger>
|
|
89
|
+
<SelectValue placeholder="Keep current status" />
|
|
90
|
+
</SelectTrigger>
|
|
91
|
+
<SelectContent>
|
|
92
|
+
<SelectItem value="__keep_current__">Keep Current</SelectItem>
|
|
93
|
+
<SelectItem value="scheduled">Scheduled</SelectItem>
|
|
94
|
+
<SelectItem value="in_progress">In Progress</SelectItem>
|
|
95
|
+
<SelectItem value="completed">Completed</SelectItem>
|
|
96
|
+
<SelectItem value="cancelled">Cancelled</SelectItem>
|
|
97
|
+
</SelectContent>
|
|
98
|
+
</Select>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="flex gap-2 justify-end">
|
|
101
|
+
{onCancel && (
|
|
102
|
+
<Button variant="outline" size="sm" onClick={onCancel}>
|
|
103
|
+
Cancel
|
|
104
|
+
</Button>
|
|
105
|
+
)}
|
|
106
|
+
<Button
|
|
107
|
+
size="sm"
|
|
108
|
+
onClick={handleSubmit}
|
|
109
|
+
disabled={isPosting || !message.trim()}
|
|
110
|
+
>
|
|
111
|
+
{isPosting ? (
|
|
112
|
+
<>
|
|
113
|
+
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
114
|
+
Posting...
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
"Post Update"
|
|
118
|
+
)}
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useApi, type SlotContext } from "@checkstack/frontend-api";
|
|
3
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
4
|
+
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
5
|
+
import { maintenanceApiRef } from "../api";
|
|
6
|
+
import {
|
|
7
|
+
MAINTENANCE_UPDATED,
|
|
8
|
+
type MaintenanceWithSystems,
|
|
9
|
+
} from "@checkstack/maintenance-common";
|
|
10
|
+
import { Badge } from "@checkstack/ui";
|
|
11
|
+
|
|
12
|
+
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Displays a maintenance badge for a system when it has an active maintenance.
|
|
16
|
+
* Shows nothing if no active maintenance.
|
|
17
|
+
* Listens for realtime updates via signals.
|
|
18
|
+
*/
|
|
19
|
+
export const SystemMaintenanceBadge: React.FC<Props> = ({ system }) => {
|
|
20
|
+
const api = useApi(maintenanceApiRef);
|
|
21
|
+
const [hasActiveMaintenance, setHasActiveMaintenance] = useState(false);
|
|
22
|
+
|
|
23
|
+
const refetch = useCallback(() => {
|
|
24
|
+
if (!system?.id) return;
|
|
25
|
+
|
|
26
|
+
api
|
|
27
|
+
.getMaintenancesForSystem({ systemId: system.id })
|
|
28
|
+
.then((maintenances: MaintenanceWithSystems[]) => {
|
|
29
|
+
const active = maintenances.some((m) => m.status === "in_progress");
|
|
30
|
+
setHasActiveMaintenance(active);
|
|
31
|
+
})
|
|
32
|
+
.catch(console.error);
|
|
33
|
+
}, [system?.id, api]);
|
|
34
|
+
|
|
35
|
+
// Initial fetch
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
refetch();
|
|
38
|
+
}, [refetch]);
|
|
39
|
+
|
|
40
|
+
// Listen for realtime maintenance updates
|
|
41
|
+
useSignal(MAINTENANCE_UPDATED, ({ systemIds }) => {
|
|
42
|
+
if (system?.id && systemIds.includes(system.id)) {
|
|
43
|
+
refetch();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!hasActiveMaintenance) return;
|
|
48
|
+
return <Badge variant="warning">Under Maintenance</Badge>;
|
|
49
|
+
};
|