Files
supabase/apps/docs/features/ui/Tabs.tsx
T
36d2982af4 docs: add reusable monitoring agent setup components (#49506)
<!-- 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>&nbsp;<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>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com>
2026-09-04 13:38:37 +10:00

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>
)
}