@checkstack/catalog-frontend 0.2.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 +64 -0
- package/package.json +1 -1
- package/src/api.ts +2 -10
- package/src/components/CatalogConfigPage.tsx +157 -120
- package/src/components/CatalogPage.tsx +2 -4
- package/src/components/SystemDetailPage.tsx +78 -75
- package/src/index.tsx +2 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# @checkstack/catalog-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 [7a23261]
|
|
60
|
+
- @checkstack/frontend-api@0.2.0
|
|
61
|
+
- @checkstack/common@0.3.0
|
|
62
|
+
- @checkstack/auth-frontend@0.3.0
|
|
63
|
+
- @checkstack/catalog-common@1.2.0
|
|
64
|
+
- @checkstack/notification-common@0.2.0
|
|
65
|
+
- @checkstack/ui@0.2.1
|
|
66
|
+
|
|
3
67
|
## 0.2.0
|
|
4
68
|
|
|
5
69
|
### Minor Changes
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import { createApiRef } from "@checkstack/frontend-api";
|
|
2
|
-
import { CatalogApi } from "@checkstack/catalog-common";
|
|
3
|
-
import type { InferClient } from "@checkstack/common";
|
|
4
|
-
|
|
5
1
|
// Re-export types for convenience
|
|
6
2
|
export type { System, Group, View } from "@checkstack/catalog-common";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export type CatalogApiClient = InferClient<typeof CatalogApi>;
|
|
10
|
-
|
|
11
|
-
export const catalogApiRef =
|
|
12
|
-
createApiRef<CatalogApiClient>("plugin.catalog.api");
|
|
3
|
+
// Client definition is in @checkstack/catalog-common - use with usePluginClient
|
|
4
|
+
export { CatalogApi } from "@checkstack/catalog-common";
|
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
useApi,
|
|
5
5
|
accessApiRef,
|
|
6
6
|
ExtensionSlot,
|
|
7
|
+
usePluginClient,
|
|
7
8
|
} from "@checkstack/frontend-api";
|
|
8
|
-
import {
|
|
9
|
+
import { System, CatalogApi } from "../api";
|
|
9
10
|
import {
|
|
10
11
|
CatalogSystemActionsSlot,
|
|
11
12
|
catalogAccess,
|
|
@@ -30,25 +31,22 @@ import { SystemEditor } from "./SystemEditor";
|
|
|
30
31
|
import { GroupEditor } from "./GroupEditor";
|
|
31
32
|
|
|
32
33
|
export const CatalogConfigPage = () => {
|
|
33
|
-
const
|
|
34
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
34
35
|
const accessApi = useApi(accessApiRef);
|
|
35
36
|
const toast = useToast();
|
|
36
37
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
37
|
-
const { allowed: canManage, loading: accessLoading } =
|
|
38
|
-
|
|
38
|
+
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
39
|
+
catalogAccess.system.manage
|
|
40
|
+
);
|
|
39
41
|
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
const [loading, setLoading] = useState(true);
|
|
42
|
+
const [selectedGroupId, setSelectedGroupId] = useState("");
|
|
43
|
+
const [selectedSystemToAdd, setSelectedSystemToAdd] = useState("");
|
|
43
44
|
|
|
44
45
|
// Dialog state
|
|
45
46
|
const [isSystemEditorOpen, setIsSystemEditorOpen] = useState(false);
|
|
46
47
|
const [editingSystem, setEditingSystem] = useState<System | undefined>();
|
|
47
48
|
const [isGroupEditorOpen, setIsGroupEditorOpen] = useState(false);
|
|
48
49
|
|
|
49
|
-
const [selectedGroupId, setSelectedGroupId] = useState("");
|
|
50
|
-
const [selectedSystemToAdd, setSelectedSystemToAdd] = useState("");
|
|
51
|
-
|
|
52
50
|
// Confirmation modal state
|
|
53
51
|
const [confirmModal, setConfirmModal] = useState<{
|
|
54
52
|
isOpen: boolean;
|
|
@@ -62,31 +60,30 @@ export const CatalogConfigPage = () => {
|
|
|
62
60
|
onConfirm: () => {},
|
|
63
61
|
});
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
} finally {
|
|
83
|
-
setLoading(false);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
63
|
+
// Fetch systems with useQuery
|
|
64
|
+
const {
|
|
65
|
+
data: systemsData,
|
|
66
|
+
isLoading: systemsLoading,
|
|
67
|
+
refetch: refetchSystems,
|
|
68
|
+
} = catalogClient.getSystems.useQuery({});
|
|
69
|
+
|
|
70
|
+
// Fetch groups with useQuery
|
|
71
|
+
const {
|
|
72
|
+
data: groupsData,
|
|
73
|
+
isLoading: groupsLoading,
|
|
74
|
+
refetch: refetchGroups,
|
|
75
|
+
} = catalogClient.getGroups.useQuery({});
|
|
76
|
+
|
|
77
|
+
const systems = systemsData?.systems ?? [];
|
|
78
|
+
const groups = groupsData ?? [];
|
|
79
|
+
const loading = systemsLoading || groupsLoading;
|
|
86
80
|
|
|
81
|
+
// Set initial group selection
|
|
87
82
|
useEffect(() => {
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
if (groups.length > 0 && !selectedGroupId) {
|
|
84
|
+
setSelectedGroupId(groups[0].id);
|
|
85
|
+
}
|
|
86
|
+
}, [groups, selectedGroupId]);
|
|
90
87
|
|
|
91
88
|
// Handle ?action=create URL parameter (from command palette)
|
|
92
89
|
useEffect(() => {
|
|
@@ -98,128 +95,168 @@ export const CatalogConfigPage = () => {
|
|
|
98
95
|
}
|
|
99
96
|
}, [searchParams, canManage, setSearchParams]);
|
|
100
97
|
|
|
101
|
-
//
|
|
98
|
+
// Mutations
|
|
99
|
+
const createSystemMutation = catalogClient.createSystem.useMutation({
|
|
100
|
+
onSuccess: () => {
|
|
101
|
+
toast.success("System created successfully");
|
|
102
|
+
setIsSystemEditorOpen(false);
|
|
103
|
+
void refetchSystems();
|
|
104
|
+
},
|
|
105
|
+
onError: (error) => {
|
|
106
|
+
toast.error(
|
|
107
|
+
error instanceof Error ? error.message : "Failed to create system"
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const updateSystemMutation = catalogClient.updateSystem.useMutation({
|
|
113
|
+
onSuccess: () => {
|
|
114
|
+
toast.success("System updated successfully");
|
|
115
|
+
setIsSystemEditorOpen(false);
|
|
116
|
+
setEditingSystem(undefined);
|
|
117
|
+
void refetchSystems();
|
|
118
|
+
},
|
|
119
|
+
onError: (error) => {
|
|
120
|
+
toast.error(
|
|
121
|
+
error instanceof Error ? error.message : "Failed to update system"
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const deleteSystemMutation = catalogClient.deleteSystem.useMutation({
|
|
127
|
+
onSuccess: () => {
|
|
128
|
+
toast.success("System deleted successfully");
|
|
129
|
+
setConfirmModal({ ...confirmModal, isOpen: false });
|
|
130
|
+
void refetchSystems();
|
|
131
|
+
},
|
|
132
|
+
onError: (error) => {
|
|
133
|
+
toast.error(
|
|
134
|
+
error instanceof Error ? error.message : "Failed to delete system"
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const createGroupMutation = catalogClient.createGroup.useMutation({
|
|
140
|
+
onSuccess: () => {
|
|
141
|
+
toast.success("Group created successfully");
|
|
142
|
+
setIsGroupEditorOpen(false);
|
|
143
|
+
void refetchGroups();
|
|
144
|
+
},
|
|
145
|
+
onError: (error) => {
|
|
146
|
+
toast.error(
|
|
147
|
+
error instanceof Error ? error.message : "Failed to create group"
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const deleteGroupMutation = catalogClient.deleteGroup.useMutation({
|
|
153
|
+
onSuccess: () => {
|
|
154
|
+
toast.success("Group deleted successfully");
|
|
155
|
+
setConfirmModal({ ...confirmModal, isOpen: false });
|
|
156
|
+
void refetchGroups();
|
|
157
|
+
},
|
|
158
|
+
onError: (error) => {
|
|
159
|
+
toast.error(
|
|
160
|
+
error instanceof Error ? error.message : "Failed to delete group"
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const updateGroupMutation = catalogClient.updateGroup.useMutation({
|
|
166
|
+
onSuccess: () => {
|
|
167
|
+
toast.success("Group name updated successfully");
|
|
168
|
+
void refetchGroups();
|
|
169
|
+
},
|
|
170
|
+
onError: (error) => {
|
|
171
|
+
toast.error(
|
|
172
|
+
error instanceof Error ? error.message : "Failed to update group name"
|
|
173
|
+
);
|
|
174
|
+
throw error;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const addSystemToGroupMutation = catalogClient.addSystemToGroup.useMutation({
|
|
179
|
+
onSuccess: () => {
|
|
180
|
+
toast.success("System added to group successfully");
|
|
181
|
+
setSelectedSystemToAdd("");
|
|
182
|
+
void refetchGroups();
|
|
183
|
+
},
|
|
184
|
+
onError: (error) => {
|
|
185
|
+
toast.error(
|
|
186
|
+
error instanceof Error ? error.message : "Failed to add system to group"
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const removeSystemFromGroupMutation =
|
|
192
|
+
catalogClient.removeSystemFromGroup.useMutation({
|
|
193
|
+
onSuccess: () => {
|
|
194
|
+
toast.success("System removed from group successfully");
|
|
195
|
+
void refetchGroups();
|
|
196
|
+
},
|
|
197
|
+
onError: (error) => {
|
|
198
|
+
toast.error(
|
|
199
|
+
error instanceof Error
|
|
200
|
+
? error.message
|
|
201
|
+
: "Failed to remove system from group"
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Handlers
|
|
102
207
|
const handleSaveSystem = async (data: {
|
|
103
208
|
name: string;
|
|
104
209
|
description?: string;
|
|
105
210
|
}) => {
|
|
106
211
|
if (editingSystem) {
|
|
107
|
-
|
|
108
|
-
await catalogApi.updateSystem({
|
|
109
|
-
id: editingSystem.id,
|
|
110
|
-
data,
|
|
111
|
-
});
|
|
112
|
-
toast.success("System updated successfully");
|
|
113
|
-
setEditingSystem(undefined);
|
|
212
|
+
updateSystemMutation.mutate({ id: editingSystem.id, data });
|
|
114
213
|
} else {
|
|
115
|
-
|
|
116
|
-
await catalogApi.createSystem(data);
|
|
117
|
-
toast.success("System created successfully");
|
|
214
|
+
createSystemMutation.mutate(data);
|
|
118
215
|
}
|
|
119
|
-
setIsSystemEditorOpen(false);
|
|
120
|
-
await loadData();
|
|
121
216
|
};
|
|
122
217
|
|
|
123
218
|
const handleCreateGroup = async (data: { name: string }) => {
|
|
124
|
-
|
|
125
|
-
toast.success("Group created successfully");
|
|
126
|
-
await loadData();
|
|
219
|
+
createGroupMutation.mutate(data);
|
|
127
220
|
};
|
|
128
221
|
|
|
129
|
-
const handleDeleteSystem =
|
|
222
|
+
const handleDeleteSystem = (id: string) => {
|
|
130
223
|
const system = systems.find((s) => s.id === id);
|
|
131
224
|
setConfirmModal({
|
|
132
225
|
isOpen: true,
|
|
133
226
|
title: "Delete System",
|
|
134
227
|
message: `Are you sure you want to delete "${system?.name}"? This will remove the system from all groups as well.`,
|
|
135
|
-
onConfirm:
|
|
136
|
-
|
|
137
|
-
await catalogApi.deleteSystem(id);
|
|
138
|
-
setConfirmModal({ ...confirmModal, isOpen: false });
|
|
139
|
-
toast.success("System deleted successfully");
|
|
140
|
-
loadData();
|
|
141
|
-
} catch (error) {
|
|
142
|
-
const message =
|
|
143
|
-
error instanceof Error ? error.message : "Failed to delete system";
|
|
144
|
-
toast.error(message);
|
|
145
|
-
console.error("Failed to delete system:", error);
|
|
146
|
-
}
|
|
228
|
+
onConfirm: () => {
|
|
229
|
+
deleteSystemMutation.mutate(id);
|
|
147
230
|
},
|
|
148
231
|
});
|
|
149
232
|
};
|
|
150
233
|
|
|
151
|
-
const handleDeleteGroup =
|
|
234
|
+
const handleDeleteGroup = (id: string) => {
|
|
152
235
|
const group = groups.find((g) => g.id === id);
|
|
153
236
|
setConfirmModal({
|
|
154
237
|
isOpen: true,
|
|
155
238
|
title: "Delete Group",
|
|
156
239
|
message: `Are you sure you want to delete "${group?.name}"? This action cannot be undone.`,
|
|
157
|
-
onConfirm:
|
|
158
|
-
|
|
159
|
-
await catalogApi.deleteGroup(id);
|
|
160
|
-
setConfirmModal({ ...confirmModal, isOpen: false });
|
|
161
|
-
toast.success("Group deleted successfully");
|
|
162
|
-
loadData();
|
|
163
|
-
} catch (error) {
|
|
164
|
-
const message =
|
|
165
|
-
error instanceof Error ? error.message : "Failed to delete group";
|
|
166
|
-
toast.error(message);
|
|
167
|
-
console.error("Failed to delete group:", error);
|
|
168
|
-
}
|
|
240
|
+
onConfirm: () => {
|
|
241
|
+
deleteGroupMutation.mutate(id);
|
|
169
242
|
},
|
|
170
243
|
});
|
|
171
244
|
};
|
|
172
245
|
|
|
173
|
-
const handleAddSystemToGroup =
|
|
246
|
+
const handleAddSystemToGroup = () => {
|
|
174
247
|
if (!selectedGroupId || !selectedSystemToAdd) return;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
180
|
-
setSelectedSystemToAdd("");
|
|
181
|
-
toast.success("System added to group successfully");
|
|
182
|
-
loadData();
|
|
183
|
-
} catch (error) {
|
|
184
|
-
const message =
|
|
185
|
-
error instanceof Error
|
|
186
|
-
? error.message
|
|
187
|
-
: "Failed to add system to group";
|
|
188
|
-
toast.error(message);
|
|
189
|
-
console.error("Failed to add system to group:", error);
|
|
190
|
-
}
|
|
248
|
+
addSystemToGroupMutation.mutate({
|
|
249
|
+
groupId: selectedGroupId,
|
|
250
|
+
systemId: selectedSystemToAdd,
|
|
251
|
+
});
|
|
191
252
|
};
|
|
192
253
|
|
|
193
|
-
const handleRemoveSystemFromGroup =
|
|
194
|
-
groupId
|
|
195
|
-
systemId: string
|
|
196
|
-
) => {
|
|
197
|
-
try {
|
|
198
|
-
await catalogApi.removeSystemFromGroup({ groupId, systemId });
|
|
199
|
-
toast.success("System removed from group successfully");
|
|
200
|
-
loadData();
|
|
201
|
-
} catch (error) {
|
|
202
|
-
const message =
|
|
203
|
-
error instanceof Error
|
|
204
|
-
? error.message
|
|
205
|
-
: "Failed to remove system from group";
|
|
206
|
-
toast.error(message);
|
|
207
|
-
console.error("Failed to remove system from group:", error);
|
|
208
|
-
}
|
|
254
|
+
const handleRemoveSystemFromGroup = (groupId: string, systemId: string) => {
|
|
255
|
+
removeSystemFromGroupMutation.mutate({ groupId, systemId });
|
|
209
256
|
};
|
|
210
257
|
|
|
211
|
-
const handleUpdateGroupName =
|
|
212
|
-
|
|
213
|
-
await catalogApi.updateGroup({ id, data: { name: newName } });
|
|
214
|
-
toast.success("Group name updated successfully");
|
|
215
|
-
loadData();
|
|
216
|
-
} catch (error) {
|
|
217
|
-
const message =
|
|
218
|
-
error instanceof Error ? error.message : "Failed to update group name";
|
|
219
|
-
toast.error(message);
|
|
220
|
-
console.error("Failed to update group name:", error);
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
258
|
+
const handleUpdateGroupName = (id: string, newName: string) => {
|
|
259
|
+
updateGroupMutation.mutate({ id, data: { name: newName } });
|
|
223
260
|
};
|
|
224
261
|
|
|
225
262
|
if (loading || accessLoading) return <LoadingSpinner />;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useApi, loggerApiRef } from "@checkstack/frontend-api";
|
|
3
|
-
import { catalogApiRef } from "../api";
|
|
4
3
|
|
|
5
4
|
export const CatalogPage = () => {
|
|
6
5
|
const logger = useApi(loggerApiRef);
|
|
7
|
-
const catalog = useApi(catalogApiRef);
|
|
8
6
|
|
|
9
7
|
React.useEffect(() => {
|
|
10
|
-
logger.info("Catalog Page loaded"
|
|
11
|
-
}, [logger
|
|
8
|
+
logger.info("Catalog Page loaded");
|
|
9
|
+
}, [logger]);
|
|
12
10
|
|
|
13
11
|
return (
|
|
14
12
|
<div className="p-4 rounded-lg bg-white shadow">
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { useParams, useNavigate } from "react-router-dom";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
usePluginClient,
|
|
5
|
+
ExtensionSlot,
|
|
6
|
+
useApi,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { Group, CatalogApi } from "../api";
|
|
6
9
|
import {
|
|
7
10
|
SystemDetailsSlot,
|
|
8
11
|
SystemDetailsTopSlot,
|
|
@@ -28,16 +31,13 @@ const CATALOG_PLUGIN_ID = "catalog";
|
|
|
28
31
|
export const SystemDetailPage: React.FC = () => {
|
|
29
32
|
const { systemId } = useParams<{ systemId: string }>();
|
|
30
33
|
const navigate = useNavigate();
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const notificationApi = rpcApi.forPlugin(NotificationApi);
|
|
34
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
35
|
+
const notificationClient = usePluginClient(NotificationApi);
|
|
34
36
|
const toast = useToast();
|
|
35
37
|
const authApi = useApi(authApiRef);
|
|
36
38
|
const { data: session } = authApi.useSession();
|
|
37
39
|
|
|
38
|
-
const [system, setSystem] = useState<System | undefined>();
|
|
39
40
|
const [groups, setGroups] = useState<Group[]>([]);
|
|
40
|
-
const [loading, setLoading] = useState(true);
|
|
41
41
|
const [notFound, setNotFound] = useState(false);
|
|
42
42
|
|
|
43
43
|
// Subscription state
|
|
@@ -49,85 +49,84 @@ export const SystemDetailPage: React.FC = () => {
|
|
|
49
49
|
return `${CATALOG_PLUGIN_ID}.system.${systemId}`;
|
|
50
50
|
}, [systemId]);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
// Fetch system data with useQuery
|
|
53
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
54
|
+
catalogClient.getSystems.useQuery({});
|
|
55
|
+
|
|
56
|
+
// Fetch groups data with useQuery
|
|
57
|
+
const { data: groupsData, isLoading: groupsLoading } =
|
|
58
|
+
catalogClient.getGroups.useQuery({});
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Find the system from the fetched data
|
|
61
|
+
const system = systemsData?.systems.find((s) => s.id === systemId);
|
|
62
|
+
const loading = systemsLoading || groupsLoading;
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
64
|
+
// Fetch subscriptions with useQuery
|
|
65
|
+
const { data: subscriptions, refetch: refetchSubscriptions } =
|
|
66
|
+
notificationClient.getSubscriptions.useQuery({});
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// Subscribe/unsubscribe mutations
|
|
69
|
+
const subscribeMutation = notificationClient.subscribe.useMutation({
|
|
70
|
+
onSuccess: () => {
|
|
71
|
+
setIsSubscribed(true);
|
|
72
|
+
toast.success("Subscribed to system notifications");
|
|
73
|
+
void refetchSubscriptions();
|
|
74
|
+
},
|
|
75
|
+
onError: (error) => {
|
|
76
|
+
toast.error(
|
|
77
|
+
error instanceof Error ? error.message : "Failed to subscribe"
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
+
const unsubscribeMutation = notificationClient.unsubscribe.useMutation({
|
|
83
|
+
onSuccess: () => {
|
|
84
|
+
setIsSubscribed(false);
|
|
85
|
+
toast.success("Unsubscribed from system notifications");
|
|
86
|
+
void refetchSubscriptions();
|
|
87
|
+
},
|
|
88
|
+
onError: (error) => {
|
|
89
|
+
toast.error(
|
|
90
|
+
error instanceof Error ? error.message : "Failed to unsubscribe"
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
82
94
|
|
|
83
|
-
//
|
|
95
|
+
// Update not found state
|
|
84
96
|
useEffect(() => {
|
|
85
|
-
if (!systemId)
|
|
97
|
+
if (!systemsLoading && !system && systemId) {
|
|
98
|
+
setNotFound(true);
|
|
99
|
+
}
|
|
100
|
+
}, [system, systemsLoading, systemId]);
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
})
|
|
97
|
-
.catch((error) => {
|
|
98
|
-
console.error("Failed to check subscription status:", error);
|
|
99
|
-
})
|
|
100
|
-
.finally(() => setSubscriptionLoading(false));
|
|
101
|
-
}, [systemId, notificationApi, getSystemGroupId]);
|
|
102
|
+
// Update groups that contain this system
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (groupsData && systemId) {
|
|
105
|
+
const systemGroups = groupsData.filter((group) =>
|
|
106
|
+
group.systemIds?.includes(systemId)
|
|
107
|
+
);
|
|
108
|
+
setGroups(systemGroups);
|
|
109
|
+
}
|
|
110
|
+
}, [groupsData, systemId]);
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} catch (error) {
|
|
110
|
-
const message =
|
|
111
|
-
error instanceof Error ? error.message : "Failed to subscribe";
|
|
112
|
-
toast.error(message);
|
|
113
|
-
} finally {
|
|
112
|
+
// Update subscription status from query
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (subscriptions && systemId) {
|
|
115
|
+
const groupId = getSystemGroupId();
|
|
116
|
+
const hasSubscription = subscriptions.some((s) => s.groupId === groupId);
|
|
117
|
+
setIsSubscribed(hasSubscription);
|
|
114
118
|
setSubscriptionLoading(false);
|
|
115
119
|
}
|
|
120
|
+
}, [subscriptions, systemId, getSystemGroupId]);
|
|
121
|
+
|
|
122
|
+
const handleSubscribe = () => {
|
|
123
|
+
setSubscriptionLoading(true);
|
|
124
|
+
subscribeMutation.mutate({ groupId: getSystemGroupId() });
|
|
116
125
|
};
|
|
117
126
|
|
|
118
|
-
const handleUnsubscribe =
|
|
127
|
+
const handleUnsubscribe = () => {
|
|
119
128
|
setSubscriptionLoading(true);
|
|
120
|
-
|
|
121
|
-
await notificationApi.unsubscribe({ groupId: getSystemGroupId() });
|
|
122
|
-
setIsSubscribed(false);
|
|
123
|
-
toast.success("Unsubscribed from system notifications");
|
|
124
|
-
} catch (error) {
|
|
125
|
-
const message =
|
|
126
|
-
error instanceof Error ? error.message : "Failed to unsubscribe";
|
|
127
|
-
toast.error(message);
|
|
128
|
-
} finally {
|
|
129
|
-
setSubscriptionLoading(false);
|
|
130
|
-
}
|
|
129
|
+
unsubscribeMutation.mutate({ groupId: getSystemGroupId() });
|
|
131
130
|
};
|
|
132
131
|
|
|
133
132
|
if (loading) {
|
|
@@ -175,7 +174,11 @@ export const SystemDetailPage: React.FC = () => {
|
|
|
175
174
|
isSubscribed={isSubscribed}
|
|
176
175
|
onSubscribe={handleSubscribe}
|
|
177
176
|
onUnsubscribe={handleUnsubscribe}
|
|
178
|
-
loading={
|
|
177
|
+
loading={
|
|
178
|
+
subscriptionLoading ||
|
|
179
|
+
subscribeMutation.isPending ||
|
|
180
|
+
unsubscribeMutation.isPending
|
|
181
|
+
}
|
|
179
182
|
/>
|
|
180
183
|
)}
|
|
181
184
|
<BackLink onClick={() => navigate("/")}>Back to Dashboard</BackLink>
|
package/src/index.tsx
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
rpcApiRef,
|
|
3
|
-
ApiRef,
|
|
4
2
|
UserMenuItemsSlot,
|
|
5
3
|
createSlotExtension,
|
|
6
4
|
createFrontendPlugin,
|
|
7
5
|
} from "@checkstack/frontend-api";
|
|
8
|
-
import { catalogApiRef, type CatalogApiClient } from "./api";
|
|
9
6
|
import {
|
|
10
7
|
catalogRoutes,
|
|
11
|
-
CatalogApi,
|
|
12
8
|
pluginMetadata,
|
|
13
9
|
catalogAccess,
|
|
14
10
|
} from "@checkstack/catalog-common";
|
|
@@ -20,16 +16,8 @@ import { SystemDetailPage } from "./components/SystemDetailPage";
|
|
|
20
16
|
|
|
21
17
|
export const catalogPlugin = createFrontendPlugin({
|
|
22
18
|
metadata: pluginMetadata,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ref: catalogApiRef,
|
|
26
|
-
factory: (deps: { get: <T>(ref: ApiRef<T>) => T }): CatalogApiClient => {
|
|
27
|
-
const rpcApi = deps.get(rpcApiRef);
|
|
28
|
-
// CatalogApiClient is derived from the contract type
|
|
29
|
-
return rpcApi.forPlugin(CatalogApi);
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
],
|
|
19
|
+
// No APIs needed - components use usePluginClient() directly
|
|
20
|
+
apis: [],
|
|
33
21
|
routes: [
|
|
34
22
|
{
|
|
35
23
|
route: catalogRoutes.routes.home,
|