JitDbAccessRulesTable.tsx 7.6 KB

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