@@ -86,6 +86,148 @@ export const Dashboard = Shade<DashboardProps>({
8686 }
8787
8888 if ( ! props . stackName ) {
89+ const allDefsState = useCollectionSync ( options , ServiceDefinition , { } )
90+ const allDefs =
91+ allDefsState . status === 'synced' || allDefsState . status === 'cached' ? allDefsState . data . entries : [ ]
92+
93+ const allStatusesState = useCollectionSync ( options , ServiceStatus , { } )
94+ const allStatuses =
95+ allStatusesState . status === 'synced' || allStatusesState . status === 'cached'
96+ ? allStatusesState . data . entries
97+ : [ ]
98+
99+ const allConfigsState = useCollectionSync ( options , ServiceConfig , { } )
100+ const allConfigs =
101+ allConfigsState . status === 'synced' || allConfigsState . status === 'cached' ? allConfigsState . data . entries : [ ]
102+
103+ const allGitState = useCollectionSync ( options , ServiceGitStatus , { } )
104+ const allGitStatuses =
105+ allGitState . status === 'synced' || allGitState . status === 'cached' ? allGitState . data . entries : [ ]
106+
107+ const allPrereqsState = useCollectionSync ( options , Prerequisite , { } )
108+ const allPrereqs =
109+ allPrereqsState . status === 'synced' || allPrereqsState . status === 'cached' ? allPrereqsState . data . entries : [ ]
110+
111+ const allCheckResultsState = useCollectionSync ( options , PrerequisiteCheckResult , { } )
112+ const allCheckResults =
113+ allCheckResultsState . status === 'synced' || allCheckResultsState . status === 'cached'
114+ ? allCheckResultsState . data . entries
115+ : [ ]
116+
117+ const allStatusMap = new Map ( allStatuses . map ( ( s ) => [ s . serviceId , s ] ) )
118+ const allConfigMap = new Map ( allConfigs . map ( ( c ) => [ c . serviceId , c ] ) )
119+ const allGitMap = new Map ( allGitStatuses . map ( ( g ) => [ g . serviceId , g ] ) )
120+ const allCheckMap = new Map ( allCheckResults . map ( ( r ) => [ r . prerequisiteId , r ] ) )
121+
122+ const allServices : ServiceView [ ] = allDefs . map ( ( def ) => ( {
123+ serviceId : def . id ,
124+ autoFetchEnabled : false ,
125+ autoFetchIntervalMinutes : 60 ,
126+ autoRestartOnFetch : false ,
127+ environmentVariableOverrides : { } ,
128+ localFiles : [ ] ,
129+ cloneStatus : 'not-cloned' as const ,
130+ installStatus : 'not-installed' as const ,
131+ buildStatus : 'not-built' as const ,
132+ runStatus : 'stopped' as const ,
133+ ...def ,
134+ ...( allConfigMap . get ( def . id ) ?? { } ) ,
135+ ...( allStatusMap . get ( def . id ) ?? { } ) ,
136+ ...( allGitMap . get ( def . id ) ?? { } ) ,
137+ } ) )
138+
139+ const servicesByStack = new Map < string , ServiceView [ ] > ( )
140+ for ( const svc of allServices ) {
141+ const list = servicesByStack . get ( svc . stackName ) ?? [ ]
142+ list . push ( svc )
143+ servicesByStack . set ( svc . stackName , list )
144+ }
145+
146+ const prereqsByStack = new Map < string , typeof allPrereqs > ( )
147+ for ( const prereq of allPrereqs ) {
148+ const list = prereqsByStack . get ( prereq . stackName ) ?? [ ]
149+ list . push ( prereq )
150+ prereqsByStack . set ( prereq . stackName , list )
151+ }
152+
153+ const globalRunningCount = allServices . filter ( ( s ) => s . runStatus === 'running' ) . length
154+ const globalStoppedCount = allServices . filter ( ( s ) => s . runStatus === 'stopped' ) . length
155+ const globalErrorCount = allServices . filter ( ( s ) => s . runStatus === 'error' ) . length
156+ const globalClonedCount = allServices . filter ( ( s ) => s . repositoryId && s . cloneStatus === 'cloned' ) . length
157+
158+ const [ isStartingAll , setIsStartingAll ] = useState ( 'globalIsStartingAll' , false )
159+ const [ isStoppingAll , setIsStoppingAll ] = useState ( 'globalIsStoppingAll' , false )
160+ const [ isUpdatingAll , setIsUpdatingAll ] = useState ( 'globalIsUpdatingAll' , false )
161+
162+ const api = injector . getInstance ( ServicesApiClient )
163+ const noty = injector . getInstance ( NotyService )
164+
165+ const triggerGlobalStartAll = async ( ) => {
166+ setIsStartingAll ( true )
167+ const failures : string [ ] = [ ]
168+ for ( const svc of allServices ) {
169+ if ( isServiceReady ( svc ) && svc . runStatus === 'stopped' ) {
170+ try {
171+ await api . call ( { method : 'POST' , action : '/services/:id/start' , url : { id : svc . id } } )
172+ } catch {
173+ failures . push ( svc . displayName )
174+ }
175+ }
176+ }
177+ if ( failures . length > 0 ) {
178+ noty . emit ( 'onNotyAdded' , {
179+ title : 'Start failed' ,
180+ body : `Failed for: ${ failures . join ( ', ' ) } ` ,
181+ type : 'error' ,
182+ } )
183+ }
184+ setIsStartingAll ( false )
185+ }
186+
187+ const triggerGlobalStopAll = async ( ) => {
188+ setIsStoppingAll ( true )
189+ const failures : string [ ] = [ ]
190+ for ( const svc of allServices ) {
191+ if ( svc . runStatus === 'running' ) {
192+ try {
193+ await api . call ( { method : 'POST' , action : '/services/:id/stop' , url : { id : svc . id } } )
194+ } catch {
195+ failures . push ( svc . displayName )
196+ }
197+ }
198+ }
199+ if ( failures . length > 0 ) {
200+ noty . emit ( 'onNotyAdded' , {
201+ title : 'Stop failed' ,
202+ body : `Failed for: ${ failures . join ( ', ' ) } ` ,
203+ type : 'error' ,
204+ } )
205+ }
206+ setIsStoppingAll ( false )
207+ }
208+
209+ const triggerGlobalUpdateAll = async ( ) => {
210+ setIsUpdatingAll ( true )
211+ const failures : string [ ] = [ ]
212+ for ( const svc of allServices ) {
213+ if ( svc . repositoryId && svc . cloneStatus === 'cloned' ) {
214+ try {
215+ await api . call ( { method : 'POST' , action : '/services/:id/update' , url : { id : svc . id } } )
216+ } catch {
217+ failures . push ( svc . displayName )
218+ }
219+ }
220+ }
221+ if ( failures . length > 0 ) {
222+ noty . emit ( 'onNotyAdded' , {
223+ title : 'Update failed' ,
224+ body : `Failed for: ${ failures . join ( ', ' ) } ` ,
225+ type : 'error' ,
226+ } )
227+ }
228+ setIsUpdatingAll ( false )
229+ }
230+
89231 return (
90232 < PageContainer >
91233 < PageHeader
@@ -97,7 +239,42 @@ export const Dashboard = Shade<DashboardProps>({
97239 : 'No stacks yet. Create a stack to start managing your services.'
98240 }
99241 actions = {
100- < div style = { { display : 'flex' , gap : '8px' } } >
242+ < div style = { { display : 'flex' , gap : '8px' , flexWrap : 'wrap' } } >
243+ { allServices . length > 0 ? (
244+ < div style = { { display : 'contents' } } >
245+ < Button
246+ variant = "contained"
247+ size = "small"
248+ color = "success"
249+ disabled = { globalStoppedCount === 0 && globalErrorCount === 0 }
250+ loading = { isStartingAll }
251+ onclick = { ( ) => void triggerGlobalStartAll ( ) }
252+ startIcon = { < Icon icon = { icons . play } size = "small" /> }
253+ >
254+ Start All
255+ </ Button >
256+ < Button
257+ variant = "outlined"
258+ size = "small"
259+ disabled = { globalRunningCount === 0 }
260+ loading = { isStoppingAll }
261+ onclick = { ( ) => void triggerGlobalStopAll ( ) }
262+ startIcon = { < Icon icon = { icons . stopCircle } size = "small" /> }
263+ >
264+ Stop All
265+ </ Button >
266+ < Button
267+ variant = "outlined"
268+ size = "small"
269+ disabled = { globalClonedCount === 0 }
270+ loading = { isUpdatingAll }
271+ onclick = { ( ) => void triggerGlobalUpdateAll ( ) }
272+ startIcon = { < Icon icon = { icons . download } size = "small" /> }
273+ >
274+ Update All
275+ </ Button >
276+ </ div >
277+ ) : null }
101278 < StackCraftNestedRouteLink href = "/stacks/create" >
102279 < Button variant = "contained" size = "small" startIcon = { < Icon icon = { icons . plus } size = "small" /> } >
103280 Create Stack
@@ -111,26 +288,123 @@ export const Dashboard = Shade<DashboardProps>({
111288 </ div >
112289 }
113290 />
114- { stacks . map ( ( stack ) => (
115- < StackCraftNestedRouteLink
116- href = "/stacks/:stackName"
117- params = { { stackName : stack . name } }
118- style = { { textDecoration : 'none' , color : 'inherit' } }
119- >
120- < Card variant = "outlined" clickable >
121- < CardHeader
122- title = { stack . displayName }
123- avatar = { < Icon icon = { icons . layers } /> }
124- action = { < Icon icon = { icons . chevronRight } size = "small" /> }
125- />
126- { stack . description ? (
127- < CardContent >
128- < MarkdownDisplay content = { stack . description } />
129- </ CardContent >
130- ) : null }
131- </ Card >
132- </ StackCraftNestedRouteLink >
133- ) ) }
291+ < div
292+ style = { {
293+ display : 'grid' ,
294+ gridTemplateColumns : 'repeat(auto-fit, minmax(400px, 1fr))' ,
295+ gap : '16px' ,
296+ } }
297+ >
298+ { stacks . map ( ( stack ) => {
299+ const stackServices = servicesByStack . get ( stack . name ) ?? [ ]
300+ const stackPrereqs = prereqsByStack . get ( stack . name ) ?? [ ]
301+
302+ const running = stackServices . filter ( ( s ) => s . runStatus === 'running' ) . length
303+ const stopped = stackServices . filter ( ( s ) => s . runStatus === 'stopped' ) . length
304+ const errored = stackServices . filter ( ( s ) => s . runStatus === 'error' ) . length
305+ const starting = stackServices . filter ( ( s ) => s . runStatus === 'starting' ) . length
306+ const stopping = stackServices . filter ( ( s ) => s . runStatus === 'stopping' ) . length
307+
308+ const satisfied = stackPrereqs . filter ( ( p ) => allCheckMap . get ( p . id ) ?. status === 'satisfied' ) . length
309+ const failed = stackPrereqs . filter ( ( p ) => allCheckMap . get ( p . id ) ?. status === 'failed' ) . length
310+ const unchecked = stackPrereqs . length - satisfied - failed
311+
312+ return (
313+ < StackCraftNestedRouteLink
314+ href = "/stacks/:stackName"
315+ params = { { stackName : stack . name } }
316+ style = { { textDecoration : 'none' , color : 'inherit' } }
317+ >
318+ < Card variant = "outlined" clickable style = { { height : '100%' } } >
319+ < CardHeader
320+ title = { stack . displayName }
321+ avatar = { < Icon icon = { icons . layers } /> }
322+ action = { < Icon icon = { icons . chevronRight } size = "small" /> }
323+ />
324+ < CardContent >
325+ { stackServices . length > 0 ? (
326+ < div style = { { display : 'flex' , gap : '6px' , flexWrap : 'wrap' , marginBottom : '8px' } } >
327+ < Chip variant = "outlined" size = "small" color = "secondary" >
328+ { stackServices . length } service{ stackServices . length !== 1 ? 's' : '' }
329+ </ Chip >
330+ { running > 0 ? (
331+ < Chip variant = "outlined" size = "small" color = "success" >
332+ { running } running
333+ </ Chip >
334+ ) : null }
335+ { starting > 0 ? (
336+ < Chip variant = "outlined" size = "small" color = "warning" >
337+ { starting } starting
338+ </ Chip >
339+ ) : null }
340+ { stopping > 0 ? (
341+ < Chip variant = "outlined" size = "small" color = "warning" >
342+ { stopping } stopping
343+ </ Chip >
344+ ) : null }
345+ { stopped > 0 ? (
346+ < Chip variant = "outlined" size = "small" color = "secondary" >
347+ { stopped } stopped
348+ </ Chip >
349+ ) : null }
350+ { errored > 0 ? (
351+ < Chip variant = "outlined" size = "small" color = "error" >
352+ { errored } error
353+ </ Chip >
354+ ) : null }
355+ </ div >
356+ ) : (
357+ < div
358+ style = { {
359+ fontSize : cssVariableTheme . typography . fontSize . sm ,
360+ color : cssVariableTheme . text . secondary ,
361+ marginBottom : '8px' ,
362+ } }
363+ >
364+ No services yet
365+ </ div >
366+ ) }
367+ { stackPrereqs . length > 0 ? (
368+ < div style = { { display : 'flex' , gap : '6px' , flexWrap : 'wrap' } } >
369+ { satisfied > 0 ? (
370+ < Chip variant = "outlined" size = "small" color = "success" >
371+ { satisfied } satisfied
372+ </ Chip >
373+ ) : null }
374+ { failed > 0 ? (
375+ < Chip variant = "outlined" size = "small" color = "error" >
376+ { failed } failed
377+ </ Chip >
378+ ) : null }
379+ { unchecked > 0 ? (
380+ < Chip variant = "outlined" size = "small" color = "secondary" >
381+ { unchecked } unchecked
382+ </ Chip >
383+ ) : null }
384+ </ div >
385+ ) : null }
386+ { stack . description ? (
387+ < div
388+ style = { {
389+ marginTop : '8px' ,
390+ fontSize : cssVariableTheme . typography . fontSize . sm ,
391+ color : cssVariableTheme . text . secondary ,
392+ overflow : 'hidden' ,
393+ textOverflow : 'ellipsis' ,
394+ display : '-webkit-box' ,
395+ webkitLineClamp : '2' ,
396+ webkitBoxOrient : 'vertical' ,
397+ } }
398+ >
399+ < MarkdownDisplay content = { stack . description } />
400+ </ div >
401+ ) : null }
402+ </ CardContent >
403+ </ Card >
404+ </ StackCraftNestedRouteLink >
405+ )
406+ } ) }
407+ </ div >
134408 </ PageContainer >
135409 )
136410 }
0 commit comments