Skip to content

refactor(sdl): placement-first form values schema#3263

Open
ygrishajev wants to merge 1 commit into
mainfrom
refactor/sdl-placement-first-form-values
Open

refactor(sdl): placement-first form values schema#3263
ygrishajev wants to merge 1 commit into
mainfrom
refactor/sdl-placement-first-form-values

Conversation

@ygrishajev
Copy link
Copy Markdown
Contributor

@ygrishajev ygrishajev commented Jun 3, 2026

Why

Closes CON-413. Foundational data-layer change for the redesigned configure-deployment screen (CON-411): every subsequent pane (CON-414..427) reads or writes one shared deployment model, and that model now treats placements as first-class entities. With the legacy embedded `service.placement` shape, empty placements have no host, renames cascade through every referencing service, and region is a global `generateSdl` arg with no per-placement granularity — none of which work for a UI that lets users create multiple placements and pick a provider per placement.

The new screen also needs a single source of truth that SDL generation and validation can reuse without re-authoring rules. Evolving the existing schema in place (rather than branching a parallel model) keeps the legacy wizard and the new redesign on one shape, and the legacy form continues to render identically against it.

What

  • `SdlBuilderFormValuesSchema` reshape (`apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts`): `placements` becomes a top-level array (each with a stable `id` and an optional `region`); services drop their embedded `placement` and gain `placementId` + a top-level `pricing` field. A top-level `superRefine` guards against orphan `placementId` references.
  • `generateSdl` now takes the full `SdlBuilderFormValuesType`, resolves each service's placement via id, and writes `location-region` as a per-placement attribute. The `(services, region?)` signature is gone.
  • `importSimpleSdl` now returns `SdlBuilderFormValuesType`, deduplicates placements by SDL name, lifts `attributes["location-region"]` onto `placement.region`, and lifts per-service pricing onto `service.pricing`. SDL YAML wire format is unchanged.
  • `defaultServiceWithPlacement` helper (`utils/sdl/data.ts`) collapses the typical "init the form" pattern that consumers (`SdlBuilder`, `RemoteDeployUpdate`, `SimpleSdlBuilderForm`) all repeated.
  • Legacy form controls (`PlacementFormModal`, `SignedByFormControl`, `AttributesFormControl`, `SimpleServiceFormControl`, `LogCollectorControl`, `ImportSdlModal`, `SimpleSdlBuilderForm`) rebind from `services.${i}.placement.X` paths to `placements.${p}.X` (shared) or `services.${i}.pricing.X` (per-service). A new `usePlacementIndexForService` hook in `PlacementFormModal` resolves service-index → placement-index.
  • `SdlBuilder.tsx` gains try/catch around `generateSdl` in the watch subscription (surfaces errors via `setError` instead of silently killing the RHF subscription), and `getSdl` handles non-`TransformError` throws.
  • Roundtrip test: imports the canonical `two-services-sdl.yml` fixture, regenerates, parses both via `yaml.load`, asserts deep equality. Guards against silent drift between generator and importer.

Bonus pre-existing bug fixes surfaced by the refactor

  • `signedBy.allOf`-only placements were silently dropped from SDL output — the legacy guard tested `anyOf?.length > 0 || anyOf?.length > 0` (both halves identical typo) instead of `anyOf || allOf`. Now both branches are emitted independently.
  • A second IP-using service overwrote the first's `endpoints` block due to non-idempotent endpoints initialization. Now initialized lazily (`||= {}`).

Out of scope

  • IP endpoints stay as `expose[].ipName: string`. CON-416 decides if they get a first-class array.
  • Multi-group services. The "only one placement available" assumption from the legacy importer is preserved.
  • The new redesign panes under `components/deployments/ConfigureDeployment/` — they don't read this schema yet (CON-415+).
  • `apps/api`, `apps/indexer`, etc. — SDL wire format is unchanged.

