Skip to content

Commit 356d301

Browse files
committed
main dashboard refactor
1 parent 40788eb commit 356d301

File tree

1 file changed

+295
-21
lines changed

1 file changed

+295
-21
lines changed

frontend/src/pages/dashboard/index.tsx

Lines changed: 295 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)