11import { useAtomValue } from "@effect/atom-react" ;
22import { EnvironmentId } from "@t3tools/contracts" ;
33import { Atom } from "effect/reactivity" ;
4+ import { ChevronRightIcon } from "lucide-react" ;
5+ import { Fragment , type RefObject , useRef } from "react" ;
46
57import { environmentThreadShells } from "~/state/threads" ;
68import {
@@ -13,7 +15,10 @@ import {
1315 AlertDialogTitle ,
1416} from "../ui/alert-dialog" ;
1517import { Button } from "../ui/button" ;
16- import type { RestartingMachine , SkippedMachine } from "./updateEverything.logic" ;
18+ import { Collapsible , CollapsiblePanel , CollapsibleTrigger } from "../ui/collapsible" ;
19+ import { MiddleTruncate } from "../ui/middle-truncate" ;
20+ import { ScrollArea } from "../ui/scroll-area" ;
21+ import type { RestartingMachine , UpdateEverythingRow } from "./updateEverything.logic" ;
1722
1823const isRunning = ( status : string ) =>
1924 status === "preparing" || status === "starting" || status === "running" ;
@@ -37,35 +42,54 @@ const runningThreadCountAtom = Atom.family((environmentIds: string) =>
3742interface UpdateEverythingDialogProps {
3843 readonly open : boolean ;
3944 readonly onOpenChange : ( open : boolean ) => void ;
40- readonly summary : string ;
41- readonly lines : ReadonlyArray < string > ;
45+ readonly version : string | null ;
46+ readonly providerCount : number ;
47+ readonly rows : ReadonlyArray < UpdateEverythingRow > ;
4248 readonly restarts : ReadonlyArray < RestartingMachine > ;
43- readonly skipped : ReadonlyArray < SkippedMachine > ;
4449 readonly onConfirm : ( ) => void ;
4550}
4651
52+ type UpdateEverythingDialogContentProps = Omit <
53+ UpdateEverythingDialogProps ,
54+ "open" | "onOpenChange"
55+ > & {
56+ readonly cancelRef : RefObject < HTMLButtonElement | null > ;
57+ } ;
58+
4759export function UpdateEverythingDialog ( {
4860 open,
4961 onOpenChange,
5062 ...content
5163} : UpdateEverythingDialogProps ) {
64+ const cancelRef = useRef < HTMLButtonElement > ( null ) ;
5265 return (
5366 < AlertDialog open = { open } onOpenChange = { onOpenChange } >
5467 { /* Refocusing the trigger after a pointer close shows its focus ring and opens its tooltip. */ }
55- < AlertDialogPopup finalFocus = { ( closeType ) => closeType === "keyboard" } >
56- < UpdateEverythingDialogContent { ...content } />
68+ < AlertDialogPopup
69+ className = "sm:max-w-xl"
70+ initialFocus = { cancelRef }
71+ finalFocus = { ( closeType ) => closeType === "keyboard" }
72+ >
73+ < UpdateEverythingDialogContent { ...content } cancelRef = { cancelRef } />
5774 </ AlertDialogPopup >
5875 </ AlertDialog >
5976 ) ;
6077}
6178
79+ const APP_LABELS = {
80+ "this-app" : "This app" ,
81+ server : "Server" ,
82+ "desktop-app" : "Desktop app" ,
83+ } as const ;
84+
6285function UpdateEverythingDialogContent ( {
63- summary,
64- lines,
86+ version,
87+ providerCount,
88+ rows,
6589 restarts,
66- skipped,
6790 onConfirm,
68- } : Omit < UpdateEverythingDialogProps , "open" | "onOpenChange" > ) {
91+ cancelRef,
92+ } : UpdateEverythingDialogContentProps ) {
6993 const stopped = useAtomValue (
7094 runningThreadCountAtom (
7195 restarts
@@ -84,33 +108,113 @@ function UpdateEverythingDialogContent({
84108 ) ;
85109 const running = stopped + continuing ;
86110 const threads = `${ running } running ${ running === 1 ? "thread" : "threads" } ` ;
111+ const restarting = new Set ( restarts . map ( ( machine ) => machine . environmentId ) ) ;
112+ const updating = rows . filter ( ( row ) => row . app !== null || row . providers . length > 0 ) ;
113+ const skipped = rows . filter ( ( row ) => row . skipped !== null ) ;
87114
88115 return (
89116 < >
90- < AlertDialogHeader >
91- < AlertDialogTitle > { summary } ?</ AlertDialogTitle >
92- < AlertDialogDescription className = "whitespace-pre-line" >
93- { lines . join ( "\n" ) }
117+ < AlertDialogHeader className = "shrink-0" >
118+ < AlertDialogTitle >
119+ Update { updating . length } { updating . length === 1 ? "machine" : "machines" } ?
120+ </ AlertDialogTitle >
121+ < AlertDialogDescription >
122+ { version !== null ? (
123+ < >
124+ T3 Code < span className = "font-mono text-xs" > { version } </ span >
125+ </ >
126+ ) : null }
127+ { version !== null && providerCount > 0 ? " · " : null }
128+ { providerCount > 0
129+ ? `${ providerCount } provider ${ providerCount === 1 ? "update" : "updates" } `
130+ : null }
94131 </ AlertDialogDescription >
132+ </ AlertDialogHeader >
133+ < ScrollArea scrollFade >
134+ < div className = "flex flex-col gap-3 px-6 pb-4" >
135+ { /* Machine names truncate first. Provider lists wrap between names, never inside one. */ }
136+ < div className = "grid grid-cols-[minmax(4rem,max-content)_max-content_minmax(min-content,1fr)_max-content] gap-x-4 text-sm" >
137+ < div className = "col-span-full grid grid-cols-subgrid border-b pb-1.5 text-muted-foreground text-xs" >
138+ < span > Machine</ span >
139+ < span > T3 Code</ span >
140+ < span > Providers</ span >
141+ < span />
142+ </ div >
143+ { updating . map ( ( row ) => (
144+ < div
145+ key = { row . environmentId ?? "this-app" }
146+ className = "col-span-full grid grid-cols-subgrid border-b border-border/50 py-1.5 last:border-b-0"
147+ >
148+ < span className = "max-w-32 min-w-0" >
149+ < MiddleTruncate value = { row . label } />
150+ </ span >
151+ < span >
152+ { row . app !== null ? APP_LABELS [ row . app ] : null }
153+ { row . app === null && row . skipped !== null ? (
154+ < span className = "text-muted-foreground" > Skipped</ span >
155+ ) : null }
156+ { row . app === "this-app" ? (
157+ < span className = "block text-muted-foreground text-xs" > restarts last</ span >
158+ ) : null }
159+ </ span >
160+ < span >
161+ { row . providers . map ( ( provider , index ) => (
162+ < Fragment key = { provider } >
163+ < span className = "whitespace-nowrap" > { provider } </ span >
164+ { index < row . providers . length - 1 ? ", " : null }
165+ </ Fragment >
166+ ) ) }
167+ </ span >
168+ < span className = "text-right" >
169+ { row . environmentId !== null && restarting . has ( row . environmentId ) ? (
170+ < RunningThreads environmentId = { row . environmentId } />
171+ ) : null }
172+ </ span >
173+ </ div >
174+ ) ) }
175+ </ div >
176+ { skipped . length > 0 ? (
177+ < Collapsible >
178+ < CollapsibleTrigger className = "flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground data-panel-open:[&_svg]:rotate-90" >
179+ < ChevronRightIcon className = "size-3.5 transition-transform" aria-hidden />
180+ { skipped . length } skipped
181+ </ CollapsibleTrigger >
182+ < CollapsiblePanel >
183+ < div className = "grid grid-cols-[minmax(0,max-content)_1fr] gap-x-3 gap-y-0.5 pt-1.5 ps-4.5 text-muted-foreground text-xs" >
184+ { skipped . map ( ( row ) => (
185+ < Fragment key = { row . environmentId ?? "this-app" } >
186+ < span className = "max-w-32 min-w-0" >
187+ < MiddleTruncate value = { row . label } />
188+ </ span >
189+ < span className = "wrap-anywhere" > { row . skipped } </ span >
190+ </ Fragment >
191+ ) ) }
192+ </ div >
193+ </ CollapsiblePanel >
194+ </ Collapsible >
195+ ) : null }
196+ </ div >
197+ </ ScrollArea >
198+ < AlertDialogFooter >
95199 { running > 0 ? (
96- < p className = "pt-2 text-sm " >
200+ < p className = "self-center text-muted-foreground text-sm max-sm:order-1 max-sm:text-center sm:mr-auto " >
97201 { stopped === 0
98202 ? `${ threads } will be interrupted and can resume after the restart.`
99203 : continuing === 0
100204 ? `${ threads } will be interrupted.`
101205 : `${ threads } will be interrupted. ${ continuing } can resume after the restart.` }
102206 </ p >
103207 ) : null }
104- { skipped . length > 0 ? (
105- < p className = "whitespace-pre-line pt-2 text-sm" >
106- { skipped . map ( ( machine ) => `Skips ${ machine . label } : ${ machine . reason } ` ) . join ( "\n" ) }
107- </ p >
108- ) : null }
109- </ AlertDialogHeader >
110- < AlertDialogFooter >
111- < AlertDialogClose render = { < Button variant = "outline" /> } > Cancel</ AlertDialogClose >
208+ < AlertDialogClose ref = { cancelRef } render = { < Button variant = "outline" /> } >
209+ Cancel
210+ </ AlertDialogClose >
112211 < Button onClick = { onConfirm } > Update</ Button >
113212 </ AlertDialogFooter >
114213 </ >
115214 ) ;
116215}
216+
217+ function RunningThreads ( { environmentId } : { readonly environmentId : EnvironmentId } ) {
218+ const count = useAtomValue ( runningThreadCountAtom ( environmentId ) ) ;
219+ return count > 0 ? < span className = "whitespace-nowrap text-warning" > { count } running</ span > : null ;
220+ }
0 commit comments