Files
supabase/apps/studio/components/interfaces/Linter/GraphqlExposureLintCTA.tsx
Alaister YoungandAlaister Young ca2b50a0a7 chore(ui-patterns): collapse the admonition shim into ui-patterns/Admonition (#48377)
Follow-up to #48344: collapses the two resolution paths for the
Admonition module into one.

`src/admonition.tsx` was a back-compat shim re-exporting
`src/Admonition/`. Two ways to resolve one module is exactly what
produced the macOS self-import bug fixed in #48344, and the local
typecheck errors that #48374 worked around. This removes the shim and
standardizes on the PascalCase subpath, matching every other export in
the package.

**Changed:**

- Codemodded all 246 `ui-patterns/admonition` imports to
`ui-patterns/Admonition` (240 `.tsx`, 5 `.mdx`, 1 `.ts` across studio,
docs, www, design-system, and lite-studio)
- Pointed the 5 internal `'../admonition'` imports back at the
`'../Admonition'` directory

**Removed:**

- `packages/ui-patterns/src/admonition.tsx`, and its `./admonition`
entry in the exports map (regenerated with `pnpm gen:exports`)

## To test

- `grep -r "ui-patterns/admonition" --include='*.ts*'` → no hits
- `pnpm test:case-hazards` → passes
- `pnpm typecheck` → all 15 tasks green
- `pnpm --filter studio run lint:ratchet` → passes
- `pnpm --filter ui-patterns vitest run src/Admonition` → 11 tests pass

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Standardized Admonition component imports across the application and
documentation.
* Improved compatibility with case-sensitive environments by using the
canonical component path.
  * Removed the legacy Admonition import entry point.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-07-29 00:48:56 +08:00

192 lines
6.7 KiB
TypeScript

import { ident, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQueryClient } from '@tanstack/react-query'
import { EyeOff, Lock } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { Badge, Button } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { InlineLink } from '@/components/ui/InlineLink'
import { lintKeys } from '@/data/lint/keys'
import { Lint } from '@/data/lint/lint-query'
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const GRAPHQL_EXPOSURE_LINT_NAMES = [
'pg_graphql_anon_table_exposed',
'pg_graphql_authenticated_table_exposed',
] as const
export type GraphqlExposureLintName = (typeof GRAPHQL_EXPOSURE_LINT_NAMES)[number]
export const asGraphqlExposureLint = (
name: string | undefined | null
): GraphqlExposureLintName | null =>
!!name && (GRAPHQL_EXPOSURE_LINT_NAMES as readonly string[]).includes(name)
? (name as GraphqlExposureLintName)
: null
interface GraphqlExposureLintCTAProps {
lintName: GraphqlExposureLintName
projectRef: string
metadata: Lint['metadata']
onAfterAction?: () => void
}
const ROLE_BY_LINT: Record<GraphqlExposureLintName, 'anon' | 'authenticated'> = {
pg_graphql_anon_table_exposed: 'anon',
pg_graphql_authenticated_table_exposed: 'authenticated',
}
const AUDIENCE: Record<GraphqlExposureLintName, { lower: string; upper: string }> = {
pg_graphql_anon_table_exposed: { lower: 'anonymous users', upper: 'Anonymous users' },
pg_graphql_authenticated_table_exposed: { lower: 'signed-in users', upper: 'Signed-in users' },
}
const TRIGGER_LABEL: Record<GraphqlExposureLintName, string> = {
pg_graphql_anon_table_exposed: 'Remove access for anonymous users',
pg_graphql_authenticated_table_exposed: 'Remove access for signed-in users',
}
export const GraphqlExposureLintCTA = ({
lintName,
projectRef,
metadata,
onAfterAction,
}: GraphqlExposureLintCTAProps) => {
const { data: project } = useSelectedProjectQuery()
const queryClient = useQueryClient()
const [showConfirmRevoke, setShowConfirmRevoke] = useState(false)
const schema = metadata?.schema
const name = metadata?.name
const objectType = metadata?.type ?? 'object'
const role = ROLE_BY_LINT[lintName]
const audience = AUDIENCE[lintName]
const canAct = !!schema && !!name
const revokeSql =
schema && name
? safeSql`revoke all on ${ident(schema)}.${ident(name)} from ${ident(role)};`
: undefined
const { mutate: executeSql, isPending: isRevoking } = useExecuteSqlMutation({
onSuccess: async () => {
toast.success(
`Revoked access to ${schema}.${name} from ${role}. ${audience.upper} can no longer query this ${objectType} via GraphQL or Data API.`
)
setShowConfirmRevoke(false)
await queryClient.invalidateQueries({ queryKey: lintKeys.lint(projectRef) })
onAfterAction?.()
},
onError: (error) => {
toast.error(`Failed to revoke access: ${error.message}`)
},
})
const handleRevoke = () => {
if (!revokeSql) return
executeSql({
projectRef,
connectionString: project?.connectionString,
sql: revokeSql,
})
}
return (
<>
<Button variant="primary" disabled={!canAct} onClick={() => setShowConfirmRevoke(true)}>
{TRIGGER_LABEL[lintName]}
</Button>
<ConfirmationModal
visible={showConfirmRevoke}
size="xlarge"
title={
canAct
? `Remove access to ${schema}.${name} for ${audience.lower}?`
: `Remove access for ${audience.lower}?`
}
confirmLabel="Remove access"
confirmLabelLoading="Removing access..."
cancelLabel="Cancel"
loading={isRevoking}
onCancel={() => setShowConfirmRevoke(false)}
onConfirm={handleRevoke}
>
<div className="text-sm text-foreground mb-6">
<p>This change affects both schema visibility and data access for {audience.lower}.</p>
<p>
Alternatively, you can{' '}
<InlineLink href={`/project/${projectRef}/database/extensions`}>
disable GraphQL
</InlineLink>{' '}
to remove schema visibility.
</p>
</div>
<div className="space-y-5">
<div className="flex gap-3">
<Lock className="text-foreground-light shrink-0 mt-0.5" size={20} strokeWidth={1.5} />
<div>
<div className="flex items-center gap-2">
<p className="text-sm text-foreground">Data API access removed</p>
<Badge variant="warning">Breaking change</Badge>
</div>
<p className="text-sm text-foreground-light mt-1">
{audience.upper} will no longer be able to read or write to this {objectType} via
Supabase APIs (GraphQL or Data API), even if RLS policies allow it.
</p>
</div>
</div>
<div className="flex gap-3">
<EyeOff className="text-foreground-light shrink-0 mt-0.5" size={20} strokeWidth={1.5} />
<div>
<p className="text-sm text-foreground">Schema hidden from GraphQL</p>
<p className="text-sm text-foreground-light mt-1">
This {objectType} will no longer appear in the GraphQL schema. {audience.upper}{' '}
won't be able to discover its name, columns, or relationships.
</p>
</div>
</div>
</div>
<Admonition
type="warning"
title="When to keep access"
description={`If your app needs ${audience.lower} to query this ${objectType}, keep access and ignore this warning. Be aware that this ${objectType}'s schema will remain visible via the GraphQL API.`}
className="mt-6"
/>
<p className="text-sm text-foreground-light mt-6">
The following statement will be executed:
</p>
<pre className="mt-2 px-3 py-2 rounded bg-surface-200 text-xs font-mono whitespace-pre-wrap break-all">
{revokeSql}
</pre>
</ConfirmationModal>
</>
)
}
export const GraphqlExposureCallout = ({ projectRef }: { projectRef: string }) => {
return (
<Admonition
type="default"
title="Why this appears"
description={
<p>
These warnings are triggered by GraphQL exposing your table schemas. If you're not using
GraphQL, disable it from the{' '}
<InlineLink href={`/project/${projectRef}/database/extensions`}>
Database extensions page
</InlineLink>
.
</p>
}
/>
)
}