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