Files
supabase/apps/studio/components/ui/AdvisorPanel/useAdvisorSignals.ts
T
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

107 lines
3.6 KiB
TypeScript

import { useCallback, useEffect, useMemo } from 'react'
import type { AdvisorSignalItem } from './AdvisorPanel.types'
import { useBannedIPsQuery } from '@/data/banned-ips/banned-ips-query'
import type { IPData } from '@/data/banned-ips/banned-ips-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
const createDismissalStorageKey = (projectRef: string) => `advisor-signal-dismissals:${projectRef}`
const createBannedIPDismissalKey = (ip: string) => `signal:banned-ip:${ip}:v1`
const createBannedIPSignalItems = ({
projectRef,
bannedIPsData,
}: {
projectRef?: string
bannedIPsData?: IPData
}): AdvisorSignalItem[] => {
if (!projectRef) return []
const bannedIPs = bannedIPsData?.banned_ipv4_addresses ?? []
return bannedIPs.map((ip) => ({
id: createBannedIPDismissalKey(ip),
dismissalKey: createBannedIPDismissalKey(ip),
source: 'signal' as const,
type: 'banned-ip' as const,
severity: 'warning' as const,
category: 'security' as const,
title: 'Banned IP address',
summary: `The IP address \`${ip}\` is temporarily blocked because of suspicious traffic or repeated failed password attempts.`,
description:
'This IP address is temporarily blocked because of suspicious traffic or repeated failed password attempts. If this block is expected, you can dismiss this signal or remove the ban.',
docsUrl: 'https://supabase.com/docs/reference/cli/supabase-network-bans',
actions: [
{
label: 'Edit network bans',
href: `/project/${projectRef}/database/settings#banned-ips`,
},
],
sourceData: { type: 'banned-ip' as const, ip },
}))
}
interface UseAdvisorSignalsOptions {
projectRef?: string
enabled?: boolean
}
export const useAdvisorSignals = ({ projectRef, enabled = true }: UseAdvisorSignalsOptions) => {
const { data, isPending, isError } = useBannedIPsQuery({ projectRef }, { enabled })
const storageKey = projectRef
? createDismissalStorageKey(projectRef)
: 'advisor-signal-dismissals:unknown-project'
const [dismissedKeys, setDismissedKeys] = useLocalStorageQuery<string[]>(storageKey, [])
const dismissedKeySet = useMemo(() => new Set(dismissedKeys), [dismissedKeys])
const dismissSignal = useCallback(
(dismissalKey: string) => {
setDismissedKeys((current) =>
current.includes(dismissalKey) ? current : [...current, dismissalKey]
)
},
[setDismissedKeys]
)
const signalItems = useMemo(
() => createBannedIPSignalItems({ projectRef, bannedIPsData: data }),
[projectRef, data]
)
// Prune stale dismissals when the active signal list changes (e.g. an IP was unbanned).
// Only call the setter when pruning would actually change something — otherwise we
// churn subscribers unnecessarily, which can cause feedback loops when this hook is
// mounted in more than one place (AdvisorSection + AdvisorPanel).
useEffect(() => {
if (!data) return
const hasStaleBannedIPDismissal = dismissedKeys.some(
(key) =>
key.startsWith('signal:banned-ip:') &&
!signalItems.some((item) => item.dismissalKey === key)
)
if (!hasStaleBannedIPDismissal) return
const activeKeys = new Set(signalItems.map((item) => item.dismissalKey))
setDismissedKeys((current) =>
current.filter((key) => (key.startsWith('signal:banned-ip:') ? activeKeys.has(key) : true))
)
}, [data, signalItems, dismissedKeys, setDismissedKeys])
const formattedData = useMemo(
() => signalItems.filter((item) => !dismissedKeySet.has(item.dismissalKey)),
[signalItems, dismissedKeySet]
)
return {
data: formattedData,
dismissSignal,
isPending,
isError,
}
}