Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { PublicationsAvailability } from './PublicationsAvailability'

const { mockUseHighAvailability } = vi.hoisted(() => ({
mockUseHighAvailability: vi.fn(),
}))

vi.mock('@/hooks/misc/useHighAvailability', () => ({
useHighAvailability: mockUseHighAvailability,
}))

describe('PublicationsAvailability', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('shows the disabled empty state instead of page content for High Availability projects', () => {
mockUseHighAvailability.mockReturnValue({ isHighAvailability: true })

render(
<PublicationsAvailability>
<div>Publications content</div>
</PublicationsAvailability>
)

expect(
screen.getByText('Publications unavailable on High Availability projects')
).toBeInTheDocument()
expect(
screen.getByText(
"We're working to bring publications to High Availability projects. Contact support if this is blocking your work."
)
).toBeInTheDocument()
expect(screen.queryByText('Publications content')).not.toBeInTheDocument()
})

it('renders page content for non-High Availability projects', () => {
mockUseHighAvailability.mockReturnValue({ isHighAvailability: false })

render(
<PublicationsAvailability>
<div>Publications content</div>
</PublicationsAvailability>
)

expect(screen.getByText('Publications content')).toBeInTheDocument()
expect(
screen.queryByText('Publications unavailable on High Availability projects')
).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BookOpen } from 'lucide-react'
import type { PropsWithChildren } from 'react'

import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'

export const PublicationsAvailability = ({ children }: PropsWithChildren) => {
const { isHighAvailability } = useHighAvailability()

if (isHighAvailability) {
return (
<div className="flex h-full w-full items-center justify-center p-6">
<HighAvailabilityDisabledEmptyState
icon={BookOpen}
title="Publications unavailable on High Availability projects"
description="We're working to bring publications to High Availability projects. Contact support if this is blocking your work."
/>
</div>
)
}

return children
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { Usage } from './Usage'
import { customRender } from '@/tests/lib/custom-render'

const {
mockUseAsyncCheckPermissions,
mockUseOrgDailyStatsQuery,
mockUseOrgSubscriptionQuery,
mockUseProjectDetailQuery,
} = vi.hoisted(() => ({
mockUseAsyncCheckPermissions: vi.fn(),
mockUseOrgDailyStatsQuery: vi.fn(),
mockUseOrgSubscriptionQuery: vi.fn(),
mockUseProjectDetailQuery: vi.fn(),
}))

vi.mock('common', async (importOriginal) => ({
...(await importOriginal<typeof import('common')>()),
useParams: () => ({ slug: 'test-org' }),
}))

vi.mock('@/hooks/misc/useCheckPermissions', () => ({
useAsyncCheckPermissions: mockUseAsyncCheckPermissions,
}))

vi.mock('@/data/analytics/org-daily-stats-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@/data/analytics/org-daily-stats-query')>()),
useOrgDailyStatsQuery: mockUseOrgDailyStatsQuery,
}))

vi.mock('@/data/projects/project-detail-query', () => ({
useProjectDetailQuery: mockUseProjectDetailQuery,
}))

vi.mock('@/data/subscriptions/org-subscription-query', () => ({
useOrgSubscriptionQuery: mockUseOrgSubscriptionQuery,
}))

describe('Usage', () => {
beforeEach(() => {
vi.clearAllMocks()

mockUseAsyncCheckPermissions.mockReturnValue({ can: true, isLoading: false })
mockUseProjectDetailQuery.mockReturnValue({
data: {
ref: 'ha-project',
name: 'HA project',
high_availability: true,
},
isPending: false,
})
mockUseOrgSubscriptionQuery.mockReturnValue({
data: undefined,
error: undefined,
isPending: false,
isError: false,
isSuccess: false,
})
mockUseOrgDailyStatsQuery.mockReturnValue({
data: undefined,
error: undefined,
isPending: false,
isError: false,
})
})

it('shows a coming-soon empty state for a High Availability project', () => {
customRender(<Usage />, {
nuqs: { searchParams: { projectRef: 'ha-project' } },
})

expect(screen.getByText('Usage unavailable on High Availability projects')).toBeInTheDocument()
expect(
screen.getByText('Usage insights for High Availability projects are coming soon.')
).toBeInTheDocument()
expect(screen.queryByText('Usage filtered by project')).not.toBeInTheDocument()

expect(mockUseProjectDetailQuery).toHaveBeenCalledWith({ ref: 'ha-project' })
expect(mockUseOrgSubscriptionQuery).toHaveBeenCalledWith(
{ orgSlug: 'test-org' },
{ enabled: false }
)
expect(mockUseOrgDailyStatsQuery).toHaveBeenCalledWith(
expect.objectContaining({ orgSlug: 'test-org', projectRef: 'ha-project' }),
{ enabled: false }
)
})
})
43 changes: 31 additions & 12 deletions apps/studio/components/interfaces/Organization/Usage/Usage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { Check, ChevronDown } from 'lucide-react'
import { ChartArea, Check, ChevronDown } from 'lucide-react'
import Link from 'next/link'
import { useQueryState } from 'nuqs'
import { useMemo, useState } from 'react'
Expand All @@ -26,12 +26,14 @@ import {
} from '@/components/layouts/Scaffold'
import { AlertError } from '@/components/ui/AlertError'
import { DateRangePicker } from '@/components/ui/DateRangePicker'
import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState'
import { NoPermission } from '@/components/ui/NoPermission'
import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query'
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { resolveHighAvailability } from '@/hooks/misc/useHighAvailability'
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'

export const Usage = () => {
Expand All @@ -42,6 +44,12 @@ export const Usage = () => {
const [selectedProjectRef, setSelectedProjectRef] = useQueryState('projectRef')
const [openProjectSelector, setOpenProjectSelector] = useState(false)

const { data: selectedProject, isPending: isLoadingSelectedProject } = useProjectDetailQuery({
ref: selectedProjectRef ?? undefined,
})
const isHighAvailability = resolveHighAvailability(selectedProject)
const canLoadUsage = !selectedProjectRef || (!isLoadingSelectedProject && !isHighAvailability)

const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.BILLING_READ,
'stripe.subscriptions'
Expand All @@ -53,11 +61,7 @@ export const Usage = () => {
isPending: isLoadingSubscription,
isError: isErrorSubscription,
isSuccess: isSuccessSubscription,
} = useOrgSubscriptionQuery({ orgSlug: slug })

const { data: selectedProject } = useProjectDetailQuery({
ref: selectedProjectRef ?? undefined,
})
} = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canLoadUsage })

const billingCycleStart = useMemo(() => {
return dayjs.unix(subscription?.current_period_start ?? 0).utc()
Expand Down Expand Up @@ -107,12 +111,27 @@ export const Usage = () => {
error: orgDailyStatsError,
isPending: isLoadingOrgDailyStats,
isError: isErrorOrgDailyStats,
} = useOrgDailyStatsQuery({
orgSlug: slug,
projectRef: selectedProjectRef ?? undefined,
startDate,
endDate,
})
} = useOrgDailyStatsQuery(
{
orgSlug: slug,
projectRef: selectedProjectRef ?? undefined,
startDate,
endDate,
},
{ enabled: canLoadUsage }
)

if (isHighAvailability) {
return (
<div className="flex flex-1 items-center justify-center p-6">
<HighAvailabilityDisabledEmptyState
icon={ChartArea}
title="Usage unavailable on High Availability projects"
description="Usage insights for High Availability projects are coming soon."
/>
</div>
)
}

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const OrganizationInvite = () => {
Accept invite
</Button>
<Button asChild variant="text" block>
<Link href="/projects">Decline</Link>
<Link href="/organizations">Decline</Link>
</Button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import { useRouter } from 'next/router'
import { useMemo, useState, type ComponentPropsWithoutRef } from 'react'
import { useMemo, useRef, useState, type ComponentPropsWithoutRef } from 'react'
import { useForm } from 'react-hook-form'
import { AWS_REGIONS, CloudProvider } from 'shared-data'
import { toast } from 'sonner'
Expand Down Expand Up @@ -99,6 +99,7 @@ export const ResumeProjectButton = ({
mode: 'onChange',
defaultValues: { postgresVersionSelection: '' },
})
const lastValidPostgresVersionSelection = useRef('')

const onSelectRestore = () => {
if (project?.status !== PROJECT_STATUS.INACTIVE) {
Expand Down Expand Up @@ -209,6 +210,7 @@ export const ResumeProjectButton = ({
dbRegion={region?.displayName ?? ''}
cloudProvider={(project?.cloud_provider ?? 'AWS') as CloudProvider}
organizationSlug={selectedOrganization?.slug}
lastValidSelectionRef={lastValidPostgresVersionSelection}
/>
)}
/>
Expand Down
Loading
Loading