Skip to content

feat(frontend): refresh button shows in-flight state + toasts#52

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/refresh-button-toast-feedback
Apr 24, 2026
Merged

feat(frontend): refresh button shows in-flight state + toasts#52
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/refresh-button-toast-feedback

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented Apr 24, 2026

Closes #86

Summary

  • Refresh button on the shared freshness indicator (Dashboard + Recommendations) now temporarily renames to "Refreshing..." and disables itself while the POST /api/recommendations/refresh call is in flight.
  • Surfaces a sticky info toast ("Refreshing recommendations…") on start, which is dismissed and replaced by a 5s success toast ("Recommendations refreshed") on completion. On failure the info toast is replaced by an error toast and the button text/enabled state are restored.
  • The freshness bar is re-rendered on success, so the "Data from " timestamp updates in the same round-trip.

Test plan

  • npx jest src/__tests__/freshness.test.ts — 17/17 pass, including two new cases covering the in-flight button state + toast sequence and the error-path rollback.
  • npx tsc --noEmit — clean.
  • Pre-commit build + frontend tests — passed locally.
  • End-to-end browser verification against the production bundle (results posted as a PR comment).

Rename the shared freshness Refresh button to "Refreshing..." while the
POST /api/recommendations/refresh call is in flight, and surface a sticky
info toast on start + a 5s success toast on completion (or an error toast
on failure). The freshness bar is re-rendered on success, which naturally
updates the "Data from <relative-time>" timestamp.

Adds two freshness.test.ts cases: one that holds the refresh promise open
to assert the in-flight button text/disabled state + info toast, and one
that asserts the error path restores the button and emits an error toast.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 24, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

Review profile: CHILL

Plan: Pro

Run ID: aeec175d-7af1-4b81-a36d-28fb888cb028

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

Walkthrough

The refresh button handler in the freshness component now provides toast-based user feedback during refresh operations, including button state management (disabled/enabled, label changes) and error handling with appropriate toast messages displayed to the user.

Changes

Cohort / File(s) Summary
Refresh Button Handler Implementation
frontend/src/freshness.ts
Enhanced refresh button handler with toast-based feedback: captures original button text, disables button during refresh, shows "Refreshing..." label, displays indefinite info toast during operation, and shows success or error toasts upon completion with proper button state restoration.
Refresh Button Tests
frontend/src/__tests__/freshness.test.ts
New test cases covering refresh button UI transitions: verifies button becomes disabled with "Refreshing..." label and info toast appears during refresh, success toast displays after completion, and error handling restores button state with error toast on failure.

Sequence Diagram

sequenceDiagram
    actor User
    participant Button as Refresh Button
    participant Handler as Click Handler
    participant Toast as Toast System
    participant API as Recommendations API

    User->>Button: Click refresh button
    Button->>Handler: Trigger click event
    Handler->>Button: Disable & set label to "Refreshing..."
    Handler->>Toast: Show info toast "Refreshing recommendations…"
    Handler->>API: Call refreshRecommendations()
    
    API-->>Handler: API resolves (success or error)
    
    alt Success Path
        Handler->>Toast: Dismiss info toast
        Handler->>Toast: Show success toast "Recommendations refreshed"
        Handler->>Button: Re-enable & restore original label
    else Error Path
        Handler->>Toast: Dismiss info toast
        Handler->>Toast: Show error toast with error message
        Handler->>Button: Re-enable & restore original label
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A button that refreshes, now speaks with care,
With toasts that flutter through the digital air,
"Refreshing..." it whispers, then back to the start,
Success or sweet sorrow, each message an art!
The freshness flows better, the feedback so clear,
A rabbit's delight to see progress so near! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(frontend): refresh button shows in-flight state + toasts' directly and clearly describes the main changes: adding in-flight UI state feedback and toast notifications to the refresh button.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/refresh-button-toast-feedback

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 and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
frontend/src/freshness.ts (1)

186-188: Optional: use the HTMLButtonElement property setters.

Now that btn is typed as HTMLButtonElement, btn.disabled = true / btn.disabled = false is more idiomatic than setAttribute('disabled', 'true') + removeAttribute('disabled'), and textContent on an element is never null so the ?? 'Refresh' fallback on Line 186 is dead. Feel free to defer — behaviourally equivalent.

♻️ Suggested tidy-up
-      const originalText = btn.textContent ?? 'Refresh';
-      btn.setAttribute('disabled', 'true');
+      const originalText = btn.textContent;
+      btn.disabled = true;
       btn.textContent = 'Refreshing...';
@@
-        btn.removeAttribute('disabled');
-        btn.textContent = originalText;
+        btn.disabled = false;
+        btn.textContent = originalText;

Note: the existing success-path test asserts getAttribute('disabled') === 'true' (Line 147 of the test), which would need to become expect(btn.disabled).toBe(true) to match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/freshness.ts` around lines 186 - 188, The code unnecessarily
uses setAttribute/removeAttribute and a dead null-coalescing fallback on
textContent: since btn is an HTMLButtonElement, replace
btn.setAttribute('disabled','true') / btn.removeAttribute('disabled') with
btn.disabled = true / btn.disabled = false, remove the redundant "?? 'Refresh'"
fallback when reading btn.textContent, and update the corresponding test
assertion (which currently checks getAttribute('disabled') === 'true') to assert
btn.disabled === true; target the btn variable usage in freshness.ts and the
test that asserts disabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/freshness.ts`:
- Around line 204-212: The catch handler in the refresh flow currently assumes
err has a .message and can throw if err is null/undefined; change the block to
safely extract a message before calling showToast and console.error: compute a
message variable using a safe check (e.g., err instanceof Error ? err.message :
(err !== null && err !== undefined ? String(err) : 'unknown error')), use that
message in console.error and showToast, and then proceed to call
inFlight.dismiss(), btn.removeAttribute('disabled'), and restore btn.textContent
to originalText; update references in this handler (err, showToast,
inFlight.dismiss, btn, originalText) only.

---

Nitpick comments:
In `@frontend/src/freshness.ts`:
- Around line 186-188: The code unnecessarily uses setAttribute/removeAttribute
and a dead null-coalescing fallback on textContent: since btn is an
HTMLButtonElement, replace btn.setAttribute('disabled','true') /
btn.removeAttribute('disabled') with btn.disabled = true / btn.disabled = false,
remove the redundant "?? 'Refresh'" fallback when reading btn.textContent, and
update the corresponding test assertion (which currently checks
getAttribute('disabled') === 'true') to assert btn.disabled === true; target the
btn variable usage in freshness.ts and the test that asserts disabled.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b388fedb-d119-4daf-8c3e-1be85f0185e0

📥 Commits

Reviewing files that changed from the base of the PR and between 1a84b65 and b230052.

📒 Files selected for processing (2)
  • frontend/src/__tests__/freshness.test.ts
  • frontend/src/freshness.ts

