Files
supabase/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx
T
Charis 4343e21da0 feat(studio): tighten the notebook diff preview (#49218)
## 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?

UI refactor of the notebook create/update preview in the AI Assistant
panel, plus a small additive prop on the shared `CodeBlock`.

## What is the current behavior?

The assistant's notebook diff renders each cell as its own bordered box
with a gap between them, under a `6 cells` line that is easy to miss.
Cells can't be collapsed, each one carries a repeated `ADDED` badge and
a nested "Show more" toggle, and long markdown scrolls sideways instead
of wrapping.

## What is the new behavior?

<img width="796" height="1076" alt="CleanShot 2026-08-18 at 14 41 30@2x"
src="https://github.com/user-attachments/assets/45e58c6c-48f2-404b-8699-757ee96a4a8d"
/>

- The whole diff is one card: a distinct header row (notebook name,
summary, expand/collapse all) over cells glued together by dividers.
- Every cell is a `Collapsible`. Added and replaced cells open by
default; unchanged, moved, and removed cells stay as single rows but are
now inspectable instead of being content-free.
- The per-row badge is replaced by a colored gutter glyph (`+` `−` `~`
`↕`) with a tooltip naming the change type. The change type reaches the
accessible name via `aria-label` on the row.
- The nested "Show more" toggle inside each cell is gone — the row
itself is the only control.
- `CodeBlock` gains a `wrapLongLines` prop (default `false`, no change
for existing callers), used here so markdown and SQL soft-wrap. The
highlighter sets `white-space` inline on the `<code>` element, so a
class on the `<pre>` can't do this.

## Additional context

Towards FE-4143

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Notebook previews now display titles, notebook icons, and clearer
bordered layouts.
* Added per-cell expand/collapse controls, including “Expand all” and
“Collapse all.”
  * Long code lines can now wrap for improved readability.

* **Improvements**
* Added mode-based fallback labels when notebook titles are unavailable.
* Newly added and replaced cells expand by default, while unchanged
cells remain collapsed.
* Improved change markers, tooltips, removed-cell styling, and notebook
proposal preview spacing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-19 12:11:10 -04:00

168 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ChevronRight } from 'lucide-react'
import {
cn,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { CodeBlock } 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
isExpanded: boolean
onExpandedChange: (isExpanded: boolean) => void
}
/**
* A single-character gutter marker per change type The glyph is decorative —
* `changeLabel` carries the same information into the row's accessible name and its tooltip.
*/
const CHANGE_MARKERS = {
added: { glyph: '+', changeLabel: 'Added', className: 'text-brand-600' },
removed: { glyph: '−', changeLabel: 'Removed', className: 'text-destructive' },
replaced: { glyph: '~', changeLabel: 'Replaced', className: 'text-warning-600' },
moved: { glyph: '↕', changeLabel: 'Moved', className: 'text-foreground-lighter' },
unchanged: { glyph: '', changeLabel: 'Unchanged', className: '' },
} as const
type ChangeMarker = (typeof CHANGE_MARKERS)[keyof typeof CHANGE_MARKERS]
/** The cell a row is labelled by — for a replacement, the proposed cell rather than the current one. */
function getEntryCell(entry: NotebookCellDiffEntry): CellWire | AgentCell {
return entry._tag === 'replaced' ? entry.after : entry.cell
}
/** One row of the diff card: a header line that collapses to a single row, plus its content. */
export const NotebookPreviewCell = ({
entry,
isExpanded,
onExpandedChange,
}: NotebookPreviewCellProps) => {
const marker = CHANGE_MARKERS[entry._tag]
const isRemoved = entry._tag === 'removed'
const label = getCellLabel(getEntryCell(entry))
return (
<Collapsible open={isExpanded} onOpenChange={onExpandedChange}>
<CollapsibleTrigger
aria-label={`${marker.changeLabel} ${label}`}
className="group flex w-full items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-surface-200"
>
<ChangeGlyph marker={marker} />
<ChevronRight
size={12}
className="shrink-0 text-foreground-lighter transition-transform group-data-[state=open]:rotate-90"
/>
<span
className={cn(
'truncate text-sm text-foreground-light',
isRemoved && 'text-foreground-lighter line-through'
)}
>
{label}
</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5 px-3 pb-2.5">
<CellBody entry={entry} />
</div>
</CollapsibleContent>
</Collapsible>
)
}
/**
* The gutter glyph, with a tooltip naming the change type it stands for. The glyph stays
* `aria-hidden` — the row's `aria-label` already carries the same word — so the tooltip is a
* pointer affordance rather than a second announcement.
*/
const ChangeGlyph = ({ marker }: { marker: ChangeMarker }) => {
const className = cn('w-3 shrink-0 text-center font-mono text-xs leading-none', marker.className)
// An unchanged cell has no glyph, so there is nothing to explain — render the spacer alone.
if (!marker.glyph) return <span aria-hidden className={className} />
return (
<Tooltip>
<TooltipTrigger asChild>
<span aria-hidden className={className}>
{marker.glyph}
</span>
</TooltipTrigger>
<TooltipContent side="left">{marker.changeLabel}</TooltipContent>
</Tooltip>
)
}
const CellBody = ({ entry }: { entry: NotebookCellDiffEntry }) =>
entry._tag === 'replaced' ? (
<ReplacedCellBody before={entry.before} after={entry.after} />
) : (
<>
<CellSource cell={entry.cell} />
<MetadataLine text={getCellMetadataLine(entry.cell)} />
</>
)
/**
* 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 ReplacedCellBody = ({ before, after }: { before: CellWire; after: AgentCell }) => {
const beforeMetadata = getCellMetadataLine(before)
const afterMetadata = getCellMetadataLine(after)
return (
<>
<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} />
)}
</>
)
}
/**
* The cell's source, always rendered as literal text via `CodeBlock` — agent-authored markdown
* must never be interpreted into real DOM nodes.
*/
const CellSource = ({ cell }: { cell: CellWire | AgentCell }) => (
<CodeBlock
hideCopy
language={getCellCodeBlockLanguage(cell)}
value={getCellSourceText(cell)}
hideLineNumbers
wrapLongLines
className="wrap-break-word rounded-none border-0 p-0 text-xs"
/>
)
/** 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