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,
|
error: null as string | null,
|
||||||
partitionStatuses: new Map<string, DashboardPartition>(),
|
partitionStatuses: new Map<string, DashboardPartition>(),
|
||||||
buildId: '',
|
buildId: '',
|
||||||
|
mermaidDiagram: null as string | null,
|
||||||
|
mermaidLoading: false,
|
||||||
|
mermaidError: null as string | null,
|
||||||
|
|
||||||
oninit(vnode: m.Vnode<BuildStatusAttrs>) {
|
oninit(vnode: m.Vnode<BuildStatusAttrs>) {
|
||||||
this.buildId = vnode.attrs.id;
|
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;
|
this.loading = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
} catch (error) {
|
} 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() {
|
startPolling() {
|
||||||
// Use different poll intervals based on build status
|
// Use different poll intervals based on build status
|
||||||
const isActive = this.data?.status_name === 'EXECUTING' ||
|
const isActive = this.data?.status_name === 'EXECUTING' ||
|
||||||
|
|
@ -414,6 +453,15 @@ export const BuildStatus: TypedComponent<BuildStatusAttrs> = {
|
||||||
|
|
||||||
const build = this.data;
|
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', [
|
return m('div.container.mx-auto.p-4', [
|
||||||
// Build Header
|
// Build Header
|
||||||
m('.build-header.mb-6', [
|
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.bg-base-100.shadow.rounded-lg.p-4', [
|
||||||
m('.stat-title', 'Jobs'),
|
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-desc', 'completed')
|
||||||
]),
|
]),
|
||||||
m('.stat.bg-base-100.shadow.rounded-lg.p-4', [
|
m('.stat.bg-base-100.shadow.rounded-lg.p-4', [
|
||||||
m('.stat-title', 'Duration'),
|
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')
|
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('.metric-label.text-sm.opacity-60', 'Total Jobs')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
m('.build-timeline.mt-6', [
|
|
||||||
m('.timeline.text-sm', [
|
m('ul.timeline.mx-auto',timelineData.map((item) => {
|
||||||
m('.timeline-item', [
|
return m('li', [
|
||||||
m('.timeline-marker.text-primary', '●'),
|
...(item.stage === 'Build Requested' ? [] : [m("hr")]),
|
||||||
m('.timeline-content', [
|
m('div.font-medium.timeline-middle', item.icon),
|
||||||
m('.font-medium', 'Requested'),
|
m("div.timeline-box.timeline-end", {
|
||||||
m('.opacity-60', formatDateTime(build.requested_at))
|
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', '●'),
|
// Build Graph Visualization
|
||||||
m('.timeline-content', [
|
m('.build-graph.card.bg-base-100.shadow-xl', [
|
||||||
m('.font-medium', 'Started'),
|
m('.card-body', [
|
||||||
m('.opacity-60', formatDateTime(build.started_at))
|
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', [
|
this.mermaidLoading ?
|
||||||
m('.timeline-marker.text-success', '●'),
|
m('.flex.justify-center.items-center.h-32', [
|
||||||
m('.timeline-content', [
|
m('span.loading.loading-spinner.loading-lg'),
|
||||||
m('.font-medium', 'Completed'),
|
m('span.ml-4', 'Loading diagram...')
|
||||||
m('.opacity-60', formatDateTime(build.completed_at))
|
]) :
|
||||||
])
|
this.mermaidError ?
|
||||||
])
|
m('.alert.alert-warning', [
|
||||||
].filter(Boolean))
|
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;
|
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
|
// Polling manager with Page Visibility API integration
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue