Skip to content

UX-1338 — Redpanda Connect Pipeline (RPCN) Visual View & Editor#2553

Open
SpicyPete wants to merge 22 commits into
masterfrom
UX-1338/rpcn-visual
Open

UX-1338 — Redpanda Connect Pipeline (RPCN) Visual View & Editor#2553
SpicyPete wants to merge 22 commits into
masterfrom
UX-1338/rpcn-visual

Conversation

@SpicyPete

@SpicyPete SpicyPete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a full visual editor for Redpanda Connect (RPCN) pipelines: an interactive, node-based canvas that renders a pipeline's YAML as a directed graph and lets you inspect and edit nodes without hand-writing YAML. It sits alongside the existing YAML editor as a new "Visual" lane.

The editor is feature-flagged and currently scoped to the embedded (Cloud) context — it activates only when enableRpcnVisualEditor and enablePipelineDiagrams are on and the page is embedded. When enabled, editing opens on the Visual lane by default; otherwise the experience is unchanged (YAML lane).

What's included

Canvas & layout

  • Flow canvas (pipeline-flow-canvas.tsx, pipeline-flow-canvas-nodes.tsx) — renders the pipeline as a left-to-right DAG using React Flow + Dagre (@dagrejs/dagre, new dependency): input → processors → output, with control-flow constructs (switch, branch, try/catch, broker, fallback) drawn as split → case-lanes → merge, plus conditional/error/reference edge styling.
  • Scope regions — each control-flow construct gets a faint dashed enclosure. These are traced as a single connected outline that hugs the construct's actual (L-shaped) footprint per Dagre column, so a fan-out no longer paints over unrelated neighbor cards. Tall regions tile their fill under the browser's max paintable-element size so the background never silently drops.
  • Flow parser & layout (pipeline-flow-parser.ts, pipeline-flow-meta.ts) — parses pipeline YAML into a flow tree and computes the layout with source-aligned ranks. Replaces the old sidebar diagram (pipeline-flow-diagram.tsx removed).

Editing

  • Node inspector (node-inspector.tsx, node-config-form.tsx) — side panel to view/edit a selected node's config via Registry Field forms (react-hook-form + Zod), drill into control-flow children (switch cases / steps), and edit a node's raw YAML in place.
  • Editor store (use-pipeline-editor-store.ts) — Zustand store with undo/redo; pending visual edits are flushed to YAML at save time.
  • Component picker / command palettes (pipeline-canvas-command-palette.tsx, onboarding/connect-command-palette.tsx, component-aliases.ts) — quick actions (view in YAML, undo/redo, jump to node) and a connector/processor inserter with curated defaults, alias-aware search (e.g. "queue" → kafka_franz), and de-emphasis of deprecated/experimental components.

Feedback surfaces

  • Structure tree (pipeline-structure-tree.tsx) — sidebar outline in document order with collapsible groups, roving-tabindex keyboard navigation, and click-to-reveal.
  • Problems panel (pipeline-problems-panel.tsx, pipeline-lint.ts) — floating chip listing lint problems (mapped from backend LintHints by YAML line range, merged with save-error hints) and detected secret references; click a problem to jump to its node.
  • Unsaved panel (pipeline-unsaved-panel.tsx, pipeline-diff.ts) — floating chip listing nodes whose config differs from the last save (signature-based diff); click to jump.
  • Tips bar & template CTA (editor-tips-bar.tsx, template-cta.tsx) — platform-aware keyboard-shortcut hints and a "start from a template" entry point for empty pipelines.

Testing

Unit + integration tests accompany the new modules — flow parser/layout, lint mapping, dirty-diff, canvas render (incl. tall-region paint-cap), inspector, config form, structure tree, problems/unsaved panels, and command palettes. type:check, lint, and the test suite are green.

Known limitations / deferrals

  • Delete-guard (confirming deletion of a node with dependents) is a TODO.

How to test

Enable enableRpcnVisualEditor + enablePipelineDiagrams, open a pipeline in the embedded editor, and confirm the Visual lane loads. Verify node selection/edit, undo/redo, save round-trips to YAML, and the problems/unsaved chips.

Screenshots

Sidelane / YAML enhancements

Screenshot 2026-07-07 at 9 31 10 AM Screenshot 2026-07-07 at 9 31 41 AM

Visual view

