mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Stack Draft stack extracted from `docs/monitoring`. Merge bottom-up. Troubleshooting / debugging-guide rewrite is out of scope. 1. #49503 move inspect and advisors 2. #49501 split Studio logs from ClickHouse queries 3. #49500 treat reports as signal dashboards 4. #49502 add Observe the data hub 5. **#49506** add agent setup components ← **this PR** 6. #49504 add hire-an-agent templates 7. #49505 restructure observability nav and overview ## 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? Docs app feature (MDX components + markdown export). Fifth layer in the observability stack. ## What is the current behavior? There is no shared way to render a monitoring agent prompt, schedule, and Claude/Codex/Cursor setup instructions in both HTML and generated markdown. ## What is the new behavior? - `AgentSetup` and `AgentWatchSchedule` MDX components, registered for HTML and markdown export - Shared `monitoring-agents` data (cadence, prompt ids, harness steps) - Opt-in `AiPrompt` markdown export (`includeInMarkdown`) so quickstarts stay HTML-only - Optional content-listing `subtitle` for schedule labels on cards No agent guide pages yet — those land in #49504 so this PR stays a reviewable code change. ## Additional context Markdown schema handlers share the same data module as the React components. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-a3cb5ece-925b-4046-b58a-5d69e9a9d794?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-a3cb5ece-925b-4046-b58a-5d69e9a9d794&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com>
275 lines
7.0 KiB
TypeScript
275 lines
7.0 KiB
TypeScript
'use client'
|
|
|
|
import { cva } from 'class-variance-authority'
|
|
import { Tabs as TabsPrimitive } from 'radix-ui'
|
|
import {
|
|
Children,
|
|
isValidElement,
|
|
ReactElement,
|
|
ReactNode,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type KeyboardEvent,
|
|
} from 'react'
|
|
import { cn } from 'ui'
|
|
|
|
import { useTocRerenderTrigger } from '../docs/GuidesMdx.state'
|
|
import { useStickyTabs, UseStickyTabsOptions } from './useStickyTabs'
|
|
import { useTabsWithQueryParams, UseTabsWithQueryParamsOptions } from './useTabsWithQueryParams'
|
|
|
|
export interface TabsProps {
|
|
children: ReactNode
|
|
type?: 'pills' | 'underlined' | 'cards' | 'rounded-pills'
|
|
defaultActiveId?: string
|
|
activeId?: string
|
|
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
|
|
block?: boolean
|
|
onChange?: (id: string) => void
|
|
onClick?: any
|
|
scrollable?: boolean
|
|
wrappable?: boolean
|
|
addOnBefore?: React.ReactNode
|
|
addOnAfter?: React.ReactNode
|
|
listClassNames?: string
|
|
baseClassNames?: string
|
|
}
|
|
|
|
export const tabsListVariants = cva(cn('flex'), {
|
|
variants: {
|
|
type: {
|
|
pills: 'space-x-1',
|
|
underlined: 'items-center border-b border-secondary',
|
|
cards: '',
|
|
'rounded-pills': 'flex-wrap gap-2',
|
|
},
|
|
scrollable: {
|
|
true: 'overflow-auto whitespace-nowrap no-scrollbar mask-fadeout-right',
|
|
},
|
|
wrappable: {
|
|
true: 'flex-wrap',
|
|
},
|
|
},
|
|
})
|
|
|
|
export const tabsTriggerListVariants = cva(
|
|
cn(
|
|
'relative cursor-pointer flex items-center space-x-2 text-center transition-colors focus-ring [&_img]:m-0 [&_img]:size-3.5 [&_svg]:size-3.5'
|
|
),
|
|
{
|
|
variants: {
|
|
type: {
|
|
pills: 'shadow-xs rounded-sm border',
|
|
underlined: 'text-foreground-lighter',
|
|
cards: '',
|
|
'rounded-pills': 'shadow-xs rounded-full',
|
|
},
|
|
size: {
|
|
tiny: 'text-xs px-2.5 py-1',
|
|
small: 'text-base md:text-sm leading-4 px-3 py-2',
|
|
medium: 'text-base md:text-sm px-4 py-2',
|
|
large: 'text-base px-4 py-2',
|
|
xlarge: 'text-base px-6 py-3',
|
|
},
|
|
block: {
|
|
true: 'w-full flex items-center justify-center',
|
|
},
|
|
isActive: {
|
|
false: 'hover:text-foreground',
|
|
},
|
|
},
|
|
compoundVariants: [
|
|
{
|
|
type: 'pills',
|
|
isActive: true,
|
|
className: 'bg-selection text-foreground border-stronger',
|
|
},
|
|
{
|
|
type: 'pills',
|
|
isActive: false,
|
|
className:
|
|
'bg-background border-strong hover:border-foreground-muted text-foreground-muted',
|
|
},
|
|
{
|
|
type: 'underlined',
|
|
isActive: true,
|
|
className: '!text-foreground border-b-2 border-foreground',
|
|
},
|
|
{
|
|
type: 'rounded-pills',
|
|
isActive: true,
|
|
className: 'bg-foreground text-background border-foreground',
|
|
},
|
|
{
|
|
type: 'rounded-pills',
|
|
isActive: false,
|
|
className:
|
|
'bg-surface-200 hover:bg-surface-300 hover:border-foreground-lighter text-foreground-lighter',
|
|
},
|
|
],
|
|
}
|
|
)
|
|
|
|
const isString = (maybeStr: unknown): maybeStr is string => typeof maybeStr === 'string'
|
|
|
|
export const Tabs = ({
|
|
defaultActiveId,
|
|
activeId,
|
|
type = 'pills',
|
|
size = 'tiny',
|
|
block,
|
|
onChange,
|
|
onClick,
|
|
scrollable,
|
|
wrappable,
|
|
addOnBefore,
|
|
addOnAfter,
|
|
listClassNames,
|
|
baseClassNames,
|
|
children,
|
|
queryGroup,
|
|
stickyTabList,
|
|
}: TabsProps &
|
|
Pick<UseTabsWithQueryParamsOptions, 'queryGroup'> & {
|
|
stickyTabList?: UseStickyTabsOptions
|
|
}) => {
|
|
const childrenArr: ReactElement<TabPanelProps>[] = []
|
|
const tabIds: string[] = []
|
|
Children.forEach(children, (child) => {
|
|
if (isValidElement<TabPanelProps>(child) && isString(child.props.id)) {
|
|
childrenArr.push(child)
|
|
tabIds.push(child.props.id)
|
|
}
|
|
})
|
|
const { queryTab, onTabSelected: onTabSelectedForQuery } = useTabsWithQueryParams({
|
|
tabIds,
|
|
queryGroup,
|
|
})
|
|
|
|
const sanitizedStickyTabList = useMemo(
|
|
() =>
|
|
stickyTabList != undefined
|
|
? {
|
|
...stickyTabList,
|
|
// Magic number is the height of tab list + paragraph margin, worth getting
|
|
// rid of this?
|
|
scrollMarginTop:
|
|
stickyTabList.scrollMarginTop || 'calc(var(--header-height) + 43px + 20px)',
|
|
}
|
|
: undefined,
|
|
[stickyTabList]
|
|
)
|
|
const {
|
|
observedRef,
|
|
stickyRef,
|
|
onTabSelected: onTabSelectedForSticky,
|
|
} = useStickyTabs(sanitizedStickyTabList)
|
|
const rerenderToc = useTocRerenderTrigger()
|
|
|
|
const [activeTab, setActiveTab] = useState(
|
|
queryTab ??
|
|
activeId ??
|
|
defaultActiveId ??
|
|
// if no defaultActiveId is set use the first panel
|
|
childrenArr?.[0]?.props?.id
|
|
)
|
|
|
|
useEffect(() => {
|
|
// If we have a queryTab, Tabs is controller by URL params
|
|
if (queryTab && queryTab !== activeTab) {
|
|
setActiveTab(queryTab)
|
|
return
|
|
}
|
|
if (activeId && activeId !== activeTab) {
|
|
setActiveTab(activeId)
|
|
}
|
|
}, [activeId, activeTab, queryTab])
|
|
|
|
const onTabClick = useCallback(
|
|
(id: string) => {
|
|
if (id !== activeTab) {
|
|
setActiveTab(id)
|
|
onTabSelectedForSticky()
|
|
onTabSelectedForQuery(id)
|
|
rerenderToc()
|
|
onChange?.(id)
|
|
onClick?.(id)
|
|
}
|
|
},
|
|
[
|
|
activeTab,
|
|
onTabSelectedForSticky,
|
|
onTabSelectedForQuery,
|
|
rerenderToc,
|
|
onChange,
|
|
onClick,
|
|
setActiveTab,
|
|
]
|
|
)
|
|
return (
|
|
<TabsPrimitive.Root
|
|
value={activeTab}
|
|
className={cn('w-full justify-between space-y-4', baseClassNames)}
|
|
ref={observedRef}
|
|
>
|
|
<TabsPrimitive.List
|
|
className={tabsListVariants({
|
|
type,
|
|
scrollable,
|
|
wrappable,
|
|
className: cn('not-prose', { 'bg-background': stickyTabList != null }, listClassNames),
|
|
})}
|
|
ref={stickyRef}
|
|
>
|
|
{addOnBefore}
|
|
{childrenArr.map((tab) => {
|
|
const isActive = activeTab === tab.props.id
|
|
|
|
return (
|
|
<TabsPrimitive.Trigger
|
|
onKeyDown={(e: KeyboardEvent<HTMLButtonElement>) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
onTabClick(tab.props.id)
|
|
}
|
|
}}
|
|
onClick={() => onTabClick(tab.props.id)}
|
|
key={`${tab.props.id}-tab-button`}
|
|
value={tab.props.id}
|
|
className={tabsTriggerListVariants({ type, isActive, size, block })}
|
|
>
|
|
{tab.props.icon}
|
|
<span>{tab.props.label}</span>
|
|
{tab.props.iconRight}
|
|
</TabsPrimitive.Trigger>
|
|
)
|
|
})}
|
|
{addOnAfter}
|
|
</TabsPrimitive.List>
|
|
{childrenArr}
|
|
</TabsPrimitive.Root>
|
|
)
|
|
}
|
|
|
|
interface TabPanelProps {
|
|
children: ReactNode
|
|
id: string
|
|
label?: string
|
|
icon?: React.ReactNode
|
|
iconRight?: React.ReactNode
|
|
className?: string
|
|
}
|
|
|
|
export const TabPanel = ({ children, id, className }: TabPanelProps) => {
|
|
return (
|
|
<TabsPrimitive.Content
|
|
value={id}
|
|
className={cn('focus:outline-hidden transition-height', className)}
|
|
tabIndex={-1}
|
|
>
|
|
{children}
|
|
</TabsPrimitive.Content>
|
|
)
|
|
}
|