mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Refactor following types changes + remove app actions
This commit is contained in:
5 files changed
+20
-307
No files matched your search
+5
-37
@@ -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>
|
||||
)
|
||||
}
|
||||
+5
-62
@@ -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`
|
||||
}
|
||||
-105
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user