Skip to content

refactor: unify the stepper machinery behind a shared provider and rebuild the deployment stepper on it - #14284

Open
tarciorodrigues wants to merge 7 commits into
release-1.12.0from
refactor/le-1736-pr-de
Open

refactor: unify the stepper machinery behind a shared provider and rebuild the deployment stepper on it#14284
tarciorodrigues wants to merge 7 commits into
release-1.12.0from
refactor/le-1736-pr-de

Conversation

@tarciorodrigues

@tarciorodrigues tarciorodrigues commented Jul 27, 2026

Copy link
Copy Markdown
Member

Why

The frontend carries four independent step/phase machines — the modals/stepperModal visual shell (knowledge-base upload wizard), the deploymentsPage stepper (deployment-stepper-modal.tsx + deployment-stepper-context.tsx + footer), the WizardStep state inside useKnowledgeBaseForm, and the use-deploy-choice-dialog-state phase machine — roughly 250-400 LOC of duplicated modal shell, next/back/finish footer and step-index/progress logic. Today modals/stepperModal only ships the visuals: its context merely echoes currentStep/totalSteps/title/description, and every consumer hand-rolls its own transition state. This PR promotes the module into the canonical stepper framework and rebuilds the deployment stepper's navigation on it — the framework lands together with its first production consumer, so the API is reviewed against real use, not in the abstract. The deploy-choice phase machine follows in the next PR of the sequence.

What

Additive framework layer inside modals/stepperModal:

  • hooks/useStepperState.ts — NEW: headless transition logic. 1-based currentStep with clamping, next()/back()/goTo(idOrNumber), per-step validation gate (canNext as boolean or lazily evaluated function), minStep back floor independent from initialStep, consumer-owned isBusy gating every transition, onStepChange(from, to) fired only on committed transitions, reset(), and progressPercent using the indicators' (current-1)/(total-1) formula (single-step guard included). Conditional steps are expressed as a computed steps list; if the list shrinks below the current position the exposed step clamps without losing the underlying one.
  • StepperProvider.tsx — NEW: context wrapper over the hook plus a useStepper() accessor; shell-less flows (the deploy-choice dialog, next PR) can call useStepperState directly.
  • types.tsStepperStepConfig, UseStepperStateOptions, StepperState, StepperProviderProps added.
  • StepperModal.tsx — re-exports the new API from the module entry (public API unchanged otherwise).

First consumer — the deployment stepper runs its navigation on the shared primitive:

  • deploymentsPage/contexts/deployment-stepper-context.tsx — the hand-rolled useState step machine, next/back clamps, per-mode totalSteps and the edit-mode getLogicalStep index shifting are replaced by useStepperState with a computed step list (edit mode simply omits the Provider step: [type, flows, review] vs [provider, type, flows, review]). The public useDeploymentStepper() API is unchanged — currentStep, totalSteps, minStep, canGoNext, handleNext, handleBack keep their exact semantics: canGoNext stays the per-step domain gate (now keyed by step id instead of shifted numbers) and handleNext stays ungated with the same clamping, matching the machine it replaces. Step panels, modal, footer and indicator are untouched.

The API shape was designed from the real consumers before any code:

Capability Source of truth API
Steps + 1-based index deployment stepper (3 edit / 4 create), KB wizard (2) steps config list, currentStep, currentStepId, totalSteps
Conditional steps edit mode drops the Provider step computed steps list + shrink clamping
Per-step validation gate canGoNext memo per logical step in the deployment context canNext per step config; gated next(); exposed canGoNext
Open at a later step / back floor deployment opens at step 2 on preselect, Back disabled at minStep initialStep + independent minStep; exposed canGoBack
Free jumps by name deploy-choice phases provider/deployments/review (next PR) step ids + goTo, initialStep with minStep: 1
Busy finish isDeploying/isSubmitting/isCreatingAccount disable navigation isBusy option gates all transitions (busy stays consumer-owned)
Fresh reopen remount-by-key / manual resets today reset()

Untouched on purpose (ticket rules): StepperModal shell, StepperModalFooter, KnowledgeBaseUploadModal and useKnowledgeBaseForm (its wizard state migrates in the Tier C decomposition — this PR only guarantees the primitive can host it).

How to validate

Two surfaces, one walkthrough each. The KB upload wizard has zero rewired code, so its check is the walkthrough staying IDENTICAL; the deployment stepper runs on the new primitive with unchanged semantics, so its walkthrough must also be indistinguishable from the base. The evidence screenshots are numbered 1:1 with these steps (compare step Sn with image Sn).

Knowledge page → Add Knowledge (needs at least one configured embedding provider):

  1. S1 Open the wizard → step 1 renders name/embedding/DB-provider/chunking fields, footer shows Next Step, no side panel; the embedding field comes pre-filled (the model input auto-selects the first available option when empty).
  2. S2 Add two .txt files → the Sources side panel slides in listing both files with the total size, dialog shifts left.
  3. S3 Click Next Step with the name still empty → the wizard stays on step 1 with the inline "Name is required" error; the embedding field cannot error while options exist (see S1 auto-select).
  4. S4 Fill the name and switch the embedding model through the dropdown → error clears, field shows the picked model.
  5. S5 Next Step → Review & Build: chunk preview renders from the first file, summary lists name/files/chunk size/overlap/separator/model/provider, footer swaps to Back + Create.
  6. S6 Back → step 1 keeps name, model and files intact.
  7. S7 Next Step → Create → success toast, wizard closes, the new KB row appears in the list.
  8. S8 Reopen the wizard → fresh state: empty name, re-auto-selected first model, no side panel.
  9. S9 Dark theme (set before first load): step 1 filled.
  10. S10 Dark theme: Review & Build step.

Deployments tab (Beta; behind the wxo_deployments feature flag) → Create Deployment, with one watsonx environment configured and at least one flow with a version:

  1. S11 Deployments tab shows the create-deployment empty state.
  2. S12 Open the wizard → step 1 (Provider): 4-step indicator, watsonx card, existing environment listed, Next DISABLED until one is picked.
  3. S13 Pick the environment → Next enables (per-step gate).
  4. S14 Next → step 2 (Type): Next disabled while empty; fill name and pick the LLM → Next enables.
  5. S15 Next → step 3 (Flows): select the flow, attach its version (the connection sub-panel is optional — Skip) → version shows ATTACHED, Next enables.
  6. S16 Next → step 4 (Review & Confirm): deployment summary + attached flow; primary action is Deploy.
  7. S17 Back → step 3 keeps the attachment intact (forward/back loses nothing).
  8. S18 Next → Deploy → success screen ("Deployment successful", indicator's last step reads Deployed) with Done.
  9. S19 Done, then reopen the wizard → fresh state on step 1, nothing kept, Next disabled again.
  10. S20 Dark theme (set before first load): Review step.

Edit mode (3 steps, Provider omitted) is covered by the 31-file deploymentsPage unit suite — totalSteps/step-shifting assertions run against the rebuilt context unchanged. Plus: devtools console shows zero new errors vs baseline throughout.

Evidence — KB upload wizard walkthrough (S1-S10, before | after)

Both worlds ran the same capture script (network-mocked KB list, two embedding models, deterministic chunk-preview payload), so the pairs are byte-comparable; identical pairs are the point — this consumer is untouched.

before (release-1.12.0) after (this PR)
01-kbwizard-S1-before 01-kbwizard-S1-after
S1 — wizard opens on step 1: same fields, Next Step footer, no side panel; embedding auto-filled with the first available model
02-kbwizard-S2-before 02-kbwizard-S2-after
S2 — two files added: Sources panel slides in listing product-faq.txt and onboarding-guide.txt (2 files, 401 B), dialog shifts left
03-kbwizard-S3-before 03-kbwizard-S3-after
S3 — Next Step with empty name: blocked on step 1, red border + "Name is required"; embedding keeps the auto-selected model so it cannot error here
04-kbwizard-S4-before 04-kbwizard-S4-after
S4 — name filled and embedding switched to text-embedding-3-large through the dropdown; error cleared
05-kbwizard-S5-before 05-kbwizard-S5-after
S5 — Review & Build: chunk preview from product-faq.txt, summary (name, 2 files, 1000/200, \n, model, Chroma Local), footer Back + Create
06-kbwizard-S6-before 06-kbwizard-S6-after
S6 — Back returns to step 1 with name, model and files panel preserved
07-kbwizard-S7-before 07-kbwizard-S7-after
S7 — Create: success toast, wizard closed, customer_support_docs appears in the list as Ingesting
08-kbwizard-S8-before 08-kbwizard-S8-after
S8 — reopening starts fresh: empty name, first model re-auto-selected, no side panel
09-kbwizard-S9-before 09-kbwizard-S9-after
S9 — dark theme set before first load: step 1 filled (name, model, files panel)
10-kbwizard-S10-before 10-kbwizard-S10-after
S10 — dark theme: Review & Build step

Evidence — deployment stepper walkthrough (S11-S20, before | after)

Both worlds ran the same capture script (real backend for flows/versions; deployments endpoints network-mocked — the OSS backend has no watsonx account), so the pairs are byte-comparable; the rebuilt navigation must be indistinguishable from the base.

before (release-1.12.0) after (this PR)
11-deploy-S11-before 11-deploy-S11-after
S11 — Deployments tab Beta with the create-deployment empty state
12-deploy-S12-before 12-deploy-S12-after
S12 — wizard opens on step 1 Provider: 4-step indicator, watsonx card, existing environment listed, Next disabled until one is picked
13-deploy-S13-before 13-deploy-S13-after
S13 — picking the environment enables Next (per-step gate on the shared primitive)
14-deploy-S14-before 14-deploy-S14-after
S14 — step 2 Type filled: agent type, name, LLM picked; Next enabled
15-deploy-S15-before 15-deploy-S15-after
S15 — step 3 Flows: Basic Prompting v1 ATTACHED created today, Next enabled
16-deploy-S16-before 16-deploy-S16-after
S16 — step 4 Review & Confirm: deployment summary + attached flow; Deploy is the primary action
17-deploy-S17-before 17-deploy-S17-after
S17 — Back from Review: step 3 keeps the attachment intact, forward/back loses nothing
18-deploy-S18-before 18-deploy-S18-after
S18 — Deploy: success screen, indicator's last step reads Deployed, Done closes
19-deploy-S19-before 19-deploy-S19-after
S19 — reopening after Done starts fresh on step 1: nothing kept, Next disabled again
20-deploy-S20-before 20-deploy-S20-after
S20 — dark theme set before first load: Review step

Tests

  • NEW stepperModal/hooks/__tests__/useStepperState.test.tsx — 26 tests covering the transition logic: initial clamping, next/back with boolean and lazily re-evaluated function gates, minStep floor vs free back after an auto-skip opening, goTo by id and clamped number, conditional list shrink/regrow, busy gating with reset still available, onStepChange only on committed transitions, progress formula with the single-step division-by-zero guard, provider/useStepper wiring.
  • All 31 deploymentsPage test suites — 547 tests — pass against the rebuilt context with ZERO assertion changes (create-mode, edit-mode, stepper modal/footer, step panels, payload builders, custom tool naming).
  • stepperModal + knowledgeBaseUploadModal suites green (module + consumer).
  • Full make test_frontend from the repo root: 5,528 tests, 100% green.
  • npx playwright test --list: clean collection.
  • npx biome check clean on the touched files; tsc --noEmit introduces zero new errors on touched files (repo-wide baseline diagnostics reproduce identically on the base — verified by running tsc on both trees and diffing the area output).
  • git diff --name-only origin/release-1.12.0 = exactly the 6 expected files (5 framework/test + 1 deployment context).

Note

Net diff is +565 (+230 production): the framework is additive and the deployment context sheds only its navigation internals here; WP2's net-negative lands as the remaining machines migrate (deploy-choice next). The S3 evidence shows only the name error because the model input auto-selects the first embedding option whenever the field is empty — pre-existing behavior this PR does not touch. The deployment capture mocks the deployments/* endpoints (no watsonx account in OSS backends) while flows/versions/snapshots are real.

Refs: LE-1736

Summary by CodeRabbit

  • New Features

    • Added reusable stepper navigation for multi-step workflows.
    • Supports next, back, direct step navigation, reset, progress tracking, validation gates, and busy-state handling.
    • Added provider and hook APIs for accessing stepper state.
    • Added type-safe configuration for steps and navigation state.
  • Improvements

    • Deployment setup now uses the shared stepper behavior, including streamlined edit-mode navigation and existing validation rules.

… stepperModal

Promote modals/stepperModal into the canonical stepper framework (LE-1736
WP2 PR-a). The module so far only shipped the visual shell: the existing
StepperContext merely echoes currentStep/totalSteps/title/description and
every consumer hand-rolls its own transition state. This adds the missing
generic layer, purely additive:

- hooks/useStepperState.ts: headless transition logic — 1-based step,
  next/back/goTo with clamping, per-step canNext validation gate,
  minStep back floor (independent from initialStep), consumer-owned
  isBusy gating, onStepChange callback, reset, progressPercent using
  the indicators' (current-1)/(total-1) formula. Conditional steps are
  a computed steps list; the exposed step clamps if the list shrinks.
- StepperProvider.tsx: context wrapper over the hook + useStepper();
  shell-less flows (deploy-choice) can call the hook directly.
- types.ts: StepperStepConfig / UseStepperStateOptions / StepperState /
  StepperProviderProps.
- StepperModal.tsx: re-export the new API from the module entry.

The API covers what the deployment stepper needs today (conditional step
lists per mode, busy finish, back-disabled states, opening at a later
step) and the deploy-choice phase machine (named steps, free goTo,
auto-skip via initialStep with minStep 1), per the capability map in the
ticket plan. StepperModal shell, StepperModalFooter and the knowledge
base upload modal are untouched (module + consumer suites 125/125).
Consumer migrations land in the next PRs of the sequence.

Refs: LE-1736 W15
Unit suite for the new headless stepper framework (26 tests): initial
state and clamping, next/back with per-step canNext gates (boolean and
lazily re-evaluated function), minStep back floor vs free back after an
auto-skip opening, goTo by id and by clamped number, conditional steps
as a computed list (shrink clamps the exposed step, growing back
restores it, navigation runs from the clamped step), busy gating of all
transitions with reset still available, onStepChange firing only on
committed transitions, progress formula including the single-step
division-by-zero guard, and StepperProvider/useStepper context wiring.

Refs: LE-1736 W16
…mitive

Rebuild the deployment stepper's navigation machine on top of the
stepperModal framework: the hand-rolled currentStep state, next/back
clamps, edit-mode logical-step shifting and per-mode totalSteps in
deployment-stepper-context are replaced by useStepperState with a
computed step list (edit mode simply omits the provider step, so the
getLogicalStep index shifting disappears). The public
useDeploymentStepper() API is unchanged — currentStep, totalSteps,
minStep, canGoNext, handleNext, handleBack keep their exact semantics:
canGoNext stays the per-step domain gate (now keyed by step id instead
of shifted numbers) and handleNext stays ungated with the same clamping
(delegated to goTo), matching the setState machine it replaces. Step
panels, modal, footer and indicator are untouched; all 31 deploymentsPage
test suites (547 tests) pass without a single assertion change.

Refs: LE-1736 W18
@github-actions github-actions Bot added the refactor Maintenance tasks and housekeeping label Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e426384f-3a5b-41ac-86a1-4cc17aff128b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

A reusable typed stepper state hook and context provider are added, exported, and covered by tests. Deployment navigation now uses the shared stepper state model with conditional edit-mode steps and existing validation gates.

Changes

Stepper state and deployment integration

Layer / File(s) Summary
Stepper contracts and state engine
src/frontend/src/modals/stepperModal/types.ts, src/frontend/src/modals/stepperModal/hooks/useStepperState.ts
Defines step configuration and state APIs, then implements clamping, navigation, validation gates, busy handling, reset, callbacks, and progress calculation.
Stepper context API and validation
src/frontend/src/modals/stepperModal/StepperProvider.tsx, src/frontend/src/modals/stepperModal/StepperModal.tsx, src/frontend/src/modals/stepperModal/hooks/__tests__/useStepperState.test.tsx
Adds provider and consumer hooks, re-exports the public API, and tests navigation, dynamic steps, busy state, callbacks, progress, reset, and context errors.
Deployment stepper migration
src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx
Builds conditional deployment steps and delegates step indexing and movement to useStepperState while retaining deployment-specific gates.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Quality And Coverage ⚠️ Warning Good breadth overall, but the new hook tests miss known edge cases: batched transitions and invalid numeric inputs (NaN/fractional/minStep 0), so coverage isn’t comprehensive. Add regressions for next();next() batching and for initialStep/minStep/goTo with 0, fractional, and NaN values; keep the deployment-stepper context tests against the shared primitive.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor: adding a shared stepper provider and migrating the deployment stepper to it.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed Yes—useStepperState.test.tsx covers the new hook/provider in depth, and existing deployment page suites assert the migrated stepper behavior with real cases.
Test File Naming And Structure ✅ Passed The new frontend test is correctly named, placed under tests, and uses clear describe/it grouping with positive, negative, and edge cases.
Excessive Mock Usage Warning ✅ Passed Tests exercise real hook/provider behavior; only two jest.fn callbacks and one console.error spy are used, not excessive core-logic mocking.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/le-1736-pr-de

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added refactor Maintenance tasks and housekeeping and removed refactor Maintenance tasks and housekeeping labels Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/src/modals/stepperModal/hooks/useStepperState.ts`:
- Around line 42-46: Update commitStep in useStepperState so onStepChange is
invoked only after the corresponding state update is committed, preventing
batched next() calls from emitting duplicate transitions; preserve the no-op
behavior when from equals to, and add a regression test covering batched
transitions.
- Around line 8-10: Update clampStep to normalize minStep to an integer within
[1, totalSteps] before clamping, while retaining the zero-total-steps result of
0. Ensure back() cannot commit step 0, and validate or normalize fractional,
NaN, and other invalid numeric inputs from goTo and initial state so currentStep
is always a valid 1-based index.

In `@src/frontend/src/modals/stepperModal/StepperProvider.tsx`:
- Around line 13-23: Update StepperProvider and useStepperState so navigation
state is held in a provider-scoped Zustand store rather than local hook-managed
mutable state. Create or select the store per provider instance using the
existing key/instance mechanism, keep separate StepperProvider instances
isolated, and expose the selected store state through StepperStateContext
without changing the provider’s public behavior.

In
`@src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx`:
- Around line 251-306: Guard handleNext with canGoNext before advancing the
stepper, so invalid state cannot move to the next step through the public
context. Preserve the existing stepper.goTo increment for valid transitions and
keep the current canGoNext predicates unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: edfa0cc3-2cae-4024-b33e-610285298599

📥 Commits

Reviewing files that changed from the base of the PR and between d021b3e and 3f700aa.

📒 Files selected for processing (6)
  • src/frontend/src/modals/stepperModal/StepperModal.tsx
  • src/frontend/src/modals/stepperModal/StepperProvider.tsx
  • src/frontend/src/modals/stepperModal/hooks/__tests__/useStepperState.test.tsx
  • src/frontend/src/modals/stepperModal/hooks/useStepperState.ts
  • src/frontend/src/modals/stepperModal/types.ts
  • src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx

Comment on lines +8 to +10
function clampStep(step: number, minStep: number, totalSteps: number): number {
if (totalSteps === 0) return 0;
return Math.min(Math.max(step, minStep), totalSteps);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize 1-based numeric inputs before clamping.

minStep: 0 lets back() commit step 0; fractional or NaN values can produce a non-index currentStep. Normalize minStep to an integer in [1, totalSteps] and reject or normalize invalid numeric goTo/initial values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/modals/stepperModal/hooks/useStepperState.ts` around lines 8
- 10, Update clampStep to normalize minStep to an integer within [1, totalSteps]
before clamping, while retaining the zero-total-steps result of 0. Ensure back()
cannot commit step 0, and validate or normalize fractional, NaN, and other
invalid numeric inputs from goTo and initial state so currentStep is always a
valid 1-based index.

Comment on lines +42 to +46
const commitStep = useCallback(
(from: number, to: number) => {
if (to === from) return;
setRawStep(to);
onStepChange?.(from, to);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fire onStepChange only for committed transitions.

Batched calls such as next(); next() synchronously emit (1, 2) twice, although state commits only once. Notify after the committed state change and add a regression test for batched transitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/modals/stepperModal/hooks/useStepperState.ts` around lines
42 - 46, Update commitStep in useStepperState so onStepChange is invoked only
after the corresponding state update is committed, preventing batched next()
calls from emitting duplicate transitions; preserve the no-op behavior when from
equals to, and add a regression test covering batched transitions.

Comment on lines +13 to +23
export function StepperProvider({
children,
...options
}: StepperProviderProps) {
const state = useStepperState(options);
return (
<StepperStateContext.Provider value={state}>
{children}
</StepperStateContext.Provider>
);
}

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
rg -n -C 3 --glob '*.{ts,tsx}' 'zustand|createStore\(' src/frontend/src

Repository: langflow-ai/langflow

Length of output: 35983


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate files =="
fd -a 'StepperProvider.tsx|Stepper.*\.ts|Stepper.*\.tsx|stepper.*Store' src/frontend/src | sed 's#^\./##'

echo
echo "== StepperProvider =="
file="$(fd 'StepperProvider.tsx' src/frontend/src | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  cat -n "$file"
fi

echo
echo "== stepper-related files =="
rg -n -C 4 --glob '*.ts' --glob '*.tsx' 'StepperProvider|useStepperState|StepperStateContext|useStepper' src/frontend/src || true

Repository: langflow-ai/langflow

Length of output: 50376


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
root = Path('src/frontend/src/modals/stepperModal')
candidates = list(root.glob('*'))
for p in candidates:
    if p.is_file():
        print(p.relative_to('src/frontend/src/modals/stepperModal'))
PY

Repository: langflow-ai/langflow

Length of output: 217


Use Zustand for the stepper’s provider-scoped state.

StepperProvider manages navigation state inside useStepperState, so each provider instance owns its own mutable state outside the frontend’s Zustand-store convention. Move this to a provider-scoped Zustand store while keeping instances isolated via key/instance store selection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/modals/stepperModal/StepperProvider.tsx` around lines 13 -
23, Update StepperProvider and useStepperState so navigation state is held in a
provider-scoped Zustand store rather than local hook-managed mutable state.
Create or select the store per provider instance using the existing key/instance
mechanism, keep separate StepperProvider instances isolated, and expose the
selected store state through StepperStateContext without changing the provider’s
public behavior.

Source: Coding guidelines

Comment on lines +251 to +306
// Navigation runs on the shared stepper primitive. Conditional steps are
// the computed-list model: edit mode omits the Provider step entirely
// (3 steps: Type → Attach → Review), so no index shifting is needed.
const steps = useMemo<StepperStepConfig[]>(
() =>
isEditMode
? [{ id: "type" }, { id: "flows" }, { id: "review" }]
: [
{ id: "provider" },
{ id: "type" },
{ id: "flows" },
{ id: "review" },
],
[isEditMode],
);

const stepper = useStepperState({ steps, initialStep: minStep, minStep });
const { currentStep, totalSteps } = stepper;

const canGoNext = useMemo(() => {
const logical = getLogicalStep(currentStep);
if (logical === 1) {
return (
selectedProvider !== null &&
(selectedInstance !== null || hasValidCredentials)
);
}
if (logical === 2) {
return isDeploymentNameValid && selectedLlm.trim() !== "";
}
if (logical === 3) {
// In edit mode, user can proceed without new attachments (may just change desc/LLM).
return isEditMode || selectedVersionByFlow.size > 0;
}
if (logical === 4) {
return !hasToolNameErrors;
switch (stepper.currentStepId) {
case "provider":
return (
selectedProvider !== null &&
(selectedInstance !== null || hasValidCredentials)
);
case "type":
return isDeploymentNameValid && selectedLlm.trim() !== "";
case "flows":
// In edit mode, user can proceed without new attachments (may just change desc/LLM).
return isEditMode || selectedVersionByFlow.size > 0;
case "review":
return !hasToolNameErrors;
default:
return true;
}
return true;
}, [
currentStep,
getLogicalStep,
stepper.currentStepId,
selectedProvider,
selectedInstance,
hasValidCredentials,
deploymentName,
isDeploymentNameValid,
selectedLlm,
selectedVersionByFlow,
isEditMode,
hasToolNameErrors,
]);

// Ungated like the setState machine it replaces — the per-step gate is
// canGoNext, consumed by the footer's disabled state; goTo clamps to
// [minStep, totalSteps].
const handleNext = useCallback(() => {
setCurrentStep((prev) => (prev < totalSteps ? prev + 1 : prev));
}, [totalSteps]);
stepper.goTo(stepper.currentStep + 1);
}, [stepper]);

const handleBack = useCallback(() => {
setCurrentStep((prev) => (prev > minStep ? prev - 1 : prev));
}, [minStep]);
const handleBack = stepper.back;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate relevant files =="
fd -a 'deployment-stepper-context\.tsx|useStepperState\.ts|stepper' . | sed 's#^\./##' | head -100

echo
echo "== Main file outline/imports/context =="
file="src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx"
if [ -f "$file" ]; then
  wc -l "$file"
  ast-grep outline "$file" --view compact || true
  sed -n '1,360p' "$file" | cat -n
fi

echo
echo "== Stepper hook candidates =="
for f in $(fd 'useStepperState\.ts|stepper.*\.(ts|tsx)$' src/frontend || true); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" --view compact || true
  sed -n '1,260p' "$f" | cat -n
done

Repository: langflow-ai/langflow

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deployment stepper main component outline =="
ast-grep outline "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx" --view signatures || true
echo
echo "== deployment-stepper.tsx relevant keyboard/modal call sites =="
rg -n "handleNext|handleKeyDown|onPressEnter|Enter|keyDown|onKeyDown|canGoNext|onPrimaryAction|handlePrimary" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx" -C 4
sed -n '1,260p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx" | cat -n

echo
echo "== all handleNext call sites in deployments page =="
rg -n "handleNext\(\)|handleNext\s*=" "src/frontend/src/pages/MainPage/pages/deploymentsPage" -C 3

echo
echo "== all KeyboardEvent call sites in stepper modal components =="
rg -n "KeyboardEvent|onKeyDown|onKeyPress|handleKeyDown|enter" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/*.tsx" -C 3

echo
echo "== payload builder concern excerpt =="
sed -n '360,460p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx" | cat -n

Repository: langflow-ai/langflow

Length of output: 640


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deployment-stepper.tsx all relevant references =="
rg -n "handleNext|handleBack|onPrimary|onBack|useDeploymentStepper|handleKeyDown|onKeyDown|onKeyPress|keyboard|Enter|key" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx" -C 4 || true
echo
echo "== deployment-stepper.tsx first 220 lines =="
sed -n '1,220p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx" | cat -n

echo
echo "== deployments page handleNext call sites with imports =="
rg -n "handleNext|useDeploymentStepper|canGoNext" "src/frontend/src/pages/MainPage/pages/deploymentsPage" -C 3

echo
echo "== all KeyboardEvent call sites in deployments page components =="
rg -n "KeyboardEvent|onKeyDown|onKeyPress|handleKeyDown|Enter|key" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/*.tsx" -C 3

Repository: langflow-ai/langflow

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DeploymentStepperModal lines 240-310 (handleNext call) =="
sed -n '240,310p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx" | cat -n

echo
echo "== deployment-stepper-footer props in DeploymentStepperModal =="
sed -n '380,410p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx" | cat -n

echo
echo "== deployment-steps footer props =="
sed -n '1,240p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-steps.tsx" | cat -n
rg -n "handleNext|handleBack|onPrimary|onBack|canGoNext" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-steps.tsx" -C 2 || true

echo
echo "== all programmatic handleNext usage =="
rg -n "handleNext\s*\(" "src/frontend/src/pages/MainPage/pages/deploymentsPage" -C 3

Repository: langflow-ai/langflow

Length of output: 4567


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deployment-stepper-modal full keyboard/modal dialog setup =="
rg -n "onOpenChange|onClose|onCloseAutoFocus|onEscapeKeyDown|onSelect|onKeyDown|onKeyPress|onEnter|Enter|handleOpen|setOpen|Modal|StepperContent" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx" -C 5 || true

echo
echo "== deployment-stepper-modal all handleNext references =="
rg -n "handleNext\(\)|handleStepNext|keyboard|on.*key" "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx" -C 3 || true

echo
echo "== focused deployment-stepper-modal relevant section =="
sed -n '1,120p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx" | cat -n
sed -n '330,430p' "src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx" | cat -n

echo
echo "== tests covering handleNext bypass behavior =="
rg -n "handleNext\([^)]*not|!canGoNext|blocks when .*,|blocks when|handleNext" "src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__" -C 3 || true

Repository: langflow-ai/langflow

Length of output: 35152


Gate handleNext before using the stepper primitive.

handleNext currently calls stepper.goTo(stepper.currentStep + 1) without canGoNext, while canGoNext is only used to disable the footer button. Since useStepperState.goTo() ignores per-step canNext predicates, exposing handleNext on the public context lets invalid state advance steps and can feed incomplete data into buildDeploymentPayload/buildDeploymentUpdatePayload. Guard it with the local gate, or rely on the shared stepper next()/step canNext configuration instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx`
around lines 251 - 306, Guard handleNext with canGoNext before advancing the
stepper, so invalid state cannot move to the next step through the public
context. Preserve the existing stepper.goTo increment for valid transitions and
keep the current canGoNext predicates unchanged.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 48%
48.36% (69334/143364) 69.93% (9709/13883) 46.36% (1605/3462)

Unit Test Results

Tests Skipped Failures Errors Time
5471 0 💤 0 ❌ 0 🔥 19m 16s ⏱️

@HzaRashid
HzaRashid requested a review from viktoravelino July 28, 2026 03:33

@Cristhianzl Cristhianzl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚠️ Important (preferably this PR)

I1 — The framework's headline capability is not used by its "first production consumer"

File: src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx:254-306

Issue: The capability table in the description maps "Per-step validation gate" to canNext per step config. The adopted code declares its steps without any canNext ([{ id: "type" }, { id: "flows" }, { id: "review" }]), computes canGoNext itself in a separate useMemo, and routes handleNext through the ungated goTo rather than next() — deliberately, and honestly documented in the comment at line 299-301. So stepper.canGoNext is unconditionally true and stepper.next() is never called.

The same holds for the rest of the API: isBusy is never passed (the deployment flow's isDeploying / isSubmitting / isCreatingAccount stay outside the primitive), onStepChange is never passed, reset() is never called, progressPercent is never read, and StepperProvider / useStepper have no production call site — the deployment context keeps its own React context.

Why it matters: Roughly half of a 119-line hook plus a 31-line provider plus 63 lines of types are speculative surface (YAGNI), and the API-validated-against-real-use argument is what justifies landing them now. The risk is concrete: the deploy-choice dialog is the consumer these features were designed for, and its needs are being inferred rather than observed. If canNext turns out to want async validation, or isBusy needs to gate reset too, the API changes with a released consumer already on it.

Suggested fix: Pick one. (a) Wire the real gate through the primitive in this PR — move the four canGoNext clauses into canNext on each step config and have handleNext call stepper.next(). That preserves observable behavior (the footer already disables on canGoNext) and makes the flagship feature load-bearing. Or (b) ship only steps/goTo/back/currentStepId now and land canNext, isBusy, onStepChange, reset, progressPercent and StepperProvider with the deploy-choice PR that actually needs them. Option (a) is the smaller change and the stronger validation.

Code reference
const stepper = useStepperState({ steps, initialStep: minStep, minStep });
// …
const handleNext = useCallback(() => {
  stepper.goTo(stepper.currentStep + 1);   // never stepper.next()
}, [stepper]);

I2 — handleNext now churns identity on every step change, through the context value

File: src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx:302-304

Issue: useCallback(…, [stepper]) where stepper is the memoized state object whose deps include currentStep, canGoNext, canGoBack and progressPercent. Every transition — and every re-render that flips canGoNext — produces a new stepper, therefore a new handleNext, which goes into the context value at lines 422 and 468.

Before this PR, handleNext depended on [totalSteps] and was stable for the entire wizard.

Why it matters: Every component reading useDeploymentStepper() re-renders on each transition even if it only consumes handleNext — that includes the footer and the step panels. Not a correctness bug, but it's a regression introduced by the refactor in a context that already fans out widely, and it quietly undercuts the memoization the rest of the file maintains.

Suggested fix: Depend on the stable pieces rather than the container: useCallback(() => stepper.goTo(stepper.currentStep + 1), [stepper.goTo, stepper.currentStep]) still churns on step change. The clean fix pairs with I1(a) — const handleNext = stepper.next is stable across renders where canGoNext and currentStep hold, exactly like handleBack = stepper.back on line 306.


💡 Recommended (can ship as a follow-up)

R1 — A stale rawStep survives a shrinking step list and resurfaces if the list regrows

File: src/frontend/src/modals/stepperModal/hooks/useStepperState.ts:36-49 · locked in by hooks/__tests__/useStepperState.test.tsx:183-198

Issue: currentStep is clampStep(rawStep, …) but commitStep early-returns when to === from computed from the clamped value. So with rawStep = 4 and a list that shrank to 2 entries, goTo(2) is a no-op — rawStep stays 4 — and the moment the list regrows to 4 the stepper jumps straight back to step 4. The test at line 183 asserts this as intended ("restores when it grows back").

Why it matters: It's a defensible design for an auto-skip that toggles back, and it's unreachable in the deployment stepper (isEditMode is fixed for the life of the provider). But it's a genuine footgun for the deploy-choice dialog in the next PR, where the phase list is conditional on user selections — a user who backs out of a branch can find themselves teleported forward when the branch re-enables. The behavior is documented in the hook's docblock but not on StepperState.currentStep, where a consumer would look.

Suggested fix: Document the "underlying position is preserved" contract on the currentStep field in types.ts alongside the docblock, and expose rawStep (or a clearPendingStep()) so a consumer that wants shrink-to-be-permanent can opt out. At minimum, make goTo write rawStep even when the clamped value is unchanged.


R2 — canNext is invoked during render with no purity contract

File: src/frontend/src/modals/stepperModal/hooks/useStepperState.ts:13-16, 51-52

resolveCanNext(currentStepConfig) runs on every render as part of the canGoNext expression, calling the consumer's function form. The test at useStepperState.test.tsx:151-164 relies on exactly that (re-evaluated per render). Nothing tells a consumer the function must be pure and side-effect-free — a canNext that touches a store, logs, or triggers validation will fire on every render including ones caused by unrelated state.

Suggested fix: Add the constraint to the StepperStepConfig.canNext JSDoc: "Evaluated on every render — must be pure."


R3 — goTo(0) is silently treated as "step id not found"

File: src/frontend/src/modals/stepperModal/hooks/useStepperState.ts:66-73

findIndex(...) + 1 yields 0 for an unknown id, and the guard if (target < 1) return; handles it. But that same guard swallows a numeric goTo(0) — a plausible off-by-one from a consumer used to 0-based indexing — which should clamp to minStep under the documented "clamped to [minStep, total]" contract. Two different intents share one sentinel.

Suggested fix: Resolve the id branch to -1 explicitly and guard if (target === -1) return;, letting numeric inputs fall through to clampStep. One test each for goTo(0) and goTo(-1).


R4 — deployment-stepper-context.tsx crosses the 500-LOC limit

File: src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx (513 LOC)

The file was already near the ceiling and this PR pushes it to 513, over the 500-LOC baseline in .claude/CLAUDE.md (500–700 acceptable only when single-responsibility clearly holds — this file currently owns navigation, provider/credentials state, flow-attachment state, name validation and payload building). Not introduced by this PR, but it grew here. Now that navigation is a one-liner on the shared primitive, splitting the domain state out is a much smaller job than it was. Worth a follow-up ticket alongside the WP4 extractions.


🔹 Nice-to-have

N1 — The module has no index.ts, so consumers import from a file that looks internal

File: src/frontend/src/pages/MainPage/pages/deploymentsPage/contexts/deployment-stepper-context.tsx:14-17

import { type StepperStepConfig, useStepperState } from "@/modals/stepperModal/StepperModal" reaches into the component file for a hook and a type. StepperModal.tsx re-exports the public API deliberately (lines 125-140), so it works — but the folder has no index.ts, and importing a headless hook from a file named after a modal reads as an accident. Adding modals/stepperModal/index.ts that re-exports the same set would make the public surface explicit and let consumers write @/modals/stepperModal.

@github-actions github-actions Bot added the lgtm This PR has been approved by a maintainer label Jul 28, 2026
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.57252% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.12%. Comparing base (d021b3e) to head (e02ea71).
⚠️ Report is 32 commits behind head on release-1.12.0.

Files with missing lines Patch % Lines
src/frontend/src/modals/stepperModal/types.ts 0.00% 64 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.12.0   #14284      +/-   ##
==================================================
+ Coverage           61.52%   62.12%   +0.59%     
==================================================
  Files                2349     2347       -2     
  Lines              238463   238193     -270     
  Branches            33541    35523    +1982     
==================================================
+ Hits               146719   147969    +1250     
+ Misses              89929    88409    -1520     
  Partials             1815     1815              
Flag Coverage Δ
frontend 60.72% <75.57%> (+0.92%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../frontend/src/modals/stepperModal/StepperModal.tsx 96.77% <100.00%> (+0.12%) ⬆️
...ontend/src/modals/stepperModal/StepperProvider.tsx 85.41% <100.00%> (ø)
...d/src/modals/stepperModal/hooks/useStepperState.ts 100.00% <100.00%> (ø)
src/frontend/src/modals/stepperModal/index.ts 100.00% <100.00%> (ø)
...ymentsPage/contexts/deployment-stepper-context.tsx 97.40% <100.00%> (+0.58%) ⬆️
src/frontend/src/modals/stepperModal/types.ts 0.00% <0.00%> (ø)

... and 196 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ep gate

Wires the deployment stepper's advance gates through the shared primitive
(review I1/I2): each advancing step carries its own canNext, so canGoNext and
handleNext come from useStepperState instead of a parallel canGoNext useMemo and
an ungated goTo. handleNext is now stepper.next — stable except when
canGoNext/currentStep change, replacing the whole-stepper dependency that churned
on every transition.

The Review step keeps no canNext: it is the last step and its primary action is
Deploy, so its button gate stays hasToolNameErrors, applied via isLastStep. The
primitive's canGoNext is false on the last step by design, so folding the review
clause into canNext would have disabled Deploy.

One navigation test now seeds each step to reach step 4, since handleNext is
gated; the clamp assertion itself is unchanged.

Refs: LE-1736
The function form is evaluated on every render as part of canGoNext, so a
canNext that touches a store, logs or triggers validation would fire on
unrelated re-renders. Document the purity constraint (review R2).

Refs: LE-1736
Adds modals/stepperModal/index.ts re-exporting the public surface (the hooks,
provider and types StepperModal.tsx already re-exports, plus the default) so the
headless deployment consumer imports from @/modals/stepperModal instead of
reaching into the modal component file. Review N1.

Refs: LE-1736
@github-actions github-actions Bot added refactor Maintenance tasks and housekeeping and removed refactor Maintenance tasks and housekeeping labels Jul 30, 2026
@github-actions github-actions Bot added refactor Maintenance tasks and housekeeping and removed refactor Maintenance tasks and housekeeping labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer refactor Maintenance tasks and housekeeping

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants