@checkstack/maintenance-frontend 0.2.0 → 0.3.1
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 +79 -0
- package/package.json +2 -1
- package/src/api.ts +9 -10
- package/src/components/MaintenanceEditor.tsx +60 -54
- package/src/components/MaintenanceUpdateForm.tsx +25 -25
- package/src/components/SystemMaintenanceBadge.tsx +42 -27
- package/src/components/SystemMaintenancePanel.tsx +14 -24
- package/src/index.tsx +2 -15
- package/src/pages/MaintenanceConfigPage.tsx +57 -73
- package/src/pages/MaintenanceDetailPage.tsx +43 -49
- package/src/pages/SystemMaintenanceHistoryPage.tsx +20 -38
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @checkstack/maintenance-frontend
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [4eed42d]
|
|
8
|
+
- @checkstack/frontend-api@0.3.0
|
|
9
|
+
- @checkstack/dashboard-frontend@0.3.0
|
|
10
|
+
- @checkstack/auth-frontend@0.3.1
|
|
11
|
+
- @checkstack/catalog-common@1.2.1
|
|
12
|
+
- @checkstack/maintenance-common@0.3.1
|
|
13
|
+
- @checkstack/ui@0.2.2
|
|
14
|
+
|
|
15
|
+
## 0.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 7a23261: ## TanStack Query Integration
|
|
20
|
+
|
|
21
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
22
|
+
|
|
23
|
+
### New Features
|
|
24
|
+
|
|
25
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
26
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
27
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
28
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
29
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
30
|
+
|
|
31
|
+
### Contract Changes
|
|
32
|
+
|
|
33
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const getItems = proc()
|
|
37
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
38
|
+
.output(z.array(itemSchema))
|
|
39
|
+
.query();
|
|
40
|
+
|
|
41
|
+
const createItem = proc()
|
|
42
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
43
|
+
.input(createItemSchema)
|
|
44
|
+
.output(itemSchema)
|
|
45
|
+
.mutation();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Migration
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Before (forPlugin pattern)
|
|
52
|
+
const api = useApi(myPluginApiRef);
|
|
53
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
api.getItems().then(setItems);
|
|
56
|
+
}, [api]);
|
|
57
|
+
|
|
58
|
+
// After (usePluginClient pattern)
|
|
59
|
+
const client = usePluginClient(MyPluginApi);
|
|
60
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Bug Fixes
|
|
64
|
+
|
|
65
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
66
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
67
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- Updated dependencies [180be38]
|
|
72
|
+
- Updated dependencies [7a23261]
|
|
73
|
+
- @checkstack/dashboard-frontend@0.2.0
|
|
74
|
+
- @checkstack/frontend-api@0.2.0
|
|
75
|
+
- @checkstack/common@0.3.0
|
|
76
|
+
- @checkstack/auth-frontend@0.3.0
|
|
77
|
+
- @checkstack/catalog-common@1.2.0
|
|
78
|
+
- @checkstack/maintenance-common@0.3.0
|
|
79
|
+
- @checkstack/ui@0.2.1
|
|
80
|
+
- @checkstack/signal-frontend@0.0.7
|
|
81
|
+
|
|
3
82
|
## 0.2.0
|
|
4
83
|
|
|
5
84
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/maintenance-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"scripts": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"@checkstack/auth-frontend": "workspace:*",
|
|
13
13
|
"@checkstack/catalog-common": "workspace:*",
|
|
14
14
|
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@checkstack/dashboard-frontend": "workspace:*",
|
|
15
16
|
"@checkstack/frontend-api": "workspace:*",
|
|
16
17
|
"@checkstack/maintenance-common": "workspace:*",
|
|
17
18
|
"@checkstack/signal-frontend": "workspace:*",
|
package/src/api.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
);
|
|
1
|
+
// Re-export types for convenience
|
|
2
|
+
export type {
|
|
3
|
+
MaintenanceWithSystems,
|
|
4
|
+
MaintenanceDetail,
|
|
5
|
+
MaintenanceUpdate,
|
|
6
|
+
MaintenanceStatus,
|
|
7
|
+
} from "@checkstack/maintenance-common";
|
|
8
|
+
// Client definition is in @checkstack/maintenance-common - use with usePluginClient
|
|
9
|
+
export { MaintenanceApi } from "@checkstack/maintenance-common";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useState, useEffect
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { MaintenanceApi } from "../api";
|
|
4
4
|
import type {
|
|
5
5
|
MaintenanceWithSystems,
|
|
6
6
|
MaintenanceUpdate,
|
|
@@ -42,7 +42,7 @@ export const MaintenanceEditor: React.FC<Props> = ({
|
|
|
42
42
|
systems,
|
|
43
43
|
onSave,
|
|
44
44
|
}) => {
|
|
45
|
-
const
|
|
45
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
46
46
|
const toast = useToast();
|
|
47
47
|
|
|
48
48
|
// Maintenance fields
|
|
@@ -53,29 +53,45 @@ export const MaintenanceEditor: React.FC<Props> = ({
|
|
|
53
53
|
const [selectedSystemIds, setSelectedSystemIds] = useState<Set<string>>(
|
|
54
54
|
new Set()
|
|
55
55
|
);
|
|
56
|
-
const [saving, setSaving] = useState(false);
|
|
57
56
|
|
|
58
57
|
// Status update fields
|
|
59
58
|
const [updates, setUpdates] = useState<MaintenanceUpdate[]>([]);
|
|
60
|
-
const [loadingUpdates, setLoadingUpdates] = useState(false);
|
|
61
59
|
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
61
|
+
// Query for maintenance details (only when editing)
|
|
62
|
+
const { data: maintenanceDetail, refetch: refetchDetail } =
|
|
63
|
+
maintenanceClient.getMaintenance.useQuery(
|
|
64
|
+
{ id: maintenance?.id ?? "" },
|
|
65
|
+
{ enabled: !!maintenance?.id && open }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Mutations
|
|
69
|
+
const createMutation = maintenanceClient.createMaintenance.useMutation({
|
|
70
|
+
onSuccess: () => {
|
|
71
|
+
toast.success("Maintenance created");
|
|
72
|
+
onSave();
|
|
76
73
|
},
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
onError: (error) => {
|
|
75
|
+
toast.error(error instanceof Error ? error.message : "Failed to save");
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const updateMutation = maintenanceClient.updateMaintenance.useMutation({
|
|
80
|
+
onSuccess: () => {
|
|
81
|
+
toast.success("Maintenance updated");
|
|
82
|
+
onSave();
|
|
83
|
+
},
|
|
84
|
+
onError: (error) => {
|
|
85
|
+
toast.error(error instanceof Error ? error.message : "Failed to save");
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Sync updates from query
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (maintenanceDetail) {
|
|
92
|
+
setUpdates(maintenanceDetail.updates);
|
|
93
|
+
}
|
|
94
|
+
}, [maintenanceDetail]);
|
|
79
95
|
|
|
80
96
|
// Reset form when maintenance changes
|
|
81
97
|
useEffect(() => {
|
|
@@ -85,8 +101,6 @@ export const MaintenanceEditor: React.FC<Props> = ({
|
|
|
85
101
|
setStartAt(new Date(maintenance.startAt));
|
|
86
102
|
setEndAt(new Date(maintenance.endAt));
|
|
87
103
|
setSelectedSystemIds(new Set(maintenance.systemIds));
|
|
88
|
-
// Load full maintenance with updates
|
|
89
|
-
loadMaintenanceDetails(maintenance.id);
|
|
90
104
|
} else {
|
|
91
105
|
// Default to 1 hour from now to 2 hours from now
|
|
92
106
|
const now = new Date();
|
|
@@ -100,7 +114,7 @@ export const MaintenanceEditor: React.FC<Props> = ({
|
|
|
100
114
|
setUpdates([]);
|
|
101
115
|
setShowUpdateForm(false);
|
|
102
116
|
}
|
|
103
|
-
}, [maintenance, open
|
|
117
|
+
}, [maintenance, open]);
|
|
104
118
|
|
|
105
119
|
const handleSystemToggle = (systemId: string) => {
|
|
106
120
|
setSelectedSystemIds((prev) => {
|
|
@@ -114,7 +128,7 @@ export const MaintenanceEditor: React.FC<Props> = ({
|
|
|
114
128
|
});
|
|
115
129
|
};
|
|
116
130
|
|
|
117
|
-
const handleSubmit =
|
|
131
|
+
const handleSubmit = () => {
|
|
118
132
|
if (!title.trim()) {
|
|
119
133
|
toast.error("Title is required");
|
|
120
134
|
return;
|
|
@@ -128,46 +142,38 @@ export const MaintenanceEditor: React.FC<Props> = ({
|
|
|
128
142
|
return;
|
|
129
143
|
}
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
endAt,
|
|
149
|
-
systemIds: [...selectedSystemIds],
|
|
150
|
-
});
|
|
151
|
-
toast.success("Maintenance created");
|
|
152
|
-
}
|
|
153
|
-
onSave();
|
|
154
|
-
} catch (error) {
|
|
155
|
-
const message = error instanceof Error ? error.message : "Failed to save";
|
|
156
|
-
toast.error(message);
|
|
157
|
-
} finally {
|
|
158
|
-
setSaving(false);
|
|
145
|
+
if (maintenance) {
|
|
146
|
+
updateMutation.mutate({
|
|
147
|
+
id: maintenance.id,
|
|
148
|
+
title,
|
|
149
|
+
description: description || undefined,
|
|
150
|
+
startAt,
|
|
151
|
+
endAt,
|
|
152
|
+
systemIds: [...selectedSystemIds],
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
createMutation.mutate({
|
|
156
|
+
title,
|
|
157
|
+
description,
|
|
158
|
+
startAt,
|
|
159
|
+
endAt,
|
|
160
|
+
systemIds: [...selectedSystemIds],
|
|
161
|
+
});
|
|
159
162
|
}
|
|
160
163
|
};
|
|
161
164
|
|
|
162
165
|
const handleUpdateSuccess = () => {
|
|
163
166
|
if (maintenance) {
|
|
164
|
-
|
|
167
|
+
void refetchDetail();
|
|
165
168
|
}
|
|
166
169
|
setShowUpdateForm(false);
|
|
167
170
|
// Notify parent to refresh list (status may have changed)
|
|
168
171
|
onSave();
|
|
169
172
|
};
|
|
170
173
|
|
|
174
|
+
const saving = createMutation.isPending || updateMutation.isPending;
|
|
175
|
+
const loadingUpdates = false; // Now handled by useQuery
|
|
176
|
+
|
|
171
177
|
return (
|
|
172
178
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
173
179
|
<DialogContent size="xl">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { MaintenanceApi } from "../api";
|
|
4
4
|
import type { MaintenanceStatus } from "@checkstack/maintenance-common";
|
|
5
5
|
import {
|
|
6
6
|
Button,
|
|
@@ -30,37 +30,37 @@ export const MaintenanceUpdateForm: React.FC<MaintenanceUpdateFormProps> = ({
|
|
|
30
30
|
onSuccess,
|
|
31
31
|
onCancel,
|
|
32
32
|
}) => {
|
|
33
|
-
const
|
|
33
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
34
34
|
const toast = useToast();
|
|
35
35
|
|
|
36
36
|
const [message, setMessage] = useState("");
|
|
37
37
|
const [statusChange, setStatusChange] = useState<MaintenanceStatus | "">("");
|
|
38
|
-
const [isPosting, setIsPosting] = useState(false);
|
|
39
38
|
|
|
40
|
-
const
|
|
41
|
-
|
|
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
|
-
});
|
|
39
|
+
const addUpdateMutation = maintenanceClient.addUpdate.useMutation({
|
|
40
|
+
onSuccess: () => {
|
|
53
41
|
toast.success("Update posted");
|
|
54
42
|
setMessage("");
|
|
55
43
|
setStatusChange("");
|
|
56
44
|
onSuccess();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
},
|
|
46
|
+
onError: (error) => {
|
|
47
|
+
toast.error(
|
|
48
|
+
error instanceof Error ? error.message : "Failed to post update"
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const handleSubmit = () => {
|
|
54
|
+
if (!message.trim()) {
|
|
55
|
+
toast.error("Update message is required");
|
|
56
|
+
return;
|
|
63
57
|
}
|
|
58
|
+
|
|
59
|
+
addUpdateMutation.mutate({
|
|
60
|
+
maintenanceId,
|
|
61
|
+
message,
|
|
62
|
+
statusChange: statusChange || undefined,
|
|
63
|
+
});
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
return (
|
|
@@ -106,9 +106,9 @@ export const MaintenanceUpdateForm: React.FC<MaintenanceUpdateFormProps> = ({
|
|
|
106
106
|
<Button
|
|
107
107
|
size="sm"
|
|
108
108
|
onClick={handleSubmit}
|
|
109
|
-
disabled={
|
|
109
|
+
disabled={addUpdateMutation.isPending || !message.trim()}
|
|
110
110
|
>
|
|
111
|
-
{
|
|
111
|
+
{addUpdateMutation.isPending ? (
|
|
112
112
|
<>
|
|
113
113
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
114
114
|
Posting...
|
|
@@ -1,49 +1,64 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
4
4
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
5
|
-
import {
|
|
5
|
+
import { MaintenanceApi } from "../api";
|
|
6
6
|
import {
|
|
7
7
|
MAINTENANCE_UPDATED,
|
|
8
8
|
type MaintenanceWithSystems,
|
|
9
9
|
} from "@checkstack/maintenance-common";
|
|
10
10
|
import { Badge } from "@checkstack/ui";
|
|
11
|
+
import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
|
|
11
12
|
|
|
12
13
|
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Checks if any maintenance is currently in progress.
|
|
17
|
+
*/
|
|
18
|
+
function hasActiveMaintenance(maintenances: MaintenanceWithSystems[]): boolean {
|
|
19
|
+
return maintenances.some((m) => m.status === "in_progress");
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Displays a maintenance badge for a system when it has an active maintenance.
|
|
16
24
|
* Shows nothing if no active maintenance.
|
|
25
|
+
*
|
|
26
|
+
* When rendered within SystemBadgeDataProvider, uses bulk-fetched data.
|
|
27
|
+
* Otherwise, falls back to individual fetch.
|
|
28
|
+
*
|
|
17
29
|
* Listens for realtime updates via signals.
|
|
18
30
|
*/
|
|
19
31
|
export const SystemMaintenanceBadge: React.FC<Props> = ({ system }) => {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Listen for realtime maintenance updates
|
|
32
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
33
|
+
const badgeData = useSystemBadgeDataOptional();
|
|
34
|
+
|
|
35
|
+
// Try to get data from provider first
|
|
36
|
+
const providerData = badgeData?.getSystemBadgeData(system?.id ?? "");
|
|
37
|
+
const providerHasActive = providerData
|
|
38
|
+
? hasActiveMaintenance(providerData.maintenances)
|
|
39
|
+
: false;
|
|
40
|
+
|
|
41
|
+
// Query for maintenances if not using provider
|
|
42
|
+
const { data: maintenances, refetch } =
|
|
43
|
+
maintenanceClient.getMaintenancesForSystem.useQuery(
|
|
44
|
+
{ systemId: system?.id ?? "" },
|
|
45
|
+
{ enabled: !badgeData && !!system?.id }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const localHasActive = maintenances
|
|
49
|
+
? hasActiveMaintenance(maintenances)
|
|
50
|
+
: false;
|
|
51
|
+
|
|
52
|
+
// Listen for realtime maintenance updates (only in fallback mode)
|
|
41
53
|
useSignal(MAINTENANCE_UPDATED, ({ systemIds }) => {
|
|
42
|
-
if (system?.id && systemIds.includes(system.id)) {
|
|
43
|
-
refetch();
|
|
54
|
+
if (!badgeData && system?.id && systemIds.includes(system.id)) {
|
|
55
|
+
void refetch();
|
|
44
56
|
}
|
|
45
57
|
});
|
|
46
58
|
|
|
47
|
-
if
|
|
59
|
+
// Use provider data if available, otherwise use local state
|
|
60
|
+
const hasActive = badgeData ? providerHasActive : localHasActive;
|
|
61
|
+
|
|
62
|
+
if (!hasActive) return;
|
|
48
63
|
return <Badge variant="warning">Under Maintenance</Badge>;
|
|
49
64
|
};
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
4
4
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
5
5
|
import { resolveRoute } from "@checkstack/common";
|
|
6
6
|
import { SystemDetailsSlot } from "@checkstack/catalog-common";
|
|
7
|
-
import {
|
|
7
|
+
import { MaintenanceApi } from "../api";
|
|
8
8
|
import {
|
|
9
9
|
maintenanceRoutes,
|
|
10
10
|
MAINTENANCE_UPDATED,
|
|
11
|
-
type MaintenanceWithSystems,
|
|
12
11
|
} from "@checkstack/maintenance-common";
|
|
13
12
|
import {
|
|
14
13
|
Card,
|
|
@@ -29,31 +28,22 @@ type Props = SlotContext<typeof SystemDetailsSlot>;
|
|
|
29
28
|
* Listens for realtime updates via signals.
|
|
30
29
|
*/
|
|
31
30
|
export const SystemMaintenancePanel: React.FC<Props> = ({ system }) => {
|
|
32
|
-
const
|
|
33
|
-
const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
|
|
34
|
-
[]
|
|
35
|
-
);
|
|
36
|
-
const [loading, setLoading] = useState(true);
|
|
37
|
-
|
|
38
|
-
const refetch = useCallback(() => {
|
|
39
|
-
if (!system?.id) return;
|
|
31
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
refetch();
|
|
51
|
-
}, [refetch]);
|
|
33
|
+
// Fetch maintenances with useQuery
|
|
34
|
+
const {
|
|
35
|
+
data: maintenances = [],
|
|
36
|
+
isLoading: loading,
|
|
37
|
+
refetch,
|
|
38
|
+
} = maintenanceClient.getMaintenancesForSystem.useQuery(
|
|
39
|
+
{ systemId: system?.id ?? "" },
|
|
40
|
+
{ enabled: !!system?.id }
|
|
41
|
+
);
|
|
52
42
|
|
|
53
43
|
// Listen for realtime maintenance updates
|
|
54
44
|
useSignal(MAINTENANCE_UPDATED, ({ systemIds }) => {
|
|
55
45
|
if (system?.id && systemIds.includes(system.id)) {
|
|
56
|
-
refetch();
|
|
46
|
+
void refetch();
|
|
57
47
|
}
|
|
58
48
|
});
|
|
59
49
|
|
package/src/index.tsx
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createFrontendPlugin,
|
|
3
3
|
createSlotExtension,
|
|
4
|
-
rpcApiRef,
|
|
5
|
-
type ApiRef,
|
|
6
4
|
UserMenuItemsSlot,
|
|
7
5
|
} from "@checkstack/frontend-api";
|
|
8
|
-
import { maintenanceApiRef, type MaintenanceApiClient } from "./api";
|
|
9
6
|
import {
|
|
10
7
|
maintenanceRoutes,
|
|
11
|
-
MaintenanceApi,
|
|
12
8
|
pluginMetadata,
|
|
13
9
|
maintenanceAccess,
|
|
14
10
|
} from "@checkstack/maintenance-common";
|
|
@@ -43,17 +39,8 @@ export default createFrontendPlugin({
|
|
|
43
39
|
title: "Maintenance Details",
|
|
44
40
|
},
|
|
45
41
|
],
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ref: maintenanceApiRef,
|
|
49
|
-
factory: (deps: {
|
|
50
|
-
get: <T>(ref: ApiRef<T>) => T;
|
|
51
|
-
}): MaintenanceApiClient => {
|
|
52
|
-
const rpcApi = deps.get(rpcApiRef);
|
|
53
|
-
return rpcApi.forPlugin(MaintenanceApi);
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
],
|
|
42
|
+
// No APIs needed - components use usePluginClient() directly
|
|
43
|
+
apis: [],
|
|
57
44
|
extensions: [
|
|
58
45
|
createSlotExtension(UserMenuItemsSlot, {
|
|
59
46
|
id: "maintenance.user-menu.items",
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import React, { useEffect, useState
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { useSearchParams } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
rpcApiRef,
|
|
4
|
+
usePluginClient,
|
|
6
5
|
accessApiRef,
|
|
6
|
+
useApi,
|
|
7
7
|
wrapInSuspense,
|
|
8
8
|
} from "@checkstack/frontend-api";
|
|
9
|
-
import {
|
|
9
|
+
import { MaintenanceApi } from "../api";
|
|
10
10
|
import type {
|
|
11
11
|
MaintenanceWithSystems,
|
|
12
12
|
MaintenanceStatus,
|
|
13
13
|
} from "@checkstack/maintenance-common";
|
|
14
14
|
import { maintenanceAccess } from "@checkstack/maintenance-common";
|
|
15
|
-
import { CatalogApi
|
|
15
|
+
import { CatalogApi } from "@checkstack/catalog-common";
|
|
16
16
|
import {
|
|
17
17
|
Card,
|
|
18
18
|
CardHeader,
|
|
@@ -50,22 +50,16 @@ import { MaintenanceEditor } from "../components/MaintenanceEditor";
|
|
|
50
50
|
import { getMaintenanceStatusBadge } from "../utils/badges";
|
|
51
51
|
|
|
52
52
|
const MaintenanceConfigPageContent: React.FC = () => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
53
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
54
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
55
55
|
const accessApi = useApi(accessApiRef);
|
|
56
56
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
57
|
-
|
|
58
|
-
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
59
57
|
const toast = useToast();
|
|
60
58
|
|
|
61
|
-
const { allowed: canManage, loading: accessLoading } =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
|
|
65
|
-
[]
|
|
59
|
+
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
60
|
+
maintenanceAccess.maintenance.manage
|
|
66
61
|
);
|
|
67
|
-
|
|
68
|
-
const [loading, setLoading] = useState(true);
|
|
62
|
+
|
|
69
63
|
const [statusFilter, setStatusFilter] = useState<MaintenanceStatus | "all">(
|
|
70
64
|
"all"
|
|
71
65
|
);
|
|
@@ -78,35 +72,26 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
78
72
|
|
|
79
73
|
// Delete confirmation state
|
|
80
74
|
const [deleteId, setDeleteId] = useState<string | undefined>();
|
|
81
|
-
const [isDeleting, setIsDeleting] = useState(false);
|
|
82
75
|
|
|
83
76
|
// Complete confirmation state
|
|
84
77
|
const [completeId, setCompleteId] = useState<string | undefined>();
|
|
85
|
-
const [isCompleting, setIsCompleting] = useState(false);
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
catalogApi.getSystems(),
|
|
96
|
-
]);
|
|
97
|
-
setMaintenances(maintenanceList);
|
|
98
|
-
setSystems(systemList);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
const message = error instanceof Error ? error.message : "Failed to load";
|
|
101
|
-
toast.error(message);
|
|
102
|
-
} finally {
|
|
103
|
-
setLoading(false);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
79
|
+
// Fetch maintenances with useQuery
|
|
80
|
+
const {
|
|
81
|
+
data: maintenancesData,
|
|
82
|
+
isLoading: maintenancesLoading,
|
|
83
|
+
refetch: refetchMaintenances,
|
|
84
|
+
} = maintenanceClient.listMaintenances.useQuery(
|
|
85
|
+
statusFilter === "all" ? {} : { status: statusFilter }
|
|
86
|
+
);
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
88
|
+
// Fetch systems with useQuery
|
|
89
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
90
|
+
catalogClient.getSystems.useQuery({});
|
|
91
|
+
|
|
92
|
+
const maintenances = maintenancesData?.maintenances ?? [];
|
|
93
|
+
const systems = systemsData?.systems ?? [];
|
|
94
|
+
const loading = maintenancesLoading || systemsLoading;
|
|
110
95
|
|
|
111
96
|
// Handle ?action=create URL parameter (from command palette)
|
|
112
97
|
useEffect(() => {
|
|
@@ -119,6 +104,31 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
119
104
|
}
|
|
120
105
|
}, [searchParams, canManage, setSearchParams]);
|
|
121
106
|
|
|
107
|
+
// Mutations
|
|
108
|
+
const deleteMutation = maintenanceClient.deleteMaintenance.useMutation({
|
|
109
|
+
onSuccess: () => {
|
|
110
|
+
toast.success("Maintenance deleted");
|
|
111
|
+
void refetchMaintenances();
|
|
112
|
+
setDeleteId(undefined);
|
|
113
|
+
},
|
|
114
|
+
onError: (error) => {
|
|
115
|
+
toast.error(error instanceof Error ? error.message : "Failed to delete");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const completeMutation = maintenanceClient.closeMaintenance.useMutation({
|
|
120
|
+
onSuccess: () => {
|
|
121
|
+
toast.success("Maintenance completed");
|
|
122
|
+
void refetchMaintenances();
|
|
123
|
+
setCompleteId(undefined);
|
|
124
|
+
},
|
|
125
|
+
onError: (error) => {
|
|
126
|
+
toast.error(
|
|
127
|
+
error instanceof Error ? error.message : "Failed to complete"
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
122
132
|
const handleCreate = () => {
|
|
123
133
|
setEditingMaintenance(undefined);
|
|
124
134
|
setEditorOpen(true);
|
|
@@ -129,45 +139,19 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
129
139
|
setEditorOpen(true);
|
|
130
140
|
};
|
|
131
141
|
|
|
132
|
-
const handleDelete =
|
|
142
|
+
const handleDelete = () => {
|
|
133
143
|
if (!deleteId) return;
|
|
134
|
-
|
|
135
|
-
setIsDeleting(true);
|
|
136
|
-
try {
|
|
137
|
-
await api.deleteMaintenance({ id: deleteId });
|
|
138
|
-
toast.success("Maintenance deleted");
|
|
139
|
-
loadData();
|
|
140
|
-
} catch (error) {
|
|
141
|
-
const message =
|
|
142
|
-
error instanceof Error ? error.message : "Failed to delete";
|
|
143
|
-
toast.error(message);
|
|
144
|
-
} finally {
|
|
145
|
-
setIsDeleting(false);
|
|
146
|
-
setDeleteId(undefined);
|
|
147
|
-
}
|
|
144
|
+
deleteMutation.mutate({ id: deleteId });
|
|
148
145
|
};
|
|
149
146
|
|
|
150
|
-
const handleComplete =
|
|
147
|
+
const handleComplete = () => {
|
|
151
148
|
if (!completeId) return;
|
|
152
|
-
|
|
153
|
-
setIsCompleting(true);
|
|
154
|
-
try {
|
|
155
|
-
await api.closeMaintenance({ id: completeId });
|
|
156
|
-
toast.success("Maintenance completed");
|
|
157
|
-
loadData();
|
|
158
|
-
} catch (error) {
|
|
159
|
-
const message =
|
|
160
|
-
error instanceof Error ? error.message : "Failed to complete";
|
|
161
|
-
toast.error(message);
|
|
162
|
-
} finally {
|
|
163
|
-
setIsCompleting(false);
|
|
164
|
-
setCompleteId(undefined);
|
|
165
|
-
}
|
|
149
|
+
completeMutation.mutate({ id: completeId });
|
|
166
150
|
};
|
|
167
151
|
|
|
168
152
|
const handleSave = () => {
|
|
169
153
|
setEditorOpen(false);
|
|
170
|
-
|
|
154
|
+
void refetchMaintenances();
|
|
171
155
|
};
|
|
172
156
|
|
|
173
157
|
const getSystemNames = (systemIds: string[]): string => {
|
|
@@ -327,7 +311,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
327
311
|
confirmText="Delete"
|
|
328
312
|
variant="danger"
|
|
329
313
|
onConfirm={handleDelete}
|
|
330
|
-
isLoading={
|
|
314
|
+
isLoading={deleteMutation.isPending}
|
|
331
315
|
/>
|
|
332
316
|
|
|
333
317
|
<ConfirmationModal
|
|
@@ -338,7 +322,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
338
322
|
confirmText="Complete"
|
|
339
323
|
variant="info"
|
|
340
324
|
onConfirm={handleComplete}
|
|
341
|
-
isLoading={
|
|
325
|
+
isLoading={completeMutation.isPending}
|
|
342
326
|
/>
|
|
343
327
|
</PageLayout>
|
|
344
328
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useParams,
|
|
4
4
|
Link,
|
|
@@ -6,23 +6,18 @@ import {
|
|
|
6
6
|
useSearchParams,
|
|
7
7
|
} from "react-router-dom";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
rpcApiRef,
|
|
9
|
+
usePluginClient,
|
|
11
10
|
wrapInSuspense,
|
|
12
11
|
accessApiRef,
|
|
12
|
+
useApi,
|
|
13
13
|
} from "@checkstack/frontend-api";
|
|
14
14
|
import { resolveRoute } from "@checkstack/common";
|
|
15
|
-
import {
|
|
15
|
+
import { MaintenanceApi } from "../api";
|
|
16
16
|
import {
|
|
17
17
|
maintenanceRoutes,
|
|
18
18
|
maintenanceAccess,
|
|
19
19
|
} from "@checkstack/maintenance-common";
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
catalogRoutes,
|
|
23
|
-
CatalogApi,
|
|
24
|
-
type System,
|
|
25
|
-
} from "@checkstack/catalog-common";
|
|
20
|
+
import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
|
|
26
21
|
import {
|
|
27
22
|
Card,
|
|
28
23
|
CardHeader,
|
|
@@ -54,61 +49,55 @@ const MaintenanceDetailPageContent: React.FC = () => {
|
|
|
54
49
|
const { maintenanceId } = useParams<{ maintenanceId: string }>();
|
|
55
50
|
const navigate = useNavigate();
|
|
56
51
|
const [searchParams] = useSearchParams();
|
|
57
|
-
const
|
|
58
|
-
const
|
|
52
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
53
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
59
54
|
const accessApi = useApi(accessApiRef);
|
|
60
55
|
const toast = useToast();
|
|
61
56
|
|
|
62
|
-
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
63
|
-
|
|
64
57
|
const { allowed: canManage } = accessApi.useAccess(
|
|
65
58
|
maintenanceAccess.maintenance.manage
|
|
66
59
|
);
|
|
67
60
|
|
|
68
|
-
const [maintenance, setMaintenance] = useState<MaintenanceDetail>();
|
|
69
|
-
const [systems, setSystems] = useState<System[]>([]);
|
|
70
|
-
const [loading, setLoading] = useState(true);
|
|
71
61
|
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
72
62
|
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
// Fetch maintenance with useQuery
|
|
64
|
+
const {
|
|
65
|
+
data: maintenance,
|
|
66
|
+
isLoading: maintenanceLoading,
|
|
67
|
+
refetch: refetchMaintenance,
|
|
68
|
+
} = maintenanceClient.getMaintenance.useQuery(
|
|
69
|
+
{ id: maintenanceId ?? "" },
|
|
70
|
+
{ enabled: !!maintenanceId }
|
|
71
|
+
);
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
api.getMaintenance({ id: maintenanceId }),
|
|
80
|
-
catalogApi.getSystems(),
|
|
81
|
-
]);
|
|
82
|
-
setMaintenance(maintenanceData ?? undefined);
|
|
83
|
-
setSystems(systemList);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error("Failed to load maintenance details:", error);
|
|
86
|
-
} finally {
|
|
87
|
-
setLoading(false);
|
|
88
|
-
}
|
|
89
|
-
}, [maintenanceId, api, catalogApi]);
|
|
73
|
+
// Fetch systems with useQuery
|
|
74
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
75
|
+
catalogClient.getSystems.useQuery({});
|
|
90
76
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
const systems = systemsData?.systems ?? [];
|
|
78
|
+
const loading = maintenanceLoading || systemsLoading;
|
|
79
|
+
|
|
80
|
+
// Complete mutation
|
|
81
|
+
const completeMutation = maintenanceClient.closeMaintenance.useMutation({
|
|
82
|
+
onSuccess: () => {
|
|
83
|
+
toast.success("Maintenance completed");
|
|
84
|
+
void refetchMaintenance();
|
|
85
|
+
},
|
|
86
|
+
onError: (error) => {
|
|
87
|
+
toast.error(
|
|
88
|
+
error instanceof Error ? error.message : "Failed to complete"
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
94
92
|
|
|
95
93
|
const handleUpdateSuccess = () => {
|
|
96
94
|
setShowUpdateForm(false);
|
|
97
|
-
|
|
95
|
+
void refetchMaintenance();
|
|
98
96
|
};
|
|
99
97
|
|
|
100
|
-
const handleComplete =
|
|
98
|
+
const handleComplete = () => {
|
|
101
99
|
if (!maintenanceId) return;
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
await api.closeMaintenance({ id: maintenanceId });
|
|
105
|
-
toast.success("Maintenance completed");
|
|
106
|
-
await loadData();
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const message =
|
|
109
|
-
error instanceof Error ? error.message : "Failed to complete";
|
|
110
|
-
toast.error(message);
|
|
111
|
-
}
|
|
100
|
+
completeMutation.mutate({ id: maintenanceId });
|
|
112
101
|
};
|
|
113
102
|
|
|
114
103
|
const getSystemName = (systemId: string): string => {
|
|
@@ -182,7 +171,12 @@ const MaintenanceDetailPageContent: React.FC = () => {
|
|
|
182
171
|
<div className="flex items-center gap-2">
|
|
183
172
|
{getMaintenanceStatusBadge(maintenance.status)}
|
|
184
173
|
{canComplete && (
|
|
185
|
-
<Button
|
|
174
|
+
<Button
|
|
175
|
+
variant="outline"
|
|
176
|
+
size="sm"
|
|
177
|
+
onClick={handleComplete}
|
|
178
|
+
disabled={completeMutation.isPending}
|
|
179
|
+
>
|
|
186
180
|
<CheckCircle2 className="h-4 w-4 mr-1" />
|
|
187
181
|
Complete
|
|
188
182
|
</Button>
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { useParams, useNavigate } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
|
|
4
4
|
import { resolveRoute } from "@checkstack/common";
|
|
5
|
-
import {
|
|
5
|
+
import { MaintenanceApi } from "../api";
|
|
6
6
|
import { maintenanceRoutes } from "@checkstack/maintenance-common";
|
|
7
|
-
import type {
|
|
8
|
-
MaintenanceWithSystems,
|
|
9
|
-
MaintenanceStatus,
|
|
10
|
-
} from "@checkstack/maintenance-common";
|
|
7
|
+
import type { MaintenanceStatus } from "@checkstack/maintenance-common";
|
|
11
8
|
import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
|
|
12
9
|
import {
|
|
13
10
|
Card,
|
|
@@ -32,40 +29,25 @@ import { format } from "date-fns";
|
|
|
32
29
|
const SystemMaintenanceHistoryPageContent: React.FC = () => {
|
|
33
30
|
const { systemId } = useParams<{ systemId: string }>();
|
|
34
31
|
const navigate = useNavigate();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
32
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
33
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const [loading, setLoading] = useState(true);
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (!systemId) return;
|
|
35
|
+
// Fetch maintenances with useQuery
|
|
36
|
+
const { data: maintenancesData, isLoading: maintenancesLoading } =
|
|
37
|
+
maintenanceClient.listMaintenances.useQuery(
|
|
38
|
+
{ systemId },
|
|
39
|
+
{ enabled: !!systemId }
|
|
40
|
+
);
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const [{ maintenances: maintenanceList }, { systems: systemList }] =
|
|
53
|
-
await Promise.all([
|
|
54
|
-
api.listMaintenances({ systemId }),
|
|
55
|
-
catalogApi.getSystems(),
|
|
56
|
-
]);
|
|
57
|
-
setMaintenances(maintenanceList);
|
|
58
|
-
const system = systemList.find((s) => s.id === systemId);
|
|
59
|
-
setSystemName(system?.name ?? "Unknown System");
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error("Failed to load maintenance history:", error);
|
|
62
|
-
} finally {
|
|
63
|
-
setLoading(false);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
42
|
+
// Fetch systems with useQuery
|
|
43
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
44
|
+
catalogClient.getSystems.useQuery({});
|
|
66
45
|
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
const maintenances = maintenancesData?.maintenances ?? [];
|
|
47
|
+
const systems = systemsData?.systems ?? [];
|
|
48
|
+
const system = systems.find((s) => s.id === systemId);
|
|
49
|
+
const systemName = system?.name ?? "Unknown System";
|
|
50
|
+
const loading = maintenancesLoading || systemsLoading;
|
|
69
51
|
|
|
70
52
|
const getStatusBadge = (status: MaintenanceStatus) => {
|
|
71
53
|
switch (status) {
|