From 5c28d1126557e7b7376e61cb772d58b2790f4fc0 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:11:35 +0200 Subject: [PATCH 01/11] database report: fix keys in getConnectionLimits (#37795) fix keys --- apps/studio/data/reports/database-charts.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 1f38e714f72..135657f6c20 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -613,11 +613,11 @@ export const getConnectionLimits = (computeSize: string = 'medium') => { medium: { direct: 120, pooler: 600 }, large: { direct: 160, pooler: 800 }, xl: { direct: 240, pooler: 1000 }, - '2xl': { direct: 380, pooler: 1500 }, - '4xl': { direct: 480, pooler: 3000 }, - '8xl': { direct: 490, pooler: 6000 }, - '12xl': { direct: 500, pooler: 9000 }, - '16xl': { direct: 500, pooler: 12000 }, + '2xlarge': { direct: 380, pooler: 1500 }, + '4xlarge': { direct: 480, pooler: 3000 }, + '8xlarge': { direct: 490, pooler: 6000 }, + '12xlarge': { direct: 500, pooler: 9000 }, + '16xlarge': { direct: 500, pooler: 12000 }, } return ( From 3f8db287d37cb4ae24e87db75ebe57acd67e628e Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Fri, 8 Aug 2025 23:13:17 +0800 Subject: [PATCH 02/11] chore: add missing env vars to workflow (#37779) --- .../guides/deployment/branching/working-with-branches.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/docs/content/guides/deployment/branching/working-with-branches.mdx b/apps/docs/content/guides/deployment/branching/working-with-branches.mdx index f04727d7504..e86588cd9ba 100644 --- a/apps/docs/content/guides/deployment/branching/working-with-branches.mdx +++ b/apps/docs/content/guides/deployment/branching/working-with-branches.mdx @@ -48,6 +48,9 @@ jobs: - wait if: ${{ needs.wait.outputs.status == 'success' }} runs-on: ubuntu-latest + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }} steps: - uses: supabase/setup-cli@v1 with: From 00cad157239d625f013a04b83bb04be40f01914f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 8 Aug 2025 22:20:59 +0700 Subject: [PATCH 03/11] Set unusualLineTerminators for grid monaco editor to auto (#37786) * Set unusualLineTerminators for grid monaco editor to auto * Set to off instead --- apps/studio/components/grid/components/common/MonacoEditor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/studio/components/grid/components/common/MonacoEditor.tsx b/apps/studio/components/grid/components/common/MonacoEditor.tsx index e5ec4ebb075..d5e8c0dc827 100644 --- a/apps/studio/components/grid/components/common/MonacoEditor.tsx +++ b/apps/studio/components/grid/components/common/MonacoEditor.tsx @@ -68,6 +68,7 @@ export const MonacoEditor = ({ lineNumbersMinChars: 0, scrollBeyondLastLine: false, wordWrap: 'on', + unusualLineTerminators: 'off', }} /> ) From d6fe8476f334052269d1b47ddccf46f4ce2510ee Mon Sep 17 00:00:00 2001 From: Drake Costa Date: Fri, 8 Aug 2025 11:12:06 -0700 Subject: [PATCH 04/11] Refactor `EditSecretModal` to use shadcn components (#37258) * chore: Refactor `NewAccessTokenButton` to use shadcn components This brings `NewAccessTokenButton` into conformance with the latest UI patterns of using shadcn based components, zod schemas, and react-hook-form for form submission. I based this refactor on the implementation of `CreateSecretAPIKeyDialog`, which already made use of the above. * add success and failure toasts * add smoke test for `NewAccessTokenButton`, fix vitest config This adds a minimal test for `NewAccessTokenButton` but more importantly fixes a critical bug in the vitest configuration for `studio`. Because `restoreMocks: true` was set in the config, this meant that the mock implementation for `window.matchMedia` defined in the `tests/vitestSetup.ts` file was being **reset** to an empty function before each test suite was run. While this didn't appear to be a problem before, that appears to be because none of the existing tests had `motion.div` in their component trees. Since `motion.div` calls `addListener` on a media query as part of it's lifecycle, that meant that any test which included it would fail as `addListener` would be undefined in this situation. Removing `restoreMocks: true` from the config results in the desired mocking behavior. This change is necessary in order to test any component that has the `FormItemLayout` component, as it uses `framer-motion` to animate in error messages for input elements. * add warning comment in case of future config regression * update test cases, reset form on dialog close, testing setup fixes This fixes an issue with the polyfills for the testing environment, where a call to `useMutation` would fail as a result of `TransformStream` being set to `null`. Basic tests for access token creation and form resetting added. Adds `@faker-js/faker` to the studio app devDependencies to generate mock data for `msw` endpoint response. Adds `shx` to the docs app devDependencies to ensure that the `codegen:examples` script runs cross-platform. * ensure mocked date is a string * update testing setup, pollyfills and add rich type support to addAPIMock * add missing changes * Move `EditSecretModal` to be child of `SecretRow` In the accompanying PR for `DeleteSecretModal`, that one will also be moved over to `SecretRow` * Update studio testing setup files Improves API mocking type safety and polyfills browser APIs necessary to run tests with framer-motion components * chore: Refactor `NewAccessTokenButton` to use shadcn components This brings `NewAccessTokenButton` into conformance with the latest UI patterns of using shadcn based components, zod schemas, and react-hook-form for form submission. I based this refactor on the implementation of `CreateSecretAPIKeyDialog`, which already made use of the above. * add success and failure toasts * add smoke test for `NewAccessTokenButton`, fix vitest config This adds a minimal test for `NewAccessTokenButton` but more importantly fixes a critical bug in the vitest configuration for `studio`. Because `restoreMocks: true` was set in the config, this meant that the mock implementation for `window.matchMedia` defined in the `tests/vitestSetup.ts` file was being **reset** to an empty function before each test suite was run. While this didn't appear to be a problem before, that appears to be because none of the existing tests had `motion.div` in their component trees. Since `motion.div` calls `addListener` on a media query as part of it's lifecycle, that meant that any test which included it would fail as `addListener` would be undefined in this situation. Removing `restoreMocks: true` from the config results in the desired mocking behavior. This change is necessary in order to test any component that has the `FormItemLayout` component, as it uses `framer-motion` to animate in error messages for input elements. * update test cases, reset form on dialog close, testing setup fixes This fixes an issue with the polyfills for the testing environment, where a call to `useMutation` would fail as a result of `TransformStream` being set to `null`. Basic tests for access token creation and form resetting added. Adds `@faker-js/faker` to the studio app devDependencies to generate mock data for `msw` endpoint response. Adds `shx` to the docs app devDependencies to ensure that the `codegen:examples` script runs cross-platform. * ensure mocked date is a string * update testing setup, pollyfills and add rich type support to addAPIMock * add missing changes * Move `EditSecretModal` to be child of `SecretRow` In the accompanying PR for `DeleteSecretModal`, that one will also be moved over to `SecretRow` * fix imports * Update test, fix rebase errors * remove unnecessary code * resolve type errors * fix missing listen call for msw, resolve test type error * fix imports * Update studio testing setup files Improves API mocking type safety and polyfills browser APIs necessary to run tests with framer-motion components * fix missing listen call for msw, resolve test type error * fix imports * remove duplicate test * update testing setup * implement requested changes * cleanup unnecessary conditional logic for undefined `secret` prop * remove unused import * remove unnecessary code * cleanup, replace lodash import with JS * add required router mock * fix broken lockfile --------- Co-authored-by: Terry Sutton --- .../Vault/Secrets/EditSecretModal.tsx | 303 ++++++++++-------- .../Integrations/Vault/Secrets/SecretRow.tsx | 15 +- .../Vault/Secrets/SecretsManagement.tsx | 60 ++-- .../__tests__/EditSecretModal.test.tsx | 103 ++++++ apps/studio/tests/lib/msw.ts | 13 +- apps/studio/tests/lib/route-mock.ts | 27 +- apps/studio/tests/setup/polyfills.ts | 2 + pnpm-lock.yaml | 42 ++- 8 files changed, 365 insertions(+), 200 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx index 02bc97521dd..b87b8782648 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx @@ -1,158 +1,207 @@ -import { isEmpty } from 'lodash' -import { Eye, EyeOff } from 'lucide-react' -import { useEffect, useState } from 'react' +import { useState } from 'react' +import { EyeOff, Eye } from 'lucide-react' +import { type SubmitHandler, useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' import { toast } from 'sonner' +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' import { useVaultSecretUpdateMutation } from 'data/vault/vault-secret-update-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import type { VaultSecret } from 'types' -import { Button, Form, Input, Modal } from 'ui' interface EditSecretModalProps { - selectedSecret: VaultSecret | undefined + visible: boolean + secret: VaultSecret onClose: () => void } -const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => { - const { data: project } = useSelectedProjectQuery() +const SecretSchema = z.object({ + name: z.string().min(1, 'Please provide a name for your secret'), + description: z.string().optional(), + secret: z.string().min(1, 'Please enter your secret value'), +}) + +const formId = 'edit-vault-secret-form' + +const EditSecretModal = ({ visible, secret, onClose }: EditSecretModalProps) => { const [showSecretValue, setShowSecretValue] = useState(false) - - const { mutateAsync: updateSecret } = useVaultSecretUpdateMutation() - - let INITIAL_VALUES = { - name: selectedSecret?.name ?? '', - description: selectedSecret?.description ?? '', - secret: selectedSecret?.decryptedSecret ?? '', + const { data: project } = useSelectedProjectQuery() + const { data, isLoading: isLoadingSecretValue } = useVaultSecretDecryptedValueQuery( + { + projectRef: project?.ref, + id: secret.id, + connectionString: project?.connectionString, + }, + { enabled: !!project?.ref } + ) + const values = { + name: secret.name ?? '', + description: secret.description ?? '', + secret: secret.decryptedSecret ?? data ?? '', } + const form = useForm>({ + resolver: zodResolver(SecretSchema), + defaultValues: values, + values, + }) - useEffect(() => { - if (selectedSecret !== undefined) { - setShowSecretValue(false) - } - }, [selectedSecret]) + const { mutate: updateSecret, isLoading: isSubmitting } = useVaultSecretUpdateMutation() - const validate = (values: any) => { - const errors: any = {} - if (values.name.length === 0) errors.name = 'Please provide a name for your secret' - if (values.secret.length === 0) errors.secret = 'Please enter your secret value' - return errors - } - - const onUpdateSecret = async (values: any, { setSubmitting }: any) => { + const onSubmit: SubmitHandler> = async (values) => { if (!project) return console.error('Project is required') - try { - const payload: Partial = {} - if (values.name !== selectedSecret?.name) payload.name = values.name - if (values.description !== selectedSecret?.description) - payload.description = values.description - payload.secret = values.secret + const payload: Partial = { + secret: values.secret, + } + if (values.name !== secret.name) payload.name = values.name + if (values.description !== secret.description) payload.description = values.description - if (!isEmpty(payload) && selectedSecret) { - setSubmitting(true) - const res = await updateSecret({ + if (Object.keys(payload).length > 0) { + updateSecret( + { projectRef: project.ref, connectionString: project?.connectionString, - id: selectedSecret.id, + id: secret.id, ...payload, - }) - if (!res.error) { - toast.success('Successfully updated secret') - setSubmitting(false) - onClose() - } else { - toast.error(`Failed to update secret: ${res.error.message}`) - setSubmitting(false) + }, + { + onSuccess: () => { + toast.success('Successfully updated secret') + onClose() + }, + onError: (error) => { + toast.error(`Failed to update secret: ${error.message}`) + }, } - } - } finally { + ) } } return ( - Edit secret} + { + if (!open) { + form.reset() + onClose() + } + }} > -
- {({ isSubmitting, resetForm }: any) => { - const { - data, - isLoading: isLoadingSecretValue, - isSuccess: isSuccessSecretValue, - // [Joshen] JFYI this is breaking rules of hooks, will be fixed once we move to - // using react hook form instead - // eslint-disable-next-line react-hooks/rules-of-hooks - } = useVaultSecretDecryptedValueQuery( - { - projectRef: project?.ref!, - id: selectedSecret?.id!, - connectionString: project?.connectionString, - }, - { enabled: selectedSecret !== undefined && !!(project?.ref && selectedSecret?.id) } - ) - - // [Joshen] JFYI this is breaking rules of hooks, will be fixed once we move to - // using react hook form instead - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (selectedSecret !== undefined && isSuccessSecretValue) { - resetForm({ - values: { ...INITIAL_VALUES, secret: data }, - initialValues: { ...INITIAL_VALUES, secret: data }, - }) - } - }, [selectedSecret, isSuccessSecretValue]) - - return isLoadingSecretValue ? ( - - - - ) : ( - <> - - - - - - - - - ) - }} -
-
+ + + Edit secret + + + {isLoadingSecretValue ? ( + + + + ) : ( + <> + + +
+ ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + +
+ +
+
+
+ )} + /> + +
+
+ + + + + + )} +
+ ) } diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx index f7b1f5719ff..20a1ace90eb 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx @@ -19,17 +19,17 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react' import type { VaultSecret } from 'types' +import EditSecretModal from './EditSecretModal' interface SecretRowProps { secret: VaultSecret - onSelectEdit: (secret: VaultSecret) => void onSelectRemove: (secret: VaultSecret) => void } -const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) => { +const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() - + const [modal, setModal] = useState(null) const [revealSecret, setRevealSecret] = useState(false) const name = secret?.name ?? 'No name provided' @@ -46,6 +46,8 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) => } ) + const onCloseModal = () => setModal(null) + return (
@@ -94,9 +96,10 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) => {secret.updated_at === secret.created_at ? 'Added' : 'Updated'} on{' '} {dayjs(secret.updated_at).format('MMM D, YYYY')}