Test plan

  • `npm run lint -- --quiet`, `npx tsc --noEmit`, `npm test` pass under `apps/deploy-web`
  • Legacy SDL builder at `/new-deployment` renders identically — template selection, manifest edit step, create-lease step
  • Legacy `/sdl-builder` page renders and Save Template / Preview / Reset all work
  • Remote-deploy update flow (`RemoteDeployUpdate`) renders existing deployments and edits stay coherent
  • Importing a representative existing SDL (single placement, multiple services, GPU, signedBy with allOf-only, location-region attribute) roundtrips cleanly
  • Log-collector sync still ties the log-collector service to the target service's placement and pricing

Summary by CodeRabbit

  • Refactor

    • Reorganized deployment form structure to separate placement configuration from service pricing, improving form clarity and validation.
  • Bug Fixes

    • Enhanced error handling during SDL import and generation with clearer user-facing error messages when operations fail.
  • Tests

    • Updated test suite to reflect internal form structure changes and added validation tests for placement references.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: db6430c0-1f62-4e87-b759-bb331496825d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b98baf and f1f7d1d.

📒 Files selected for processing (24)
  • apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx
  • apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx
  • apps/deploy-web/src/components/sdl/AttributesFormControl.tsx
  • apps/deploy-web/src/components/sdl/ImportSdlModal.tsx
  • apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.spec.tsx
  • apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx
  • apps/deploy-web/src/components/sdl/PlacementFormModal.tsx
  • apps/deploy-web/src/components/sdl/SignedByFormControl.tsx
  • apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx
  • apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx
  • apps/deploy-web/src/hooks/useImportSimpleSdl.ts
  • apps/deploy-web/src/hooks/useSdlEnv/useSdlEnv.spec.tsx
  • apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.spec.tsx
  • apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.ts
  • apps/deploy-web/src/types/sdlBuilder/sdlBuilder.spec.ts
  • apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts
  • apps/deploy-web/src/utils/sdl/data.spec.ts
  • apps/deploy-web/src/utils/sdl/data.ts
  • apps/deploy-web/src/utils/sdl/sdlGenerator.spec.ts
  • apps/deploy-web/src/utils/sdl/sdlGenerator.ts
  • apps/deploy-web/src/utils/sdl/sdlImport.spec.ts
  • apps/deploy-web/src/utils/sdl/sdlImport.ts
  • apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts
  • apps/deploy-web/tests/seeders/sdlService.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.spec.tsx
  • apps/deploy-web/src/types/sdlBuilder/sdlBuilder.spec.ts
  • apps/deploy-web/src/components/sdl/AttributesFormControl.tsx
  • apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.spec.tsx
  • apps/deploy-web/src/hooks/useImportSimpleSdl.ts
  • apps/deploy-web/src/hooks/useSdlEnv/useSdlEnv.spec.tsx
  • apps/deploy-web/src/components/sdl/ImportSdlModal.tsx
  • apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx
  • apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx
  • apps/deploy-web/src/components/sdl/SignedByFormControl.tsx
  • apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx
  • apps/deploy-web/src/components/sdl/PlacementFormModal.tsx
  • apps/deploy-web/tests/seeders/sdlService.ts
  • apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.ts
  • apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx
  • apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts
  • apps/deploy-web/src/utils/sdl/sdlImport.ts
  • apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx
  • apps/deploy-web/src/utils/sdl/data.ts
  • apps/deploy-web/src/utils/sdl/sdlGenerator.spec.ts
  • apps/deploy-web/src/utils/sdl/sdlImport.spec.ts
  • apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts

📝 Walkthrough

Walkthrough

This PR refactors the SDL builder data model to separate placements from services: placements move to a top-level form array, services hold placementId references and their own pricing objects, and all import/generation/binding logic is updated accordingly. Schema validation ensures placement IDs are unique and referenced correctly.

Changes

SDL builder placement refactor

Layer / File(s) Summary
Schema and type definitions
apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts, sdlBuilder.spec.ts
PlacementSchema removes pricing; ServiceSchema adds required placementId and pricing; SdlBuilderFormValuesSchema requires both placements and services with referential validation.
Default data factories
apps/deploy-web/src/utils/sdl/data.ts, data.spec.ts
New defaultPlacement(), defaultService(placementId), and defaultServiceWithPlacement() factories replace cloneDeep-based defaults; sshServiceOverrides constant replaces runtime SSH mutation.
SDL import and placement hydration
apps/deploy-web/src/utils/sdl/sdlImport.ts, sdlImport.spec.ts
importSimpleSdl returns {placements, services} and deduplicates placements; new hydratePlacement helper lifts location-region to region and maps attributes/signedBy; imports bind each service to a placement via placementId.
SDL generation from form values
apps/deploy-web/src/utils/sdl/sdlGenerator.ts, sdlGenerator.spec.ts
generateSdl accepts full form values, validates placement references, and builds SDL profiles from resolved placement objects instead of inline service placement data; deployment counts keyed by service title and placement name.
Custom SDL field transformation
apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts
transformCustomSdlFields signature changed to operate on full form values and return transformed form values instead of service array only.
Component form initialization and SDL binding
apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx, remote-deploy/update/RemoteDeployUpdate.tsx, sdl/SimpleSdlBuilderForm.tsx, sdl/ImportSdlModal.tsx
Components initialize with defaultServiceWithPlacement, synchronize both placements and services to form/atom, generate SDL from full form values, and import SDL into both placements and services.
Placement form modal and index resolution
apps/deploy-web/src/components/sdl/PlacementFormModal.tsx
New usePlacementIndexForService hook resolves placement index from service placementId; pricing amount moves to services.${index}.pricing.amount; controls receive placementIndex instead of serviceIndex.
Attribute and SignedBy form controls
apps/deploy-web/src/components/sdl/AttributesFormControl.tsx, SignedByFormControl.tsx
Both controls retargeted to placements.${placementIndex}.* paths instead of services.${serviceIndex}.placement.* paths.
Service summary and placement display
apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx
Watches placements, derives current placement by matching ID to placementId, and renders placement details and service-level pricing from resolved placement and service pricing.
Log collector service synchronization
apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx, LogCollectorControl.spec.tsx
Syncs placementId and pricing fields separately instead of nested placement; generateLogCollectorService updated to use new shape.
Hook updates for placements and service defaults
apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.ts, useImportSimpleSdl.ts, test specs`
Service manager now watches placements and ensures at least one exists before appending services; import hook returns services only; test specs add placement fixtures.
Test data seeder update
apps/deploy-web/tests/seeders/sdlService.ts
buildSDLService returns placementId and top-level pricing instead of nested placement object.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Suggested labels

size: XL


Suggested reviewers

  • baktun14
  • stalniy
✨ 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/sdl-placement-first-form-values

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

❌ Patch coverage is 55.92105% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.70%. Comparing base (12d939f) to head (f1f7d1d).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...oy-web/src/components/sdl/SimpleSdlBuilderForm.tsx 0.00% 13 Missing and 1 partial ⚠️
...eb/src/components/sdl/SimpleServiceFormControl.tsx 0.00% 8 Missing and 2 partials ⚠️
...onents/remote-deploy/update/RemoteDeployUpdate.tsx 0.00% 6 Missing and 2 partials ⚠️
...ploy-web/src/components/sdl/PlacementFormModal.tsx 0.00% 7 Missing and 1 partial ⚠️
apps/deploy-web/src/utils/sdl/sdlGenerator.ts 66.66% 3 Missing and 4 partials ⚠️
...y-web/src/components/new-deployment/SdlBuilder.tsx 73.68% 3 Missing and 2 partials ⚠️
...s/deploy-web/src/components/sdl/ImportSdlModal.tsx 0.00% 4 Missing ⚠️
...hooks/useSdlServiceManager/useSdlServiceManager.ts 70.00% 3 Missing ⚠️
apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts 80.00% 2 Missing ⚠️
apps/deploy-web/src/utils/sdl/sdlImport.ts 93.33% 2 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3263      +/-   ##
==========================================
- Coverage   68.06%   66.70%   -1.37%     
==========================================
  Files        1074      989      -85     
  Lines       26124    24052    -2072     
  Branches     6243     5847     -396     
==========================================
- Hits        17781    16043    -1738     
+ Misses       7314     7005     -309     
+ Partials     1029     1004      -25     
Flag Coverage Δ *Carryforward flag
api 84.69% <ø> (ø) Carriedforward from 12d939f
deploy-web 52.75% <55.92%> (+0.07%) ⬆️
log-collector ?
notifications 91.06% <ø> (ø) Carriedforward from 12d939f
provider-console 81.38% <ø> (ø) Carriedforward from 12d939f
provider-inventory ?
provider-proxy 86.37% <ø> (ø) Carriedforward from 12d939f
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...y-web/src/components/sdl/AttributesFormControl.tsx 13.33% <100.00%> (ø)
...ts/sdl/LogCollectorControl/LogCollectorControl.tsx 98.46% <100.00%> (+0.04%) ⬆️
apps/deploy-web/src/utils/sdl/data.ts 100.00% <100.00%> (ø)
...loy-web/src/components/sdl/SignedByFormControl.tsx 8.00% <0.00%> (ø)
apps/deploy-web/src/hooks/useImportSimpleSdl.ts 0.00% <0.00%> (ø)
apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts 49.64% <80.00%> (+4.67%) ⬆️
apps/deploy-web/src/utils/sdl/sdlImport.ts 82.24% <93.33%> (+1.13%) ⬆️
...ploy-web/src/utils/sdl/transformCustomSdlFields.ts 2.32% <33.33%> (-0.06%) ⬇️
...hooks/useSdlServiceManager/useSdlServiceManager.ts 87.87% <70.00%> (-7.96%) ⬇️
...s/deploy-web/src/components/sdl/ImportSdlModal.tsx 0.00% <0.00%> (ø)
... and 6 more

... and 85 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx (1)

70-88: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync pricing independently from placementId.

After this refactor, pricing lives on the service. Right now the log collector only copies pricing when placementId changes, so editing the target service price leaves the generated log-collector service stale.

Suggested fix
       if (targetService.placementId !== logCollectorService.placementId) {
         changes.placementId = targetService.placementId;
+      }
+
+      if (
+        targetService.pricing.amount !== logCollectorService.pricing.amount ||
+        targetService.pricing.denom !== logCollectorService.pricing.denom
+      ) {
         changes.pricing = targetService.pricing;
       }
@@
-    [logCollectorService, logCollectorServiceIndex, targetService.placementId, targetService.title, update, env]
+    [
+      logCollectorService,
+      logCollectorServiceIndex,
+      targetService.placementId,
+      targetService.pricing.amount,
+      targetService.pricing.denom,
+      targetService.title,
+      update,
+      env
+    ]
   );
🤖 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
`@apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx`
around lines 70 - 88, The current update logic only copies targetService.pricing
when targetService.placementId changes, leaving logCollectorService.pricing
stale; modify the change detection to compare pricing separately and set
changes.pricing when logCollectorService.pricing !== targetService.pricing (in
addition to the existing placementId check). Update the block that builds the
changes object (referencing logCollectorService, targetService, and changes) so
pricing is assigned whenever it differs, then call
update(logCollectorServiceIndex, { ...logCollectorService, ...changes }) as
before; ensure you still include placementId handling that also sets pricing
when placementId changes if needed.
apps/deploy-web/src/utils/sdl/data.ts (1)

39-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clone nextCases when seeding default HTTP options.

Each service gets a fresh httpOptions object, but nextCases still points at the shared defaultHttpOptions.nextCases array. Mutating retry cases on one service will leak into every other service created from this factory.

Proposed fix
       httpOptions: {
         maxBodySize: defaultHttpOptions.maxBodySize,
         readTimeout: defaultHttpOptions.readTimeout,
         sendTimeout: defaultHttpOptions.sendTimeout,
-        nextCases: defaultHttpOptions.nextCases,
+        nextCases: [...defaultHttpOptions.nextCases],
         nextTries: defaultHttpOptions.nextTries,
         nextTimeout: defaultHttpOptions.nextTimeout
       }
🤖 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 `@apps/deploy-web/src/utils/sdl/data.ts` around lines 39 - 89, The
defaultService factory assigns httpOptions.nextCases by reference to
defaultHttpOptions.nextCases, causing mutations to leak across services; update
the httpOptions assignment inside defaultService to deep-clone nextCases (e.g.,
replace nextCases: defaultHttpOptions.nextCases with a cloned copy such as
nextCases: defaultHttpOptions.nextCases.map(c => ({ ...c })) or use
structuredClone/default deep-copy helper) so each ServiceType gets its own
independent nextCases array; modify only the httpOptions construction in
defaultService (referencing defaultService, httpOptions, and
defaultHttpOptions.nextCases).
🧹 Nitpick comments (2)
apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.spec.tsx (1)

85-104: ⚡ Quick win

Extend this propagation test to cover pricing too.

This scenario now verifies placementId only, so the stale-pricing regression in LogCollectorControl still passes. Adding a price change plus an assertion on services.1.pricing would lock the new contract down.

🤖 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
`@apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.spec.tsx`
around lines 85 - 104, The test "updates log-collector placement when target
service placement is changed" only asserts placementId propagation and misses
pricing; extend it to also change the target service pricing and assert the
log-collector service pricing updates. After setting newPlacementId via
form.setValue("services.0.placementId", newPlacementId), also set a new pricing
value (e.g., const newPricing = { /* minimal contract shape used in tests */ })
using form.setValue("services.0.pricing", newPricing) inside act, then add a
waitFor assertion that
expect(form.getValues("services.1.pricing")).toEqual(newPricing). Use the same
helpers already in the test (act, vi.waitFor, form.getValues) so the test
verifies both placementId and pricing propagation for the LogCollectorControl.
apps/deploy-web/src/utils/sdl/data.spec.ts (1)

20-25: ⚡ Quick win

Exercise a nested field in the isolation test.

This only proves top-level fields are independent. A nested alias like expose[0].httpOptions.nextCases can still be shared while this test stays green.

Suggested test shape
   it("defaultService returns independent copies", () => {
     const placement = defaultPlacement();
     const a = defaultService(placement.id);
     const b = defaultService(placement.id);
-    a.title = "modified";
-    expect(b.title).not.toBe("modified");
+    a.expose[0].httpOptions.nextCases.push("403");
+    expect(b.expose[0].httpOptions.nextCases).not.toContain("403");
   });
🤖 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 `@apps/deploy-web/src/utils/sdl/data.spec.ts` around lines 20 - 25, The test
for defaultService independence only checks a top-level field; update it to also
mutate and assert on a nested field to ensure deep copies are returned. In the
test that calls defaultPlacement(), defaultService(placement.id) and creates a
and b, modify a nested property such as a.expose[0].httpOptions.nextCases (e.g.,
push/replace an element or change a value) and add an assertion that
b.expose[0].httpOptions.nextCases did not change; keep references to
defaultService and defaultPlacement so the test targets the same factory
functions.
🤖 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 `@apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx`:
- Around line 95-102: When generateSdl(data) throws once, setError is never
cleared on subsequent successful generations so the error alert persists; update
the watch callback's success path (inside the try block where generateSdl,
lastSyncedSdlRef.current and setEditedManifest are used) to clear the error
(e.g., call setError("") or null) after a successful generation so the previous
error is removed.

In `@apps/deploy-web/src/components/sdl/PlacementFormModal.tsx`:
- Around line 185-194: The hook usePlacementIndexForService currently remaps a
missing placementId to 0 which causes bindings to target placements[0]; change
it to return the raw found index (allow returning -1 when not found) instead of
coercing to 0 so callers can detect invalid references (i.e., return index
as-is, not index >= 0 ? index : 0), update the function comment to document that
-1 means unresolved, and ensure callers (e.g., the modal fields) gate on
placementIndex >= 0 before binding to placements[placementIndex].

In `@apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx`:
- Around line 97-102: The code sets currentPlacementIndex to 0 when
placements.findIndex(...) returns -1, causing a broken placementId to display
placements[0]; change the logic around useWatch/placements/currentPlacementIndex
so that if findIndex(...) === -1 you do not default to 0 but instead leave
currentPlacement undefined (e.g., set currentPlacementIndex to -1 or directly
compute currentPlacement = undefined when not found), and update the UI paths
that reference currentPlacement (the summary/edit affordance) to only render
when currentPlacement is truthy; refer to useWatch, placements,
currentPlacementIndex, currentPlacement and currentService.

In `@apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts`:
- Around line 387-405: The schema SdlBuilderFormValuesSchema currently only
ensures referenced placementIds exist but does not enforce that placement ids
are unique; update the superRefine on SdlBuilderFormValuesSchema to detect
duplicate placement ids in data.placements (iterate placements, track seen ids,
and when a duplicate is found call ctx.addIssue with code z.ZodIssueCode.custom,
a clear message like "Placement id must be unique.", path ["placements",
<index>, "id"], and fatal:true) so duplicates are rejected before service
resolution; keep the existing service->placement existence check intact.

In `@apps/deploy-web/src/utils/sdl/sdlImport.ts`:
- Around line 25-33: The function importSimpleSdl should guard against
non-object YAML roots returned by yaml.load before accessing yamlJson.services:
in importSimpleSdl, after const yamlJson = yaml.load(yamlStr) as any, validate
that yamlJson is a non-null object (e.g., typeof yamlJson === "object" &&
yamlJson !== null && !Array.isArray(yamlJson)) and if not either return the
empty { placements, services } or throw a clear validation error; update the
check that currently does if (!yamlJson.services) to first verify yamlJson is an
object so you don't get a TypeError when yaml.load returns undefined or a
scalar.
- Around line 125-139: The code dereferences placementProfile.pricing[svcName]
without checking it, which can cause a generic runtime crash; before using
placementPricing (the variable) validate that placementProfile.pricing exists
and that placementProfile.pricing[svcName] is defined, and if not throw a
CustomValidationError with a clear message including placementName and svcName;
update the block that sets placementPricing and service.pricing (and any code
that expects placementPricing.amount/denom) to perform this null/undefined check
and throw the CustomValidationError rather than allowing a TypeError.

---

Outside diff comments:
In
`@apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx`:
- Around line 70-88: The current update logic only copies targetService.pricing
when targetService.placementId changes, leaving logCollectorService.pricing
stale; modify the change detection to compare pricing separately and set
changes.pricing when logCollectorService.pricing !== targetService.pricing (in
addition to the existing placementId check). Update the block that builds the
changes object (referencing logCollectorService, targetService, and changes) so
pricing is assigned whenever it differs, then call
update(logCollectorServiceIndex, { ...logCollectorService, ...changes }) as
before; ensure you still include placementId handling that also sets pricing
when placementId changes if needed.

In `@apps/deploy-web/src/utils/sdl/data.ts`:
- Around line 39-89: The defaultService factory assigns httpOptions.nextCases by
reference to defaultHttpOptions.nextCases, causing mutations to leak across
services; update the httpOptions assignment inside defaultService to deep-clone
nextCases (e.g., replace nextCases: defaultHttpOptions.nextCases with a cloned
copy such as nextCases: defaultHttpOptions.nextCases.map(c => ({ ...c })) or use
structuredClone/default deep-copy helper) so each ServiceType gets its own
independent nextCases array; modify only the httpOptions construction in
defaultService (referencing defaultService, httpOptions, and
defaultHttpOptions.nextCases).

---

Nitpick comments:
In
`@apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.spec.tsx`:
- Around line 85-104: The test "updates log-collector placement when target
service placement is changed" only asserts placementId propagation and misses
pricing; extend it to also change the target service pricing and assert the
log-collector service pricing updates. After setting newPlacementId via
form.setValue("services.0.placementId", newPlacementId), also set a new pricing
value (e.g., const newPricing = { /* minimal contract shape used in tests */ })
using form.setValue("services.0.pricing", newPricing) inside act, then add a
waitFor assertion that
expect(form.getValues("services.1.pricing")).toEqual(newPricing). Use the same
helpers already in the test (act, vi.waitFor, form.getValues) so the test
verifies both placementId and pricing propagation for the LogCollectorControl.

In `@apps/deploy-web/src/utils/sdl/data.spec.ts`:
- Around line 20-25: The test for defaultService independence only checks a
top-level field; update it to also mutate and assert on a nested field to ensure
deep copies are returned. In the test that calls defaultPlacement(),
defaultService(placement.id) and creates a and b, modify a nested property such
as a.expose[0].httpOptions.nextCases (e.g., push/replace an element or change a
value) and add an assertion that b.expose[0].httpOptions.nextCases did not
change; keep references to defaultService and defaultPlacement so the test
targets the same factory functions.
🪄 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

Run ID: 605838cb-438d-4d30-99bd-de65c8bd6c61

📥 Commits

Reviewing files that changed from the base of the PR and between e97cf2d and 9aa6f53.

📒 Files selected for processing (24)
  • apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx
  • apps/deploy-web/src/components/remote-deploy/update/RemoteDeployUpdate.tsx
  • apps/deploy-web/src/components/sdl/AttributesFormControl.tsx
  • apps/deploy-web/src/components/sdl/ImportSdlModal.tsx
  • apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.spec.tsx
  • apps/deploy-web/src/components/sdl/LogCollectorControl/LogCollectorControl.tsx
  • apps/deploy-web/src/components/sdl/PlacementFormModal.tsx
  • apps/deploy-web/src/components/sdl/SignedByFormControl.tsx
  • apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx
  • apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx
  • apps/deploy-web/src/hooks/useImportSimpleSdl.ts
  • apps/deploy-web/src/hooks/useSdlEnv/useSdlEnv.spec.tsx
  • apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.spec.tsx
  • apps/deploy-web/src/hooks/useSdlServiceManager/useSdlServiceManager.ts
  • apps/deploy-web/src/types/sdlBuilder/sdlBuilder.spec.ts
  • apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts
  • apps/deploy-web/src/utils/sdl/data.spec.ts
  • apps/deploy-web/src/utils/sdl/data.ts
  • apps/deploy-web/src/utils/sdl/sdlGenerator.spec.ts
  • apps/deploy-web/src/utils/sdl/sdlGenerator.ts
  • apps/deploy-web/src/utils/sdl/sdlImport.spec.ts
  • apps/deploy-web/src/utils/sdl/sdlImport.ts
  • apps/deploy-web/src/utils/sdl/transformCustomSdlFields.ts
  • apps/deploy-web/tests/seeders/sdlService.ts

Comment thread apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx
Comment thread apps/deploy-web/src/components/sdl/PlacementFormModal.tsx Outdated
Comment thread apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx Outdated
Comment thread apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts
Comment thread apps/deploy-web/src/utils/sdl/sdlImport.ts
Comment thread apps/deploy-web/src/utils/sdl/sdlImport.ts
@ygrishajev ygrishajev force-pushed the refactor/sdl-placement-first-form-values branch from 9aa6f53 to d71f5a8 Compare June 3, 2026 14:09
Comment thread apps/deploy-web/src/types/sdlBuilder/sdlBuilder.ts
…lues schema

Reshapes SdlBuilderFormValuesSchema so placements are first-class:
each placement carries a stable id and an optional region; services
reference one via placementId and own their own pricing. generateSdl
and importSimpleSdl read/write the new shape; SDL YAML output is
byte-equivalent to the previous structure for the same logical input.
The legacy single-placement wizard's form controls are rebound to the
new field paths and behave identically.

Bonus pre-existing bug fixes surfaced by the refactor:
- signedBy.allOf-only placements were silently dropped from SDL output
  (typo in the legacy guard tested anyOf twice instead of anyOf || allOf).
- A second IP-using service overwrote the first's endpoints block due
  to non-idempotent endpoints initialization.
@ygrishajev ygrishajev force-pushed the refactor/sdl-placement-first-form-values branch from 9b98baf to f1f7d1d Compare June 5, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants