Skip to content

feat: pipeline page - view, edit, create modes with pipeline flow diagram#2285

Merged
eblairmckee merged 39 commits intomasterfrom
pr4/edit-create-mode-pipeline-page
Mar 25, 2026
Merged

feat: pipeline page - view, edit, create modes with pipeline flow diagram#2285
eblairmckee merged 39 commits intomasterfrom
pr4/edit-create-mode-pipeline-page

Conversation

@eblairmckee
Copy link
Contributor

@eblairmckee eblairmckee commented Mar 11, 2026

Summary

  • Redesigns the pipeline page with an IDE-like resizable panel layout across all three modes (view, edit, create)
  • View mode: read-only details panel (pipeline info, throughput chart, tags, secrets/topics references), logs panel, and flow diagram sidebar
  • Edit/create mode: YAML editor with lint hints, config dialog (name, description, compute units, tags), command palette (Cmd+Shift+P), connector picker, and direct topic/user dialogs for Redpanda components
  • Flow diagram: renders PipelineFlowDiagram in the left sidebar for all modes, gated behind enablePipelineDiagrams feature flag. Redpanda component leaves show setup-hint buttons ("+ Topic", "+ User") when topic/SASL config is missing, and doc links on hover
  • Connector picker: all connectors (including Redpanda) are handled uniformly via getConnectTemplate, inlined directly in PipelinePage. No separate wizard component — diagram hint buttons guide users to add topic/user config after insertion
  • Topic/user dialogs: clicking "+ Topic" or "+ User" on a diagram leaf opens a single-purpose dialog with the relevant form (AddTopicStep / AddUserStep). On submit, surgically patches the YAML via tryPatchRedpandaYaml without regenerating the entire component block
  • Bug fix: serverless init — extracted to useCreateModeInitialYaml hook. useEffect early-returns when components.length === 0, reads sessionStorage directly as hydration fallback, includes 3s timeout. Fixes race where getConnectTemplate silently failed before component list resolved
  • Persisted YAML decoupling: when enablePipelineDiagrams is active, YAML is no longer synced to the onboarding YAML store (the diagram UX doesn't use the wizard persistence path)
  • Dead code cleanup: deleted connector-wizard.tsx, create-pipeline-sidebar.tsx, add-secrets-card.tsx, add-contextual-variables-card.tsx

PR #2284 review comments addressed

  • Extracted throughput chart helpers to utils/pipeline-throughput.utils.ts with unit tests
  • Removed unnecessary useMemo/useCallback in throughput card
  • Regenerated yarn.lock

Test coverage (18 test files, ~225 test cases)

Onboarding steps (24 tests):

  • add-topic-step.test.tsx — topic creation/selection form, validation, error states
  • add-user-step.test.tsx — SASL + service account auth flows, consumer group fields

Pipeline page (51 tests):

  • index.test.tsx (17) — create/edit/view mode rendering, form validation, save/cancel flows, serverless init skeleton, connector dialog integration
  • config-dialog.test.tsx (9) — name input, description, compute slider, tags CRUD
  • details-dialog.test.tsx (11) — pipeline info, compute units, URL, service account, tags, secrets, topics, delete
  • pipeline-command-menu.test.tsx (14) — slash command menu, variable insertion, keyboard navigation

Flow diagram (30 tests):

  • pipeline-flow-diagram.test.tsx (15) — empty/error states, section headers, placeholder nodes, topic badges, setup hint buttons ("+ Topic" / "+ User"), docs links
  • pipeline-flow-nodes.test.ts (6) — getConnectorDocsUrl for inputs/outputs/processors, non-docs sections
  • pipeline-flow-parser.test.tsx (9 new) — missingTopic/missingSasl flags on Redpanda components, SASL detection (component-level + root redpanda.sasl), flag passthrough to layout

Utilities (109 tests):

  • yaml.test.tsx (24 new) — patchRedpandaConfig (topic/SASL patching), applyRedpandaSetup (surgical patch + template fallback), generateYamlFromWizardData, buildSaslPatch (service-account + SASL)
  • use-create-mode-initial-yaml.test.tsx (12) — serverless path, non-serverless restoration, timeout, disabled state, diagrams-enabled gating
  • use-slash-command.test.ts + .test.tsx (61) — slash trigger detection, cursor positioning, command insertion, popover lifecycle
  • pipeline-throughput.utils.test.ts (12) — series merging, timestamp formatting, empty data handling

Other (15 tests):

  • constants.test.ts (5) — isSystemTag, isCloudManagedTagKey
  • onboarding-wizard-store.test.tsx (5) — store hydration, getWizardConnectionData sessionStorage fallback

Feature flags

  • enablePipelineDiagrams — gates PipelineFlowDiagram in the left sidebar (all modes), setup hint buttons, doc links, and YAML persistence decoupling
  • enableConnectSlashMenu — gates inline / slash command menu in the YAML editor
  • enableRpcnTiles — gates the tile-based connector picker in the onboarding wizard
  • enableNewPipelineLogs — gates LogExplorer vs legacy LogsTab in view mode

Known issues

  • react-doctor set-state-in-effect at index.tsx:327setYamlContent(pipeline.configYaml) in the form-reset effect triggers a React Compiler warning. Alternative approaches (queueMicrotask, derivation, key-remount) have worse tradeoffs. For now commenting ot react-doctor to ignore and suppress error.

@secpanda
Copy link

secpanda commented Mar 11, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

isLoading,
nameError,
autoFocus,
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Toolbar renders multiple conditional UI states
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: if you do find any time, please try and avoid the complexity being ignored, I think the LLM starts to notice this pattern more and more so it would just slam another biome-ignore and move on

@malinskibeniamin
Copy link
Contributor

@claude review

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @malinskibeniamin's task in 2m 32s —— View job


PR Review: Edit/Create Mode Pipeline Page

  • Gather context and understand the PR
  • Read key changed files
  • Review architecture and patterns
  • Check for bugs, security, and correctness issues
  • Review test coverage

Overall the PR is well-structured with good separation of concerns. The orchestration in index.tsx is intentionally complex and the biome-ignore is justified. Below are findings ranging from bugs to nits.

Bugs

1. Duplicate entry in PRIORITY_COMPONENTSconnect-tiles.tsx:40,44

mysql_cdc appears twice in the PRIORITY_COMPONENTS array. The second occurrence (index 12) is a no-op since the sort deduplication logic uses PRIORITY_COMPONENTS.includes(), but it's still dead/confusing:

const PRIORITY_COMPONENTS = [
  // ...
  'mysql_cdc',      // index 7
  'mongodb_cdc',
  'snowflake_streaming',
  'redpanda_migrator',
  'mysql_cdc',      // index 12 - duplicate
  'snowflake_put',

Fix this →

2. Conflicting SVG gradient IDs — pipeline-throughput-card.tsx:163-170 and chart-skeleton.tsx:97-104

Both components use hardcoded gradient IDs (fillIngress/fillEgress in throughput card; skelFill1/skelFill2 in chart skeleton). If two instances ever render on the same page (e.g., in a list view or Storybook), the gradients will silently collide — the second card will paint with the first card's gradient colors.

Consider generating unique IDs with useId() from React.
Fix this →

3. crypto.randomUUID() as React key — connect-tiles.tsx:135

Skeleton tiles use crypto.randomUUID() directly as the key prop, which regenerates on every render and defeats React's reconciliation entirely:

const skeletonTiles = Array.from({ length: tileCount }, () => (
  <div ... key={crypto.randomUUID()}>   // new key every render

Replace with index-based keys or a stable pre-generated list.
Fix this →

Correctness / Potential Issues

4. Render-body ref mutation — index.tsx:201-205

The pattern of comparing and resetting a ref directly in the render body is fragile with React Strict Mode / Concurrent Features — the render function may run multiple times before committing:

// These lines run during render, not in an effect
const prevYamlRef = useRef(yamlContent);
if (prevYamlRef.current !== yamlContent) {
  prevYamlRef.current = yamlContent;
  userLintOverrideRef.current = null;  // side effect in render
}

The intent (reset manual lint panel override when YAML changes) is better served by moving this into the handleYamlChange callback directly, which already has a reference to userLintOverrideRef.

5. existingSecrets={[]} always empty — index.tsx:692

The AddSecretsDialog is passed existingSecrets={[]} unconditionally, so QuickAddSecrets never knows about secrets already referenced in the YAML. This means users could create duplicate secrets without any warning. Consider deriving existing secrets from yamlContent (you already have extractSecretReferences used in view-details.tsx).

6. useListSecretsQuery is always active — pipeline-command-menu.tsx:83

Unlike useListUsersQuery (which correctly gates on { enabled: open }), the secrets query runs unconditionally regardless of whether the command menu is open. This causes an unnecessary network request on page load.

7. form.watch('name') in JSX — index.tsx:579

form.watch('name') subscribes to all form re-renders. Given the name is typed character-by-character and drives the toolbar, this will cause the entire PipelinePage to re-render on every keystroke. Consider useWatch({ control, name: 'name' }) which is more explicit, or lift the name state out of the form for the toolbar.

Nits / Style

8. MIN_TASKS re-declared in test — config.test.tsx:22

The test file re-declares const MIN_TASKS = 1 instead of importing from source. If the minimum changes, the test silently continues to pass:

// config.test.tsx
const MIN_TASKS = 1;  // should import from '../tasks'

9. Brittle className assertion in test — config.test.tsx:139-149

Testing expect(keyInput.className).toContain('bg-primary-alpha-default') couples the test to an implementation-detail Tailwind class. If the styling changes (e.g., different shade), the test breaks without any behavior change. Consider testing visible behavior instead.

10. useListTopicsQuery({ pageSize: 500 }) hardcoded — pipeline-command-menu.tsx:89

500 is likely sufficient for most clusters, but worth a comment explaining why this cap was chosen, or wiring it to a constant.

11. hasInput / hasOutput detection is fragile — index.tsx:588-591

yamlContent.includes('input:') will also match input: appearing inside comments or string values. This is low-risk in practice, but consider using the existing YAML parser utilities instead.

12. lint-hint-list.tsx indentation inconsistency

Lines 44, 46-48 have mixed indentation and inline className on SimpleCodeBlock that sits outside the component's normal style. The reviewer's note about adding syntax highlighting support is acknowledged.

Addressing Reviewer Comments

  • chart-skeleton.tsx / recharts v2 vs v3: The reviewer's question is valid. Since shadcn is still holding on recharts v2, continuing with v2 here avoids a potentially breaking migration and is the right call for now. Worth tracking the upstream shadcn issue.
  • editable-text.tsx: The reviewer asks if this should live in the UI registry or under typography. Given it's a fairly general-purpose editable input that matches heading styles, it's a good candidate for the registry — especially since it would benefit from consistent behavior across the app.
  • Icon imports (lint-hint-list.tsx:15, toolbar.tsx:42): Both lucide-react imports should go through components/icons per the reviewer's notes for consistency.
  • toolbar.tsx:201 biome-ignore: The noExcessiveCognitiveComplexity suppression is added to a component that was already complex. The reviewer is right that this pattern signals the need for decomposition — the renderActionButton logic and the state/config factories are already a step in the right direction, but could be split into a PipelineActionButton sub-component.

Summary

The PR introduces a well-thought-out IDE-like layout with clean state management and good test coverage for the Config form. The main actionable items before merge are:

  1. Bug: Deduplicate mysql_cdc in PRIORITY_COMPONENTS
  2. Bug: Fix crypto.randomUUID() skeleton keys
  3. Bug: Scope SVG gradient IDs with useId()
  4. Cleanup: Move render-body side effect (userLintOverrideRef.current = null) into handleYamlChange
  5. Correctness: Gate secrets query on open in command menu

@eblairmckee eblairmckee changed the title Pr4/edit create mode pipeline page feat: pipeline page - view, edit, create modes with pipeline flow diagram Mar 12, 2026
@eblairmckee
Copy link
Contributor Author

@claude

@claude
Copy link

claude bot commented Mar 12, 2026

Claude finished @eblairmckee's task in 24s —— View job


Hey @eblairmckee! It looks like your comment just has @claude with no additional instructions. Could you clarify what you'd like me to do?

For example:

  • Implement the bug fixes from my previous review (duplicate mysql_cdc, crypto.randomUUID() keys, SVG gradient IDs, secrets query gating, etc.)
  • Answer a specific question about the PR
  • Something else entirely

Just let me know and I'll get started!

@eblairmckee
Copy link
Contributor Author

@claude review this PR

@claude
Copy link

claude bot commented Mar 12, 2026

Claude finished @eblairmckee's task in 1m 59s —— View job


PR Review: Pipeline Page - View, Edit, Create Modes

  • Gather context and understand the PR
  • Read key changed files
  • Review architecture and patterns
  • Check for bugs, security, and correctness issues
  • Review test coverage

Good news first: the bugs flagged in the previous review (@malinskibeniamin's round) have all been addressed in the current state of this branch:

  • ✅ Duplicate mysql_cdc in PRIORITY_COMPONENTS — gone
  • crypto.randomUUID() skeleton keys — now uses index-based (_, i) with key={i}
  • ✅ SVG gradient ID collisions — both AreaVariant (chart-skeleton.tsx) and pipeline-throughput-card.tsx correctly use useId()
  • ✅ Secrets query gated on openuseListSecretsQuery({}, { enabled: open }) at line 83
  • useWatch instead of form.watchuseWatch({ control: form.control, name: 'name' }) at line 559

Below are the remaining issues I found.

Bugs / Correctness

1. useListTopicsQuery not gated on openpipeline-command-menu.tsx:90

Unlike secrets/users queries which correctly use { enabled: open }, the topics query fires unconditionally on every page load regardless of whether the command menu has ever been opened:

// secrets — correctly gated ✅
const { data: secretsResponse } = useListSecretsQuery({}, { enabled: open });

// topics — always fires ❌
const { data: topicsResponse } = useListTopicsQuery({ pageSize: 500 });

Fix this →

2. existingSecrets={[]} always empty — index.tsx:692

AddSecretsDialog is passed a hardcoded empty array. The component uses existingSecrets to warn about duplicates, so users can create colliding secrets without any feedback. The YAML secrets are already extracted elsewhere — extractSecretReferences/getUniqueSecretNames are imported in view-details.tsx and could be reused here.

Fix this →

3. Fragile hasInput/hasOutput detection — index.tsx:588-591

String .includes('input:') and .includes('output:') will match occurrences inside YAML comments (# no input: here) or string values. Low-risk today but can produce false positives that hide the sidebar card incorrectly. The YAML parser is already available in this file via parseSchema/getConnectTemplate.

Nits / Code style

4. Lucide icons imported directly — config.tsx:26, pipeline-command-menu.tsx:23, connect-tiles.tsx:20-21

Several new files import directly from lucide-react rather than going through components/icons, which the reviewer flagged consistently (and toolbar.tsx already complies):

// config.tsx
import { PlusIcon, XIcon } from 'lucide-react'; // should be components/icons

// pipeline-command-menu.tsx
import { PlusIcon } from 'lucide-react'; // same

// connect-tiles.tsx
import { Search } from 'lucide-react'; // same

5. ref={null} in standalone dialogs — index.tsx:710, index.tsx:724

AddTopicStep and AddUserStep are forwardRef components. Passing ref={null} explicitly (rather than omitting the prop) works but is misleading — it suggests the ref is intentionally suppressed rather than simply not needed. Since these are standalone dialogs without a wizard flow, the ref can just be omitted.

6. EditableText autoFocus initializes state — editable-text.tsx:51

const [isEditing, setIsEditing] = useState(autoFocus ?? false);

This works correctly for initial render, but if autoFocus changes after mount (e.g. switching from create → edit mode on the same component instance), the component won't respond. This is unlikely in practice given the current routing, but worth noting for future-proofing.

Test coverage

7. config.test.tsx — Redundant final test (lines 138-151)

'tag key input is rendered and editable' is almost identical to 'tag key and value inputs are editable' (lines 121-136) — both type 'test' into the key input and assert the value. The test suite would be cleaner with this test removed or replaced with validation-specific coverage (e.g., verify the error message appears when a key is empty).

8. config.test.tsx — Test schema diverges from source

The test defines its own inline Zod schema (line 23) with a different tags.value rule (.min(0) vs .min(1, 'Value is required') in production). If the source schema validation changes, tests may not catch regressions. Consider importing the schema and form defaults from the production module.

Summary

The architecture is clean and well-structured. The previous review's bug fixes are all in. The main items worth addressing before merge:

  1. Gate useListTopicsQuery on open in pipeline-command-menu.tsx (unnecessary network call)
  2. Populate existingSecrets in AddSecretsDialog from YAML parse (otherwise duplicate-secret guard is inert)
  3. Icon imports through components/icons in 3 new files
  4. Test cleanup — remove redundant final test in config.test.tsx

Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a nice addition to backport to registry afterwards

@claude
Copy link

claude bot commented Mar 20, 2026

Claude finished @eblairmckee's task in 3m 22s —— View job


Review: Changes Since March 12th 2026 (Post-Approval)

  • Gather commits since March 12th 2026
  • Read changed files
  • Review changes
  • Post review feedback

Good work on the feature-flag layering strategy — the three-tier rollback plan is well thought out. Below are findings specific to the post-approval work (March 18–19).

Summary of post-approval changes

  • enableConnectSlashMenu — typing / in the YAML editor opens a popover command menu
  • enableNewPipelineLogs — swaps LogsTab for the new LogExplorer in view mode
  • enablePipelineDiagrams — wizard-less UX; when false, wizard.tsx still renders ConnectOnboardingWizard (with PipelinePage embedded in the final step)
  • Header hiding, layout cleanup, functional parity fixes, alert for duplicate topic/user input

Bugs / Regressions

1. Feature flag name mismatch — constants.ts:17 vs PR description

The PR description (eblairmckee's March 19 comment) names the flag enablePipelineSlashMenu, but the code registers it as enableConnectSlashMenu:

// constants.ts
enableConnectSlashMenu: false,

// index.tsx
const isSlashMenuEnabled = isFeatureFlagEnabled('enableConnectSlashMenu');

If Cloud UI injects flags by name, a mismatch here means the feature silently stays off. Worth confirming the name matches the flag key in Cloud UI config. Fix this →

2. Secrets query lost its open gating — pipeline-command-menu.tsx:383

The March 12th review confirmed secrets was gated (useListSecretsQuery({}, { enabled: open })). In the current file it's unconditional again:

// ❌ now fires on every page load
const { data: secretsResponse } = useListSecretsQuery({});

// ❌ still not gated (flagged in March 12 review)
const { data: topicsResponse } = useListTopicsQuery({ pageSize: 500 });

// ✅ still correctly gated
const { data: usersResponse } = useListUsersQuery(undefined, { enabled: open });

Fix this →

3. Slash trigger fires on URLs — use-slash-command.ts:17,33

SLASH_TRIGGER_PATTERN = /[\s:]/ means a / typed after : triggers the menu. So typing http:/ (the char before the second / is :) will open the command menu mid-URL:

// 'http:/' → charBefore === ':' → TRIGGERS
// 'https:/' → same

Users typing full URLs in YAML values (e.g. an HTTP input URL) will get an unexpected popover. Consider also excluding the case where the char before the previous slash is also a non-whitespace character, or restricting to start-of-value patterns.

Fix this →

4. Duplicate test files for same module — use-slash-command.test.ts + use-slash-command.test.tsx

Both use-slash-command.test.ts and use-slash-command.test.tsx exist and test the same hook. Two test runners may pick both up or only one depending on glob config. The .ts file should be removed (utility functions) or their coverage merged into one file.

Fix this →

Correctness / Potential Issues

5. PipelineCommandMenu rendered twice, both instances fetch secrets and topics

index.tsx always mounts the dialog variant and (when isSlashMenuEnabled) the popover variant simultaneously:

// Always mounted — fetches secrets + topics unconditionally
<PipelineCommandMenu ... open={isCommandMenuOpen} />

{isSlashMenuEnabled ? (
  // Also mounted — same unconditional fetches
  <PipelineCommandMenu ... variant="popover" open={slashCommand.isOpen} />
) : null}

React Query deduplicates identical query keys so there won't be double network requests, but the unconditional fetching (issue #2 above) is amplified because fixing gating on one instance also needs to be applied correctly to both. The dialog variant's open state is isCommandMenuOpen, the popover's is slashCommand.isOpen — they're different booleans, so any enabled guard in the component needs to correctly reflect when either variant might be open.

6. header.tsx — hiding condition may not match commit message intent

The commit message says "hides header when either enableRpcnTiles or enablePipelineDiagrams are enabled", but the code only hides when enableRpcnTiles and isEmbedded() together:

// header.tsx:218-223
if (
  isPipelineRoute &&
  (isFeatureFlagEnabled('enablePipelineDiagrams') || (isFeatureFlagEnabled('enableRpcnTiles') && isEmbedded()))
) {
  return true;
}

In non-embedded OSS mode with only enableRpcnTiles=true, the header is not hidden. This may be intentional (OSS doesn't embed PipelinePage the same way), but worth a comment to clarify the intent. The screenshots in the PR show the new layout without the breadcrumb header, which only renders in that configuration.

7. PipelinePage wrapped in Card inside onboarding-wizard.tsx:427-434

The wizard's CREATE_CONFIG step wraps PipelinePage in a Card/CardContent, but PipelinePage now renders its own bordered panel layout. This creates a double-border nesting (Card border → PipelinePage's rounded-lg border). Visual regression risk when enablePipelineDiagrams=false and wizard is used. Fix this →

Nits

8. Lucide icons still imported directly in pipeline-command-menu.tsx:28

import { PlusIcon } from 'lucide-react'; // should use components/icons

This was flagged in the original review for other files and is still present in the new file.

Summary

The feature flag architecture is solid and the rollback story is clear. The main actionable items:

# Severity Item
1 High Confirm enableConnectSlashMenu vs enablePipelineSlashMenu flag name matches Cloud UI
2 High Re-gate useListSecretsQuery + useListTopicsQuery on open (regression)
3 Medium Slash trigger fires on http:// URLs
4 Low Remove duplicate use-slash-command.test.ts / .test.tsx
5 Low Double-border in wizard's CREATE_CONFIG step
6 Nit Clarify header-hiding condition in comment

@eblairmckee
Copy link
Contributor Author

react doctor fixes in c2d847e

  • connector-wizard.tsx — Extracted conditional logic from handleNext into a separate submitStep function so the try/catch
    only contains a simple await call (compiler can't handle conditionals inside try/catch)
  • details-dialog.tsx — Extracted pipeline?.configYaml into a local configYaml variable so useMemo deps match what the
    compiler infers
  • pipeline-command-menu.tsx — Three fixes:
    • Removed synchronous setState from useAnchorPosition effect; derive hidden style during render instead
    • Added clickOutsideRef to popoverRef callback deps to match compiler inference
    • Removed try/catch from handleCreateTopic/handleCreateUser; replaced optional chaining with explicit ref guards

@eblairmckee
Copy link
Contributor Author

@malinskibeniamin when I updated data-table noticed an unintended breaking change that could've been upstreamed. fixed in commit: 4c43700

Breaking Change: DataTablePagination pagination prop removed

What actually happened

The registry never had the pagination prop. This isn't an upstream breaking change — it's a local console modification getting overwritten by a registry sync.

Git blame timeline

Date Who Commit Repo What
Aug 12 2025 Beniamin Malinski 9c6620b3 ui-registry Created data-table.tsx with simple { table } API
Mar 6 2026 Beniamin Malinski 7b92169bb console Added DataTablePaginationState type + pagination prop to console's local copy of data-table.tsx. Updated all 8 call sites. Commit msg: "fix pagination issues on all list views"
Mar 13 2026 Beniamin Malinski 27d5e581b console Added 'use no memo' directive to console's copy — belt-and-suspenders fix for more React Compiler issues in DataTable (PR #2297)
Mar 18 2026 Blair McKee f61dccaf7 console "updated ui registry components" — overwrote local modifications with upstream, which never had them

Root cause

Beniamin modified the registry component in-place in the console repo instead of either:

  1. Upstreaming the fix to the ui-registry, or
  2. Creating a local wrapper component that wouldn't be overwritten on sync

When Blair synced registry components, the upstream version (which never had the pagination prop) replaced the locally-modified version, breaking all 8 call sites.

Why the pagination prop existed

It was a React Compiler workaround. The commit 7b92169bb included this comment:

"Pass pagination primitives from the page so this footer still rerenders
when React Compiler keeps the table instance identity stable."

The problem: React Compiler aggressively memoizes, and TanStack Table reuses the same object reference for table across renders. The compiler saw a stable reference and skipped re-rendering the pagination footer — even when page index/size changed.

The workaround was to pass primitive values (pageIndex, pageSize, etc.) as a separate prop so React could detect changes via shallow comparison.

One week later (27d5e581b), 'use no memo' was added as a second workaround — opting the entire file out of React Compiler. This made the pagination prop redundant, but it was never removed.

Risk: React Compiler re-rendering regression

Both workarounds are now gone ('use no memo' directive + pagination prop). If the pagination stale-render bug resurfaces, the fix should be:

  • Upstreamed to the registry, or
  • Applied via a local wrapper component that won't be stomped by registry syncs

Do not modify registry files in-place — this is exactly how we got here.

What changed in the registry update

Old API (console-local modification):

<DataTablePagination
  pagination={{
    canNextPage: table.getCanNextPage(),
    canPreviousPage: table.getCanPreviousPage(),
    pageCount: table.getPageCount(),
    pageIndex: pagination.pageIndex,
    pageSize: pagination.pageSize,
  }}
  table={table}
/>

New API (what the registry always had):

<DataTablePagination table={table} />

Other changes in this update (non-breaking)

  • Props interfaces now extend SharedProps instead of defining testId inline
  • DataTableFacetedFilter gained a labelClassName prop
  • DataTablePaginationState type deleted (never imported externally)
  • Various styling tweaks (button variants, badge variants, class ordering)

Affected files (8 call sites)

File Line
src/components/pages/agents/list/ai-agent-list-page.tsx ~484
src/components/pages/knowledgebase/details/knowledge-base-document-list.tsx ~294
src/components/pages/knowledgebase/list/knowledge-base-list-page.tsx ~559
src/components/pages/mcp-servers/list/remote-mcp-list-page.tsx ~450
src/components/pages/rp-connect/pipeline/list.tsx ~681
src/components/pages/secrets-store/secrets-store-list-page.tsx ~416
src/components/pages/topics/Tab.Messages/index.tsx ~1772
src/components/ui/connect/log-explorer.tsx ~469

Fix

At each call site, replace the multi-line <DataTablePagination pagination={{...}} table={table} /> with <DataTablePagination table={table} />.

@@ -0,0 +1,191 @@
# useEffect Rules
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know we're adding a react-doctor rule for this, but this saved my sanity during this effort.

@eblairmckee
Copy link
Contributor Author

for transparency:

Adversarial Review: pr4/edit-create-mode-pipeline-page

Branch: pr4/edit-create-mode-pipeline-page vs master
Scope: 74 files changed, +8,364 / -2,942 lines
Date: 2026-03-19
Reviewer persona: Senior engineer, 15+ years distributed systems. Cost-aware, security-paranoid, biased toward simplification.


Problem

This branch introduces a view/edit/create mode system for the pipeline authoring page, a slash command menu for YAML editing, a throughput chart, config/details dialogs, connector wizard integration, and significant shared UI component refactoring. It is gated behind two feature flags (enablePipelineDiagrams, enableRpcnTiles) for cloud-only rollout.


Assumptions

ASSUMPTIONS:
1. All DataTablePagination consumers have been updated (VERIFIED — 7 call sites migrated)
2. Feature flags are cloud-only (isEmbedded() guard present)
3. The `react-resizable-panels` dependency is the only new runtime dep
4. No other branches are simultaneously modifying the shared UI components
5. The old wizard flow remains intact when both flags are OFF
→ Correct me now or I proceed with these.

Risk Analysis

SEVERITY: CRITICAL

1. Form + YAML desync race condition in EDIT mode

File: pipeline/index.tsx:281-291
What: form.reset() and setYamlContent() are decoupled via queueMicrotask. The form state updates synchronously but the YAML editor content is deferred.
Failure mode: If the user clicks Save before the microtask fires, the request sends stale/empty YAML with valid form metadata. The pipeline gets updated with wrong config.
Blast radius: Data corruption — pipeline saved with wrong YAML. Requires manual recovery.
Quantified risk: Low probability (microtask fires within ~1ms), but non-zero. More likely on slow devices or if React batching delays the microtask.
Recommendation: Either gate the Save button on a yamlLoaded flag, or set YAML synchronously within the same useEffect. The queueMicrotask pattern here is solving a React rendering order issue — consider using flushSync or a combined state update instead.

2. Shared UI component changes have wide blast radius

Files: combobox.tsx, data-table.tsx, dropdown-menu.tsx, select.tsx, typography.tsx, input.tsx
What: 6 shared components modified. These are consumed across the entire application.
Changes include:

  • DataTablePagination signature breaking change (old pagination prop removed)
  • DataTableFacetedFilter badge variant changed (primary-invertedsecondary)
  • Combobox create button relocated from inline to footer
  • DropdownMenu portal container behavior changed
  • Select portal container fallback added

Verification status: DataTablePagination consumers updated on this branch ✅. But:

  • Badge variant change affects visual appearance of ALL faceted filters app-wide — visual regression tests?
  • Combobox create button relocation affects any page using creatable=true
  • Portal container changes could affect z-index stacking in nested modals

Recommendation: This is the highest-risk area due to cross-cutting impact. These shared component changes should ideally be in a separate PR merged first, with explicit before/after visual regression testing on all consumer pages. Bundling them here makes rollback impossible without also losing the pipeline feature.


SEVERITY: HIGH

3. Error lint hints get stuck after failed save

File: pipeline/index.tsx:265-278, 409, 438
What: errorLintHints is set from API error responses on failed create/update. It's only cleared on successful save (setErrorLintHints({}) at lines 409, 438). If the user fixes the YAML and re-lints (which updates responseLintHints), the stale errorLintHints still wins because of this precedence:

const lintHints = Object.keys(errorLintHints).length > 0 ? errorLintHints : responseLintHints;

Failure mode: User sees stale error hints from a previous failed save attempt, even after correcting the YAML. They cannot tell which errors are current.
Recommendation: Clear errorLintHints when YAML content changes, or when a new lint response arrives. The current approach is "last error wins" but should be "latest signal wins."

4. Focus management in command menu uses fragile retry loop

File: pipeline-command-menu.tsx:341-364
What: Popover variant uses setTimeout(0)requestAnimationFrame with up to 10 retry attempts to steal focus from Monaco into the command input. This fights the editor for focus.
Failure mode: On slow devices or under high load, the retry loop exhausts without acquiring focus. User types into Monaco instead of the command menu. Or the loop acquires focus, Monaco reclaims it, and the menu becomes unusable.
Quantified risk: Hard to reproduce in dev, likely manifests on low-powered Chromebooks or when DevTools is open.
Recommendation: Use Monaco's onDidBlurEditorWidget or explicitly editor.updateOptions({ readOnly: true }) while the menu is open, then restore. Fighting for focus is inherently fragile.

5. Zustand store cleanup not error-safe in connector wizard

File: connector-wizard.tsx:264-291
What: The handleComplete callback populates Zustand stores with topic/user data, calls getConnectTemplate(), then resets both stores. If getConnectTemplate() throws, the stores are left polluted.
Failure mode: Subsequent wizard invocations start with stale data from a failed previous attempt.
Recommendation: Wrap in try/finally to guarantee store reset regardless of success/failure.

6. Two command menus can be open simultaneously

File: pipeline/index.tsx:679-693
What: Two PipelineCommandMenu instances exist — one triggered by Cmd+Shift+P (dialog variant), one triggered by / in the editor (popover variant). They have independent open states.
Failure mode: User opens one, then triggers the other. Both render. Both try to insert text into the editor. Edits could conflict.
Recommendation: Add mutual exclusion — when one opens, close the other. A single commandMenuMode: 'dialog' | 'popover' | null state would be cleaner.


SEVERITY: MEDIUM

7. Tag merge-on-save can silently overwrite server-side system tag changes

File: pipeline/index.tsx:287-289 vs 427-429
What: On load, system tags are filtered OUT so users only edit user tags. On save, the original system tags are merged back in with user tags. If system tags changed server-side between load and save, the save overwrites the server's current system tags with stale values.
Quantified risk: Low — system tags are managed by the platform and rarely change. But if they do, this is a silent data loss.
Recommendation: Re-fetch pipeline before save, or use If-Match / ETag semantics if the API supports it.

8. userLintOverrideRef appears to be dead code

File: pipeline/index.tsx:258
What: userLintOverrideRef is declared and reset when YAML changes (line 486), but it is never read anywhere in the component.
Risk: Dead code. Either an incomplete feature or leftover from refactoring.
Recommendation: Remove it. If it's an intentional hook for future use, add a comment explaining the intent. Dead refs add confusion.

9. SLASH_TRIGGER_PATTERN doesn't match colon despite comment

File: use-slash-command.ts:17
What: Comment says "after whitespace/colon" but the regex is /\s/ which only matches whitespace. Colon-preceded slashes (common in YAML like input:\n /) would not trigger the menu.
Risk: Either the comment is wrong (low risk — just misleading), or the implementation is wrong (medium risk — missing intended trigger).
Recommendation: Verify intent. If colon should trigger, update pattern to /[\s:]/. If not, fix the comment.

10. No concurrent edit protection

What: If two browser tabs open the same pipeline in edit mode, both can save independently. Last write wins with no conflict detection.
Quantified risk: Multi-user/multi-tab editing is plausible in team environments.
Recommendation: Not necessarily a blocker for this PR, but worth noting as a known gap. Optimistic locking (version counter or ETag) would be the standard fix.

11. Throughput card has no error UI

File: pipeline-throughput-card.tsx:72-96
What: useExecuteRangeQuery hooks don't expose error states. If metric queries fail, the card shows "Throughput metrics not available" (the empty-data state) with no indication that it's an error vs. genuinely no data.
Recommendation: Distinguish "no data" from "query failed" in the UI. Even a subtle error icon would help debugging.


SEVERITY: LOW

12. New runtime dependency: react-resizable-panels@^3.0.4

What: One new dependency added. It's well-maintained (1.6M weekly npm downloads), MIT licensed, tree-shakeable.
Risk: Minimal. Bundle size impact is ~8KB gzipped.
Recommendation: Acceptable. No action needed.

13. details.tsx deleted, replaced by details-dialog.tsx

File: pipeline/details.tsx (deleted), pipeline/details-dialog.tsx (added)
What: The old details component is removed entirely. If any other code imports from pipeline/details, it will fail at build time.
Risk: Build-time failure (caught immediately), not a runtime risk.
Recommendation: Verify no other imports reference the deleted file. [CONFIDENCE: HIGH — build would catch this]

14. Log explorer changes are unrelated to pipeline feature

File: log-explorer.tsx
What: Grid layout changes, enableLiveView prop added, keyCode display removed, message code block added. These are UX improvements to the log explorer, bundled into the pipeline PR.
Risk: Scope creep. Makes the PR harder to review and rollback.
Recommendation: Consider splitting into a separate PR. If already reviewed and approved, acceptable.


Test Coverage Assessment

Overall: ~40% of critical paths tested

Component Tested Gap
config-dialog Basic rendering only No save flow, no validation, no edit/view modes
details-dialog Happy path rendering No empty states, no delete flow, no overflow
use-slash-command Good unit + integration Missing multi-slash, rapid open/close, special chars
pipeline-throughput-utils Good utility coverage No error cases, no large dataset perf
constants Basic coverage No edge cases (empty string, mixed case)
pipeline/index.tsx NONE Zero test coverage for the main 700-line component
pipeline-command-menu.tsx NONE Zero test coverage for the 643-line command menu
connector-wizard.tsx NONE Zero test coverage for the 337-line wizard
toolbar.tsx NONE Zero test coverage

The two largest components by line count (index.tsx and command-menu.tsx) have zero tests. This is the single biggest risk in the PR. Mode transitions, save flows, error handling, feature flag gating, and wizard integration are all untested.


Scope Creep Check

Items in this PR that are not directly related to "edit/create mode pipeline page":

  1. 6 shared UI component refactors (combobox, data-table, dropdown-menu, select, input, typography) — affects entire app
  2. Log explorer UX improvements — grid layout, live view prop, key removal
  3. DataTablePagination API simplification across 7 consumer pages — good cleanup, but cross-cutting
  4. react-best-practices/rules/use-effect.md — documentation file, not code
  5. AI gateway import cleanups — 3 files with minor import changes
  6. MCP server component tweaks — type changes in tool-card, tools-step, remote-mcp-configuration-tab

Verdict: ~15 files are tangential to the core feature. The shared UI changes are the most concerning because they create merge conflicts and make rollback complex.


Alternatives Considered

Approach Pros Cons Why rejected/accepted
Split shared UI changes into separate PR Clean rollback, isolated testing More PRs to manage Recommended but not done
Feature flag everything Safe rollout Complexity Done — good choice
Single monolithic component Simpler file structure 1000+ line component Rejected — decomposed into dialogs/hooks
MobX for state Existing pattern Being removed from codebase Rejected — uses React Query + Hook Form + Zustand correctly

Summary

PLAN SUMMARY:
- Pipeline page: View/Edit/Create modes with proper routing and feature flag gating
- Command menu: Dual-mode (dialog + popover) for inserting variables/secrets/topics
- Shared UI: DataTablePagination simplified, portal container support added
- State: React Hook Form + Zustand + React Query layered approach

OUT OF SCOPE:
- Concurrent edit protection: Intentionally excluded, standard gap
- E2E tests: Not included in this branch
- Mobile/responsive: Not addressed

OPEN RISKS:
- Form+YAML desync (CRITICAL): queueMicrotask can cause stale YAML on save
- Shared UI blast radius (CRITICAL): 6 components modified, affects entire app
- Stuck error hints (HIGH): errorLintHints not cleared on new YAML edits
- Focus fighting (HIGH): Command menu retry loop is fragile
- Zero test coverage on main components (HIGH): index.tsx, command-menu.tsx untested

KNOWN UNKNOWNS:
- How does CloudV2 integration behave with the new getWizardConnectionData() fallback?
- What is the actual latency of metric queries for the throughput card at scale?
- Are there other consumers of the deleted details.tsx outside this repo?

Reviewer Verdict

Not ready to merge without addressing:

  1. The form+YAML desync race (Critical Add readiness & liveness check #1) — fix or add a guard
  2. The stuck error hints (High Create helm chart #3) — clear on YAML change
  3. Mutual exclusion for the two command menus (High Check list message performance (response time) #6)

Strongly recommended but not blocking:
4. Split shared UI component changes into a separate PR
5. Add at least smoke tests for index.tsx mode transitions and save flows
6. Zustand store cleanup in try/finally (High #5)

Acceptable as-is:
7. Everything marked MEDIUM and LOW
8. Test coverage gaps for utilities (they're covered at basic level)
9. New dependency (react-resizable-panels)

return (
<BadgeGroup
className="min-w-[184px]"
className="min-w-[174px]"
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems oddly specific but all good

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I originally wanted to use flex-* values that were relative to available space, but it resulted in CLS when columns actually loaded, so opted to hard code them (purely eye balled what seemed right).

if (isFeatureFlagEnabled('enablePipelineDiagrams') && isEmbedded()) {
navigate({ to: '/rp-connect/create' });
} else {
navigate({ to: '/rp-connect/wizard', search: { step: undefined, serverless: undefined } });
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't it be step 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, I guess it was implicit, as step 1 is default if no explicit step is provided.

<ChartContainer className="h-40 w-full" config={chartConfig}>
<AreaChart data={chartData}>
<defs>
<linearGradient id={`${id}-ingress`} x1="0" x2="0" y1="0" y2="1">
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can extract this to another component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How so? like a LinearGradient component? Or something else?

@eblairmckee
Copy link
Contributor Author

Commit e40e25c changes

Bug Fixes (all HIGH/MEDIUM issues from adversarial review addressed)

  • H1 — Lint hint merge: Error hints and real-time lint warnings now display simultaneously instead of one source overriding the other. Editing YAML clears stale save-error hints.
  • H2 — Focus loop guard: Command menu popover focus logic uses an abort ref to prevent rAF retries after unmount. Retry attempts reduced from 10 to a single deferred pass.
  • H3 — Store cleanup: Connector wizard wraps template generation in try/finally so Zustand stores always reset, even when YAML generation fails.
  • H4 — Command menu mutual exclusion: Typing / closes an open Cmd+Shift+P dialog; opening the dialog closes an active slash menu. Handled at event sources, no syncing effects.
  • M1 — Clarifying comment on tag merge behavior.
  • M2 — Fixed stale JSDoc ("after whitespace/colon" → "after whitespace").
  • M3 — Throughput card now shows a warning alert on query errors instead of silently falling through to the empty state.

Test Coverage

Before: 77 tests across 4 files. Zero coverage on index.tsx, pipeline-command-menu.tsx, connector-wizard.tsx, add-topic-step.tsx, add-user-step.tsx, and pipeline-throughput-card.tsx.

After: 145 tests across 10 files.

New molecule-level integration tests (bottom-up)

File Tests Covers
add-topic-step.test.tsx 10 Existing/new topic selection, form validation, createTopic RPC via ref API
add-user-step.test.tsx 12 SASL + Service Account auth paths, permission alerts, consumer groups
connector-wizard.test.tsx 12 Non-Redpanda YAML generation, Redpanda stepper flow (Next/Skip/Back/Done), store cleanup on error, serverless auto-open
pipeline-command-menu.test.tsx 14 Item selection, category filter, create-button visibility per tab, popover variant
pipeline-throughput-card.test.tsx 5 Error alert (M3 fix), loading skeleton, chart rendering, empty state
use-slash-command.test.tsx +3 onOpen callback fires on valid slash trigger (H4 fix validation)

Route-level tests (index.test.tsx — 18 tests)

Tests only what molecules can't cover: orchestration wiring, feature flag routing, mode routing, and cross-component state.

  • Creating a pipeline: save sends name + YAML to backend, successful save redirects, failed save shows errors alongside lint warnings, editing clears stale errors
  • Feature flags: enableConnectSlashMenu toggles slash tip banner and popover menu, enableNewPipelineLogs switches between LogExplorer and legacy LogsTab
  • Mode routing: edit mode pre-fills form + editor from server data, view mode navigates back to pipeline list
  • Serverless: Redpanda connector in wizard store auto-opens the setup wizard
  • Mutual exclusion: typing / dismisses an open command dialog (H4 fix)

@eblairmckee
Copy link
Contributor Author

eblairmckee commented Mar 25, 2026

Throughput card improvements:

Improved pipeline-throughput-card to be gated behind /observability page feature flags.

// Before
<PipelineThroughputCard pipelineId={pipeline.id} />

// After
{isEmbedded() &&
  (isServerless()
    ? isFeatureFlagEnabled('enableDataplaneObservabilityServerless')
    : isFeatureFlagEnabled('enableDataplaneObservability')) && (
  <PipelineThroughputCard pipelineId={pipeline.id} />
)}

This means:

  • Self-managed (!isEmbedded()): card hidden (no observability backend)
  • Serverless embedded: shown only when enableDataplaneObservabilityServerless flag is on
  • Dedicated/BYOC embedded: shown only when enableDataplaneObservability flag is on
  • listQueries stays as a defense-in-depth check inside the card for edge cases

why

  • dataplane_observability_enabled is a per-cluster feature flag (featureflag/enable-dataplane-observability), defaults false, uniform across cluster types
  • The observability page route (route-utils.tsx:209-215) gates with separate flags:
    isEmbedded() && (isServerless()
      ? isFeatureFlagEnabled('enableDataplaneObservabilityServerless')
      : isFeatureFlagEnabled('enableDataplaneObservability'))
    
  • The throughput card (pipeline/index.tsx:462) is rendered unconditionally — no feature flag or cluster-type check fixed
  • listQueries is the only runtime guard preventing broken metrics when observability isn't enabled fixed

@eblairmckee eblairmckee enabled auto-merge (squash) March 25, 2026 02:54
@eblairmckee eblairmckee merged commit 2023eae into master Mar 25, 2026
16 checks passed
@eblairmckee eblairmckee deleted the pr4/edit-create-mode-pipeline-page branch March 25, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants