From 995f6f65c776d60d7e508eb6637a27b055948311 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 29 Sep 2026 10:51:14 -0700 Subject: [PATCH] [FE-4483] fix(studio): disable network bans for v3 projects (#50997) Disables network bans for v3 (`AWS_K8S`) projects and shows a specific unsupported notice. The shared banned-IP query waits for project details and skips unsupported projects, covering both Database Settings and Advisor for v3 and High Availability projects. The hook returns the standard query result and uses `skipToken` to prevent unsupported requests, including manual refetches. Database Settings handles project-detail errors at the call site. Open unban confirmations are cleared when the section becomes disabled, and submission checks eligibility. Addresses [FE-4483](https://linear.app/supabase/issue/FE-4483/disable-network-bans-for-v3-aws-k8s-projects). ## To test - Open Database Settings on a v3 project. Check that Network bans shows the v3 notice, hides the IP list and unban controls, and makes no network-bans retrieval request on initial load or reload, including while Advisor is mounted. - Check that an HA project still shows its existing notice and makes no network-bans retrieval request on initial load or reload. - Navigate from a supported project to a v3 or HA project and check that no banned-IP request is sent for the unsupported project and no banned-IP signals from the previous project appear in Advisor. - If project details fail without cached data, check that Network bans shows an error after retries finish and does not retrieve bans. A successful retry should restore normal behavior. - Open an unban confirmation on a supported project, then navigate to a v3 or HA project. Check that the dialog closes without sending an unban request and stays closed when returning. A newly opened confirmation should still work. - On a supported project, check the empty state and banned IP list. Confirm that users with permission can unban an IP and users without permission see a disabled button with the permissions tooltip. Validation: 17 focused tests passed, covering automatic and manual request suppression, project-detail error display and recovery, and navigation between supported and unsupported projects. Changed-file ESLint, Prettier, and full Studio typecheck (without the incremental cache) passed. Earlier local browser checks on `9912c6c` confirmed no retrieval requests for an HA project on AWS_K8S across reloads and Advisor, and a successful empty state on a supported project. The local failed-project case redirected to the organization after retries, so the inline error remains verified by the component test only. The latest preview, standalone v3 notice, populated bans/unban, and no-permission tooltip still need browser verification. ## Summary by CodeRabbit * **Bug Fixes** * Banned IP settings now show an unsupported-project notice for AWS Kubernetes projects and hide ban lists and unban actions for AWS Kubernetes and High Availability projects. * Banned IP data loads only after project details are available and only for supported projects; unsupported projects do not display cached ban data. * Project-detail errors are shown separately from ban-list errors. * Unban confirmations close when a project becomes unsupported or an unban succeeds. Unbanning is unavailable when you lack update permission or the project is unsupported. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../Settings/Database/BannedIPs.test.tsx | 134 ++++++++++ .../Settings/Database/BannedIPs.tsx | 154 +++++++----- .../data/banned-ips/banned-ips-query.test.tsx | 233 ++++++++++++++++++ .../data/banned-ips/banned-ips-query.ts | 20 +- 4 files changed, 470 insertions(+), 71 deletions(-) create mode 100644 apps/studio/components/interfaces/Settings/Database/BannedIPs.test.tsx create mode 100644 apps/studio/data/banned-ips/banned-ips-query.test.tsx diff --git a/apps/studio/components/interfaces/Settings/Database/BannedIPs.test.tsx b/apps/studio/components/interfaces/Settings/Database/BannedIPs.test.tsx new file mode 100644 index 00000000000..5026bb02d25 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/BannedIPs.test.tsx @@ -0,0 +1,134 @@ +import { QueryClient } from '@tanstack/react-query' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { HttpResponse } from 'msw' +import { beforeEach, expect, test, vi } from 'vitest' + +import { BannedIPs } from './BannedIPs' +import type { deleteBannedIPs } from '@/data/banned-ips/banned-ips-delete-mutations' +import type { IPData } from '@/data/banned-ips/banned-ips-query' +import type { ProjectDetail } from '@/data/projects/project-detail-query' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw' + +mockAnimationsApi() + +const routeParams = vi.hoisted(() => ({ ref: 'default' })) +beforeEach(() => { + routeParams.ref = 'default' +}) + +vi.mock('common', async (importOriginal) => ({ + ...(await importOriginal()), + IS_PLATFORM: true, + useParams: () => routeParams, +})) + +vi.mock('@/lib/constants', async (importOriginal) => ({ + ...(await importOriginal()), + IS_PLATFORM: true, +})) + +vi.mock('@/hooks/misc/useCheckPermissions', () => ({ + useAsyncCheckPermissions: () => ({ can: true }), +})) + +vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => vi.fn() })) + +test('shows a project-details error instead of leaving Network bans loading', async () => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => + HttpResponse.json({ message: 'Project unavailable' }, { status: 500 }), + }) + + customRender() + + expect(await screen.findByText('Failed to retrieve project details')).toBeVisible() + expect(screen.getByText('Error: Project unavailable')).toBeVisible() + expect(screen.getByRole('link', { name: 'Contact support' })).toHaveAttribute( + 'href', + expect.stringContaining('projectRef=default') + ) + expect(screen.queryByRole('button', { name: 'Unban IP' })).not.toBeInTheDocument() + expect( + screen.queryByText('There are no banned IP addresses for your project') + ).not.toBeInTheDocument() +}) + +const PROJECT: ProjectDetail = { + cloud_provider: 'AWS', + connectionString: 'postgresql://postgres:password@db.default.supabase.co:5432/postgres', + db_host: 'db.default.supabase.co', + dbVersion: 'supabase-postgres-15.1.0', + high_availability: false, + id: 1, + infra_compute_size: 'micro', + inserted_at: '2026-01-01T00:00:00.000Z', + integration_source: null, + is_branch_enabled: false, + is_physical_backups_enabled: false, + name: 'Test project', + organization_id: 1, + ref: 'default', + region: 'us-east-1', + restUrl: 'https://default.supabase.co', + status: 'ACTIVE_HEALTHY', + subscription_id: 'subscription-1', + updated_at: '2026-01-01T00:00:00.000Z', +} + +test.each([ + { name: 'v3', cloud_provider: 'AWS_K8S', high_availability: false }, + { name: 'HA', cloud_provider: 'AWS', high_availability: true }, +])('clears an open unban confirmation when navigating to $name', async (unsupported) => { + let bannedIPs: IPData = { banned_ipv4_addresses: ['203.0.113.10'] } + const unbanRequests: unknown[] = [] + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: ({ params }) => + HttpResponse.json( + params.ref === 'default' ? PROJECT : { ...PROJECT, ...unsupported, ref: String(params.ref) } + ), + }) + addAPIMock({ + method: 'post', + path: '/v1/projects/:ref/network-bans/retrieve', + response: () => HttpResponse.json(bannedIPs), + }) + addAPIMock({ + method: 'delete', + path: '/v1/projects/:ref/network-bans', + response: async ({ request }) => { + unbanRequests.push(await request.json()) + bannedIPs = { banned_ipv4_addresses: [] } + return HttpResponse.json>>(null) + }, + }) + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const { rerender } = customRender(, { queryClient }) + + fireEvent.click(await screen.findByRole('button', { name: 'Unban IP' })) + expect(await screen.findByRole('dialog', { name: 'Confirm Unban IP' })).toBeVisible() + + routeParams.ref = 'unsupported' + rerender() + + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + expect(screen.queryByRole('button', { name: 'Unban IP' })).not.toBeInTheDocument() + expect(unbanRequests).toEqual([]) + + routeParams.ref = 'default' + rerender() + + const unbanButton = await screen.findByRole('button', { name: 'Unban IP' }) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(unbanRequests).toEqual([]) + + fireEvent.click(unbanButton) + fireEvent.click(await screen.findByRole('button', { name: 'Confirm Unban' })) + expect(await screen.findByText('There are no banned IP addresses for your project')).toBeVisible() + expect(unbanRequests).toEqual([{ ipv4_addresses: ['203.0.113.10'] }]) +}) diff --git a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx index 1a8fd206b02..ed01b76b1ec 100644 --- a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx +++ b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx @@ -1,9 +1,10 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { Globe } from 'lucide-react' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { toast } from 'sonner' import { Badge, Card, CardContent, Skeleton } from 'ui' +import { Admonition } from 'ui-patterns/Admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { PageSection, @@ -23,52 +24,56 @@ import { useBannedIPsQuery } from '@/data/banned-ips/banned-ips-query' import { useUserIPAddressQuery } from '@/data/misc/user-ip-address-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useHighAvailability } from '@/hooks/misc/useHighAvailability' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' const HA_DISABLED_TITLE = 'Network bans unavailable on High Availability projects' const HA_DISABLED_DESCRIPTION = "We're working to bring network bans to High Availability projects. Contact support if this is blocking your work." +const V3_DISABLED_TITLE = 'Network bans unavailable on v3 projects' export const BannedIPs = () => { const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() + const { data: project, error: projectError } = useSelectedProjectQuery() const { isHighAvailability } = useHighAvailability() + const isAwsK8s = useIsAwsK8sCloudProvider() - const [selectedIPToUnban, setSelectedIPToUnban] = useState(null) // Track the selected IP for unban + const [selectedIPToUnban, setSelectedIPToUnban] = useState(null) const { isPending: isLoadingIPList, isFetching: isFetchingIPList, data: ipList, error: ipListError, - } = useBannedIPsQuery({ - projectRef: ref, - }) + } = useBannedIPsQuery({ projectRef: ref }) const { data: userIPAddress } = useUserIPAddressQuery() + const hasProjectError = !project && !!projectError const ipListLoading = isLoadingIPList || isFetchingIPList - const [showUnban, setShowUnban] = useState(false) - const [confirmingIP, setConfirmingIP] = useState(null) // Track the IP being confirmed for unban - const { can: canUnbanNetworks } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, }, }) - const isSectionDisabled = isHighAvailability || !canUnbanNetworks - const sectionDisabledReason = isHighAvailability - ? HA_DISABLED_TITLE - : 'You need additional permissions to unban networks' + const isSectionDisabled = isHighAvailability || isAwsK8s || !canUnbanNetworks + + if (isSectionDisabled && selectedIPToUnban !== null) { + setSelectedIPToUnban(null) + } + + const sectionDisabledReason = useMemo(() => { + if (isHighAvailability) return HA_DISABLED_TITLE + if (isAwsK8s) return V3_DISABLED_TITLE + return 'You need additional permissions to unban networks' + }, [isHighAvailability, isAwsK8s]) const { mutate: unbanIPs, isPending: isUnbanning } = useBannedIPsDeleteMutation({ onSuccess: () => { toast.success('IP address successfully unbanned') - setSelectedIPToUnban(null) // Reset the selected IP for unban - setShowUnban(false) + setSelectedIPToUnban(null) }, onError: (error) => { toast.error(`Failed to unban IP: ${error?.message}`) @@ -76,19 +81,13 @@ export const BannedIPs = () => { }) const onConfirmUnbanIP = () => { - if (confirmingIP == null || !ref) return + if (selectedIPToUnban === null || !ref || isSectionDisabled) return unbanIPs({ projectRef: ref, - ips: [confirmingIP], // Pass the IP as an array + ips: [selectedIPToUnban], // Pass the IP as an array }) } - const openConfirmationModal = (ip: string) => { - setSelectedIPToUnban(ip) // Set the selected IP for unban - setConfirmingIP(ip) // Set the IP being confirmed for unban - setShowUnban(true) - } - return ( <> @@ -102,6 +101,13 @@ export const BannedIPs = () => { + {hasProjectError && ( + + )} {isHighAvailability && (
{ />
)} - {!isHighAvailability && - (ipListLoading ? ( - - - - - - - ) : ipListError ? ( - - ) : ipList.banned_ipv4_addresses.length > 0 ? ( - - {ipList.banned_ipv4_addresses.map((ip) => ( - -
- -

{ip}

- {ip === userIPAddress && Your IP address} -
- openConfirmationModal(ip)} - tooltip={{ - content: { - side: 'bottom', - text: isSectionDisabled ? sectionDisabledReason : undefined, - }, - }} - > - Unban IP - + {!isHighAvailability && isAwsK8s && ( + + )} + {!hasProjectError && !isHighAvailability && !isAwsK8s && ( + <> + {ipListError && ( + + )} + {!ipListError && ipListLoading && ( + + + + - ))} - - ) : ( - - - There are no banned IP addresses for your project - - - ))} +
+ )} + {!ipListError && !ipListLoading && ipList && ( + + {ipList.banned_ipv4_addresses.length > 0 ? ( + ipList.banned_ipv4_addresses.map((ip) => ( + +
+ +

{ip}

+ {ip === userIPAddress && Your IP address} +
+ setSelectedIPToUnban(ip)} + tooltip={{ + content: { + side: 'bottom', + text: isSectionDisabled ? sectionDisabledReason : undefined, + }, + }} + > + Unban IP + +
+ )) + ) : ( + + There are no banned IP addresses for your project + + )} +
+ )} + + )}
@@ -158,11 +180,11 @@ export const BannedIPs = () => { variant="destructive" size="medium" loading={isUnbanning} - visible={showUnban} + visible={selectedIPToUnban !== null && !isSectionDisabled} title="Confirm Unban IP" confirmLabel="Confirm Unban" confirmLabelLoading="Unbanning..." - onCancel={() => setShowUnban(false)} + onCancel={() => setSelectedIPToUnban(null)} onConfirm={onConfirmUnbanIP} alert={{ title: 'This action cannot be undone', diff --git a/apps/studio/data/banned-ips/banned-ips-query.test.tsx b/apps/studio/data/banned-ips/banned-ips-query.test.tsx new file mode 100644 index 00000000000..1622cd0d11d --- /dev/null +++ b/apps/studio/data/banned-ips/banned-ips-query.test.tsx @@ -0,0 +1,233 @@ +import { QueryClient } from '@tanstack/react-query' +import { act, waitFor } from '@testing-library/react' +import { HttpResponse } from 'msw' +import { describe, expect, test, vi } from 'vitest' + +import { useBannedIPsQuery, type IPData } from './banned-ips-query' +import { useAdvisorSignals } from '@/components/ui/AdvisorPanel/useAdvisorSignals' +import { useProjectDetailQuery, type ProjectDetail } from '@/data/projects/project-detail-query' +import { customRenderHook } from '@/tests/lib/custom-render' +import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw' + +vi.mock('common', async (importOriginal) => ({ + ...(await importOriginal()), + IS_PLATFORM: true, +})) + +const PROJECT: ProjectDetail = { + cloud_provider: 'AWS', + connectionString: 'postgresql://postgres:password@db.default.supabase.co:5432/postgres', + db_host: 'db.default.supabase.co', + dbVersion: 'supabase-postgres-15.1.0', + high_availability: false, + id: 1, + infra_compute_size: 'micro', + inserted_at: '2026-01-01T00:00:00.000Z', + integration_source: null, + is_branch_enabled: false, + is_physical_backups_enabled: false, + name: 'Test project', + organization_id: 1, + ref: 'default', + region: 'us-east-1', + restUrl: 'https://default.supabase.co', + status: 'ACTIVE_HEALTHY', + subscription_id: 'subscription-1', + updated_at: '2026-01-01T00:00:00.000Z', +} + +const BANNED_IPS: IPData = { banned_ipv4_addresses: ['203.0.113.10'] } + +const mockBannedIPs = () => { + const requests: string[] = [] + addAPIMock({ + method: 'post', + path: '/v1/projects/:ref/network-bans/retrieve', + response: ({ request }) => { + requests.push(request.url) + return HttpResponse.json(BANNED_IPS) + }, + }) + return requests +} + +describe('useBannedIPsQuery', () => { + test.each([ + { name: 'v3', cloud_provider: 'AWS_K8S', high_availability: false }, + { name: 'HA', cloud_provider: 'AWS', high_availability: true }, + { name: 'v3 HA', cloud_provider: 'AWS_K8S', high_availability: true }, + ])('does not retrieve bans for $name with Settings and Advisor enabled', async (project) => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => HttpResponse.json({ ...PROJECT, ...project }), + }) + const requests = mockBannedIPs() + const { result } = customRenderHook(() => ({ + project: useProjectDetailQuery({ ref: 'default' }), + settings: useBannedIPsQuery({ projectRef: 'default' }), + advisor: useAdvisorSignals({ projectRef: 'default' }), + })) + + await waitFor(() => expect(result.current.project.isSuccess).toBe(true)) + expect(result.current.settings.fetchStatus).toBe('idle') + expect(result.current.advisor.data).toEqual([]) + expect(requests).toEqual([]) + + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + await act(async () => { + await expect(result.current.settings.refetch({ throwOnError: true })).rejects.toThrow() + }) + expect(requests).toEqual([]) + } finally { + consoleError.mockRestore() + } + }) + + test('shares banned IPs with Advisor on a supported project', async () => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => HttpResponse.json(PROJECT), + }) + const requests = mockBannedIPs() + const { result } = customRenderHook(() => ({ + settings: useBannedIPsQuery({ projectRef: 'default' }), + advisor: useAdvisorSignals({ projectRef: 'default' }), + })) + + await waitFor(() => expect(result.current.advisor.data).toHaveLength(1)) + expect(result.current.settings.data).toEqual(BANNED_IPS) + expect(requests).toHaveLength(1) + }) + + test('waits for project details before retrieving bans', async () => { + const { promise, resolve } = Promise.withResolvers() + let hasRequestedProject = false + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: async () => { + hasRequestedProject = true + await promise + return HttpResponse.json(PROJECT) + }, + }) + const requests = mockBannedIPs() + const { result } = customRenderHook(() => useBannedIPsQuery({ projectRef: 'default' })) + + try { + await waitFor(() => expect(hasRequestedProject).toBe(true)) + expect(result.current.fetchStatus).toBe('idle') + expect(requests).toEqual([]) + } finally { + resolve() + } + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(requests).toHaveLength(1) + }) + + test('waits for project details to recover before retrieving bans', async () => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => + HttpResponse.json({ message: 'Project unavailable' }, { status: 500 }), + }) + const requests = mockBannedIPs() + const { result } = customRenderHook(() => ({ + project: useProjectDetailQuery({ ref: 'default' }), + bans: useBannedIPsQuery({ projectRef: 'default' }), + })) + + await waitFor(() => expect(result.current.project.isError).toBe(true)) + expect(result.current.bans.fetchStatus).toBe('idle') + expect(requests).toEqual([]) + + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => HttpResponse.json(PROJECT), + }) + await act(() => result.current.project.refetch()) + + await waitFor(() => expect(result.current.bans.isSuccess).toBe(true)) + expect(result.current.bans.data).toEqual(BANNED_IPS) + expect(requests).toHaveLength(1) + }) + + test('keeps bans visible when project details fail to refresh with cached supported data', async () => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => HttpResponse.json(PROJECT), + }) + mockBannedIPs() + const { result } = customRenderHook(() => ({ + project: useProjectDetailQuery({ ref: 'default' }), + bans: useBannedIPsQuery({ projectRef: 'default' }), + advisor: useAdvisorSignals({ projectRef: 'default' }), + })) + + await waitFor(() => expect(result.current.advisor.data).toHaveLength(1)) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: () => + HttpResponse.json({ message: 'Project unavailable' }, { status: 500 }), + }) + await act(async () => { + const projectResult = await result.current.project.refetch() + expect(projectResult.isError).toBe(true) + }) + expect(result.current.bans.data).toEqual(BANNED_IPS) + expect(result.current.advisor.data).toHaveLength(1) + }) + + test.each([ + { projectRef: 'default', enabled: false }, + { projectRef: undefined, enabled: true }, + ])('does not fetch with $projectRef and enabled=$enabled', ({ projectRef, enabled }) => { + const { result } = customRenderHook(() => useBannedIPsQuery({ projectRef }, { enabled })) + + expect(result.current.fetchStatus).toBe('idle') + }) + + test('checks the requested project when navigating from supported to v3', async () => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: ({ params }) => + HttpResponse.json({ + ...PROJECT, + ref: String(params.ref), + cloud_provider: params.ref === 'v3' ? 'AWS_K8S' : 'AWS', + }), + }) + const requests = mockBannedIPs() + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + let projectRef = 'default' + const { result, rerender } = customRenderHook( + () => ({ + project: useProjectDetailQuery({ ref: projectRef }), + bans: useBannedIPsQuery({ projectRef }), + advisor: useAdvisorSignals({ projectRef }), + }), + { queryClient } + ) + + await waitFor(() => expect(result.current.bans.isSuccess).toBe(true)) + act(() => { + projectRef = 'v3' + rerender() + }) + + await waitFor(() => expect(result.current.project.data?.ref).toBe('v3')) + expect(result.current.bans.fetchStatus).toBe('idle') + expect(result.current.advisor.data).toEqual([]) + expect(requests).toHaveLength(1) + expect(requests[0]).toContain('/projects/default/network-bans/retrieve') + }) +}) diff --git a/apps/studio/data/banned-ips/banned-ips-query.ts b/apps/studio/data/banned-ips/banned-ips-query.ts index 13a9f5dbcfc..370dfe83009 100644 --- a/apps/studio/data/banned-ips/banned-ips-query.ts +++ b/apps/studio/data/banned-ips/banned-ips-query.ts @@ -1,8 +1,10 @@ -import { useQuery } from '@tanstack/react-query' +import { skipToken, useQuery } from '@tanstack/react-query' import { IS_PLATFORM } from 'common' import { BannedIPKeys } from './keys' import { handleError, post } from '@/data/fetchers' +import { useProjectDetailQuery } from '@/data/projects/project-detail-query' +import { PROVIDERS } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' type BannedIPVariables = { projectRef?: string } @@ -25,13 +27,21 @@ export type IPError = ResponseError export const useBannedIPsQuery = ( { projectRef }: BannedIPVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} -) => - useQuery({ +) => { + const { data: project } = useProjectDetailQuery( + { ref: projectRef }, + { enabled: enabled && IS_PLATFORM } + ) + const isSupported = + !!project && !project.high_availability && project.cloud_provider !== PROVIDERS.AWS_K8S.id + + return useQuery({ queryKey: BannedIPKeys.list(projectRef), - queryFn: ({ signal }) => getBannedIPs({ projectRef }, signal), - enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined', + queryFn: isSupported ? ({ signal }) => getBannedIPs({ projectRef }, signal) : skipToken, + enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined' && isSupported, retry: false, refetchOnWindowFocus: false, staleTime: 60_000, ...options, }) +}