Skip to content

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

Merged
SpicyPete merged 33 commits into
masterfrom
UX-1338/rpcn-visual
Jul 14, 2026
Merged

UX-1338 — Redpanda Connect Pipeline (RPCN) Visual View & Editor#2553
SpicyPete merged 33 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), edit a switch case's routing condition inline, 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) — 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; aliases live in connect-command-palette-utils.ts), 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: 59

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

@SpicyPete

Copy link
Copy Markdown
Contributor Author

@claude review

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @SpicyPete's task in 10m 32s —— View job


Code Review — UX-1338 RPCN Visual Editor

Reviewed the current state of the branch (5 focused passes: parser/layout, canvas, inspector/store, yaml/utils, panels + a store/diff/lint deep-dive). All four correctness/perf findings from the earlier review are resolved, and the delete-guard TODO is now implemented. Below are the issues I found on this pass, most impactful first — each verified against callers and test fixtures.


✅ Previously-flagged issues — all fixed

  • Add readiness & liveness check #1 mergeScanner input-key selection → uses firstKey(inputObj) (yaml.ts:133)
  • Add prometheus metrics #2 switch-case draft leak → editors re-keyed by case path (node-inspector.tsx:255,316)
  • Create helm chart #3 multiMemberSpecs phantom leaf → filters non-object entries (pipeline-flow-parser.ts:171-177)
  • Add dark theme  #4 scope-region pan/zoom+hover recompute → measuredKey digests only scoped node dims; RegionBox memoized (pipeline-flow-canvas.tsx:701,839)
  • Delete-guard, minimap onPointerCancel, token-bounded aliases, palette-utils extraction → all done

🟠 Correctness

1. mergeScanner drops the scanner-name wrapper → invalid scanner configutils/yaml.ts:138-141

const scannerName = Object.keys(newConfigObject)[0];  // 'csv'
const scannerConfig = newConfigObject[scannerName];   // the INNER fields
doc.setIn(['input', inputType, 'scanner'], scannerConfig);  // ← drops the `csv:` key

schemaToConfig returns a scanner as { [name]: config } (schema.ts:216-217), and the correct shape is scanner:\n csv: {…} (confirmed by the input fixtures at yaml.test.tsx:328,355,360). Setting scanner to the inner scannerConfig writes scanner: {…fields} with no csv: wrapper — a malformed scanner on save. Should pass the whole newConfigObject:

doc.setIn(['input', inputType, 'scanner'], newConfigObject);

Repro: input { file: { paths: [...] } } → add a csv scanner → scanner: holds the fields directly, not scanner: { csv: … }. No merge test asserts this shape today, so it slipped through.
Fix this →

2. An output switch/broker case with a check but no output body vanishesutils/pipeline-flow-parser.ts:290-297 + buildOutputNodes:858-863
outputContainerMembers sets obj: caseRecord.output, which is undefined for a case authored with only a check. buildOutputNodes then hits firstKey(undefined) → undefined and returns [], so the case node — its condition, its caseEditTarget, and any affordance to add an output — disappears entirely and can't be edited from the canvas. This is asymmetric with the processor switch (parseSwitchCases:547-566), which deliberately renders an empty case with an insert slot so it "never silently vanishes." Worth mirroring that behavior for output switches.
Repro: output: { switch: { cases: [ { check: 'foo' } ] } } → the case is invisible/uneditable in the visual lane.
Fix this →


🟡 Minor / low-impact

3. Help-comments target processor/resource index 0, but merges appendutils/yaml.ts:366,371
addCommentsFromSpec hardcodes ['pipeline','processors','0',specName] (and [resourceKey,'0',specName]), but the add-component flow calls configToYaml(mergedConfig, spec) (:488) on the full merged doc, where mergeProcessor/mergeResourceArray append (:86,:111). So when adding a component to a pipeline that already has one, the new component's field help-comments are silently not applied (the nav loop breaks at the missing key), and in the rare case that processors[0] is the same type, they land on the wrong pre-existing node. Config values are unaffected — this is comment placement only. Consider targeting length-1 (or applying comments to the standalone new component before merging).

4. Scanner help-comments never appliedutils/yaml.ts:375
configPath = [specName] for scanners, but a merged scanner lives at input.<type>.scanner.<name>, so the nav loop breaks immediately and no comments are added. Same area as #1.

5. extractConnectorTopics ignores YAML merge anchorsutils/yaml.ts:736-747
Uses parseDocument(...).getIn(...), which doesn't resolve <<: *anchor merge keys, whereas the flow parser and extractAllTopics (:799) both use merge: true. A component pulling topics from a shared anchor reports no topics here (possible false "missing topic") while the diagram shows them. Edge case (anchors only), but the two extractors should agree.

6. useSaveHotkey has no e.repeat / synchronous in-flight guardpipeline/use-save-hotkey.ts:39-50
The only re-entrancy guard is latest.current.isSaving, which is render-lagged (flips on the mutation's isPending re-render). A held or very fast double ⌘S before that re-render can call onSave() twice → duplicate create/update. Add if (e.repeat) return; and/or a synchronous in-flight ref. (The Save button shares the same lag, so low severity.)


The store/undo-redo, save-flush ordering, dirty-diff signatures, and lint line-range mapping all check out — nicely defensive. Nothing here blocks a feature-flagged surface, but I'd land #1 before enabling scanners in the visual flow (it writes malformed YAML) and #2 is a small parity fix that removes a dead-end in the canvas.
· branch UX-1338/rpcn-visual

@eblairmckee

Copy link
Copy Markdown
Contributor

@claude conduct an adversarial review using this skill: https://claude.ai/customize/skills?selectedId=skill_01U57srusYSMerLEneHv2hgM
specifically, I want you to focus on:

  1. the various utils modified for parsing redpanda connect schemas and yaml.
  2. any not feature flagged change is well tested and regardless of whether visual editor flag is on/off everything will still work.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @eblairmckee's task in 9m 28s —— View job


Adversarial Review — RPCN schema/YAML utils + feature-flag safety

Note: I couldn't open the linked claude.ai/customize/skills URL (private skill-config page, not reachable from CI). I ran the review in that spirit: three focused adversarial passes feeding hostile/edge-case YAML (labeled components, merge anchors <<: *a, null array entries, empty configs, bodyless switch cases, scanners), plus a dedicated flag-OFF trace. Findings below are each backed by a code quote + repro and cross-checked against the tests.

  • Flag-gating map + CLAUDE.md
  • Adversarial pass: yaml.ts
  • Adversarial pass: schema.ts + pipeline-flow-parser.ts / layout / meta
  • Verify non-flagged (YAML-lane) paths & coverage
  • Post findings

✅ Prior review findings — all confirmed fixed

Verified against current code: mergeScanner now re-wraps scanner: { <name>: {…} } (yaml.ts:145); addCommentsFromSpec targets lastArrayIndex(...) not '0', and the scanner comment path resolves input.<type>.scanner.<name> (yaml.ts:349-388); output bodyless switch case renders an editable empty case (pipeline-flow-parser.ts:874-895); processor case labels use a null-skipping counter (:554-564); sequence input no longer renders as a broker (pipeline-flow-layout.ts:112-118); emptyConfigNodes is now a factory, not a shared array. Nice.


🟠 New — Correctness (reachable with the flag OFF)

1. mergeProcessor destroys comments on pre-existing processors when appendingutils/yaml.ts:78-86

const processors = (processorsNode?.toJSON?.() as unknown[]) || [];      // strips AST → plain JS, no comments/anchors
...
doc.setIn(['pipeline', 'processors'], [...processors, newProcessor]);    // rebuilds array from stripped values

Reading the existing array through toJSON() discards every node's comments/anchors, then setIn writes fresh scalar nodes. The final configToYaml can't recover them — the AST is already gone. This directly undercuts the branch's comment-preservation goal, and it's on the non-flagged add-connector path (index.tsx:1036): each time you add a processor to a pipeline that already has one, the previously-added processors lose their # field: description help-comments (and any user comments/anchors).
Repro: pipeline with processors: [ { mapping: 'root = this' # keep me } ] → add a second processor → # keep me is gone.
The only merge-comment test (yaml.test.tsx:208) merges a new output section into a generate input — it never appends to an existing array, so this slips through.
Fix this →

2. mergeResourceArray — same toJSON() round-trip comment/anchor lossutils/yaml.ts:97-111

const existing = (node?.toJSON?.() as unknown[]) || [];
...
doc.setIn([key], [...existing, newResource]);

Identical defect for cache_resources / rate_limit_resources: appending a second resource wipes comments/anchors on the ones already defined. Same non-flagged reachability, untested (no resource-merge-with-comments fixture).
Fix this →

3. Picking a scanner when the config has no input writes a bogus top-level keyutils/yaml.ts:174-211
detectComponentType only classifies a single-key snippet as 'scanner' when doc?.has('input') (:178). With no input present (e.g. output-only or empty YAML) it falls through to 'unknown'defaultmergeRootComponentdoc.set('csv', {...}), injecting an invalid top-level csv: block instead of nesting under an input.
Repro: output:\n drop: {} → add a csv scanner → emits a stray top-level csv: {}. Structurally invalid, produced silently. No test covers scanner-merge without an input.
Fix this →


🟡 Minor

4. Output empty-case labels reuse the raw YAML index — the "no numbering gaps" fix from the processor path wasn't mirroredpipeline-flow-parser.ts:879,884
For a bodyless output switch/broker case, the label is derived from routing.caseEditTarget.path.at(-1) (the raw cases[] index), not the null-skipping counter parseSwitchCases introduced. So output: { switch: { cases: [ null, { check: 'a==1' } ] } } labels the sole visible case "case 2" with no "case 1" — the exact gap the processor path (:552) deliberately avoids. Low impact (adversarial null case), but it's an inconsistency the fix should have covered symmetrically.

5. multiMemberSpecs vs parseMultiInputs disagree → phantom placeholder leafpipeline-flow-parser.ts:178-183
multiMemberSpecs accepts any item && typeof item === 'object', while parseMultiInputs (yaml.ts:46) filters entries whose firstKey is undefined. A broker input entry with only a reserved key (- label: orphan, mid-edit) renders a spurious child leaf labeled input. Paths stay correct (index preserved) so no corruption — cosmetic/placeholder only. Very low.

6. resourceKindForFieldName prefix-matches any *_cache / *_rate_limit fieldutils/yaml.ts:1136-1144
name.endsWith('_cache') / _rate_limit will treat an unrelated field with that suffix as a resource reference if its value happens to equal a real label — a narrow false-positive in count/rename. Acknowledged in comments; low.


Feature-flag safety (requested focus #2) — sound, with two test gaps

The flag-OFF experience is correct. Visual lanes render only when activeEditLane/activeViewLane === 'visual', gated on isVisualEditorEnabled (index.tsx:1187,1195); with the flag off initialEditLane is 'yaml' (:857) and only the YAML EditorPanel + sidebar render. Verified:

  • Save path (index.tsx:269-334) sends yamlContent verbatim; pendingEditCommit?.() is optional-chained (visual-only) — no visual dependency, no YAML transform.
  • New parser/lint/diff utils are all try/catch-guarded (pipeline-flow-parser.ts:1051, pipeline-lint.ts:130, pipeline-diff.ts:29), so malformed YAML can't crash the flag-OFF editor.
  • The visual-only rewrites (extractConnectorTopics, patchComponent) are wired only into VisualEditorPanel. Removed files (pipeline-flow-diagram.tsx, pipeline-flow-nodes.tsx) have no remaining imports. schema.ts refactor is behavior-preserving; its new predicate exports are visual-only.

Non-flagged behavior changes worth noting:

  • YAML_STRINGIFY_OPTIONS: { lineWidth: 0 } (yaml.ts:22) changes the output of every generated/merged template on the flag-OFF path (add-connector, onboarding wizard, create-mode) — long single-line values (bloblang, descriptions) no longer fold at col 120. Intentional (comment at :20) and valid YAML, but no test asserts the no-fold behavior on a >120-char value.
  • Coverage gap: the lastArrayIndex append-comment fix (a genuinely non-flagged code path) is never exercised — the sole merge-comment test doesn't append to an existing array. Combined with findings Add readiness & liveness check #1/Add prometheus metrics #2 above, the append-into-existing-array path is the least-tested corner of the shared merge code and is exactly where the comment loss lives.

Bottom line: flag-OFF is safe to ship. Before enabling scanners/component-insertion widely I'd land #1#3 (all silently produce degraded/invalid YAML on the shared, non-flagged merge path) and add append-into-existing-array merge tests. #4#6 are minor.
· branch UX-1338/rpcn-visual

@SpicyPete SpicyPete merged commit b440550 into master Jul 14, 2026
18 checks passed
@SpicyPete SpicyPete deleted the UX-1338/rpcn-visual branch July 14, 2026 20:17
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