mirror of
https://github.com/supabase/supabase.git
synced 2026-05-23 19:13:13 +08:00
* feat(docs): main troubleshooting page * feat(docs): global troubleshooting page layout * feat(docs): global troubleshooting page filter logic * enhance(docs): troubleshooting page ui & filters Improve UX and accessibility of global troubleshooting search page. - Implement TroubleshootingFilterStateProvider for managing filter state - Add TroubleshootingFilterEmptyState component - Improve TroubleshootingFilter UI with applied filters display - Update TroubleshootingPreview component with better layout and accessibility - Add TroubleshootingEntryAssociatedErrors component for displaying related errors - Refactor Troubleshooting utils and schemas - Update troubleshooting content template * feat(docs): individual troubleshooting pages Add individual pages for detailed information on troubleshooting entries. Small fixes to UI for troubleshooting displays. - Wrap GlobalTroubleshootingPage with SidebarSkeleton - Add TroubleshootingErrorListDetailed component for detailed error display - Implement TroubleshootingBackLink component for navigation - Update TroubleshootingPreview to use Next.js Link and include parent page - Refactor Footer component to accept className prop - Add hideFooter option to SidebarSkeleton - Minor updates to TopNavBar and other utility functions * fix: minor styling issues * tweak(troubleshooting docs): use breadcrumbs * tweak(troubleshooting docs): use sidebar for filter * enhance(troubleshooting docs): navigation, keywords Better navigation for individual troubleshooting entries, using a version of the sidebar + keywords handling via search params for good linking behavior Details: - Add TroubleshootingSidebar to TroubleshootingPage - Implement query state management for keywords using nuqs - Improve accessibility and styling of troubleshooting components - Update empty state handling and related keywords display - Upgrade nuqs package to version 1.19.1 * fix(docs): wrap useSearchParams in Suspense * seo(docs): add canonical link to troubleshooting * enhance(docs,troubleshooting): auto-reveal errors On troubleshooting search pages, if the searched term matches a hidden error, auto-reveal it when searching. When the search term is cleared, hide programmatically-opened errors while preserving user-opened ones. * enhance(docs,troubleshooting): search bar Add search bar to individual troubleshooting pages * fix(docs): breadcrumb links Fix breadcrumb linking so it uses next/link instead of reloading entire app on navigation. * fix(docs): typo * enh(docs): new troubleshooting design * enh(docs): use new multi-select * enh(docs): troubleshooting responsive styles * Update apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx
197 lines
5.5 KiB
TypeScript
197 lines
5.5 KiB
TypeScript
'use client'
|
|
|
|
import { ChevronsUpDown } from 'lucide-react'
|
|
import {
|
|
createContext,
|
|
forwardRef,
|
|
useCallback,
|
|
useContext,
|
|
useId,
|
|
useState,
|
|
PropsWithChildren,
|
|
useRef,
|
|
useEffect,
|
|
} from 'react'
|
|
|
|
import {
|
|
cn,
|
|
Badge,
|
|
Checkbox_Shadcn_ as Checkbox,
|
|
Popover_Shadcn_ as Popover,
|
|
PopoverContent_Shadcn_ as PopoverContent,
|
|
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
|
} from 'ui'
|
|
|
|
interface Item {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
const MultiSelectContext = createContext<{
|
|
selected: Item[]
|
|
handleSelect: (item: Item) => void
|
|
setOpen: (open: boolean) => void
|
|
}>({
|
|
selected: [],
|
|
handleSelect: () => {},
|
|
setOpen: () => {},
|
|
})
|
|
|
|
function MultiSelectProvider({
|
|
selected,
|
|
onSelectedChange,
|
|
setOpen,
|
|
children,
|
|
}: PropsWithChildren<{
|
|
selected: Item[]
|
|
onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void
|
|
setOpen: (open: boolean) => void
|
|
}>) {
|
|
const handleSelect = useCallback(
|
|
(item: Item) => {
|
|
onSelectedChange((current: Item[]) => {
|
|
const isSelected = current.some((currItem) => currItem.value === item.value)
|
|
if (isSelected) {
|
|
return current.filter((currItem) => currItem.value !== item.value)
|
|
} else {
|
|
return [...current, item]
|
|
}
|
|
})
|
|
},
|
|
[onSelectedChange]
|
|
)
|
|
|
|
return (
|
|
<MultiSelectContext.Provider value={{ selected, handleSelect, setOpen }}>
|
|
{children}
|
|
</MultiSelectContext.Provider>
|
|
)
|
|
}
|
|
|
|
function useMultiSelect() {
|
|
const context = useContext(MultiSelectContext)
|
|
if (!context) {
|
|
throw new Error('useMultiSelect must be used within a MultiSelectProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
export function MultiSelect({
|
|
open: _controlledOpen,
|
|
setOpen: _setControlledOpen,
|
|
selected,
|
|
onSelectedChange,
|
|
children,
|
|
...props
|
|
}: PropsWithChildren<
|
|
{
|
|
open?: boolean
|
|
setOpen?: React.Dispatch<React.SetStateAction<boolean>>
|
|
selected?: Item[]
|
|
onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void
|
|
} & React.ComponentProps<typeof Popover>
|
|
>) {
|
|
const [_internalOpen, _setInternalOpen] = useState(false)
|
|
const open = _controlledOpen ?? _internalOpen
|
|
const setOpen = _setControlledOpen ?? _setInternalOpen
|
|
|
|
return (
|
|
<MultiSelectProvider selected={selected} onSelectedChange={onSelectedChange} setOpen={setOpen}>
|
|
<Popover open={open} onOpenChange={setOpen} {...props}>
|
|
{children}
|
|
</Popover>
|
|
</MultiSelectProvider>
|
|
)
|
|
}
|
|
|
|
const Trigger = forwardRef<
|
|
HTMLButtonElement,
|
|
{ label?: string; className?: string } & React.ComponentProps<typeof PopoverTrigger>
|
|
>(({ label, className, ...props }, ref) => {
|
|
const { selected } = useMultiSelect()
|
|
|
|
const [measuredWidth, setMeasuredWidth] = useState(0)
|
|
const measuredRef = (node: HTMLElement) => {
|
|
if (node) {
|
|
setMeasuredWidth(node.getBoundingClientRect().width)
|
|
}
|
|
}
|
|
|
|
const badgeWidth = 100 // Approximate width of a badge in pixels
|
|
const maxBadges = Math.max(1, Math.floor((measuredWidth || 200) / badgeWidth))
|
|
const visibleBadges = selected.slice(0, maxBadges)
|
|
const extraBadgesCount = Math.max(0, selected.length - maxBadges)
|
|
|
|
const badgeClasses = 'bg-200'
|
|
|
|
return (
|
|
<PopoverTrigger asChild ref={ref}>
|
|
<button
|
|
role="combobox"
|
|
className={cn(
|
|
'flex w-full min-w-[200px] items-center justify-between rounded-md border',
|
|
'border-alternative bg-foreground/[.026] px-3 py-2 text-sm',
|
|
'ring-offset-background placeholder:text-muted-foreground',
|
|
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
'hover:border-primary transition-colors duration-200',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{selected.length === 0 ? (
|
|
// Leading prevents shift when switching to badges
|
|
<span className="text-muted-foreground leading-[1.375rem]">{label}</span>
|
|
) : (
|
|
<div ref={measuredRef} className="flex gap-1 overflow-hidden">
|
|
{visibleBadges.map((item) => (
|
|
<Badge key={item.value} className={cn('shrink-0', badgeClasses)}>
|
|
{item.label}
|
|
</Badge>
|
|
))}
|
|
{extraBadgesCount > 0 && <Badge className={badgeClasses}>+{extraBadgesCount}</Badge>}
|
|
</div>
|
|
)}
|
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
</button>
|
|
</PopoverTrigger>
|
|
)
|
|
})
|
|
Trigger.displayName = 'Trigger'
|
|
MultiSelect.Trigger = Trigger
|
|
|
|
const Content = forwardRef<
|
|
HTMLDivElement,
|
|
PropsWithChildren<{ className?: string } & React.ComponentProps<typeof PopoverContent>>
|
|
>(({ children, className, ...props }, ref) => {
|
|
return (
|
|
<PopoverContent ref={ref} className={cn('w-full p-4 space-y-4', className)} {...props}>
|
|
{children}
|
|
</PopoverContent>
|
|
)
|
|
})
|
|
Content.displayName = 'Content'
|
|
MultiSelect.Content = Content
|
|
|
|
export function Item({ item, className }: { item: Item; className?: string }) {
|
|
const id = useId()
|
|
const { selected, handleSelect } = useMultiSelect()
|
|
|
|
return (
|
|
<div className={cn('flex items-center space-x-2', className)}>
|
|
<Checkbox
|
|
id={`${id}-checkbox-${item.value}`}
|
|
checked={selected.some((sel) => sel.value === item.value)}
|
|
onCheckedChange={() => handleSelect(item)}
|
|
/>
|
|
<label
|
|
htmlFor={`${id}-checkbox-${item.value}`}
|
|
className="text-sm leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
{item.label}
|
|
</label>
|
|
</div>
|
|
)
|
|
}
|
|
MultiSelect.Item = Item
|