Skip to content

Commit fd5de75

Browse files
authored
feat(miner-ui): add a pause-reason input to the governor control (#6186) (#6207)
The dashboard Pause button called pauseGovernor with no arguments, so a reason could never be recorded from the web UI even though the action and CLI both support one. Add an optional reason input to GovernorControlSection, wired through to the pause action; an empty field is passed as undefined, matching the CLI's optional --reason flag. No change to pauseGovernor's signature or the CLI's behavior. Closes #6186
1 parent 74a79be commit fd5de75

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

apps/loopover-miner-ui/src/ledgers.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,58 @@ describe("LedgersPage (#4855)", () => {
156156
await waitFor(() => expect(screen.getByRole("button", { name: "Resume governor" })).toBeTruthy());
157157
});
158158

159+
it("passes the typed pause reason through to the pause action (#6186)", async () => {
160+
const pausedResult: GovernorPauseStateResult = {
161+
ok: true,
162+
pauseState: { paused: true, reason: "deploying a hotfix", pausedAt: "2026-07-13T12:30:00.000Z" },
163+
};
164+
const pauseGovernorAction = vi.fn(async (): Promise<GovernorPauseStateResult> => pausedResult);
165+
render(
166+
<LedgersPage
167+
loadLedgers={loadLedgersEmpty}
168+
loadGovernorPauseState={loadGovernorPauseStateDefault}
169+
pauseGovernorAction={pauseGovernorAction}
170+
/>,
171+
);
172+
await waitFor(() => expect(screen.getByText("Not paused")).toBeTruthy());
173+
fireEvent.change(screen.getByLabelText("Pause reason"), { target: { value: "deploying a hotfix" } });
174+
fireEvent.click(screen.getByRole("button", { name: "Pause governor" }));
175+
expect(pauseGovernorAction).toHaveBeenCalledWith("deploying a hotfix");
176+
await waitFor(() => expect(screen.getByRole("button", { name: "Resume governor" })).toBeTruthy());
177+
});
178+
179+
it("passes undefined to the pause action when the reason field is left empty (#6186)", async () => {
180+
const pauseGovernorAction = vi.fn(async (): Promise<GovernorPauseStateResult> => ({
181+
ok: true,
182+
pauseState: { paused: true, reason: null, pausedAt: "2026-07-13T12:30:00.000Z" },
183+
}));
184+
render(
185+
<LedgersPage
186+
loadLedgers={loadLedgersEmpty}
187+
loadGovernorPauseState={loadGovernorPauseStateDefault}
188+
pauseGovernorAction={pauseGovernorAction}
189+
/>,
190+
);
191+
await waitFor(() => expect(screen.getByText("Not paused")).toBeTruthy());
192+
expect((screen.getByLabelText("Pause reason") as HTMLInputElement).value).toBe("");
193+
fireEvent.click(screen.getByRole("button", { name: "Pause governor" }));
194+
expect(pauseGovernorAction).toHaveBeenCalledWith(undefined);
195+
});
196+
197+
it("shows the reason input only while unpaused, not once the governor is paused (#6186)", async () => {
198+
render(
199+
<LedgersPage
200+
loadLedgers={loadLedgersEmpty}
201+
loadGovernorPauseState={async () => ({
202+
ok: true,
203+
pauseState: { paused: true, reason: "bad PR", pausedAt: "2026-07-13T12:00:00.000Z" },
204+
})}
205+
/>,
206+
);
207+
await waitFor(() => expect(screen.getByRole("button", { name: "Resume governor" })).toBeTruthy());
208+
expect(screen.queryByLabelText("Pause reason")).toBeNull();
209+
});
210+
159211
it("clicking Resume governor calls the injected resume action and updates the displayed state from its result", async () => {
160212
const initiallyPaused: GovernorPauseState = { paused: true, reason: null, pausedAt: "2026-07-13T12:00:00.000Z" };
161213
const resumeGovernorAction = vi.fn(async (): Promise<GovernorPauseStateResult> => ({

apps/loopover-miner-ui/src/routes/ledgers.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
33

44
import { Button } from "@loopover/ui-kit/components/button";
55
import { Card, CardContent, CardHeader } from "@loopover/ui-kit/components/card";
6+
import { Input } from "@loopover/ui-kit/components/input";
67
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@loopover/ui-kit/components/table";
78

89
import { CLAIM_STATUSES, fetchLedgers, type ClaimStatus, type LedgersResult } from "../lib/ledgers";
@@ -64,9 +65,12 @@ export function GovernorControlSection({
6465
}: {
6566
result: GovernorPauseStateResult | null;
6667
pending: boolean;
67-
onPause: () => void;
68+
onPause: (reason?: string) => void;
6869
onResume: () => void;
6970
}) {
71+
// Optional pause reason, mirroring the CLI's `governor pause [--reason <text>]`; an empty field
72+
// is passed through as `undefined` so it matches the CLI's own optional-flag behavior.
73+
const [reason, setReason] = useState("");
7074
return (
7175
<section className="grid gap-3">
7276
<h3 className="font-display text-token-base font-semibold">Governor control</h3>
@@ -88,9 +92,20 @@ export function GovernorControlSection({
8892
Resume governor
8993
</Button>
9094
) : (
91-
<Button size="sm" variant="destructive" disabled={pending} onClick={onPause}>
92-
Pause governor
93-
</Button>
95+
<>
96+
<Input
97+
type="text"
98+
value={reason}
99+
onChange={(event) => setReason(event.target.value)}
100+
disabled={pending}
101+
placeholder="Reason (optional)"
102+
aria-label="Pause reason"
103+
className="w-auto flex-1 min-w-[12rem]"
104+
/>
105+
<Button size="sm" variant="destructive" disabled={pending} onClick={() => onPause(reason || undefined)}>
106+
Pause governor
107+
</Button>
108+
</>
94109
)}
95110
</div>
96111
)}
@@ -183,7 +198,7 @@ export function LedgersPage({
183198
}: {
184199
loadLedgers?: () => Promise<LedgersResult>;
185200
loadGovernorPauseState?: () => Promise<GovernorPauseStateResult>;
186-
pauseGovernorAction?: () => Promise<GovernorPauseStateResult>;
201+
pauseGovernorAction?: (reason?: string) => Promise<GovernorPauseStateResult>;
187202
resumeGovernorAction?: () => Promise<GovernorPauseStateResult>;
188203
}) {
189204
const [result, setResult] = useState<LedgersResult | null>(null);
@@ -231,7 +246,7 @@ export function LedgersPage({
231246
<GovernorControlSection
232247
result={pauseState}
233248
pending={actionPending}
234-
onPause={() => runGovernorAction(pauseGovernorAction)}
249+
onPause={(reason) => runGovernorAction(() => pauseGovernorAction(reason))}
235250
onResume={() => runGovernorAction(resumeGovernorAction)}
236251
/>
237252
<LedgersView result={result} />

0 commit comments

Comments
 (0)