@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/catalog-frontend",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
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
- // CatalogApi client type inferred from the client definition
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 { catalogApiRef, System, Group } from "../api";
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 catalogApi = useApi(catalogApiRef);
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
- accessApi.useAccess(catalogAccess.system.manage);
38
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
39
+ catalogAccess.system.manage
40
+ );
39
41
 
40
- const [systems, setSystems] = useState<System[]>([]);
41
- const [groups, setGroups] = useState<Group[]>([]);
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
- const loadData = async () => {
66
- setLoading(true);
67
- try {
68
- const [{ systems: s }, g] = await Promise.all([
69
- catalogApi.getSystems(),
70
- catalogApi.getGroups(),
71
- ]);
72
- setSystems(s);
73
- setGroups(g);
74
- if (g.length > 0 && !selectedGroupId) {
75
- setSelectedGroupId(g[0].id);
76
- }
77
- } catch (error) {
78
- const message =
79
- error instanceof Error ? error.message : "Failed to load catalog data";
80
- toast.error(message);
81
- console.error("Failed to load catalog data:", error);
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
- loadData();
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
- // Unified save handler for both create and edit
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
- // Update existing system
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
- // Create new system
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
- await catalogApi.createGroup(data);
125
- toast.success("Group created successfully");
126
- await loadData();
219
+ createGroupMutation.mutate(data);
127
220
  };
128
221
 
129
- const handleDeleteSystem = async (id: string) => {
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: async () => {
136
- try {
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 = async (id: string) => {
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: async () => {
158
- try {
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 = async () => {
246
+ const handleAddSystemToGroup = () => {
174
247
  if (!selectedGroupId || !selectedSystemToAdd) return;
175
- try {
176
- await catalogApi.addSystemToGroup({
177
- groupId: selectedGroupId,
178
- systemId: selectedSystemToAdd,
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 = async (
194
- groupId: string,
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 = async (id: string, newName: string) => {
212
- try {
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", catalog);
11
- }, [logger, catalog]);
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 { useApi, rpcApiRef } from "@checkstack/frontend-api";
4
- import { catalogApiRef, System, Group } from "../api";
5
- import { ExtensionSlot } from "@checkstack/frontend-api";
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 catalogApi = useApi(catalogApiRef);
32
- const rpcApi = useApi(rpcApiRef);
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
- useEffect(() => {
53
- if (!systemId) {
54
- setNotFound(true);
55
- setLoading(false);
56
- return;
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
- Promise.all([catalogApi.getSystems(), catalogApi.getGroups()])
60
- .then(([{ systems }, allGroups]) => {
61
- const foundSystem = systems.find((s) => s.id === systemId);
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
- if (!foundSystem) {
64
- setNotFound(true);
65
- return;
66
- }
64
+ // Fetch subscriptions with useQuery
65
+ const { data: subscriptions, refetch: refetchSubscriptions } =
66
+ notificationClient.getSubscriptions.useQuery({});
67
67
 
68
- setSystem(foundSystem);
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
- // Find groups that contain this system
71
- const systemGroups = allGroups.filter((group) =>
72
- group.systemIds?.includes(systemId)
73
- );
74
- setGroups(systemGroups);
75
- })
76
- .catch((error) => {
77
- console.error("Error fetching system details:", error);
78
- setNotFound(true);
79
- })
80
- .finally(() => setLoading(false));
81
- }, [systemId, catalogApi]);
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
- // Check subscription status
95
+ // Update not found state
84
96
  useEffect(() => {
85
- if (!systemId) return;
97
+ if (!systemsLoading && !system && systemId) {
98
+ setNotFound(true);
99
+ }
100
+ }, [system, systemsLoading, systemId]);
86
101
 
87
- setSubscriptionLoading(true);
88
- notificationApi
89
- .getSubscriptions()
90
- .then((subscriptions) => {
91
- const groupId = getSystemGroupId();
92
- const hasSubscription = subscriptions.some(
93
- (s) => s.groupId === groupId
94
- );
95
- setIsSubscribed(hasSubscription);
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
- const handleSubscribe = async () => {
104
- setSubscriptionLoading(true);
105
- try {
106
- await notificationApi.subscribe({ groupId: getSystemGroupId() });
107
- setIsSubscribed(true);
108
- toast.success("Subscribed to system notifications");
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 = async () => {
127
+ const handleUnsubscribe = () => {
119
128
  setSubscriptionLoading(true);
120
- try {
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={subscriptionLoading}
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
- apis: [
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,