From 5a3d12edecfa7356546aab35e03361c4a14a7611 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:42:25 +0100 Subject: [PATCH 1/9] docs: update axiom docs (#42983) --- apps/docs/content/guides/telemetry/log-drains.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/telemetry/log-drains.mdx index f05a4e6f73f5d..525102a8df93f 100644 --- a/apps/docs/content/guides/telemetry/log-drains.mdx +++ b/apps/docs/content/guides/telemetry/log-drains.mdx @@ -220,12 +220,12 @@ with timestamp modified to be parsed by ingestion endpoint. To set up the Axiom log drain, you have to: -1. Create a dataset for ingestion in Axiom dashboard -> Datasets +1. Create a dataset for ingestion in Axiom Console -> Datasets 2. Generate an Axiom API Token with permission to ingest into the created dataset (see [Axiom docs](https://axiom.co/docs/reference/tokens#create-basic-api-token)) 3. Create log drain in [Supabase dashboard](/dashboard/project/_/settings/log-drains), providing: - Name of the dataset - API token -4. Watch for events in the Stream panel of Axiom dashboard +4. Watch for events in the Stream panel of Axiom Console ## Amazon S3 From e125ad7ff6f3c3888b5fc12574a39830b9f882a9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 19 Feb 2026 13:46:25 +0800 Subject: [PATCH 2/9] Chore/deprecate use query state with select part 03 (#42734) ## Context Related to FE-2461 More refactoring to clean up usage of `useQueryStateWithSelect`, mainly in the database pages - Roles - Triggers - Functions - Enumerated Types ## To test In each of those pages, verify that - [ ] Clicking the "new" cta updates the URL params, and refreshing should re-open the sheet - [ ] Editing an existing item should update URL params, and refreshing should re-open the sheet with the right item - Verify that if the URL param has the wrong id, page should not open the sheet, show a toast, and reset the URL param - [ ] Deleting an existing item should update URL params, and refreshing should re-open the sheet with the right item - Verify that if the URL param has the wrong id, page should not open the sheet, show a toast, and reset the URL param --------- Co-authored-by: Ali Waseem --- .../DeleteEnumeratedTypeModal.tsx | 75 -------- .../EnumeratedTypes/EnumeratedTypes.tsx | 110 +++++++---- .../Database/Functions/DeleteFunction.tsx | 59 ------ .../Functions/FunctionsList/FunctionsList.tsx | 178 +++++++++--------- .../Database/Roles/DeleteRoleModal.tsx | 54 ------ .../interfaces/Database/Roles/RolesList.tsx | 96 ++++++---- .../Triggers/TriggersList/TriggersList.tsx | 138 +++++++------- apps/studio/components/ui/SparkBar.tsx | 2 +- .../project/[ref]/database/functions.tsx | 5 +- e2e/studio/features/database.spec.ts | 12 +- 10 files changed, 316 insertions(+), 413 deletions(-) delete mode 100644 apps/studio/components/interfaces/Database/EnumeratedTypes/DeleteEnumeratedTypeModal.tsx delete mode 100644 apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx delete mode 100644 apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx diff --git a/apps/studio/components/interfaces/Database/EnumeratedTypes/DeleteEnumeratedTypeModal.tsx b/apps/studio/components/interfaces/Database/EnumeratedTypes/DeleteEnumeratedTypeModal.tsx deleted file mode 100644 index 37e3b1ab71de0..0000000000000 --- a/apps/studio/components/interfaces/Database/EnumeratedTypes/DeleteEnumeratedTypeModal.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { toast } from 'sonner' - -import { useEnumeratedTypeDeleteMutation } from 'data/enumerated-types/enumerated-type-delete-mutation' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' - -interface DeleteEnumeratedTypeModalProps { - visible: boolean - selectedEnumeratedType?: any - onClose: () => void - onDelete?: () => void -} - -const DeleteEnumeratedTypeModal = ({ - visible, - selectedEnumeratedType, - onClose, - onDelete, -}: DeleteEnumeratedTypeModalProps) => { - const { data: project } = useSelectedProjectQuery() - const { mutate: deleteEnumeratedType, isPending: isDeleting } = useEnumeratedTypeDeleteMutation({ - onSuccess: () => { - toast.success(`Successfully deleted "${selectedEnumeratedType.name}"`) - onClose() - }, - }) - - const onConfirmDeleteType = () => { - if (selectedEnumeratedType === undefined) return console.error('No enumerated type selected') - if (project?.ref === undefined) return console.error('Project ref required') - if (project?.connectionString === undefined) - return console.error('Project connectionString required') - - onDelete?.() - deleteEnumeratedType({ - projectRef: project?.ref, - connectionString: project?.connectionString, - name: selectedEnumeratedType.name, - schema: selectedEnumeratedType.schema, - }) - } - - return ( - - Confirm to delete enumerated type{' '} - {selectedEnumeratedType?.name} - - } - confirmLabel="Confirm delete" - confirmLabelLoading="Deleting..." - onCancel={onClose} - onConfirm={() => onConfirmDeleteType()} - alert={{ - title: 'This action cannot be undone', - description: - 'You will need to re-create the enumerated type if you want to revert the deletion.', - }} - > -

Before deleting this enumerated type, consider:

-
    -
  • - This enumerated type is no longer in use in any tables or functions -
  • -
-
- ) -} - -export default DeleteEnumeratedTypeModal diff --git a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx index 6c4243e6e7569..4b9acd56602e3 100644 --- a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx +++ b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx @@ -1,16 +1,14 @@ -import { Edit, MoreVertical, Search, Trash } from 'lucide-react' -import { parseAsBoolean, useQueryState } from 'nuqs' -import { useRef, useState } from 'react' -import { toast } from 'sonner' - import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import SchemaSelector from 'components/ui/SchemaSelector' import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query' -import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' +import { Edit, MoreVertical, Search, Trash } from 'lucide-react' +import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' import { Button, Card, @@ -18,7 +16,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - Input, Table, TableBody, TableCell, @@ -26,20 +23,22 @@ import { TableHeader, TableRow, } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' import CreateEnumeratedTypeSidePanel from './CreateEnumeratedTypeSidePanel' -import DeleteEnumeratedTypeModal from './DeleteEnumeratedTypeModal' import EditEnumeratedTypeSidePanel from './EditEnumeratedTypeSidePanel' +import { useEnumeratedTypeDeleteMutation } from '@/data/enumerated-types/enumerated-type-delete-mutation' export const EnumeratedTypes = () => { const { data: project } = useSelectedProjectQuery() const [search, setSearch] = useState('') const { selectedSchema, setSelectedSchema } = useQuerySchemaState() - const deletingTypeIdRef = useRef(null) const { - data, + data = [], error, isPending: isLoading, isError, @@ -49,25 +48,27 @@ export const EnumeratedTypes = () => { connectionString: project?.connectionString, }) + const { + mutate: deleteEnumeratedType, + isPending: isDeleting, + isSuccess: isSuccessDelete, + } = useEnumeratedTypeDeleteMutation({ + onSuccess: (_, vars) => { + toast.success(`Successfully deleted type "${vars.name}"`) + setSelectedTypeIdToDelete(null) + }, + }) + const [showCreateTypePanel, setShowCreateTypePanel] = useQueryState( 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) + parseAsBoolean.withDefault(false) ) - const { value: typeToEdit, setValue: setSelectedTypeIdToEdit } = useQueryStateWithSelect({ - urlKey: 'edit', - select: (id) => (id ? data?.find((type) => type.id.toString() === id) : undefined), - enabled: !!data, - onError: () => toast.error(`Enumerated Type not found`), - }) + const [selectedTypeIdToEdit, setSelectedTypeIdToEdit] = useQueryState('edit', parseAsString) + const typeToEdit = data?.find((type) => type.id.toString() === selectedTypeIdToEdit) - const { value: typeToDelete, setValue: setSelectedTypeIdToDelete } = useQueryStateWithSelect({ - urlKey: 'delete', - select: (id) => (id ? data?.find((type) => type.id.toString() === id) : undefined), - enabled: !!data, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingTypeIdRef, selectedId, `Enumerated Type not found`), - }) + const [selectedTypeIdToDelete, setSelectedTypeIdToDelete] = useQueryState('delete', parseAsString) + const typeToDelete = data?.find((type) => type.id.toString() === selectedTypeIdToDelete) const enumeratedTypes = (data ?? []).filter((type) => type.enums.length > 0) const filteredEnumeratedTypes = @@ -79,6 +80,34 @@ export const EnumeratedTypes = () => { const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) + const onConfirmDeleteType = () => { + if (typeToDelete === undefined) return console.error('No enumerated type selected') + if (project?.ref === undefined) return console.error('Project ref required') + if (project?.connectionString === undefined) + return console.error('Project connectionString required') + + deleteEnumeratedType({ + projectRef: project?.ref, + connectionString: project?.connectionString, + name: typeToDelete.name, + schema: typeToDelete.schema, + }) + } + + useEffect(() => { + if (isSuccess && !!selectedTypeIdToEdit && !typeToEdit) { + toast('Type cannot be found') + setSelectedTypeIdToEdit(null) + } + }, [isSuccess, selectedTypeIdToEdit, setSelectedTypeIdToEdit, typeToEdit]) + + useEffect(() => { + if (isSuccess && !!selectedTypeIdToDelete && !typeToDelete && !isSuccessDelete) { + toast('Type cannot be found') + setSelectedTypeIdToDelete(null) + } + }, [isSuccess, selectedTypeIdToDelete, setSelectedTypeIdToDelete, typeToDelete, isSuccessDelete]) + return (
@@ -212,16 +241,33 @@ export const EnumeratedTypes = () => { onClose={() => setSelectedTypeIdToEdit(null)} /> - setSelectedTypeIdToDelete(null)} - onDelete={() => { - if (typeToDelete) { - deletingTypeIdRef.current = typeToDelete.id.toString() - } + title={ + <> + Confirm to delete enumerated type {typeToDelete?.name} + + } + confirmLabel="Confirm delete" + confirmLabelLoading="Deleting..." + onCancel={() => setSelectedTypeIdToDelete(null)} + onConfirm={() => onConfirmDeleteType()} + alert={{ + title: 'This action cannot be undone', + description: + 'You will need to re-create the enumerated type if you want to revert the deletion.', }} - /> + > +

Before deleting this enumerated type, consider:

+
    +
  • + This enumerated type is no longer in use in any tables or functions +
  • +
+
) } diff --git a/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx b/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx deleted file mode 100644 index fcb4c53f8de59..0000000000000 --- a/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { TextConfirmModal } from 'components/ui/TextConfirmModalWrapper' -import type { DatabaseFunction } from 'data/database-functions/database-functions-query' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' - -interface DeleteFunctionProps { - func?: DatabaseFunction - visible: boolean - setVisible: (value: string | null) => void - onDelete: (params: { - func: DatabaseFunction - projectRef: string - connectionString?: string | null - }) => void - isLoading: boolean -} - -export const DeleteFunction = ({ - func, - visible, - setVisible, - onDelete, - isLoading, -}: DeleteFunctionProps) => { - const { data: project } = useSelectedProjectQuery() - const { name, schema } = func ?? {} - - async function handleDelete() { - if (!func) return console.error('Function is required') - if (!project) return console.error('Project is required') - - onDelete({ - func, - projectRef: project.ref, - connectionString: project.connectionString, - }) - } - - return ( - setVisible(null)} - onConfirm={handleDelete} - title="Delete this function" - loading={isLoading} - confirmLabel={`Delete function ${name}`} - confirmPlaceholder="Type in name of function" - confirmString={name ?? 'Unknown'} - text={ - <> - This will delete the function{' '} - {name} from the schema{' '} - {schema} - - } - alert={{ title: 'You cannot recover this function once deleted.' }} - /> - ) -} diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 57c7a40814924..236dcd7bb2543 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -1,11 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Search } from 'lucide-react' -import { useRouter } from 'next/router' -import { parseAsBoolean, parseAsJson, useQueryState } from 'nuqs' -import { useRef } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' +import { useIsInlineEditorEnabled } from 'components/interfaces/Account/Preferences/InlineEditorSettings' +import { CreateFunction } from 'components/interfaces/Database/Functions/CreateFunction' import { ReportsSelectFilter, selectFilterSchema, @@ -20,30 +16,24 @@ import type { DatabaseFunction } from 'data/database-functions/database-function import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' +import { Search } from 'lucide-react' +import { useRouter } from 'next/router' +import { parseAsBoolean, parseAsJson, parseAsString, useQueryState } from 'nuqs' +import { useEffect } from 'react' +import { toast } from 'sonner' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' -import { - AiIconAnimation, - Card, - Input, - Table, - TableBody, - TableHead, - TableHeader, - TableRow, -} from 'ui' +import { AiIconAnimation, Card, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning' import FunctionList from './FunctionList' - -import { useIsInlineEditorEnabled } from 'components/interfaces/Account/Preferences/InlineEditorSettings' -import { CreateFunction } from 'components/interfaces/Database/Functions/CreateFunction' -import { DeleteFunction } from 'components/interfaces/Database/Functions/DeleteFunction' -import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' +import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' const createFunctionSnippet = `create function function_name() returns void @@ -54,7 +44,7 @@ begin end; $$;` -const FunctionsList = () => { +export const FunctionsList = () => { const router = useRouter() const { search } = useParams() const { data: project } = useSelectedProjectQuery() @@ -68,9 +58,6 @@ const FunctionsList = () => { setInitialPrompt: setEditorPanelInitialPrompt, } = useEditorPanelStateSnapshot() - // Track the ID being deleted to exclude it from error checking - const deletingFunctionIdRef = useRef(null) - const createFunction = () => { setSelectedFunctionIdToDuplicate(null) if (isInlineEditorEnabled) { @@ -109,10 +96,6 @@ const FunctionsList = () => { } } - const deleteFunction = (fn: DatabaseFunction) => { - setSelectedFunctionToDelete(fn.id.toString()) - } - const filterString = search ?? '' // Filters @@ -149,17 +132,18 @@ const FunctionsList = () => { }) const { - data: functions, + data: functions = [], error, isPending: isLoading, isError, + isSuccess, } = useDatabaseFunctionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) // Get unique return types from functions in the selected schema - const schemaFunctions = (functions ?? []).filter((fn) => fn.schema === selectedSchema) + const schemaFunctions = functions.filter((fn) => fn.schema === selectedSchema) const uniqueReturnTypes = Array.from(new Set(schemaFunctions.map((fn) => fn.return_type))).sort() // Get security options based on what exists in the selected schema @@ -175,44 +159,66 @@ const FunctionsList = () => { parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) ) - const { setValue: setSelectedFunctionToEdit, value: functionToEdit } = useQueryStateWithSelect({ - urlKey: 'edit', - select: (id: string) => (id ? functions?.find((fn) => fn.id.toString() === id) : undefined), - enabled: !!functions, - onError: () => toast.error(`Function not found`), + const [functionIdToEdit, setSelectedFunctionToEdit] = useQueryState('edit', parseAsString) + const functionToEdit = functions.find((fn) => fn.id.toString() === functionIdToEdit) + + const [functionIdToDuplicate, setSelectedFunctionIdToDuplicate] = useQueryState( + 'duplicate', + parseAsString + ) + const functionToDuplicate = functions.find((fn) => fn.id.toString() === functionIdToDuplicate) + + const [functionIdToDelete, setSelectedFunctionToDelete] = useQueryState('delete', parseAsString) + const functionToDelete = functions.find((fn) => fn.id.toString() === functionIdToDelete) + + const { + mutate: deleteDatabaseFunction, + isPending: isDeletingFunction, + isSuccess: isSuccessDelete, + } = useDatabaseFunctionDeleteMutation({ + onSuccess: (_, variables) => { + toast.success(`Successfully removed function ${variables.func.name}`) + setSelectedFunctionToDelete(null) + }, }) - const { setValue: setSelectedFunctionIdToDuplicate, value: functionToDuplicate } = - useQueryStateWithSelect({ - urlKey: 'duplicate', - select: (id: string) => { - if (!id) return undefined - const original = functions?.find((fn) => fn.id.toString() === id) - return original ? { ...original, name: `${original.name}_duplicate` } : undefined - }, - enabled: !!functions, - onError: () => toast.error(`Function not found`), - }) + const onDeleteFunction = () => { + if (!project) return console.error('Project is required') + if (!functionToDelete) return console.error('Function is required') - const { setValue: setSelectedFunctionToDelete, value: functionToDelete } = - useQueryStateWithSelect({ - urlKey: 'delete', - select: (id: string) => (id ? functions?.find((fn) => fn.id.toString() === id) : undefined), - enabled: !!functions, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingFunctionIdRef, selectedId, `Function not found`), + deleteDatabaseFunction({ + func: functionToDelete, + projectRef: project.ref, + connectionString: project.connectionString, }) + } - const { mutate: deleteDatabaseFunction, isPending: isDeletingFunction } = - useDatabaseFunctionDeleteMutation({ - onSuccess: (_, variables) => { - toast.success(`Successfully removed function ${variables.func.name}`) - setSelectedFunctionToDelete(null) - }, - onError: () => { - deletingFunctionIdRef.current = null - }, - }) + useEffect(() => { + if (isSuccess && !!functionIdToEdit && !functionToEdit) { + toast('Function not found') + setSelectedFunctionToEdit(null) + } + }, [functionIdToEdit, functionToEdit, isSuccess, setSelectedFunctionToEdit]) + + useEffect(() => { + if (isSuccess && !!functionIdToDuplicate && !functionToDuplicate) { + toast('Function not found') + setSelectedFunctionIdToDuplicate(null) + } + }, [functionIdToDuplicate, functionToDuplicate, isSuccess, setSelectedFunctionIdToDuplicate]) + + useEffect(() => { + if (isSuccess && !!functionIdToDelete && !functionToDelete && !isSuccessDelete) { + toast('Function not found') + setSelectedFunctionToDelete(null) + } + }, [ + functionIdToDelete, + functionToDelete, + isSuccess, + isSuccessDelete, + setSelectedFunctionToDelete, + ]) if (isLoading) return if (isError) return @@ -351,7 +357,7 @@ const FunctionsList = () => { securityFilter={securityFilter ?? []} duplicateFunction={duplicateFunction} editFunction={editFunction} - deleteFunction={deleteFunction} + deleteFunction={(fn) => setSelectedFunctionToDelete(fn.id.toString())} functions={functions ?? []} /> @@ -360,37 +366,37 @@ const FunctionsList = () => {
)} - {/* Create Function */} - { - setShowCreateFunctionForm(false) - }} - /> - - {/* Edit or Duplicate Function */} { + setShowCreateFunctionForm(false) setSelectedFunctionToEdit(null) setSelectedFunctionIdToDuplicate(null) }} isDuplicating={!!functionToDuplicate} /> - [0]) => { - deletingFunctionIdRef.current = params.func.id.toString() - deleteDatabaseFunction(params) - }} - isLoading={isDeletingFunction} + onCancel={() => setSelectedFunctionToDelete(null)} + onConfirm={onDeleteFunction} + title="Delete this function" + loading={isDeletingFunction} + confirmLabel={`Delete function ${functionToDelete?.name}`} + confirmPlaceholder="Type in name of function" + confirmString={functionToDelete?.name ?? 'Unknown'} + text={ + <> + This will delete the function{' '} + {functionToDelete?.name}{' '} + from the schema{' '} + {functionToDelete?.schema} + + } + alert={{ title: 'You cannot recover this function once deleted.' }} /> ) } - -export default FunctionsList diff --git a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx deleted file mode 100644 index 4bc9d70584d18..0000000000000 --- a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { PostgresRole } from '@supabase/postgres-meta' -import { toast } from 'sonner' - -import { useDatabaseRoleDeleteMutation } from 'data/database-roles/database-role-delete-mutation' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Modal } from 'ui' - -interface DeleteRoleModalProps { - role: PostgresRole - visible: boolean - onClose: () => void - onDelete?: () => void -} - -export const DeleteRoleModal = ({ role, visible, onClose, onDelete }: DeleteRoleModalProps) => { - const { data: project } = useSelectedProjectQuery() - - const { mutate: deleteDatabaseRole, isPending: isDeleting } = useDatabaseRoleDeleteMutation({ - onSuccess: () => { - toast.success(`Successfully deleted role: ${role.name}`) - onClose() - }, - }) - - const deleteRole = async () => { - if (!project) return console.error('Project is required') - if (!role) return console.error('Failed to delete role: role is missing') - onDelete?.() - deleteDatabaseRole({ - projectRef: project.ref, - connectionString: project.connectionString, - id: role.id, - }) - } - - return ( - Confirm to delete role "{role?.name}"} - loading={isDeleting} - > - -

- This will automatically revoke any membership of this role in other roles, and this action - cannot be undone. -

-
-
- ) -} diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index c8cc91ad8a86c..70748b7adea2f 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -1,24 +1,25 @@ -import type { PostgresRole } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { NoSearchResults } from 'components/ui/NoSearchResults' -import SparkBar from 'components/ui/SparkBar' +import { SparkBar } from 'components/ui/SparkBar' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { partition, sortBy } from 'lodash' import { Plus, Search, X } from 'lucide-react' -import { parseAsBoolean, useQueryState } from 'nuqs' -import { useRef, useState } from 'react' -import { Badge, Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' +import { Badge, Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { CreateRolePanel } from './CreateRolePanel' -import { DeleteRoleModal } from './DeleteRoleModal' import { RoleRow } from './RoleRow' import { RoleRowSkeleton } from './RoleRowSkeleton' import { SUPABASE_ROLES } from './Roles.constants' +import { useDatabaseRoleDeleteMutation } from '@/data/database-roles/database-role-delete-mutation' type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number] @@ -27,7 +28,6 @@ export const RolesList = () => { const [filterString, setFilterString] = useState('') const [filterType, setFilterType] = useState<'all' | 'active'>('all') - const deletingRoleIdRef = useRef(null) const { can: canUpdateRoles } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, @@ -40,25 +40,34 @@ export const RolesList = () => { }) const maxConnectionLimit = maxConnData?.maxConnections - const { data, isPending: isLoading } = useDatabaseRolesQuery({ + const { + data, + isPending: isLoading, + isSuccess, + } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) + const { + mutate: deleteDatabaseRole, + isPending: isDeleting, + isSuccess: isSuccessDelete, + } = useDatabaseRoleDeleteMutation({ + onSuccess: () => { + toast.success(`Successfully deleted role`) + setSelectedRoleIdToDelete(null) + }, + }) + const [isCreatingRole, setIsCreatingRole] = useQueryState( 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) + parseAsBoolean.withDefault(false) ) - const { setValue: setSelectedRoleIdToDelete, value: roleToDelete } = useQueryStateWithSelect({ - urlKey: 'delete', - select: (id: string) => (id ? data?.find((role) => role.id.toString() === id) : undefined), - enabled: !!data, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingRoleIdRef, selectedId, `Database Role not found`), - }) - + const [selectedRoleIdToDelete, setSelectedRoleIdToDelete] = useQueryState('delete', parseAsString) const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase()) + const roleToDelete = roles?.find((role) => role.id.toString() === selectedRoleIdToDelete) const filteredRoles = ( filterType === 'active' ? roles.filter((role) => role.activeConnections > 0) : roles @@ -76,6 +85,23 @@ export const RolesList = () => { (r) => -r.activeConnections ) + const deleteRole = async () => { + if (!project) return console.error('Project is required') + if (!roleToDelete) return console.error('Failed to delete role: role is missing') + deleteDatabaseRole({ + projectRef: project.ref, + connectionString: project.connectionString, + id: roleToDelete.id, + }) + } + + useEffect(() => { + if (isSuccess && !!selectedRoleIdToDelete && !roleToDelete && !isSuccessDelete) { + toast('Role cannot be found') + setSelectedRoleIdToDelete(null) + } + }, [isSuccess, selectedRoleIdToDelete, roleToDelete, isSuccessDelete, setSelectedRoleIdToDelete]) + return ( <>
@@ -86,7 +112,7 @@ export const RolesList = () => { placeholder="Search for a role" icon={} value={filterString} - onChange={(event: any) => setFilterString(event.target.value)} + onChange={(event) => setFilterString(event.target.value)} actions={ filterString && (
@@ -297,7 +300,7 @@ export const SchemaGraph = () => { )} {isSuccessTables && ( <> - {tables.length === 0 ? ( + {hasNoTables ? (
Date: Thu, 19 Feb 2026 15:22:07 +0800 Subject: [PATCH 4/9] Final clean up for useQueryStateWithSelect (#43008) ## Context Final clean up of `useQueryStateWithSelect` - removes the hook --- .../hooks/misc/useQueryStateWithSelect.ts | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 apps/studio/hooks/misc/useQueryStateWithSelect.ts diff --git a/apps/studio/hooks/misc/useQueryStateWithSelect.ts b/apps/studio/hooks/misc/useQueryStateWithSelect.ts deleted file mode 100644 index 31cbe6fa6bd8c..0000000000000 --- a/apps/studio/hooks/misc/useQueryStateWithSelect.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { parseAsString, useQueryState } from 'nuqs' -import { MutableRefObject, useEffect, useMemo } from 'react' -import { toast } from 'sonner' - -/** - * Hook for managing URL query parameters with a custom select function and error handling. - * - * @param enabled - Whether error handling is active (shows error when selectedId exists but select returns undefined) - * @param urlKey - The query parameter key (e.g., 'edit', 'delete') - * @param select - Function to transform the selected ID into the desired value (returns undefined if not found) - * @param onError - Callback invoked when enabled is true and selectedId exists but select returns undefined - * - * @returns Object with: - * - value: The result of select(selectedId) or undefined - * - setValue: Function to set/clear the selected ID in the URL - * - * @deprecated Avoid using this hook, and use nuqs directly - * Refer to this PR for more information: https://github.com/supabase/supabase/pull/41380 - * as well as context on how to refactor to remove usage of this hook - */ -export function useQueryStateWithSelect({ - enabled, - urlKey, - select, - onError, -}: { - enabled: boolean - urlKey: string - select: (id: string) => T | undefined - onError: (error: Error, selectedId: string) => void -}) { - const [selectedId, setSelectedId] = useQueryState( - urlKey, - parseAsString.withOptions({ history: 'push', clearOnDefault: true }) - ) - - const value = useMemo(() => (selectedId ? select(selectedId) : undefined), [selectedId, select]) - - useEffect(() => { - if (enabled && selectedId && !value) { - onError(new Error(`not found`), selectedId) - setSelectedId(null) - } - }, [enabled, onError, selectedId, setSelectedId, value]) - - return { - value, - setValue: setSelectedId as (value: string | null) => void, - } -} - -export const handleErrorOnDelete = ( - deletingIdRef: MutableRefObject, - selectedId: string, - errorMessage: string -) => { - if (selectedId !== deletingIdRef.current) { - toast.error(errorMessage) - } else { - deletingIdRef.current = null - } -} From faba8296841bca6eaeee2868915df37dd62bf1d1 Mon Sep 17 00:00:00 2001 From: Yogeshwaran C <84272111+yogeshwaran-c@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:28:46 +0530 Subject: [PATCH 5/9] docs: fix broken "Control your costs" link in billing overview (#42906) ## What kind of change does this PR introduce? Documentation fix ## What is the current behavior? In `apps/docs/content/guides/platform/billing-on-supabase.mdx`, the "Control your costs" link has an empty URL (`[Control your costs]()`), making it non-functional. Users clicking this link are not taken to the cost control documentation. ## What is the new behavior? The link now correctly points to `/docs/guides/platform/cost-control`, which is the existing page that covers Spend Cap, Billing Alerts, and other cost control features. ## Additional context The other two links in the same list (`Your monthly invoice` and `Manage your usage`) already have correct URLs. This appears to be an oversight where the cost-control page was created but the link in the billing overview was not updated. --- apps/docs/content/guides/platform/billing-on-supabase.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/billing-on-supabase.mdx b/apps/docs/content/guides/platform/billing-on-supabase.mdx index 868a0f87059d2..c8d502e283d8a 100644 --- a/apps/docs/content/guides/platform/billing-on-supabase.mdx +++ b/apps/docs/content/guides/platform/billing-on-supabase.mdx @@ -39,7 +39,7 @@ Monthly costs for paid plans include a fixed subscription fee based on your chos - [Your monthly invoice](/docs/guides/platform/your-monthly-invoice) - For a detailed breakdown of what a monthly invoice includes - [Manage your usage](/docs/guides/platform/manage-your-usage) - For details on how the different usage items are billed, and how to optimize usage and reduce costs -- [Control your costs]() - For details on how you can control your costs in case unexpected high usage occurs +- [Control your costs](/docs/guides/platform/cost-control) - For details on how you can control your costs in case unexpected high usage occurs ### Compute costs for projects From 3f059636304347a766dfa5c5d04c499796b91843 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 19 Feb 2026 16:02:59 +0800 Subject: [PATCH 6/9] Joshen/fe 2573 table editor user still wants to run the query if it causing (#43004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Related to this previous PR [here](https://github.com/supabase/supabase/pull/42321) Table Editor: Adding a CTA to the `HighQueryCost` UI to allow users to proceed with fetching data despite the high query cost warning, to prevent completely blocking the users from their workflows (realised that certain heavy queries are required and this safeguard shouldn't be creating dead-ends for users) image Clicking "Load more" will open a confirmation dialog, in which proceeding to load the data will thereafter suppress this preflight check for the table, for the rest of the browser session image ## Other changes - Also bumped the query cost threshold from 100,000 to 200,000 - the former might have been too aggressive 😓 - (Unrelated) Added query cost tooltip for cron jobs high query cost warning image --- apps/studio/components/grid/SupabaseGrid.tsx | 2 + .../footer/pagination/Pagination.tsx | 9 +- .../grid/components/grid/GridError.tsx | 9 ++ .../grid/components/header/Header.tsx | 12 +- .../grid/components/header/HeaderNew.tsx | 6 + .../CronJobs/CronJobsTab.CleanupNotice.tsx | 28 ++++- .../Integrations/CronJobs/CronJobsTab.tsx | 9 +- .../CronJobs/CronJobsTab.useCronJobsData.ts | 2 + apps/studio/components/ui/HighQueryCost.tsx | 113 +++++++++++++++--- apps/studio/data/sql/execute-sql-query.ts | 2 +- .../data/table-rows/table-rows-query.ts | 30 ++--- apps/studio/state/table-editor-table.tsx | 9 +- apps/studio/state/table-editor.tsx | 14 ++- 13 files changed, 194 insertions(+), 51 deletions(-) diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 13f492af43cb0..be97e1cefa5c7 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -49,6 +49,7 @@ export const SupabaseGrid = ({ const { data: project } = useSelectedProjectQuery() const tableEditorSnap = useTableEditorStateSnapshot() const snap = useTableEditorTableStateSnapshot() + const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1) const gridRef = useRef(null) const [mounted, setMounted] = useState(false) @@ -81,6 +82,7 @@ export const SupabaseGrid = ({ sorts, filters, page: snap.page, + preflightCheck, limit: tableEditorSnap.rowsPerPage, roleImpersonationState: roleImpersonationState as RoleImpersonationState, }, diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx index 2794e1fe8058c..50d3b92f8b9be 100644 --- a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx +++ b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx @@ -1,7 +1,4 @@ import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/sql/studio/get-count-estimate' -import { ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react' -import { useEffect, useState } from 'react' - import { keepPreviousData } from '@tanstack/react-query' import { useParams } from 'common' import { useTableFilter } from 'components/grid/hooks/useTableFilter' @@ -12,12 +9,15 @@ import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { RoleImpersonationState } from 'lib/role-impersonation' +import { ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react' +import { useEffect, useState } from 'react' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' + import { DropdownControl } from '../../common/DropdownControl' import { formatEstimatedCount } from './Pagination.utils' @@ -106,6 +106,8 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) = const maxPages = Math.ceil(count / tableEditorSnap.rowsPerPage) const totalPages = count > 0 ? maxPages : 1 + const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(id ?? -1) + // [Joshen] This is only applicable for foreign tables, as we use the number of rows on the page to determine // if we've reached the last page (and hence disable the next button) const { data: rowsData, isPending: isLoadingRows } = useTableRowsQuery( @@ -116,6 +118,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) = sorts, filters, page: snap.page, + preflightCheck, limit: tableEditorSnap.rowsPerPage, roleImpersonationState: roleImpersonationState as RoleImpersonationState, }, diff --git a/apps/studio/components/grid/components/grid/GridError.tsx b/apps/studio/components/grid/components/grid/GridError.tsx index 05b855083f46c..a44ca11a8f675 100644 --- a/apps/studio/components/grid/components/grid/GridError.tsx +++ b/apps/studio/components/grid/components/grid/GridError.tsx @@ -11,12 +11,18 @@ import { Admonition } from 'ui-patterns' import { HighCostError } from '@/components/ui/HighQueryCost' import { COST_THRESHOLD_ERROR } from '@/data/sql/execute-sql-query' +import { useTableEditorStateSnapshot } from '@/state/table-editor' import { ResponseError } from '@/types' export const GridError = ({ error }: { error?: ResponseError | null }) => { + const { id: _id } = useParams() + const tableId = _id ? Number(_id) : undefined + const { filters } = useTableFilter() const { sorts } = useTableSort() + const snap = useTableEditorTableStateSnapshot() + const tableEditorSnap = useTableEditorStateSnapshot() if (!error) return null @@ -42,6 +48,9 @@ export const GridError = ({ error }: { error?: ResponseError | null }) => { 'Remove any sorts or filters on unindexed columns, or', 'Create indexes for columns that you want to filter or sort on', ]} + onSelectLoadData={() => { + if (!!tableId) tableEditorSnap.setTableToIgnorePreflightCheck(tableId) + }} /> ) } else if (isForeignTableMissingVaultKeyError) { diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 90721b2c0c12e..14efbcf9d3218 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -31,18 +31,18 @@ import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, + cn, + copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Separator, - cn, - copyToClipboard, } from 'ui' import { ExportDialog } from './ExportDialog' -import { formatRowsForCSV } from './Header.utils' import { FilterPopover } from './filter/FilterPopover' +import { formatRowsForCSV } from './Header.utils' import { SortPopover } from './sort/SortPopover' export type HeaderProps = { @@ -222,6 +222,9 @@ type RowHeaderProps = { } const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { + const { id: _id } = useParams() + const tableId = _id ? Number(_id) : undefined + const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const tableEditorSnap = useTableEditorStateSnapshot() @@ -237,6 +240,8 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { const [isExporting, setIsExporting] = useState(false) const [showExportModal, setShowExportModal] = useState(false) + const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1) + const { data } = useTableRowsQuery( { projectRef: project?.ref, @@ -245,6 +250,7 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { sorts, filters, page: snap.page, + preflightCheck, limit: tableEditorSnap.rowsPerPage, roleImpersonationState: roleImpersonationState as RoleImpersonationState, }, diff --git a/apps/studio/components/grid/components/header/HeaderNew.tsx b/apps/studio/components/grid/components/header/HeaderNew.tsx index d1bd062b213be..3259451354da9 100644 --- a/apps/studio/components/grid/components/header/HeaderNew.tsx +++ b/apps/studio/components/grid/components/header/HeaderNew.tsx @@ -223,6 +223,9 @@ type RowHeaderProps = { } const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { + const { id: _id } = useParams() + const tableId = _id ? Number(_id) : undefined + const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const tableEditorSnap = useTableEditorStateSnapshot() @@ -238,6 +241,8 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { const [isExporting, setIsExporting] = useState(false) const [showExportModal, setShowExportModal] = useState(false) + const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1) + const { data } = useTableRowsQuery( { projectRef: project?.ref, @@ -246,6 +251,7 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { sorts, filters, page: snap.page, + preflightCheck, limit: tableEditorSnap.rowsPerPage, roleImpersonationState: roleImpersonationState as RoleImpersonationState, }, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx index 2f59734a55860..97155317b1a78 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.CleanupNotice.tsx @@ -16,6 +16,9 @@ import { SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { Admonition } from 'ui-patterns/admonition' @@ -24,15 +27,15 @@ import { useCronJobsCleanupActions, type BatchDeletionProgress, } from './CronJobsTab.useCleanupActions' +import { InlineLinkClassName } from '@/components/ui/InlineLink' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' interface CronJobRunDetailsOverflowNoticeV2Props { + queryCost?: number refetchJobs: () => void } -export const CronJobRunDetailsOverflowNoticeV2 = ( - props: CronJobRunDetailsOverflowNoticeV2Props -) => { +export const CronJobRunDetailsOverflowNotice = (props: CronJobRunDetailsOverflowNoticeV2Props) => { return ( { const { data: project } = useSelectedProjectQuery() @@ -75,7 +79,10 @@ const CronJobRunDetailsOverflowDialog = ({ - + event.preventDefault()} + > Last run for cron jobs omitted for overview @@ -89,8 +96,17 @@ const CronJobRunDetailsOverflowDialog = ({

- However, the join was skipped as the estimated query cost exceeds safety thresholds, - likely due to the size of{' '} + However, the join was skipped as the{' '} + + estimated query cost + +

Estimated cost: {queryCost?.toLocaleString()}

+

+ Determined via the EXPLAIN command +

+ + {' '} + exceeds safety thresholds, likely due to the size of{' '} cron.job_run_details table.

diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 53510a42f3bfb..807d37b8a8e0e 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -18,7 +18,7 @@ import { LoadingLine, Sheet, SheetContent } from 'ui' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { formatCronJobColumns } from './CronJobs.utils' -import { CronJobRunDetailsOverflowNoticeV2 } from './CronJobsTab.CleanupNotice' +import { CronJobRunDetailsOverflowNotice } from './CronJobsTab.CleanupNotice' import { CronJobsTabDataGrid } from './CronJobsTab.DataGrid' import { CronJobsTabHeader } from './CronJobsTab.Header' import { useCronJobsData } from './CronJobsTab.useCronJobsData' @@ -171,7 +171,12 @@ export const CronjobsTab = () => { onCreateJob={onOpenCreateJobSheet} /> - {grid.isMinimal && } + {grid.isMinimal && ( + + )} + queryCost?: number isSuccess: boolean isLoading: boolean error: ResponseError | null @@ -130,6 +131,7 @@ export function useCronJobsData({ return { grid: { rows: cronJobs, + queryCost: cronJobsError?.metadata?.cost, error: useMinimalQuery ? cronJobsMinimalError : cronJobsError, isSuccess: useMinimalQuery ? isCronJobsMinimalSuccess : isCronJobsSuccess, isLoading: useMinimalQuery ? isCronJobsMinimalLoading : isCronJobsLoading, diff --git a/apps/studio/components/ui/HighQueryCost.tsx b/apps/studio/components/ui/HighQueryCost.tsx index e6a73ac0ec551..ca266201e1c41 100644 --- a/apps/studio/components/ui/HighQueryCost.tsx +++ b/apps/studio/components/ui/HighQueryCost.tsx @@ -24,17 +24,26 @@ import { ResponseError } from '@/types' interface HighQueryCostErrorProps { error: ResponseError suggestions?: string[] + onSelectLoadData?: () => void } -export const HighCostError = ({ error, suggestions }: HighQueryCostErrorProps) => { - // [Joshen] The CTA could be to use a read replica to query or something? +export const HighCostError = ({ + error, + suggestions, + onSelectLoadData, +}: HighQueryCostErrorProps) => { return ( - +
+ {!!onSelectLoadData && ( + + )} + +
) } @@ -45,9 +54,7 @@ const HighQueryCostDialog = ({ error, suggestions = [] }: HighQueryCostErrorProp return ( - + event.preventDefault()}> @@ -73,19 +80,33 @@ const HighQueryCostDialog = ({ error, suggestions = [] }: HighQueryCostErrorProp

{' '} - is high and could place significant load on the database. + is high and could place significant load on the database with high disk I/O or CPU + usage.

- {suggestions.length > 0 && ( -
-

You may check the following to ensure that the query cost is lower

-
    - {suggestions.map((x) => ( -
  • {x}
  • - ))} -
-
- )} + + {suggestions.length > 0 && ( + <> + + +

+ Suggested steps +

+ + {suggestions.length > 0 && ( +
+

You may check the following to lower the cost of the query

+
    + {suggestions.map((x) => ( +
  • {x}
  • + ))} +
+
+ )} +
+ + )} + ) } + +const LoadDataWarningDialog = ({ + error, + onSelectLoadData, +}: { + error: ResponseError + onSelectLoadData: () => void +}) => { + const metadata = error.metadata + + return ( + + + + + event.preventDefault()}> + + Confirm to proceed loading data + + Preventive measure to mitigate impacting the database + + + + +

+ The query to load your table's data was initially skipped as its{' '} + + estimated cost + +

Estimated cost: {metadata?.cost.toLocaleString()}

+

+ Determined via the EXPLAIN command +

+ + {' '} + is high and could place significant load on the database with high disk I/O or CPU + usage. +

+ +

+ You may proceed to run the query, and we'll suppress this warning for this table for the + rest of this browser session. +

+
+ + + + + + +
+
+ ) +} diff --git a/apps/studio/data/sql/execute-sql-query.ts b/apps/studio/data/sql/execute-sql-query.ts index 7182f5ae52cbf..79626f6c0ed9b 100644 --- a/apps/studio/data/sql/execute-sql-query.ts +++ b/apps/studio/data/sql/execute-sql-query.ts @@ -21,7 +21,7 @@ import { * Reckon we ensure that the dashboard just caps query costs at "heavy", so that it doesn't impact the DB for other queries * (e.g from the user's application) */ -const COST_THRESHOLD = 100_000 +const COST_THRESHOLD = 200_000 export const COST_THRESHOLD_ERROR = 'Query cost exceeds threshold' export type ExecuteSqlVariables = { diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 926edb3742de5..8332f6929cc13 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -1,6 +1,6 @@ import { Query, type QueryFilter } from '@supabase/pg-meta/src/query' import { getTableRowsSql } from '@supabase/pg-meta/src/query/table-row-query' -import { type QueryClient, useQuery, useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query' import { IS_PLATFORM } from 'common' import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types' @@ -16,11 +16,12 @@ import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' import { ResponseError, UseCustomQueryOptions } from 'types' import { handleError } from '../fetchers' -import { ExecuteSqlError, executeSql } from '../sql/execute-sql-query' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' import { tableRowKeys } from './keys' import { formatFilterValue } from './utils' +import { timeout } from '@/lib/helpers' -export interface GetTableRowsArgs { +interface GetTableRowsArgs { table?: SupaTable filters?: Filter[] sorts?: Sort[] @@ -87,10 +88,6 @@ function getRetryAfter(error: any): number | undefined { return undefined } -async function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - export async function executeWithRetry( fn: () => Promise, maxRetries: number = 3, @@ -106,7 +103,7 @@ export async function executeWithRetry( const retryAfter = getRetryAfter(error) const delayMs = retryAfter ? retryAfter * 1000 : baseDelay * Math.pow(2, attempt) - await sleep(delayMs) + await timeout(delayMs) continue } throw error @@ -267,7 +264,7 @@ export const fetchAllTableRows = async ({ if (result.length < rowsPerPage) break - await sleep(THROTTLE_DELAY) + await timeout(THROTTLE_DELAY) } catch (error) { throw new Error( `Error fetching all table rows: ${error instanceof Error ? error.message : 'Unknown error'}` @@ -294,7 +291,7 @@ export const fetchAllTableRows = async ({ if (result.length < rowsPerPage) break - await sleep(THROTTLE_DELAY) + await timeout(THROTTLE_DELAY) } catch (error) { throw new Error( `Error fetching all table rows: ${error instanceof Error ? error.message : 'Unknown error'}` @@ -306,19 +303,20 @@ export const fetchAllTableRows = async ({ return rows.filter((row) => row[ROLE_IMPERSONATION_NO_RESULTS] !== 1) } -export type TableRows = { rows: SupaRow[] } +type TableRows = { rows: SupaRow[] } -export type TableRowsVariables = Omit & { +type TableRowsVariables = Omit & { queryClient: QueryClient projectRef?: string connectionString?: string | null tableId?: number + preflightCheck?: boolean } export type TableRowsData = TableRows -export type TableRowsError = ExecuteSqlError +type TableRowsError = ExecuteSqlError -export async function getTableRows( +async function getTableRows( { queryClient, projectRef, @@ -329,6 +327,7 @@ export async function getTableRows( sorts, limit, page, + preflightCheck = false, }: TableRowsVariables, signal?: AbortSignal ) { @@ -375,7 +374,7 @@ export async function getTableRows( sql, queryKey: ['table-rows', table?.id], isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), - preflightCheck: true, + preflightCheck, }, signal ) @@ -395,6 +394,7 @@ export const useTableRowsQuery = ( { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { const queryClient = useQueryClient() + return useQuery({ queryKey: tableRowKeys.tableRows(projectRef, { table: { id: tableId }, diff --git a/apps/studio/state/table-editor-table.tsx b/apps/studio/state/table-editor-table.tsx index 65c41ddd676a2..4641a5548abb8 100644 --- a/apps/studio/state/table-editor-table.tsx +++ b/apps/studio/state/table-editor-table.tsx @@ -1,15 +1,15 @@ import { useFlag } from 'common' +import { TableIndexAdvisorProvider } from 'components/grid/context/TableIndexAdvisorContext' import { loadTableEditorStateFromLocalStorage, parseSupaTable, saveTableEditorStateToLocalStorageDebounced, } from 'components/grid/SupabaseGrid.utils' -import { TableIndexAdvisorProvider } from 'components/grid/context/TableIndexAdvisorContext' import { Filter, SupaRow } from 'components/grid/types' import { getInitialGridColumns } from 'components/grid/utils/column' import { getGridColumns } from 'components/grid/utils/gridColumns' import { Entity } from 'data/table-editor/table-editor-types' -import { PropsWithChildren, createContext, useContext, useEffect, useRef } from 'react' +import { createContext, PropsWithChildren, useContext, useEffect, useRef } from 'react' import { CalculatedColumn } from 'react-data-grid' import { proxy, ref, subscribe, useSnapshot } from 'valtio' import { proxySet } from 'valtio/utils' @@ -20,6 +20,7 @@ export const createTableEditorTableState = ({ projectRef, table: originalTable, editable = true, + preflightCheck = true, onAddColumn, onExpandJSONEditor, onExpandTextEditor, @@ -28,6 +29,7 @@ export const createTableEditorTableState = ({ table: Entity /** If set to true, render an additional "+" column to support adding a new column in the grid editor */ editable?: boolean + preflightCheck?: boolean onAddColumn: () => void onExpandJSONEditor: (column: string, row: SupaRow) => void onExpandTextEditor: (column: string, row: SupaRow) => void @@ -171,6 +173,9 @@ export const createTableEditorTableState = ({ clearFilters: () => { state.filters = [] }, + + preflightCheck, + setPreflightCheck: (value: boolean) => (state.preflightCheck = value), }) return state diff --git a/apps/studio/state/table-editor.tsx b/apps/studio/state/table-editor.tsx index 297e977fde7d9..9f78e710c751d 100644 --- a/apps/studio/state/table-editor.tsx +++ b/apps/studio/state/table-editor.tsx @@ -10,15 +10,15 @@ import { generateTableChangeKey } from 'components/grid/utils/queueOperationUtil import { ForeignKey } from 'components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.types' import type { EditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' import type { TableField } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types' -import { PropsWithChildren, createContext, useContext } from 'react' +import { createContext, PropsWithChildren, useContext } from 'react' import type { Dictionary } from 'types' import { proxy, useSnapshot } from 'valtio' import { NewQueuedOperation, + QueuedOperationType, type OperationQueueState, type QueueStatus, - QueuedOperationType, } from './table-editor-operation-queue.types' export const TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE = 100 @@ -331,6 +331,16 @@ export const createTableEditorState = () => { }) return state.operationQueue.operations.some((op) => op.id === key) }, + + /** + * Toggle the preflight check behaviour for each table + */ + tablesToIgnorePreflightCheck: [] as number[], + setTableToIgnorePreflightCheck: (id: number) => { + const set = new Set(state.tablesToIgnorePreflightCheck) + set.add(id) + state.tablesToIgnorePreflightCheck = [...set] + }, }) return state From 25f9f7c5e341ec431c1929d35dccc0bbe2eec977 Mon Sep 17 00:00:00 2001 From: Jonathan Fulton Date: Thu, 19 Feb 2026 03:08:08 -0500 Subject: [PATCH 7/9] docs(realtime): add note about sync event behavior in Presence (#42338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Documentation improvement for Realtime Presence. ## What is the current behavior? The Presence documentation doesn't explain that during a `sync` event, clients may receive `join` and `leave` events simultaneously even when no users are actually joining or leaving. This can be confusing for developers new to Presence. ## What is the new behavior? Added an explanatory note clarifying that: - During `sync`, you may receive `join` and `leave` events at the same time - This is normal and expected - It reflects state reconciliation with the server, not real user movement This helps developers understand this behavior upfront rather than having to discover it through debugging. ## Additional context As mentioned in the issue, this behavior is discussed in [this community discussion](https://github.com/orgs/supabase/discussions/26748) where the solution had to be discovered by users. Fixes #41175 ## Summary by CodeRabbit * **Documentation** * Clarified behavior during sync events in the Realtime Presence guide. Added explanation that simultaneous join and leave events may occur due to state reconciliation rather than actual user movement changes. ✏️ Tip: You can customize this high-level summary in your review settings. --- apps/docs/content/guides/realtime/presence.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/docs/content/guides/realtime/presence.mdx b/apps/docs/content/guides/realtime/presence.mdx index dae2120972f87..972446f8850bb 100644 --- a/apps/docs/content/guides/realtime/presence.mdx +++ b/apps/docs/content/guides/realtime/presence.mdx @@ -20,6 +20,12 @@ When any client subscribes, disconnects, or updates their presence payload, Supa - **`join`** — a new client has started tracking presence - **`leave`** — a client has stopped tracking presence + + +During a `sync` event, you may receive `join` and `leave` events simultaneously, even though no users are actually joining or leaving. This is expected behavior—Presence reconciles its local state with the server state, which can trigger these events as part of the synchronization process. This reflects state reconciliation, not real user movement. + + + The complete presence state returned by `presenceState()` looks like this: ```json From fcdff89e13a184af0129ef334c89a6cc1d225a91 Mon Sep 17 00:00:00 2001 From: Aaron Byrne <65355719+aaronByrne1@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:20:39 +0000 Subject: [PATCH 8/9] fix(studio):Synced Git Branch now shows error message (#42790) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix ## What is the current behavior? image It allows you to attempt to create a branch even when that branch is used. We then get a vague error that the branch creation has failed. image ## What is the new behavior? image ## Additional context Add any other context or screenshots. ## Summary by CodeRabbit * **Bug Fixes** * Prevented users from linking the same GitHub branch to multiple local branches, with clear error messages shown during both branch creation and editing. * Enhanced validation checks to ensure branch linkage uniqueness and provide real-time feedback on invalid selections. Co-authored-by: Ali Waseem --- .../BranchManagement/CreateBranchModal.tsx | 13 ++++++++++++- .../BranchManagement/EditBranchModal.tsx | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 50631d6e6b168..c1c80df0652c1 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -236,6 +236,17 @@ export const CreateBranchModal = () => { { onSuccess: () => { if (form.getValues('gitBranchName') !== branchName) return + + // Check if another branch is already linked to this git branch + const existingBranch = (branches ?? []).find((b) => b.git_branch === branchName) + if (existingBranch) { + setIsGitBranchValid(false) + form.setError('gitBranchName', { + message: `Branch "${existingBranch.name}" is already linked to git branch "${branchName}"`, + }) + return + } + setIsGitBranchValid(true) form.clearErrors('gitBranchName') }, @@ -250,7 +261,7 @@ export const CreateBranchModal = () => { } ) }, - [githubConnection, form, checkGithubBranchValidity, repoOwner, repoName] + [githubConnection, form, checkGithubBranchValidity, repoOwner, repoName, branches] ) const onSubmit = (data: z.infer) => { diff --git a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx index bb290791dd64e..eeb573b8326c7 100644 --- a/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EditBranchModal.tsx @@ -167,6 +167,19 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro { onSuccess: () => { if (form.getValues('gitBranchName') !== requested) return + + // Check if another branch is already linked to this git branch + const existingBranch = (branches ?? []).find( + (b) => b.git_branch === branchName && b.id !== branch?.id + ) + if (existingBranch) { + setIsGitBranchValid(false) + form.setError('gitBranchName', { + message: `Branch "${existingBranch.name}" is already linked to git branch "${branchName}"`, + }) + return + } + setIsGitBranchValid(true) form.clearErrors('gitBranchName') }, @@ -181,7 +194,7 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro } ) }, - [githubConnection, form, checkGithubBranchValidity, repoOwner, repoName] + [githubConnection, form, checkGithubBranchValidity, repoOwner, repoName, branches, branch] ) // Pre-fill form when the modal becomes visible and branch data is available From 42f13443033991e4acd1cbf6cca4377182cd9131 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 19 Feb 2026 17:53:45 +0800 Subject: [PATCH 9/9] Ensure logs pages are accessible irregardless of project's status (#43010) ## Context Ensure that project's logs pages are accessible irregardless of project's status, given that logs are mutually exclusive from the project's database and doesn't require the database to be online for it to be functional. ## Other changes Mainly around simplifying some logic within ProjectLayout - Remove `disabled` prop in `SideBarNavLink` component, given that `route` prop already has a `disabled` param - Define `disabled` within `NavigationBar.utils.tsx` -> just have one place to define the "configuration" of a route - Minimize manually declaring specific routes to render specific UI (for Restore failed) - All of these can be grouped under `routesToIgnoreDBConnection` ## To test - [ ] Verify that you can access the logs page while the project is coming up - [ ] Verify that you can access the logs page on a paused project (and also use it) --- apps/studio/components/interfaces/Sidebar.tsx | 60 +++++++------------ .../NavigationBar/NavigationBar.utils.tsx | 23 ++++--- .../layouts/ProjectLayout/index.tsx | 42 ++++++------- .../SettingsMenu.utils.tsx | 16 ++--- 4 files changed, 60 insertions(+), 81 deletions(-) diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index dc65c2d2a1e06..2df6201e6bcf6 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -1,11 +1,3 @@ -import { AnimatePresence, motion, MotionProps } from 'framer-motion' -import { isUndefined } from 'lodash' -import { Blocks, Boxes, ChartArea, PanelLeftDashed, Receipt, Settings, Users } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react' - -import { PROJECT_STATUS } from '@/lib/constants' import { LOCAL_STORAGE_KEYS, useFlag, useIsMFAEnabled, useParams } from 'common' import { generateOtherRoutes, @@ -15,6 +7,7 @@ import { } from 'components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils' import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { AnimatePresence, motion, MotionProps } from 'framer-motion' import { useHideSidebar } from 'hooks/misc/useHideSidebar' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useLints } from 'hooks/misc/useLints' @@ -22,6 +15,11 @@ import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Home } from 'icons' +import { isUndefined } from 'lodash' +import { Blocks, Boxes, ChartArea, PanelLeftDashed, Receipt, Settings, Users } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react' import { useAppStateSnapshot } from 'state/app-state' import { Button, @@ -43,6 +41,7 @@ import { Sidebar as SidebarPrimitive, useSidebar, } from 'ui' + import { Route } from '../ui/ui.types' import { useIsAPIDocsSidePanelEnabled, @@ -161,12 +160,10 @@ export function SideBarNavLink({ route, active, onClick, - disabled, ...props }: { route: Route active?: boolean - disabled?: boolean onClick?: () => void } & ComponentPropsWithoutRef) { const [sidebarBehaviour] = useLocalStorageQuery( @@ -175,7 +172,7 @@ export function SideBarNavLink({ ) const buttonProps = { - disabled, + disabled: route.disabled, tooltip: sidebarBehaviour === 'closed' ? route.label : '', isActive: active, className: cn('text-sm', sidebarBehaviour === 'open' ? '!px-2' : ''), @@ -194,7 +191,7 @@ export function SideBarNavLink({ return ( - {route.link && !disabled ? ( + {route.link && !route.disabled ? ( {content} @@ -205,16 +202,12 @@ export function SideBarNavLink({ ) } -const ActiveDot = (errorArray: any[], warningArray: any[]) => { +const ActiveDot = ({ hasErrors, hasWarnings }: { hasErrors: boolean; hasWarnings: boolean }) => { return (
0 - ? 'bg-destructive-600' - : warningArray.length > 0 - ? 'bg-warning-600' - : 'bg-transparent' + hasErrors ? 'bg-destructive-600' : hasWarnings ? 'bg-warning-600' : 'bg-transparent' )} /> ) @@ -230,8 +223,6 @@ const ProjectLinks = () => { const showReports = useIsFeatureEnabled('reports:all') const { mutate: sendEvent } = useSendEventMutation() - const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY - const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled() const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() @@ -283,7 +274,6 @@ const ProjectLinks = () => { {toolRoutes.map((route, i) => ( @@ -294,7 +284,6 @@ const ProjectLinks = () => { {productRoutes.map((route, i) => ( @@ -322,41 +311,36 @@ const ProjectLinks = () => { return ( ) } else if (route.key === 'advisors') { return (
- {isProjectActive && ActiveDot(errorLints, securityLints)} - + {!route.disabled && ( + 0} + hasWarnings={securityLints.length > 0} + /> + )} +
) } else { return ( - + ) } })} @@ -440,7 +424,6 @@ const OrganizationLinks = () => { {navMenuItems.map((item, i) => ( { link: item.href, key: item.label, icon: item.icon, + disabled: disableAccessMfa, }} /> ))} diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 4a06b218dfe43..995e6915d8730 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -10,6 +10,7 @@ import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react' export const generateToolRoutes = (ref?: string, project?: Project, features?: {}): Route[] => { + const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP const buildingUrl = `/project/${ref}` @@ -17,6 +18,7 @@ export const generateToolRoutes = (ref?: string, project?: Project, features?: { { key: 'editor', label: 'Table Editor', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`), linkElement: , @@ -24,6 +26,7 @@ export const generateToolRoutes = (ref?: string, project?: Project, features?: { { key: 'sql', label: 'SQL Editor', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/sql`), }, @@ -58,6 +61,7 @@ export const generateProductRoutes = ( { key: 'database', label: 'Database', + disabled: !isProjectActive, icon: , link: ref && @@ -73,6 +77,7 @@ export const generateProductRoutes = ( { key: 'auth', label: 'Authentication', + disabled: !isProjectActive, icon: , link: ref && @@ -90,6 +95,7 @@ export const generateProductRoutes = ( { key: 'storage', label: 'Storage', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/storage/files`), }, @@ -100,6 +106,7 @@ export const generateProductRoutes = ( { key: 'functions', label: 'Edge Functions', + disabled: false, icon: , link: ref && `/project/${ref}/functions`, }, @@ -110,6 +117,7 @@ export const generateProductRoutes = ( { key: 'realtime', label: 'Realtime', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/realtime/inspector`), }, @@ -123,6 +131,7 @@ export const generateOtherRoutes = ( project?: Project, features?: { unifiedLogs?: boolean; showReports?: boolean; apiDocsSidePanel?: boolean } ): Route[] => { + const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP const buildingUrl = `/project/${ref}` @@ -135,6 +144,7 @@ export const generateOtherRoutes = ( { key: 'advisors', label: 'Advisors', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/advisors/security`), }, @@ -143,6 +153,7 @@ export const generateOtherRoutes = ( { key: 'observability', label: 'Observability', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/observability`), }, @@ -151,20 +162,16 @@ export const generateOtherRoutes = ( { key: 'logs', label: 'Logs', + disabled: false, icon: , - link: - ref && - (isProjectBuilding - ? buildingUrl - : unifiedLogsEnabled - ? `/project/${ref}/logs` - : `/project/${ref}/logs/explorer`), + link: ref && (unifiedLogsEnabled ? `/project/${ref}/logs` : `/project/${ref}/logs/explorer`), }, ...(apiDocsSidePanelEnabled ? [ { key: 'api', label: 'API Docs', + disabled: !isProjectActive, icon: , link: ref && @@ -175,6 +182,7 @@ export const generateOtherRoutes = ( { key: 'integrations', label: 'Integrations', + disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/integrations`), }, @@ -192,6 +200,7 @@ export const generateSettingsRoutes = (ref?: string, project?: Project): Route[] ref && (IS_PLATFORM ? `/project/${ref}/settings/general` : `/project/${ref}/settings/log-drains`), items: settingsMenu, + disabled: false, }, ] } diff --git a/apps/studio/components/layouts/ProjectLayout/index.tsx b/apps/studio/components/layouts/ProjectLayout/index.tsx index 660c784aa4193..5c63046e4dca2 100644 --- a/apps/studio/components/layouts/ProjectLayout/index.tsx +++ b/apps/studio/components/layouts/ProjectLayout/index.tsx @@ -1,7 +1,3 @@ -import Head from 'next/head' -import { useRouter } from 'next/router' -import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect } from 'react' - import { mergeRefs, useParams } from 'common' import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal' import { ProjectAPIDocs } from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs' @@ -13,10 +9,14 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { usePHFlag } from 'hooks/ui/useFlag' import { PROJECT_STATUS } from 'lib/constants' +import Head from 'next/head' +import { useRouter } from 'next/router' +import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect } from 'react' import { useAppStateSnapshot } from 'state/app-state' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { cn, LogoLoader, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui' import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav' + import { useEditorType } from '../editors/EditorsLayout.hooks' import { useSetMainScrollContainer } from '../MainScrollContainerContext' import BuildingState from './BuildingState' @@ -35,27 +35,27 @@ import { UpgradingState } from './UpgradingState' // [Joshen] This is temporary while we unblock users from managing their project // if their project is not responding well for any reason. Eventually needs a bit of an overhaul const routesToIgnoreProjectDetailsRequest = [ + '/project/[ref]/settings/infrastructure', + '/project/[ref]/settings/addons', '/project/[ref]/settings/general', '/project/[ref]/database/settings', '/project/[ref]/storage/settings', - '/project/[ref]/settings/infrastructure', - '/project/[ref]/settings/addons', ] const routesToIgnoreDBConnection = [ '/project/[ref]/branches', - '/project/[ref]/database/backups/scheduled', - '/project/[ref]/database/backups/pitr', - '/project/[ref]/settings/addons', + '/project/[ref]/database/backups', + '/project/[ref]/settings', '/project/[ref]/functions', + '/project/[ref]/logs', ] const routesToIgnorePostgrestConnection = [ - '/project/[ref]/reports', '/project/[ref]/settings/general', - '/project/[ref]/database/settings', '/project/[ref]/settings/infrastructure', '/project/[ref]/settings/addons', + '/project/[ref]/database/settings', + '/project/[ref]/reports', ] export interface ProjectLayoutProps { @@ -108,7 +108,8 @@ export const ProjectLayout = forwardRef
- {/* autoSaveId="project-layout" */} {productMenu && ( router.pathname.includes(x)) const requiresPostgrestConnection = !routesToIgnorePostgrestConnection.includes(router.pathname) const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname) @@ -304,11 +296,11 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp // handle redirect to home for building state const shouldRedirectToHomeForBuilding = - isHomeNew && requiresDbConnection && isProjectBuilding && !isBranchesPage && !isHomePage + isProjectBuilding && requiresDbConnection && isHomeNew && !isHomePage // We won't be showing the building state with the new home page const shouldShowBuildingState = - requiresDbConnection && isProjectBuilding && !isBranchesPage && !(isHomeNew && isHomePage) + isProjectBuilding && requiresDbConnection && !(isHomeNew && isHomePage) useEffect(() => { if (shouldRedirectToHomeForBuilding && ref) { @@ -352,7 +344,7 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp return } - if (isProjectRestoreFailed && !isBackupsPage && !isEdgeFunctionPages) { + if (requiresDbConnection && isProjectRestoreFailed) { return } diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index cd82c7aa6a5f5..17320fc9d33ef 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -1,8 +1,7 @@ -import { ArrowUpRight } from 'lucide-react' - import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' import type { Project } from 'data/projects/project-detail-query' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' +import { ArrowUpRight } from 'lucide-react' import type { Organization } from 'types' export const generateSettingsMenu = ( @@ -37,13 +36,7 @@ export const generateSettingsMenu = ( } const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY - const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP - const buildingUrl = `/project/${ref}` - const authEnabled = features?.auth ?? true - const authProvidersEnabled = features?.authProviders ?? true - const edgeFunctionsEnabled = features?.edgeFunctions ?? true - const storageEnabled = features?.storage ?? true const legacyJwtKeysEnabled = features?.legacyJwtKeys ?? true const billingEnabled = features?.billing ?? true @@ -67,7 +60,7 @@ export const generateSettingsMenu = ( { name: 'Infrastructure', key: 'infrastructure', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/settings/infrastructure`, + url: `/project/${ref}/settings/infrastructure`, items: [], disabled: !isProjectActive, }, @@ -118,14 +111,15 @@ export const generateSettingsMenu = ( { name: 'Data API', key: 'api', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/integrations/data_api/overview`, + url: `/project/${ref}/integrations/data_api/overview`, items: [], rightIcon: , + disabled: !isProjectActive, }, { name: 'Vault', key: 'vault', - url: isProjectBuilding ? buildingUrl : `/project/${ref}/integrations/vault/overview`, + url: `/project/${ref}/integrations/vault/overview`, items: [], rightIcon: , label: 'Beta',