Add persistent action status banner and fix stacked toast overlap
CI / build-and-push (push) Successful in 1m54s
CI / build-and-push (push) Successful in 1m54s
Stack actions (start/stop/pull/update/…) now surface a dismissible status banner on the stack detail page instead of relying on the transient top-right toast alone. Also enable toast expand mode so multiple notifications no longer collapse behind each other. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pMmFFkdfxkoYjcEcpZTa5
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d81c48a5c0
commit
9d28e12cd7
@@ -1,28 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { stacksApi } from "@/api/stacks";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
|
||||
export type StackActionPhase = "running" | "success" | "error";
|
||||
|
||||
export type StackActionStatus = {
|
||||
id: string;
|
||||
label: string;
|
||||
phase: StackActionPhase;
|
||||
message?: string;
|
||||
at: number;
|
||||
};
|
||||
|
||||
/** Success banners self-clear so they don't linger forever; errors stay until
|
||||
* dismissed or the next action overwrites them. */
|
||||
const SUCCESS_CLEAR_MS = 5000;
|
||||
|
||||
export function useStackActions() {
|
||||
const qc = useQueryClient();
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState<StackActionStatus | null>(null);
|
||||
const clearTimer = useRef<number>();
|
||||
|
||||
const run = async (
|
||||
id: string,
|
||||
label: string,
|
||||
fn: (id: string) => Promise<unknown>
|
||||
) => {
|
||||
window.clearTimeout(clearTimer.current);
|
||||
setBusyId(id);
|
||||
setStatus({ id, label, phase: "running", at: Date.now() });
|
||||
const t = toast.loading(`${label} ${id}…`);
|
||||
try {
|
||||
await fn(id);
|
||||
toast.success(`${label} ${id} ✓`, { id: t });
|
||||
setStatus({ id, label, phase: "success", at: Date.now() });
|
||||
clearTimer.current = window.setTimeout(() => {
|
||||
setStatus((s) => (s?.phase === "success" ? null : s));
|
||||
}, SUCCESS_CLEAR_MS);
|
||||
qc.invalidateQueries({ queryKey: ["stacks"] });
|
||||
qc.invalidateQueries({ queryKey: ["stack", id] });
|
||||
qc.invalidateQueries({ queryKey: ["stack-updates"] });
|
||||
} catch (err) {
|
||||
toast.error(apiErrorMessage(err), { id: t });
|
||||
const message = apiErrorMessage(err);
|
||||
toast.error(message, { id: t });
|
||||
setStatus({ id, label, phase: "error", message, at: Date.now() });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
@@ -30,6 +54,11 @@ export function useStackActions() {
|
||||
|
||||
return {
|
||||
busyId,
|
||||
status,
|
||||
dismissStatus: () => {
|
||||
window.clearTimeout(clearTimer.current);
|
||||
setStatus(null);
|
||||
},
|
||||
start: (id: string) => run(id, "Starting", stacksApi.start),
|
||||
stop: (id: string) => run(id, "Stopping", stacksApi.stop),
|
||||
restart: (id: string) => run(id, "Restarting", stacksApi.restart),
|
||||
|
||||
Reference in New Issue
Block a user