Screenshot 2026-07-07 at 9 32 07 AM Screenshot 2026-07-07 at 9 32 38 AM

@SpicyPete SpicyPete requested review from a team, c-julin, eblairmckee and malinskibeniamin July 7, 2026 16:33
@SpicyPete SpicyPete self-assigned this Jul 7, 2026
@SpicyPete SpicyPete added feature New feature or request frontend labels Jul 7, 2026
@SpicyPete SpicyPete requested review from datamali and removed request for a team July 7, 2026 16:34
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Clean — no registry drift, off-token colours, or ad-hoc classes

App: frontend · Scope: diff vs origin/master · Files: 62

Count
⚠️ Outdated registry components 0
🛠 Locally-modified components 0
❓ Unknown to registry 0
🎨 Off-token palette colours 0
🔢 Ad-hoc utility classes 0

Generated by lookout audit-changes.

@SpicyPete SpicyPete changed the title UX-1338 — Redpanda Connect Pipeline (ROCN) Visual View & Editor UX-1338 — Redpanda Connect Pipeline (RPCN) Visual View & Editor Jul 7, 2026
@SpicyPete

Copy link
Copy Markdown
Contributor Author

@claude review

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @SpicyPete's task in 1m 24s —— View job


Code Review — UX-1338 RPCN Visual Editor

Reviewed across 5 parallel passes (parser/layout, canvas, inspector/store, yaml/utils, panels/shared-UI). This is a large but carefully written PR — effect cleanup, undo/redo, the save-flush path, and the shared-UI signature changes all check out. Below are the findings worth acting on, most impactful first.


🟠 Correctness

1. mergeScanner picks the wrong input key when the input has a labelutils/yaml.ts:134

const inputType = Object.keys(inputObj)[0];   // ← should be firstKey(inputObj)

Everywhere else in this file uses firstKey() (e.g. yaml.ts:534, :543) specifically to skip reserved keys (label, <<). Here the raw first key is used, so for input: { label: my_in, kafka_franz: {...} } the scanner gets written to input.label.scanner (line 142), corrupting the config instead of attaching to kafka_franz.
Repro: labeled input file → pick a scanner from the palette → scanner nested under the label scalar.
Fix this →

2. Switch-case condition draft leaks across sibling cases → silent misroutingpipeline/node-inspector.tsx:510-522

const initial = typeof caseObject.check === 'string' ? caseObject.check : '';
const [check, setCheck] = useState(initial);
useEffect(() => setCheck(initial), [initial]);   // only resyncs when the primitive value changes

SwitchCaseEditor/CaseConditionSection isn't re-keyed across selection changes, so the instance persists. Select case A (empty check), type foo, then select sibling case B (also empty check): initial is still '' (unchanged), the resync effect never fires, check stays foo, dirty flips true, and a phantom draft caseWithCheck(B, 'foo') is reported. On the next flush/Save, B's routing condition is silently set to a value the user never entered for it.
Fix direction: key the editor by case/target identity, or resync on caseObject identity rather than the primitive initial.
Fix this →

3. multiMemberSpecs doesn't skip null/unparseable input entries → phantom leafutils/pipeline-flow-parser.ts:175-184
Unlike extractProcessorEntries (:473) and parseSwitchCases (:565), this maps every array element without filtering non-objects. For input: { broker: { inputs: [null, { kafka: {} }] } }, parseMultiInputs filters to one key but multiMemberSpecs returns two specs → a phantom input leaf whose editTarget points at the null slot (['input','broker','inputs',0]). Clicking/deleting it operates on the null entry. (Array-index alignment itself is correct — this is a spurious-node bug, not an off-by-one.)
Fix this →


🟡 Performance (large graphs)

4. Scope-region work re-runs on pan/zoom and hoverpipeline/pipeline-flow-canvas.tsx

  • ScopeRegions useStore selector (:903-912) folds a hash over every node on each React Flow store mutation — including the transform updates emitted per animation frame during pan/zoom. On a few-hundred-node pipeline this is a full-node loop ~60×/s just to detect measurement changes.
  • RegionBox (:820) calls regionGeometry(...) (which runs unionOutline — grid build + boundary chaining) in its render body, and ScopeRegions re-renders on any hoveredNodeId/selectedNodeId change (:956-967). So hovering any node re-traces the outline of every region — contradicting the "hover stays cheap" design intent. The regions memo caches columns but not the outline path.

Fix direction: derive measuredKey only from node dimensions (not viewport transform), and memoize RegionBox/the outline path so it recomputes only when its own geometry changes.
Fix this →


🟢 Minor / nits

  • component-aliases.ts:4-47 — substring alias matching over-matches: bloblang contains log so searching "log" surfaces the bloblang mapping processor; the sql fragment tags any *sql* name with all three db aliases. Consider matching alias tokens on word boundaries rather than substring.
  • Minimap a11y (pipeline-flow-canvas.tsx:195-245) — interactive <svg role="img"> with pointer handlers but no tabIndex/keyboard support, and no onPointerCancel (:199-212), so an interrupted drag can leave draggingRef.current === true and keep re-centering on subsequent moves.
  • Sequence-input semantics ignored (pipeline-flow-parser.ts:1316-1321) — childFlow: 'sequential' is set at parse time but fanSides forces in: true for all input containers, so a sequence input renders identically to a broker (parallel fan-in) despite the different intended semantics.
  • Switch-case labels skip numbers (pipeline-flow-parser.ts:573) — caseId/caseLabel use the raw array index, so a skipped/null case yields "case 1", "case 3" (no "case 2").
  • connect-command-palette.tsx:447recents snapshot read once via useMemo(…, []); the "Recent" tab won't reflect a just-added component if the palette is reused without unmounting (harmless if it always unmounts on commit).
  • Unstable onConfigChange deps (node-inspector.tsx:518, :786; node-config-form.tsx:871) — inline arrow in effect deps makes reporter effects run every render. Benign (writes only to refs) but wrap in useCallback for clarity.
  • EMPTY_CONFIG_NODES (pipeline-flow-parser.ts:1036) — module-level array returned by reference for empty configs; safe today but a latent shared-mutable-state footgun.
  • tabs.tsx:5import React used only as a type (React.ComponentProps); import type would be cleaner (won't fail CI — redpanda-ui is lint-excluded).

Also worth noting the PR's own stated deferral: the delete-guard for nodes with dependents is still a TODO, and countResourceReferences (yaml.ts:1235) only counts explicit resource: fields — it can under-warn vs. the flow parser's usedByCount if it ever feeds that prompt.


Nothing here is a merge-blocker for a feature-flagged surface, but I'd fix #1 and #2 before wider rollout — both silently corrupt/misroute saved config. Nice work on the parser test coverage and the paint-cap tiling.

@SpicyPete

Copy link
Copy Markdown
Contributor Author

#2554
This PR pulls out a bunch of the non-core updates to this PR

Comment on lines -64 to -73
<ConnectTiles
className="px-0 py-0"
components={components}
componentTypeFilter={typeFilter}
gridCols={3}
hideHeader
onChange={onAddConnector}
searchPlaceholder={searchPlaceholder}
variant="ghost"
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make an action item that once this is GA and well tested we remove the old RPCN UXs. potentially Q3 or Q4

}) => {
const { labels, onCreateResource } = useContext(ResourceFieldContext);
const options = labels[kind];
const current = value ?? '';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could just set the initial value on line 170 value = ''

<SelectValue placeholder="Select a resource…" />
</SelectTrigger>
<SelectContent>
{isMissing ? <SelectItem value={current}>{current} (missing)</SelectItem> : null}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the edge case for a missing item? not clear to me

Comment on lines +205 to +210
<SelectItem value={CREATE_RESOURCE_VALUE}>
<span className="flex items-center gap-1.5 text-primary">
<Plus className="size-3.5" />
Create new {kind === 'cache' ? 'cache' : 'rate limit'}…
</span>
</SelectItem>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

99% sure we have a component for this.. I would look at what ui registry combobox does... it may or may not be exported as a subcomponent

);
};

const SCALAR_TYPES = new Set(['string', 'int', 'float', 'bool']);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like a constant exists for this in schema.ts (one of them...)

Comment on lines +234 to +242
// A single editable value: a primitive (string/int/float/bool), an enum select, or a
// resource reference (stored as a string).
function isScalarField(spec: RawFieldSpec): boolean {
return (
Boolean(spec.name) &&
spec.kind === 'scalar' &&
(SCALAR_TYPES.has(spec.type) || hasOptions(spec) || isResourceRefField(spec))
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. when we parse the rpcn schema we have a few utils and consts for determining field types. would love to see those reused, or move these utils to schema.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that said, since I'd love to see these utils generalized in schema.ts we should remove these unnecessary comments. when in doubt, give a util a better name, don't vomit comments unnecessarily (I am look at your, claude)

Comment on lines +257 to +267
const COMPONENT_FIELD_TYPES = new Set([
'input',
'output',
'processor',
'cache',
'rate_limit',
'buffer',
'metrics',
'tracer',
'scanner',
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also exists in schema.ts

Comment on lines +292 to +320
function setInObj(obj: Record<string, unknown>, path: string[], value: unknown): void {
let cur = obj;
for (const key of path.slice(0, -1)) {
if (!cur[key] || typeof cur[key] !== 'object' || Array.isArray(cur[key])) {
cur[key] = {};
}
cur = cur[key] as Record<string, unknown>;
}
cur[path.at(-1) as string] = value;
}

function deleteInObj(obj: Record<string, unknown>, path: string[]): void {
const parent = path.slice(0, -1).reduce<Record<string, unknown> | undefined>((cur, key) => {
const next = cur?.[key];
return next && typeof next === 'object' ? (next as Record<string, unknown>) : undefined;
}, obj);
if (parent) {
delete parent[path.at(-1) as string];
}
}

// Drop objects that became empty after clearing their fields, so the YAML stays tidy.
function pruneEmptyObjects(obj: Record<string, unknown>): void {
for (const [key, val] of Object.entries(obj)) {
if (val && typeof val === 'object' && !Array.isArray(val)) {
pruneEmptyObjects(val as Record<string, unknown>);
if (Object.keys(val).length === 0) {
delete obj[key];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these seem like lodash util rewrites... I would be shocked if we didn't already have an utils/object file with helpers for this. if not... let's add one

Comment on lines +336 to +366
function coerceScalar(spec: RawFieldSpec, raw: string | boolean): string | number | boolean {
if (spec.type === 'bool') {
return Boolean(raw);
}
const text = String(raw);
// Interpolations (`${ENV}`, secrets, Bloblang) are valid even in numeric fields — keep verbatim
// rather than coercing to NaN (→ '' → dropped on commit). Matches numericHint.
if ((spec.type === 'int' || spec.type === 'float') && text.includes('${')) {
return text;
}
if (spec.type === 'int') {
const n = Number.parseInt(text, 10);
return Number.isNaN(n) ? '' : n;
}
if (spec.type === 'float') {
const n = Number(text);
return text === '' || Number.isNaN(n) ? '' : n;
}
return text;
}

function coerceArrayItems(spec: RawFieldSpec, text: string): unknown[] {
const lines = text
.split('\n')
.map((l) => l.trim())
.filter((l) => l !== '');
if (spec.type === 'int' || spec.type === 'float') {
return lines.map(Number).filter((n) => !Number.isNaN(n));
}
return lines;
}

@eblairmckee eblairmckee Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again.. more schema.ts rewrites

Comment on lines +516 to +533
const FieldLabel = ({ spec, htmlFor }: { spec: RawFieldSpec; htmlFor?: string }) => (
<div className="flex items-center gap-2">
<Label className="font-medium text-sm" htmlFor={htmlFor}>
{spec.name}
</Label>
{checkRequired(spec) ? (
<span aria-hidden className="text-destructive text-xs" title="Required">
*
</span>
) : null}
{spec.type && spec.type !== 'string' ? <span className="text-muted-foreground text-xs">{spec.type}</span> : null}
{spec.defaultValue ? (
<span className="text-muted-foreground text-xs">
default: <span className="font-mono">{spec.defaultValue}</span>
</span>
) : null}
</div>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc we have a component for Required... but then again that may have been adp-ui...
regardless.. worth elevating this to a shared components directory rather than one-offing this. would be very useful in react-hook-form instances.

Comment on lines +535 to +540
const FieldDescription = ({ spec }: { spec: RawFieldSpec }) =>
spec.description ? (
<Text className="text-muted-foreground" variant="bodySmall">
{spec.description}
</Text>
) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth noting I like to have both field-description as well as field-helper-text (the latter being positioned below the field, mutually exclusive with field-error)
this is pseudo code for the sake of discussion. would be nice to elevate these kinds of things to be common components so future console UIs can reuse them. and eventually upstream to ui registry as typography utility classes 😏

Comment on lines +551 to +577
const SecretInput = (props: { id?: string; value: string; onChange: (value: unknown) => void; required?: boolean }) => {
const [show, setShow] = useState(false);
const isReference = props.value.includes('${');
return (
<div className="flex items-center gap-1.5">
<Input
aria-required={props.required || undefined}
autoComplete="off"
id={props.id}
onChange={props.onChange}
type={show || isReference ? 'text' : 'password'}
value={props.value}
/>
{isReference ? null : (
<Button
aria-label={show ? 'Hide value' : 'Show value'}
onClick={() => setShow((v) => !v)}
size="icon-sm"
type="button"
variant="ghost"
>
{show ? <EyeOff className="size-3.5" /> : <Eye className="size-3.5" />}
</Button>
)}
</div>
);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a Input type="password" in the registry already, prefer using that... or if we need something more custom this deserves to be its own component.

Actually... I think the registry also has a SecretSelector component, too, that's compatible with auto-form/react-hook-form... worth investigating.

Comment on lines +587 to +588
value: string | boolean;
onChange: (value: unknown) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems a little off.... should was have a Value: string | boolean and reuse for the onChange value type?

Comment on lines +642 to +654
function numericHint(spec: RawFieldSpec, value: string | boolean): string | null {
const text = String(value ?? '').trim();
if (text === '' || typeof value === 'boolean' || text.includes('${')) {
return null;
}
if (spec.type === 'int' && !INT_VALUE_RE.test(text)) {
return "Not a valid integer — this change won't be saved until fixed.";
}
if (spec.type === 'float' && Number.isNaN(Number(text))) {
return "Not a valid number — this change won't be saved until fixed.";
}
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again with reusability. these could be lovely helpers for auto-form instances

}

// biome-ignore lint/suspicious/noTemplateCurlyInString: a literal Connect secret reference, not a JS template
const SECRET_REF_EXAMPLE = '${secrets.MY_SECRET}';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also pretty sure we have a function for this already

Comment on lines +731 to +738
<Collapsible className="rounded-md border border-border/60" defaultOpen={defaultOpen}>
<CollapsibleTrigger className="group flex w-full items-center justify-between px-3 py-2 text-left">
<Text variant="bodyStrongMedium">{label}</Text>
<ChevronDown
className={cn('size-4 text-muted-foreground transition-transform group-data-[panel-open]:rotate-180')}
/>
</CollapsibleTrigger>
<CollapsibleContent className="flex flex-col gap-4 px-3 pt-1 pb-3">{children}</CollapsibleContent>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is basically ui registry Accordion. worth looking at whether it's a viable substitute rather than yet another one-off

Comment on lines +773 to +777
const ordered = [
...formFields.filter((f) => checkRequired(f) && !f.advanced),
...formFields.filter((f) => !(checkRequired(f) || f.advanced)),
...formFields.filter((f) => f.advanced),
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a flapMap + predicate util would be lovely to use here.

Comment on lines +932 to +1006
{isListValued && hasChildList ? (
<ChildItemsList
items={childItems as InspectorChildItem[]}
label="Cases"
onSelect={onSelectChild as (item: InspectorChildItem) => void}
/>
) : null}
{isListValued && !hasChildList ? (
<div className="rounded-md border border-border/60 border-dashed px-3 py-2">
<Text className="text-muted-foreground" variant="bodySmall">
This component's items (cases / processors) are edited on the canvas — select one to edit it.
</Text>
</div>
) : null}

{isListValued ? null : required.map((f) => <SchemaField control={control} key={f.name} path={[]} spec={f} />)}

{!isListValued && optional.length > 0 ? (
<FieldGroup label="Optional">
{optional.map((f) => (
<SchemaField control={control} key={f.name} path={[]} spec={f} />
))}
</FieldGroup>
) : null}

{!isListValued && advanced.length > 0 ? (
<FieldGroup defaultOpen={false} label="Advanced">
{advanced.map((f) => (
<SchemaField control={control} key={f.name} path={[]} spec={f} />
))}
</FieldGroup>
) : null}

{!isListValued && componentFields.length > 0 && hasChildList ? (
<ChildItemsList
items={childItems as InspectorChildItem[]}
label="Steps"
onSelect={onSelectChild as (item: InspectorChildItem) => void}
/>
) : null}
{!isListValued && componentFields.length > 0 && !hasChildList ? (
<div className="rounded-md border border-border/60 border-dashed px-3 py-2">
<Text className="text-muted-foreground" variant="bodySmall">
{componentFields.map((f) => f.name).join(', ')}{' '}
{componentFields.length === 1 ? 'is a nested component' : 'are nested components'} — select{' '}
{componentFields.length === 1 ? 'it' : 'them'} on the canvas to edit.
</Text>
</div>
) : null}

{!isListValued && showRaw ? (
<FieldGroup defaultOpen={false} label="Other settings (YAML)">
<Controller
control={control}
name="raw"
render={({ field }) => {
const invalid = field.value.trim() !== '' && parseRawSection(true, field.value) === null;
return (
<div className="flex flex-col gap-1.5">
<div className="h-[200px] overflow-hidden rounded-md border border-border">
<YamlEditor
onChange={(v) => field.onChange(v || '')}
options={{ minimap: { enabled: false } }}
transparentBackground
value={field.value}
/>
</div>
{invalid ? (
<Text className="text-destructive" variant="bodySmall">
Invalid YAML — these settings won't be saved until fixed.
</Text>
) : null}
</div>
);
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a lot more readable if we hoisted this to a constant and returned most -> least specific conditionals.

const invalid = field.value.trim() !== '' && parseRawSection(true, field.value) === null;
return (
<div className="flex flex-col gap-1.5">
<div className="h-[200px] overflow-hidden rounded-md border border-border">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why overflow-hidden? seems like a broken UI for longer (over 200px high) yaml configs

@SpicyPete SpicyPete Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the extra yaml field within the side-panel, so it needs some height set and overflow-hidden, I will increase the height so more can fit in, but the extra yaml field for this is likely rarely used

/>
) : null}
{!isListValued && componentFields.length > 0 && !hasChildList ? (
<div className="rounded-md border border-border/60 border-dashed px-3 py-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be curious to see a screenshot of this.... dashed borders are typically used for placeholder/empty/you-could-add-something-optional form sections

{danglingRef && !readOnly ? (
<DanglingRefBanner onCreate={handleCreateMissingResource} refLabel={danglingRef.ref} />
) : null}
{(() => {

@eblairmckee eblairmckee Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: can we lift this to a constant... I lean against inline function calls just to render more jsx... screams that it should be its own subcomponent.. or const within component body

Comment on lines +344 to +363
const conditionSection =
caseTarget && caseObject ? (
// Key by the case's path so selecting a sibling remounts + resets the draft (see above).
<CaseConditionSection
caseObject={caseObject}
error={conditionError}
key={JSON.stringify(editTargetPath(caseTarget))}
onConfigChange={readOnly ? undefined : reportConditionDraft}
readOnly={readOnly}
/>
) : null;
if (readOnly) {
return (
<>
{conditionSection}
<ReadOnlyComponent component={component} />
</>
);
}
if (useForm && spec) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my earlier comment about early returns for most -> least specific conditions... this is a great example of that. let's hoist this to outside the render

Comment on lines +443 to +460
const InspectorLintErrors = ({ hints }: { hints: LintHint[] }) => (
<div className="shrink-0 border-destructive/30 border-b bg-destructive/5 px-4 py-3">
<div className="mb-1 flex items-center gap-1.5 text-destructive">
<AlertCircle className="size-4 shrink-0" />
<Text as="span" className="text-destructive" variant="bodyStrongMedium">
{hints.length === 1 ? '1 problem' : `${hints.length} problems`}
</Text>
</div>
<ul className="flex flex-col gap-1">
{hints.map((hint, i) => (
<li className="text-destructive text-xs" key={`${hint.line}-${hint.column}-${i}`}>
{hint.hint}
{hint.line > 0 ? <span className="text-destructive/70"> (line {hint.line})</span> : null}
</li>
))}
</ul>
</div>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like it could be a registry Alert

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2026-07-10 at 4 03 37 PM

Maybe, but in this instance then we'll have more boxes-in-boxes, so a custom full-width alert area makes sense to me


// Write a component-config draft at `target`, cascading a resource-label rename to every component
// that references it (so the link is never silently broken).
function applyComponentDraft(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am dying to know how claude decides whether a function should be a helper outside of the component body... vs a callback... this screams "should've been a callback"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pure function which doesn't rely on anything in the component, so keeping it out should be okay

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request frontend team/ux

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants