mirror of
https://github.com/supabase/supabase.git
synced 2026-09-09 11:30:17 +08:00
## Problem We're working on OAuth Apps Scoped Grants and want to display an alternative UI for Authorized apps depending on a feature flag. However, the `/apps` page currently contains the code of both the _Publishable apps_ and _Authorized apps_ sections. ## Solution No functional nor design changes. - Extract both sections into their own components to ease the introduction of the alternative UI - Improve a11y <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added separate views for managing publishable and authorized OAuth applications. - Users can publish, update, and delete applications, with convenient credential-copy controls. - Authorized applications can be sorted and revoked through a confirmation dialog. - **User Experience** - Added clear loading, empty, error, and permission-denied states for OAuth application lists. - Improved organization settings layout for OAuth app management. - Added consistent sorting behavior when selecting and toggling table columns. - Improved accessibility for credential notifications and dismissal controls. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
177 lines
6.2 KiB
TypeScript
177 lines
6.2 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { useMemo, useState } from 'react'
|
|
import {
|
|
Card,
|
|
cn,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableHeadSort,
|
|
TableRow,
|
|
} from 'ui'
|
|
import {
|
|
PageSection,
|
|
PageSectionContent,
|
|
PageSectionDescription,
|
|
PageSectionMeta,
|
|
PageSectionSummary,
|
|
PageSectionTitle,
|
|
} from 'ui-patterns/PageSection'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { AuthorizedAppRow } from './AuthorizedAppRow'
|
|
import { parseSort, toggleSort } from './OAuthApps.utils'
|
|
import { RevokeAppModal } from './RevokeAppModal'
|
|
import { AlertError } from '@/components/ui/AlertError'
|
|
import { NoPermission } from '@/components/ui/NoPermission'
|
|
import { AuthorizedApp, useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
|
|
type AuthorizedAppsSort = 'authorized:asc' | 'authorized:desc'
|
|
type AuthorizedAppsSortColumn = 'authorized'
|
|
|
|
export const AuthorizedApps = () => {
|
|
const { slug } = useParams()
|
|
|
|
const [selectedAppToRevoke, setSelectedAppToRevoke] = useState<AuthorizedApp>()
|
|
const [authorizedAppsSort, setAuthorizedAppsSort] = useState<AuthorizedAppsSort>('authorized:asc')
|
|
|
|
const { can: canReadOAuthApps, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
|
|
PermissionAction.READ,
|
|
'approved_oauth_apps'
|
|
)
|
|
|
|
const {
|
|
data: authorizedApps,
|
|
isPending: isLoadingAuthorizedApps,
|
|
isSuccess: isSuccessAuthorizedApps,
|
|
isError: isErrorAuthorizedApps,
|
|
error: authorizedAppsError,
|
|
} = useAuthorizedAppsQuery({ slug })
|
|
|
|
const sortedAuthorizedApps = useMemo(() => {
|
|
const [sortColumn, sortOrder] = parseSort<AuthorizedAppsSortColumn>(authorizedAppsSort)
|
|
const orderMultiplier = sortOrder === 'asc' ? 1 : -1
|
|
|
|
return [...(authorizedApps ?? [])].sort((a, b) => {
|
|
if (sortColumn === 'authorized') {
|
|
return (
|
|
(new Date(a.authorized_at).getTime() - new Date(b.authorized_at).getTime()) *
|
|
orderMultiplier
|
|
)
|
|
}
|
|
|
|
return 0
|
|
})
|
|
}, [authorizedApps, authorizedAppsSort])
|
|
|
|
const hasAuthorizedApps = (authorizedApps?.length ?? 0) > 0
|
|
const avatarHeadClass = 'w-[62px] min-w-[62px] max-w-[62px]'
|
|
const avatarHeadCollapsedClass = 'w-0 min-w-0 max-w-0 p-0'
|
|
|
|
const handleAuthorizedSortChange = (column: AuthorizedAppsSortColumn) => {
|
|
toggleSort(authorizedAppsSort, column, setAuthorizedAppsSort)
|
|
}
|
|
|
|
return (
|
|
<PageSection id="authorized-apps">
|
|
<PageSectionMeta>
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>Authorized apps</PageSectionTitle>
|
|
<PageSectionDescription>
|
|
Applications that have access to your organization's settings and projects
|
|
</PageSectionDescription>
|
|
</PageSectionSummary>
|
|
</PageSectionMeta>
|
|
<PageSectionContent className="space-y-4">
|
|
{isLoadingAuthorizedApps || isLoadingPermissions ? (
|
|
<div className="space-y-2">
|
|
<ShimmeringLoader />
|
|
<ShimmeringLoader className="w-3/4" />
|
|
<ShimmeringLoader className="w-1/2" />
|
|
</div>
|
|
) : !canReadOAuthApps ? (
|
|
<NoPermission resourceText="view authorized apps" />
|
|
) : null}
|
|
|
|
{isErrorAuthorizedApps && (
|
|
<AlertError subject="Failed to retrieve authorized apps" error={authorizedAppsError} />
|
|
)}
|
|
|
|
{isSuccessAuthorizedApps && (
|
|
<Card>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead
|
|
className={cn(
|
|
hasAuthorizedApps ? avatarHeadClass : avatarHeadCollapsedClass,
|
|
!hasAuthorizedApps && 'text-foreground-muted'
|
|
)}
|
|
>
|
|
<span className="sr-only">Avatar</span>
|
|
</TableHead>
|
|
<TableHead className={cn(!hasAuthorizedApps && 'text-foreground-muted')}>
|
|
Name
|
|
</TableHead>
|
|
<TableHead className={cn(!hasAuthorizedApps && 'text-foreground-muted')}>
|
|
Author
|
|
</TableHead>
|
|
<TableHead className={cn(!hasAuthorizedApps && 'text-foreground-muted')}>
|
|
App ID
|
|
</TableHead>
|
|
<TableHead className={cn(!hasAuthorizedApps && 'text-foreground-muted')}>
|
|
{hasAuthorizedApps ? (
|
|
<TableHeadSort
|
|
column="authorized"
|
|
currentSort={authorizedAppsSort}
|
|
onSortChange={handleAuthorizedSortChange}
|
|
>
|
|
AUTHORIZED
|
|
</TableHeadSort>
|
|
) : (
|
|
'AUTHORIZED'
|
|
)}
|
|
</TableHead>
|
|
<TableHead
|
|
className={cn('text-right', !hasAuthorizedApps && 'text-foreground-muted')}
|
|
>
|
|
<span className="sr-only">Actions</span>
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{hasAuthorizedApps ? (
|
|
sortedAuthorizedApps?.map((app) => (
|
|
<AuthorizedAppRow
|
|
key={app.id}
|
|
app={app}
|
|
onSelectRevoke={() => setSelectedAppToRevoke(app)}
|
|
/>
|
|
))
|
|
) : (
|
|
<TableRow className="[&>td]:hover:bg-inherit">
|
|
<TableCell colSpan={6}>
|
|
<p className="text-sm text-foreground">No results found</p>
|
|
<p className="text-sm text-foreground-lighter">
|
|
You do not have any authorized applications yet
|
|
</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
</PageSectionContent>
|
|
<RevokeAppModal
|
|
selectedApp={selectedAppToRevoke}
|
|
onClose={() => setSelectedAppToRevoke(undefined)}
|
|
/>
|
|
</PageSection>
|
|
)
|
|
}
|