import { CheckIcon } from "lucide-react"; import { type ReactNode, useCallback, useMemo, useRef, useState } from "react"; import type { EnvironmentId, ServerProvider } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, type AtomCommandResult, } from "@t3tools/client-runtime/state/runtime"; import { cn } from "~/lib/utils"; import { serverEnvironment } from "~/state/server"; import { useAtomCommand } from "~/state/use-atom-command"; import { useLocalEnvironmentUpdateGroups } from "./ProviderUpdateLaunchNotification.environments"; import { collectProviderUpdateOutcomeSnapshots, firstRejectedProviderUpdateMessage, getProviderUpdateProgressToastView, getProviderUpdateSidebarPillView, isTerminalProviderUpdatePhase, resolveEnvironmentUpdateRowStatus, type LocalEnvironmentUpdateGroup, type LocalProviderUpdateOutcome, type ProviderUpdateRowStatus, type ProviderUpdateRowStatusKind, type ProviderUpdateToastView, } from "./ProviderUpdateLaunchNotification.logic"; import { Button } from "./ui/button"; import { Spinner } from "./ui/spinner"; type ProviderUpdateCommandResult = AtomCommandResult< { readonly providers: ReadonlyArray }, unknown >; /** * Map one targeted instance's update command result into the settled-outcome * shape the multi-backend reducers consume: a non-interrupted failure becomes a * rejection carrying its message; a success carries the post-update snapshot of * the targeted instance (null when the backend did not report it). */ function toProviderUpdateOutcome(input: { readonly environmentId: EnvironmentId; readonly isPrimary: boolean; readonly target: { readonly driver: ServerProvider["driver"]; readonly instanceId: ServerProvider["instanceId"]; }; readonly result: ProviderUpdateCommandResult; }): PromiseSettledResult { if (input.result._tag === "Failure") { if (isAtomCommandInterrupted(input.result)) { // An interrupted dispatch (e.g. superseded) is neither a success nor a // hard failure — surface it as a non-contributing, non-rejecting outcome. return { status: "fulfilled", value: { environmentId: input.environmentId, isPrimary: input.isPrimary, driver: input.target.driver, instanceId: input.target.instanceId, provider: null, }, }; } const error = squashAtomCommandFailure(input.result); return { status: "rejected", reason: error instanceof Error ? error : new Error("Provider update failed."), }; } const provider = input.result.value.providers.find( (candidate) => candidate.instanceId === input.target.instanceId, ) ?? null; return { status: "fulfilled", value: { environmentId: input.environmentId, isPrimary: input.isPrimary, driver: input.target.driver, instanceId: input.target.instanceId, provider, }, }; } // Transport-hang safety net. The dispatch's `finally` clears the spinner and the // in-flight guard on completion, so this only matters if a request never resolves // at all (e.g. the socket drops mid-flight without surfacing an error). Keep it // well beyond the server's own update timeout (5 min) so a legitimately slow // update (npm installs routinely run tens of seconds) is never cut off and left // showing a dead, unresponsive Update button. const PENDING_EXPIRY_MS = 6 * 60_000; function rowToneClass(kind: ProviderUpdateRowStatusKind): string { switch (kind) { case "failed": return "text-destructive"; case "unchanged": return "text-warning"; case "success": return "text-success"; default: return "text-muted-foreground"; } } function EnvironmentUpdateRow({ group, status, onUpdate, }: { readonly group: LocalEnvironmentUpdateGroup; readonly status: ProviderUpdateRowStatus; readonly onUpdate: () => void; }) { let trailing: ReactNode; switch (status.kind) { case "loading": trailing = ; break; case "success": trailing =