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.
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:
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',
}}
/>
)
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
-
-
-
+
+
+
+
+ )}
)
}
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/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/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}
+
- }
- />
-
-
-
-
-
-
- >
- )
- }}
-
-
+
+
+ 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')}
+
- } />
+ } />
@@ -104,7 +107,7 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
onSelectEdit(secret)}
+ onClick={() => setModal(`edit`)}
>
Edit
@@ -136,6 +139,8 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
+
+
)
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/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/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/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)
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/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/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts
index 1f38e714f72..0efc542aedd 100644
--- a/apps/studio/data/reports/database-charts.ts
+++ b/apps/studio/data/reports/database-charts.ts
@@ -612,12 +612,12 @@ 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 },
- '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 },
+ xlarge: { direct: 240, pooler: 1000 },
+ '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 (
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/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')
})
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
+
+
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 && }
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/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