mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## 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 - Add a Health Advisor page at `/project/[ref]/advisors/health` - Put Health Advisor first in the Advisors left nav (above Security), platform-only - Register `V` then `H` and a command-menu entry Stacked on #49662. Top of the stack. ## To test 1. Open any project in Studio. 2. Click **Advisors** in the main nav (or go to `/project/<ref>/advisors/security`). 3. In the left nav, confirm the order is **Health Advisor**, then Security Advisor, then Performance Advisor, then Query Performance. 4. Click **Health Advisor**. You should land on a page titled “Health Advisor” with Errors / Warnings / Info tabs, same layout as Security Advisor. 5. If the project is healthy, Errors should say no errors were detected. If it is not, the failing checks should list here (database down, connection limit, and so on). 6. Click **Refresh** (or Shift+R) and confirm the list reloads. 7. Click a row and confirm the detail panel opens with a link through to logs, connections, or infrastructure. 8. While still in Advisors, press **V** then **H**. You should jump back to Health Advisor. 9. Open the command menu and search **Health Advisor**. Choosing it should navigate to this page. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a Health Advisor page for reviewing project health findings by severity and category. - Added Health Advisor navigation in the advisor menu and a keyboard shortcut (`V`, then `H`) on supported platforms. - Added refresh, filtering, selection, and lint detail navigation for health findings. - **Bug Fixes** - Added validation for linter severity values, safely handling unsupported or missing inputs. - **Documentation** - Updated migration and shortcut documentation to include the Health Advisor. <!-- 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>
101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
import { Ruler } from 'lucide-react'
|
|
import { isValidElement } from 'react'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { lintInfoMap, parseLinterLevel } from './Linter.utils'
|
|
import { LINTER_LEVELS } from '@/components/interfaces/Linter/Linter.constants'
|
|
import { Lint } from '@/data/lint/lint-query'
|
|
|
|
const projectRef = 'abc'
|
|
const trickySchema = 'a&b=c'
|
|
const trickyName = 'd e+f'
|
|
|
|
describe('Linter.utils lintInfoMap link encoding', () => {
|
|
const cases: Array<{ name: string; nameParam?: string; hasSchema: boolean }> = [
|
|
{ name: 'unindexed_foreign_keys', hasSchema: true },
|
|
{ name: 'unused_index', nameParam: 'table', hasSchema: true },
|
|
{ name: 'multiple_permissive_policies', nameParam: 'search', hasSchema: true },
|
|
{ name: 'policy_exists_rls_disabled', nameParam: 'search', hasSchema: true },
|
|
{ name: 'rls_enabled_no_policy', nameParam: 'search', hasSchema: true },
|
|
{ name: 'duplicate_index', nameParam: 'table', hasSchema: true },
|
|
{ name: 'function_search_path_mutable', nameParam: 'search', hasSchema: true },
|
|
{ name: 'rls_disabled_in_public', nameParam: 'search', hasSchema: true },
|
|
{ name: 'extension_in_public', nameParam: 'filter', hasSchema: false },
|
|
{ name: 'sensitive_columns_exposed', nameParam: 'table', hasSchema: true },
|
|
{ name: 'rls_policy_always_true', nameParam: 'search', hasSchema: true },
|
|
{ name: 'pg_graphql_anon_table_exposed', nameParam: 'table', hasSchema: true },
|
|
{ name: 'pg_graphql_authenticated_table_exposed', nameParam: 'table', hasSchema: true },
|
|
{ name: 'anon_security_definer_function_executable', nameParam: 'search', hasSchema: true },
|
|
{
|
|
name: 'authenticated_security_definer_function_executable',
|
|
nameParam: 'search',
|
|
hasSchema: true,
|
|
},
|
|
]
|
|
|
|
for (const { name, nameParam, hasSchema } of cases) {
|
|
it(`preserves special characters in metadata for ${name}`, () => {
|
|
const info = lintInfoMap.find((entry) => entry.name === name)
|
|
expect(info, `expected ${name} in lintInfoMap`).toBeDefined()
|
|
|
|
const url = info!.link({
|
|
projectRef,
|
|
metadata: {
|
|
schema: trickySchema,
|
|
name: trickyName,
|
|
} as unknown as Lint['metadata'],
|
|
})
|
|
|
|
const parsed = new URL(url, 'http://example.com')
|
|
|
|
if (hasSchema) {
|
|
expect(parsed.searchParams.get('schema')).toBe(trickySchema)
|
|
}
|
|
if (nameParam) {
|
|
expect(parsed.searchParams.get(nameParam)).toBe(trickyName)
|
|
}
|
|
})
|
|
}
|
|
|
|
it('preserves slash and space in bucket_id for public_bucket_allows_listing', () => {
|
|
const info = lintInfoMap.find((entry) => entry.name === 'public_bucket_allows_listing')
|
|
expect(info).toBeDefined()
|
|
const url = info!.link({
|
|
projectRef,
|
|
metadata: {
|
|
bucket_id: 'a/b c',
|
|
} as unknown as Lint['metadata'],
|
|
})
|
|
expect(url).toBe('/project/abc/storage/files/buckets/a%2Fb%20c')
|
|
})
|
|
})
|
|
|
|
describe('Linter.utils lintInfoMap pitr_archiving_stale entry', () => {
|
|
it('registers a security entry linking to the PITR settings page', () => {
|
|
const info = lintInfoMap.find((entry) => entry.name === 'pitr_archiving_stale')
|
|
expect(info, 'expected pitr_archiving_stale in lintInfoMap').toBeDefined()
|
|
|
|
expect(info!.title).toBe('PITR archiving may be broken')
|
|
expect(isValidElement(info!.icon) && info!.icon.type).toBe(Ruler)
|
|
expect(info!.category).toBe('security')
|
|
expect(info!.linkText).toBe('View settings')
|
|
// metadata is unused by this entry's link(), and every field on Lint['metadata'] is optional, so {} needs no cast
|
|
expect(info!.link({ projectRef, metadata: {} })).toBe('/project/abc/database/backups/pitr')
|
|
expect(info!.docsLink).toContain('/guides/platform/backups#point-in-time-recovery')
|
|
})
|
|
})
|
|
|
|
describe('parseLinterLevel', () => {
|
|
it('returns the matching level', () => {
|
|
expect(parseLinterLevel('ERROR')).toBe(LINTER_LEVELS.ERROR)
|
|
expect(parseLinterLevel('WARN')).toBe(LINTER_LEVELS.WARN)
|
|
expect(parseLinterLevel('INFO')).toBe(LINTER_LEVELS.INFO)
|
|
})
|
|
|
|
it('returns undefined for values that are not a level', () => {
|
|
expect(parseLinterLevel('error')).toBeUndefined()
|
|
expect(parseLinterLevel('SOMETHING_ELSE')).toBeUndefined()
|
|
expect(parseLinterLevel(undefined)).toBeUndefined()
|
|
})
|
|
})
|