@checkstack/dashboard-frontend 0.3.1 → 0.3.3

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,24 @@
1
1
  # @checkstack/dashboard-frontend
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - cad3073: Fixed notification group subscription for catalog groups:
8
+ - Fixed group ID format using colon separator instead of dots and missing entity type prefix
9
+ - Fixed subscription button state not updating after subscribe/unsubscribe by using refetch instead of invalidateQueries
10
+
11
+ ## 0.3.2
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [f6464a2]
16
+ - @checkstack/ui@0.2.3
17
+ - @checkstack/auth-frontend@0.4.1
18
+ - @checkstack/catalog-frontend@0.3.3
19
+ - @checkstack/command-frontend@0.2.2
20
+ - @checkstack/queue-frontend@0.2.2
21
+
3
22
  ## 0.3.1
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@checkstack/dashboard-frontend",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
7
+ "test": "bun test",
7
8
  "typecheck": "tsc --noEmit",
8
9
  "lint": "bun run lint:code",
9
10
  "lint:code": "eslint . --max-warnings 0"
package/src/Dashboard.tsx CHANGED
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
3
3
  import {
4
4
  useApi,
5
5
  usePluginClient,
6
- useQueryClient,
7
6
  ExtensionSlot,
8
7
  } from "@checkstack/frontend-api";
9
8
  import {
@@ -57,10 +56,10 @@ interface GroupWithSystems extends Group {
57
56
  systems: System[];
58
57
  }
59
58
 
60
- const getGroupId = (groupId: string) => `${CATALOG_PLUGIN_ID}:${groupId}`;
59
+ const getGroupId = (groupId: string) => `${CATALOG_PLUGIN_ID}.group.${groupId}`;
61
60
 
62
61
  const statusToVariant = (
63
- status: string
62
+ status: string,
64
63
  ): "default" | "success" | "warning" | "error" => {
65
64
  switch (status) {
66
65
  case "healthy": {
@@ -87,7 +86,6 @@ export const Dashboard: React.FC = () => {
87
86
  const navigate = useNavigate();
88
87
  const toast = useToast();
89
88
  const authApi = useApi(authApiRef);
90
- const queryClient = useQueryClient();
91
89
  const { data: session } = authApi.useSession();
92
90
 
93
91
  // Terminal feed entries from real healthcheck signals
@@ -112,7 +110,7 @@ export const Dashboard: React.FC = () => {
112
110
  const { data: incidentsData, isLoading: incidentsLoading } =
113
111
  incidentClient.listIncidents.useQuery(
114
112
  { includeResolved: false },
115
- { staleTime: 30_000 }
113
+ { staleTime: 30_000 },
116
114
  );
117
115
  const incidents = incidentsData?.incidents ?? [];
118
116
 
@@ -120,15 +118,15 @@ export const Dashboard: React.FC = () => {
120
118
  const { data: maintenancesData, isLoading: maintenancesLoading } =
121
119
  maintenanceClient.listMaintenances.useQuery(
122
120
  { status: "in_progress" },
123
- { staleTime: 30_000 }
121
+ { staleTime: 30_000 },
124
122
  );
125
123
  const maintenances = maintenancesData?.maintenances ?? [];
126
124
 
127
125
  // Fetch subscriptions (only when logged in)
128
- const { data: subscriptions = [] } =
126
+ const { data: subscriptions = [], refetch: refetchSubscriptions } =
129
127
  notificationClient.getSubscriptions.useQuery(
130
128
  {},
131
- { enabled: !!session, staleTime: 60_000 }
129
+ { enabled: !!session, staleTime: 60_000 },
132
130
  );
133
131
 
134
132
  // Combined loading state
@@ -141,8 +139,7 @@ export const Dashboard: React.FC = () => {
141
139
  const subscribeMutation = notificationClient.subscribe.useMutation({
142
140
  onSuccess: () => {
143
141
  toast.success("Subscribed to group notifications");
144
- // Invalidate subscriptions query to refetch
145
- queryClient.invalidateQueries({ queryKey: ["notification"] });
142
+ void refetchSubscriptions();
146
143
  },
147
144
  onError: (error: Error) => {
148
145
  toast.error(error.message || "Failed to subscribe");
@@ -152,7 +149,7 @@ export const Dashboard: React.FC = () => {
152
149
  const unsubscribeMutation = notificationClient.unsubscribe.useMutation({
153
150
  onSuccess: () => {
154
151
  toast.success("Unsubscribed from group notifications");
155
- queryClient.invalidateQueries({ queryKey: ["notification"] });
152
+ void refetchSubscriptions();
156
153
  },
157
154
  onError: (error: Error) => {
158
155
  toast.error(error.message || "Failed to unsubscribe");
@@ -195,9 +192,9 @@ export const Dashboard: React.FC = () => {
195
192
  };
196
193
 
197
194
  setTerminalEntries((prev) =>
198
- [newEntry, ...prev].slice(0, MAX_TERMINAL_ENTRIES)
195
+ [newEntry, ...prev].slice(0, MAX_TERMINAL_ENTRIES),
199
196
  );
200
- }
197
+ },
201
198
  );
202
199
 
203
200
  // -------------------------------------------------------------------------
@@ -211,7 +208,7 @@ export const Dashboard: React.FC = () => {
211
208
  const isSubscribed = (groupId: string) => {
212
209
  const fullId = getGroupId(groupId);
213
210
  return subscriptions.some(
214
- (s: EnrichedSubscription) => s.groupId === fullId
211
+ (s: EnrichedSubscription) => s.groupId === fullId,
215
212
  );
216
213
  };
217
214
 
@@ -224,7 +221,7 @@ export const Dashboard: React.FC = () => {
224
221
  onSettled: () => {
225
222
  setSubscriptionLoading((prev) => ({ ...prev, [groupId]: false }));
226
223
  },
227
- }
224
+ },
228
225
  );
229
226
  };
230
227
 
@@ -237,7 +234,7 @@ export const Dashboard: React.FC = () => {
237
234
  onSettled: () => {
238
235
  setSubscriptionLoading((prev) => ({ ...prev, [groupId]: false }));
239
236
  },
240
- }
237
+ },
241
238
  );
242
239
  };
243
240
 
@@ -262,7 +259,7 @@ export const Dashboard: React.FC = () => {
262
259
 
263
260
  // Collect all system IDs for bulk data fetching
264
261
  const allSystemIds = groupsWithSystems.flatMap((g) =>
265
- g.systems.map((s) => s.id)
262
+ g.systems.map((s) => s.id),
266
263
  );
267
264
 
268
265
  return (
@@ -0,0 +1,115 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ /**
4
+ * Notification Group ID Format Tests
5
+ *
6
+ * These tests ensure the correct format for notification group IDs.
7
+ * The format must match what the backend creates:
8
+ * - Format: `{pluginId}.{entityType}.{entityId}`
9
+ * - Example: `catalog.group.855f7a1f-7287-4650-abf3-f91117e3bde1`
10
+ *
11
+ * Regression test for: Dashboard subscription failures due to incorrect
12
+ * group ID format (was using colon separator and missing entity type prefix).
13
+ */
14
+
15
+ const CATALOG_PLUGIN_ID = "catalog";
16
+
17
+ /**
18
+ * Constructs the full notification group ID for a catalog group.
19
+ * Must match the format created by catalog-backend notification group creation.
20
+ */
21
+ export const getCatalogGroupNotificationId = (groupId: string) =>
22
+ `${CATALOG_PLUGIN_ID}.group.${groupId}`;
23
+
24
+ /**
25
+ * Constructs the full notification group ID for a catalog system.
26
+ * Must match the format created by catalog-backend notification group creation.
27
+ */
28
+ export const getCatalogSystemNotificationId = (systemId: string) =>
29
+ `${CATALOG_PLUGIN_ID}.system.${systemId}`;
30
+
31
+ describe("Notification Group ID Format", () => {
32
+ describe("getCatalogGroupNotificationId", () => {
33
+ test("uses dot separators, not colons", () => {
34
+ const groupId = "test-uuid";
35
+ const result = getCatalogGroupNotificationId(groupId);
36
+
37
+ // Must use dots, not colons
38
+ expect(result).not.toContain(":");
39
+ expect(result).toBe("catalog.group.test-uuid");
40
+ });
41
+
42
+ test("includes 'group' type prefix", () => {
43
+ const groupId = "855f7a1f-7287-4650-abf3-f91117e3bde1";
44
+ const result = getCatalogGroupNotificationId(groupId);
45
+
46
+ // Must include the entity type
47
+ expect(result).toContain(".group.");
48
+ expect(result).toBe("catalog.group.855f7a1f-7287-4650-abf3-f91117e3bde1");
49
+ });
50
+
51
+ test("follows {pluginId}.{entityType}.{entityId} format", () => {
52
+ const groupId = "my-group-id";
53
+ const result = getCatalogGroupNotificationId(groupId);
54
+
55
+ const parts = result.split(".");
56
+ expect(parts).toHaveLength(3);
57
+ expect(parts[0]).toBe("catalog"); // pluginId
58
+ expect(parts[1]).toBe("group"); // entityType
59
+ expect(parts[2]).toBe("my-group-id"); // entityId
60
+ });
61
+ });
62
+
63
+ describe("getCatalogSystemNotificationId", () => {
64
+ test("uses dot separators, not colons", () => {
65
+ const systemId = "test-uuid";
66
+ const result = getCatalogSystemNotificationId(systemId);
67
+
68
+ // Must use dots, not colons
69
+ expect(result).not.toContain(":");
70
+ expect(result).toBe("catalog.system.test-uuid");
71
+ });
72
+
73
+ test("includes 'system' type prefix", () => {
74
+ const systemId = "855f7a1f-7287-4650-abf3-f91117e3bde1";
75
+ const result = getCatalogSystemNotificationId(systemId);
76
+
77
+ // Must include the entity type
78
+ expect(result).toContain(".system.");
79
+ expect(result).toBe(
80
+ "catalog.system.855f7a1f-7287-4650-abf3-f91117e3bde1",
81
+ );
82
+ });
83
+
84
+ test("follows {pluginId}.{entityType}.{entityId} format", () => {
85
+ const systemId = "my-system-id";
86
+ const result = getCatalogSystemNotificationId(systemId);
87
+
88
+ const parts = result.split(".");
89
+ expect(parts).toHaveLength(3);
90
+ expect(parts[0]).toBe("catalog"); // pluginId
91
+ expect(parts[1]).toBe("system"); // entityType
92
+ expect(parts[2]).toBe("my-system-id"); // entityId
93
+ });
94
+ });
95
+
96
+ describe("Format consistency between group and system", () => {
97
+ test("both use same separator and structure", () => {
98
+ const id = "test-id";
99
+ const groupResult = getCatalogGroupNotificationId(id);
100
+ const systemResult = getCatalogSystemNotificationId(id);
101
+
102
+ // Same plugin prefix
103
+ expect(groupResult.startsWith("catalog.")).toBe(true);
104
+ expect(systemResult.startsWith("catalog.")).toBe(true);
105
+
106
+ // Same structure (3 parts)
107
+ expect(groupResult.split(".")).toHaveLength(3);
108
+ expect(systemResult.split(".")).toHaveLength(3);
109
+
110
+ // Different entity types
111
+ expect(groupResult).toContain(".group.");
112
+ expect(systemResult).toContain(".system.");
113
+ });
114
+ });
115
+ });