@checkstack/auth-frontend 0.0.3 → 0.1.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 +91 -0
- package/package.json +1 -1
- package/src/components/ApplicationsTab.tsx +9 -2
- package/src/components/AuthSettingsPage.tsx +39 -14
- package/src/components/RoleDialog.tsx +6 -0
- package/src/components/TeamAccessEditor.tsx +560 -0
- package/src/components/TeamsTab.tsx +569 -0
- package/src/index.tsx +6 -5
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardHeader,
|
|
5
|
+
CardTitle,
|
|
6
|
+
CardContent,
|
|
7
|
+
Button,
|
|
8
|
+
Badge,
|
|
9
|
+
useToast,
|
|
10
|
+
LoadingSpinner,
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
Checkbox,
|
|
17
|
+
Toggle,
|
|
18
|
+
Label,
|
|
19
|
+
} from "@checkstack/ui";
|
|
20
|
+
import {
|
|
21
|
+
Plus,
|
|
22
|
+
Trash2,
|
|
23
|
+
Users2,
|
|
24
|
+
Shield,
|
|
25
|
+
Eye,
|
|
26
|
+
Settings,
|
|
27
|
+
Lock,
|
|
28
|
+
} from "lucide-react";
|
|
29
|
+
import { useApi, rpcApiRef, permissionApiRef } from "@checkstack/frontend-api";
|
|
30
|
+
import {
|
|
31
|
+
AuthApi,
|
|
32
|
+
permissions as authPermissions,
|
|
33
|
+
} from "@checkstack/auth-common";
|
|
34
|
+
|
|
35
|
+
interface TeamAccess {
|
|
36
|
+
teamId: string;
|
|
37
|
+
teamName: string;
|
|
38
|
+
canRead: boolean;
|
|
39
|
+
canManage: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Team {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
description?: string | null;
|
|
46
|
+
memberCount: number;
|
|
47
|
+
isManager: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TeamAccessEditorProps {
|
|
51
|
+
/** Resource type identifier (e.g., "catalog.system", "healthcheck.configuration") */
|
|
52
|
+
resourceType: string;
|
|
53
|
+
/** Resource ID */
|
|
54
|
+
resourceId: string;
|
|
55
|
+
/** Whether the editor is expanded/visible */
|
|
56
|
+
expanded?: boolean;
|
|
57
|
+
/** Compact mode for inline display */
|
|
58
|
+
compact?: boolean;
|
|
59
|
+
/** Called when access is modified */
|
|
60
|
+
onChange?: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reusable component for managing team-based access to resources.
|
|
65
|
+
* Used in System editor, Health Check editor, Incident/Maintenance forms.
|
|
66
|
+
*/
|
|
67
|
+
export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
68
|
+
resourceType,
|
|
69
|
+
resourceId,
|
|
70
|
+
expanded: initialExpanded = false,
|
|
71
|
+
compact = false,
|
|
72
|
+
onChange,
|
|
73
|
+
}) => {
|
|
74
|
+
const rpcApi = useApi(rpcApiRef);
|
|
75
|
+
const permissionApi = useApi(permissionApiRef);
|
|
76
|
+
const authClient = rpcApi.forPlugin(AuthApi);
|
|
77
|
+
const toast = useToast();
|
|
78
|
+
|
|
79
|
+
const { allowed: canManageTeams } = permissionApi.usePermission(
|
|
80
|
+
authPermissions.teamsManage.id
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const [expanded, setExpanded] = useState(initialExpanded);
|
|
84
|
+
const [loading, setLoading] = useState(false);
|
|
85
|
+
const [accessList, setAccessList] = useState<TeamAccess[]>([]);
|
|
86
|
+
const [teams, setTeams] = useState<Team[]>([]);
|
|
87
|
+
const [selectedTeamId, setSelectedTeamId] = useState("");
|
|
88
|
+
const [adding, setAdding] = useState(false);
|
|
89
|
+
const [teamOnly, setTeamOnly] = useState(false);
|
|
90
|
+
|
|
91
|
+
const loadData = useCallback(async () => {
|
|
92
|
+
setLoading(true);
|
|
93
|
+
try {
|
|
94
|
+
const [accessData, teamsData, settingsData] = await Promise.all([
|
|
95
|
+
authClient.getResourceTeamAccess({ resourceType, resourceId }),
|
|
96
|
+
authClient.getTeams(),
|
|
97
|
+
authClient.getResourceAccessSettings({ resourceType, resourceId }),
|
|
98
|
+
]);
|
|
99
|
+
setAccessList(accessData);
|
|
100
|
+
setTeams(teamsData);
|
|
101
|
+
setTeamOnly(settingsData.teamOnly);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
toast.error(
|
|
104
|
+
error instanceof Error ? error.message : "Failed to load team access"
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
}, [authClient, resourceType, resourceId, toast]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (expanded && resourceId) {
|
|
113
|
+
loadData();
|
|
114
|
+
}
|
|
115
|
+
}, [expanded, resourceId, loadData]);
|
|
116
|
+
|
|
117
|
+
const handleAddTeam = async () => {
|
|
118
|
+
if (!selectedTeamId) return;
|
|
119
|
+
|
|
120
|
+
setAdding(true);
|
|
121
|
+
try {
|
|
122
|
+
await authClient.setResourceTeamAccess({
|
|
123
|
+
resourceType,
|
|
124
|
+
resourceId,
|
|
125
|
+
teamId: selectedTeamId,
|
|
126
|
+
canRead: true,
|
|
127
|
+
canManage: false,
|
|
128
|
+
});
|
|
129
|
+
toast.success("Team access granted");
|
|
130
|
+
setSelectedTeamId("");
|
|
131
|
+
await loadData();
|
|
132
|
+
onChange?.();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
toast.error(
|
|
135
|
+
error instanceof Error ? error.message : "Failed to add team access"
|
|
136
|
+
);
|
|
137
|
+
} finally {
|
|
138
|
+
setAdding(false);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleUpdateAccess = async (
|
|
143
|
+
teamId: string,
|
|
144
|
+
updates: { canRead?: boolean; canManage?: boolean }
|
|
145
|
+
) => {
|
|
146
|
+
try {
|
|
147
|
+
await authClient.setResourceTeamAccess({
|
|
148
|
+
resourceType,
|
|
149
|
+
resourceId,
|
|
150
|
+
teamId,
|
|
151
|
+
...updates,
|
|
152
|
+
});
|
|
153
|
+
await loadData();
|
|
154
|
+
onChange?.();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
toast.error(
|
|
157
|
+
error instanceof Error ? error.message : "Failed to update access"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleUpdateSettings = async (newTeamOnly: boolean) => {
|
|
163
|
+
try {
|
|
164
|
+
await authClient.setResourceAccessSettings({
|
|
165
|
+
resourceType,
|
|
166
|
+
resourceId,
|
|
167
|
+
teamOnly: newTeamOnly,
|
|
168
|
+
});
|
|
169
|
+
setTeamOnly(newTeamOnly);
|
|
170
|
+
onChange?.();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
toast.error(
|
|
173
|
+
error instanceof Error ? error.message : "Failed to update settings"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleRemoveAccess = async (teamId: string) => {
|
|
179
|
+
try {
|
|
180
|
+
await authClient.removeResourceTeamAccess({
|
|
181
|
+
resourceType,
|
|
182
|
+
resourceId,
|
|
183
|
+
teamId,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Check if this was the last team - if so, clear settings too
|
|
187
|
+
const remainingTeams = accessList.filter((a) => a.teamId !== teamId);
|
|
188
|
+
if (remainingTeams.length === 0 && teamOnly) {
|
|
189
|
+
// Reset teamOnly when no teams have access
|
|
190
|
+
await authClient.setResourceAccessSettings({
|
|
191
|
+
resourceType,
|
|
192
|
+
resourceId,
|
|
193
|
+
teamOnly: false,
|
|
194
|
+
});
|
|
195
|
+
setTeamOnly(false);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
toast.success("Team access removed");
|
|
199
|
+
await loadData();
|
|
200
|
+
onChange?.();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
toast.error(
|
|
203
|
+
error instanceof Error ? error.message : "Failed to remove access"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Get teams that don't already have access
|
|
209
|
+
const availableTeams = teams.filter(
|
|
210
|
+
(t) => !accessList.some((a) => a.teamId === t.id)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Compact summary mode
|
|
214
|
+
if (!expanded) {
|
|
215
|
+
return (
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="sm"
|
|
221
|
+
onClick={() => setExpanded(true)}
|
|
222
|
+
className="gap-1.5"
|
|
223
|
+
>
|
|
224
|
+
<Users2 className="h-4 w-4" />
|
|
225
|
+
<span>Team Access</span>
|
|
226
|
+
{accessList.length > 0 && (
|
|
227
|
+
<Badge variant="secondary" className="ml-1">
|
|
228
|
+
{accessList.length}
|
|
229
|
+
</Badge>
|
|
230
|
+
)}
|
|
231
|
+
</Button>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Full editor mode
|
|
237
|
+
if (compact) {
|
|
238
|
+
return (
|
|
239
|
+
<div className="border rounded-lg p-4 space-y-4">
|
|
240
|
+
<div className="flex items-center justify-between">
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<Shield className="h-4 w-4 text-muted-foreground" />
|
|
243
|
+
<span className="font-medium text-sm">Team Access Control</span>
|
|
244
|
+
</div>
|
|
245
|
+
<Button
|
|
246
|
+
type="button"
|
|
247
|
+
variant="ghost"
|
|
248
|
+
size="sm"
|
|
249
|
+
onClick={() => setExpanded(false)}
|
|
250
|
+
>
|
|
251
|
+
Collapse
|
|
252
|
+
</Button>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{loading ? (
|
|
256
|
+
<div className="flex justify-center py-4">
|
|
257
|
+
<LoadingSpinner />
|
|
258
|
+
</div>
|
|
259
|
+
) : (
|
|
260
|
+
<>
|
|
261
|
+
{/* Resource-level Team Only setting */}
|
|
262
|
+
{accessList.length > 0 && (
|
|
263
|
+
<div className="flex items-center justify-between p-2 bg-muted/30 rounded-md">
|
|
264
|
+
<div className="flex items-center gap-2">
|
|
265
|
+
<Lock className="h-4 w-4 text-muted-foreground" />
|
|
266
|
+
<Label
|
|
267
|
+
htmlFor="team-only-compact"
|
|
268
|
+
className="text-sm cursor-pointer"
|
|
269
|
+
>
|
|
270
|
+
Team Only
|
|
271
|
+
</Label>
|
|
272
|
+
<span className="text-xs text-muted-foreground">
|
|
273
|
+
(Bypass global permissions)
|
|
274
|
+
</span>
|
|
275
|
+
</div>
|
|
276
|
+
<Toggle
|
|
277
|
+
checked={teamOnly}
|
|
278
|
+
onCheckedChange={handleUpdateSettings}
|
|
279
|
+
disabled={!canManageTeams}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* Add team row */}
|
|
285
|
+
{canManageTeams && (
|
|
286
|
+
<div className="flex gap-2">
|
|
287
|
+
<Select
|
|
288
|
+
value={selectedTeamId}
|
|
289
|
+
onValueChange={setSelectedTeamId}
|
|
290
|
+
>
|
|
291
|
+
<SelectTrigger className="flex-1">
|
|
292
|
+
<SelectValue placeholder="Select team to add" />
|
|
293
|
+
</SelectTrigger>
|
|
294
|
+
<SelectContent>
|
|
295
|
+
{availableTeams.length === 0 ? (
|
|
296
|
+
<SelectItem value="_none" disabled>
|
|
297
|
+
No teams available
|
|
298
|
+
</SelectItem>
|
|
299
|
+
) : (
|
|
300
|
+
availableTeams.map((team) => (
|
|
301
|
+
<SelectItem key={team.id} value={team.id}>
|
|
302
|
+
{team.name}
|
|
303
|
+
</SelectItem>
|
|
304
|
+
))
|
|
305
|
+
)}
|
|
306
|
+
</SelectContent>
|
|
307
|
+
</Select>
|
|
308
|
+
<Button
|
|
309
|
+
type="button"
|
|
310
|
+
onClick={handleAddTeam}
|
|
311
|
+
disabled={!selectedTeamId || adding}
|
|
312
|
+
size="sm"
|
|
313
|
+
>
|
|
314
|
+
<Plus className="h-4 w-4" />
|
|
315
|
+
</Button>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{/* Access list */}
|
|
320
|
+
<div className="space-y-2">
|
|
321
|
+
{accessList.length === 0 ? (
|
|
322
|
+
<p className="text-sm text-muted-foreground text-center py-2">
|
|
323
|
+
No team restrictions. All users with permission can access.
|
|
324
|
+
</p>
|
|
325
|
+
) : (
|
|
326
|
+
accessList.map((access) => (
|
|
327
|
+
<div
|
|
328
|
+
key={access.teamId}
|
|
329
|
+
className="flex items-center justify-between p-2 bg-muted/50 rounded-md"
|
|
330
|
+
>
|
|
331
|
+
<div className="flex items-center gap-2">
|
|
332
|
+
<Users2 className="h-4 w-4 text-muted-foreground" />
|
|
333
|
+
<span className="font-medium text-sm">
|
|
334
|
+
{access.teamName}
|
|
335
|
+
</span>
|
|
336
|
+
<div className="flex gap-1">
|
|
337
|
+
{canManageTeams ? (
|
|
338
|
+
<>
|
|
339
|
+
<Badge
|
|
340
|
+
variant={access.canRead ? "default" : "secondary"}
|
|
341
|
+
className="text-xs cursor-pointer hover:opacity-80"
|
|
342
|
+
onClick={() =>
|
|
343
|
+
handleUpdateAccess(access.teamId, {
|
|
344
|
+
canRead: !access.canRead,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
>
|
|
348
|
+
<Eye className="h-3 w-3 mr-1" />
|
|
349
|
+
Read
|
|
350
|
+
</Badge>
|
|
351
|
+
<Badge
|
|
352
|
+
variant={
|
|
353
|
+
access.canManage ? "default" : "secondary"
|
|
354
|
+
}
|
|
355
|
+
className="text-xs cursor-pointer hover:opacity-80"
|
|
356
|
+
onClick={() =>
|
|
357
|
+
handleUpdateAccess(access.teamId, {
|
|
358
|
+
canManage: !access.canManage,
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
>
|
|
362
|
+
<Settings className="h-3 w-3 mr-1" />
|
|
363
|
+
Manage
|
|
364
|
+
</Badge>
|
|
365
|
+
</>
|
|
366
|
+
) : (
|
|
367
|
+
<>
|
|
368
|
+
{access.canRead && (
|
|
369
|
+
<Badge variant="outline" className="text-xs">
|
|
370
|
+
<Eye className="h-3 w-3 mr-1" />
|
|
371
|
+
Read
|
|
372
|
+
</Badge>
|
|
373
|
+
)}
|
|
374
|
+
{access.canManage && (
|
|
375
|
+
<Badge variant="secondary" className="text-xs">
|
|
376
|
+
<Settings className="h-3 w-3 mr-1" />
|
|
377
|
+
Manage
|
|
378
|
+
</Badge>
|
|
379
|
+
)}
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
{canManageTeams && (
|
|
385
|
+
<Button
|
|
386
|
+
type="button"
|
|
387
|
+
variant="ghost"
|
|
388
|
+
size="sm"
|
|
389
|
+
onClick={() => handleRemoveAccess(access.teamId)}
|
|
390
|
+
>
|
|
391
|
+
<Trash2 className="h-4 w-4" />
|
|
392
|
+
</Button>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
))
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
</>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Card mode (default)
|
|
405
|
+
return (
|
|
406
|
+
<Card>
|
|
407
|
+
<CardHeader className="flex flex-row items-center justify-between py-3">
|
|
408
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
409
|
+
<Shield className="h-4 w-4" />
|
|
410
|
+
Team Access Control
|
|
411
|
+
</CardTitle>
|
|
412
|
+
<Button
|
|
413
|
+
type="button"
|
|
414
|
+
variant="ghost"
|
|
415
|
+
size="sm"
|
|
416
|
+
onClick={() => setExpanded(false)}
|
|
417
|
+
>
|
|
418
|
+
Collapse
|
|
419
|
+
</Button>
|
|
420
|
+
</CardHeader>
|
|
421
|
+
<CardContent className="space-y-4">
|
|
422
|
+
{loading ? (
|
|
423
|
+
<div className="flex justify-center py-4">
|
|
424
|
+
<LoadingSpinner />
|
|
425
|
+
</div>
|
|
426
|
+
) : (
|
|
427
|
+
<>
|
|
428
|
+
{/* Add team row */}
|
|
429
|
+
{canManageTeams && (
|
|
430
|
+
<div className="flex gap-2">
|
|
431
|
+
<Select
|
|
432
|
+
value={selectedTeamId}
|
|
433
|
+
onValueChange={setSelectedTeamId}
|
|
434
|
+
>
|
|
435
|
+
<SelectTrigger className="flex-1">
|
|
436
|
+
<SelectValue placeholder="Select team to add" />
|
|
437
|
+
</SelectTrigger>
|
|
438
|
+
<SelectContent>
|
|
439
|
+
{availableTeams.length === 0 ? (
|
|
440
|
+
<SelectItem value="_none" disabled>
|
|
441
|
+
No teams available
|
|
442
|
+
</SelectItem>
|
|
443
|
+
) : (
|
|
444
|
+
availableTeams.map((team) => (
|
|
445
|
+
<SelectItem key={team.id} value={team.id}>
|
|
446
|
+
{team.name}
|
|
447
|
+
</SelectItem>
|
|
448
|
+
))
|
|
449
|
+
)}
|
|
450
|
+
</SelectContent>
|
|
451
|
+
</Select>
|
|
452
|
+
<Button
|
|
453
|
+
type="button"
|
|
454
|
+
onClick={handleAddTeam}
|
|
455
|
+
disabled={!selectedTeamId || adding}
|
|
456
|
+
size="sm"
|
|
457
|
+
>
|
|
458
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
459
|
+
Add
|
|
460
|
+
</Button>
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
|
|
464
|
+
{/* Resource-level Team Only setting */}
|
|
465
|
+
{accessList.length > 0 && (
|
|
466
|
+
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
|
|
467
|
+
<div className="flex items-center gap-2">
|
|
468
|
+
<Lock className="h-4 w-4 text-muted-foreground" />
|
|
469
|
+
<div>
|
|
470
|
+
<Label htmlFor="team-only-card" className="cursor-pointer">
|
|
471
|
+
Team Only Mode
|
|
472
|
+
</Label>
|
|
473
|
+
<p className="text-xs text-muted-foreground">
|
|
474
|
+
When enabled, only team members can access (global
|
|
475
|
+
permissions bypassed)
|
|
476
|
+
</p>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
<Toggle
|
|
480
|
+
checked={teamOnly}
|
|
481
|
+
onCheckedChange={handleUpdateSettings}
|
|
482
|
+
disabled={!canManageTeams}
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* Access list */}
|
|
488
|
+
{accessList.length === 0 ? (
|
|
489
|
+
<p className="text-sm text-muted-foreground text-center py-4 bg-muted/30 rounded-lg">
|
|
490
|
+
No team restrictions configured. All users with appropriate
|
|
491
|
+
permissions can access this resource.
|
|
492
|
+
</p>
|
|
493
|
+
) : (
|
|
494
|
+
<div className="border rounded-lg divide-y">
|
|
495
|
+
{accessList.map((access) => (
|
|
496
|
+
<div
|
|
497
|
+
key={access.teamId}
|
|
498
|
+
className="p-3 flex items-center justify-between"
|
|
499
|
+
>
|
|
500
|
+
<div className="flex items-center gap-3">
|
|
501
|
+
<Users2 className="h-5 w-5 text-muted-foreground" />
|
|
502
|
+
<div>
|
|
503
|
+
<div className="font-medium">{access.teamName}</div>
|
|
504
|
+
<div className="flex gap-3 mt-1">
|
|
505
|
+
<label className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
506
|
+
<Checkbox
|
|
507
|
+
checked={access.canRead}
|
|
508
|
+
onCheckedChange={(checked) =>
|
|
509
|
+
handleUpdateAccess(access.teamId, {
|
|
510
|
+
canRead: !!checked,
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
disabled={!canManageTeams}
|
|
514
|
+
/>
|
|
515
|
+
<Eye className="h-3 w-3" />
|
|
516
|
+
Read
|
|
517
|
+
</label>
|
|
518
|
+
<label className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
519
|
+
<Checkbox
|
|
520
|
+
checked={access.canManage}
|
|
521
|
+
onCheckedChange={(checked) =>
|
|
522
|
+
handleUpdateAccess(access.teamId, {
|
|
523
|
+
canManage: !!checked,
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
disabled={!canManageTeams}
|
|
527
|
+
/>
|
|
528
|
+
<Settings className="h-3 w-3" />
|
|
529
|
+
Manage
|
|
530
|
+
</label>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
{canManageTeams && (
|
|
535
|
+
<Button
|
|
536
|
+
type="button"
|
|
537
|
+
variant="ghost"
|
|
538
|
+
size="sm"
|
|
539
|
+
onClick={() => handleRemoveAccess(access.teamId)}
|
|
540
|
+
>
|
|
541
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
542
|
+
</Button>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
))}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
|
|
549
|
+
<p className="text-xs text-muted-foreground">
|
|
550
|
+
<strong>Read:</strong> View this resource •{" "}
|
|
551
|
+
<strong>Manage:</strong> Edit this resource
|
|
552
|
+
</p>
|
|
553
|
+
</>
|
|
554
|
+
)}
|
|
555
|
+
</CardContent>
|
|
556
|
+
</Card>
|
|
557
|
+
);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
export default TeamAccessEditor;
|