From 75e08577c17b9afab9ccb2e1765f645a87635769 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 22 Apr 2026 21:37:48 +0800 Subject: [PATCH] chore(studio): remove tableEditorApiAccessToggle flag (#45081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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`) ## 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. Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../studio-telemetry.instructions.md | 7 +- .../Settings/API/PostgrestConfig.tsx | 402 ++++++------------ .../SidePanelEditor/SidePanelEditor.tsx | 45 +- .../TableEditor/TableEditor.tsx | 31 +- .../misc/useDataApiGrantTogglesEnabled.ts | 22 - .../misc/useDataApiRevokeOnCreateDefault.ts | 5 +- apps/studio/pages/new/[slug].tsx | 4 - packages/common/telemetry-constants.ts | 8 - 8 files changed, 174 insertions(+), 350 deletions(-) delete mode 100644 apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts diff --git a/.github/instructions/studio-telemetry.instructions.md b/.github/instructions/studio-telemetry.instructions.md index 3892a6fde6..0a9897dce6 100644 --- a/.github/instructions/studio-telemetry.instructions.md +++ b/.github/instructions/studio-telemetry.instructions.md @@ -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' }) ``` diff --git a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx index 7d9e066eb3..c64915ea53 100644 --- a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx +++ b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx @@ -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 = () => { ) : ( <> - {isApiGrantTogglesEnabled ? ( - - - { - 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, - }) - } - }} - /> - + + + { + 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, + }) + } + }} + /> + - - { - 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, - }) - } - }} - /> - + + { + 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, + }) + } + }} + /> + - - { - 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, - }) - } - }} - /> - + + { + 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, + }) + } + }} + /> + - {watchedDbSchema.includes('public') && ( - ( - - - -
- -
-
-
-
- )} - /> - )} - - {watchedDbSchema.length === 0 && ( - - )} -
- ) : ( - + {watchedDbSchema.includes('public') && ( ( - {isLoadingSchemas ? ( -
- -
- ) : ( - - +
+ - - - {schemas.length <= 0 ? ( - - no - - ) : ( - schemas.map((x) => ( - - {x.name} - - )) - )} - - - - )} +
+
- {!field.value.includes('public') && field.value.length > 0 && ( - -

- You will not be able to query tables and views in the{' '} - public schema via - supabase-js or HTTP clients. -

- {isGraphqlExtensionEnabled && ( - <> -

- Tables in the{' '} - public schema - are still exposed over our GraphQL endpoints. -

- - - )} - - } - /> - )}
)} /> -
- )} + )} + + {watchedDbSchema.length === 0 && ( + + )} +
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) } } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 5b86f6a818..4559ad46ad 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -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 && ( - <> - - - - - - )} + + + + ) } diff --git a/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts b/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts deleted file mode 100644 index fdd1f92e80..0000000000 --- a/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts +++ /dev/null @@ -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('tableEditorApiAccessToggle') - - // In test environment, enable the feature for E2E testing - if (IS_TEST_ENV) { - return true - } - - return !!isTableEditorApiAccessEnabled -} diff --git a/apps/studio/hooks/misc/useDataApiRevokeOnCreateDefault.ts b/apps/studio/hooks/misc/useDataApiRevokeOnCreateDefault.ts index 4aba44e35a..29f589ee6d 100644 --- a/apps/studio/hooks/misc/useDataApiRevokeOnCreateDefault.ts +++ b/apps/studio/hooks/misc/useDataApiRevokeOnCreateDefault.ts @@ -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('dataApiRevokeOnCreateDefault') diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 84f3e29a42..de4105f407 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -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('tableEditorApiAccessToggle') const dataApiRevokeOnCreateDefaultFlag = usePHFlag('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, }), diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index aac1e034d9..889907864a 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -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)