1288 lines
No EOL
46 KiB
TypeScript
1288 lines
No EOL
46 KiB
TypeScript
import m from 'mithril';
|
|
import { DashboardService, pollingManager, formatTime, formatDateTime, formatDuration, formatDate, RecentActivitySummary } from './services';
|
|
import { encodePartitionRef, decodePartitionRef, encodeJobLabel, decodeJobLabel, BuildStatusBadge, PartitionStatusBadge, EventTypeBadge } from './utils';
|
|
|
|
// Page scaffold components
|
|
export const RecentActivity = {
|
|
data: null as RecentActivitySummary | null,
|
|
loading: true,
|
|
error: null as string | null,
|
|
pollInterval: null as NodeJS.Timeout | null,
|
|
|
|
loadData() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
m.redraw(); // Redraw to show loading state
|
|
|
|
const service = DashboardService.getInstance();
|
|
|
|
return service.getRecentActivity()
|
|
.then(data => {
|
|
this.data = data;
|
|
this.loading = false;
|
|
m.redraw(); // Explicitly redraw after data loads
|
|
})
|
|
.catch(error => {
|
|
console.error('RecentActivity: Error in loadData:', error);
|
|
this.error = error instanceof Error ? error.message : 'Failed to load data';
|
|
this.loading = false;
|
|
m.redraw(); // Redraw after error
|
|
});
|
|
},
|
|
|
|
oninit() {
|
|
// Load initial data - Mithril will automatically redraw after promise resolves
|
|
this.loadData();
|
|
|
|
// Set up polling for real-time updates (5 second interval)
|
|
if (pollingManager.isVisible()) {
|
|
pollingManager.startPolling('recent-activity', () => {
|
|
this.loadData();
|
|
}, 5000);
|
|
}
|
|
},
|
|
|
|
onremove() {
|
|
// Clean up polling when component is removed
|
|
pollingManager.stopPolling('recent-activity');
|
|
},
|
|
|
|
view: function() {
|
|
|
|
if (this.loading && !this.data) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.flex.flex-col.justify-center.items-center.min-h-96', [
|
|
m('span.loading.loading-spinner.loading-lg'),
|
|
m('span.ml-4.text-lg.mb-4', 'Loading dashboard...'),
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadData()
|
|
}, 'Retry Load')
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (this.error) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.alert.alert-error', [
|
|
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: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
})
|
|
]),
|
|
m('span', this.error),
|
|
m('div', [
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadData()
|
|
}, 'Retry')
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
const data = this.data;
|
|
if (!data) return m('div');
|
|
|
|
return m('div.container.mx-auto.p-4', [
|
|
// Dashboard Header
|
|
m('div.dashboard-header.mb-6', [
|
|
m('div.flex.justify-between.items-center.mb-4', [
|
|
m('h1.text-3xl.font-bold', 'DataBuild Dashboard'),
|
|
m('div.badge.badge-primary.badge-lg', data.graphName)
|
|
]),
|
|
|
|
// Statistics
|
|
m('div.stats.shadow.w-full.bg-base-100', [
|
|
m('div.stat', [
|
|
m('div.stat-figure.text-primary', [
|
|
m('svg.w-8.h-8', {
|
|
fill: 'none',
|
|
stroke: 'currentColor',
|
|
viewBox: '0 0 24 24'
|
|
}, [
|
|
m('path', {
|
|
'stroke-linecap': 'round',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-width': '2',
|
|
d: 'M13 10V3L4 14h7v7l9-11h-7z'
|
|
})
|
|
])
|
|
]),
|
|
m('div.stat-title', 'Active Builds'),
|
|
m('div.stat-value.text-primary', data.activeBuilds),
|
|
m('div.stat-desc', 'Currently running')
|
|
]),
|
|
m('div.stat', [
|
|
m('div.stat-figure.text-secondary', [
|
|
m('svg.w-8.h-8', {
|
|
fill: 'none',
|
|
stroke: 'currentColor',
|
|
viewBox: '0 0 24 24'
|
|
}, [
|
|
m('path', {
|
|
'stroke-linecap': 'round',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-width': '2',
|
|
d: 'M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z'
|
|
})
|
|
])
|
|
]),
|
|
m('div.stat-title', 'Recent Builds'),
|
|
m('div.stat-value.text-secondary', data.recentBuilds.length),
|
|
m('div.stat-desc', 'In the last hour')
|
|
]),
|
|
m('div.stat', [
|
|
m('div.stat-figure.text-accent', [
|
|
m('svg.w-8.h-8', {
|
|
fill: 'none',
|
|
stroke: 'currentColor',
|
|
viewBox: '0 0 24 24'
|
|
}, [
|
|
m('path', {
|
|
'stroke-linecap': 'round',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-width': '2',
|
|
d: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M9 5l8 4'
|
|
})
|
|
])
|
|
]),
|
|
m('div.stat-title', 'Total Partitions'),
|
|
m('div.stat-value.text-accent', data.totalPartitions),
|
|
m('div.stat-desc', 'Managed partitions')
|
|
])
|
|
])
|
|
]),
|
|
|
|
// Dashboard Content Grid
|
|
m('div.dashboard-content.grid.grid-cols-1.lg:grid-cols-2.gap-6', [
|
|
// Recent Build Requests
|
|
m('div.recent-builds.card.bg-base-100.shadow-xl', [
|
|
m('div.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', [
|
|
m('svg.w-6.h-6.mr-2', {
|
|
fill: 'none',
|
|
stroke: 'currentColor',
|
|
viewBox: '0 0 24 24'
|
|
}, [
|
|
m('path', {
|
|
'stroke-linecap': 'round',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-width': '2',
|
|
d: 'M13 10V3L4 14h7v7l9-11h-7z'
|
|
})
|
|
]),
|
|
'Recent Build Requests'
|
|
]),
|
|
data.recentBuilds.length === 0
|
|
? m('div.text-center.py-8.text-base-content.opacity-60', 'No recent builds')
|
|
: m('div.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Build ID'),
|
|
m('th', 'Status'),
|
|
m('th', 'Created'),
|
|
])
|
|
]),
|
|
m('tbody',
|
|
data.recentBuilds.map(build =>
|
|
m('tr.hover', [
|
|
m('td', [
|
|
m('a.link.link-primary.font-mono.text-sm', {
|
|
href: `/builds/${build.id}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/builds/${build.id}`);
|
|
}
|
|
}, build.id)
|
|
]),
|
|
m('td', [
|
|
m(BuildStatusBadge, { status: build.status })
|
|
]),
|
|
m('td.text-sm.opacity-70', formatTime(build.createdAt)),
|
|
])
|
|
)
|
|
)
|
|
])
|
|
])
|
|
])
|
|
]),
|
|
|
|
// Recent Partition Builds
|
|
m('div.recent-partitions.card.bg-base-100.shadow-xl', [
|
|
m('div.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', [
|
|
m('svg.w-6.h-6.mr-2', {
|
|
fill: 'none',
|
|
stroke: 'currentColor',
|
|
viewBox: '0 0 24 24'
|
|
}, [
|
|
m('path', {
|
|
'stroke-linecap': 'round',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-width': '2',
|
|
d: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M9 5l8 4'
|
|
})
|
|
]),
|
|
'Recent Partition Builds'
|
|
]),
|
|
data.recentPartitions.length === 0
|
|
? m('div.text-center.py-8.text-base-content.opacity-60', 'No recent partitions')
|
|
: m('div.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Partition Reference'),
|
|
m('th', 'Status'),
|
|
m('th', 'Updated'),
|
|
])
|
|
]),
|
|
m('tbody',
|
|
data.recentPartitions.map(partition =>
|
|
m('tr.hover', [
|
|
m('td', [
|
|
m('a.link.link-primary.font-mono.text-sm.break-all', {
|
|
href: `/partitions/${encodePartitionRef(partition.ref)}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/partitions/${encodePartitionRef(partition.ref)}`);
|
|
},
|
|
title: partition.ref
|
|
}, partition.ref)
|
|
]),
|
|
m('td', [
|
|
m(PartitionStatusBadge, { status: partition.status })
|
|
]),
|
|
m('td.text-sm.opacity-70', formatTime(partition.updatedAt)),
|
|
])
|
|
)
|
|
)
|
|
])
|
|
])
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
|
|
export const BuildStatus = {
|
|
data: null as any | null,
|
|
loading: true,
|
|
error: null as string | null,
|
|
partitionStatuses: new Map<string, any>(),
|
|
buildId: '',
|
|
|
|
oninit(vnode: any) {
|
|
this.buildId = vnode.attrs.id;
|
|
this.loadBuild();
|
|
this.startPolling();
|
|
},
|
|
|
|
onremove() {
|
|
pollingManager.stopPolling(`build-status-${this.buildId}`);
|
|
},
|
|
|
|
async loadBuild() {
|
|
try {
|
|
this.loading = true;
|
|
this.error = null;
|
|
m.redraw();
|
|
|
|
// Import types dynamically to avoid circular dependencies
|
|
const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index');
|
|
const apiClient = new DefaultApi(new Configuration({ basePath: '' }));
|
|
|
|
// Get build status
|
|
const buildResponse = await apiClient.apiV1BuildsBuildRequestIdGet({ buildRequestId: this.buildId });
|
|
this.data = buildResponse;
|
|
|
|
// Load partition statuses for all requested partitions
|
|
if (buildResponse.requestedPartitions) {
|
|
for (const partitionRef of buildResponse.requestedPartitions) {
|
|
try {
|
|
const partitionStatus = await apiClient.apiV1PartitionsRefStatusGet({
|
|
ref: partitionRef
|
|
});
|
|
this.partitionStatuses.set(partitionRef, partitionStatus);
|
|
} catch (e) {
|
|
console.warn(`Failed to load status for partition ${partitionRef}:`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.loading = false;
|
|
m.redraw();
|
|
} catch (error) {
|
|
console.error('Failed to load build:', error);
|
|
this.error = error instanceof Error ? error.message : 'Failed to load build';
|
|
this.loading = false;
|
|
m.redraw();
|
|
}
|
|
},
|
|
|
|
startPolling() {
|
|
// Use different poll intervals based on build status
|
|
const isActive = this.data?.status === 'BuildRequestExecuting' ||
|
|
this.data?.status === 'BuildRequestPlanning';
|
|
const interval = isActive ? 2000 : 10000; // 2s for active, 10s for completed
|
|
|
|
pollingManager.startPolling(`build-status-${this.buildId}`, () => {
|
|
this.loadBuild();
|
|
}, interval);
|
|
},
|
|
|
|
|
|
getEventLink(event: any): { href: string; text: string } | null {
|
|
const eventType = event.eventType;
|
|
|
|
switch (eventType) {
|
|
case 'job':
|
|
if (event.jobLabel) {
|
|
return {
|
|
href: `/jobs/${encodeURIComponent(event.jobLabel)}`,
|
|
text: 'Job Details'
|
|
};
|
|
}
|
|
return null;
|
|
case 'partition':
|
|
if (event.partitionRef) {
|
|
return {
|
|
href: `/partitions/${encodePartitionRef(event.partitionRef)}`,
|
|
text: 'Partition Status'
|
|
};
|
|
}
|
|
return null;
|
|
case 'delegation':
|
|
if (event.delegatedBuildId) {
|
|
return {
|
|
href: `/builds/${event.delegatedBuildId}`,
|
|
text: 'Delegated Build'
|
|
};
|
|
}
|
|
return null;
|
|
case 'build_request':
|
|
// Self-referential, no additional link needed
|
|
return null;
|
|
default:
|
|
return null;
|
|
}
|
|
},
|
|
|
|
view() {
|
|
// Loading/error states similar to RecentActivity component
|
|
if (this.loading && !this.data) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.flex.flex-col.justify-center.items-center.min-h-96', [
|
|
m('span.loading.loading-spinner.loading-lg'),
|
|
m('span.ml-4.text-lg.mb-4', 'Loading build status...'),
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadBuild()
|
|
}, 'Retry Load')
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (this.error) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.alert.alert-error', [
|
|
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: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
})
|
|
]),
|
|
m('span', this.error),
|
|
m('div', [
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadBuild()
|
|
}, 'Retry')
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (!this.data) return m('div');
|
|
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('.build-header.mb-6', [
|
|
m('h1.text-3xl.font-bold.mb-4', `Build ${this.buildId}`),
|
|
m('.build-meta.flex.gap-4.items-center.mb-4', [
|
|
m(BuildStatusBadge, { status: this.data.status, size: 'lg' }),
|
|
m('.timestamp.text-sm.opacity-70', formatDateTime(new Date(this.data.createdAt).toISOString())),
|
|
m('.partitions.text-sm.opacity-70', `${this.data.requestedPartitions?.length || 0} partitions`),
|
|
])
|
|
]),
|
|
|
|
m('.build-content.space-y-6', [
|
|
m('.partition-status.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', 'Partition Status'),
|
|
m('.partition-grid.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-3',
|
|
this.data.requestedPartitions?.map((partitionRef: string) => {
|
|
const status = this.partitionStatuses.get(partitionRef);
|
|
return m('.partition-card.border.border-base-300.rounded.p-3', [
|
|
m('a.partition-ref.font-mono.text-sm.break-all.mb-2.link.link-primary', {
|
|
href: `/partitions/${encodePartitionRef(partitionRef)}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/partitions/${encodePartitionRef(partitionRef)}`);
|
|
},
|
|
title: `View details for partition: ${partitionRef}`
|
|
}, partitionRef),
|
|
m('.flex.justify-between.items-center', [
|
|
m(PartitionStatusBadge, { status: status?.status || 'Unknown' }),
|
|
status?.lastUpdated ?
|
|
m('.updated-time.text-xs.opacity-60',
|
|
formatDateTime(new Date(status.lastUpdated).toISOString())) : null
|
|
])
|
|
]);
|
|
}) || [m('.text-center.py-8.text-base-content.opacity-60', 'No partitions')]
|
|
)
|
|
])
|
|
]),
|
|
|
|
m('.execution-events.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', 'Build Events'),
|
|
this.data.events?.length > 0 ?
|
|
m('.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Timestamp'),
|
|
m('th', 'Event Type'),
|
|
m('th', 'Message'),
|
|
m('th', 'Link')
|
|
])
|
|
]),
|
|
m('tbody',
|
|
this.data.events.map((event: any) =>
|
|
m('tr.hover', [
|
|
m('td.text-xs.font-mono',
|
|
formatDateTime(new Date(event.timestamp).toISOString())),
|
|
m('td', [
|
|
m(EventTypeBadge, { eventType: event.eventType })
|
|
]),
|
|
m('td.text-sm', event.message || ''),
|
|
m('td', [
|
|
(() => {
|
|
const link = this.getEventLink(event);
|
|
return link ?
|
|
m(m.route.Link, {
|
|
href: link.href,
|
|
class: 'link link-primary text-sm'
|
|
}, link.text) :
|
|
m('span.text-xs.opacity-50', '—');
|
|
})()
|
|
])
|
|
])
|
|
)
|
|
)
|
|
])
|
|
]) :
|
|
m('.text-center.py-8.text-base-content.opacity-60', 'No events')
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
|
|
export const PartitionsList = {
|
|
data: null as any | null,
|
|
loading: true,
|
|
error: null as string | null,
|
|
searchTerm: '',
|
|
|
|
async loadPartitions() {
|
|
try {
|
|
this.loading = true;
|
|
this.error = null;
|
|
m.redraw();
|
|
|
|
const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index');
|
|
const apiClient = new DefaultApi(new Configuration({ basePath: '' }));
|
|
|
|
const response = await apiClient.apiV1PartitionsGet();
|
|
this.data = response;
|
|
this.loading = false;
|
|
m.redraw();
|
|
} catch (error) {
|
|
console.error('Failed to load partitions:', error);
|
|
this.error = error instanceof Error ? error.message : 'Failed to load partitions';
|
|
this.loading = false;
|
|
m.redraw();
|
|
}
|
|
},
|
|
|
|
async buildPartition(partitionRef: string) {
|
|
try {
|
|
const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index');
|
|
const apiClient = new DefaultApi(new Configuration({ basePath: '' }));
|
|
|
|
const buildRequest = {
|
|
partitions: [partitionRef]
|
|
};
|
|
|
|
const response = await apiClient.apiV1BuildsPost({ buildRequest });
|
|
|
|
// Redirect to build status page
|
|
m.route.set(`/builds/${response.buildRequestId}`);
|
|
} catch (error) {
|
|
console.error('Failed to start build:', error);
|
|
alert(`Failed to start build: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
},
|
|
|
|
filteredPartitions() {
|
|
if (!this.data?.partitions) return [];
|
|
|
|
if (!this.searchTerm) return this.data.partitions;
|
|
|
|
const search = this.searchTerm.toLowerCase();
|
|
return this.data.partitions.filter((partition: any) =>
|
|
partition.partition_ref.toLowerCase().includes(search)
|
|
);
|
|
},
|
|
|
|
oninit() {
|
|
this.loadPartitions();
|
|
},
|
|
|
|
view() {
|
|
if (this.loading && !this.data) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.flex.flex-col.justify-center.items-center.min-h-96', [
|
|
m('span.loading.loading-spinner.loading-lg'),
|
|
m('span.ml-4.text-lg.mb-4', 'Loading partitions...'),
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadPartitions()
|
|
}, 'Retry Load')
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (this.error) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.alert.alert-error', [
|
|
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: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
})
|
|
]),
|
|
m('span', this.error),
|
|
m('div', [
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadPartitions()
|
|
}, 'Retry')
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
const filteredPartitions = this.filteredPartitions();
|
|
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('.partitions-header.mb-6', [
|
|
m('div.flex.justify-between.items-center.mb-4', [
|
|
m('h1.text-3xl.font-bold', 'Partitions'),
|
|
m('.badge.badge-primary.badge-lg', `${this.data?.totalCount || 0} total`)
|
|
]),
|
|
|
|
m('div.form-control.mb-4', [
|
|
m('input.input.input-bordered.w-full.max-w-md', {
|
|
placeholder: 'Search partitions...',
|
|
value: this.searchTerm,
|
|
oninput: (e: Event) => {
|
|
this.searchTerm = (e.target as HTMLInputElement).value;
|
|
m.redraw();
|
|
}
|
|
})
|
|
])
|
|
]),
|
|
|
|
m('.partitions-content', [
|
|
filteredPartitions.length === 0 ?
|
|
m('div.card.bg-base-100.shadow-xl', [
|
|
m('div.card-body.text-center', [
|
|
m('h2.card-title.justify-center', 'No Partitions Found'),
|
|
m('p.text-base-content.opacity-60',
|
|
this.searchTerm ?
|
|
'No partitions match your search criteria.' :
|
|
'No partitions have been built yet.')
|
|
])
|
|
]) :
|
|
m('div.card.bg-base-100.shadow-xl', [
|
|
m('div.card-body', [
|
|
m('h2.card-title.mb-4', `Showing ${filteredPartitions.length} partitions`),
|
|
m('div.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Partition Reference'),
|
|
m('th', 'Status'),
|
|
m('th', 'Last Updated'),
|
|
m('th', 'Actions'),
|
|
])
|
|
]),
|
|
m('tbody',
|
|
filteredPartitions.map((partition: any) =>
|
|
m('tr.hover', [
|
|
m('td', [
|
|
m('a.link.link-primary.font-mono.text-sm.break-all', {
|
|
href: `/partitions/${encodePartitionRef(partition.partition_ref)}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/partitions/${encodePartitionRef(partition.partition_ref)}`);
|
|
},
|
|
title: partition.partition_ref
|
|
}, partition.partition_ref)
|
|
]),
|
|
m('td', [
|
|
m(PartitionStatusBadge, { status: partition.status })
|
|
]),
|
|
m('td.text-sm.opacity-70', formatTime(new Date(partition.updated_at).toISOString())),
|
|
m('td', [
|
|
m('button.btn.btn-sm.btn-primary', {
|
|
onclick: () => this.buildPartition(partition.partition_ref)
|
|
}, 'Build'),
|
|
partition.build_request_id ?
|
|
m('a.btn.btn-sm.btn-outline.ml-2', {
|
|
href: `/builds/${partition.build_request_id}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/builds/${partition.build_request_id}`);
|
|
}
|
|
}, 'View Build') : null
|
|
])
|
|
])
|
|
)
|
|
)
|
|
])
|
|
])
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
|
|
export const PartitionStatus = {
|
|
data: null as any | null,
|
|
events: null as any | null,
|
|
loading: true,
|
|
error: null as string | null,
|
|
partitionRef: '',
|
|
buildHistory: [] as any[],
|
|
|
|
async loadPartition() {
|
|
try {
|
|
this.loading = true;
|
|
this.error = null;
|
|
m.redraw();
|
|
|
|
const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index');
|
|
const apiClient = new DefaultApi(new Configuration({ basePath: '' }));
|
|
|
|
// Load partition status
|
|
const statusResponse = await apiClient.apiV1PartitionsRefStatusGet({
|
|
ref: this.partitionRef
|
|
});
|
|
this.data = statusResponse;
|
|
|
|
// Load partition events for build history
|
|
const eventsResponse = await apiClient.apiV1PartitionsRefEventsGet({
|
|
ref: this.partitionRef
|
|
});
|
|
this.events = eventsResponse;
|
|
|
|
// Create build history from events
|
|
this.buildHistory = this.extractBuildHistory(eventsResponse.events);
|
|
|
|
this.loading = false;
|
|
m.redraw();
|
|
} catch (error) {
|
|
console.error('Failed to load partition:', error);
|
|
this.error = error instanceof Error ? error.message : 'Failed to load partition';
|
|
this.loading = false;
|
|
m.redraw();
|
|
}
|
|
},
|
|
|
|
extractBuildHistory(events: any[]): any[] {
|
|
// Group events by build request ID to create build history entries
|
|
const buildRequests = new Map();
|
|
|
|
events.forEach(event => {
|
|
if (event.buildRequestId) {
|
|
if (!buildRequests.has(event.buildRequestId)) {
|
|
buildRequests.set(event.buildRequestId, {
|
|
id: event.buildRequestId,
|
|
status: 'Unknown',
|
|
startedAt: event.timestamp,
|
|
completedAt: null,
|
|
events: []
|
|
});
|
|
}
|
|
|
|
const build = buildRequests.get(event.buildRequestId);
|
|
build.events.push(event);
|
|
|
|
// Update status based on event type
|
|
if (event.eventType === 'build_request') {
|
|
if (event.message?.includes('completed') || event.message?.includes('successful')) {
|
|
build.status = 'Completed';
|
|
build.completedAt = event.timestamp;
|
|
} else if (event.message?.includes('failed') || event.message?.includes('error')) {
|
|
build.status = 'Failed';
|
|
build.completedAt = event.timestamp;
|
|
} else if (event.message?.includes('executing') || event.message?.includes('running')) {
|
|
build.status = 'Executing';
|
|
} else if (event.message?.includes('planning')) {
|
|
build.status = 'Planning';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Convert to array and sort by start time (newest first)
|
|
return Array.from(buildRequests.values()).sort((a, b) => b.startedAt - a.startedAt);
|
|
},
|
|
|
|
async buildPartition(forceRebuild: boolean = false) {
|
|
try {
|
|
const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index');
|
|
const apiClient = new DefaultApi(new Configuration({ basePath: '' }));
|
|
|
|
const buildRequest = {
|
|
partitions: [this.partitionRef]
|
|
};
|
|
|
|
const response = await apiClient.apiV1BuildsPost({ buildRequest });
|
|
|
|
// Redirect to build status page
|
|
m.route.set(`/builds/${response.buildRequestId}`);
|
|
} catch (error) {
|
|
console.error('Failed to start build:', error);
|
|
alert(`Failed to start build: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
},
|
|
|
|
oninit(vnode: any) {
|
|
this.partitionRef = decodePartitionRef(vnode.attrs.base64_ref);
|
|
this.loadPartition();
|
|
},
|
|
|
|
view() {
|
|
if (this.loading && !this.data) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.flex.flex-col.justify-center.items-center.min-h-96', [
|
|
m('span.loading.loading-spinner.loading-lg'),
|
|
m('span.ml-4.text-lg.mb-4', 'Loading partition...'),
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadPartition()
|
|
}, 'Retry Load')
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (this.error) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.alert.alert-error', [
|
|
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: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
})
|
|
]),
|
|
m('span', this.error),
|
|
m('div', [
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => this.loadPartition()
|
|
}, 'Retry')
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (!this.data) return m('div');
|
|
|
|
return m('div.container.mx-auto.p-4', [
|
|
// Partition Header
|
|
m('.partition-header.mb-6', [
|
|
m('div.flex.justify-between.items-start.mb-4', [
|
|
m('div.flex-1', [
|
|
m('h1.text-3xl.font-bold.mb-2', 'Partition Status'),
|
|
m('div.font-mono.text-lg.break-all.bg-base-200.p-3.rounded', this.partitionRef)
|
|
]),
|
|
m('div.flex.flex-col.gap-2', [
|
|
m('button.btn.btn-primary', {
|
|
onclick: () => this.buildPartition(false)
|
|
}, 'Build Now'),
|
|
m('button.btn.btn-secondary', {
|
|
onclick: () => this.buildPartition(true)
|
|
}, 'Force Rebuild'),
|
|
])
|
|
]),
|
|
|
|
m('div.partition-meta.flex.gap-4.items-center.mb-4', [
|
|
m(PartitionStatusBadge, { status: this.data?.status || 'Unknown', size: 'lg' }),
|
|
this.data?.lastUpdated ?
|
|
m('.timestamp.text-sm.opacity-70',
|
|
`Last updated: ${formatDateTime(new Date(this.data.lastUpdated).toISOString())}`) : null,
|
|
])
|
|
]),
|
|
|
|
// Main Content
|
|
m('.partition-content.space-y-6', [
|
|
// Build History
|
|
m('.build-history.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', `Build History (${this.buildHistory?.length || 0} builds)`),
|
|
!this.buildHistory || this.buildHistory.length === 0 ?
|
|
m('.text-center.py-8.text-base-content.opacity-60', 'No build history available') :
|
|
m('.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Build Request'),
|
|
m('th', 'Status'),
|
|
m('th', 'Started'),
|
|
m('th', 'Completed'),
|
|
m('th', 'Events'),
|
|
])
|
|
]),
|
|
m('tbody',
|
|
this.buildHistory.map((build: any) =>
|
|
m('tr.hover', [
|
|
m('td', [
|
|
m('a.link.link-primary.font-mono.text-sm', {
|
|
href: `/builds/${build.id}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/builds/${build.id}`);
|
|
}
|
|
}, build.id)
|
|
]),
|
|
m('td', [
|
|
m(BuildStatusBadge, { status: build.status })
|
|
]),
|
|
m('td.text-sm.opacity-70',
|
|
formatDateTime(new Date(build.startedAt).toISOString())),
|
|
m('td.text-sm.opacity-70',
|
|
build.completedAt ?
|
|
formatDateTime(new Date(build.completedAt).toISOString()) :
|
|
'—'),
|
|
m('td.text-sm.opacity-70', `${build.events?.length || 0} events`)
|
|
])
|
|
)
|
|
)
|
|
])
|
|
])
|
|
])
|
|
]),
|
|
|
|
// Related Build Requests
|
|
this.data?.buildRequests && this.data.buildRequests.length > 0 ?
|
|
m('.related-builds.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', 'Related Build Requests'),
|
|
m('.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-3',
|
|
this.data.buildRequests.map((buildId: string) =>
|
|
m('.build-card.border.border-base-300.rounded.p-3', [
|
|
m('a.link.link-primary.font-mono.text-sm', {
|
|
href: `/builds/${buildId}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/builds/${buildId}`);
|
|
}
|
|
}, buildId)
|
|
])
|
|
)
|
|
)
|
|
])
|
|
]) : null,
|
|
|
|
// Raw Events
|
|
this.events?.events && this.events.events.length > 0 ?
|
|
m('.partition-events.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', `All Events (${this.events.events.length})`),
|
|
m('.overflow-x-auto', [
|
|
m('table.table.table-xs', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Timestamp'),
|
|
m('th', 'Event Type'),
|
|
m('th', 'Build Request'),
|
|
m('th', 'Message'),
|
|
])
|
|
]),
|
|
m('tbody',
|
|
this.events.events.slice(0, 20).map((event: any) => // Show first 20 events
|
|
m('tr.hover', [
|
|
m('td.text-xs.font-mono',
|
|
formatDateTime(new Date(event.timestamp).toISOString())),
|
|
m('td', [
|
|
m(EventTypeBadge, { eventType: event.eventType, size: 'xs' })
|
|
]),
|
|
m('td',
|
|
event.buildRequestId ?
|
|
m('a.link.link-primary.font-mono.text-xs', {
|
|
href: `/builds/${event.buildRequestId}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/builds/${event.buildRequestId}`);
|
|
}
|
|
}, event.buildRequestId) : '—'),
|
|
m('td.text-xs', event.message || ''),
|
|
])
|
|
)
|
|
)
|
|
])
|
|
]),
|
|
this.events.events.length > 20 ?
|
|
m('.text-center.mt-4', [
|
|
m('.text-sm.opacity-60', `Showing first 20 of ${this.events.events.length} events`)
|
|
]) : null
|
|
])
|
|
]) : null
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
|
|
export const JobsList = {
|
|
jobs: [] as any[],
|
|
searchTerm: '',
|
|
loading: false,
|
|
error: null as string | null,
|
|
searchTimeout: null as NodeJS.Timeout | null,
|
|
|
|
oninit(vnode: any) {
|
|
JobsList.loadJobs();
|
|
},
|
|
|
|
async loadJobs() {
|
|
JobsList.loading = true;
|
|
JobsList.error = null;
|
|
|
|
try {
|
|
const service = DashboardService.getInstance();
|
|
JobsList.jobs = await service.getJobs(JobsList.searchTerm || undefined);
|
|
} catch (error) {
|
|
console.error('Failed to load jobs:', error);
|
|
JobsList.error = 'Failed to load jobs. Please try again.';
|
|
} finally {
|
|
JobsList.loading = false;
|
|
m.redraw();
|
|
}
|
|
},
|
|
|
|
filteredJobs() {
|
|
if (!JobsList.searchTerm) {
|
|
return JobsList.jobs;
|
|
}
|
|
const search = JobsList.searchTerm.toLowerCase();
|
|
return JobsList.jobs.filter((job: any) =>
|
|
job.job_label.toLowerCase().includes(search)
|
|
);
|
|
},
|
|
|
|
view: () => {
|
|
if (JobsList.loading) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.flex.justify-center.items-center.h-64', [
|
|
m('div.loading.loading-spinner.loading-lg')
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (JobsList.error) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.alert.alert-error', [
|
|
m('span', JobsList.error),
|
|
m('div', [
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => JobsList.loadJobs()
|
|
}, 'Retry')
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
return m('div.container.mx-auto.p-4', [
|
|
// Jobs Header
|
|
m('.jobs-header.mb-6', [
|
|
m('h1.text-3xl.font-bold.mb-4', 'Jobs'),
|
|
m('div.flex.gap-4.items-center.mb-4', [
|
|
m('input.input.input-bordered.flex-1[placeholder="Search jobs..."]', {
|
|
value: JobsList.searchTerm,
|
|
oninput: (e: Event) => {
|
|
JobsList.searchTerm = (e.target as HTMLInputElement).value;
|
|
// Debounce search
|
|
if (JobsList.searchTimeout) clearTimeout(JobsList.searchTimeout);
|
|
JobsList.searchTimeout = setTimeout(() => JobsList.loadJobs(), 300);
|
|
}
|
|
}),
|
|
m('button.btn.btn-outline', {
|
|
onclick: () => JobsList.loadJobs()
|
|
}, 'Refresh')
|
|
])
|
|
]),
|
|
|
|
// Jobs Table
|
|
JobsList.filteredJobs().length === 0 ?
|
|
m('div.text-center.py-8.text-base-content.opacity-60', 'No jobs found') :
|
|
m('.jobs-table.card.bg-base-100.shadow-xl', [
|
|
m('.card-body.p-0', [
|
|
m('.overflow-x-auto', [
|
|
m('table.table.table-zebra', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Job Label'),
|
|
m('th', 'Success Rate'),
|
|
m('th', 'Avg Duration'),
|
|
m('th', 'Recent Runs'),
|
|
m('th', 'Last Run'),
|
|
])
|
|
]),
|
|
m('tbody', JobsList.filteredJobs().map((job: any) =>
|
|
m('tr.hover', [
|
|
m('td', [
|
|
m('a.link.link-primary.font-mono.text-sm', {
|
|
href: `/jobs/${encodeJobLabel(job.job_label)}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/jobs/${encodeJobLabel(job.job_label)}`);
|
|
}
|
|
}, job.job_label)
|
|
]),
|
|
m('td', [
|
|
m(`span.badge.${job.success_rate >= 0.9 ? 'badge-success' : job.success_rate >= 0.7 ? 'badge-warning' : 'badge-error'}`,
|
|
`${Math.round(job.success_rate * 100)}%`)
|
|
]),
|
|
m('td', formatDuration(job.avg_duration_ms)),
|
|
m('td', (job.recent_runs || 0).toString()),
|
|
m('td.text-sm.opacity-70',
|
|
job.last_run ? formatTime(new Date(job.last_run).toISOString()) : '—'),
|
|
])
|
|
))
|
|
])
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
|
|
export const JobMetrics = {
|
|
jobLabel: '',
|
|
metrics: null as any,
|
|
loading: false,
|
|
error: null as string | null,
|
|
|
|
oninit(vnode: any) {
|
|
JobMetrics.jobLabel = decodeJobLabel(vnode.attrs.label);
|
|
JobMetrics.loadJobMetrics();
|
|
},
|
|
|
|
async loadJobMetrics() {
|
|
JobMetrics.loading = true;
|
|
JobMetrics.error = null;
|
|
|
|
try {
|
|
const service = DashboardService.getInstance();
|
|
JobMetrics.metrics = await service.getJobMetrics(JobMetrics.jobLabel);
|
|
if (!JobMetrics.metrics) {
|
|
JobMetrics.error = 'Job not found or no metrics available';
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load job metrics:', error);
|
|
JobMetrics.error = 'Failed to load job metrics. Please try again.';
|
|
} finally {
|
|
JobMetrics.loading = false;
|
|
m.redraw();
|
|
}
|
|
},
|
|
|
|
view: () => {
|
|
if (JobMetrics.loading) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.flex.justify-center.items-center.h-64', [
|
|
m('div.loading.loading-spinner.loading-lg')
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (JobMetrics.error) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.alert.alert-error', [
|
|
m('span', JobMetrics.error),
|
|
m('div', [
|
|
m('button.btn.btn-sm.btn-outline', {
|
|
onclick: () => JobMetrics.loadJobMetrics()
|
|
}, 'Retry')
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
if (!JobMetrics.metrics) {
|
|
return m('div.container.mx-auto.p-4', [
|
|
m('div.text-center.py-8.text-base-content.opacity-60', 'No metrics available')
|
|
]);
|
|
}
|
|
|
|
return m('div.container.mx-auto.p-4', [
|
|
// Job Header
|
|
m('.job-header.mb-6', [
|
|
m('h1.text-3xl.font-bold.mb-4', [
|
|
'Job Metrics: ',
|
|
m('span.font-mono.text-2xl', JobMetrics.jobLabel)
|
|
]),
|
|
m('.job-stats.grid.grid-cols-1.md:grid-cols-3.gap-4.mb-6', [
|
|
m('.stat.bg-base-100.shadow-xl.rounded-lg.p-4', [
|
|
m('.stat-title', 'Success Rate'),
|
|
m('.stat-value.text-3xl', [
|
|
m(`span.${JobMetrics.metrics.success_rate >= 0.9 ? 'text-success' : JobMetrics.metrics.success_rate >= 0.7 ? 'text-warning' : 'text-error'}`,
|
|
`${Math.round(JobMetrics.metrics.success_rate * 100)}%`)
|
|
]),
|
|
]),
|
|
m('.stat.bg-base-100.shadow-xl.rounded-lg.p-4', [
|
|
m('.stat-title', 'Avg Duration'),
|
|
m('.stat-value.text-3xl', formatDuration(JobMetrics.metrics.avg_duration_ms)),
|
|
]),
|
|
m('.stat.bg-base-100.shadow-xl.rounded-lg.p-4', [
|
|
m('.stat-title', 'Total Runs'),
|
|
m('.stat-value.text-3xl', JobMetrics.metrics.total_runs),
|
|
]),
|
|
])
|
|
]),
|
|
|
|
// Main Content
|
|
m('.job-content.space-y-6', [
|
|
// Performance Trends
|
|
JobMetrics.metrics.daily_stats?.length > 0 && m('.performance-trends.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', 'Performance Trends (Last 30 Days)'),
|
|
m('.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Date'),
|
|
m('th', 'Success Rate'),
|
|
m('th', 'Avg Duration'),
|
|
m('th', 'Total Runs'),
|
|
])
|
|
]),
|
|
m('tbody', JobMetrics.metrics.daily_stats.map((stat: any) =>
|
|
m('tr.hover', [
|
|
m('td', formatDate(stat.date)),
|
|
m('td', [
|
|
m(`span.badge.${stat.success_rate >= 0.9 ? 'badge-success' : stat.success_rate >= 0.7 ? 'badge-warning' : 'badge-error'}`,
|
|
`${Math.round(stat.success_rate * 100)}%`)
|
|
]),
|
|
m('td', formatDuration(stat.avg_duration_ms)),
|
|
m('td', stat.total_runs),
|
|
])
|
|
))
|
|
])
|
|
])
|
|
])
|
|
]),
|
|
|
|
// Recent Runs
|
|
m('.recent-runs.card.bg-base-100.shadow-xl', [
|
|
m('.card-body', [
|
|
m('h2.card-title.text-xl.mb-4', `Recent Runs (${JobMetrics.metrics.recent_runs?.length || 0})`),
|
|
!JobMetrics.metrics.recent_runs || JobMetrics.metrics.recent_runs.length === 0 ?
|
|
m('.text-center.py-8.text-base-content.opacity-60', 'No recent runs available') :
|
|
m('.overflow-x-auto', [
|
|
m('table.table.table-sm', [
|
|
m('thead', [
|
|
m('tr', [
|
|
m('th', 'Build Request'),
|
|
m('th', 'Partitions'),
|
|
m('th', 'Status'),
|
|
m('th', 'Duration'),
|
|
m('th', 'Started'),
|
|
])
|
|
]),
|
|
m('tbody', JobMetrics.metrics.recent_runs.map((run: any) =>
|
|
m('tr.hover', [
|
|
m('td', [
|
|
m('a.link.link-primary.font-mono.text-sm', {
|
|
href: `/builds/${run.build_request_id}`,
|
|
onclick: (e: Event) => {
|
|
e.preventDefault();
|
|
m.route.set(`/builds/${run.build_request_id}`);
|
|
}
|
|
}, run.build_request_id)
|
|
]),
|
|
m('td.text-sm', [
|
|
m('span.font-mono', run.partitions.slice(0, 3).join(', ')),
|
|
run.partitions.length > 3 && m('span.opacity-60', ` +${run.partitions.length - 3} more`)
|
|
]),
|
|
m('td', [
|
|
m(`span.badge.${run.status === 'completed' ? 'badge-success' :
|
|
run.status === 'failed' ? 'badge-error' :
|
|
run.status === 'running' ? 'badge-warning' : 'badge-info'}`,
|
|
run.status)
|
|
]),
|
|
m('td', formatDuration(run.duration_ms)),
|
|
m('td.text-sm.opacity-70',
|
|
formatTime(new Date(run.started_at).toISOString())),
|
|
])
|
|
))
|
|
])
|
|
])
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
|
|
export const GraphAnalysis = {
|
|
view: () => m('div.container.mx-auto.p-4', [
|
|
m('h1.text-3xl.font-bold.mb-4', 'Graph Analysis'),
|
|
m('div.card.bg-base-100.shadow-xl', [
|
|
m('div.card-body', [
|
|
m('h2.card-title', 'Interactive Build Graph'),
|
|
m('p', 'Analyze partition dependencies and execution plans.'),
|
|
m('div.form-control.mb-4', [
|
|
m('label.label', [
|
|
m('span.label-text', 'Partition References'),
|
|
]),
|
|
m('textarea.textarea.textarea-bordered[placeholder="Enter partition references to analyze..."]'),
|
|
]),
|
|
m('div.card-actions.justify-end', [
|
|
m('button.btn.btn-primary', 'Analyze Graph'),
|
|
]),
|
|
]),
|
|
]),
|
|
])
|
|
}; |