mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
## Summary Stacked on #49109 (PR 1 — `deriveNotebookDiff`). This is PR 3 of the notebook approval-preview stack: a pure presentational component that renders the cell-level diff for a proposed notebook create/update, for use in the assistant approval UI (wired in a later PR). - `NotebookPreview` — header summary (`"6 cells"` for create, `"+2 −1 ~1 ↕1"` for update) + entry list + "Show N more cells" for long notebooks. - `NotebookPreviewCell` — dispatches per entry tag: `unchanged`/`removed`/`moved` collapse to a muted badge row; `added` renders source via `CodeBlock` (with a max-height/expand toggle); `replaced` renders a `DiffEditor` diff, plus a before → after metadata line when only `database_identifier`/`time_range` changed (SQL/text identical). - `NotebookPreview.utils` — pure helpers (labels, source/metadata extraction, language mapping, summary formatting), unit tested. - **Safety property**: cell content only ever renders through `CodeBlock`/`DiffEditor` (literal source), never through a markdown renderer — agent-authored text can't trigger image loads or link navigation before the user approves. Covered by an adversarial test (``, `[y](evil)`, `<img onerror>` → zero `img`/`[href]`/`[src]` DOM nodes). - Adds `'markdown'` as a supported `CodeBlock` language (small, additive change to `packages/ui-patterns`). Towards FE-4143 ## Test plan - [x] `pnpm --filter studio test` — NotebookPreview suite (21 tests) passes - [x] `pnpm --filter studio exec eslint components/interfaces/Explorer/NotebookPreview` — clean - [x] `pnpm --filter studio exec tsc --noEmit` — no new errors - [x] `pnpm exec prettier --check` — clean <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added notebook previews showing create and update summaries. * Displayed added, removed, moved, unchanged, and replaced cells with metadata and source diffs. * Added expandable previews with truncation and a “Show more cells” option. * Added Markdown syntax highlighting to code blocks. * **Bug Fixes** * Safely render adversarial agent-authored Markdown as literal content. * **Tests** * Added comprehensive coverage for notebook previews, summaries, metadata, formatting, and truncation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
155 lines
4.9 KiB
TypeScript
155 lines
4.9 KiB
TypeScript
import { useState, type ReactNode } from 'react'
|
|
import { Badge, Button, cn } from 'ui'
|
|
import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock'
|
|
|
|
import {
|
|
getCellCodeBlockLanguage,
|
|
getCellLabel,
|
|
getCellMetadataLine,
|
|
getCellMonacoLanguage,
|
|
getCellSourceText,
|
|
} from './NotebookPreview.utils'
|
|
import { DiffEditor } from '@/components/ui/DiffEditor'
|
|
import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations'
|
|
import type { AgentCell, CellWire } from '@/data/content/notebooks/notebook-schema'
|
|
|
|
export interface NotebookPreviewCellProps {
|
|
entry: NotebookCellDiffEntry
|
|
}
|
|
|
|
/** Renders a single diff entry, dispatching on its tag. */
|
|
export const NotebookPreviewCell = ({ entry }: NotebookPreviewCellProps) => {
|
|
switch (entry._tag) {
|
|
case 'unchanged':
|
|
return <CollapsedRow label={getCellLabel(entry.cell)} />
|
|
case 'removed':
|
|
return (
|
|
<CollapsedRow
|
|
label={getCellLabel(entry.cell)}
|
|
strikethrough
|
|
badge={{ variant: 'destructive', label: 'Removed' }}
|
|
/>
|
|
)
|
|
case 'moved':
|
|
return (
|
|
<CollapsedRow
|
|
label={getCellLabel(entry.cell)}
|
|
badge={{ variant: 'secondary', label: 'Moved' }}
|
|
/>
|
|
)
|
|
case 'added':
|
|
return <AddedCell cell={entry.cell} />
|
|
case 'replaced':
|
|
return <ReplacedCell before={entry.before} after={entry.after} />
|
|
}
|
|
}
|
|
|
|
interface CollapsedRowProps {
|
|
label: string
|
|
strikethrough?: boolean
|
|
badge?: { variant: 'destructive' | 'secondary'; label: string }
|
|
}
|
|
|
|
/** A single muted row with no content — used for unchanged, removed, and moved entries. */
|
|
const CollapsedRow = ({ label, strikethrough, badge }: CollapsedRowProps) => (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground-light">
|
|
{badge && <Badge variant={badge.variant}>{badge.label}</Badge>}
|
|
<span className={cn('truncate', strikethrough && 'line-through text-foreground-lighter')}>
|
|
{label}
|
|
</span>
|
|
</div>
|
|
)
|
|
|
|
interface ContentCellProps {
|
|
badge: { variant: 'success' | 'warning'; label: string }
|
|
label: string
|
|
children: ReactNode
|
|
}
|
|
|
|
/** Shared frame for entries that show full cell content — added and replaced cells. */
|
|
const ContentCell = ({ badge, label, children }: ContentCellProps) => (
|
|
<div className="flex flex-col gap-2 px-3 py-2 border rounded-md bg-surface-75">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Badge variant={badge.variant}>{badge.label}</Badge>
|
|
<span className="text-foreground truncate">{label}</span>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
)
|
|
|
|
const AddedCell = ({ cell }: { cell: AgentCell }) => (
|
|
<ContentCell badge={{ variant: 'success', label: 'Added' }} label={getCellLabel(cell)}>
|
|
<ExpandableCodeBlock
|
|
language={getCellCodeBlockLanguage(cell)}
|
|
value={getCellSourceText(cell)}
|
|
/>
|
|
<MetadataLine text={getCellMetadataLine(cell)} />
|
|
</ContentCell>
|
|
)
|
|
|
|
/**
|
|
* A `replace_cell` can change only the source parameters (`database_identifier`,
|
|
* `time_range`) and leave `sql`/`text` identical — the `DiffEditor` above would then show no
|
|
* change at all, so the metadata is compared independently and rendered as its own
|
|
* before → after line whenever it differs.
|
|
*/
|
|
const ReplacedCell = ({ before, after }: { before: CellWire; after: AgentCell }) => {
|
|
const beforeMetadata = getCellMetadataLine(before)
|
|
const afterMetadata = getCellMetadataLine(after)
|
|
|
|
return (
|
|
<ContentCell badge={{ variant: 'warning', label: 'Replaced' }} label={getCellLabel(after)}>
|
|
<DiffEditor
|
|
original={getCellSourceText(before)}
|
|
modified={getCellSourceText(after)}
|
|
language={getCellMonacoLanguage(after)}
|
|
height={240}
|
|
/>
|
|
{beforeMetadata !== afterMetadata ? (
|
|
<MetadataLine
|
|
text={`${beforeMetadata ?? 'No metadata'} → ${afterMetadata ?? 'No metadata'}`}
|
|
/>
|
|
) : (
|
|
<MetadataLine text={afterMetadata} />
|
|
)}
|
|
</ContentCell>
|
|
)
|
|
}
|
|
|
|
/** A plain-text metadata line for a query cell — never rendered as a link or attribute. */
|
|
const MetadataLine = ({ text }: { text: string | null }) =>
|
|
text ? <p className="text-xs text-foreground-lighter">{text}</p> : null
|
|
|
|
interface ExpandableCodeBlockProps {
|
|
language: CodeBlockLang
|
|
value: string
|
|
}
|
|
|
|
/**
|
|
* `CodeBlock` clipped to a fixed height with a "Show more/less" toggle. `CodeBlock`'s
|
|
* wrapper already scrolls (`overflow-auto`), so clipping just changes what's visible.
|
|
*/
|
|
const ExpandableCodeBlock = ({ language, value }: ExpandableCodeBlockProps) => {
|
|
const [isExpanded, setIsExpanded] = useState(false)
|
|
|
|
return (
|
|
<div>
|
|
<CodeBlock
|
|
language={language}
|
|
value={value}
|
|
hideLineNumbers
|
|
className="text-xs"
|
|
wrapperClassName={cn(!isExpanded && 'max-h-56')}
|
|
/>
|
|
<Button
|
|
variant="text"
|
|
size="tiny"
|
|
className="mt-1"
|
|
onClick={() => setIsExpanded((prev) => !prev)}
|
|
>
|
|
{isExpanded ? 'Show less' : 'Show more'}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|