ViewTokenSheet.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import dayjs from 'dayjs'
  2. import { useMemo } from 'react'
  3. import {
  4. Card,
  5. CardContent,
  6. cn,
  7. ScrollArea,
  8. Sheet,
  9. SheetContent,
  10. SheetHeader,
  11. Table,
  12. TableBody,
  13. TableCell,
  14. TableHead,
  15. TableHeader,
  16. TableRow,
  17. } from 'ui'
  18. import { TimestampInfo } from 'ui-patterns/TimestampInfo'
  19. import { ACCESS_TOKEN_RESOURCES } from '../AccessToken.constants'
  20. import { formatAccessText, getRealAccess } from '../AccessToken.utils'
  21. import { useOrgAndProjectData } from '../hooks/useOrgAndProjectData'
  22. import { DocsButton } from '@/components/ui/DocsButton'
  23. import { useScopedAccessTokenQuery } from '@/data/scoped-access-tokens/scoped-access-token-query'
  24. interface ViewTokenSheetProps {
  25. visible: boolean
  26. tokenId: string | undefined
  27. onClose: () => void
  28. }
  29. export function ViewTokenSheet({ visible, tokenId, onClose }: ViewTokenSheetProps) {
  30. const { organizations, projects } = useOrgAndProjectData()
  31. const {
  32. data: token,
  33. isLoading: isTokenLoading,
  34. error: tokenError,
  35. } = useScopedAccessTokenQuery(
  36. { id: tokenId! },
  37. {
  38. enabled: visible && !!tokenId,
  39. retry: 1,
  40. retryDelay: 1000,
  41. }
  42. )
  43. const groupedResourcesByAccess = useMemo(() => {
  44. const grouped: Record<string, string[]> = {}
  45. if (!token?.permissions) {
  46. return grouped
  47. }
  48. ACCESS_TOKEN_RESOURCES.forEach((resource) => {
  49. const access = getRealAccess(resource.resource, token.permissions)
  50. if (access !== 'no access') {
  51. const formattedAccess = formatAccessText(access)
  52. if (!grouped[formattedAccess]) {
  53. grouped[formattedAccess] = []
  54. }
  55. grouped[formattedAccess].push(resource.title)
  56. }
  57. })
  58. return grouped
  59. }, [token?.permissions])
  60. const getResourceAccessInfo = () => {
  61. const resources: Array<{ name: string; type: string; identifier: string }> = []
  62. const organizationSlugs = token?.organization_slugs
  63. if (organizationSlugs && Array.isArray(organizationSlugs) && organizationSlugs.length > 0) {
  64. organizationSlugs.forEach((orgSlug: string) => {
  65. const org = organizations.find((o) => o.slug === orgSlug)
  66. resources.push({
  67. name: org?.name || orgSlug,
  68. type: 'Organization',
  69. identifier: orgSlug,
  70. })
  71. })
  72. }
  73. const projectRefs = token?.project_refs
  74. if (projectRefs && Array.isArray(projectRefs) && projectRefs.length > 0) {
  75. projectRefs.forEach((projectRef: string) => {
  76. const project = projects.find((p) => p.ref === projectRef)
  77. resources.push({
  78. name: project?.name || projectRef,
  79. type: 'Project',
  80. identifier: projectRef,
  81. })
  82. })
  83. }
  84. return resources
  85. }
  86. return (
  87. <>
  88. <Sheet open={visible} onOpenChange={() => onClose()}>
  89. <SheetContent
  90. showClose={false}
  91. size="default"
  92. className="min-w-[600px]! flex flex-col h-full gap-0"
  93. >
  94. <SheetHeader
  95. className={cn('flex flex-row justify-between gap-x-4 items-center border-b')}
  96. >
  97. <p className="truncate" title={`Manage access for ${token?.name}`}>
  98. View access for {token?.name}
  99. </p>
  100. <DocsButton href="https://supabase.com/docs/reference/api/introduction" />
  101. </SheetHeader>
  102. <ScrollArea className="flex-1 max-h-[calc(100vh-60px)]">
  103. <div className="space-y-8 px-5 sm:px-6 py-6">
  104. {isTokenLoading && (
  105. <div className="flex items-center justify-center py-8">
  106. <p className="text-foreground-light">Loading token information...</p>
  107. </div>
  108. )}
  109. {tokenError && (
  110. <div className="flex items-center justify-center py-8">
  111. <p className="text-foreground-light text-red-500">
  112. Error loading token information. Please try again.
  113. </p>
  114. </div>
  115. )}
  116. {token && (
  117. <>
  118. <div className="space-y-3">
  119. <h3 className="text-sm font-medium text-foreground">Token Information</h3>
  120. <Card className="w-full overflow-hidden bg-surface-100">
  121. <CardContent className="p-0">
  122. <Table className="p-5 table-auto">
  123. <TableHeader>
  124. <TableRow className="bg-200">
  125. <TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 w-[60%]">
  126. Info
  127. </TableHead>
  128. <TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
  129. Date
  130. </TableHead>
  131. </TableRow>
  132. </TableHeader>
  133. <TableBody>
  134. <TableRow>
  135. <TableCell>
  136. <p className="truncate text-foreground-light">Created</p>
  137. </TableCell>
  138. <TableCell>
  139. {token?.created_at ? (
  140. <TimestampInfo
  141. utcTimestamp={token.created_at}
  142. label={dayjs(token.created_at).format('DD MMM YYYY')}
  143. className="text-sm"
  144. />
  145. ) : (
  146. <span className="text-foreground">Unknown</span>
  147. )}
  148. </TableCell>
  149. </TableRow>
  150. <TableRow>
  151. <TableCell>
  152. <p className="truncate text-foreground-light">Last used</p>
  153. </TableCell>
  154. <TableCell>
  155. {token?.last_used_at ? (
  156. <TimestampInfo
  157. utcTimestamp={token.last_used_at}
  158. label={dayjs(token.last_used_at).fromNow()}
  159. className="text-sm"
  160. />
  161. ) : (
  162. <span className="text-foreground">Never</span>
  163. )}
  164. </TableCell>
  165. </TableRow>
  166. <TableRow>
  167. <TableCell>
  168. <p className="truncate text-foreground-light">Expires</p>
  169. </TableCell>
  170. <TableCell>
  171. {token?.expires_at ? (
  172. <TimestampInfo
  173. utcTimestamp={token.expires_at}
  174. label={dayjs(token.expires_at).format('DD MMM YYYY')}
  175. className="text-sm"
  176. />
  177. ) : (
  178. <span className="text-foreground">Never</span>
  179. )}
  180. </TableCell>
  181. </TableRow>
  182. </TableBody>
  183. </Table>
  184. </CardContent>
  185. </Card>
  186. </div>
  187. <div className="space-y-3">
  188. <h3 className="text-sm font-medium text-foreground">Resource Access</h3>
  189. <Card className="w-full overflow-hidden bg-surface-100">
  190. <CardContent className="p-0">
  191. <Table className="p-5 table-auto">
  192. <TableHeader>
  193. <TableRow className="bg-200">
  194. <TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 w-[60%]">
  195. Resource
  196. </TableHead>
  197. <TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
  198. Type
  199. </TableHead>
  200. </TableRow>
  201. </TableHeader>
  202. <TableBody>
  203. {getResourceAccessInfo().length > 0 ? (
  204. getResourceAccessInfo().map((resource, index) => (
  205. <TableRow key={`${resource.type}-${resource.identifier}-${index}`}>
  206. <TableCell>
  207. <p className="truncate text-foreground">{resource.name}</p>
  208. </TableCell>
  209. <TableCell>
  210. <span className="text-foreground-light">{resource.type}</span>
  211. </TableCell>
  212. </TableRow>
  213. ))
  214. ) : (
  215. <TableRow>
  216. <TableCell colSpan={2}>
  217. <p className="text-foreground-light text-center py-4">
  218. {(token?.organization_slugs &&
  219. token.organization_slugs.length > 0) ||
  220. (token?.project_refs && token.project_refs.length > 0)
  221. ? 'This token has access to specific organizations and projects.'
  222. : 'This token has access to all resources.'}
  223. </p>
  224. </TableCell>
  225. </TableRow>
  226. )}
  227. </TableBody>
  228. </Table>
  229. </CardContent>
  230. </Card>
  231. </div>
  232. <div className="space-y-3">
  233. <h3 className="text-sm font-medium text-foreground">Permissions</h3>
  234. <Card className="w-full overflow-hidden bg-surface-100">
  235. <CardContent className="p-0">
  236. <Table className="p-5 table-auto">
  237. <TableHeader>
  238. <TableRow className="bg-200">
  239. <TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 w-[60%]">
  240. Permission
  241. </TableHead>
  242. <TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
  243. Access
  244. </TableHead>
  245. </TableRow>
  246. </TableHeader>
  247. <TableBody>
  248. {Object.keys(groupedResourcesByAccess).length === 0 ? (
  249. <TableRow>
  250. <TableCell colSpan={2}>
  251. <p className="text-foreground-light text-center py-4">
  252. No permissions configured for this token.
  253. </p>
  254. </TableCell>
  255. </TableRow>
  256. ) : (
  257. Object.entries(groupedResourcesByAccess).map(
  258. ([accessLevel, resources]) => {
  259. return resources.map((resource) => (
  260. <TableRow key={`${accessLevel}-${resource}`}>
  261. <TableCell>
  262. <p className="truncate text-foreground capitalize">
  263. {resource}
  264. </p>
  265. </TableCell>
  266. <TableCell>
  267. <span className="text-foreground-light">
  268. {formatAccessText(accessLevel)}
  269. </span>
  270. </TableCell>
  271. </TableRow>
  272. ))
  273. }
  274. )
  275. )}
  276. </TableBody>
  277. </Table>
  278. </CardContent>
  279. </Card>
  280. </div>
  281. </>
  282. )}
  283. </div>
  284. </ScrollArea>
  285. </SheetContent>
  286. </Sheet>
  287. </>
  288. )
  289. }