@checkstack/maintenance-frontend 0.1.0 → 0.3.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.
- package/CHANGELOG.md +148 -0
- package/package.json +2 -1
- package/src/api.ts +9 -10
- package/src/components/MaintenanceEditor.tsx +60 -54
- package/src/components/MaintenanceMenuItems.tsx +4 -7
- 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 +4 -17
- package/src/pages/MaintenanceConfigPage.tsx +60 -75
- package/src/pages/MaintenanceDetailPage.tsx +50 -54
- package/src/pages/SystemMaintenanceHistoryPage.tsx +20 -38
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,153 @@
|
|
|
1
1
|
# @checkstack/maintenance-frontend
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7a23261: ## TanStack Query Integration
|
|
8
|
+
|
|
9
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
10
|
+
|
|
11
|
+
### New Features
|
|
12
|
+
|
|
13
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
14
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
15
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
16
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
17
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
18
|
+
|
|
19
|
+
### Contract Changes
|
|
20
|
+
|
|
21
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const getItems = proc()
|
|
25
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
26
|
+
.output(z.array(itemSchema))
|
|
27
|
+
.query();
|
|
28
|
+
|
|
29
|
+
const createItem = proc()
|
|
30
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
31
|
+
.input(createItemSchema)
|
|
32
|
+
.output(itemSchema)
|
|
33
|
+
.mutation();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Before (forPlugin pattern)
|
|
40
|
+
const api = useApi(myPluginApiRef);
|
|
41
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
api.getItems().then(setItems);
|
|
44
|
+
}, [api]);
|
|
45
|
+
|
|
46
|
+
// After (usePluginClient pattern)
|
|
47
|
+
const client = usePluginClient(MyPluginApi);
|
|
48
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
54
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
55
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [180be38]
|
|
60
|
+
- Updated dependencies [7a23261]
|
|
61
|
+
- @checkstack/dashboard-frontend@0.2.0
|
|
62
|
+
- @checkstack/frontend-api@0.2.0
|
|
63
|
+
- @checkstack/common@0.3.0
|
|
64
|
+
- @checkstack/auth-frontend@0.3.0
|
|
65
|
+
- @checkstack/catalog-common@1.2.0
|
|
66
|
+
- @checkstack/maintenance-common@0.3.0
|
|
67
|
+
- @checkstack/ui@0.2.1
|
|
68
|
+
- @checkstack/signal-frontend@0.0.7
|
|
69
|
+
|
|
70
|
+
## 0.2.0
|
|
71
|
+
|
|
72
|
+
### Minor Changes
|
|
73
|
+
|
|
74
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
75
|
+
|
|
76
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
77
|
+
|
|
78
|
+
## Changes
|
|
79
|
+
|
|
80
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
81
|
+
|
|
82
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
83
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
84
|
+
- Added `access()` builder for individual access rules
|
|
85
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
86
|
+
|
|
87
|
+
### API Changes
|
|
88
|
+
|
|
89
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
90
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
91
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
92
|
+
- Route `permission:` field → `accessRule:` field
|
|
93
|
+
|
|
94
|
+
### UI Changes
|
|
95
|
+
|
|
96
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
97
|
+
- "You don't have permission..." → "You don't have access..."
|
|
98
|
+
- All permission-related UI text updated
|
|
99
|
+
|
|
100
|
+
### Documentation & Templates
|
|
101
|
+
|
|
102
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
103
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
104
|
+
- All code examples use new AccessRule API
|
|
105
|
+
|
|
106
|
+
## Migration Guide
|
|
107
|
+
|
|
108
|
+
### Backend Plugins
|
|
109
|
+
|
|
110
|
+
```diff
|
|
111
|
+
- import { permissionList } from "./permissions";
|
|
112
|
+
- env.registerPermissions(permissionList);
|
|
113
|
+
+ import { accessRules } from "./access";
|
|
114
|
+
+ env.registerAccessRules(accessRules);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### RPC Contracts
|
|
118
|
+
|
|
119
|
+
```diff
|
|
120
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
121
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Frontend Hooks
|
|
125
|
+
|
|
126
|
+
```diff
|
|
127
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
128
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Routes
|
|
132
|
+
|
|
133
|
+
```diff
|
|
134
|
+
- permission: permissions.entityRead.id,
|
|
135
|
+
+ accessRule: access.read,
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Patch Changes
|
|
139
|
+
|
|
140
|
+
- Updated dependencies [9faec1f]
|
|
141
|
+
- Updated dependencies [95eeec7]
|
|
142
|
+
- Updated dependencies [f533141]
|
|
143
|
+
- @checkstack/auth-frontend@0.2.0
|
|
144
|
+
- @checkstack/catalog-common@1.1.0
|
|
145
|
+
- @checkstack/common@0.2.0
|
|
146
|
+
- @checkstack/frontend-api@0.1.0
|
|
147
|
+
- @checkstack/maintenance-common@0.2.0
|
|
148
|
+
- @checkstack/ui@0.2.0
|
|
149
|
+
- @checkstack/signal-frontend@0.0.6
|
|
150
|
+
|
|
3
151
|
## 0.1.0
|
|
4
152
|
|
|
5
153
|
### 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.0",
|
|
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">
|
|
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
|
|
|
3
3
|
import { Wrench } from "lucide-react";
|
|
4
4
|
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
5
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveRoute } from "@checkstack/common";
|
|
7
7
|
import {
|
|
8
8
|
maintenanceRoutes,
|
|
9
|
-
|
|
9
|
+
maintenanceAccess,
|
|
10
10
|
pluginMetadata,
|
|
11
11
|
} from "@checkstack/maintenance-common";
|
|
12
12
|
|
|
13
13
|
export const MaintenanceMenuItems = ({
|
|
14
|
-
|
|
14
|
+
accessRules: userPerms,
|
|
15
15
|
}: UserMenuItemsContext) => {
|
|
16
|
-
const qualifiedId =
|
|
17
|
-
pluginMetadata,
|
|
18
|
-
permissions.maintenanceManage
|
|
19
|
-
);
|
|
16
|
+
const qualifiedId = `${pluginMetadata.pluginId}.${maintenanceAccess.maintenance.manage.id}`;
|
|
20
17
|
const canManage = userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
21
18
|
|
|
22
19
|
if (!canManage) {
|
|
@@ -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,16 +1,12 @@
|
|
|
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";
|
|
15
11
|
import {
|
|
16
12
|
SystemDetailsTopSlot,
|
|
@@ -30,7 +26,7 @@ export default createFrontendPlugin({
|
|
|
30
26
|
route: maintenanceRoutes.routes.config,
|
|
31
27
|
element: <MaintenanceConfigPage />,
|
|
32
28
|
title: "Maintenances",
|
|
33
|
-
|
|
29
|
+
accessRule: maintenanceAccess.maintenance.manage,
|
|
34
30
|
},
|
|
35
31
|
{
|
|
36
32
|
route: maintenanceRoutes.routes.systemHistory,
|
|
@@ -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,17 +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
|
+
usePluginClient,
|
|
5
|
+
accessApiRef,
|
|
4
6
|
useApi,
|
|
5
|
-
rpcApiRef,
|
|
6
|
-
permissionApiRef,
|
|
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
|
-
import {
|
|
14
|
+
import { maintenanceAccess } from "@checkstack/maintenance-common";
|
|
15
|
+
import { CatalogApi } from "@checkstack/catalog-common";
|
|
15
16
|
import {
|
|
16
17
|
Card,
|
|
17
18
|
CardHeader,
|
|
@@ -49,22 +50,16 @@ import { MaintenanceEditor } from "../components/MaintenanceEditor";
|
|
|
49
50
|
import { getMaintenanceStatusBadge } from "../utils/badges";
|
|
50
51
|
|
|
51
52
|
const MaintenanceConfigPageContent: React.FC = () => {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
53
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
54
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
55
|
+
const accessApi = useApi(accessApiRef);
|
|
55
56
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
56
|
-
|
|
57
|
-
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
58
57
|
const toast = useToast();
|
|
59
58
|
|
|
60
|
-
const { allowed: canManage, loading:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
|
|
64
|
-
[]
|
|
59
|
+
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
60
|
+
maintenanceAccess.maintenance.manage
|
|
65
61
|
);
|
|
66
|
-
|
|
67
|
-
const [loading, setLoading] = useState(true);
|
|
62
|
+
|
|
68
63
|
const [statusFilter, setStatusFilter] = useState<MaintenanceStatus | "all">(
|
|
69
64
|
"all"
|
|
70
65
|
);
|
|
@@ -77,35 +72,26 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
77
72
|
|
|
78
73
|
// Delete confirmation state
|
|
79
74
|
const [deleteId, setDeleteId] = useState<string | undefined>();
|
|
80
|
-
const [isDeleting, setIsDeleting] = useState(false);
|
|
81
75
|
|
|
82
76
|
// Complete confirmation state
|
|
83
77
|
const [completeId, setCompleteId] = useState<string | undefined>();
|
|
84
|
-
const [isCompleting, setIsCompleting] = useState(false);
|
|
85
78
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
catalogApi.getSystems(),
|
|
95
|
-
]);
|
|
96
|
-
setMaintenances(maintenanceList);
|
|
97
|
-
setSystems(systemList);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
const message = error instanceof Error ? error.message : "Failed to load";
|
|
100
|
-
toast.error(message);
|
|
101
|
-
} finally {
|
|
102
|
-
setLoading(false);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
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
|
+
);
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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;
|
|
109
95
|
|
|
110
96
|
// Handle ?action=create URL parameter (from command palette)
|
|
111
97
|
useEffect(() => {
|
|
@@ -118,6 +104,31 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
118
104
|
}
|
|
119
105
|
}, [searchParams, canManage, setSearchParams]);
|
|
120
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
|
+
|
|
121
132
|
const handleCreate = () => {
|
|
122
133
|
setEditingMaintenance(undefined);
|
|
123
134
|
setEditorOpen(true);
|
|
@@ -128,45 +139,19 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
128
139
|
setEditorOpen(true);
|
|
129
140
|
};
|
|
130
141
|
|
|
131
|
-
const handleDelete =
|
|
142
|
+
const handleDelete = () => {
|
|
132
143
|
if (!deleteId) return;
|
|
133
|
-
|
|
134
|
-
setIsDeleting(true);
|
|
135
|
-
try {
|
|
136
|
-
await api.deleteMaintenance({ id: deleteId });
|
|
137
|
-
toast.success("Maintenance deleted");
|
|
138
|
-
loadData();
|
|
139
|
-
} catch (error) {
|
|
140
|
-
const message =
|
|
141
|
-
error instanceof Error ? error.message : "Failed to delete";
|
|
142
|
-
toast.error(message);
|
|
143
|
-
} finally {
|
|
144
|
-
setIsDeleting(false);
|
|
145
|
-
setDeleteId(undefined);
|
|
146
|
-
}
|
|
144
|
+
deleteMutation.mutate({ id: deleteId });
|
|
147
145
|
};
|
|
148
146
|
|
|
149
|
-
const handleComplete =
|
|
147
|
+
const handleComplete = () => {
|
|
150
148
|
if (!completeId) return;
|
|
151
|
-
|
|
152
|
-
setIsCompleting(true);
|
|
153
|
-
try {
|
|
154
|
-
await api.closeMaintenance({ id: completeId });
|
|
155
|
-
toast.success("Maintenance completed");
|
|
156
|
-
loadData();
|
|
157
|
-
} catch (error) {
|
|
158
|
-
const message =
|
|
159
|
-
error instanceof Error ? error.message : "Failed to complete";
|
|
160
|
-
toast.error(message);
|
|
161
|
-
} finally {
|
|
162
|
-
setIsCompleting(false);
|
|
163
|
-
setCompleteId(undefined);
|
|
164
|
-
}
|
|
149
|
+
completeMutation.mutate({ id: completeId });
|
|
165
150
|
};
|
|
166
151
|
|
|
167
152
|
const handleSave = () => {
|
|
168
153
|
setEditorOpen(false);
|
|
169
|
-
|
|
154
|
+
void refetchMaintenances();
|
|
170
155
|
};
|
|
171
156
|
|
|
172
157
|
const getSystemNames = (systemIds: string[]): string => {
|
|
@@ -186,7 +171,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
186
171
|
<PageLayout
|
|
187
172
|
title="Planned Maintenances"
|
|
188
173
|
subtitle="Manage scheduled maintenance windows for systems"
|
|
189
|
-
loading={
|
|
174
|
+
loading={accessLoading}
|
|
190
175
|
allowed={canManage}
|
|
191
176
|
actions={
|
|
192
177
|
<Button onClick={handleCreate}>
|
|
@@ -326,7 +311,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
326
311
|
confirmText="Delete"
|
|
327
312
|
variant="danger"
|
|
328
313
|
onConfirm={handleDelete}
|
|
329
|
-
isLoading={
|
|
314
|
+
isLoading={deleteMutation.isPending}
|
|
330
315
|
/>
|
|
331
316
|
|
|
332
317
|
<ConfirmationModal
|
|
@@ -337,7 +322,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
|
|
|
337
322
|
confirmText="Complete"
|
|
338
323
|
variant="info"
|
|
339
324
|
onConfirm={handleComplete}
|
|
340
|
-
isLoading={
|
|
325
|
+
isLoading={completeMutation.isPending}
|
|
341
326
|
/>
|
|
342
327
|
</PageLayout>
|
|
343
328
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useParams,
|
|
4
4
|
Link,
|
|
@@ -6,20 +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 {
|
|
16
|
-
import { maintenanceRoutes } from "@checkstack/maintenance-common";
|
|
17
|
-
import type { MaintenanceDetail } from "@checkstack/maintenance-common";
|
|
15
|
+
import { MaintenanceApi } from "../api";
|
|
18
16
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "@checkstack/catalog-common";
|
|
17
|
+
maintenanceRoutes,
|
|
18
|
+
maintenanceAccess,
|
|
19
|
+
} from "@checkstack/maintenance-common";
|
|
20
|
+
import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
|
|
23
21
|
import {
|
|
24
22
|
Card,
|
|
25
23
|
CardHeader,
|
|
@@ -51,62 +49,55 @@ const MaintenanceDetailPageContent: React.FC = () => {
|
|
|
51
49
|
const { maintenanceId } = useParams<{ maintenanceId: string }>();
|
|
52
50
|
const navigate = useNavigate();
|
|
53
51
|
const [searchParams] = useSearchParams();
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
52
|
+
const maintenanceClient = usePluginClient(MaintenanceApi);
|
|
53
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
54
|
+
const accessApi = useApi(accessApiRef);
|
|
57
55
|
const toast = useToast();
|
|
58
56
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const { allowed: canManage } = permissionApi.useResourcePermission(
|
|
62
|
-
"maintenance",
|
|
63
|
-
"manage"
|
|
57
|
+
const { allowed: canManage } = accessApi.useAccess(
|
|
58
|
+
maintenanceAccess.maintenance.manage
|
|
64
59
|
);
|
|
65
60
|
|
|
66
|
-
const [maintenance, setMaintenance] = useState<MaintenanceDetail>();
|
|
67
|
-
const [systems, setSystems] = useState<System[]>([]);
|
|
68
|
-
const [loading, setLoading] = useState(true);
|
|
69
61
|
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
);
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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]);
|
|
73
|
+
// Fetch systems with useQuery
|
|
74
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
75
|
+
catalogClient.getSystems.useQuery({});
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
});
|
|
92
92
|
|
|
93
93
|
const handleUpdateSuccess = () => {
|
|
94
94
|
setShowUpdateForm(false);
|
|
95
|
-
|
|
95
|
+
void refetchMaintenance();
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
const handleComplete =
|
|
98
|
+
const handleComplete = () => {
|
|
99
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
|
-
}
|
|
100
|
+
completeMutation.mutate({ id: maintenanceId });
|
|
110
101
|
};
|
|
111
102
|
|
|
112
103
|
const getSystemName = (systemId: string): string => {
|
|
@@ -180,7 +171,12 @@ const MaintenanceDetailPageContent: React.FC = () => {
|
|
|
180
171
|
<div className="flex items-center gap-2">
|
|
181
172
|
{getMaintenanceStatusBadge(maintenance.status)}
|
|
182
173
|
{canComplete && (
|
|
183
|
-
<Button
|
|
174
|
+
<Button
|
|
175
|
+
variant="outline"
|
|
176
|
+
size="sm"
|
|
177
|
+
onClick={handleComplete}
|
|
178
|
+
disabled={completeMutation.isPending}
|
|
179
|
+
>
|
|
184
180
|
<CheckCircle2 className="h-4 w-4 mr-1" />
|
|
185
181
|
Complete
|
|
186
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) {
|