remove: session operations center and progress panel
The bottom-right progress panel duplicated feedback already shown by toasts. Remove it and the operation-tracking state to simplify the UI: - Delete state/session-operations.tsx and session-progress-panel.tsx. - Remove SessionOperationsProvider/SessionProgressPanel from AppShell. - Remove startOperation/completeOperation calls from useInstanceActions and ToolStarter. - Remove SessionOperationsProvider wrapper from DashboardPage.test.tsx. - Remove .session-progress-panel CSS rules. - Format use-events.test.ts mock to match project lint rules. Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed).
This commit is contained in:
@@ -1,259 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
import type { InstanceEventPayload } from "../types/events";
|
||||
|
||||
export type OperationType =
|
||||
| "create"
|
||||
| "start"
|
||||
| "stop"
|
||||
| "restart"
|
||||
| "delete"
|
||||
| "recreate-tunnel";
|
||||
|
||||
export type OperationStatus = "pending" | "active" | "success" | "error";
|
||||
|
||||
export interface Operation {
|
||||
id: string;
|
||||
type: OperationType;
|
||||
instanceId: string;
|
||||
displayName: string;
|
||||
status: OperationStatus;
|
||||
message: string;
|
||||
step: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SessionOperationsContextType {
|
||||
operations: Operation[];
|
||||
startOperation: (
|
||||
type: OperationType,
|
||||
instanceId: string,
|
||||
displayName: string,
|
||||
) => string;
|
||||
updateOperationFromEvent: (event: InstanceEventPayload) => void;
|
||||
completeOperation: (
|
||||
instanceId: string,
|
||||
type: OperationType,
|
||||
outcome: "success" | "error",
|
||||
message?: string,
|
||||
) => void;
|
||||
dismissOperation: (id: string) => void;
|
||||
}
|
||||
|
||||
const SessionOperationsContext =
|
||||
createContext<SessionOperationsContextType | undefined>(undefined);
|
||||
|
||||
let operationIdCounter = 0;
|
||||
|
||||
function actionLabel(type: OperationType): string {
|
||||
switch (type) {
|
||||
case "create":
|
||||
return "Creating";
|
||||
case "start":
|
||||
return "Starting";
|
||||
case "stop":
|
||||
return "Stopping";
|
||||
case "restart":
|
||||
return "Restarting";
|
||||
case "delete":
|
||||
return "Deleting";
|
||||
case "recreate-tunnel":
|
||||
return "Recreating tunnel";
|
||||
default:
|
||||
return "Working";
|
||||
}
|
||||
}
|
||||
|
||||
function messageForEvent(
|
||||
type: OperationType,
|
||||
event: InstanceEventPayload,
|
||||
): string {
|
||||
if (event.message) return event.message;
|
||||
|
||||
switch (event.event) {
|
||||
case "instance.created":
|
||||
return "Created";
|
||||
case "instance.started":
|
||||
return "Starting container";
|
||||
case "instance.restarted":
|
||||
return "Restarting container";
|
||||
case "instance.stopped":
|
||||
return "Stopped";
|
||||
case "instance.deleted":
|
||||
return "Deleted";
|
||||
case "instance.health_changed":
|
||||
if (event.status === "running") return "Running";
|
||||
if (event.status === "unhealthy") return "Unhealthy";
|
||||
return `Status: ${event.status ?? event.event}`;
|
||||
case "instance.error":
|
||||
return event.message ?? "Error";
|
||||
default:
|
||||
return event.message ?? actionLabel(type);
|
||||
}
|
||||
}
|
||||
|
||||
function stepForEvent(event: InstanceEventPayload): number {
|
||||
switch (event.event) {
|
||||
case "instance.created":
|
||||
return 1;
|
||||
case "instance.started":
|
||||
case "instance.restarted":
|
||||
return 2;
|
||||
case "instance.health_changed":
|
||||
if (event.status === "running") return 4;
|
||||
if (event.status === "unhealthy") return 4;
|
||||
return 3;
|
||||
case "instance.error":
|
||||
return 4;
|
||||
case "instance.stopped":
|
||||
return 4;
|
||||
case "instance.deleted":
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionOperationsProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const [operations, setOperations] = useState<Operation[]>([]);
|
||||
|
||||
const startOperation = useCallback(
|
||||
(type: OperationType, instanceId: string, displayName: string): string => {
|
||||
const id = `op-${++operationIdCounter}`;
|
||||
const operation: Operation = {
|
||||
id,
|
||||
type,
|
||||
instanceId,
|
||||
displayName,
|
||||
status: "pending",
|
||||
message: actionLabel(type),
|
||||
step: 0,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
setOperations((prev) => [operation, ...prev].slice(0, 20));
|
||||
return id;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateOperationFromEvent = useCallback(
|
||||
(event: InstanceEventPayload) => {
|
||||
setOperations((prev) => {
|
||||
const matches = prev.filter(
|
||||
(op) => op.instanceId === event.instance_id && op.status !== "success",
|
||||
);
|
||||
if (matches.length === 0) return prev;
|
||||
|
||||
const updated = new Map<string, Operation>();
|
||||
for (const op of prev) updated.set(op.id, op);
|
||||
|
||||
for (const op of matches) {
|
||||
const nextStep = stepForEvent(event);
|
||||
const message = messageForEvent(op.type, event);
|
||||
let nextStatus: OperationStatus = op.status;
|
||||
|
||||
if (event.event === "instance.error") {
|
||||
nextStatus = "error";
|
||||
} else if (
|
||||
event.event === "instance.health_changed" &&
|
||||
event.status === "running"
|
||||
) {
|
||||
nextStatus = "success";
|
||||
} else if (event.event === "instance.deleted") {
|
||||
nextStatus = "success";
|
||||
} else if (event.event === "instance.stopped") {
|
||||
nextStatus = "success";
|
||||
} else if (nextStatus === "pending") {
|
||||
nextStatus = "active";
|
||||
}
|
||||
|
||||
updated.set(op.id, {
|
||||
...op,
|
||||
status: nextStatus,
|
||||
message,
|
||||
step: Math.max(op.step, nextStep),
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(updated.values());
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const completeOperation = useCallback(
|
||||
(
|
||||
instanceId: string,
|
||||
type: OperationType,
|
||||
outcome: "success" | "error",
|
||||
message?: string,
|
||||
) => {
|
||||
setOperations((prev) => {
|
||||
const match = prev.find(
|
||||
(op) => op.instanceId === instanceId && op.type === type,
|
||||
);
|
||||
if (!match) return prev;
|
||||
return prev.map((op) =>
|
||||
op.id === match.id
|
||||
? {
|
||||
...op,
|
||||
status: outcome,
|
||||
message:
|
||||
message ?? (outcome === "success" ? "Done" : "Failed"),
|
||||
step: 4,
|
||||
}
|
||||
: op,
|
||||
);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const dismissOperation = useCallback((id: string) => {
|
||||
setOperations((prev) => prev.filter((op) => op.id !== id));
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
operations,
|
||||
startOperation,
|
||||
updateOperationFromEvent,
|
||||
completeOperation,
|
||||
dismissOperation,
|
||||
}),
|
||||
[
|
||||
operations,
|
||||
startOperation,
|
||||
updateOperationFromEvent,
|
||||
completeOperation,
|
||||
dismissOperation,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<SessionOperationsContext.Provider value={value}>
|
||||
{children}
|
||||
</SessionOperationsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSessionOperations = (): SessionOperationsContextType => {
|
||||
const context = useContext(SessionOperationsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useSessionOperations must be used within a SessionOperationsProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user