mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
chore(studio): remove tableEditorApiAccessToggle flag (#45081)
Cleans up the `tableEditorApiAccessToggle` PostHog flag now that the gated UI is shipping to everyone. Follow-up to #45034 — the new project-creation checkbox makes the management UI a prerequisite, so no reason to keep it behind a flag. **Removed:** - `useDataApiGrantTogglesEnabled` hook - Old schemas-only multi-selector branch in the Data API settings page (the rich per-table / per-function toggles + default-privileges switch become the only UI) - Flag gate around the `<ApiAccessToggle>` section in the table editor side panel - Flag gates around `updateTableApiAccess` calls in the save pipeline (create / duplicate / update) - `tableEditorApiAccessToggleEnabled` telemetry property + stale JSDoc / docs references **Changed:** - `createTableApiAccessHandlerParams` no longer takes an `enabled` param — it was always `true` after removal ## To test - Integrations → Data API settings page: exposed tables, exposed functions, default-privileges toggle all render and save correctly - Table editor: creating, duplicating, and editing a table all run the expected Data API privilege updates - Project creation flow still works end-to-end (unchanged, but the submit telemetry no longer includes `tableEditorApiAccessToggleEnabled`) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * API access configuration is now always available in the table editor and PostgreSQL settings, removing previous conditional gating. * Simplified the "Automatically expose new tables and functions" interface by consolidating UI branches. * **Documentation** * Updated telemetry guidance and examples with current feature-flag references. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
applyTo: "apps/studio/**,packages/common/telemetry*"
|
||||
applyTo: 'apps/studio/**,packages/common/telemetry*'
|
||||
---
|
||||
|
||||
# Studio Telemetry Review Rules
|
||||
@@ -11,8 +11,8 @@ All comments are **advisory** — suggest, do not request changes.
|
||||
Use judgment — not every PR needs telemetry. But **always flag** when:
|
||||
|
||||
1. **Changes to `packages/common/telemetry-constants.ts`** — validate event naming, property conventions, and JSDoc accuracy.
|
||||
2. **PostHog feature flags without measurement.** If a PR uses `usePHFlag` or PostHog-backed hooks like `useDataApiGrantTogglesEnabled` to gate behavior, the flag state should be captured in a telemetry event so the rollout can be measured. Flag if the flag value isn't included in a relevant `track()` call. (Note: `useFlag` from `common` reads ConfigCat flags, not PostHog — different system, different guidance.)
|
||||
3. **Feature-flagged rollouts without outcome tracking.** If a flag gates new behavior, there should be telemetry on both the flag state *and* how users respond to the new behavior (e.g., toggle clicks, opt-in actions).
|
||||
2. **PostHog feature flags without measurement.** If a PR uses `usePHFlag` or PostHog-backed hooks like `useDataApiRevokeOnCreateDefaultEnabled` to gate behavior, the flag state should be captured in a telemetry event so the rollout can be measured. Flag if the flag value isn't included in a relevant `track()` call. (Note: `useFlag` from `common` reads ConfigCat flags, not PostHog — different system, different guidance.)
|
||||
3. **Feature-flagged rollouts without outcome tracking.** If a flag gates new behavior, there should be telemetry on both the flag state _and_ how users respond to the new behavior (e.g., toggle clicks, opt-in actions).
|
||||
4. **Growth-oriented components adding user interactions without tracking** — onboarding flows, setup wizards, upgrade CTAs, A/B experiment variants.
|
||||
|
||||
When tracking is missing, comment: _"This adds a user interaction (or feature flag) that may benefit from tracking."_ Then propose an event name and `useTrack()` call.
|
||||
@@ -52,6 +52,7 @@ Flag: unapproved verbs (`saved`, `viewed`, `pressed`), wrong order (`click_produ
|
||||
|
||||
```typescript
|
||||
import { useTrack } from 'lib/telemetry/track'
|
||||
|
||||
const track = useTrack()
|
||||
track('product_card_clicked', { productType: 'database', planTier: 'pro' })
|
||||
```
|
||||
|
||||
@@ -3,7 +3,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import { Lock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
@@ -43,17 +42,14 @@ import { ExposedTableSelector } from '@/components/interfaces/Settings/API/Expos
|
||||
import { FormActions } from '@/components/ui/Forms/FormActions'
|
||||
import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
|
||||
import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation'
|
||||
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
|
||||
import { useSchemasQuery } from '@/data/database/schemas-query'
|
||||
import { defaultPrivilegesQueryOptions } from '@/data/privileges/default-privileges-query'
|
||||
import { privilegeKeys } from '@/data/privileges/keys'
|
||||
import { useUpdateDefaultPrivilegesMutation } from '@/data/privileges/update-default-privileges-mutation'
|
||||
import { useUpdateExposedEntitiesMutation } from '@/data/privileges/update-exposed-entities-mutation'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useDataApiGrantTogglesEnabled } from '@/hooks/misc/useDataApiGrantTogglesEnabled'
|
||||
import useLatest from '@/hooks/misc/useLatest'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import { noop } from '@/lib/void'
|
||||
import type { ResponseError } from '@/types'
|
||||
@@ -84,7 +80,6 @@ export const PostgrestConfig = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const queryClient = useQueryClient()
|
||||
const isApiGrantTogglesEnabled = useDataApiGrantTogglesEnabled()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
@@ -94,10 +89,6 @@ export const PostgrestConfig = () => {
|
||||
isPending: isLoadingConfig,
|
||||
isSuccess: isSuccessConfig,
|
||||
} = useProjectPostgrestConfigQuery({ projectRef })
|
||||
const { data: extensions } = useDatabaseExtensionsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const {
|
||||
data: allSchemas = [],
|
||||
isPending: isLoadingSchemas,
|
||||
@@ -125,24 +116,6 @@ export const PostgrestConfig = () => {
|
||||
|
||||
const isLoading = isLoadingConfig || isLoadingSchemas || isLoadingDefaultPrivileges
|
||||
|
||||
const schemas = useMemo(
|
||||
() =>
|
||||
allSchemas
|
||||
.filter((x) => {
|
||||
if (x.name === 'graphql_public') return true
|
||||
return !INTERNAL_SCHEMAS.includes(x.name)
|
||||
})
|
||||
.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
value: x.name,
|
||||
name: x.name,
|
||||
disabled: false,
|
||||
}
|
||||
}) ?? [],
|
||||
[allSchemas]
|
||||
)
|
||||
|
||||
const { mutateAsync: updatePostgrestConfig } = useProjectPostgrestConfigUpdateMutation({
|
||||
onError: noop,
|
||||
})
|
||||
@@ -159,9 +132,6 @@ export const PostgrestConfig = () => {
|
||||
useAsyncCheckPermissions(PermissionAction.UPDATE, 'custom_config_postgrest')
|
||||
const canUpdatePostgrestConfig = IS_PLATFORM && canUpdatePostgrestConfigPermission
|
||||
|
||||
const isGraphqlExtensionEnabled =
|
||||
(extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
return {
|
||||
dbSchema: configDbSchemas,
|
||||
@@ -198,23 +168,21 @@ export const PostgrestConfig = () => {
|
||||
try {
|
||||
let dbSchema = values.dbSchema.join(',')
|
||||
|
||||
if (isApiGrantTogglesEnabled) {
|
||||
await updateExposedEntities({
|
||||
await updateExposedEntities({
|
||||
projectRef,
|
||||
connectionString: project?.connectionString,
|
||||
tableIdsToAdd: values.tableIdsToAdd,
|
||||
tableIdsToRemove: values.tableIdsToRemove,
|
||||
functionNamesToAdd: values.functionNamesToAdd,
|
||||
functionNamesToRemove: values.functionNamesToRemove,
|
||||
})
|
||||
|
||||
if (values.defaultPrivilegesGranted !== defaultPrivilegesGranted) {
|
||||
await updateDefaultPrivileges({
|
||||
projectRef,
|
||||
connectionString: project?.connectionString,
|
||||
tableIdsToAdd: values.tableIdsToAdd,
|
||||
tableIdsToRemove: values.tableIdsToRemove,
|
||||
functionNamesToAdd: values.functionNamesToAdd,
|
||||
functionNamesToRemove: values.functionNamesToRemove,
|
||||
granted: values.defaultPrivilegesGranted,
|
||||
})
|
||||
|
||||
if (values.defaultPrivilegesGranted !== defaultPrivilegesGranted) {
|
||||
await updateDefaultPrivileges({
|
||||
projectRef,
|
||||
connectionString: project?.connectionString,
|
||||
granted: values.defaultPrivilegesGranted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await updatePostgrestConfig(
|
||||
@@ -308,238 +276,150 @@ export const PostgrestConfig = () => {
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
{isApiGrantTogglesEnabled ? (
|
||||
<CardContent className="space-y-6">
|
||||
<FormItemLayout
|
||||
isReactForm={false}
|
||||
layout="flex-row-reverse"
|
||||
label="Exposed schemas"
|
||||
description="Select schemas to include in the Data API. Schemas must be included before tables can be exposed."
|
||||
>
|
||||
<ExposedSchemaSelector
|
||||
selectedSchemas={watchedDbSchema}
|
||||
disabled={!canUpdatePostgrestConfig}
|
||||
onToggleSchema={(schema) => {
|
||||
const current = form.getValues('dbSchema')
|
||||
if (current.includes(schema)) {
|
||||
form.setValue(
|
||||
'dbSchema',
|
||||
current.filter((x) => x !== schema),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('dbSchema', [...current, schema], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
<CardContent className="space-y-6">
|
||||
<FormItemLayout
|
||||
isReactForm={false}
|
||||
layout="flex-row-reverse"
|
||||
label="Exposed schemas"
|
||||
description="Select schemas to include in the Data API. Schemas must be included before tables can be exposed."
|
||||
>
|
||||
<ExposedSchemaSelector
|
||||
selectedSchemas={watchedDbSchema}
|
||||
disabled={!canUpdatePostgrestConfig}
|
||||
onToggleSchema={(schema) => {
|
||||
const current = form.getValues('dbSchema')
|
||||
if (current.includes(schema)) {
|
||||
form.setValue(
|
||||
'dbSchema',
|
||||
current.filter((x) => x !== schema),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('dbSchema', [...current, schema], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
|
||||
<FormItemLayout
|
||||
isReactForm={false}
|
||||
layout="flex-row-reverse"
|
||||
label="Exposed tables"
|
||||
description="Toggle Data API access for individual tables."
|
||||
>
|
||||
<ExposedTableSelector
|
||||
selectedSchemas={watchedDbSchema}
|
||||
pendingAddTableIds={watchedTableIdsToAdd}
|
||||
pendingRemoveTableIds={watchedTableIdsToRemove}
|
||||
onTogglePendingAdd={(tableId) => {
|
||||
const current = form.getValues('tableIdsToAdd')
|
||||
if (current.includes(tableId)) {
|
||||
form.setValue(
|
||||
'tableIdsToAdd',
|
||||
current.filter((x) => x !== tableId),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('tableIdsToAdd', [...current, tableId], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onTogglePendingRemove={(tableId) => {
|
||||
const current = form.getValues('tableIdsToRemove')
|
||||
if (current.includes(tableId)) {
|
||||
form.setValue(
|
||||
'tableIdsToRemove',
|
||||
current.filter((x) => x !== tableId),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('tableIdsToRemove', [...current, tableId], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
<FormItemLayout
|
||||
isReactForm={false}
|
||||
layout="flex-row-reverse"
|
||||
label="Exposed tables"
|
||||
description="Toggle Data API access for individual tables."
|
||||
>
|
||||
<ExposedTableSelector
|
||||
selectedSchemas={watchedDbSchema}
|
||||
pendingAddTableIds={watchedTableIdsToAdd}
|
||||
pendingRemoveTableIds={watchedTableIdsToRemove}
|
||||
onTogglePendingAdd={(tableId) => {
|
||||
const current = form.getValues('tableIdsToAdd')
|
||||
if (current.includes(tableId)) {
|
||||
form.setValue(
|
||||
'tableIdsToAdd',
|
||||
current.filter((x) => x !== tableId),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('tableIdsToAdd', [...current, tableId], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onTogglePendingRemove={(tableId) => {
|
||||
const current = form.getValues('tableIdsToRemove')
|
||||
if (current.includes(tableId)) {
|
||||
form.setValue(
|
||||
'tableIdsToRemove',
|
||||
current.filter((x) => x !== tableId),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('tableIdsToRemove', [...current, tableId], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
|
||||
<FormItemLayout
|
||||
isReactForm={false}
|
||||
layout="flex-row-reverse"
|
||||
label="Exposed functions"
|
||||
description="Toggle Data API access for individual functions."
|
||||
>
|
||||
<ExposedFunctionSelector
|
||||
selectedSchemas={watchedDbSchema}
|
||||
pendingAddFunctionNames={watchedFunctionNamesToAdd}
|
||||
pendingRemoveFunctionNames={watchedFunctionNamesToRemove}
|
||||
onTogglePendingAdd={(functionName) => {
|
||||
const current = form.getValues('functionNamesToAdd')
|
||||
if (current.includes(functionName)) {
|
||||
form.setValue(
|
||||
'functionNamesToAdd',
|
||||
current.filter((x) => x !== functionName),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('functionNamesToAdd', [...current, functionName], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onTogglePendingRemove={(functionName) => {
|
||||
const current = form.getValues('functionNamesToRemove')
|
||||
if (current.includes(functionName)) {
|
||||
form.setValue(
|
||||
'functionNamesToRemove',
|
||||
current.filter((x) => x !== functionName),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('functionNamesToRemove', [...current, functionName], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
<FormItemLayout
|
||||
isReactForm={false}
|
||||
layout="flex-row-reverse"
|
||||
label="Exposed functions"
|
||||
description="Toggle Data API access for individual functions."
|
||||
>
|
||||
<ExposedFunctionSelector
|
||||
selectedSchemas={watchedDbSchema}
|
||||
pendingAddFunctionNames={watchedFunctionNamesToAdd}
|
||||
pendingRemoveFunctionNames={watchedFunctionNamesToRemove}
|
||||
onTogglePendingAdd={(functionName) => {
|
||||
const current = form.getValues('functionNamesToAdd')
|
||||
if (current.includes(functionName)) {
|
||||
form.setValue(
|
||||
'functionNamesToAdd',
|
||||
current.filter((x) => x !== functionName),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('functionNamesToAdd', [...current, functionName], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onTogglePendingRemove={(functionName) => {
|
||||
const current = form.getValues('functionNamesToRemove')
|
||||
if (current.includes(functionName)) {
|
||||
form.setValue(
|
||||
'functionNamesToRemove',
|
||||
current.filter((x) => x !== functionName),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
} else {
|
||||
form.setValue('functionNamesToRemove', [...current, functionName], {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
|
||||
{watchedDbSchema.includes('public') && (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="defaultPrivilegesGranted"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_>
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Automatically expose new tables and functions"
|
||||
description="Grants privileges to Data API roles by default, exposing new tables and functions. We recommend disabling this to control access manually."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<div>
|
||||
<Switch
|
||||
size="large"
|
||||
disabled={!canUpdatePostgrestConfig}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedDbSchema.length === 0 && (
|
||||
<Admonition
|
||||
type="warning"
|
||||
title="No schema is currently selected"
|
||||
description="Saving with no selected schema or table will disable the Data API."
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
) : (
|
||||
<CardContent>
|
||||
{watchedDbSchema.includes('public') && (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="dbSchema"
|
||||
name="defaultPrivilegesGranted"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_>
|
||||
<FormItemLayout
|
||||
label="Exposed schemas"
|
||||
description="The schemas to expose in your API. Tables, views and functions in
|
||||
these schemas will get API endpoints."
|
||||
layout="flex-row-reverse"
|
||||
label="Automatically expose new tables and functions"
|
||||
description="Grants privileges to Data API roles by default, exposing new tables and functions. We recommend disabling this to control access manually."
|
||||
>
|
||||
{isLoadingSchemas ? (
|
||||
<div className="col-span-12 flex flex-col gap-2 lg:col-span-7">
|
||||
<Skeleton className="w-full h-[38px]" />
|
||||
</div>
|
||||
) : (
|
||||
<MultiSelector
|
||||
onValuesChange={field.onChange}
|
||||
values={field.value}
|
||||
size="small"
|
||||
disabled={!canUpdatePostgrestConfig}
|
||||
>
|
||||
<MultiSelectorTrigger
|
||||
mode="inline-combobox"
|
||||
label="Select schemas..."
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge
|
||||
<FormControl_Shadcn_>
|
||||
<div>
|
||||
<Switch
|
||||
size="large"
|
||||
disabled={!canUpdatePostgrestConfig}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{schemas.length <= 0 ? (
|
||||
<MultiSelectorItem key="empty" value="no">
|
||||
no
|
||||
</MultiSelectorItem>
|
||||
) : (
|
||||
schemas.map((x) => (
|
||||
<MultiSelectorItem
|
||||
key={x.id + '-' + x.name}
|
||||
value={x.name}
|
||||
>
|
||||
{x.name}
|
||||
</MultiSelectorItem>
|
||||
))
|
||||
)}
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
)}
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
{!field.value.includes('public') && field.value.length > 0 && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="The public schema for this project is not exposed"
|
||||
className="mt-2"
|
||||
description={
|
||||
<>
|
||||
<p className="text-sm">
|
||||
You will not be able to query tables and views in the{' '}
|
||||
<code className="text-code-inline">public</code> schema via
|
||||
supabase-js or HTTP clients.
|
||||
</p>
|
||||
{isGraphqlExtensionEnabled && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
Tables in the{' '}
|
||||
<code className="text-code-inline">public</code> schema
|
||||
are still exposed over our GraphQL endpoints.
|
||||
</p>
|
||||
<Button asChild type="default" className="mt-2">
|
||||
<Link href={`/project/${projectRef}/database/extensions`}>
|
||||
Disable the pg_graphql extension
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
)}
|
||||
|
||||
{watchedDbSchema.length === 0 && (
|
||||
<Admonition
|
||||
type="warning"
|
||||
title="No schema is currently selected"
|
||||
description="Saving with no selected schema or table will disable the Data API."
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
|
||||
+16
-29
@@ -56,7 +56,6 @@ import { tableRowKeys } from '@/data/table-rows/keys'
|
||||
import { tableKeys } from '@/data/tables/keys'
|
||||
import { RetrieveTableResult } from '@/data/tables/table-retrieve-query'
|
||||
import { getTables } from '@/data/tables/tables-query'
|
||||
import { useDataApiGrantTogglesEnabled } from '@/hooks/misc/useDataApiGrantTogglesEnabled'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
|
||||
@@ -137,16 +136,12 @@ const DUMMY_TABLE_API_ACCESS_PARAMS: TableApiAccessParams = {
|
||||
}
|
||||
|
||||
const createTableApiAccessHandlerParams = ({
|
||||
enabled,
|
||||
snap,
|
||||
selectedTable,
|
||||
}: {
|
||||
enabled: boolean
|
||||
snap: DeepReadonly<TableEditorState>
|
||||
selectedTable?: PostgresTable
|
||||
}): TableApiAccessParams | undefined => {
|
||||
if (!enabled) return undefined
|
||||
|
||||
const tableSidePanel = snap.sidePanel?.type === 'table' ? snap.sidePanel : undefined
|
||||
if (!tableSidePanel) return undefined
|
||||
|
||||
@@ -198,7 +193,6 @@ export const SidePanelEditor = ({
|
||||
const queryClient = useQueryClient()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
const isApiGrantTogglesEnabled = useDataApiGrantTogglesEnabled()
|
||||
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
|
||||
const { updateRow, addRow, isEditPending } = useTableRowOperations()
|
||||
|
||||
@@ -211,7 +205,6 @@ export const SidePanelEditor = ({
|
||||
})
|
||||
|
||||
const tableApiAccessParams = createTableApiAccessHandlerParams({
|
||||
enabled: isApiGrantTogglesEnabled,
|
||||
snap,
|
||||
selectedTable,
|
||||
})
|
||||
@@ -595,7 +588,7 @@ export const SidePanelEditor = ({
|
||||
let toastId
|
||||
let saveTableError = false
|
||||
|
||||
if (isApiGrantTogglesEnabled && !apiAccessToggleHandler.isSuccess) {
|
||||
if (!apiAccessToggleHandler.isSuccess) {
|
||||
if (apiAccessToggleHandler.isPending) {
|
||||
toast.info(
|
||||
'Cannot save table yet because Data API settings are still loading. Please try again in a moment.'
|
||||
@@ -687,13 +680,11 @@ export const SidePanelEditor = ({
|
||||
async () => {
|
||||
if (isRealtimeEnabled) await updateTableRealtime(table, true)
|
||||
|
||||
if (isApiGrantTogglesEnabled) {
|
||||
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
|
||||
? apiAccessToggleHandler.data.privileges
|
||||
: undefined
|
||||
if (privilegesToSet) {
|
||||
await updateTableApiAccess(table, privilegesToSet)
|
||||
}
|
||||
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
|
||||
? apiAccessToggleHandler.data.privileges
|
||||
: undefined
|
||||
if (privilegesToSet) {
|
||||
await updateTableApiAccess(table, privilegesToSet)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -762,13 +753,11 @@ export const SidePanelEditor = ({
|
||||
})
|
||||
if (isRealtimeEnabled) await updateTableRealtime(table, isRealtimeEnabled)
|
||||
|
||||
if (isApiGrantTogglesEnabled) {
|
||||
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
|
||||
? apiAccessToggleHandler.data.privileges
|
||||
: undefined
|
||||
if (privilegesToSet) {
|
||||
await updateTableApiAccess(table, privilegesToSet)
|
||||
}
|
||||
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
|
||||
? apiAccessToggleHandler.data.privileges
|
||||
: undefined
|
||||
if (privilegesToSet) {
|
||||
await updateTableApiAccess(table, privilegesToSet)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
@@ -808,13 +797,11 @@ export const SidePanelEditor = ({
|
||||
}
|
||||
if (isTableLike(table)) {
|
||||
await updateTableRealtime(table, isRealtimeEnabled)
|
||||
if (isApiGrantTogglesEnabled) {
|
||||
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
|
||||
? apiAccessToggleHandler.data.privileges
|
||||
: undefined
|
||||
if (privilegesToSet) {
|
||||
await updateTableApiAccess(table, privilegesToSet)
|
||||
}
|
||||
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
|
||||
? apiAccessToggleHandler.data.privileges
|
||||
: undefined
|
||||
if (privilegesToSet) {
|
||||
await updateTableApiAccess(table, privilegesToSet)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-19
@@ -32,7 +32,6 @@ import { useForeignKeyConstraintsQuery } from '@/data/database/foreign-key-const
|
||||
import { useEnumeratedTypesQuery } from '@/data/enumerated-types/enumerated-types-query'
|
||||
import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
|
||||
import { useChanged } from '@/hooks/misc/useChanged'
|
||||
import { useDataApiGrantTogglesEnabled } from '@/hooks/misc/useDataApiGrantTogglesEnabled'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
@@ -79,8 +78,6 @@ export const TableEditor = ({
|
||||
const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all'])
|
||||
const { docsRowLevelSecurityGuidePath } = useCustomContent(['docs:row_level_security_guide_path'])
|
||||
|
||||
const isApiGrantTogglesEnabled = useDataApiGrantTogglesEnabled()
|
||||
|
||||
const [params, setParams] = useUrlState()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { selectedSchema } = useQuerySchemaState()
|
||||
@@ -528,22 +525,18 @@ export const TableEditor = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{isApiGrantTogglesEnabled && (
|
||||
<>
|
||||
<SidePanel.Separator />
|
||||
<SidePanel.Content className="py-6 space-y-6">
|
||||
<ApiAccessToggle
|
||||
projectRef={project?.ref}
|
||||
schemaName={isNewRecord ? selectedSchema : table?.schema}
|
||||
tableName={
|
||||
isNewRecord || isDuplicating ? tableFields.name : tableFields.name || table?.name
|
||||
}
|
||||
isNewRecord={isNewRecord || isDuplicating}
|
||||
handler={apiAccessToggleHandler}
|
||||
/>
|
||||
</SidePanel.Content>
|
||||
</>
|
||||
)}
|
||||
<SidePanel.Separator />
|
||||
<SidePanel.Content className="py-6 space-y-6">
|
||||
<ApiAccessToggle
|
||||
projectRef={project?.ref}
|
||||
schemaName={isNewRecord ? selectedSchema : table?.schema}
|
||||
tableName={
|
||||
isNewRecord || isDuplicating ? tableFields.name : tableFields.name || table?.name
|
||||
}
|
||||
isNewRecord={isNewRecord || isDuplicating}
|
||||
handler={apiAccessToggleHandler}
|
||||
/>
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { usePHFlag } from '../ui/useFlag'
|
||||
import { IS_TEST_ENV } from '@/lib/constants'
|
||||
|
||||
/**
|
||||
* Determine whether a user has access to Data API grant toggles.
|
||||
*
|
||||
* Requires that the ConfigCat flag for Data API badges and the PostHog flag
|
||||
* for Table Editor API access are both enabled.
|
||||
*
|
||||
* In test environments, this returns true to allow E2E testing of the feature
|
||||
* without requiring the feature flag infrastructure.
|
||||
*/
|
||||
export const useDataApiGrantTogglesEnabled = (): boolean => {
|
||||
const isTableEditorApiAccessEnabled = usePHFlag<boolean>('tableEditorApiAccessToggle')
|
||||
|
||||
// In test environment, enable the feature for E2E testing
|
||||
if (IS_TEST_ENV) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !!isTableEditorApiAccessEnabled
|
||||
}
|
||||
@@ -5,13 +5,10 @@ import { IS_TEST_ENV } from '@/lib/constants'
|
||||
import { useTrack } from '@/lib/telemetry/track'
|
||||
|
||||
/**
|
||||
* Controls the default state of the "Default privileges for new entities"
|
||||
* Controls the default state of the "Automatically expose new tables and functions"
|
||||
* checkbox at project creation. When the flag is on, the checkbox defaults
|
||||
* to unchecked (i.e. revoke SQL runs). When off/absent, the checkbox defaults
|
||||
* to checked (current behaviour — default grants remain).
|
||||
*
|
||||
* Scoped to project-creation only. The existing `tableEditorApiAccessToggle`
|
||||
* flag continues to gate the integrations → Data API settings surface.
|
||||
*/
|
||||
export const useDataApiRevokeOnCreateDefaultEnabled = (): boolean => {
|
||||
const flag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
|
||||
|
||||
@@ -104,7 +104,6 @@ const Wizard: NextPageWithLayout = () => {
|
||||
// Read the raw flag for telemetry — coerce-undefined-to-false would record false for
|
||||
// users whose flags haven't loaded yet. The raw value preserves undefined (omitted from
|
||||
// PostHog) so we only record true/false when the flag is resolved.
|
||||
const tableEditorApiAccessToggleFlag = usePHFlag<boolean>('tableEditorApiAccessToggle')
|
||||
const dataApiRevokeOnCreateDefaultFlag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
|
||||
const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled()
|
||||
|
||||
@@ -274,9 +273,6 @@ const Wizard: NextPageWithLayout = () => {
|
||||
dataApiEnabled: form.getValues('dataApi'),
|
||||
dataApiDefaultPrivilegesGranted: form.getValues('dataApiDefaultPrivileges'),
|
||||
useOrioleDb: form.getValues('useOrioleDb'),
|
||||
...(tableEditorApiAccessToggleFlag !== undefined && {
|
||||
tableEditorApiAccessToggleEnabled: tableEditorApiAccessToggleFlag,
|
||||
}),
|
||||
...(dataApiRevokeOnCreateDefaultFlag !== undefined && {
|
||||
dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag,
|
||||
}),
|
||||
|
||||
@@ -359,14 +359,6 @@ export interface ProjectCreationSimpleVersionSubmittedEvent {
|
||||
* false = "Postgres" (default)
|
||||
*/
|
||||
useOrioleDb?: boolean
|
||||
/**
|
||||
* Whether the tableEditorApiAccessToggle PostHog flag was enabled for this user.
|
||||
* Gates the integrations → Data API settings surface only; no longer controls
|
||||
* project-creation revoke behaviour (see dataApiRevokeOnCreateDefaultEnabled).
|
||||
* true/false = flag state when project was created
|
||||
* omitted = PostHog flags had not loaded at the time of project creation
|
||||
*/
|
||||
tableEditorApiAccessToggleEnabled?: boolean
|
||||
/**
|
||||
* Raw checkbox state for "Automatically expose new tables and functions" at submission.
|
||||
* true = default privileges are granted on new entities (current behaviour)
|
||||
|
||||
Reference in New Issue
Block a user