+ -
) diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx index 8484346130f..43388850602 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx @@ -9,22 +9,29 @@ import { DocsButton } from 'components/ui/DocsButton' import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import type { VaultSecret } from 'types' -import { Button, Input, Listbox, Separator } from 'ui' +import { + Button, + Input, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Separator, +} from 'ui' import AddNewSecretModal from './AddNewSecretModal' import DeleteSecretModal from './DeleteSecretModal' -import EditSecretModal from './EditSecretModal' import SecretRow from './SecretRow' +import type { VaultSecret } from 'types' export const SecretsManagement = () => { const { search } = useParams() const { data: project } = useSelectedProjectQuery() const [searchValue, setSearchValue] = useState('') - const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at') const [showAddSecretModal, setShowAddSecretModal] = useState(false) - const [selectedSecretToEdit, setSelectedSecretToEdit] = useState() const [selectedSecretToRemove, setSelectedSecretToRemove] = useState() + const [selectedSort, setSelectedSort] = useState('updated_at') const canManageSecrets = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') @@ -49,7 +56,7 @@ export const SecretsManagement = () => { if (selectedSort === 'updated_at') { return Number(new Date(s.updated_at)) } else { - return s[selectedSort] + return s[selectedSort as keyof VaultSecret] } } ) @@ -81,26 +88,21 @@ export const SecretsManagement = () => { : [] } /> -
- - + + + + <>Sort by {selectedSort} + + + + Updated at - - + + Name - - -
+ + +
@@ -134,11 +136,7 @@ export const SecretsManagement = () => { {secrets.map((secret, idx) => { return ( - + {idx !== secrets.length - 1 && } ) @@ -167,10 +165,6 @@ export const SecretsManagement = () => {
- setSelectedSecretToEdit(undefined)} - /> setSelectedSecretToRemove(undefined)} diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx new file mode 100644 index 00000000000..92fd59d67e1 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react' +import { describe, expect, it, beforeEach, vi } from 'vitest' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { screen, waitFor, fireEvent } from '@testing-library/dom' +import userEvent from '@testing-library/user-event' +import { render } from 'tests/helpers' +import { addAPIMock } from 'tests/lib/msw' +import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import EditSecretModal from '../EditSecretModal' +import { routerMock } from 'tests/lib/route-mock' + +const secret = { + id: '47ca58b4-01c5-4a71-8814-c73856b02e0e', + name: 'test', + description: 'new text', + secret: 'NASR0SoksURJ0OorMJ9FzraTzcqSWk5u1PQa2r4c3w9rUVc=', + created_at: '2025-07-13 12:57:47.62208+00', + updated_at: '2025-07-13 14:51:37.818223+00', +} + +const Page = ({ onClose }: { onClose: () => void }) => { + const [open, setOpen] = useState(false) + + return ( + + + + { + setOpen(false) + onClose() + }} + /> + + ) +} + +mockAnimationsApi() + +describe(`EditSecretModal`, () => { + beforeEach(() => { + // useSelectedProjectQuery -> useParams + routerMock.setCurrentUrl(`/project/default/integrations/vault/secrets`) + // 'http://localhost:3000/api/platform/projects/default' + addAPIMock({ + method: `get`, + path: `/platform/projects/:ref`, + // @ts-expect-error + response: { + cloud_provider: 'localhost', + id: 1, + inserted_at: '2021-08-02T06:40:40.646Z', + name: 'Default Project', + organization_id: 1, + ref: 'default', + region: 'local', + status: 'ACTIVE_HEALTHY', + }, + }) + // 'http://localhost:3000/api/platform/pg-meta/default/query?key=projects-default-secrets-47ca58b4-01c5-4a71-8814-c73856b02e0e' + // 'http://localhost:3000/api/platform/pg-meta/default/query?key=' + // useVaultSecretDecryptedValueQuery + useVaultSecretUpdateMutation + // both call the same endpoint but execute different SQL queries + addAPIMock({ + method: `post`, + path: `/platform/pg-meta/:ref/query`, + // @ts-expect-error this path erroneously has a `never` return type when it should be `unknown` since it executes a SQL query + response: [{ decrypted_secret: 'bar', update_secret: '' }], + }) + }) + + it(`renders a modal pre-filled with the secret's values`, async () => { + const onClose = vi.fn() + render() + + const openButton = screen.getByRole(`button`, { name: `Open` }) + await userEvent.click(openButton) + + await screen.findByRole(`dialog`) + + const nameInput = screen.getByLabelText(`Name`) + const descriptionInput = screen.getByLabelText(`Description`) + const valueInput = screen.getByLabelText(`Secret value`) + const togglePasswordButton = screen.getByRole(`button`, { name: `Show secret value` }) + const submitButton = screen.getByRole(`button`, { name: `Update secret` }) + + expect(nameInput).toHaveValue(secret.name) + expect(descriptionInput).toHaveValue(secret.description) + expect(valueInput).toHaveAttribute(`type`, `password`) + await userEvent.click(togglePasswordButton) + expect(valueInput).toHaveAttribute(`type`, `text`) + + await userEvent.type(nameInput, `updated-name`) + await userEvent.clear(descriptionInput) + await userEvent.type(valueInput, `qux`) + + fireEvent.click(submitButton) + + await waitFor(() => expect(onClose).toHaveBeenCalledOnce()) + }) +}) diff --git a/apps/studio/tests/lib/msw.ts b/apps/studio/tests/lib/msw.ts index 383df91d8c8..070b38ff798 100644 --- a/apps/studio/tests/lib/msw.ts +++ b/apps/studio/tests/lib/msw.ts @@ -44,11 +44,14 @@ export const addAPIMock =

, M> | HttpResponseResolver -}) => { +}: SuccessResponse, M> extends never + ? // Don't require a mocked response when the API doesn't return one + { method: M; path: P; response?: never } + : { + method: M + path: P + response: SuccessResponse, M> | HttpResponseResolver + }) => { const fullPath = `${API_URL}${path}` console.log('[MSW] Adding mock:', method.toUpperCase(), fullPath) diff --git a/apps/studio/tests/lib/route-mock.ts b/apps/studio/tests/lib/route-mock.ts index 9eeed08ce53..4dca8b21498 100644 --- a/apps/studio/tests/lib/route-mock.ts +++ b/apps/studio/tests/lib/route-mock.ts @@ -1,11 +1,26 @@ +import { glob } from 'fs/promises' import _routerMock from 'next-router-mock' import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes' +import { normalize, posix, sep, join } from 'path' export const routerMock = _routerMock -routerMock.useParser( - createDynamicRouteParser([ - // These paths should match those found in the `/pages` folder: - '/projects/[ref]', - ]) -) +const pipe = + (...fns: Array<(arg: T) => T>) => + (value: T) => + fns.reduce((acc, fn) => fn(acc), value) +// normalize file paths to posix format (windows FS support) +const toPosix = (str: string) => str.replaceAll(sep, posix.sep) +const pagesRoot = toPosix(join(process.cwd(), `./pages`)) +// remove the base filepath +const removePrefix = (str: string) => str.replace(pagesRoot, ``) +// remove the file extension or index segment +const removeExt = (str: string) => str.replace(/.tsx|\/index.tsx/, ``) + +// Glob the `pages/` directory for all dynamic route segments +const paths = [ + ...(await Array.fromAsync(glob(`${pagesRoot}/**/*\].tsx`))), + ...(await Array.fromAsync(glob(`${pagesRoot}/**/**\]/**/index.tsx`))), +].map(pipe(normalize, toPosix, removePrefix, removeExt)) + +routerMock.useParser(createDynamicRouteParser(paths)) diff --git a/apps/studio/tests/setup/polyfills.ts b/apps/studio/tests/setup/polyfills.ts index f8702bb7e71..e37e5482f5a 100644 --- a/apps/studio/tests/setup/polyfills.ts +++ b/apps/studio/tests/setup/polyfills.ts @@ -35,3 +35,5 @@ Object.defineProperties(globalThis, { ReadableStream: { value: ReadableStream }, TransformStream: { value: TransformStream }, }) + +window.HTMLElement.prototype.hasPointerCapture = vi.fn() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a940cf978c9..f22f313f01b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -537,7 +537,7 @@ importers: devDependencies: '@graphiql/toolkit': specifier: ^0.9.1 - version: 0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0) + version: 0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0) '@graphql-codegen/cli': specifier: 5.0.5 version: 5.0.5(@parcel/watcher@2.5.1)(@types/node@22.13.14)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.11.0))(graphql@16.11.0)(supports-color@8.1.1)(typescript@5.5.2) @@ -609,7 +609,7 @@ importers: version: 13.2.2 graphiql: specifier: ^4.0.2 - version: 4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -11203,6 +11203,7 @@ packages: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: eslint: '>= 5' + bundledDependencies: [] eslint-plugin-import@2.31.0: resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} @@ -20653,9 +20654,9 @@ snapshots: '@gar/promisify@1.1.3': {} - '@graphiql/plugin-doc-explorer@0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/plugin-doc-explorer@0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@headlessui/react': 2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.11.0 react: 18.3.1 @@ -20669,10 +20670,10 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/plugin-history@0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/plugin-history@0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0) + '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0) react: 18.3.1 react-compiler-runtime: 19.1.0-rc.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1) @@ -20712,9 +20713,9 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/react@0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/react@0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0) + '@graphiql/toolkit': 0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0) '@radix-ui/react-dialog': 1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': 2.1.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': 1.2.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -20741,13 +20742,13 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/toolkit@0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)': + '@graphiql/toolkit@0.11.2(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.11.0 meros: 1.3.0(@types/node@22.13.14) optionalDependencies: - graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.1) + graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.3) transitivePeerDependencies: - '@types/node' @@ -20761,13 +20762,13 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@graphiql/toolkit@0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)': + '@graphiql/toolkit@0.9.1(@types/node@22.13.14)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.11.0 meros: 1.3.0(@types/node@22.13.14) optionalDependencies: - graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.1) + graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.3) transitivePeerDependencies: - '@types/node' @@ -31234,11 +31235,11 @@ snapshots: graphemer@1.4.0: {} - graphiql@4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + graphiql@4.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@graphiql/plugin-doc-explorer': 0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@graphiql/plugin-history': 0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/plugin-doc-explorer': 0.0.1(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/plugin-history': 0.0.2(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 0.32.0(@codemirror/language@6.11.0)(@emotion/is-prop-valid@1.2.1)(@types/node@22.13.14)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3))(graphql@16.11.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.11.0 react: 18.3.1 react-compiler-runtime: 19.1.0-rc.1(react@18.3.1) @@ -31325,13 +31326,6 @@ snapshots: dependencies: graphql: 16.11.0 - graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.1): - dependencies: - graphql: 16.11.0 - optionalDependencies: - ws: 8.18.1 - optional: true - graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.3): dependencies: graphql: 16.11.0 From f28e6e9c8fadc92b2e3039b6f1988e807bd90fe1 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Sat, 9 Aug 2025 09:37:48 +1000 Subject: [PATCH 05/11] Check error before handling (#37801) check error before handling --- apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts index c3ad2cc47fd..c593efce4a7 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts @@ -106,7 +106,7 @@ export const getContextualInvalidationKeys = ({ } export const onErrorChat = (error: Error) => { - const parsedError = tryParseJson(error.message) + const parsedError = error ? tryParseJson(error.message) : undefined try { handleError(parsedError?.error || parsedError || error) From 1e10ee684449dd4ea81d07b93a71557ab81d59f1 Mon Sep 17 00:00:00 2001 From: Eduardo Gurgel Date: Sat, 9 Aug 2025 13:45:46 +1200 Subject: [PATCH 06/11] fix: specify that 3rd party JWTs must have kid header parameter (#37778) --- apps/docs/content/guides/auth/third-party/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/third-party/overview.mdx b/apps/docs/content/guides/auth/third-party/overview.mdx index 9698ececa01..77d950275da 100644 --- a/apps/docs/content/guides/auth/third-party/overview.mdx +++ b/apps/docs/content/guides/auth/third-party/overview.mdx @@ -26,7 +26,7 @@ This is made possible if the providers are using JWTs signed with asymmetric key There are some limitations you should be aware of when using third-party authentication providers with Supabase. -1. The third-party provider must use asymmetrically signed JWTs (exposed as an OIDC Issuer Discovery URL by the third-party authentication provider). Using symmetrically signed JWTs is not possible at this time. +1. The third-party provider must use asymmetrically signed JWTs (exposed as an OIDC Issuer Discovery URL by the third-party authentication provider). The signed JWTs must have a `kid` header parameter to identify which key must be used. Using symmetrically signed JWTs is not possible at this time. 2. The JWT signing keys from the third-party provider are stored in the configuration of your project, and are checked for changes periodically. If you are rotating your keys (when supported) allow up to 30 minutes for the change to be picked up. 3. It is not possible to disable Supabase Auth at this time. From adfc138450c98191ee539c6b9b3e18b87d28ba0b Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Sat, 9 Aug 2025 14:56:05 +1000 Subject: [PATCH 07/11] Remove error display temporarily (#37804) --- apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx | 5 ----- apps/studio/pages/api/ai/sql/generate-v4.ts | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 3f085b23f7d..4d4c0346ad1 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -218,7 +218,6 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { onFinish: handleChatFinish, }) - const formattedError = tryParseJson(error?.message ?? {}) const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming' const updateMessage = useCallback( @@ -446,10 +445,6 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { Sorry, I'm having trouble responding right now. If the error persists while retrying, you may try clearing the conversation's messages and try again.

- -

- Error: {formattedError.message} -

diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts index a50a11ddc39..4603683725d 100644 --- a/apps/studio/pages/api/ai/sql/generate-v4.ts +++ b/apps/studio/pages/api/ai/sql/generate-v4.ts @@ -83,7 +83,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { } // [Joshen] Am also filtering out any tool calls which state is "input-streaming" // this happens when a user stops the assistant response while the tool is being called - if (msg && msg.role === 'assistant') { + if (msg && msg.role === 'assistant' && msg.parts) { const cleanedParts = msg.parts.filter((part: any) => { return !(part.type.startsWith('tool-') && part.state === 'input-streaming') }) From c538c4dc6ed225595452bc5145cc62aad409102d Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 10 Aug 2025 11:01:48 -0500 Subject: [PATCH 08/11] database report: fix xlarge compute size max db connection in dashboard report (#37819) * fix xlarge compute size max db connection in dashboard report * pnpm format --- apps/studio/data/reports/database-charts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 135657f6c20..0efc542aedd 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -612,7 +612,7 @@ export const getConnectionLimits = (computeSize: string = 'medium') => { small: { direct: 90, pooler: 400 }, medium: { direct: 120, pooler: 600 }, large: { direct: 160, pooler: 800 }, - xl: { direct: 240, pooler: 1000 }, + xlarge: { direct: 240, pooler: 1000 }, '2xlarge': { direct: 380, pooler: 1500 }, '4xlarge': { direct: 480, pooler: 3000 }, '8xlarge': { direct: 490, pooler: 6000 }, From d196ee3fdd5fe629801815540c305fc48a773f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Mon, 11 Aug 2025 12:35:56 +0800 Subject: [PATCH 09/11] chore: disallow larger instance size launch (#37785) There is usually no good reason to immediately launch a project on a very large instance, unless it is fraudulent usage. To prevent easy fraud, we are not allowing to immediately launch >medium instances and just show a hint. --- apps/studio/pages/new/[slug].tsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 84301d2040a..311c60f67c9 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -90,18 +90,7 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' -const sizes: DesiredInstanceSize[] = [ - 'micro', - 'small', - 'medium', - 'large', - 'xlarge', - '2xlarge', - '4xlarge', - '8xlarge', - '12xlarge', - '16xlarge', -] +const sizes: DesiredInstanceSize[] = ['micro', 'small', 'medium'] const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small'] @@ -783,7 +772,7 @@ const Wizard: NextPageWithLayout = () => { CPU

${instanceSizeSpecs[option].priceHourly}/hour (~$ @@ -794,6 +783,15 @@ const Wizard: NextPageWithLayout = () => { ) })} + +

