mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
b5daafd264
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature ## Summary - Replace advisor panel tabs with multi-select category filters, including Health - Load health lints in the advisor panel (without blocking other categories on the slower health request) - Rename item `tab` to `category` and add empty-state copy for health Stacked on #49661. ## To test 1. Open any project in Studio. 2. Open Advisor Center from the toolbar (the advisor / lightbulb control). 3. Confirm the old All / Security / Performance / Messages **tabs are gone**. You should see **Category**, **Status**, and **Severity** filters instead. 4. Open Category and confirm **Health** is in the list with Security, Performance, and Messages. 5. Select only **Health**: - If the project is healthy: empty state “No health issues detected” / “Your database, instance and services are all responding normally”. - If it is not: only health issues in the list. 6. Clear Health, then filter **Security** and **Performance** separately. Those lists should still match what you expect from before. 7. With Health selected, also filter Severity to **Info** only. If nothing matches, you should get “No items found” and a way to clear filters — not a false “no health issues” message. 8. From project home, click an advisor card. Advisor Center should still open on that same item. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added category-based filtering for Advisor recommendations, including Security, Performance, Health, and Messages. - Added Health issue recommendations and category-specific icons, labels, and empty-state messaging. - Advisor results now load according to the selected categories. - Added clearer project requirements and hidden-item controls for filtered results. - **Bug Fixes** - Invalid category and severity filter values are safely ignored. - Improved categorization and telemetry for Advisor items, including health and security recommendations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { AdvisorSignalItem } from './AdvisorPanel.types'
|
|
import {
|
|
createAdvisorLintItems,
|
|
createAdvisorNotificationItems,
|
|
getAdvisorItemSecondaryText,
|
|
getAdvisorItemTelemetryCategory,
|
|
sortAdvisorItems,
|
|
} from './AdvisorPanel.utils'
|
|
import type { Lint } from '@/data/lint/lint-query'
|
|
import type { Notification } from '@/data/notifications/notifications-v2-query'
|
|
|
|
const createLint = (overrides: Partial<Lint> = {}): Lint =>
|
|
({
|
|
cache_key: 'lint-1',
|
|
name: 'unknown_lint',
|
|
detail: 'Critical lint detail',
|
|
level: 'ERROR',
|
|
categories: ['SECURITY'],
|
|
metadata: {},
|
|
...overrides,
|
|
}) as Lint
|
|
|
|
const createNotification = (overrides: Partial<Notification> = {}): Notification =>
|
|
({
|
|
id: 'notification-1',
|
|
inserted_at: '2026-03-01T00:00:00.000Z',
|
|
priority: 'Info',
|
|
status: 'seen',
|
|
data: {
|
|
title: 'Notification title',
|
|
message: 'Notification body',
|
|
actions: [],
|
|
},
|
|
...overrides,
|
|
}) as Notification
|
|
|
|
const createBannedIPSignalItem = (ip: string): AdvisorSignalItem => ({
|
|
id: `signal:banned-ip:${ip}:v1`,
|
|
dismissalKey: `signal:banned-ip:${ip}:v1`,
|
|
source: 'signal',
|
|
type: 'banned-ip',
|
|
severity: 'warning',
|
|
category: 'security',
|
|
title: 'Banned IP address',
|
|
summary: `The IP address \`${ip}\` is temporarily blocked.`,
|
|
docsUrl: 'https://supabase.com/docs/reference/cli/supabase-network-bans',
|
|
actions: [],
|
|
sourceData: { type: 'banned-ip', ip },
|
|
})
|
|
|
|
describe('AdvisorPanel.utils', () => {
|
|
it('orders mixed lint, signal and notification items by severity and recency', () => {
|
|
const lintItems = createAdvisorLintItems([
|
|
createLint({ cache_key: 'lint-critical', detail: 'Critical lint detail' }),
|
|
])
|
|
const signalItems = [createBannedIPSignalItem('203.0.113.10')]
|
|
const notificationItems = createAdvisorNotificationItems([
|
|
createNotification({
|
|
id: 'notification-info',
|
|
data: { title: 'Notification title', message: 'Body', actions: [] },
|
|
}),
|
|
])
|
|
|
|
const sorted = sortAdvisorItems([...notificationItems, ...signalItems, ...lintItems])
|
|
|
|
expect(sorted.map((item) => item.source)).toEqual(['lint', 'signal', 'notification'])
|
|
})
|
|
|
|
it('uses database surface-area metadata and the IP address for banned IP signals', () => {
|
|
const bannedIpSignal = createBannedIPSignalItem('203.0.113.10')
|
|
expect(getAdvisorItemSecondaryText(bannedIpSignal)).toBe('Database · 203.0.113.10')
|
|
})
|
|
|
|
describe('lint categories', () => {
|
|
it('files a health lint under the health category', () => {
|
|
const [item] = createAdvisorLintItems([
|
|
createLint({
|
|
cache_key: 'instance_db_down',
|
|
name: 'instance_db_down',
|
|
categories: ['HEALTH'],
|
|
metadata: { type: 'health', entity: 'Database' },
|
|
}),
|
|
])
|
|
|
|
expect(item?.category).toBe('health')
|
|
expect(getAdvisorItemTelemetryCategory(item!)).toBe('HEALTH')
|
|
})
|
|
|
|
it('keeps security ahead of health when a lint carries both categories', () => {
|
|
const [item] = createAdvisorLintItems([
|
|
createLint({ cache_key: 'both', categories: ['HEALTH', 'SECURITY'] }),
|
|
])
|
|
|
|
expect(item?.category).toBe('security')
|
|
expect(getAdvisorItemTelemetryCategory(item!)).toBe('SECURITY')
|
|
})
|
|
|
|
it('drops lints with no recognised category', () => {
|
|
expect(
|
|
createAdvisorLintItems([createLint({ categories: [] as Lint['categories'] })])
|
|
).toEqual([])
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|
|
})
|