Add mermaid diagram and build timeline
This commit is contained in:
parent
73bea35d4c
commit
475b9433ec
2 changed files with 142 additions and 25 deletions
|
|
@ -311,6 +311,9 @@ export const BuildStatus: TypedComponent<BuildStatusAttrs> = {
|
|||
error: null as string | null,
|
||||
partitionStatuses: new Map<string, DashboardPartition>(),
|
||||
buildId: '',
|
||||
mermaidDiagram: null as string | null,
|
||||
mermaidLoading: false,
|
||||
mermaidError: null as string | null,
|
||||
|
||||
oninit(vnode: m.Vnode<BuildStatusAttrs>) {
|
||||
this.buildId = vnode.attrs.id;
|
||||
|
|
@ -351,6 +354,11 @@ export const BuildStatus: TypedComponent<BuildStatusAttrs> = {
|
|||
}
|
||||
}
|
||||
|
||||
// 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<BuildStatusAttrs> = {
|
|||
}
|
||||
},
|
||||
|
||||
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<BuildStatusAttrs> = {
|
|||
|
||||
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<BuildStatusAttrs> = {
|
|||
]),
|
||||
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<BuildStatusAttrs> = {
|
|||
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.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 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')
|
||||
]),
|
||||
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))
|
||||
])
|
||||
])
|
||||
].filter(Boolean))
|
||||
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'
|
||||
})
|
||||
]),
|
||||
m('span', this.mermaidError),
|
||||
m('div', [
|
||||
m('button.btn.btn-sm.btn-outline', {
|
||||
onclick: () => this.loadMermaidDiagram()
|
||||
}, 'Retry')
|
||||
])
|
||||
]) :
|
||||
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')
|
||||
])
|
||||
])
|
||||
])
|
||||
|
|
|
|||
|
|
@ -320,6 +320,32 @@ export class DashboardService {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getMermaidDiagram(buildId: string): Promise<string | null> {
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue