Skip to content

fix(clusters): show advanced settings save action on payload changes - #2916

Open
rmnbrd wants to merge 1 commit into
fix-deploy-build-timerfrom
fix/advanced-settings-save-condition
Open

fix(clusters): show advanced settings save action on payload changes#2916
rmnbrd wants to merge 1 commit into
fix-deploy-build-timerfrom
fix/advanced-settings-save-condition

Conversation

@rmnbrd

@rmnbrd rmnbrd commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Issue: Fixed the Advanced Settings save banner to reliably show up only when the payload changes.

Jira ticket : QOV-2209

  • Compare normalized advanced-settings payloads instead of raw form dirtiness.
  • Preserve the current payload as the form baseline after loading and saving.
  • Extract payload construction into a reusable helper.
  • Keep the sticky action toaster visible when visibility changes during its exit delay.
  • Add regression coverage for payload-equivalent edits, form reset behavior, and toaster transitions.

Screenshots / Recordings

Not applicable.

Testing

  • Changes tested locally in the relevant Console's pages and Storybooks
  • yarn test or yarn test -u (if you need to regenerate snapshots)
  • yarn format
  • yarn lint

PR Checklist

  • I followed naming, styling, and TypeScript rules (see .cursor/rules)
  • I performed a self-review (diff inspected, dead code removed)
  • I titled the PR using Conventional Commits with a scope when possible (e.g. feat(service): add new Terraform service) - required for semantic-release
  • I only kept necessary comments, written in English (watch for useless AI comments)
  • I involved a designer to validate UI changes if I am not a designer
  • I covered new business logic with tests (unit)
  • I confirmed CI is green (Codecov red can be accepted)
  • I reviewed and executed locally any AI-assisted code

- Compare normalized settings payloads instead of raw form dirtiness
- Preserve sticky toaster visibility during exit animations
Copilot AI lite review requested due to automatic review settings August 28, 2026 15:59
@nx-cloud

nx-cloud Bot commented Aug 28, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit fd428d2

Command Status Duration Result
nx run console:build --parallel=3 --configurati... ✅ Succeeded 1m 10s View ↗
nx affected --target=test --parallel=3 --config... ✅ Succeeded 3m 5s View ↗
nx affected --target=lint --parallel=3 ✅ Succeeded 2m 35s View ↗
nx-cloud record -- yarn nx format:check ✅ Succeeded 6s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-28 16:07:34 UTC

@rmnbrd

rmnbrd commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@@cubic-dev-ai Can you review this PR?

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@@cubic-dev-ai Can you review this PR?

@rmnbrd I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 7 files

Confidence score: 2/5

  • buildClusterAdvancedSettingsPayload.ts can omit unchanged fields whose names contain . when another setting is edited, causing saved configuration to lose existing values; retain the flat key unless a nested value for the same path exists.
  • cluster-advanced-settings-feature.tsx compares a raw form baseline with a payload that substitutes defaults, so editing an unrelated field can incorrectly show the save action; normalize the baseline and current payload consistently before comparison.
  • cluster-advanced-settings-feature.spec.tsx does not cover the normalized-payload behavior being changed, leaving the field-omission and false-dirty-state regressions undetected; add assertions for both cases.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.tsx">

<violation number="1" location="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.tsx:30">
P2: When a loaded setting is empty or null and its default is non-empty, editing any other field incorrectly makes the save action appear because the baseline is raw while the form payload substitutes the default. Initialize `formBaseline` through `buildClusterAdvancedSettingsPayload` and update it when `defaultAdvancedSettings` becomes available.</violation>
</file>

<file name="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/build-cluster-advanced-settings-payload.ts">

<violation number="1" location="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/build-cluster-advanced-settings-payload.ts:12">
P1: When another setting is edited, this deletion drops unchanged fields whose names contain `.` from the payload. Retain the flat key unless a nested value for the same path exists, so saving one setting does not omit the others.</violation>
</file>

<file name="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.spec.tsx">

<violation number="1" location="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.spec.tsx:36">
P2: This test does not exercise the behavior the PR fixes, so it won't catch the regression. The new logic compares the normalized `buildClusterAdvancedSettingsPayload(formValues, ...)` against `formBaseline` instead of raw `formState.isDirty`; the regression is that payload-equivalent (formatting-only) edits wrongly trigger the save banner. This test only edits the value from `1` to `2`, which is a genuine payload change, so the banner shows under both the old `isDirty` logic and the new logic — it passes against the pre-fix code and validates nothing new. Add a case that types a payload-equivalent value (e.g. whitespace or equivalent JSON formatting) and asserts the toaster stays `hidden`, plus a case asserting the toaster stays visible during the exit-delay transition that this PR also touches.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


Object.keys(dataWithoutFlatKeys).forEach((key) => {
if (key.includes('.')) {
delete dataWithoutFlatKeys[key]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When another setting is edited, this deletion drops unchanged fields whose names contain . from the payload. Retain the flat key unless a nested value for the same path exists, so saving one setting does not omit the others.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/clusters/feature/src/lib/cluster-advanced-settings/build-cluster-advanced-settings-payload.ts, line 12:

<comment>When another setting is edited, this deletion drops unchanged fields whose names contain `.` from the payload. Retain the flat key unless a nested value for the same path exists, so saving one setting does not omit the others.</comment>

<file context>
@@ -0,0 +1,34 @@
+
+  Object.keys(dataWithoutFlatKeys).forEach((key) => {
+    if (key.includes('.')) {
+      delete dataWithoutFlatKeys[key]
+    }
+  })
</file context>
Suggested change
delete dataWithoutFlatKeys[key]
if (
key.split('.').reduce<unknown>((value, part) => {
return value !== null && typeof value === 'object' && part in value
? (value as Record<string, unknown>)[part]
: undefined
}, dataWithoutFlatKeys) !== undefined
) {
delete dataWithoutFlatKeys[key]
}

useEffect(() => {
if (clusterAdvancedSettings) {
methods.reset(initFormValues(keys, clusterAdvancedSettings))
setFormBaseline(clusterAdvancedSettings)

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.

P2: When a loaded setting is empty or null and its default is non-empty, editing any other field incorrectly makes the save action appear because the baseline is raw while the form payload substitutes the default. Initialize formBaseline through buildClusterAdvancedSettingsPayload and update it when defaultAdvancedSettings becomes available.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.tsx, line 30:

<comment>When a loaded setting is empty or null and its default is non-empty, editing any other field incorrectly makes the save action appear because the baseline is raw while the form payload substitutes the default. Initialize `formBaseline` through `buildClusterAdvancedSettingsPayload` and update it when `defaultAdvancedSettings` becomes available.</comment>

<file context>
@@ -19,57 +19,33 @@ export function ClusterAdvancedSettingsFeature() {
   useEffect(() => {
     if (clusterAdvancedSettings) {
       methods.reset(initFormValues(keys, clusterAdvancedSettings))
+      setFormBaseline(clusterAdvancedSettings)
     }
   }, [clusterAdvancedSettings, keys, methods])
</file context>

mockUseEditClusterAdvancedSettings.mockReturnValue({ mutateAsync: jest.fn() })
})

it('should keep the save banner hidden until the payload changes after the form reset', async () => {

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.

P2: This test does not exercise the behavior the PR fixes, so it won't catch the regression. The new logic compares the normalized buildClusterAdvancedSettingsPayload(formValues, ...) against formBaseline instead of raw formState.isDirty; the regression is that payload-equivalent (formatting-only) edits wrongly trigger the save banner. This test only edits the value from 1 to 2, which is a genuine payload change, so the banner shows under both the old isDirty logic and the new logic — it passes against the pre-fix code and validates nothing new. Add a case that types a payload-equivalent value (e.g. whitespace or equivalent JSON formatting) and asserts the toaster stays hidden, plus a case asserting the toaster stays visible during the exit-delay transition that this PR also touches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.spec.tsx, line 36:

<comment>This test does not exercise the behavior the PR fixes, so it won't catch the regression. The new logic compares the normalized `buildClusterAdvancedSettingsPayload(formValues, ...)` against `formBaseline` instead of raw `formState.isDirty`; the regression is that payload-equivalent (formatting-only) edits wrongly trigger the save banner. This test only edits the value from `1` to `2`, which is a genuine payload change, so the banner shows under both the old `isDirty` logic and the new logic — it passes against the pre-fix code and validates nothing new. Add a case that types a payload-equivalent value (e.g. whitespace or equivalent JSON formatting) and asserts the toaster stays `hidden`, plus a case asserting the toaster stays visible during the exit-delay transition that this PR also touches.</comment>

<file context>
@@ -0,0 +1,48 @@
+    mockUseEditClusterAdvancedSettings.mockReturnValue({ mutateAsync: jest.fn() })
+  })
+
+  it('should keep the save banner hidden until the payload changes after the form reset', async () => {
+    const { userEvent } = renderWithProviders(<ClusterAdvancedSettingsFeature />)
+
</file context>

Copilot AI 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.

Pull request overview

This PR improves the “Advanced settings” save banner behavior by baselining and comparing a normalized advanced-settings payload (instead of relying on raw react-hook-form dirtiness), and by making the sticky toaster resilient to rapid hide/show transitions during its exit delay.

Changes:

  • Add a normalized payload builder and compare normalized payloads against a stored baseline to decide when the save banner should be shown.
  • Persist/update the “current payload baseline” after initial load and after a successful save.
  • Fix sticky toaster exit-delay behavior by clearing the pending hide timeout when visibility returns to true, and add regression tests for both payload equivalence and toaster transitions.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
libs/shared/ui/src/lib/components/sticky-action-form-toaster/sticky-action-form-toaster.tsx Clears the exit-delay timeout via useEffect cleanup so the toaster stays visible if re-shown before the delay ends.
libs/shared/ui/src/lib/components/sticky-action-form-toaster/sticky-action-form-toaster.spec.tsx Adds a fake-timers regression test to cover the exit-delay visibility transition.
libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings.tsx Switches banner visibility to a normalized-payload comparison against a provided baseline instead of formState.isDirty.
libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings.spec.tsx Updates/adds tests to cover payload-equivalent edits (e.g. 1 vs 1.0) and rapid payload changes.
libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.tsx Tracks and updates formBaseline on load and on successful save, and uses the shared payload builder on submit.
libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings-feature.spec.tsx Adds coverage ensuring the banner remains hidden after reset until the payload actually changes.
libs/domains/clusters/feature/src/lib/cluster-advanced-settings/build-cluster-advanced-settings-payload.ts Extracts payload construction/parsing into a reusable helper shared by submit + payload comparison logic.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +50 to +56
const hasPayloadChanged =
formState.isDirty &&
formBaseline !== undefined &&
!equal(
buildClusterAdvancedSettingsPayload(formValues as Record<string, unknown>, defaultAdvancedSettings),
formBaseline
)
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.66%. Comparing base (192fd55) to head (fd428d2).

Files with missing lines Patch % Lines
...ced-settings/cluster-advanced-settings-feature.tsx 33.33% 3 Missing and 1 partial ⚠️
...ettings/build-cluster-advanced-settings-payload.ts 92.85% 0 Missing and 1 partial ⚠️
...action-form-toaster/sticky-action-form-toaster.tsx 75.00% 1 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                     @@
##           fix-deploy-build-timer    #2916      +/-   ##
==========================================================
+ Coverage                   48.52%   48.66%   +0.14%     
==========================================================
  Files                        1304     1218      -86     
  Lines                       28128    26490    -1638     
  Branches                     8220     7906     -314     
==========================================================
- Hits                        13648    12891     -757     
+ Misses                      12186    11439     -747     
+ Partials                     2294     2160     -134     
Flag Coverage Δ
unittests 48.66% <76.92%> (+0.14%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@cubic-dev-ai cubic-dev-ai 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.

3 existing issues remain and 1 new issue found across 7 files

Confidence score: 2/5

  • build-cluster-advanced-settings-payload.ts can drop untouched dotted settings when saving a different field, causing the submitted payload to omit existing configuration; preserve literal keys while allowing nested values to overwrite them.
  • cluster-advanced-settings-feature.tsx can show a save toaster when an empty loaded value has a non-empty default even though the normalized payload is unchanged; build the comparison baseline with buildClusterAdvancedSettingsPayload.
  • cluster-advanced-settings-feature.spec.tsx does not cover the normalized-payload comparison, leaving the primary regression insufficiently guarded; add a case for an untouched dotted setting and an empty value with a default.
  • cluster-advanced-settings.tsx watches the entire form and reparses every field on each keystroke, which may cause avoidable table-wide re-renders; narrow the watched state or memoize the payload computation.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings.tsx">

<violation number="1" location="libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings.tsx:40">
P3: Watching the entire form with `useWatch({ control })` re-renders the component and recomputes `buildClusterAdvancedSettingsPayload` (parsing every field) on every keystroke, re-rendering the whole settings table for each edit. For tables with many settings this is avoidable work; limit the watch to the fields used in the comparison or memoize the payload derivation keyed on the watched values.</violation>
</file>

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Re-trigger cubic

formBaseline,
}: ClusterAdvancedSettingsProps) {
const { control, formState, reset } = useFormContext<{ [key: string]: string }>()
const formValues = useWatch({ control })

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.

P3: Watching the entire form with useWatch({ control }) re-renders the component and recomputes buildClusterAdvancedSettingsPayload (parsing every field) on every keystroke, re-rendering the whole settings table for each edit. For tables with many settings this is avoidable work; limit the watch to the fields used in the comparison or memoize the payload derivation keyed on the watched values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/clusters/feature/src/lib/cluster-advanced-settings/cluster-advanced-settings.tsx, line 40:

<comment>Watching the entire form with `useWatch({ control })` re-renders the component and recomputes `buildClusterAdvancedSettingsPayload` (parsing every field) on every keystroke, re-rendering the whole settings table for each edit. For tables with many settings this is avoidable work; limit the watch to the fields used in the comparison or memoize the payload derivation keyed on the watched values.</comment>

<file context>
@@ -32,8 +34,10 @@ export function ClusterAdvancedSettings({
+  formBaseline,
 }: ClusterAdvancedSettingsProps) {
   const { control, formState, reset } = useFormContext<{ [key: string]: string }>()
+  const formValues = useWatch({ control })
   const [showOverriddenOnly, toggleShowOverriddenOnly] = useState(false)
 
</file context>

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.

2 participants