From 4c07df1a48501c8484001bc336a5dc199256cdb5 Mon Sep 17 00:00:00 2001 From: Mert YEREKAPAN <33198490+myerekapan@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:18:36 +0200 Subject: [PATCH 01/23] feat(studio): surface affected project in metric advisories (#46203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Resolves [GROWTH-865](https://linear.app/supabase/issue/GROWTH-865) on the Studio side. Companion backend PR: [supabase/platform#33086](https://github.com/supabase/platform/pull/33086). Resource-exhaustion advisories (CPU, Disk IO, Memory) currently give users a list of identical-looking messages with no project context. This PR makes the affected project visible in the advisor panel and hardens the "Check consumption" deep-link. ### Changes **Advisor list view** — `AdvisorPanel.utils.ts`, `AdvisorPanel.types.ts`, `AdvisorPanel.tsx`, `AdvisorPanelBody.tsx` - `AdvisorNotificationItem` now carries `project_ref`. - `getAdvisorItemSecondaryText` accepts an optional `projectNameByRef` map and returns the resolved project name (falling back to the ref if the lookup hasn't loaded). Falls through to the existing date string for notifications without a project. - `AdvisorPanel.tsx` builds the map from `useProjectsInfiniteQuery` and threads it through `AdvisorPanelBody`. **NotificationDetail** — `NotificationDetail.tsx` - `[ref]` / `[slug]` substitution in action URLs falls back to `data.project_ref` / `data.org_slug` before the `_` literal. This fixes the universal-link bug Tim reported where "Check consumption" sometimes resolved to `/project/_/...` while `useProjectDetailQuery` was still loading. The companion backend PR updates the notification copy so the title/message also name the project. Both PRs degrade gracefully if landed independently. ## Test plan - [x] `pnpm test:studio -- AdvisorPanel.utils.test.ts` — 6/6 pass (4 new tests cover the notification branch of `getAdvisorItemSecondaryText`) - [x] `pnpm typecheck` passes for apps/studio - [x] `pnpm exec eslint components/ui/AdvisorPanel/` passes - [ ] Local Studio smoke test: open Advisor → Messages, confirm project name shows under each notification and "Check consumption" deep-links to the correct project even if the project detail query is slow ## Summary by CodeRabbit * **New Features** * Advisor Panel notifications now display resolved project names when available for clearer context. * **Bug Fixes** * Notification action URLs now prefer stored project and organization refs/slugs with improved fallbacks, making action links more reliable. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46203?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- .../ui/AdvisorPanel/AdvisorPanel.tsx | 14 +++++++ .../ui/AdvisorPanel/AdvisorPanel.types.ts | 1 + .../AdvisorPanel/AdvisorPanel.utils.test.ts | 40 +++++++++++++++++++ .../ui/AdvisorPanel/AdvisorPanel.utils.ts | 11 ++++- .../ui/AdvisorPanel/AdvisorPanelBody.tsx | 4 +- .../ui/AdvisorPanel/NotificationDetail.tsx | 4 +- 6 files changed, 70 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx index fde0e64638f..7622d057825 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx @@ -15,6 +15,7 @@ import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/L import { useProjectLintsQuery } from '@/data/lint/lint-query' import { Notification, useNotificationsV2Query } from '@/data/notifications/notifications-v2-query' import { useNotificationsV2UpdateMutation } from '@/data/notifications/notifications-v2-update-mutation' +import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { IS_PLATFORM } from '@/lib/constants' import { useTrack } from '@/lib/telemetry/track' @@ -95,6 +96,18 @@ export const AdvisorPanel = () => { return notificationsData?.pages.flatMap((page) => page) ?? [] }, [notificationsData?.pages]) + const { data: projectsData } = useProjectsInfiniteQuery({}, { enabled: shouldLoadNotifications }) + + const projectNameByRef = useMemo(() => { + const map = new Map() + projectsData?.pages.forEach((page) => { + page.projects.forEach((project) => { + if (project.ref) map.set(project.ref, project.name) + }) + }) + return map + }, [projectsData?.pages]) + const markNotificationsRead = () => { if (markedRead.current.length > 0) { updateNotifications({ ids: markedRead.current, status: 'seen' }) @@ -282,6 +295,7 @@ export const AdvisorPanel = () => { hiddenItemsCount={hiddenItemsCount} hasAnyFilters={hasAnyFilters} hasProjectRef={hasProjectRef} + projectNameByRef={projectNameByRef} /> diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts index c9281727c77..17f8fd43ea2 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts @@ -26,6 +26,7 @@ export type AdvisorLintItem = AdvisorBaseItem & { export type AdvisorNotificationItem = AdvisorBaseItem & { source: 'notification' original: Notification + project_ref?: string } export type AdvisorSignalItem = AdvisorBaseItem & { diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.test.ts b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.test.ts index d9e9eb5eff3..23366adc4ec 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.test.ts +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.test.ts @@ -71,4 +71,44 @@ describe('AdvisorPanel.utils', () => { const bannedIpSignal = createBannedIPSignalItem('203.0.113.10') expect(getAdvisorItemSecondaryText(bannedIpSignal)).toBe('Database · 203.0.113.10') }) + + describe('notification secondary text', () => { + const [notificationWithProject] = createAdvisorNotificationItems([ + createNotification({ + id: 'notification-with-project', + data: { + title: 'CPU usage is high on my-project.', + message: 'Project my-project has high CPU usage.', + project_ref: 'abcd1234', + actions: [], + }, + }), + ]) + + const [notificationWithoutProject] = createAdvisorNotificationItems([ + createNotification({ + id: 'notification-without-project', + data: { title: 'Generic notification', message: 'Body', actions: [] }, + }), + ]) + + it('returns the resolved project name when available in the map', () => { + const projectNameByRef = new Map([['abcd1234', 'my-production-db']]) + expect(getAdvisorItemSecondaryText(notificationWithProject, projectNameByRef)).toBe( + 'my-production-db' + ) + }) + + it('falls back to the project ref when the name is missing from the map', () => { + expect(getAdvisorItemSecondaryText(notificationWithProject, new Map())).toBe('abcd1234') + }) + + it('falls back to the project ref when no map is provided', () => { + expect(getAdvisorItemSecondaryText(notificationWithProject)).toBe('abcd1234') + }) + + it('returns undefined for notifications without a project_ref', () => { + expect(getAdvisorItemSecondaryText(notificationWithoutProject)).toBeUndefined() + }) + }) }) diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts index dc0f8a9a765..9ee683d0e20 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts @@ -83,6 +83,7 @@ export const createAdvisorNotificationItems = ( tab: 'messages' as const, source: 'notification' as const, original: notification, + project_ref: data.project_ref, } }) } @@ -129,7 +130,10 @@ export const getAdvisorPanelItemDisplayTitle = (item: AdvisorItem): string => { return getAdvisorItemDisplayTitle(item) } -export const getAdvisorItemSecondaryText = (item: AdvisorItem): string | undefined => { +export const getAdvisorItemSecondaryText = ( + item: AdvisorItem, + projectNameByRef?: ReadonlyMap +): string | undefined => { if (item.source === 'lint') { return getLintEntityString(item.original) } @@ -138,6 +142,11 @@ export const getAdvisorItemSecondaryText = (item: AdvisorItem): string | undefin return `Database · ${item.sourceData.ip}` } + if (item.source === 'notification') { + if (!item.project_ref) return undefined + return projectNameByRef?.get(item.project_ref) ?? item.project_ref + } + return undefined } diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx b/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx index dda95a37fa0..af5efd141a9 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx @@ -41,6 +41,7 @@ interface AdvisorPanelBodyProps { hiddenItemsCount: number hasAnyFilters: boolean hasProjectRef?: boolean + projectNameByRef?: ReadonlyMap } export const AdvisorPanelBody = ({ @@ -54,6 +55,7 @@ export const AdvisorPanelBody = ({ hiddenItemsCount, hasAnyFilters, hasProjectRef = true, + projectNameByRef, }: AdvisorPanelBodyProps) => { // Show notice if no project ref and trying to view project-specific tabs if (!hasProjectRef && activeTab !== 'messages' && activeTab !== 'all') { @@ -101,7 +103,7 @@ export const AdvisorPanelBody = ({ const isUnread = notification?.status === 'new' const primaryText = getAdvisorPanelItemDisplayTitle(item) - const secondaryText = getAdvisorItemSecondaryText(item) + const secondaryText = getAdvisorItemSecondaryText(item, projectNameByRef) const metadataText = secondaryText ?? (item.createdAt ? formatItemDate(item.createdAt) : undefined) // Date strings (e.g. "a few seconds ago") come from formatItemDate and diff --git a/apps/studio/components/ui/AdvisorPanel/NotificationDetail.tsx b/apps/studio/components/ui/AdvisorPanel/NotificationDetail.tsx index b7e6606a163..acae061fdd4 100644 --- a/apps/studio/components/ui/AdvisorPanel/NotificationDetail.tsx +++ b/apps/studio/components/ui/AdvisorPanel/NotificationDetail.tsx @@ -70,9 +70,9 @@ export const NotificationDetail = ({ notification, onUpdateStatus }: Notificatio const key = `${notification.id}-action-${idx}` if (action.url !== undefined) { const url = action.url.includes('[ref]') - ? action.url.replace('[ref]', project?.ref ?? '_') + ? action.url.replace('[ref]', project?.ref ?? data.project_ref ?? '_') : action.url.includes('[slug]') - ? action.url.replace('[slug]', organization?.slug ?? '_') + ? action.url.replace('[slug]', organization?.slug ?? data.org_slug ?? '_') : action.url return (