mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +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 - 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>
107 lines
3.6 KiB
TypeScript
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,
|
|
}
|
|
}
|