Skip to content

FAB-380: container lifecycle ops UI (instance + cluster), safe mode, resend invite#1429

Open
DavidCockerill wants to merge 7 commits into
stagefrom
FAB-380
Open

FAB-380: container lifecycle ops UI (instance + cluster), safe mode, resend invite#1429
DavidCockerill wants to merge 7 commits into
stagefrom
FAB-380

Conversation

@DavidCockerill

@DavidCockerill DavidCockerill commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

Studio UI for the FAB-380 container lifecycle operations (stop / start / restart, with safe mode), plus a small resend-invite addition for pending org users. This is the front-end for the central-manager / host-manager work in HarperFast/central-manager#404 and HarperFast/host-manager#126.

Container ops are a distinct class from the existing proxied Harper "restart" — they're handled host-side via the CM, are async (the endpoint returns a transitional status and the resting state lands as the poll catches up), and support safeMode (boot Harper core without loading user apps/components).

What's included

Per-instance container ops (useInstanceMenuItems)

  • A Container section in the instance ⋯ menu — Start / Start in safe mode (when STOPPED), Restart / Restart in safe mode / Stop (when RUNNING). Hidden mid-transition.
  • New useInstanceContainerOps hook + instanceContainerOperation client (POST /HDBInstance/{id}/container/{action}).

Cluster-wide container ops (ClusterCard)

  • A Container section in the cluster ⋯ menu, gated by cluster state (RUNNING → Restart/Restart-safe/Stop, STOPPED → Start/Start-safe, PARTIAL → Start/Stop/Restart).
  • Safe-mode ops force strategy: parallel (the CM rejects safeMode + rolling). Stop shows a confirm dialog; non-safe Restart opens a rolling/parallel picker.
  • New useClusterContainerOps hook + clusterContainerOperation client (POST /Cluster/{id}/container/{action}), and ClusterContainerOpModals.

Cluster overview — discoverability (ClusterStateMenu)

  • A labeled "Cluster actions" dropdown (AWS EC2 "Instance state" style) on the cluster overview that surfaces every op — Start / Start in safe mode / Restart / Restart in safe mode / Stop — plus Terminate, in one place. Ops that don't apply to the current status are shown but disabled, so the feature set stays discoverable (feedback was that safe mode in particular was hard to find). Hidden for self-managed clusters.
  • Safe-mode explain-and-confirm dialog (SafeModeConfirmDialog): "safe mode" is jargon and a recovery action, so rather than a hover-only tooltip we explain what it does at the moment of use. Shared by the start-in-safe-mode and restart-in-safe-mode entries on both the overview and the card.
  • Transient op status on the clusters-list card: while an op is in flight the card shows a Stopping / Starting / Restarting badge (with spinner), and a Stopped label when the cluster is stopped — so users can see what's happening without opening the cluster. Clears on its own as the 10s poll catches the resting state.
  • Plain (non-safe) Start/Restart send safeMode: false so a cluster that's in safe mode returns to normal mode. (Previously the UI omitted safeMode, and the CM preserves the current mode when it's omitted — so a plain restart of a safe-mode cluster stayed in safe mode.)

Safe mode indicators

  • Per-instance amber "Safe mode" badge on the Instances page.
  • A "Safe mode" pill next to the status pill on the cluster overview when every instance is in safe mode.

Stopped-state handling

  • isStoppedOrTransitioning helper; stopped/transitioning instances stop get_status polling (the ops API is unreachable), show a muted dot instead of a stale "Available"/endless spinner, and a "—" in the sign-in cell.
  • STOPPED badge is now destructive (red); cluster status pill colors STOPPED red / PARTIAL yellow. Stopped cards open the instances page; partial cards open the overview.

Resend invite

  • A "Resend invite" action in the org users table, shown only for PENDING users. Reuses the existing invite mutation (POST /UserInvite with { email, roleId }); the CM treats a repeat invite for a pending user as a resend.

Screenshots

Cluster overview — Running + Safe mode pill

image

Instances — Safe mode badges

image

Per-instance ⋯ menu — Container section

image

Cluster ⋯ menu — Container section

image

Restart strategy picker (non-safe cluster restart)

image

Resend invite (pending users only)

image

Backend dependencies / notes

  • Depends on the CM/HM container endpoints (HarperFast/central-manager#404, HarperFast/host-manager#126). The container clients are hand-typed past the generated SDK — the /container/{action} paths and safeMode/strategy body aren't in the CM OpenAPI yet (same pattern as terminateCluster). Follow-up: regenerate the SDK once the CM OpenAPI exposes them and drop the casts.
  • Instance.safeMode and Cluster.instances (now the patched Instance[]) are added in api.patch.d.ts for the same reason.
  • Resend invite needs the CM invite-endpoint change deployed. On the dev CM a repeat invite for a pending user still returns 409 "User already has a pending invite"; the UI is correct and will work once that build is out.

Out of scope / follow-ups

  • Error toasts: op/terminate failures in this PR route through the global MutationCache.onError (which shows the server's message) with no local error toast, so there's no double-toasting. The same pre-existing local-onError double-toast pattern lives elsewhere in the app (e.g. ClusterCard terminate, org settings/roles/users, ClusterForm) — intentionally left for a separate repo-wide cleanup rather than widened here.
  • Disable-vs-hide: the "Cluster actions" dropdown disables inapplicable ops (better discoverability); the ⋯ menus hide them. Worth aligning on one convention in a follow-up.

Testing

  • tsc -p tsconfig.app.json, oxlint, dprint all pass; full unit suite green (pre-commit, 1239 tests).
  • Two gemini-code-assist review passes addressed — genuine findings fixed (double-toasts, [clusterId] invalidation guard, Cancel-button variants, focus-visible a11y ring); icon-size comments verified as no-ops (DropdownMenuItem/Badge already size their svg children).
  • Verified live against dev/stage: instance + cluster ops fire and are accepted, safe-mode restart round-trips (badges + pill render), a plain restart of a safe-mode cluster returns it to normal mode, stopped-state polling stops, transient card status renders and clears, restart picker works, and the resend button shows only for pending rows.

🤖 Generated with Claude Code

DavidCockerill and others added 3 commits July 7, 2026 16:32
…te handling

Studio UI for the FAB-380 container operations:
- Per-instance container ops in the instance ⋯ menu (Start / Start in safe mode /
  Restart / Restart in safe mode / Stop) under a "Container" section, gated by manage
  permission + instance status. New CM apiClient mutation + toast/invalidate hook.
- Safe-mode badge on running instances (hidden when stopped — a UI start clears it).
- Stopped/transitioning instances: stop polling get_status (ops API unreachable), show a
  muted availability dot and a "—" placeholder instead of a perpetual spinner.
- STOPPED status badge -> red.
- Cluster card stays reachable when not running: fully-stopped opens the instances page,
  partial opens the cluster overview.
- Cluster overview StatusPill: STOPPED -> red, PARTIAL -> yellow.
- Add safeMode to the Instance type (returned by the CM, not yet in the generated schema).

The container calls are hand-typed past the generated SDK (the /HDBInstance/{id}/container/
{action} paths and the safeMode field aren't in the CM OpenAPI yet). Follow-up: regenerate
the SDK once the CM spec exposes them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add cluster-level Start/Stop/Restart to the ClusterCard ⋯ menu, status-gated
(RUNNING → Restart/Restart-safe/Stop, STOPPED → Start/Start-safe, PARTIAL →
Start/Stop/Restart). Safe-mode ops force parallel; Stop shows a confirm dialog;
non-safe Restart opens a rolling/parallel picker. Wired via useClusterContainerOps
+ clusterContainerOperation (hand-typed past the generated SDK, same as terminate).

Also show a "Safe mode" pill next to the status pill on the cluster overview when
every instance is in safe mode, and override Cluster.instances to the patched
Instance type so safeMode is available on cluster.instances.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Resend invite" action to the org users table, shown only for PENDING
users. Reuses the existing invite mutation (POST /UserInvite with { email,
roleId }, targeting the user's existing role); the backend treats a repeat
invite for a pending user as a resend. Renamed tableDefinition.ts -> .tsx to
host the JSX action cell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces container lifecycle operations (start, stop, restart, and safe mode) for both individual instances and entire clusters, alongside a new "Resend invite" button for pending organization users. The review feedback focuses on improving code robustness and UI consistency, recommending the removal of local error handlers to prevent double-toasting, guarding query invalidations against undefined parameters, adding size classes to icons, utilizing appropriate button variants, and enhancing accessibility with focus-visible ring styles on custom buttons.

Comment on lines +27 to +33
inviteUser(
{ email: user.email, roleId },
{
onSuccess: () => toast.success(`Invitation resent to ${user.email}.`),
onError: () => toast.error(`Failed to resend invitation to ${user.email}.`),
},
);

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.

medium

To prevent duplicate error notifications (double-toasting), do not add local onError handlers to individual mutations if a global error handler (such as a global MutationCache.onError in React Query) is already configured to handle and display error messages.

			inviteUser(
				{ email: user.email, roleId },
				{
					onSuccess: () => toast.success(`Invitation resent to ${user.email}.`),
				}
			);
References
  1. Do not add local onError handlers to individual mutations if a global error handler (such as a global MutationCache.onError in React Query) is already configured to handle and display error messages, to prevent duplicate error notifications (double-toasting).

Comment on lines +47 to +53
} catch {
toast.dismiss(toastId);
toast.error('Error', {
description: `Failed to ${action} cluster ${cluster.name}.`,
action: { label: 'Dismiss', onClick: () => toast.dismiss() },
});
}

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.

medium

To prevent duplicate error notifications (double-toasting), avoid adding local error toasts in the caught error block of mutations if a global error handler is already configured. Simply dismiss the loading toast and let the global handler display the error message.

			} catch {
				toast.dismiss(toastId);
			}
References
  1. Do not add local onError handlers to individual mutations if a global error handler (such as a global MutationCache.onError in React Query) is already configured to handle and display error messages, to prevent duplicate error notifications (double-toasting).

Comment on lines +50 to +56
} catch {
toast.dismiss(toastId);
toast.error('Error', {
description: `Failed to ${action} ${target}.`,
action: { label: 'Dismiss', onClick: () => toast.dismiss() },
});
}

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.

medium

To prevent duplicate error notifications (double-toasting), avoid adding local error toasts in the caught error block of mutations if a global error handler is already configured. Simply dismiss the loading toast and let the global handler display the error message.

			} catch {
				toast.dismiss(toastId);
			}
References
  1. Do not add local onError handlers to individual mutations if a global error handler (such as a global MutationCache.onError in React Query) is already configured to handle and display error messages, to prevent duplicate error notifications (double-toasting).

Comment thread src/hooks/useInstanceContainerOps.ts Outdated
Comment on lines +43 to +44
void queryClient.invalidateQueries({ queryKey: [clusterId] });
void queryClient.invalidateQueries({ queryKey: [instance.id] });

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.

medium

Since clusterId is retrieved from useParams({ strict: false }) and is optional, guard the query invalidation to prevent invalidating [undefined], which could lead to unexpected behavior or invalidating more queries than intended.

				if (clusterId) {
					void queryClient.invalidateQueries({ queryKey: [clusterId] });
				}
				void queryClient.invalidateQueries({ queryKey: [instance.id] });

Comment on lines +79 to +82
<Badge variant="warning" title="Running in safe mode — user apps/components are not loaded">
<LifeBuoyIcon />
Safe mode
</Badge>

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.

medium

Add a size class to LifeBuoyIcon inside the Badge to ensure visual consistency with the SafeModePill in ClusterHome.tsx (which uses size-3) and prevent it from rendering at its default large size.

Suggested change
<Badge variant="warning" title="Running in safe mode — user apps/components are not loaded">
<LifeBuoyIcon />
Safe mode
</Badge>
<Badge variant="warning" title="Running in safe mode — user apps/components are not loaded">
<LifeBuoyIcon className="size-3" />
Safe mode
</Badge>

</DialogHeader>
<DialogFooter>
<div className="flex justify-end gap-3">
<Button type="button" onClick={() => setStopOpen(false)}>Cancel</Button>

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.

medium

The "Cancel" button should use variant="defaultOutline" or variant="ghost" to prevent visual competition with the primary/destructive action ("Stop cluster").

Suggested change
<Button type="button" onClick={() => setStopOpen(false)}>Cancel</Button>
<Button type="button" variant="defaultOutline" onClick={() => setStopOpen(false)}>Cancel</Button>
References
  1. Do not replace the custom button variant 'defaultOutline' with the standard shadcn/ui 'outline' variant, as 'defaultOutline' is a valid custom variant in this repository (defined in 'src/components/ui/buttonVariants.tsx') that provides a primary border.

</button>
</div>
<DialogFooter>
<Button type="button" onClick={() => setRestartOpen(false)}>Cancel</Button>

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.

medium

The "Cancel" button should use variant="defaultOutline" or variant="ghost" to prevent visual competition with the primary actions.

Suggested change
<Button type="button" onClick={() => setRestartOpen(false)}>Cancel</Button>
<Button type="button" variant="defaultOutline" onClick={() => setRestartOpen(false)}>Cancel</Button>
References
  1. Do not replace the custom button variant 'defaultOutline' with the standard shadcn/ui 'outline' variant, as 'defaultOutline' is a valid custom variant in this repository (defined in 'src/components/ui/buttonVariants.tsx') that provides a primary border.

Comment on lines +66 to +71
<button
type="button"
disabled={isPending}
onClick={() => onConfirmRestart('rolling')}
className="rounded-lg border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/50 disabled:opacity-60"
>

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.

medium

Add focus-visible ring styles to the custom <button> element to ensure keyboard navigation is visible and accessible.

Suggested change
<button
type="button"
disabled={isPending}
onClick={() => onConfirmRestart('rolling')}
className="rounded-lg border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/50 disabled:opacity-60"
>
<button
type="button"
disabled={isPending}
onClick={() => onConfirmRestart('rolling')}
className="rounded-lg border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:opacity-60"
>

Comment on lines +79 to +84
<button
type="button"
disabled={isPending}
onClick={() => onConfirmRestart('parallel')}
className="rounded-lg border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/50 disabled:opacity-60"
>

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.

medium

Add focus-visible ring styles to the custom <button> element to ensure keyboard navigation is visible and accessible.

Suggested change
<button
type="button"
disabled={isPending}
onClick={() => onConfirmRestart('parallel')}
className="rounded-lg border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/50 disabled:opacity-60"
>
<button
type="button"
disabled={isPending}
onClick={() => onConfirmRestart('parallel')}
className="rounded-lg border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:opacity-60"
>

@dawsontoth

Copy link
Copy Markdown
Contributor

@DavidCockerill Can you get it to rebase, and then take a look at the new nav paradigm for clusters? Surfacing things in the ... menu for clusters is great, but you can also surface them on the cluster dashboard, before you navigate into the cluster. That can aid discoverability of these features. Take a pass at the instance level features too to see if any of those can be brought out of the ... on instances, and perhaps surfaced around that instances table for better discoverability.

@dawsontoth

Copy link
Copy Markdown
Contributor

FWIW, I tend to rebase instead of merging changes in. That will better represent what will happen when we move to bring this PR into the main branch. Plus, it gives you a clearer view of your changes being additive to the changes that have landed already.

DavidCockerill and others added 2 commits July 9, 2026 13:05
…afe-mode UX

- Cluster overview: a "Cluster actions" dropdown (AWS "Instance state" style) with all
  container ops plus Terminate; ops invalid for the current status are shown but disabled
  so the feature set is discoverable.
- Safe mode: an explain-and-confirm dialog (what it does + when to use it) that both the
  overview dropdown and the clusters-list card menu route through, so safe mode always
  explains itself instead of firing silently.
- Clusters-list card: a temporary status badge for container-op states —
  Stopping/Starting/Restarting (amber + spinner) and Stopped/Partial — that clears on its
  own as the list polls; RUNNING stays clean.
- Fix: plain Start/Restart now send safeMode:false so they return a cluster to normal mode.
  Previously they omitted safeMode and the CM preserves the current mode, so restarting an
  in-safe-mode cluster stayed in safe mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rId, a11y polish

- Remove local error toasts in ResendInviteButton, useClusterContainerOps,
  and useInstanceContainerOps; the global MutationCache.onError already
  surfaces failures with the server's message (avoids double-toasting).
- Guard the [clusterId] query invalidation (useParams strict:false can be
  undefined) so we don't invalidate [undefined].
- Cancel buttons -> defaultOutline (house style) across the container-op and
  safe-mode dialogs so they don't compete with the primary/destructive action.
- Add focus-visible ring to the raw restart-strategy <button>s for keyboard a11y.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavidCockerill

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces cluster-wide and instance-level container lifecycle operations (stop, start, restart, and safe mode) along with confirmation dialogs, status badges, and polling suppression for stopped or transitioning instances. It also adds a "Resend invite" button for pending organization users. The review feedback suggests removing a local onError handler in ClusterStateMenu to prevent duplicate error notifications, and adding size and margin classes to several icons inside dropdown items and badges to ensure proper layout and alignment.

Comment on lines +61 to +67
onError: () => {
toast.error('Error', {
description: `Failed to terminate cluster: ${cluster.name}.`,
action: { label: 'Dismiss', onClick: () => toast.dismiss() },
});
setTerminateOpen(false);
},

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.

medium

Do not add local onError handlers that display error messages if a global error handler (such as a global MutationCache.onError in React Query) is already configured to handle and display error messages, to prevent duplicate error notifications (double-toasting). Instead, just handle the local state cleanup (like closing the modal).

			onError: () => {
				setTerminateOpen(false);
			},
References
  1. Do not add local onError handlers to individual mutations if a global error handler (such as a global MutationCache.onError in React Query) is already configured to handle and display error messages, to prevent duplicate error notifications (double-toasting).

Comment on lines +102 to +130
<DropdownMenuItem
disabled={opsDisabled || !(isStopped || isPartial)}
onClick={() => void runClusterOp('start', { safeMode: false, strategy: 'parallel' })}
>
<PlayIcon /> Start
</DropdownMenuItem>
<DropdownMenuItem disabled={opsDisabled || !isStopped} onClick={() => setSafeModeAction('start')}>
<LifeBuoyIcon /> Start in safe mode
</DropdownMenuItem>
<DropdownMenuItem
disabled={opsDisabled || !(isRunning || isPartial)}
onClick={() => setRestartOpen(true)}
>
<RotateCwIcon /> Restart
</DropdownMenuItem>
<DropdownMenuItem disabled={opsDisabled || !isRunning} onClick={() => setSafeModeAction('restart')}>
<LifeBuoyIcon /> Restart in safe mode
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
disabled={opsDisabled || !(isRunning || isPartial)}
onClick={() => setStopOpen(true)}
>
<SquareIcon /> Stop
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" disabled={!remove} onClick={() => setTerminateOpen(true)}>
<TrashIcon /> Terminate
</DropdownMenuItem>

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.

medium

Icons rendered directly inside DropdownMenuItem (PlayIcon, LifeBuoyIcon, RotateCwIcon, SquareIcon, TrashIcon) lack size and margin classes, which will cause them to render at their default large size (24x24) and sit right next to the text without any spacing. Consider adding className="mr-2 size-4" to each of these icons to keep them compact and properly spaced.

					<DropdownMenuItem
						disabled={opsDisabled || !(isStopped || isPartial)}
						onClick={() => void runClusterOp('start', { safeMode: false, strategy: 'parallel' })}
					>
						<PlayIcon className="mr-2 size-4" /> Start
					</DropdownMenuItem>
					<DropdownMenuItem disabled={opsDisabled || !isStopped} onClick={() => setSafeModeAction('start')}>
						<LifeBuoyIcon className="mr-2 size-4" /> Start in safe mode
					</DropdownMenuItem>
					<DropdownMenuItem
						disabled={opsDisabled || !(isRunning || isPartial)}
						onClick={() => setRestartOpen(true)}
					>
						<RotateCwIcon className="mr-2 size-4" /> Restart
					</DropdownMenuItem>
					<DropdownMenuItem disabled={opsDisabled || !isRunning} onClick={() => setSafeModeAction('restart')}>
						<LifeBuoyIcon className="mr-2 size-4" /> Restart in safe mode
					</DropdownMenuItem>
					<DropdownMenuItem
						variant="destructive"
						disabled={opsDisabled || !(isRunning || isPartial)}
						onClick={() => setStopOpen(true)}
					>
						<SquareIcon className="mr-2 size-4" /> Stop
					</DropdownMenuItem>
					<DropdownMenuSeparator />
					<DropdownMenuItem variant="destructive" disabled={!remove} onClick={() => setTerminateOpen(true)}>
						<TrashIcon className="mr-2 size-4" /> Terminate
					</DropdownMenuItem>

{cell.row.original.safeMode && !isStoppedOrTransitioning(cell.row.original.status)
? (
<Badge variant="warning" title="Running in safe mode — user apps/components are not loaded">
<LifeBuoyIcon />

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.

medium

The LifeBuoyIcon inside the Badge lacks a size class, which may cause it to render at its default large size (24x24) and distort the badge layout. Consider adding a size class like size-3.5 to keep it compact and consistent with other status indicators.

Suggested change
<LifeBuoyIcon />
<LifeBuoyIcon className="size-3.5" />

{isActive && view && <ClusterCardAction cluster={cluster} hasCardLink={!!cardHref} />}
{showContainerOpBadge && cluster.status && (
<Badge variant={isClusterStopped ? 'destructive' : 'warning'}>
{isClusterTransitioning && <Loader2 className="animate-spin" />}

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.

medium

The Loader2 icon inside the Badge lacks a size class, which may cause it to render at its default large size (24x24) and distort the badge layout. Consider adding a size class like size-3.5 to keep it compact and consistent.

Suggested change
{isClusterTransitioning && <Loader2 className="animate-spin" />}
{isClusterTransitioning && <Loader2 className="size-3.5 animate-spin" />}

Gemini re-review: the terminate mutation's local onError toast duplicates
the global MutationCache.onError toast. Keep only the modal close.
(The 3 icon-size comments are no-ops: DropdownMenuItem already applies
[&_svg]:size-4 + gap-2, and Badge applies [&>svg]:size-3.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavidCockerill DavidCockerill marked this pull request as ready for review July 9, 2026 19:18
@DavidCockerill DavidCockerill requested a review from a team as a code owner July 9, 2026 19:18
@DavidCockerill DavidCockerill requested a review from dawsontoth July 9, 2026 19:18
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