diff --git a/databuild/dashboard/pages.ts b/databuild/dashboard/pages.ts index 904df60..6f4e432 100644 --- a/databuild/dashboard/pages.ts +++ b/databuild/dashboard/pages.ts @@ -311,6 +311,9 @@ export const BuildStatus: TypedComponent = { error: null as string | null, partitionStatuses: new Map(), buildId: '', + mermaidDiagram: null as string | null, + mermaidLoading: false, + mermaidError: null as string | null, oninit(vnode: m.Vnode) { this.buildId = vnode.attrs.id; @@ -351,6 +354,11 @@ export const BuildStatus: TypedComponent = { } } + // Load mermaid diagram if we don't have it yet + if (!this.mermaidDiagram && !this.mermaidLoading) { + this.loadMermaidDiagram(); + } + this.loading = false; m.redraw(); } catch (error) { @@ -361,6 +369,37 @@ export const BuildStatus: TypedComponent = { } }, + async loadMermaidDiagram() { + try { + this.mermaidLoading = true; + this.mermaidError = null; + m.redraw(); + + const service = DashboardService.getInstance(); + const diagram = await service.getMermaidDiagram(this.buildId); + + if (diagram) { + this.mermaidDiagram = diagram; + // Trigger mermaid to render the diagram after the DOM updates + setTimeout(() => { + if (typeof window !== 'undefined' && (window as any).mermaid) { + (window as any).mermaid.init(); + } + }, 100); + } else { + this.mermaidError = 'No job graph available for this build'; + } + + this.mermaidLoading = false; + m.redraw(); + } catch (error) { + console.error('Failed to load mermaid diagram:', error); + this.mermaidError = error instanceof Error ? error.message : 'Failed to load diagram'; + this.mermaidLoading = false; + m.redraw(); + } + }, + startPolling() { // Use different poll intervals based on build status const isActive = this.data?.status_name === 'EXECUTING' || @@ -414,6 +453,15 @@ export const BuildStatus: TypedComponent = { const build = this.data; + const timelineData = [ + {stage: 'Build Requested', time: build.requested_at, icon: '🕚'}, + ...(build.started_at ? [{stage: 'Build Started', time: build.started_at, icon: '▶️'}] : []), + // ...(this.data.events as BuildEvent[]).filter(ev => ev.job_event !== null).map((ev) => ({ + // stage: ev.job_event.status_name, time: ev.timestamp, icon: '🙃' + // })), + ...(build.completed_at ? [{stage: 'Build Completed', time: build.completed_at, icon: '✅'}] : []), + ]; + return m('div.container.mx-auto.p-4', [ // Build Header m('.build-header.mb-6', [ @@ -432,12 +480,12 @@ export const BuildStatus: TypedComponent = { ]), m('.stat.bg-base-100.shadow.rounded-lg.p-4', [ m('.stat-title', 'Jobs'), - m('.stat-value.text-2xl', `${build.completed_jobs}/${build.total_jobs}`), + m('.stat-value.text-2xl', `${build.completed_jobs}`), m('.stat-desc', 'completed') ]), m('.stat.bg-base-100.shadow.rounded-lg.p-4', [ m('.stat-title', 'Duration'), - m('.stat-value.text-2xl', build.duration_ms ? formatDuration(build.duration_ms) : '—'), + m('.stat-value.text-2xl', (build.completed_at - build.started_at) ? formatDuration((build.completed_at - build.started_at)) : '—'), m('.stat-desc', build.started_at ? formatDateTime(build.started_at) : 'Not started') ]) ]) @@ -504,31 +552,74 @@ export const BuildStatus: TypedComponent = { m('.metric-label.text-sm.opacity-60', 'Total Jobs') ]) ]), - m('.build-timeline.mt-6', [ - m('.timeline.text-sm', [ - m('.timeline-item', [ - m('.timeline-marker.text-primary', '●'), - m('.timeline-content', [ - m('.font-medium', 'Requested'), - m('.opacity-60', formatDateTime(build.requested_at)) - ]) + + m('ul.timeline.mx-auto',timelineData.map((item) => { + return m('li', [ + ...(item.stage === 'Build Requested' ? [] : [m("hr")]), + m('div.font-medium.timeline-middle', item.icon), + m("div.timeline-box.timeline-end", { + style: { + '--timeline-color': item.stage === 'Requested' ? '#1976d2' : '#6b7280' + } + }, [ + m('span.flex.flex-col.gap-2', { + class: 'timeline-point', + }, [ + m('.font-medium', item.stage), + m('.text-xs.opacity-60', formatDateTime(item.time)), + ]) + ] + ), + ...(item.stage === 'Build Completed' ? [] : [m("hr")]), + ] + ) + + })), + + ]) + ]), + + // Build Graph Visualization + m('.build-graph.card.bg-base-100.shadow-xl', [ + m('.card-body', [ + m('.flex.justify-between.items-center.mb-4', [ + m('h2.card-title.text-xl', 'Build Graph'), + this.mermaidDiagram && m('button.btn.btn-sm.btn-outline', { + onclick: () => this.loadMermaidDiagram() + }, 'Refresh Diagram') + ]), + this.mermaidLoading ? + m('.flex.justify-center.items-center.h-32', [ + m('span.loading.loading-spinner.loading-lg'), + m('span.ml-4', 'Loading diagram...') + ]) : + this.mermaidError ? + m('.alert.alert-warning', [ + m('svg.stroke-current.shrink-0.h-6.w-6', { + fill: 'none', + viewBox: '0 0 24 24' + }, [ + m('path', { + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': '2', + 'd': 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z' + }) ]), - build.started_at && m('.timeline-item', [ - m('.timeline-marker.text-info', '●'), - m('.timeline-content', [ - m('.font-medium', 'Started'), - m('.opacity-60', formatDateTime(build.started_at)) - ]) - ]), - build.completed_at && m('.timeline-item', [ - m('.timeline-marker.text-success', '●'), - m('.timeline-content', [ - m('.font-medium', 'Completed'), - m('.opacity-60', formatDateTime(build.completed_at)) - ]) + m('span', this.mermaidError), + m('div', [ + m('button.btn.btn-sm.btn-outline', { + onclick: () => this.loadMermaidDiagram() + }, 'Retry') ]) - ].filter(Boolean)) - ]) + ]) : + this.mermaidDiagram ? + m('.mermaid-container.w-full.overflow-x-auto', [ + m('pre.mermaid.text-center', { + key: `mermaid-${this.buildId}` // Force re-render when buildId changes + }, this.mermaidDiagram) + ]) : + m('.text-center.py-8.text-base-content.opacity-60', 'No graph visualization available') ]) ]) ]) diff --git a/databuild/dashboard/services.ts b/databuild/dashboard/services.ts index 51a4c39..d08aa94 100644 --- a/databuild/dashboard/services.ts +++ b/databuild/dashboard/services.ts @@ -320,6 +320,32 @@ export class DashboardService { return null; } } + + async getMermaidDiagram(buildId: string): Promise { + try { + const url = `/api/v1/builds/${buildId}/mermaid`; + + const response = await fetch(url); + if (!response.ok) { + if (response.status === 404) { + return null; // Build not found or no job graph + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + // Validate response structure + if (typeof data === 'object' && data !== null && 'mermaid' in data && typeof data.mermaid === 'string') { + return data.mermaid; + } + + throw new Error('Invalid mermaid response structure'); + } catch (error) { + console.error('Failed to fetch mermaid diagram:', error); + return null; + } + } } // Polling manager with Page Visibility API integration