JitDbAccessRulesTable.tsx213 lines · main
1import { EllipsisVertical, Pencil, Plus, Trash2 } from 'lucide-react'
2import {
3 Badge,
4 Button,
5 Card,
6 CardContent,
7 DropdownMenu,
8 DropdownMenuContent,
9 DropdownMenuItem,
10 DropdownMenuSeparator,
11 DropdownMenuTrigger,
12 Skeleton,
13 Table,
14 TableBody,
15 TableCell,
16 TableHead,
17 TableHeader,
18 TableRow,
19 Tooltip,
20 TooltipContent,
21 TooltipTrigger,
22} from 'ui'
23
24import type { JitUserRule } from './JitDbAccess.types'
25import { getJitStatusDisplay } from './JitDbAccess.utils'
26
27interface JitDbAccessRulesTableProps {
28 users: JitUserRule[]
29 isLoading?: boolean
30 canUpdate: boolean
31 disableActions?: boolean
32 allProjectMembersHaveRules?: boolean
33 onAddRule: () => void
34 onEditRule: (user: JitUserRule) => void
35 onDeleteRule: (user: JitUserRule) => void
36}
37
38export function JitDbAccessRulesTable({
39 users,
40 isLoading = false,
41 canUpdate,
42 disableActions = false,
43 allProjectMembersHaveRules = false,
44 onAddRule,
45 onEditRule,
46 onDeleteRule,
47}: JitDbAccessRulesTableProps) {
48 const addDisabled = disableActions || !canUpdate || allProjectMembersHaveRules
49 const addRuleTooltip = !canUpdate
50 ? 'Additional permissions required'
51 : allProjectMembersHaveRules
52 ? 'All project members already have temporary access rules'
53 : undefined
54
55 if (isLoading) {
56 return (
57 <Card>
58 <CardContent className="space-y-4 p-4">
59 <div className="flex items-center justify-between">
60 <div className="space-y-2">
61 <Skeleton className="h-4 w-32" />
62 <Skeleton className="h-4 w-64" />
63 </div>
64 <Skeleton className="h-9 w-24" />
65 </div>
66 <Skeleton className="h-32 w-full" />
67 </CardContent>
68 </Card>
69 )
70 }
71
72 return (
73 <Card>
74 <CardContent className="space-y-4 p-0">
75 <div className="flex items-center justify-between px-4 pb-2 pt-6">
76 <div>
77 <h3 className="text-sm text-foreground">Temporary access rules</h3>
78 <p className="text-sm text-foreground-light">
79 Manage member access, allowed roles, and expiry settings.
80 </p>
81 </div>
82
83 <Tooltip>
84 <TooltipTrigger asChild>
85 <span className="inline-flex">
86 <Button type="default" icon={<Plus />} onClick={onAddRule} disabled={addDisabled}>
87 Add rule
88 </Button>
89 </span>
90 </TooltipTrigger>
91 {addRuleTooltip && <TooltipContent side="bottom">{addRuleTooltip}</TooltipContent>}
92 </Tooltip>
93 </div>
94
95 <Table className="border-t">
96 <TableHeader>
97 <TableRow>
98 <TableHead>Member</TableHead>
99 <TableHead>Roles</TableHead>
100 <TableHead>Status</TableHead>
101 <TableHead className="w-1">
102 <span className="sr-only">Actions</span>
103 </TableHead>
104 </TableRow>
105 </TableHeader>
106 <TableBody>
107 {users.length === 0 ? (
108 <TableRow className="[&>td]:hover:bg-inherit">
109 <TableCell colSpan={4}>
110 <p className="text-sm text-foreground">No rules yet</p>
111 <p className="text-sm text-foreground-lighter">
112 Add your first temporary access rule above
113 </p>
114 </TableCell>
115 </TableRow>
116 ) : (
117 users.map((user) => {
118 const statusDisplay = getJitStatusDisplay(user.status)
119 const enabledGrants = user.grants.filter((grant) => grant.enabled)
120 const rowIsInteractive = canUpdate && !disableActions
121
122 return (
123 <TableRow
124 key={user.id}
125 className={rowIsInteractive ? 'relative inset-focus cursor-pointer' : undefined}
126 onClick={
127 rowIsInteractive
128 ? (event) => {
129 if ((event.target as HTMLElement).closest('button')) return
130 onEditRule(user)
131 }
132 : undefined
133 }
134 onKeyDown={
135 rowIsInteractive
136 ? (event) => {
137 if ((event.target as HTMLElement).closest('button')) return
138 if (event.key === 'Enter' || event.key === ' ') {
139 event.preventDefault()
140 onEditRule(user)
141 }
142 }
143 : undefined
144 }
145 tabIndex={rowIsInteractive ? 0 : undefined}
146 >
147 <TableCell className="text-sm">
148 {user.name && <p>{user.name}</p>}
149 <p className="text-foreground-lighter">{user.email}</p>
150 </TableCell>
151 <TableCell className="text-sm text-foreground-light">
152 {enabledGrants.length} role{enabledGrants.length === 1 ? '' : 's'}
153 </TableCell>
154 <TableCell className="text-sm text-foreground-light">
155 {statusDisplay.badges.length > 0 ? (
156 <span className="flex flex-wrap gap-1.5">
157 {statusDisplay.badges.map((badge) => (
158 <Badge key={badge.label} variant={badge.variant}>
159 {badge.label}
160 </Badge>
161 ))}
162 </span>
163 ) : null}
164 </TableCell>
165 <TableCell className="text-right">
166 <DropdownMenu>
167 <DropdownMenuTrigger asChild>
168 <Button
169 icon={<EllipsisVertical />}
170 aria-label="More actions"
171 type="default"
172 size="tiny"
173 className="w-7"
174 disabled={!canUpdate || disableActions}
175 />
176 </DropdownMenuTrigger>
177 <DropdownMenuContent align="end" side="bottom" className="w-40">
178 <DropdownMenuItem
179 className="gap-x-2"
180 onClick={(event) => {
181 event.stopPropagation()
182 onEditRule(user)
183 }}
184 disabled={!canUpdate || disableActions}
185 >
186 <Pencil size={14} className="text-foreground-lighter" />
187 Edit
188 </DropdownMenuItem>
189 <DropdownMenuSeparator />
190 <DropdownMenuItem
191 className="gap-x-2"
192 onClick={(event) => {
193 event.stopPropagation()
194 onDeleteRule(user)
195 }}
196 disabled={!canUpdate || disableActions}
197 >
198 <Trash2 size={14} className="text-foreground-lighter" />
199 Delete
200 </DropdownMenuItem>
201 </DropdownMenuContent>
202 </DropdownMenu>
203 </TableCell>
204 </TableRow>
205 )
206 })
207 )}
208 </TableBody>
209 </Table>
210 </CardContent>
211 </Card>
212 )
213}