diff --git a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx index fbb7b3ff373..37ce5c10bc3 100644 --- a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx +++ b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx @@ -60,7 +60,7 @@ export interface ComposedChartHandlerProps { /** * Wrapper component that handles intersection observer logic for lazy loading */ -const LazyChartWrapper = ({ children }: PropsWithChildren) => { +const LazyChartWrapper = ({ id, children }: PropsWithChildren<{ id: string }>) => { const [isVisible, setIsVisible] = useState(false) const ref = useRef(null) @@ -91,7 +91,7 @@ const LazyChartWrapper = ({ children }: PropsWithChildren) => { }, []) return ( -
+
{React.cloneElement(children as React.ReactElement<{ isVisible: boolean }>, { isVisible })}
) @@ -291,9 +291,8 @@ const ComposedChartHandler = ({
{children}
@@ -385,8 +384,9 @@ const useAttributeQueries = ( export function LazyComposedChartHandler(props: ComposedChartHandlerProps) { if (props.hide) return null + const anchorId = props.id ?? props.label.toLowerCase().replaceAll(' ', '-') return ( - + ) diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts index e0c3a935254..b5a6b9de356 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts @@ -13,9 +13,8 @@ interface ResourceWarningMessage { critical: { title?: string; description?: string } } docsUrl?: string - // In-app destination for inspecting the metric (e.g. observability charts). - // [ref] is replaced with the current project ref at render time. - metricsHref?: string + resourceLabel?: string + metricsChartId?: string buttonText?: string aiPrompt?: string metric: string | null @@ -23,7 +22,7 @@ interface ResourceWarningMessage { type ResourceWarningMessages = Record -export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { +const RESOURCE_WARNING_MESSAGE_CONFIG = { is_readonly_mode_enabled: { bannerContent: { warning: { @@ -78,10 +77,11 @@ export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { }, }, docsUrl: `${DOCS_URL}/guides/troubleshooting/exhaust-disk-io`, - metricsHref: '/project/[ref]/observability/database', buttonText: 'Upgrade compute', aiPrompt: 'My database is running out of Disk IO budget. Can you query pg_stat_statements to find the top queries by shared blocks read and written, identify which are causing the most disk I/O, and suggest specific optimizations to reduce disk usage?', + resourceLabel: 'Disk IO', + metricsChartId: 'disk-throughput', metric: 'disk_io', }, disk_space_exhaustion: { @@ -110,6 +110,8 @@ export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { }, docsUrl: `${DOCS_URL}/guides/platform/database-size#disk-management`, buttonText: undefined, + resourceLabel: 'Disk space', + metricsChartId: 'disk-size', metric: 'disk_space', }, cpu_exhaustion: { @@ -139,6 +141,8 @@ export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { buttonText: 'Upgrade compute', aiPrompt: 'My database is experiencing high CPU usage. Can you query pg_stat_statements to find the top queries by total execution time and mean execution time, identify which are most CPU-intensive, and suggest specific optimizations such as missing indexes or query rewrites to reduce CPU load?', + resourceLabel: 'CPU', + metricsChartId: 'cpu-usage', metric: 'cpu', }, memory_and_swap_exhaustion: { @@ -169,6 +173,8 @@ export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { buttonText: 'Upgrade compute', aiPrompt: 'My database is experiencing high memory and swap usage. Can you query pg_stat_statements to find the top queries by shared buffer hits and rows returned, identify which queries are putting the most pressure on memory, and suggest optimizations to reduce memory consumption?', + resourceLabel: 'Memory', + metricsChartId: 'ram-usage', metric: 'ram', }, auth_rate_limit_exhaustion: { @@ -197,18 +203,18 @@ export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { }, docsUrl: `${DOCS_URL}/guides/platform/going-into-prod#auth-rate-limits`, buttonText: 'Enable custom SMTP', + resourceLabel: 'Auth rate limits', metric: 'auth_email_rate_limit', }, multiple_resource_warnings: { bannerContent: { warning: { - title: - 'Your project is currently exhausting multiple resources, and its performance is affected', + title: 'Your project is exhausting {resources}, which is affecting its performance', description: 'Upgrade your compute or use the AI Assistant to identify and optimize the most expensive queries.', }, critical: { - title: 'Your project has exhausted multiple resources, and its performance is affected', + title: 'Your project has exhausted {resources}, which is affecting its performance', description: 'Upgrade your compute or use the AI Assistant to identify and optimize the most expensive queries.', }, @@ -226,7 +232,15 @@ export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = { docsUrl: undefined, buttonText: 'Check usage', aiPrompt: - 'My database is exhausting multiple resources (CPU, memory, and/or disk IO). Can you query pg_stat_statements to identify the most expensive queries overall, and suggest which optimizations would have the biggest impact on reducing resource consumption?', + 'My database is exhausting multiple resources ({resources}). Can you query pg_stat_statements to identify the most expensive queries overall, and suggest which optimizations would have the biggest impact on reducing resource consumption?', metric: null, }, -} +} satisfies ResourceWarningMessages + +export type ResourceWarningMessageKey = keyof typeof RESOURCE_WARNING_MESSAGE_CONFIG + +export const RESOURCE_WARNING_MESSAGES: ResourceWarningMessages = RESOURCE_WARNING_MESSAGE_CONFIG + +export const isResourceWarningMessageKey = ( + property: string +): property is ResourceWarningMessageKey => Object.hasOwn(RESOURCE_WARNING_MESSAGE_CONFIG, property) diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.test.tsx b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.test.tsx new file mode 100644 index 00000000000..9874df50b02 --- /dev/null +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.test.tsx @@ -0,0 +1,261 @@ +import { screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { platformComponents as components } from 'api-types' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { HttpResponse } from 'msw' +import { afterEach, describe, expect, test, vi } from 'vitest' + +import { ResourceExhaustionWarningBanner } from './ResourceExhaustionWarningBanner' +import type { ProfileContextType } from '@/lib/profile' +import { createMockOrganizationResponse } from '@/tests/helpers' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +type ProjectDetailResponse = components['schemas']['ProjectDetailResponse_Output'] +type ProjectResourceWarningsResponse = + components['schemas']['ProjectResourceWarningsResponse_Output'] + +const PROJECT_REF = 'project-ref' + +const { trackSpy, flags } = vi.hoisted(() => ({ + trackSpy: vi.fn(), + flags: { showDiskIOBurstBalanceChart: false }, +})) + +vi.mock('common', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_PLATFORM: true, + useIsLoggedIn: () => true, + useParams: () => ({ ref: PROJECT_REF }), + useFlag: () => flags.showDiskIOBurstBalanceChart, + } +}) + +vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => trackSpy })) + +mockAnimationsApi() + +afterEach(() => { + flags.showDiskIOBurstBalanceChart = false +}) + +const PROFILE_CONTEXT: ProfileContextType = { + profile: { + id: 1, + auth0_id: 'auth0|test', + gotrue_id: 'gotrue-test', + username: 'testuser', + primary_email: 'test@example.com', + first_name: null, + last_name: null, + mobile: null, + is_alpha_user: false, + is_sso_user: false, + disabled_features: [], + free_project_limit: null, + }, + error: null, + isLoading: false, + isError: false, + isSuccess: true, +} + +const PROJECT: ProjectDetailResponse = { + cloud_provider: 'AWS', + connectionString: 'postgresql://postgres:password@db.project-ref.supabase.co:5432/postgres', + db_host: 'db.project-ref.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: 'Production', + organization_id: 1, + ref: PROJECT_REF, + region: 'us-east-1', + restUrl: `https://${PROJECT_REF}.supabase.co`, + status: 'ACTIVE_HEALTHY', + subscription_id: 'subscription-1', + updated_at: '2026-01-01T00:00:00.000Z', +} + +const NO_WARNINGS: ProjectResourceWarningsResponse = { + project: PROJECT_REF, + is_readonly_mode_enabled: false, + auth_email_offender: null, + auth_rate_limit_exhaustion: null, + auth_restricted_email_sending: null, + cpu_exhaustion: null, + disk_io_exhaustion: null, + disk_space_exhaustion: null, + memory_and_swap_exhaustion: null, + need_pitr: null, +} + +const metricsHref = (chartId: string) => + `/project/${PROJECT_REF}/observability/database?chart=${chartId}&db=${PROJECT_REF}&isHelper=true&helperText=Last+3+hours#${chartId}` + +const renderBanner = (warnings: Partial) => { + addAPIMock({ method: 'get', path: '/platform/projects/:ref', response: PROJECT }) + addAPIMock({ + method: 'get', + path: '/platform/organizations', + response: [ + createMockOrganizationResponse({ + id: 1, + slug: 'acme', + plan: { id: 'pro', name: 'Pro' }, + usage_billing_enabled: true, + }), + ], + }) + addAPIMock({ + method: 'get', + path: '/platform/projects-resource-warnings', + response: () => + HttpResponse.json([{ ...NO_WARNINGS, ...warnings }]), + }) + + return customRender(, { profileContext: PROFILE_CONTEXT }) +} + +const openTroubleshootMenu = async () => { + await userEvent.click(await screen.findByRole('button', { name: 'Troubleshoot' })) + return within(await screen.findByRole('menu')) +} + +describe('ResourceExhaustionWarningBanner', () => { + test('multi-resource banner names each resource and links each to its chart', async () => { + renderBanner({ cpu_exhaustion: 'warning', disk_space_exhaustion: 'warning' }) + + expect( + await screen.findByText( + 'Your project is exhausting CPU and Disk space, which is affecting its performance' + ) + ).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Check usage' })).toHaveAttribute( + 'href', + `/project/${PROJECT_REF}/settings/infrastructure` + ) + + const menu = await openTroubleshootMenu() + const itemLabels = menu.getAllByRole('menuitem').map((item) => item.textContent) + expect(itemLabels).toEqual([ + 'View CPU metrics', + 'View Disk space metrics', + 'CPU documentation', + 'Disk space documentation', + 'Ask AI Assistant', + ]) + + const cpuMetrics = menu.getByRole('menuitem', { name: 'View CPU metrics' }) + expect(cpuMetrics).toHaveAttribute('href', metricsHref('cpu-usage')) + + await userEvent.click(cpuMetrics) + expect(trackSpy).toHaveBeenCalledWith('resource_exhaustion_banner_troubleshoot_clicked', { + troubleshootAction: 'metrics', + warningType: 'cpu_exhaustion', + warningTypes: ['cpu_exhaustion', 'disk_space_exhaustion'], + destination: metricsHref('cpu-usage'), + }) + }) + + test('mixed-severity multi-resource banner does not describe every resource as exhausted', async () => { + renderBanner({ cpu_exhaustion: 'critical', disk_space_exhaustion: 'warning' }) + + expect( + await screen.findByText( + "Resource warnings for CPU and Disk space are affecting your project's performance" + ) + ).toBeInTheDocument() + expect(screen.queryByText(/has exhausted CPU and Disk space/)).not.toBeInTheDocument() + }) + + test('single-resource banner keeps its title and adds View metrics', async () => { + renderBanner({ cpu_exhaustion: 'warning' }) + + expect( + await screen.findByText( + 'Your project is currently facing high CPU usage, and its performance is affected' + ) + ).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Upgrade compute' })).toBeInTheDocument() + + const menu = await openTroubleshootMenu() + const itemLabels = menu.getAllByRole('menuitem').map((item) => item.textContent) + expect(itemLabels).toEqual(['View metrics', 'Documentation', 'Ask AI Assistant']) + expect(menu.getByRole('menuitem', { name: 'View metrics' })).toHaveAttribute( + 'href', + metricsHref('cpu-usage') + ) + }) + + test('filters unconfigured warning fields before rendering', async () => { + renderBanner({ + auth_email_offender: 'warning', + auth_restricted_email_sending: 'critical', + cpu_exhaustion: 'warning', + need_pitr: 'warning', + }) + + expect( + await screen.findByText( + 'Your project is currently facing high CPU usage, and its performance is affected' + ) + ).toBeInTheDocument() + + const menu = await openTroubleshootMenu() + const metricsItem = menu.getByRole('menuitem', { name: 'View metrics' }) + await userEvent.click(metricsItem) + + expect(trackSpy).toHaveBeenCalledWith('resource_exhaustion_banner_troubleshoot_clicked', { + troubleshootAction: 'metrics', + warningType: 'cpu_exhaustion', + warningTypes: ['cpu_exhaustion'], + destination: metricsHref('cpu-usage'), + }) + }) + + test('all-compute multi banner keeps Upgrade compute and falls back to the throughput chart', async () => { + renderBanner({ cpu_exhaustion: 'warning', disk_io_exhaustion: 'warning' }) + + expect(await screen.findByRole('link', { name: 'Upgrade compute' })).toBeInTheDocument() + + const menu = await openTroubleshootMenu() + expect(menu.getByRole('menuitem', { name: 'Ask AI Assistant' })).toBeInTheDocument() + expect(menu.getByRole('menuitem', { name: 'View Disk IO metrics' })).toHaveAttribute( + 'href', + metricsHref('disk-throughput') + ) + }) + + test('disk IO links to the burst balance chart when the report renders it', async () => { + flags.showDiskIOBurstBalanceChart = true + renderBanner({ disk_io_exhaustion: 'warning' }) + + const menu = await openTroubleshootMenu() + expect(menu.getByRole('menuitem', { name: 'View metrics' })).toHaveAttribute( + 'href', + metricsHref('disk-io-burst-balance') + ) + }) + + test('non-compute multi-resource warning points users to the available actions', async () => { + renderBanner({ disk_space_exhaustion: 'warning', auth_rate_limit_exhaustion: 'warning' }) + + expect( + await screen.findByText( + 'Use the Troubleshoot menu to review the affected resources and resolve these warnings.' + ) + ).toBeInTheDocument() + expect(screen.queryByText(/AI Assistant/)).not.toBeInTheDocument() + + const menu = await openTroubleshootMenu() + expect(menu.queryByRole('menuitem', { name: 'Ask AI Assistant' })).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx index 7cbeb085ba5..57e6f4ab1e3 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -15,21 +15,83 @@ import { DropdownMenuTrigger, } from 'ui' -import { RESOURCE_WARNING_MESSAGES } from './ResourceExhaustionWarningBanner.constants' import { + isResourceWarningMessageKey, + RESOURCE_WARNING_MESSAGES, + type ResourceWarningMessageKey, +} from './ResourceExhaustionWarningBanner.constants' +import { + applyResourceList, + getResourceWarningAiPrompt, getResourceWarningCorrectionUrl, + getTroubleshootItems, getWarningContent, isComputeUpgradeWarning, + type TroubleshootItem, } from './ResourceExhaustionWarningBanner.utils' import { mapComputeSizeNameToAddonVariantId } from '@/components/interfaces/DiskManagement/DiskManagement.utils' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { useShowDiskIOBurstBalanceChart } from '@/hooks/misc/useShowDiskIOBurstBalanceChart' import { useTrack } from '@/lib/telemetry/track' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' +type LinkedTroubleshootItem = Extract +type ConfiguredResourceWarning = Exclude + +const isConfiguredResourceWarning = (property: string): property is ConfiguredResourceWarning => + isResourceWarningMessageKey(property) && property !== 'multiple_resource_warnings' + +const MULTIPLE_RESOURCE_WARNING_NO_AI_DESCRIPTION = + 'Use the Troubleshoot menu to review the affected resources and resolve these warnings.' +const MULTIPLE_RESOURCE_MIXED_SEVERITY_TITLE = + "Resource warnings for {resources} are affecting your project's performance" + +const TroubleshootMenuItem = ({ + item, + onTroubleshootClick, + onAskAI, +}: { + item: TroubleshootItem + onTroubleshootClick: (item: LinkedTroubleshootItem) => void + onAskAI: () => void +}) => { + if (item.kind === 'ai') { + return ( + + + {item.menuLabel} + + ) + } + if (item.kind === 'metrics') { + return ( + onTroubleshootClick(item)}> + + + {item.menuLabel} + + + ) + } + return ( + onTroubleshootClick(item)}> + + + {item.menuLabel} + + + ) +} + export const ResourceExhaustionWarningBanner = () => { const { ref } = useParams() const router = useRouter() @@ -45,6 +107,7 @@ export const ResourceExhaustionWarningBanner = () => { const { openSidebar } = useSidebarManagerSnapshot() const aiSnap = useAiAssistantStateSnapshot() const track = useTrack() + const showBurstBalanceChart = useShowDiskIOBurstBalanceChart() const { data: resourceWarnings } = useResourceWarningsQuery({ ref: ref }) // [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref const projectResourceWarnings = (resourceWarnings ?? [])?.find( @@ -52,62 +115,65 @@ export const ResourceExhaustionWarningBanner = () => { ) // [Joshen] Read only takes higher precedence over multiple resource warnings - const activeWarnings = + const activeWarnings: ConfiguredResourceWarning[] = projectResourceWarnings !== undefined ? projectResourceWarnings.is_readonly_mode_enabled ? ['is_readonly_mode_enabled'] : Object.keys(projectResourceWarnings).filter( - (property) => + (property): property is ConfiguredResourceWarning => property !== 'project' && property !== 'is_readonly_mode_enabled' && - projectResourceWarnings[property as keyof typeof projectResourceWarnings] !== null + isConfiguredResourceWarning(property) && + projectResourceWarnings[property] !== null ) : [] const hasCriticalWarning = projectResourceWarnings !== undefined - ? activeWarnings.some( - (x) => projectResourceWarnings[x as keyof typeof projectResourceWarnings] === 'critical' - ) + ? activeWarnings.some((warningType) => projectResourceWarnings[warningType] === 'critical') : false + const hasWarningSeverity = + projectResourceWarnings !== undefined + ? activeWarnings.some((warningType) => projectResourceWarnings[warningType] === 'warning') + : false + const hasMixedWarningSeverities = hasCriticalWarning && hasWarningSeverity const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning const warningContent = projectResourceWarnings !== undefined ? getWarningContent(projectResourceWarnings, activeWarnings[0], 'bannerContent') : undefined + const aiPrompt = getResourceWarningAiPrompt(activeWarnings) + const multipleWarningDescription = + aiPrompt === undefined + ? MULTIPLE_RESOURCE_WARNING_NO_AI_DESCRIPTION + : RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[ + hasCriticalWarning ? 'critical' : 'warning' + ].description + const multipleWarningTitle = hasMixedWarningSeverities + ? MULTIPLE_RESOURCE_MIXED_SEVERITY_TITLE + : RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[ + hasCriticalWarning ? 'critical' : 'warning' + ].title const title = applyDiskIoBaseline( - activeWarnings.length > 1 - ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[ - hasCriticalWarning ? 'critical' : 'warning' - ].title - : warningContent?.title + applyResourceList( + activeWarnings.length > 1 ? multipleWarningTitle : warningContent?.title, + activeWarnings + ) ) const description = applyDiskIoBaseline( - activeWarnings.length > 1 - ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[ - hasCriticalWarning ? 'critical' : 'warning' - ].description - : warningContent?.description + applyResourceList( + activeWarnings.length > 1 ? multipleWarningDescription : warningContent?.description, + activeWarnings + ) ) - const learnMoreUrl = - activeWarnings.length > 1 - ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.docsUrl - : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] - ?.docsUrl - - const singleWarningMessage = - activeWarnings.length === 1 ? RESOURCE_WARNING_MESSAGES[activeWarnings[0]] : undefined - const metricsHref = singleWarningMessage?.metricsHref?.replace('[ref]', ref ?? 'default') - const metric = activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.metric - : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] - ?.metric + : RESOURCE_WARNING_MESSAGES[activeWarnings[0]]?.metric const isFreePlan = organization?.plan?.id === 'free' @@ -126,17 +192,16 @@ export const ResourceExhaustionWarningBanner = () => { if (isComputeUpgradeMetric) return 'Upgrade compute' return activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.buttonText - : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] - ?.buttonText + : RESOURCE_WARNING_MESSAGES[activeWarnings[0]]?.buttonText })() - const aiPrompt = - activeWarnings.length > 1 - ? isComputeUpgradeMetric - ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.aiPrompt - : undefined - : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] - ?.aiPrompt + const troubleshootItems = getTroubleshootItems({ + activeWarnings, + projectRef: ref ?? 'default', + aiPrompt, + showBurstBalanceChart, + }) + const soleTroubleshootItem = troubleshootItems.length === 1 ? troubleshootItems[0] : undefined const handleAskAI = () => { track('resource_exhaustion_banner_ai_assistant_clicked', { @@ -146,6 +211,15 @@ export const ResourceExhaustionWarningBanner = () => { aiSnap.newChat({ initialInput: aiPrompt }) } + const handleTroubleshootClick = (item: LinkedTroubleshootItem) => { + track('resource_exhaustion_banner_troubleshoot_clicked', { + troubleshootAction: item.kind, + warningType: item.warningType, + warningTypes: activeWarnings, + destination: item.href, + }) + } + const hasNoWarnings = activeWarnings.length === 0 const hasNoWarningContent = warningContent === undefined || (!warningContent?.title && !warningContent?.description) @@ -163,8 +237,7 @@ export const ResourceExhaustionWarningBanner = () => { (activeWarnings.includes('is_readonly_mode_enabled') || (isComputeUpgradeMetric && !isFreePlan)) // these take precedence over each other, so there's only one active warning to check - const activeWarning = - RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] + const activeWarning = RESOURCE_WARNING_MESSAGES[activeWarnings[0]] const restrictToRoutes = activeWarning?.restrictToRoutes const isVisible = @@ -206,7 +279,7 @@ export const ResourceExhaustionWarningBanner = () => { {description}
- {learnMoreUrl !== undefined && aiPrompt !== undefined ? ( + {troubleshootItems.length >= 2 && ( - {metricsHref !== undefined && ( - - - - View metrics - - - )} - - - - Documentation - - - - - Ask AI Assistant - + {troubleshootItems.map((item) => ( + + ))} - ) : learnMoreUrl !== undefined ? ( - + )} + {soleTroubleshootItem?.kind === 'docs' && ( + - ) : aiPrompt !== undefined ? ( - - ) : null} + )} + {soleTroubleshootItem?.kind === 'ai' && ( + + )} {correctionUrl !== undefined && (