Refactor following types changes + remove app actions

This commit is contained in:
Gildas Garcia committed 2026-10-01 11:45:59 +02:00
1 parent 8a922a544e
commit 9c0d8ece0e
5 files changed
+20 -307

No files matched your search

@@ -1,6 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useState } from 'react'
import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import {
PageSection,
@@ -13,29 +12,20 @@ import {
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { OAuthAppsAuthorizedRow } from './OAuthAppsAuthorizedRow'
import { OAuthAppsMemberGrantsDialog } from './OAuthAppsMemberGrantsDialog'
import { OAuthAppsRevokeDialog } from './OAuthAppsRevokeDialog'
import { AlertError } from '@/components/ui/AlertError'
import { NoPermission } from '@/components/ui/NoPermission'
import { useOAuthAuthorizedAppsQuery } from '@/data/oauth-apps/oauth-apps-authorized-apps-query'
import type { OAuthAppOverviewItem } from '@/data/oauth-apps/types'
import { useOAuthApprovalsQuery } from '@/data/oauth-apps/oauth-apps-approvals-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
export const OAuthAppsAuthorizedList = () => {
const { slug } = useParams()
const [selectedAppForGrants, setSelectedAppForGrants] = useState<OAuthAppOverviewItem>()
const [selectedAppToRevoke, setSelectedAppToRevoke] = useState<OAuthAppOverviewItem>()
const { can: canReadOAuthApps, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.READ,
'approved_oauth_apps'
)
const { can: canRevokeOAuthApps } = useAsyncCheckPermissions(
PermissionAction.DELETE,
'approved_oauth_apps'
)
const { data: apps, isPending, isSuccess, isError, error } = useOAuthAuthorizedAppsQuery({ slug })
const { data: apps, isPending, isSuccess, isError, error } = useOAuthApprovalsQuery({ slug })
return (
<PageSection id="authorized-apps">
@@ -53,7 +43,6 @@ export const OAuthAppsAuthorizedList = () => {
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
@@ -69,47 +58,26 @@ export const OAuthAppsAuthorizedList = () => {
<TableHeader>
<TableRow>
<TableHead>App</TableHead>
<TableHead>Status</TableHead>
<TableHead>Grants</TableHead>
<TableHead className="text-right">
<span className="sr-only">Actions</span>
</TableHead>
<TableHead>Access</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apps.data.length === 0 ? (
<TableRow className="[&>td]:hover:bg-inherit">
<TableCell colSpan={4}>
<TableCell colSpan={2}>
<p className="text-sm text-foreground-lighter">
No apps have been authorized in this organization yet.
</p>
</TableCell>
</TableRow>
) : (
apps.data.map((app) => (
<OAuthAppsAuthorizedRow
key={app.id}
app={app}
canRevoke={canRevokeOAuthApps}
onSelectViewGrants={() => setSelectedAppForGrants(app)}
onSelectRevoke={() => setSelectedAppToRevoke(app)}
/>
))
apps.data.map((app) => <OAuthAppsAuthorizedRow key={app.id} app={app} />)
)}
</TableBody>
</Table>
</Card>
)}
</PageSectionContent>
<OAuthAppsMemberGrantsDialog
app={selectedAppForGrants}
onClose={() => setSelectedAppForGrants(undefined)}
/>
<OAuthAppsRevokeDialog
app={selectedAppToRevoke}
onClose={() => setSelectedAppToRevoke(undefined)}
/>
</PageSection>
)
}
@@ -1,35 +1,12 @@
import { MoreVertical } from 'lucide-react'
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
TableCell,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { TableCell, TableRow } from 'ui'
import type { OAuthAppOverviewItem } from '@/data/oauth-apps/types'
import type { OAuthApprovalItem } from '@/data/oauth-apps/types'
export interface OAuthAppsAuthorizedRowProps {
app: OAuthAppOverviewItem
canRevoke: boolean
onSelectViewGrants: () => void
onSelectRevoke: () => void
app: OAuthApprovalItem
}
export const OAuthAppsAuthorizedRow = ({
app,
canRevoke,
onSelectViewGrants,
onSelectRevoke,
}: OAuthAppsAuthorizedRowProps) => {
const showRevoke = canRevoke && app.status === 'active'
export const OAuthAppsAuthorizedRow = ({ app }: OAuthAppsAuthorizedRowProps) => {
return (
<TableRow>
<TableCell>
@@ -45,41 +22,7 @@ export const OAuthAppsAuthorizedRow = ({
</p>
</div>
</TableCell>
<TableCell>
<Badge variant={getStatusVariant(app.status)}>{app.status.toUpperCase()}</Badge>
</TableCell>
<TableCell>{getGrantsLabel(app)}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button icon={<MoreVertical />} className="px-1" aria-label="Actions" />
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Actions</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" side="bottom" className="w-40">
<DropdownMenuItem onClick={onSelectViewGrants}>View grants</DropdownMenuItem>
{showRevoke && (
<DropdownMenuItem className="text-destructive" onClick={onSelectRevoke}>
Revoke
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
<TableCell>{app.org_grant ? 'Organization' : 'Members'}</TableCell>
</TableRow>
)
}
function getStatusVariant(status: OAuthAppOverviewItem['status']) {
if (status === 'active') return 'success' as const
return 'warning' as const
}
function getGrantsLabel(app: OAuthAppOverviewItem) {
if (app.status === 'legacy') return 'Authorized before project controls'
if (app.member_grant_count === 1) return '1 member grant'
return `${app.member_grant_count} member grants`
}
@@ -1,105 +0,0 @@
import { useParams } from 'common'
import { Building2, User } from 'lucide-react'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from 'ui'
import { useOAuthAppMemberGrantsQuery } from '@/data/oauth-apps/oauth-apps-member-grants-query'
import type { OAuthAppOverviewItem, OAuthGrantItem } from '@/data/oauth-apps/types'
export interface OAuthAppsMemberGrantsDialogProps {
app?: OAuthAppOverviewItem
onClose: () => void
}
export const OAuthAppsMemberGrantsDialog = ({ app, onClose }: OAuthAppsMemberGrantsDialogProps) => {
const { slug } = useParams()
const { data } = useOAuthAppMemberGrantsQuery({ slug, appId: app?.id }, { enabled: Boolean(app) })
const grants = data?.data ?? []
const handleClose = () => {
onClose()
}
return (
<Dialog open={Boolean(app)} onOpenChange={(open) => !open && handleClose()}>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Member grants for {app?.name}</DialogTitle>
</DialogHeader>
<div className="px-4 max-h-90 overflow-y-auto scrollbar-gutter-stable">
<Accordion type="multiple">
{grants.map((grant) => {
const isOrganizationBound = grant.kind === 'organization_bound'
const projectRefs = grant.project_refs ?? []
const permissionCount = grant.approved_scopes.length
return (
<AccordionItem value={grant.grant_id} key={grant.grant_id}>
<AccordionTrigger className="hover:no-underline">
{isOrganizationBound ? (
<Building2 size={16} className="shrink-0 text-foreground-lighter rotate-0!" />
) : (
<User size={16} className="shrink-0 text-foreground-lighter rotate-0!" />
)}
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-foreground">{getGrantLabel(grant)}</p>
<p className="text-xs text-foreground-lighter">
{grant.project_refs === null
? 'All projects'
: `${projectRefs.length} ${projectRefs.length === 1 ? 'project' : 'projects'}`}
{' · '}
{permissionCount} {permissionCount === 1 ? 'permission' : 'permissions'}
</p>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col gap-3 pl-6">
{projectRefs.length > 0 && (
<div className="flex flex-col gap-1">
<p className="font-mono text-[11px] uppercase tracking-wider text-foreground-light">
Projects
</p>
<p className="text-xs text-foreground">{projectRefs.join(', ')}</p>
</div>
)}
<div className="flex flex-col gap-1">
<p className="font-mono text-[11px] uppercase tracking-wider text-foreground-light">
Permissions
</p>
<p className="text-xs text-foreground">
{grant.approved_scopes.join(', ')}
</p>
</div>
</div>
</AccordionContent>
</AccordionItem>
)
})}
</Accordion>
</div>
<DialogFooter>
<Button variant="default" onClick={handleClose}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function getGrantLabel(grant: OAuthGrantItem) {
if (grant.user) return grant.user.email
return 'Organization-wide'
}
@@ -1,103 +0,0 @@
import { useParams } from 'common'
import { toast } from 'sonner'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogTitle,
} from 'ui'
import { useOAuthAppRevokeMutation } from '@/data/oauth-apps/oauth-apps-revoke-mutation'
import type { OAuthAppOverviewItem } from '@/data/oauth-apps/types'
export interface OAuthAppsRevokeDialogProps {
app?: OAuthAppOverviewItem
onClose: () => void
}
export const OAuthAppsRevokeDialog = ({ app, onClose }: OAuthAppsRevokeDialogProps) => {
const { slug } = useParams()
const { mutate: revokeApp, isPending } = useOAuthAppRevokeMutation({
onSuccess: () => {
toast.success(`Revoked access for ${app?.name}`)
onClose()
},
})
const memberGrantCount = app?.member_grant_count ?? 0
const hasOrgGrant = Boolean(app?.org_grant)
return (
<Dialog open={Boolean(app)} onOpenChange={(open) => !open && onClose()}>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Revoke access for {app?.name}</DialogTitle>
</DialogHeader>
<DialogSection className="flex flex-col gap-4 text-sm text-foreground-light">
<p>
This revokes the app at{' '}
<span className="font-medium text-foreground">organization level</span> and affects{' '}
<span className="font-medium text-foreground">all users</span>.
</p>
<div className="flex flex-col gap-2">
<p>Caveats:</p>
<ul className="flex flex-col gap-1">
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>
{memberGrantCount} {memberGrantCount === 1 ? 'member grant' : 'member grants'}{' '}
will be revoked.
</span>
</li>
{hasOrgGrant && (
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>The organization-wide grant will be revoked.</span>
</li>
)}
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>The app stays blocked for this organization until an admin unblocks it.</span>
</li>
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>Every member loses access on the apps next request.</span>
</li>
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>Members will need to authorize again to reconnect.</span>
</li>
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>
This is <span className="font-medium text-foreground">not</span> a per-user
revocation.
</span>
</li>
</ul>
</div>
</DialogSection>
<DialogFooter>
<Button variant="default" disabled={isPending} onClick={onClose}>
Cancel
</Button>
<Button
variant="danger"
loading={isPending}
onClick={() => {
if (app && slug) revokeApp({ slug, appId: app.id })
}}
>
Revoke
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
+10
View File
@@ -65,6 +65,16 @@
// read are documentation of the valid values, not dead code. Scoped to
// this file so `enumMembers` keeps working everywhere else.
"data/database/constraints-query.ts": ["enumMembers"],
// Org settings authorized-apps data layer, landed in #49476 ahead of the
// tables that consume it. Scoped to the remaining files rather than the whole
// folder so everything else here stays gated.
// TODO(FE-4330): remove — used from #50529, drop this entry in that PR.
"data/oauth-apps/oauth-apps-approvals-query.ts": ["files"],
"data/oauth-apps/oauth-apps-grants-query.ts": ["files"],
"data/oauth-apps/oauth-apps-member-grants-query.ts": ["files"],
"data/oauth-apps/oauth-apps-preflight-validation-query.ts": ["files"],
"data/oauth-apps/oauth-apps-revoke-grant-mutation.ts": ["files"],
"data/oauth-apps/oauth-apps-revoke-mutation.ts": ["files"],
"hooks/misc/useTrackExperimentExposure.ts": ["files"],
// Staging-only pinned Postgres image + FDW request payload for creating
// Warehouse test projects by hand. Nothing imports it yet — it's kept