+ Larger instance sizes available after creation +
+ From a897cc27f0c5114b733181038ff9cf94376c7d40 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 11 Aug 2025 11:53:15 +0700 Subject: [PATCH 10/11] Part 1 of swapping useCheckPermissions with useAsyncCheckProjectPermissions (#37751) * Part 1 of swapping useCheckPermissions with useAsyncCheckProjectPermissions * Update apps/studio/hooks/misc/useCheckPermissions.ts Co-authored-by: Drake Costa * Address feedback --------- Co-authored-by: Drake Costa --- .../grid/components/header/Header.tsx | 8 +- .../interfaces/APIKeys/APIKeyDeleteDialog.tsx | 28 +- .../APIKeys/ApiKeysIllustrations.tsx | 6 +- .../interfaces/APIKeys/PublishableAPIKeys.tsx | 34 +- .../interfaces/APIKeys/SecretAPIKeys.tsx | 16 +- .../APIKeys/hooks/useApiKeysVisibility.ts | 14 +- .../Auth/AdvancedAuthSettingsForm.tsx | 19 +- .../AuthProvidersForm/AuthProvidersForm.tsx | 6 +- .../Auth/AuthProvidersForm/ProviderForm.tsx | 11 +- .../BasicAuthSettingsForm.tsx | 417 ++++++++++-------- .../Auth/EmailTemplates/TemplateEditor.tsx | 11 +- .../interfaces/Auth/Hooks/AddHookDropdown.tsx | 7 +- .../interfaces/Auth/Hooks/HookCard.tsx | 11 +- .../MfaAuthSettingsForm.tsx | 12 +- .../PolicyEditorPanel/PolicyDetailsV2.tsx | 9 +- .../Auth/Policies/PolicyEditorPanel/index.tsx | 7 +- .../Policies/PolicyTableRow/PolicyRow.tsx | 7 +- .../PolicyTableRow/PolicyTableRowHeader.tsx | 12 +- .../ProtectionAuthSettingsForm.tsx | 55 +-- .../interfaces/Auth/RateLimits/RateLimits.tsx | 12 +- .../Auth/RedirectUrls/RedirectUrlList.tsx | 7 +- .../SessionsAuthSettingsForm.tsx | 12 +- .../interfaces/Auth/SiteUrl/SiteUrl.tsx | 13 +- .../interfaces/Auth/SmtpForm/SmtpForm.tsx | 20 +- .../Auth/ThirdPartyAuthForm/index.tsx | 7 +- .../interfaces/Auth/Users/AddUserDropdown.tsx | 12 +- .../interfaces/Auth/Users/CreateUserModal.tsx | 7 +- .../interfaces/Auth/Users/InviteUserModal.tsx | 7 +- .../interfaces/Auth/Users/UserOverview.tsx | 26 +- .../create-key-dialog.tsx | 3 - .../TableGridEditor/GridHeaderActions.tsx | 12 +- .../components/layouts/Tabs/RecentItems.tsx | 3 - .../ui/ProjectSettings/DisplayApiSettings.tsx | 38 +- .../DisplayApiSettings.utils.ts | 5 - apps/studio/hooks/misc/useCheckPermissions.ts | 5 +- apps/studio/pages/_app.tsx | 2 + .../pages/project/[ref]/auth/providers.tsx | 11 - .../project/[ref]/settings/api-keys/new.tsx | 4 +- 38 files changed, 486 insertions(+), 410 deletions(-) diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 0985c79f2e8..2b2429cd333 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -15,7 +15,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' import { fetchAllTableRows, useTableRowsQuery } from 'data/table-rows/table-rows-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { RoleImpersonationState } from 'lib/role-impersonation' @@ -84,8 +84,10 @@ const DefaultHeader = () => { const snap = useTableEditorTableStateSnapshot() const tableEditorSnap = useTableEditorStateSnapshot() - const canCreateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') - + const { can: canCreateColumns } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'columns' + ) const { mutate: sendEvent } = useSendEventMutation() const onAddRow = diff --git a/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx b/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx index 90ed55186ba..f646d502468 100644 --- a/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx +++ b/apps/studio/components/interfaces/APIKeys/APIKeyDeleteDialog.tsx @@ -1,13 +1,13 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { Trash2 } from 'lucide-react' import { useState } from 'react' import { toast } from 'sonner' -import { Trash2 } from 'lucide-react' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common/hooks' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import { useAPIKeyDeleteMutation } from 'data/api-keys/api-key-delete-mutation' import { APIKeysData } from 'data/api-keys/api-keys-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { DropdownMenuItem } from 'ui' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' interface APIKeyDeleteDialogProps { @@ -19,7 +19,10 @@ export const APIKeyDeleteDialog = ({ apiKey, lastSeen }: APIKeyDeleteDialogProps const { ref: projectRef } = useParams() const [isOpen, setIsOpen] = useState(false) - const canDeleteAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*') + const { can: canDeleteAPIKeys } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + '*' + ) const { mutate: deleteAPIKey, isLoading: isDeletingAPIKey } = useAPIKeyDeleteMutation({ onSuccess: () => { @@ -35,17 +38,26 @@ export const APIKeyDeleteDialog = ({ apiKey, lastSeen }: APIKeyDeleteDialogProps return ( <> - { if (canDeleteAPIKeys) { e.preventDefault() setIsOpen(true) } }} + disabled={!canDeleteAPIKeys} + tooltip={{ + content: { + side: 'left', + text: !canDeleteAPIKeys + ? 'You need additional permissions to delete API keys' + : undefined, + }, + }} > Delete API key - + setIsOpen(false)} diff --git a/apps/studio/components/interfaces/APIKeys/ApiKeysIllustrations.tsx b/apps/studio/components/interfaces/APIKeys/ApiKeysIllustrations.tsx index c60ecf73031..ff5bbd74c87 100644 --- a/apps/studio/components/interfaces/APIKeys/ApiKeysIllustrations.tsx +++ b/apps/studio/components/interfaces/APIKeys/ApiKeysIllustrations.tsx @@ -160,12 +160,10 @@ export const ApiKeysCreateCallout = () => { * Feedback banner for users who have API keys and the feature is rolled out to them */ export const ApiKeysFeedbackBanner = () => { - const { hasApiKeys, isInRollout } = useApiKeysVisibility() + const { hasApiKeys } = useApiKeysVisibility() // Don't show anything if not in rollout or if keys don't exist - if (!isInRollout || !hasApiKeys) { - return null - } + if (!hasApiKeys) return null return ( { [apiKeysData] ) - const isPermissionsLoading = !usePermissionsLoaded() - const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*') + const { can: canReadAPIKeys, isLoading: isPermissionsLoading } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + '*' + ) // The default publisahble key will always be the first one const apiKey = publishableApiKeys[0] @@ -46,12 +48,12 @@ export const PublishableAPIKeys = () => {
@@ -65,31 +67,29 @@ export const PublishableAPIKeys = () => { )} -
- {/* */} - {/* @mildtomato - To add in later with follow up PR */} - {/* */} -
- - {/* */} ) } -function ApiKeyInput() { +const ApiKeyInput = () => { const { ref: projectRef } = useParams() + const { data: apiKeysData, isLoading: isApiKeysLoading, error, } = useAPIKeysQuery({ projectRef, reveal: false }) + const publishableApiKeys = useMemo( () => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [], [apiKeysData] ) - const isPermissionsLoading = !usePermissionsLoaded() - const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*') + + const { can: canReadAPIKeys, isLoading: isPermissionsLoading } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + '*' + ) // The default publisahble key will always be the first one const apiKey = publishableApiKeys[0] diff --git a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx index 5f6c6e1b83f..273b0cb6e64 100644 --- a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx +++ b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx @@ -1,15 +1,12 @@ -import dayjs from 'dayjs' -import duration from 'dayjs/plugin/duration' -import relativeTime from 'dayjs/plugin/relativeTime' - import { PermissionAction } from '@supabase/shared-types/out/constants' +import dayjs from 'dayjs' import { useMemo, useRef } from 'react' import { useParams } from 'common' import { FormHeader } from 'components/ui/Forms/FormHeader' import { APIKeysData, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import useLogsQuery from 'hooks/analytics/useLogsQuery' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Card, CardContent, EyeOffIcon, Skeleton, WarningIcon, cn } from 'ui' import { Table, @@ -22,9 +19,6 @@ import { import { APIKeyRow } from './APIKeyRow' import CreateSecretAPIKeyDialog from './CreateSecretAPIKeyDialog' -dayjs.extend(duration) -dayjs.extend(relativeTime) - interface LastSeenData { [hash: string]: { timestamp: string } } @@ -62,8 +56,10 @@ export const SecretAPIKeys = () => { error, } = useAPIKeysQuery({ projectRef, reveal: false }) - const isLoadingPermissions = !usePermissionsLoaded() - const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*') + const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + '*' + ) const lastSeen = useLastSeen(projectRef!) diff --git a/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts b/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts index caf6c85b010..f2b602522ef 100644 --- a/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts +++ b/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts @@ -1,13 +1,11 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { useMemo } from 'react' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useFlag } from 'hooks/ui/useFlag' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' interface ApiKeysVisibilityState { - isInRollout: boolean hasApiKeys: boolean isLoading: boolean canReadAPIKeys: boolean @@ -21,8 +19,7 @@ interface ApiKeysVisibilityState { */ export function useApiKeysVisibility(): ApiKeysVisibilityState { const { ref: projectRef } = useParams() - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'api_keys') - const isBasicApiKeysEnabled = useFlag('basicApiKeys') + const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions(PermissionAction.READ, 'api_keys') const { data: apiKeysData, isLoading } = useAPIKeysQuery({ projectRef, @@ -39,13 +36,12 @@ export function useApiKeysVisibility(): ApiKeysVisibilityState { const hasApiKeys = publishableApiKeys.length > 0 // Can initialize API keys when in rollout, has permissions, not loading, and no API keys yet - const canInitApiKeys = isBasicApiKeysEnabled && canReadAPIKeys && !isLoading && !hasApiKeys + const canInitApiKeys = canReadAPIKeys && !isLoading && !hasApiKeys // Disable UI for publishable keys and secrets keys if flag is not enabled OR no API keys created yet - const shouldDisableUI = !isBasicApiKeysEnabled || !hasApiKeys + const shouldDisableUI = !hasApiKeys return { - isInRollout: isBasicApiKeysEnabled, hasApiKeys, isLoading, canReadAPIKeys, diff --git a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx index 65357542272..9f498c43c16 100644 --- a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx @@ -12,7 +12,7 @@ import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { @@ -43,18 +43,19 @@ const FormSchema = z.object({ export const AdvancedAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canReadConfig } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const [isUpdatingRequestDurationForm, setIsUpdatingRequestDurationForm] = useState(false) const [isUpdatingDatabaseForm, setIsUpdatingDatabaseForm] = useState(false) - const { - data: authConfig, - error: authConfigError, - isLoading, - isError, - } = useAuthConfigQuery({ projectRef }) + const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx index a5b9834684c..1b118c25af6 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.tsx @@ -24,10 +24,10 @@ import { ProviderForm } from './ProviderForm' export const AuthProvidersForm = () => { const { ref: projectRef } = useParams() const { - isLoading, - error: authConfigError, - isError, data: authConfig, + error: authConfigError, + isLoading, + isError, isSuccess, } = useAuthConfigQuery({ projectRef }) diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index c1fcb131674..80284a6dec9 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -1,5 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Check } from 'lucide-react' +import { useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import ReactMarkdown from 'react-markdown' import { toast } from 'sonner' @@ -13,9 +14,8 @@ import type { components } from 'data/api' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' -import { useQueryState } from 'nuqs' import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui' import { Admonition } from 'ui-patterns' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' @@ -23,12 +23,14 @@ import { AuthAlert } from './AuthAlert' import type { Provider } from './AuthProvidersForm.types' import FormField from './FormField' -export interface ProviderFormProps { +interface ProviderFormProps { config: components['schemas']['GoTrueConfigResponse'] provider: Provider isActive: boolean } +const doubleNegativeKeys = ['SMS_AUTOCONFIRM'] + export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) => { const { ref: projectRef } = useParams() const [urlProvider, setUrlProvider] = useQueryState('provider', { defaultValue: '' }) @@ -36,8 +38,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) const [open, setOpen] = useState(false) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() - const doubleNegativeKeys = ['SMS_AUTOCONFIRM'] - const canUpdateConfig: boolean = useCheckPermissions( + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'custom_config_gotrue' ) diff --git a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx index 8961d29c60a..4409bc24e54 100644 --- a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx @@ -13,7 +13,7 @@ import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -29,6 +29,7 @@ import { WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' const schema = object({ @@ -41,11 +42,23 @@ const schema = object({ const BasicAuthSettingsForm = () => { const { ref: projectRef } = useParams() - const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + error: authConfigError, + isError, + isSuccess, + isLoading, + } = useAuthConfigQuery({ projectRef }) const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canReadConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const form = useForm({ resolver: yupResolver(schema), @@ -95,207 +108,223 @@ const BasicAuthSettingsForm = () => { ) } - if (isError) { - return ( - - - Failed to retrieve auth configuration - {authConfigError.message} - - ) - } - - if (!canReadConfig) { - return ( -
- -
- ) - } - return ( User Signups - -
- - - ( - - - - - - )} - /> - - - ( - - Enable{' '} - - manual linking APIs - {' '} - for your project - - } - > - - - - - )} - /> - - - ( - - Enable{' '} - - anonymous sign-ins - {' '} - for your project - - } - > - - - - - )} - /> + {isError && ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + )} - {form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( - - -
- - Anonymous users will use the authenticated{' '} - role when signing in - - -

- As a result, anonymous users will be subjected to RLS policies that apply to - the public and{' '} - authenticated roles. We strongly advise{' '} - - reviewing your RLS policies - {' '} - to ensure that access to your data is restricted where required. -

- -
-
-
- )} + {isPermissionsLoaded && !canReadConfig && ( +
+ +
+ )} - {!authConfig?.SECURITY_CAPTCHA_ENABLED && - form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( - + {isLoading && ( + + + + + + + + + + + + + + + + )} + + {isSuccess && ( + + + + + ( + + + + + + )} + /> + + + ( + + Enable{' '} + + manual linking APIs + {' '} + for your project + + } + > + + + + + )} + /> + + + ( + + Enable{' '} + + anonymous sign-ins + {' '} + for your project + + } + > + + + + + )} + /> + + {form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( + - - We highly recommend{' '} - - enabling captcha - {' '} - for anonymous sign-ins - - - This will prevent potential abuse on sign-ins which may bloat your database - and incur costs for monthly active users (MAU) - +
+ + Anonymous users will use the authenticated{' '} + role when signing in + + +

+ As a result, anonymous users will be subjected to RLS policies that apply + to the public and{' '} + authenticated roles. We strongly advise{' '} + + reviewing your RLS policies + {' '} + to ensure that access to your data is restricted where required. +

+ +
+
)} -
- - ( - - - - - + + {!authConfig?.SECURITY_CAPTCHA_ENABLED && + form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( + + + + We highly recommend{' '} + + enabling captcha + {' '} + for anonymous sign-ins + + + This will prevent potential abuse on sign-ins which may bloat your database + and incur costs for monthly active users (MAU) + + + )} + + + ( + + + + + + )} + /> + + + {form.formState.isDirty && ( + )} - /> -
- - {form.formState.isDirty && ( - - )} - - -
-
-
+ + + + + )}
) } diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index e1448112fb5..ae576925a30 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -2,21 +2,19 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Code, Monitor } from 'lucide-react' import { editor } from 'monaco-editor' import { useEffect, useMemo, useRef, useState } from 'react' +import { useForm } from 'react-hook-form' import ReactMarkdown from 'react-markdown' import { toast } from 'sonner' -import { useForm } from 'react-hook-form' import { useParams } from 'common' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useValidateSpamMutation, ValidateSpamResponse } from 'data/auth/validate-spam-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { FormSchema } from 'types' import { - Badge, Button, - Card, CardContent, CardFooter, Form_Shadcn_, @@ -42,7 +40,10 @@ interface TemplateEditorProps { const TemplateEditor = ({ template }: TemplateEditorProps) => { const { ref: projectRef } = useParams() - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) // Add a ref to the code editor const editorRef = useRef() diff --git a/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx b/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx index 87ca544e5c4..0cb29864869 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx @@ -4,7 +4,7 @@ import { ChevronDown } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button, @@ -33,7 +33,10 @@ export const AddHookDropdown = ({ const { data: organization } = useSelectedOrganizationQuery() const { data: authConfig } = useAuthConfigQuery({ projectRef }) - const canUpdateAuthHook = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*') + const { can: canUpdateAuthHook } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + '*' + ) const hooks: Hook[] = HOOKS_DEFINITIONS.map((definition) => { return { diff --git a/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx b/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx index d626ef3efee..cf5b65112e0 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/HookCard.tsx @@ -1,11 +1,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Check, Webhook } from 'lucide-react' -import { Badge, Input, copyToClipboard } from 'ui' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Hook } from './hooks.constants' import { DocsButton } from 'components/ui/DocsButton' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { Badge, Input, copyToClipboard } from 'ui' +import { Hook } from './hooks.constants' interface HookCardProps { hook: Hook @@ -13,7 +13,10 @@ interface HookCardProps { } export const HookCard = ({ hook, onSelect }: HookCardProps) => { - const canUpdateAuthHook = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*') + const { can: canUpdateAuthHook } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + '*' + ) return (
diff --git a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx index 4a00780db53..90a1c353c49 100644 --- a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx @@ -11,7 +11,7 @@ import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { @@ -87,8 +87,14 @@ const MfaAuthSettingsForm = () => { const [isUpdatingTotpForm, setIsUpdatingTotpForm] = useState(false) const [isUpdatingPhoneForm, setIsUpdatingPhoneForm] = useState(false) - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canReadConfig } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const { data: organization } = useSelectedOrganizationQuery() const isProPlanAndUp = organization?.plan?.id !== 'free' diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx index 259f5e7fd01..57a93403303 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx @@ -1,11 +1,11 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { Check, ChevronsUpDown } from 'lucide-react' import { useEffect, useState } from 'react' import { UseFormReturn } from 'react-hook-form' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useTablesQuery } from 'data/tables/tables-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, @@ -62,7 +62,10 @@ export const PolicyDetailsV2 = ({ }: PolicyDetailsV2Props) => { const { data: project } = useSelectedProjectQuery() const [open, setOpen] = useState(false) - const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canUpdatePolicies } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) const { data: tables, isSuccess: isSuccessTables } = useTablesQuery({ projectRef: project?.ref, diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx index da7f29f264c..ac3c2e99e38 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx @@ -15,7 +15,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation' import { databasePoliciesKeys } from 'data/database-policies/keys' import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, @@ -67,7 +67,10 @@ export const PolicyEditorPanel = memo(function ({ const queryClient = useQueryClient() const { data: selectedProject } = useSelectedProjectQuery() - const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canUpdatePolicies } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) // [Joshen] Hyrid form fields, just spit balling to get a decent POC out const [using, setUsing] = useState('') diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index 2bb0216f800..c58cc46fa17 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -6,7 +6,7 @@ import { Edit, MoreVertical, Trash } from 'lucide-react' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import Panel from 'components/ui/Panel' import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { @@ -38,7 +38,10 @@ const PolicyRow = ({ onSelectDeletePolicy = noop, }: PolicyRowProps) => { const aiSnap = useAiAssistantStateSnapshot() - const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies') + const { can: canUpdatePolicies } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'policies' + ) const { data: project } = useSelectedProjectQuery() const { data: authConfig } = useAuthConfigQuery({ projectRef: project?.ref }) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index c668c505a3d..6858191349a 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -5,7 +5,7 @@ import { Lock, Unlock } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { AiIconAnimation, Badge } from 'ui' @@ -35,8 +35,14 @@ const PolicyTableRowHeader = ({ const { ref } = useParams() const aiSnap = useAiAssistantStateSnapshot() - const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies') - const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canCreatePolicies } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'policies' + ) + const { can: canToggleRLS } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) const isRealtimeSchema = table.schema === 'realtime' const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages' diff --git a/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx index 98370bad538..8f9dae46e29 100644 --- a/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx @@ -12,7 +12,7 @@ import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -69,18 +69,25 @@ const schema = object({ const ProtectionAuthSettingsForm = () => { const { ref: projectRef } = useParams() - const { - data: authConfig, - error: authConfigError, - isLoading, - isError, - } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() - const [isUpdatingProtection, setIsUpdatingProtection] = useState(false) + const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({ + onError: (error) => { + toast.error(`Failed to update settings: ${error?.message}`) + }, + onSuccess: () => { + toast.success('Successfully updated settings') + }, + }) const [hidden, setHidden] = useState(true) - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canReadConfig } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const protectionForm = useForm({ resolver: yupResolver(schema), @@ -102,7 +109,7 @@ const ProtectionAuthSettingsForm = () => { }) useEffect(() => { - if (authConfig && !isUpdatingProtection) { + if (authConfig && !isUpdatingConfig) { protectionForm.reset({ DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP, EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED || false, @@ -120,11 +127,9 @@ const ProtectionAuthSettingsForm = () => { PASSWORD_HIBP_ENABLED: authConfig.PASSWORD_HIBP_ENABLED || false, }) } - }, [authConfig, isUpdatingProtection]) + }, [authConfig, isUpdatingConfig]) const onSubmitProtection = (values: any) => { - setIsUpdatingProtection(true) - const payload = { ...values } payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP // The backend uses empty string to represent no required characters in the password @@ -132,19 +137,7 @@ const ProtectionAuthSettingsForm = () => { payload.PASSWORD_REQUIRED_CHARACTERS = '' } - updateAuthConfig( - { projectRef: projectRef!, config: payload }, - { - onError: (error) => { - toast.error(`Failed to update settings: ${error?.message}`) - setIsUpdatingProtection(false) - }, - onSuccess: () => { - toast.success('Successfully updated settings') - setIsUpdatingProtection(false) - }, - } - ) + updateAuthConfig({ projectRef: projectRef!, config: payload }) } if (isError) { @@ -282,10 +275,8 @@ const ProtectionAuthSettingsForm = () => { diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index 30befae90b5..7b8ea69e7c1 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -13,7 +13,7 @@ import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Card, @@ -32,8 +32,14 @@ import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils' const RateLimits = () => { const { ref: projectRef } = useParams() - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) + const { can: canReadConfig } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) const { data: authConfig, diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx index 304cac3af4d..fdcf729a56a 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx @@ -3,7 +3,7 @@ import { Globe, Trash } from 'lucide-react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { EmptyListState } from 'components/ui/States' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Checkbox_Shadcn_ } from 'ui' import { ValueContainer } from './ValueContainer' @@ -24,7 +24,10 @@ export const RedirectUrlList = ({ onSelectRemoveURLs, onSelectClearSelection, }: RedirectUrlListProps) => { - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) // [Joshen] One for next time: maybe shift this into a reusable logic since it // seems like we can use this in multiple places for future diff --git a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx index 5f9f9d892d6..dfb4b224731 100644 --- a/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx @@ -11,7 +11,7 @@ import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { @@ -68,8 +68,14 @@ const SessionsAuthSettingsForm = () => { const [isUpdatingRefreshTokens, setIsUpdatingRefreshTokens] = useState(false) const [isUpdatingUserSessions, setIsUpdatingUserSessions] = useState(false) - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canReadConfig } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const { data: organization } = useSelectedOrganizationQuery() const isProPlanAndUp = organization?.plan?.id !== 'free' diff --git a/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx b/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx index e9e785b9c19..fd8a8a5b8d7 100644 --- a/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx +++ b/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx @@ -1,16 +1,16 @@ +import { yupResolver } from '@hookform/resolvers/yup' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { AlertCircle } from 'lucide-react' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { object, string } from 'yup' -import { yupResolver } from '@hookform/resolvers/yup' import { useParams } from 'common' import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -24,7 +24,7 @@ import { Form_Shadcn_, Input_Shadcn_, } from 'ui' -import { AlertCircle } from 'lucide-react' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' const schema = object({ SITE_URL: string().required('Must have a Site URL'), @@ -36,7 +36,10 @@ const SiteUrl = () => { const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() const [isUpdatingSiteUrl, setIsUpdatingSiteUrl] = useState(false) - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const siteUrlForm = useForm({ resolver: yupResolver(schema), diff --git a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx index e966cd43b4e..4acf7b5cd90 100644 --- a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx +++ b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx @@ -1,18 +1,18 @@ import { yupResolver } from '@hookform/resolvers/yup' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' +import { AlertTriangle, Eye, EyeOff } from 'lucide-react' +import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import * as yup from 'yup' +import { useParams } from 'common' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { AlertTriangle, Eye, EyeOff } from 'lucide-react' -import Link from 'next/link' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -30,7 +30,7 @@ import { WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { urlRegex } from './../Auth.constants' +import { urlRegex } from '../Auth.constants' import { defaultDisabledSmtpFormValues } from './SmtpForm.constants' import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils' @@ -53,8 +53,14 @@ const SmtpForm = () => { const [enableSmtp, setEnableSmtp] = useState(false) const [hidden, setHidden] = useState(true) - const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canReadConfig } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'custom_config_gotrue' + ) + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const smtpSchema = yup.object({ SMTP_ADMIN_EMAIL: yup.string().when('ENABLE_SMTP', { diff --git a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx index fdf8cf905b6..456bf706e53 100644 --- a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx +++ b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx @@ -17,7 +17,7 @@ import { ThirdPartyAuthIntegration, useThirdPartyAuthIntegrationsQuery, } from 'data/third-party-auth/integrations-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { cn } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { AddIntegrationDropdown } from './AddIntegrationDropdown' @@ -49,7 +49,10 @@ export const ThirdPartyAuthForm = () => { useState() const { mutateAsync: deleteIntegration } = useDeleteThirdPartyAuthIntegrationMutation() - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) if (isError) { return ( diff --git a/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx b/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx index d602f9fe963..4ef7f008d72 100644 --- a/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx +++ b/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx @@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { ChevronDown, Mail, UserPlus } from 'lucide-react' import { useState } from 'react' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, DropdownMenu, @@ -17,8 +17,14 @@ import CreateUserModal from './CreateUserModal' import InviteUserModal from './InviteUserModal' const AddUserDropdown = () => { - const canInviteUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user') - const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user') + const { can: canInviteUsers } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'invite_user' + ) + const { can: canCreateUsers } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'create_user' + ) const [inviteVisible, setInviteVisible] = useState(false) const [createVisible, setCreateVisible] = useState(false) diff --git a/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx index 63c4b904996..f2b630c3698 100644 --- a/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/CreateUserModal.tsx @@ -7,7 +7,7 @@ import * as z from 'zod' import { useParams } from 'common' import { useUserCreateMutation } from 'data/auth/user-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Checkbox_Shadcn_, @@ -38,7 +38,10 @@ const CreateUserFormSchema = z.object({ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => { const { ref: projectRef } = useParams() - const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user') + const { can: canCreateUsers } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'create_user' + ) const { mutate: createUser, isLoading: isCreatingUser } = useUserCreateMutation({ onSuccess(res) { diff --git a/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx index 59f7d064d3b..d2565745205 100644 --- a/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx @@ -4,7 +4,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import { useUserInviteMutation } from 'data/auth/user-invite-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Form, Input, Modal } from 'ui' export type InviteUserModalProps = { @@ -22,7 +22,10 @@ const InviteUserModal = ({ visible, setVisible }: InviteUserModalProps) => { setVisible(false) }, }) - const canInviteUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user') + const { can: canInviteUsers } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'invite_user' + ) const validate = (values: any) => { const errors: any = {} diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 0cb86332ff9..6a83f3c58fc 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -15,7 +15,7 @@ import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mut import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation' import { useUserUpdateMutation } from 'data/auth/user-update-mutation' import { User } from 'data/auth/users-infinite-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { timeout } from 'lib/helpers' import { Button, cn, Separator } from 'ui' @@ -59,12 +59,24 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { } ) - const canUpdateUser = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*') - const canSendMagicLink = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_magic_link') - const canSendRecovery = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_recovery') - const canSendOtp = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_otp') - const canRemoveUser = useCheckPermissions(PermissionAction.TENANT_SQL_DELETE, 'auth.users') - const canRemoveMFAFactors = useCheckPermissions( + const { can: canUpdateUser } = useAsyncCheckProjectPermissions(PermissionAction.AUTH_EXECUTE, '*') + const { can: canSendMagicLink } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'send_magic_link' + ) + const { can: canSendRecovery } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'send_recovery' + ) + const { can: canSendOtp } = useAsyncCheckProjectPermissions( + PermissionAction.AUTH_EXECUTE, + 'send_otp' + ) + const { can: canRemoveUser } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_DELETE, + 'auth.users' + ) + const { can: canRemoveMFAFactors } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_DELETE, 'auth.mfa_factors' ) diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx index 639fbec9886..a3151640a4c 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx @@ -1,5 +1,4 @@ import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' import { useMemo, useState } from 'react' import { toast } from 'sonner' @@ -24,8 +23,6 @@ import { Textarea, } from 'ui' -dayjs.extend(relativeTime) - const RSA_JWK_REQUIRED_PROPERTIES = ['kty', 'n', 'e', 'p', 'q', 'd', 'dq', 'dp', 'qi'] const EC_JWK_REQUIRED_PROPERTIES = ['kty', 'crv', 'x', 'y', 'd'] const ALLOWED_JWK_PROPERTIES = new Set([ diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index e9bd8a54ec0..f43ac43e38a 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -21,7 +21,7 @@ import { } from 'data/table-editor/table-editor-types' import { useTableUpdateMutation } from 'data/tables/table-update-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -107,9 +107,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { }, }) - const canSqlWriteTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') - const canSqlWriteColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') - const isReadOnly = !canSqlWriteTables && !canSqlWriteColumns + const { can: canSqlWriteTables, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canSqlWriteColumns } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'columns' + ) + const isReadOnly = !isLoadingPermissions && !canSqlWriteTables && !canSqlWriteColumns // This will change when we allow autogenerated API docs for schemas other than `public` const doesHaveAutoGeneratedAPIDocs = table.schema === 'public' diff --git a/apps/studio/components/layouts/Tabs/RecentItems.tsx b/apps/studio/components/layouts/Tabs/RecentItems.tsx index bc684c59709..c7dd4276bc9 100644 --- a/apps/studio/components/layouts/Tabs/RecentItems.tsx +++ b/apps/studio/components/layouts/Tabs/RecentItems.tsx @@ -1,5 +1,4 @@ import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' import { AnimatePresence, motion } from 'framer-motion' import Link from 'next/link' @@ -9,8 +8,6 @@ import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { editorEntityTypes, useTabsStateSnapshot } from 'state/tabs' import { useEditorType } from '../editors/EditorsLayout.hooks' -dayjs.extend(relativeTime) - export function RecentItems() { const { ref } = useParams() const tabs = useTabsStateSnapshot() diff --git a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx index 4072f6f2caa..81cefd8d75a 100644 --- a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx +++ b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx @@ -1,6 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events' import { AlertCircle, Loader2 } from 'lucide-react' +import Link from 'next/link' import { useMemo } from 'react' import { toast } from 'sonner' @@ -9,8 +10,6 @@ import Panel from 'components/ui/Panel' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' -import { useFlag } from 'hooks/ui/useFlag' -import Link from 'next/link' import { Input } from 'ui' import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils' import { ToggleLegacyApiKeysPanel } from './ToggleLegacyApiKeys' @@ -26,8 +25,6 @@ export const DisplayApiSettings = ({ }) => { const { ref: projectRef } = useParams() - const newApiKeysInRollOut = useFlag('basicApiKeys') - const { data: settings, isError: isProjectSettingsError, @@ -217,31 +214,20 @@ export const DisplayApiSettings = ({ )) )} {showNotice ? ( - newApiKeysInRollOut ? ( - - ) : ( - - ) - ) : null} + href="https://github.com/orgs/supabase/discussions/29260" + buttonText="Read the announcement" + /> + ) : ( + + )} - {newApiKeysInRollOut && !showNotice && } ) } diff --git a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.utils.ts b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.utils.ts index b8bd899e9bb..ae9fc2d3b8c 100644 --- a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.utils.ts +++ b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.utils.ts @@ -1,13 +1,8 @@ import dayjs from 'dayjs' -import duration from 'dayjs/plugin/duration' -import relativeTime from 'dayjs/plugin/relativeTime' import { useRef } from 'react' import useLogsQuery from 'hooks/analytics/useLogsQuery' -dayjs.extend(duration) -dayjs.extend(relativeTime) - export function useLastUsedAPIKeysLogQuery(projectRef: string) { const now = useRef(new Date()).current return useLogsQuery(projectRef, { diff --git a/apps/studio/hooks/misc/useCheckPermissions.ts b/apps/studio/hooks/misc/useCheckPermissions.ts index b77782fbda5..9ebf33696fb 100644 --- a/apps/studio/hooks/misc/useCheckPermissions.ts +++ b/apps/studio/hooks/misc/useCheckPermissions.ts @@ -134,7 +134,10 @@ export function useGetProjectPermissions( } /** - * @deprecated Use useAsyncCheckProjectPermissions instead + * @deprecated If checking for project permissions, use useAsyncCheckProjectPermissions instead so that we can always + * check for loading states to not prematurely show "no perms" UIs. We'll also need a separate async check for org perms too + * + * Use `import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'` instead */ export function useCheckPermissions( action: string, diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index b062e684053..edeb124fc6f 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -24,6 +24,7 @@ import { Hydrate, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' +import duration from 'dayjs/plugin/duration' import relativeTime from 'dayjs/plugin/relativeTime' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' @@ -56,6 +57,7 @@ dayjs.extend(customParseFormat) dayjs.extend(utc) dayjs.extend(timezone) dayjs.extend(relativeTime) +dayjs.extend(duration) loader.config({ // [Joshen] Attempt for offline support/bypass ISP issues is to store the assets required for monaco diff --git a/apps/studio/pages/project/[ref]/auth/providers.tsx b/apps/studio/pages/project/[ref]/auth/providers.tsx index 57840283ae4..2bdc9ab9584 100644 --- a/apps/studio/pages/project/[ref]/auth/providers.tsx +++ b/apps/studio/pages/project/[ref]/auth/providers.tsx @@ -1,21 +1,10 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' - import { AuthProvidersForm, BasicAuthSettingsForm } from 'components/interfaces/Auth' import { AuthProvidersLayout } from 'components/layouts/AuthLayout/AuthProvidersLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer } from 'components/layouts/Scaffold' -import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' const ProvidersPage: NextPageWithLayout = () => { - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const isPermissionsLoaded = usePermissionsLoaded() - - if (isPermissionsLoaded && !canReadAuthSettings) { - return - } - return ( diff --git a/apps/studio/pages/project/[ref]/settings/api-keys/new.tsx b/apps/studio/pages/project/[ref]/settings/api-keys/new.tsx index 9ceadc7cba2..99e475e2c9d 100644 --- a/apps/studio/pages/project/[ref]/settings/api-keys/new.tsx +++ b/apps/studio/pages/project/[ref]/settings/api-keys/new.tsx @@ -1,5 +1,4 @@ import { - ApiKeysComingSoonBanner, ApiKeysCreateCallout, ApiKeysFeedbackBanner, } from 'components/interfaces/APIKeys/ApiKeysIllustrations' @@ -14,11 +13,10 @@ import type { NextPageWithLayout } from 'types' import { Separator } from 'ui' const ApiKeysNewPage: NextPageWithLayout = () => { - const { isInRollout, shouldDisableUI, canInitApiKeys } = useApiKeysVisibility() + const { shouldDisableUI, canInitApiKeys } = useApiKeysVisibility() return ( <> - {!isInRollout && } {canInitApiKeys && } From e9424b98deec1a8a6e0f0ca4765410fcc59a6a87 Mon Sep 17 00:00:00 2001 From: Riccardo Busetti Date: Mon, 11 Aug 2025 08:23:15 +0200 Subject: [PATCH 11/11] feat(replication): Delete destination and pipeline atomically (#37790) --- .../Database/Replication/DestinationRow.tsx | 12 ++-- .../delete-destination-mutation.ts | 60 ----------------- .../delete-destination-pipeline-mutation.ts | 64 +++++++++++++++++++ packages/api-types/types/platform.d.ts | 50 +++++++++++++-- 4 files changed, 117 insertions(+), 69 deletions(-) delete mode 100644 apps/studio/data/replication/delete-destination-mutation.ts create mode 100644 apps/studio/data/replication/delete-destination-pipeline-mutation.ts diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index 5e693553554..85d315f7ef7 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -5,7 +5,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' -import { useDeleteDestinationMutation } from 'data/replication/delete-destination-mutation' +import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation' import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query' import { Pipeline } from 'data/replication/pipelines-query' import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation' @@ -69,7 +69,7 @@ export const DestinationRow = ({ : PipelineStatusRequestStatus.None const { mutateAsync: stopPipeline } = useStopPipelineMutation() - const { mutateAsync: deleteDestination } = useDeleteDestinationMutation({}) + const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({}) const pipelineStatus = pipelineStatusData?.status const statusName = getStatusName(pipelineStatus) @@ -84,9 +84,11 @@ export const DestinationRow = ({ try { await stopPipeline({ projectRef, pipelineId: pipeline.id }) - // deleting the destination also deletes the pipeline because of cascade delete - // so we don't need to call deletePipeline explicitly - await deleteDestination({ projectRef, destinationId: destinationId }) + await deleteDestinationPipeline({ + projectRef, + destinationId: destinationId, + pipelineId: pipeline.id, + }) } catch (error) { toast.error(PIPELINE_ERROR_MESSAGES.DELETE_DESTINATION) } diff --git a/apps/studio/data/replication/delete-destination-mutation.ts b/apps/studio/data/replication/delete-destination-mutation.ts deleted file mode 100644 index ce17893e5c4..00000000000 --- a/apps/studio/data/replication/delete-destination-mutation.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' - -import type { ResponseError } from 'types' -import { replicationKeys } from './keys' -import { handleError, del } from 'data/fetchers' - -export type DeleteDestinationParams = { - projectRef: string - destinationId: number -} - -async function deleteDestination( - { projectRef, destinationId: destinationId }: DeleteDestinationParams, - signal?: AbortSignal -) { - if (!projectRef) throw new Error('projectRef is required') - - const { data, error } = await del('/platform/replication/{ref}/destinations/{destination_id}', { - params: { path: { ref: projectRef, destination_id: destinationId } }, - signal, - }) - if (error) { - handleError(error) - } - - return data -} - -type DeleteDestinationData = Awaited> - -export const useDeleteDestinationMutation = ({ - onSuccess, - onError, - ...options -}: Omit< - UseMutationOptions, - 'mutationFn' -> = {}) => { - const queryClient = useQueryClient() - - return useMutation( - (vars) => deleteDestination(vars), - { - async onSuccess(data, variables, context) { - const { projectRef, destinationId } = variables - await queryClient.invalidateQueries(replicationKeys.destinations(projectRef)) - await onSuccess?.(data, variables, context) - }, - async onError(data, variables, context) { - if (onError === undefined) { - toast.error(`Failed to delete destination: ${data.message}`) - } else { - onError(data, variables, context) - } - }, - ...options, - } - ) -} diff --git a/apps/studio/data/replication/delete-destination-pipeline-mutation.ts b/apps/studio/data/replication/delete-destination-pipeline-mutation.ts new file mode 100644 index 00000000000..2ccb2473423 --- /dev/null +++ b/apps/studio/data/replication/delete-destination-pipeline-mutation.ts @@ -0,0 +1,64 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import type { ResponseError } from 'types' +import { replicationKeys } from './keys' +import { handleError, del } from 'data/fetchers' + +export type DeleteDestinationPipelineParams = { + projectRef: string + destinationId: number + pipelineId: number +} + +async function deleteDestinationPipeline( + { projectRef, destinationId, pipelineId }: DeleteDestinationPipelineParams, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await del( + '/platform/replication/{ref}/destinations-pipelines/{destination_id}/{pipeline_id}', + { + params: { path: { ref: projectRef, destination_id: destinationId, pipeline_id: pipelineId } }, + signal, + } + ) + if (error) { + handleError(error) + } + + return data +} + +type DeleteDestinationPipelineData = Awaited> + +export const useDeleteDestinationPipelineMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => deleteDestinationPipeline(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + await queryClient.invalidateQueries(replicationKeys.destinations(projectRef)) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to delete destination and pipeline: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 0e606ba3d32..b84082af171 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -3434,7 +3434,8 @@ export interface paths { put?: never /** Update a replication destination and pipeline. */ post: operations['ReplicationDestinationsPipelinesController_updateDestinationPipeline'] - delete?: never + /** Delete a replication destination and pipeline. */ + delete: operations['ReplicationDestinationsPipelinesController_deleteDestinationPipeline'] options?: never head?: never patch?: never @@ -4808,6 +4809,8 @@ export interface components { } | { billing_email: string | null + /** @enum {string|null} */ + billing_partner: 'fly' | 'aws' | 'vercel_marketplace' | null id: number is_owner: boolean name: string @@ -6582,6 +6585,8 @@ export interface components { } OrganizationResponse: { billing_email: string | null + /** @enum {string|null} */ + billing_partner: 'fly' | 'aws' | 'vercel_marketplace' | null id: number is_owner: boolean name: string @@ -6634,9 +6639,8 @@ export interface components { } OrganizationSlugResponse: { billing_email: string | null - billing_metadata: { - [key: string]: unknown - } | null + /** @enum {string|null} */ + billing_partner: 'fly' | 'aws' | 'vercel_marketplace' | null has_oriole_project: boolean id: number name: string @@ -19126,6 +19130,44 @@ export interface operations { } } } + ReplicationDestinationsPipelinesController_deleteDestinationPipeline: { + parameters: { + query?: never + header?: never + path: { + /** @description Destination id */ + destination_id: number + /** @description Pipeline id */ + pipeline_id: number + /** @description Project reference */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Returned when the replication destination and pipeline are deleted. */ + 201: { + headers: { + [name: string]: unknown + } + content?: never + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Returned when the API fails to delete the replication destination or pipeline. */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } ReplicationDestinationsController_getDestination: { parameters: { query?: never