mirror of
https://github.com/supabase/supabase.git
synced 2026-06-10 04:26:19 +08:00
## 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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46203?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { Gauge, Inbox, Shield } from 'lucide-react'
|
|
import type { ElementType } from 'react'
|
|
|
|
import type { AdvisorItem, AdvisorLintItem, AdvisorNotificationItem } from './AdvisorPanel.types'
|
|
import { lintInfoMap } from '@/components/interfaces/Linter/Linter.utils'
|
|
import type { Lint } from '@/data/lint/lint-query'
|
|
import type { Notification, NotificationData } from '@/data/notifications/notifications-v2-query'
|
|
import type { AdvisorSeverity, AdvisorTab } from '@/state/advisor-state'
|
|
|
|
export const MAX_HOMEPAGE_ADVISOR_ITEMS = 4
|
|
|
|
export const severityOrder: Record<AdvisorSeverity, number> = {
|
|
critical: 0,
|
|
warning: 1,
|
|
info: 2,
|
|
}
|
|
|
|
export const lintLevelToSeverity = (level: Lint['level']): AdvisorSeverity => {
|
|
switch (level) {
|
|
case 'ERROR':
|
|
return 'critical'
|
|
case 'WARN':
|
|
return 'warning'
|
|
default:
|
|
return 'info'
|
|
}
|
|
}
|
|
|
|
export const notificationPriorityToSeverity = (
|
|
priority: string | null | undefined
|
|
): AdvisorSeverity => {
|
|
switch (priority) {
|
|
case 'Critical':
|
|
return 'critical'
|
|
case 'Warning':
|
|
return 'warning'
|
|
default:
|
|
return 'info'
|
|
}
|
|
}
|
|
|
|
export const createAdvisorLintItems = (lintData?: Lint[]): AdvisorLintItem[] => {
|
|
if (!lintData) return []
|
|
|
|
return lintData
|
|
.map((lint): AdvisorLintItem | null => {
|
|
const categories = lint.categories || []
|
|
const tab = categories.includes('SECURITY')
|
|
? ('security' as const)
|
|
: categories.includes('PERFORMANCE')
|
|
? ('performance' as const)
|
|
: undefined
|
|
|
|
if (!tab) return null
|
|
|
|
return {
|
|
id: lint.cache_key,
|
|
title: lint.detail,
|
|
severity: lintLevelToSeverity(lint.level),
|
|
createdAt: undefined,
|
|
tab,
|
|
source: 'lint',
|
|
original: lint,
|
|
}
|
|
})
|
|
.filter((item): item is AdvisorLintItem => item !== null)
|
|
}
|
|
|
|
export const createAdvisorNotificationItems = (
|
|
notifications?: Notification[]
|
|
): AdvisorNotificationItem[] => {
|
|
if (!notifications) return []
|
|
|
|
return notifications.map((notification) => {
|
|
const data = notification.data as NotificationData
|
|
|
|
return {
|
|
id: notification.id,
|
|
title: data.title,
|
|
severity: notificationPriorityToSeverity(notification.priority),
|
|
createdAt: dayjs(notification.inserted_at).valueOf(),
|
|
tab: 'messages' as const,
|
|
source: 'notification' as const,
|
|
original: notification,
|
|
project_ref: data.project_ref,
|
|
}
|
|
})
|
|
}
|
|
|
|
export const sortAdvisorItems = <T extends AdvisorItem>(items: T[]) => {
|
|
return [...items].sort((a, b) => {
|
|
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity]
|
|
if (severityDiff !== 0) return severityDiff
|
|
|
|
const createdDiff = (b.createdAt ?? 0) - (a.createdAt ?? 0)
|
|
if (createdDiff !== 0) return createdDiff
|
|
|
|
return getAdvisorItemDisplayTitle(a).localeCompare(getAdvisorItemDisplayTitle(b))
|
|
})
|
|
}
|
|
|
|
export const formatItemDate = (timestamp: number): string => {
|
|
const daysFromNow = dayjs().diff(dayjs(timestamp), 'day')
|
|
const formattedTimeFromNow = dayjs(timestamp).fromNow()
|
|
const formattedInsertedAt = dayjs(timestamp).format('MMM DD, YYYY')
|
|
return daysFromNow > 1 ? formattedInsertedAt : formattedTimeFromNow
|
|
}
|
|
|
|
export const getAdvisorItemDisplayTitle = (item: AdvisorItem): string => {
|
|
if (item.source === 'lint') {
|
|
return (
|
|
lintInfoMap.find((info) => info.name === item.original.name)?.title ||
|
|
item.title.replace(/[`\\]/g, '')
|
|
)
|
|
}
|
|
|
|
if (item.source === 'signal') {
|
|
return `${item.title}`
|
|
}
|
|
|
|
return item.title.replace(/[`\\]/g, '')
|
|
}
|
|
|
|
export const getAdvisorPanelItemDisplayTitle = (item: AdvisorItem): string => {
|
|
if (item.source === 'signal') {
|
|
return item.title
|
|
}
|
|
|
|
return getAdvisorItemDisplayTitle(item)
|
|
}
|
|
|
|
export const getAdvisorItemSecondaryText = (
|
|
item: AdvisorItem,
|
|
projectNameByRef?: ReadonlyMap<string, string>
|
|
): string | undefined => {
|
|
if (item.source === 'lint') {
|
|
return getLintEntityString(item.original)
|
|
}
|
|
|
|
if (item.source === 'signal') {
|
|
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
|
|
}
|
|
|
|
export const tabIconMap: Record<Exclude<AdvisorTab, 'all'>, ElementType> = {
|
|
security: Shield,
|
|
performance: Gauge,
|
|
messages: Inbox,
|
|
}
|
|
|
|
export const severityColorClasses: Record<AdvisorSeverity, string> = {
|
|
critical: 'text-destructive',
|
|
warning: 'text-warning',
|
|
info: 'text-foreground-light',
|
|
}
|
|
|
|
export const severityBadgeVariants: Record<AdvisorSeverity, 'destructive' | 'warning' | 'default'> =
|
|
{
|
|
critical: 'destructive',
|
|
warning: 'warning',
|
|
info: 'default',
|
|
}
|
|
|
|
export const severityLabels: Record<AdvisorSeverity, string> = {
|
|
critical: 'Critical',
|
|
warning: 'Warning',
|
|
info: 'Info',
|
|
}
|
|
|
|
export const getLintEntityString = (lint: Lint | null): string | undefined => {
|
|
if (!lint?.metadata) {
|
|
return undefined
|
|
}
|
|
|
|
if (lint.metadata.entity) {
|
|
return lint.metadata.entity
|
|
}
|
|
|
|
if (lint.metadata.schema && lint.metadata.name) {
|
|
const extendedMetadata = lint.metadata as typeof lint.metadata & { arguments?: string }
|
|
const args =
|
|
typeof extendedMetadata.arguments === 'string' ? extendedMetadata.arguments : undefined
|
|
return `${lint.metadata.schema}.${lint.metadata.name}${args !== undefined ? `(${args})` : ''}`
|
|
}
|
|
|
|
return undefined
|
|
}
|