Files
supabase/apps/studio/state/advisor-state.ts
b5daafd264 feat(studio): add health category to the advisor panel (#49662)
## 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>
2026-09-03 11:22:35 +02:00

83 lines
3.1 KiB
TypeScript

import { proxy, snapshot, useSnapshot } from 'valtio'
import { z } from 'zod'
export const advisorCategorySchema = z.enum(['security', 'performance', 'health', 'messages'])
export const advisorSeveritySchema = z.enum(['critical', 'warning', 'info'])
export type AdvisorCategory = z.infer<typeof advisorCategorySchema>
export type AdvisorSeverity = z.infer<typeof advisorSeveritySchema>
export type AdvisorItemSource = 'lint' | 'notification' | 'signal'
const createInitialState = () => ({
// An empty selection means every category is shown, matching the severity filter
categoryFilters: [] as AdvisorCategory[],
severityFilters: ['critical', 'warning'] as AdvisorSeverity[],
selectedItemId: undefined as string | undefined,
selectedItemSource: undefined as AdvisorItemSource | undefined,
// Notification filters
notificationFilterStatuses: [] as string[],
notificationFilterPriorities: [] as string[],
})
export const advisorState = proxy({
...createInitialState(),
setCategoryFilters(categories: AdvisorCategory[]) {
advisorState.categoryFilters = categories
},
setSeverityFilters(severities: AdvisorSeverity[]) {
advisorState.severityFilters = severities
},
setSelectedItem(id: string | undefined, source?: AdvisorItemSource) {
advisorState.selectedItemId = id
advisorState.selectedItemSource = source
},
focusItem({ id, source }: { id: string; source?: AdvisorItemSource }) {
advisorState.selectedItemId = id
advisorState.selectedItemSource = source
},
setNotificationFilters: (value: string, type: 'status' | 'priority') => {
switch (type) {
case 'status':
if (advisorState.notificationFilterStatuses.includes(value)) {
advisorState.notificationFilterStatuses = advisorState.notificationFilterStatuses.filter(
(x) => x !== value
)
} else {
advisorState.notificationFilterStatuses = advisorState.notificationFilterStatuses.concat([
value,
])
}
break
case 'priority':
if (advisorState.notificationFilterPriorities.includes(value)) {
advisorState.notificationFilterPriorities =
advisorState.notificationFilterPriorities.filter((x) => x !== value)
} else {
advisorState.notificationFilterPriorities =
advisorState.notificationFilterPriorities.concat([value])
}
break
}
},
/** Clears the filters that hide items within the selected categories, but not the categories themselves. */
clearNarrowingFilters() {
advisorState.severityFilters = []
advisorState.notificationFilterStatuses = []
advisorState.notificationFilterPriorities = []
},
clearFilters() {
advisorState.categoryFilters = []
advisorState.severityFilters = []
advisorState.notificationFilterStatuses = []
advisorState.notificationFilterPriorities = []
},
reset() {
Object.assign(advisorState, createInitialState())
},
})
export const getAdvisorStateSnapshot = () => snapshot(advisorState)
export const useAdvisorStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(advisorState, options)