Comment thread frontend/src/freshness.ts
Comment on lines 204 to +212
} catch (err) {
console.error('Refresh failed:', err);
inFlight.dismiss();
showToast({
message: `Refresh failed: ${(err as Error).message ?? 'unknown error'}`,
kind: 'error',
});
btn.removeAttribute('disabled');
btn.textContent = originalText;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: guard against non-Error throws in the catch block.

(err as Error).message ?? 'unknown error' will throw a TypeError if err happens to be null or undefined (legal, if unusual, throw values), masking the real failure with a crash inside the error handler. ?? only rescues you when the expression returns undefined/null, not when the property access itself explodes.

🛡️ Safer extraction
-      } catch (err) {
-        console.error('Refresh failed:', err);
-        inFlight.dismiss();
-        showToast({
-          message: `Refresh failed: ${(err as Error).message ?? 'unknown error'}`,
-          kind: 'error',
-        });
+      } catch (err) {
+        console.error('Refresh failed:', err);
+        inFlight.dismiss();
+        const msg = err instanceof Error ? err.message : String(err);
+        showToast({
+          message: `Refresh failed: ${msg || 'unknown error'}`,
+          kind: 'error',
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/freshness.ts` around lines 204 - 212, The catch handler in the
refresh flow currently assumes err has a .message and can throw if err is
null/undefined; change the block to safely extract a message before calling
showToast and console.error: compute a message variable using a safe check
(e.g., err instanceof Error ? err.message : (err !== null && err !== undefined ?
String(err) : 'unknown error')), use that message in console.error and
showToast, and then proceed to call inFlight.dismiss(),
btn.removeAttribute('disabled'), and restore btn.textContent to originalText;
update references in this handler (err, showToast, inFlight.dismiss, btn,
originalText) only.

- Use idiomatic `btn.disabled = true/false` instead of setAttribute/
  removeAttribute (btn is typed as HTMLButtonElement).
- Drop the dead `?? 'Refresh'` fallback on `btn.textContent` — Element
  .textContent is never null so the branch is unreachable.
- Guard the error toast against non-Error throws: `(err as Error).message`
  would raise a TypeError if `err` were null/undefined, masking the real
  failure with a crash inside the error handler itself.
- Update the freshness tests to assert `btn.disabled` (boolean) instead
  of the attribute string.
@cristim cristim merged commit db429fe into feat/multicloud-web-frontend Apr 24, 2026
3 checks passed
cristim added a commit that referenced this pull request Apr 25, 2026
…opdown/DB (#63)

CodeRabbit flagged a key mismatch on PR #30: `SERVICE_FIELDS` in
`settings.ts` uses `service: 'savingsplans'`, while `commitmentConfigs`
in `commitmentOptions.ts` registered the AWS entry under `'savings-plans'`
(hyphenated). Any lookup via `isValidCombination`/`getCommitmentConfig`
with the non-hyphenated identifier silently fell through to `_default`
instead of hitting the Savings-Plans-specific config. Today both configs
are identical so behaviour is unchanged, but any future restriction
added to `'savings-plans'` would have been silently skipped.

CodeRabbit's suggestion was to rewrite `SERVICE_FIELDS` to the hyphenated
form, but `field.service` in `SERVICE_FIELDS` flows directly to
`api.updateServiceConfig(provider, service, cfg)` (settings.ts:1443)
which persists `service` as the primary key in the `service_configs`
table — existing production rows are `('aws', 'savingsplans')`, and
renaming SERVICE_FIELDS without a DB migration would have created
duplicate rows and orphaned the old config. The dropdown at
`index.html:112,703` (`<option value="savingsplans">`) and the backend
CLI flag (`cmd/main.go:92`) also use the non-hyphenated form.

Fix by renaming the `commitmentConfigs.aws` key from `'savings-plans'`
to `savingsplans` so every frontend call site — the SERVICE_FIELDS
path, the plan-form dropdown path, and any direct lookups — resolves
to the Savings-Plans-specific config. Update the two test cases in
`commitmentOptions.test.ts` to match.

This addresses the only live CodeRabbit finding across all open/closed
PRs (24-39, 52-54); PR #52's nitpicks were already resolved in db429fe,
and the remaining PR #30 nitpicks (double `Promise.resolve()` in
settings.test.ts:854, memorydb absent from SERVICE_FIELDS) apply only
to code that lives on PR #30's branch and not the base.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 25, 2026

Verified end-to-end in browser

Drove the actual production bundle (frontend/dist/js/app.1fe42ed8.js, the artefact built by the pre-commit hook on commit eb342cf1c) in Chrome against a tiny static + mock-API server, with the freshness endpoint stubbed to return a 1.5s in-flight window so the transient state is observable.

Three snapshots over a single Refresh click:

Phase Button text btn.disabled Toast Freshness pill
Immediate (post-click) Refreshing... true Refreshing recommendations… (info, sticky) 3h ago
Mid-flight (~600 ms in) Refreshing... true Refreshing recommendations… 3h ago
Post-completion (~2.8 s) Refresh false Recommendations refreshed (success) Just now

Confirms the coderabbit nitpick fix landed in the shipped bundle: the disabled boolean attribute is set via the HTMLButtonElement.disabled property setter (buttonAttrDisabled === "" while in flight, null after — the boolean-attribute fingerprint), not via setAttribute('disabled', 'true'). The freshness pill re-renders with the new timestamp without manual button restoration on the success path, as designed.

Tests + typecheck (already noted in the PR body) remain green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant