@checkstack/catalog-frontend 0.0.4 → 0.2.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,168 @@
1
1
  # @checkstack/catalog-frontend
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9faec1f: # Unified AccessRule Terminology Refactoring
8
+
9
+ This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
10
+
11
+ ## Changes
12
+
13
+ ### Core Infrastructure (`@checkstack/common`)
14
+
15
+ - Introduced `AccessRule` interface as the primary access control type
16
+ - Added `accessPair()` helper for creating read/manage access rule pairs
17
+ - Added `access()` builder for individual access rules
18
+ - Replaced `Permission` type with `AccessRule` throughout
19
+
20
+ ### API Changes
21
+
22
+ - `env.registerPermissions()` → `env.registerAccessRules()`
23
+ - `meta.permissions` → `meta.access` in RPC contracts
24
+ - `usePermission()` → `useAccess()` in frontend hooks
25
+ - Route `permission:` field → `accessRule:` field
26
+
27
+ ### UI Changes
28
+
29
+ - "Roles & Permissions" tab → "Roles & Access Rules"
30
+ - "You don't have permission..." → "You don't have access..."
31
+ - All permission-related UI text updated
32
+
33
+ ### Documentation & Templates
34
+
35
+ - Updated 18 documentation files with AccessRule terminology
36
+ - Updated 7 scaffolding templates with `accessPair()` pattern
37
+ - All code examples use new AccessRule API
38
+
39
+ ## Migration Guide
40
+
41
+ ### Backend Plugins
42
+
43
+ ```diff
44
+ - import { permissionList } from "./permissions";
45
+ - env.registerPermissions(permissionList);
46
+ + import { accessRules } from "./access";
47
+ + env.registerAccessRules(accessRules);
48
+ ```
49
+
50
+ ### RPC Contracts
51
+
52
+ ```diff
53
+ - .meta({ userType: "user", permissions: [permissions.read.id] })
54
+ + .meta({ userType: "user", access: [access.read] })
55
+ ```
56
+
57
+ ### Frontend Hooks
58
+
59
+ ```diff
60
+ - const canRead = accessApi.usePermission(permissions.read.id);
61
+ + const canRead = accessApi.useAccess(access.read);
62
+ ```
63
+
64
+ ### Routes
65
+
66
+ ```diff
67
+ - permission: permissions.entityRead.id,
68
+ + accessRule: access.read,
69
+ ```
70
+
71
+ ### Patch Changes
72
+
73
+ - Updated dependencies [9faec1f]
74
+ - Updated dependencies [95eeec7]
75
+ - Updated dependencies [f533141]
76
+ - @checkstack/auth-frontend@0.2.0
77
+ - @checkstack/catalog-common@1.1.0
78
+ - @checkstack/common@0.2.0
79
+ - @checkstack/frontend-api@0.1.0
80
+ - @checkstack/notification-common@0.1.0
81
+ - @checkstack/ui@0.2.0
82
+
83
+ ## 0.1.0
84
+
85
+ ### Minor Changes
86
+
87
+ - 8e43507: # Teams and Resource-Level Access Control
88
+
89
+ This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
90
+
91
+ ## Features
92
+
93
+ ### Team Management
94
+
95
+ - Create, update, and delete teams with name and description
96
+ - Add/remove users from teams
97
+ - Designate team managers with elevated privileges
98
+ - View team membership and manager status
99
+
100
+ ### Resource-Level Access Control
101
+
102
+ - Grant teams access to specific resources (systems, health checks, incidents, maintenances)
103
+ - Configure read-only or manage permissions per team
104
+ - Resource-level "Team Only" mode that restricts access exclusively to team members
105
+ - Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
106
+ - Automatic cleanup of grants when teams are deleted (database cascade)
107
+
108
+ ### Middleware Integration
109
+
110
+ - Extended `autoAuthMiddleware` to support resource access checks
111
+ - Single-resource pre-handler validation for detail endpoints
112
+ - Automatic list filtering for collection endpoints
113
+ - S2S endpoints for access verification
114
+
115
+ ### Frontend Components
116
+
117
+ - `TeamsTab` component for managing teams in Auth Settings
118
+ - `TeamAccessEditor` component for assigning team access to resources
119
+ - Resource-level "Team Only" toggle in `TeamAccessEditor`
120
+ - Integration into System, Health Check, Incident, and Maintenance editors
121
+
122
+ ## Breaking Changes
123
+
124
+ ### API Response Format Changes
125
+
126
+ List endpoints now return objects with named keys instead of arrays directly:
127
+
128
+ ```typescript
129
+ // Before
130
+ const systems = await catalogApi.getSystems();
131
+
132
+ // After
133
+ const { systems } = await catalogApi.getSystems();
134
+ ```
135
+
136
+ Affected endpoints:
137
+
138
+ - `catalog.getSystems` → `{ systems: [...] }`
139
+ - `healthcheck.getConfigurations` → `{ configurations: [...] }`
140
+ - `incident.listIncidents` → `{ incidents: [...] }`
141
+ - `maintenance.listMaintenances` → `{ maintenances: [...] }`
142
+
143
+ ### User Identity Enrichment
144
+
145
+ `RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
146
+
147
+ ## Documentation
148
+
149
+ See `docs/backend/teams.md` for complete API reference and integration guide.
150
+
151
+ ### Patch Changes
152
+
153
+ - 97c5a6b: Fix Radix UI accessibility warning in dialog components by adding visually hidden DialogDescription components
154
+ - Updated dependencies [8e43507]
155
+ - Updated dependencies [97c5a6b]
156
+ - Updated dependencies [97c5a6b]
157
+ - Updated dependencies [8e43507]
158
+ - Updated dependencies [8e43507]
159
+ - @checkstack/ui@0.1.0
160
+ - @checkstack/auth-frontend@0.1.0
161
+ - @checkstack/catalog-common@1.0.0
162
+ - @checkstack/common@0.1.0
163
+ - @checkstack/frontend-api@0.0.4
164
+ - @checkstack/notification-common@0.0.4
165
+
3
166
  ## 0.0.4
4
167
 
5
168
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/catalog-frontend",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -2,11 +2,14 @@ import React, { useState, useEffect } from "react";
2
2
  import { useSearchParams } from "react-router-dom";
3
3
  import {
4
4
  useApi,
5
- permissionApiRef,
5
+ accessApiRef,
6
6
  ExtensionSlot,
7
7
  } from "@checkstack/frontend-api";
8
8
  import { catalogApiRef, System, Group } from "../api";
9
- import { CatalogSystemActionsSlot } from "@checkstack/catalog-common";
9
+ import {
10
+ CatalogSystemActionsSlot,
11
+ catalogAccess,
12
+ } from "@checkstack/catalog-common";
10
13
  import {
11
14
  SectionHeader,
12
15
  Card,
@@ -17,22 +20,22 @@ import {
17
20
  Label,
18
21
  LoadingSpinner,
19
22
  EmptyState,
20
- PermissionDenied,
23
+ AccessDenied,
21
24
  EditableText,
22
25
  ConfirmationModal,
23
26
  useToast,
24
27
  } from "@checkstack/ui";
25
- import { Plus, Trash2, LayoutGrid, Server, Settings } from "lucide-react";
28
+ import { Plus, Trash2, LayoutGrid, Server, Settings, Edit } from "lucide-react";
26
29
  import { SystemEditor } from "./SystemEditor";
27
30
  import { GroupEditor } from "./GroupEditor";
28
31
 
29
32
  export const CatalogConfigPage = () => {
30
33
  const catalogApi = useApi(catalogApiRef);
31
- const permissionApi = useApi(permissionApiRef);
34
+ const accessApi = useApi(accessApiRef);
32
35
  const toast = useToast();
33
36
  const [searchParams, setSearchParams] = useSearchParams();
34
- const { allowed: canManage, loading: permissionLoading } =
35
- permissionApi.useManagePermission("catalog");
37
+ const { allowed: canManage, loading: accessLoading } =
38
+ accessApi.useAccess(catalogAccess.system.manage);
36
39
 
37
40
  const [systems, setSystems] = useState<System[]>([]);
38
41
  const [groups, setGroups] = useState<Group[]>([]);
@@ -40,6 +43,7 @@ export const CatalogConfigPage = () => {
40
43
 
41
44
  // Dialog state
42
45
  const [isSystemEditorOpen, setIsSystemEditorOpen] = useState(false);
46
+ const [editingSystem, setEditingSystem] = useState<System | undefined>();
43
47
  const [isGroupEditorOpen, setIsGroupEditorOpen] = useState(false);
44
48
 
45
49
  const [selectedGroupId, setSelectedGroupId] = useState("");
@@ -61,7 +65,7 @@ export const CatalogConfigPage = () => {
61
65
  const loadData = async () => {
62
66
  setLoading(true);
63
67
  try {
64
- const [s, g] = await Promise.all([
68
+ const [{ systems: s }, g] = await Promise.all([
65
69
  catalogApi.getSystems(),
66
70
  catalogApi.getGroups(),
67
71
  ]);
@@ -94,12 +98,25 @@ export const CatalogConfigPage = () => {
94
98
  }
95
99
  }, [searchParams, canManage, setSearchParams]);
96
100
 
97
- const handleCreateSystem = async (data: {
101
+ // Unified save handler for both create and edit
102
+ const handleSaveSystem = async (data: {
98
103
  name: string;
99
104
  description?: string;
100
105
  }) => {
101
- await catalogApi.createSystem(data);
102
- toast.success("System created successfully");
106
+ 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);
114
+ } else {
115
+ // Create new system
116
+ await catalogApi.createSystem(data);
117
+ toast.success("System created successfully");
118
+ }
119
+ setIsSystemEditorOpen(false);
103
120
  await loadData();
104
121
  };
105
122
 
@@ -191,42 +208,6 @@ export const CatalogConfigPage = () => {
191
208
  }
192
209
  };
193
210
 
194
- const handleUpdateSystemName = async (id: string, newName: string) => {
195
- try {
196
- await catalogApi.updateSystem({ id, data: { name: newName } });
197
- toast.success("System name updated successfully");
198
- loadData();
199
- } catch (error) {
200
- const message =
201
- error instanceof Error ? error.message : "Failed to update system name";
202
- toast.error(message);
203
- console.error("Failed to update system name:", error);
204
- throw error;
205
- }
206
- };
207
-
208
- const handleUpdateSystemDescription = async (
209
- id: string,
210
- newDescription: string
211
- ) => {
212
- try {
213
- await catalogApi.updateSystem({
214
- id,
215
- data: { description: newDescription },
216
- });
217
- toast.success("System description updated successfully");
218
- loadData();
219
- } catch (error) {
220
- const message =
221
- error instanceof Error
222
- ? error.message
223
- : "Failed to update system description";
224
- toast.error(message);
225
- console.error("Failed to update system description:", error);
226
- throw error;
227
- }
228
- };
229
-
230
211
  const handleUpdateGroupName = async (id: string, newName: string) => {
231
212
  try {
232
213
  await catalogApi.updateGroup({ id, data: { name: newName } });
@@ -241,10 +222,10 @@ export const CatalogConfigPage = () => {
241
222
  }
242
223
  };
243
224
 
244
- if (loading || permissionLoading) return <LoadingSpinner />;
225
+ if (loading || accessLoading) return <LoadingSpinner />;
245
226
 
246
227
  if (!canManage) {
247
- return <PermissionDenied />;
228
+ return <AccessDenied />;
248
229
  }
249
230
 
250
231
  const selectedGroup = groups.find((g) => g.id === selectedGroupId);
@@ -285,13 +266,9 @@ export const CatalogConfigPage = () => {
285
266
  >
286
267
  <div className="flex-1 space-y-1">
287
268
  <div className="flex items-center justify-between">
288
- <EditableText
289
- value={system.name}
290
- onSave={(newName) =>
291
- handleUpdateSystemName(system.id, newName)
292
- }
293
- className="font-medium text-foreground"
294
- />
269
+ <span className="font-medium text-foreground">
270
+ {system.name}
271
+ </span>
295
272
  <ExtensionSlot
296
273
  slot={CatalogSystemActionsSlot}
297
274
  context={{
@@ -300,25 +277,30 @@ export const CatalogConfigPage = () => {
300
277
  }}
301
278
  />
302
279
  </div>
303
- <EditableText
304
- value={system.description || "No description"}
305
- onSave={(newDescription) =>
306
- handleUpdateSystemDescription(
307
- system.id,
308
- newDescription
309
- )
310
- }
311
- className="text-xs text-muted-foreground font-mono"
312
- placeholder="Add description..."
313
- />
280
+ <p className="text-xs text-muted-foreground">
281
+ {system.description || "No description"}
282
+ </p>
283
+ </div>
284
+ <div className="flex gap-1">
285
+ <Button
286
+ variant="ghost"
287
+ size="sm"
288
+ className="h-8 w-8 p-0"
289
+ onClick={() => {
290
+ setEditingSystem(system);
291
+ setIsSystemEditorOpen(true);
292
+ }}
293
+ >
294
+ <Edit className="w-4 h-4" />
295
+ </Button>
296
+ <Button
297
+ variant="ghost"
298
+ className="text-destructive hover:text-destructive/90 hover:bg-destructive/10 h-8 w-8 p-0"
299
+ onClick={() => handleDeleteSystem(system.id)}
300
+ >
301
+ <Trash2 className="w-4 h-4" />
302
+ </Button>
314
303
  </div>
315
- <Button
316
- variant="ghost"
317
- className="text-destructive hover:text-destructive/90 hover:bg-destructive/10 h-8 w-8 p-0"
318
- onClick={() => handleDeleteSystem(system.id)}
319
- >
320
- <Trash2 className="w-4 h-4" />
321
- </Button>
322
304
  </div>
323
305
  ))}
324
306
  </div>
@@ -466,8 +448,20 @@ export const CatalogConfigPage = () => {
466
448
  {/* Dialogs */}
467
449
  <SystemEditor
468
450
  open={isSystemEditorOpen}
469
- onClose={() => setIsSystemEditorOpen(false)}
470
- onSave={handleCreateSystem}
451
+ onClose={() => {
452
+ setIsSystemEditorOpen(false);
453
+ setEditingSystem(undefined);
454
+ }}
455
+ onSave={handleSaveSystem}
456
+ initialData={
457
+ editingSystem
458
+ ? {
459
+ id: editingSystem.id,
460
+ name: editingSystem.name,
461
+ description: editingSystem.description ?? undefined,
462
+ }
463
+ : undefined
464
+ }
471
465
  />
472
466
 
473
467
  <GroupEditor
@@ -5,6 +5,7 @@ import {
5
5
  Label,
6
6
  Dialog,
7
7
  DialogContent,
8
+ DialogDescription,
8
9
  DialogHeader,
9
10
  DialogTitle,
10
11
  DialogFooter,
@@ -60,6 +61,11 @@ export const GroupEditor: React.FC<GroupEditorProps> = ({
60
61
  <DialogTitle>
61
62
  {initialData ? "Edit Group" : "Create Group"}
62
63
  </DialogTitle>
64
+ <DialogDescription className="sr-only">
65
+ {initialData
66
+ ? "Modify the settings for this group"
67
+ : "Create a new group to organize your systems"}
68
+ </DialogDescription>
63
69
  </DialogHeader>
64
70
 
65
71
  <div className="space-y-4 py-4">
@@ -57,7 +57,7 @@ export const SystemDetailPage: React.FC = () => {
57
57
  }
58
58
 
59
59
  Promise.all([catalogApi.getSystems(), catalogApi.getGroups()])
60
- .then(([systems, allGroups]) => {
60
+ .then(([{ systems }, allGroups]) => {
61
61
  const foundSystem = systems.find((s) => s.id === systemId);
62
62
 
63
63
  if (!foundSystem) {
@@ -5,17 +5,19 @@ import {
5
5
  Label,
6
6
  Dialog,
7
7
  DialogContent,
8
+ DialogDescription,
8
9
  DialogHeader,
9
10
  DialogTitle,
10
11
  DialogFooter,
11
12
  useToast,
12
13
  } from "@checkstack/ui";
14
+ import { TeamAccessEditor } from "@checkstack/auth-frontend";
13
15
 
14
16
  interface SystemEditorProps {
15
17
  open: boolean;
16
18
  onClose: () => void;
17
19
  onSave: (data: { name: string; description?: string }) => Promise<void>;
18
- initialData?: { name: string; description?: string };
20
+ initialData?: { id: string; name: string; description?: string };
19
21
  }
20
22
 
21
23
  export const SystemEditor: React.FC<SystemEditorProps> = ({
@@ -67,6 +69,11 @@ export const SystemEditor: React.FC<SystemEditorProps> = ({
67
69
  <DialogTitle>
68
70
  {initialData ? "Edit System" : "Create System"}
69
71
  </DialogTitle>
72
+ <DialogDescription className="sr-only">
73
+ {initialData
74
+ ? "Modify the settings for this system"
75
+ : "Create a new system to monitor"}
76
+ </DialogDescription>
70
77
  </DialogHeader>
71
78
 
72
79
  <div className="space-y-4 py-4">
@@ -92,6 +99,16 @@ export const SystemEditor: React.FC<SystemEditorProps> = ({
92
99
  rows={3}
93
100
  />
94
101
  </div>
102
+
103
+ {/* Team Access Editor - only shown for existing systems */}
104
+ {initialData?.id && (
105
+ <TeamAccessEditor
106
+ resourceType="catalog.system"
107
+ resourceId={initialData.id}
108
+ compact
109
+ expanded
110
+ />
111
+ )}
95
112
  </div>
96
113
 
97
114
  <DialogFooter>
@@ -3,20 +3,18 @@ import { Link } from "react-router-dom";
3
3
  import { Settings } from "lucide-react";
4
4
  import type { UserMenuItemsContext } from "@checkstack/frontend-api";
5
5
  import { DropdownMenuItem } from "@checkstack/ui";
6
- import { qualifyPermissionId, resolveRoute } from "@checkstack/common";
6
+ import { resolveRoute } from "@checkstack/common";
7
7
  import {
8
8
  catalogRoutes,
9
- permissions,
9
+ catalogAccess,
10
10
  pluginMetadata,
11
11
  } from "@checkstack/catalog-common";
12
12
 
13
13
  export const CatalogUserMenuItems = ({
14
- permissions: userPerms,
14
+ accessRules: userPerms,
15
15
  }: UserMenuItemsContext) => {
16
- const qualifiedId = qualifyPermissionId(
17
- pluginMetadata,
18
- permissions.catalogManage
19
- );
16
+ // Use the access rule's id directly
17
+ const qualifiedId = `${pluginMetadata.pluginId}.${catalogAccess.system.manage.id}`;
20
18
  const canManage = userPerms.includes("*") || userPerms.includes(qualifiedId);
21
19
 
22
20
  if (!canManage) {
package/src/index.tsx CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  catalogRoutes,
11
11
  CatalogApi,
12
12
  pluginMetadata,
13
- permissions,
13
+ catalogAccess,
14
14
  } from "@checkstack/catalog-common";
15
15
 
16
16
  import { CatalogPage } from "./components/CatalogPage";
@@ -38,7 +38,7 @@ export const catalogPlugin = createFrontendPlugin({
38
38
  {
39
39
  route: catalogRoutes.routes.config,
40
40
  element: <CatalogConfigPage />,
41
- permission: permissions.catalogManage,
41
+ accessRule: catalogAccess.system.manage,
42
42
  },
43
43
  {
44
44
  route: catalogRoutes.routes.systemDetail,