Update ui
This commit is contained in:
parent
7bb999ab47
commit
307f146e7c
5 changed files with 178 additions and 96 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import m from 'mithril';
|
||||
import { DashboardService, pollingManager, formatTime, formatDateTime, RecentActivitySummary } from './services';
|
||||
import { encodePartitionRef } from './utils';
|
||||
import { encodePartitionRef, BuildStatusBadge, PartitionStatusBadge, EventTypeBadge } from './utils';
|
||||
|
||||
// Page scaffold components
|
||||
export const RecentActivity = {
|
||||
|
|
@ -10,20 +10,16 @@ export const RecentActivity = {
|
|||
pollInterval: null as NodeJS.Timeout | null,
|
||||
|
||||
loadData() {
|
||||
console.log('RecentActivity: Starting loadData, loading=', this.loading);
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
m.redraw(); // Redraw to show loading state
|
||||
|
||||
const service = DashboardService.getInstance();
|
||||
console.log('RecentActivity: Got service instance, calling getRecentActivity');
|
||||
|
||||
return service.getRecentActivity()
|
||||
.then(data => {
|
||||
console.log('RecentActivity: Got data successfully', data);
|
||||
this.data = data;
|
||||
this.loading = false;
|
||||
console.log('RecentActivity: Data loaded, loading=', this.loading, 'data=', !!this.data);
|
||||
m.redraw(); // Explicitly redraw after data loads
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
@ -52,7 +48,6 @@ export const RecentActivity = {
|
|||
},
|
||||
|
||||
view: function() {
|
||||
console.log('RecentActivity: view() called, loading=', this.loading, 'data=', !!this.data, 'error=', this.error);
|
||||
|
||||
if (this.loading && !this.data) {
|
||||
return m('div.container.mx-auto.p-4', [
|
||||
|
|
@ -207,12 +202,7 @@ export const RecentActivity = {
|
|||
}, build.id)
|
||||
]),
|
||||
m('td', [
|
||||
m(`span.badge.badge-sm.${
|
||||
build.status === 'completed' ? 'badge-success' :
|
||||
build.status === 'running' ? 'badge-warning' :
|
||||
build.status === 'failed' ? 'badge-error' :
|
||||
'badge-neutral'
|
||||
}`, build.status)
|
||||
m(BuildStatusBadge, { status: build.status })
|
||||
]),
|
||||
m('td.text-sm.opacity-70', formatTime(build.createdAt)),
|
||||
])
|
||||
|
|
@ -266,12 +256,7 @@ export const RecentActivity = {
|
|||
}, partition.ref)
|
||||
]),
|
||||
m('td', [
|
||||
m(`span.badge.badge-sm.${
|
||||
partition.status === 'available' ? 'badge-success' :
|
||||
partition.status === 'building' ? 'badge-warning' :
|
||||
partition.status === 'failed' ? 'badge-error' :
|
||||
'badge-neutral'
|
||||
}`, partition.status)
|
||||
m(PartitionStatusBadge, { status: partition.status })
|
||||
]),
|
||||
m('td.text-sm.opacity-70', formatTime(partition.updatedAt)),
|
||||
])
|
||||
|
|
@ -291,7 +276,6 @@ export const BuildStatus = {
|
|||
loading: true,
|
||||
error: null as string | null,
|
||||
partitionStatuses: new Map<string, any>(),
|
||||
logsExpanded: {} as Record<string, boolean>,
|
||||
buildId: '',
|
||||
|
||||
oninit(vnode: any) {
|
||||
|
|
@ -310,8 +294,6 @@ export const BuildStatus = {
|
|||
this.error = null;
|
||||
m.redraw();
|
||||
|
||||
console.log(`BuildStatus: Loading build ${this.buildId}`);
|
||||
|
||||
// Import types dynamically to avoid circular dependencies
|
||||
const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index');
|
||||
const apiClient = new DefaultApi(new Configuration({ basePath: '' }));
|
||||
|
|
@ -320,8 +302,6 @@ export const BuildStatus = {
|
|||
const buildResponse = await apiClient.apiV1BuildsBuildRequestIdGet({ buildRequestId: this.buildId });
|
||||
this.data = buildResponse;
|
||||
|
||||
console.log('BuildStatus: Got build response:', buildResponse);
|
||||
|
||||
// Load partition statuses for all requested partitions
|
||||
if (buildResponse.requestedPartitions) {
|
||||
for (const partitionRef of buildResponse.requestedPartitions) {
|
||||
|
|
@ -357,42 +337,42 @@ export const BuildStatus = {
|
|||
}, interval);
|
||||
},
|
||||
|
||||
getStatusClass(status: string): string {
|
||||
switch (status) {
|
||||
case 'BuildRequestCompleted': return 'badge-success';
|
||||
case 'BuildRequestExecuting': return 'badge-warning';
|
||||
case 'BuildRequestPlanning': return 'badge-warning';
|
||||
case 'BuildRequestFailed': return 'badge-error';
|
||||
case 'BuildRequestCancelled': return 'badge-error';
|
||||
default: return 'badge-neutral';
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
getPartitionStatusClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'PartitionAvailable': return 'badge-success';
|
||||
case 'PartitionBuilding': return 'badge-warning';
|
||||
case 'PartitionScheduled': return 'badge-warning';
|
||||
case 'PartitionFailed': return 'badge-error';
|
||||
case 'PartitionDelegated': return 'badge-info';
|
||||
default: return 'badge-neutral';
|
||||
}
|
||||
},
|
||||
|
||||
isJobEvent(event: any): boolean {
|
||||
return event.eventType?.includes('Job') || event.eventType?.includes('job');
|
||||
},
|
||||
|
||||
toggleLogs(eventId: string) {
|
||||
this.logsExpanded[eventId] = !this.logsExpanded[eventId];
|
||||
m.redraw();
|
||||
},
|
||||
|
||||
formatJobLogs(event: any): string {
|
||||
// For now, just return the message as formatted log
|
||||
// In the future, this could parse structured log data
|
||||
return event.message || '';
|
||||
},
|
||||
|
||||
view() {
|
||||
// Loading/error states similar to RecentActivity component
|
||||
|
|
@ -438,7 +418,7 @@ export const BuildStatus = {
|
|||
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(`span.badge.badge-lg.${this.getStatusClass(this.data.status)}`, this.data.status),
|
||||
m(BuildStatusBadge, { status: this.data.status, size: 'lg' }),
|
||||
m('.timestamp.text-sm.opacity-70', formatDateTime(new Date(this.data.createdAt / 1000000).toISOString())),
|
||||
m('.partitions.text-sm.opacity-70', `${this.data.requestedPartitions?.length || 0} partitions`),
|
||||
])
|
||||
|
|
@ -454,8 +434,7 @@ export const BuildStatus = {
|
|||
return m('.partition-card.border.border-base-300.rounded.p-3', [
|
||||
m('.partition-ref.font-mono.text-sm.break-all.mb-2', partitionRef),
|
||||
m('.flex.justify-between.items-center', [
|
||||
m(`span.badge.${this.getPartitionStatusClass(status?.status)}`,
|
||||
status?.status || 'Unknown'),
|
||||
m(PartitionStatusBadge, { status: status?.status || 'Unknown' }),
|
||||
status?.lastUpdated ?
|
||||
m('.updated-time.text-xs.opacity-60',
|
||||
formatDateTime(new Date(status.lastUpdated / 1000000).toISOString())) : null
|
||||
|
|
@ -477,7 +456,7 @@ export const BuildStatus = {
|
|||
m('th', 'Timestamp'),
|
||||
m('th', 'Event Type'),
|
||||
m('th', 'Message'),
|
||||
m('th', 'Actions')
|
||||
m('th', 'Link')
|
||||
])
|
||||
]),
|
||||
m('tbody',
|
||||
|
|
@ -486,32 +465,24 @@ export const BuildStatus = {
|
|||
m('td.text-xs.font-mono',
|
||||
formatDateTime(new Date(event.timestamp / 1000000).toISOString())),
|
||||
m('td', [
|
||||
m('.badge.badge-sm.badge-outline', event.eventType)
|
||||
m(EventTypeBadge, { eventType: event.eventType })
|
||||
]),
|
||||
m('td.text-sm', event.message || ''),
|
||||
m('td', [
|
||||
this.isJobEvent(event) ?
|
||||
m('button.btn.btn-xs.btn-outline', {
|
||||
onclick: () => this.toggleLogs(event.eventId)
|
||||
}, this.logsExpanded[event.eventId] ? 'Hide' : 'Details') : null
|
||||
(() => {
|
||||
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', '—');
|
||||
})()
|
||||
])
|
||||
])
|
||||
)
|
||||
)
|
||||
]),
|
||||
// Expandable logs section
|
||||
this.data.events.some((event: any) => this.logsExpanded[event.eventId]) ?
|
||||
m('.mt-4', [
|
||||
m('h3.text-lg.font-semibold.mb-2', 'Event Details'),
|
||||
this.data.events.filter((event: any) => this.logsExpanded[event.eventId])
|
||||
.map((event: any) =>
|
||||
m('.bg-base-200.rounded.p-3.mb-2', [
|
||||
m('.text-sm.font-semibold.mb-1', `${event.eventType} - ${event.eventId}`),
|
||||
m('.text-xs.font-mono.whitespace-pre-wrap',
|
||||
this.formatJobLogs(event))
|
||||
])
|
||||
)
|
||||
]) : null
|
||||
])
|
||||
]) :
|
||||
m('.text-center.py-8.text-base-content.opacity-60', 'No events')
|
||||
])
|
||||
|
|
|
|||
|
|
@ -46,13 +46,9 @@ export class DashboardService {
|
|||
|
||||
async getRecentActivity(): Promise<RecentActivitySummary> {
|
||||
try {
|
||||
console.log('DashboardService: Fetching real data from API...');
|
||||
|
||||
// Use the new activity endpoint that aggregates all the data we need
|
||||
const activityResponse: ActivityResponse = await apiClient.apiV1ActivityGet();
|
||||
|
||||
console.log('DashboardService: Got activity response:', activityResponse);
|
||||
|
||||
// Convert the API response to our dashboard format
|
||||
const recentBuilds: BuildRequest[] = activityResponse.recentBuilds.map((build: BuildSummary) => ({
|
||||
id: build.buildRequestId,
|
||||
|
|
@ -79,7 +75,6 @@ export class DashboardService {
|
|||
console.error('Failed to fetch recent activity:', error);
|
||||
|
||||
// Fall back to mock data if API call fails
|
||||
console.log('DashboardService: Falling back to mock data due to API error');
|
||||
return {
|
||||
activeBuilds: 0,
|
||||
recentBuilds: [],
|
||||
|
|
|
|||
|
|
@ -8,4 +8,82 @@ export function decodePartitionRef(encoded: string): string {
|
|||
const padding = '='.repeat((4 - (encoded.length % 4)) % 4);
|
||||
const padded = encoded.replace(/-/g, '+').replace(/_/g, '/') + padding;
|
||||
return atob(padded);
|
||||
}
|
||||
}
|
||||
|
||||
import m from 'mithril';
|
||||
|
||||
// Mithril components for status badges - encapsulates both logic and presentation
|
||||
|
||||
export const BuildStatusBadge = {
|
||||
view(vnode: any) {
|
||||
const { status, size = 'sm', ...attrs } = vnode.attrs;
|
||||
const normalizedStatus = status.toLowerCase();
|
||||
|
||||
let badgeClass = 'badge-neutral';
|
||||
if (normalizedStatus.includes('completed')) {
|
||||
badgeClass = 'badge-success';
|
||||
} else if (normalizedStatus.includes('executing') || normalizedStatus.includes('planning')) {
|
||||
badgeClass = 'badge-warning';
|
||||
} else if (normalizedStatus.includes('received')) {
|
||||
badgeClass = 'badge-info';
|
||||
} else if (normalizedStatus.includes('failed') || normalizedStatus.includes('cancelled')) {
|
||||
badgeClass = 'badge-error';
|
||||
}
|
||||
|
||||
return m(`span.badge.badge-${size}.${badgeClass}`, attrs, status);
|
||||
}
|
||||
};
|
||||
|
||||
export const PartitionStatusBadge = {
|
||||
view(vnode: any) {
|
||||
const { status, size = 'sm', ...attrs } = vnode.attrs;
|
||||
if (!status) {
|
||||
return m(`span.badge.badge-${size}.badge-neutral`, attrs, 'Unknown');
|
||||
}
|
||||
|
||||
const normalizedStatus = status.toLowerCase();
|
||||
let badgeClass = 'badge-neutral';
|
||||
|
||||
if (normalizedStatus.includes('available')) {
|
||||
badgeClass = 'badge-success';
|
||||
} else if (normalizedStatus.includes('building') || normalizedStatus.includes('scheduled')) {
|
||||
badgeClass = 'badge-warning';
|
||||
} else if (normalizedStatus.includes('requested') || normalizedStatus.includes('delegated')) {
|
||||
badgeClass = 'badge-info';
|
||||
} else if (normalizedStatus.includes('failed')) {
|
||||
badgeClass = 'badge-error';
|
||||
}
|
||||
|
||||
return m(`span.badge.badge-${size}.${badgeClass}`, attrs, status);
|
||||
}
|
||||
};
|
||||
|
||||
export const EventTypeBadge = {
|
||||
view(vnode: any) {
|
||||
const { eventType, size = 'sm', ...attrs } = vnode.attrs;
|
||||
|
||||
let badgeClass = 'badge-ghost';
|
||||
let displayName = eventType;
|
||||
|
||||
switch (eventType) {
|
||||
case 'build_request':
|
||||
badgeClass = 'badge-primary';
|
||||
displayName = 'Build';
|
||||
break;
|
||||
case 'job':
|
||||
badgeClass = 'badge-secondary';
|
||||
displayName = 'Job';
|
||||
break;
|
||||
case 'partition':
|
||||
badgeClass = 'badge-accent';
|
||||
displayName = 'Partition';
|
||||
break;
|
||||
case 'delegation':
|
||||
badgeClass = 'badge-info';
|
||||
displayName = 'Delegation';
|
||||
break;
|
||||
}
|
||||
|
||||
return m(`span.badge.badge-${size}.${badgeClass}`, attrs, displayName);
|
||||
}
|
||||
};
|
||||
|
|
@ -178,11 +178,17 @@ pub async fn get_build_status(
|
|||
})?;
|
||||
|
||||
// Convert events to summary format for response
|
||||
let event_summaries: Vec<BuildEventSummary> = events.into_iter().map(|e| BuildEventSummary {
|
||||
event_id: e.event_id,
|
||||
timestamp: e.timestamp,
|
||||
event_type: event_type_to_string(&e.event_type),
|
||||
message: event_to_message(&e.event_type),
|
||||
let event_summaries: Vec<BuildEventSummary> = events.into_iter().map(|e| {
|
||||
let (job_label, partition_ref, delegated_build_id) = extract_navigation_data(&e.event_type);
|
||||
BuildEventSummary {
|
||||
event_id: e.event_id,
|
||||
timestamp: e.timestamp,
|
||||
event_type: event_type_to_string(&e.event_type),
|
||||
message: event_to_message(&e.event_type),
|
||||
job_label,
|
||||
partition_ref,
|
||||
delegated_build_id,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let final_status_string = BuildGraphService::status_to_string(final_status);
|
||||
|
|
@ -311,11 +317,17 @@ pub async fn get_partition_events(
|
|||
Path(request): Path<PartitionEventsRequest>,
|
||||
) -> Result<Json<PartitionEventsResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let events = match service.event_log.get_partition_events(&request.ref_param, None).await {
|
||||
Ok(events) => events.into_iter().map(|e| BuildEventSummary {
|
||||
event_id: e.event_id,
|
||||
timestamp: e.timestamp,
|
||||
event_type: event_type_to_string(&e.event_type),
|
||||
message: event_to_message(&e.event_type),
|
||||
Ok(events) => events.into_iter().map(|e| {
|
||||
let (job_label, partition_ref, delegated_build_id) = extract_navigation_data(&e.event_type);
|
||||
BuildEventSummary {
|
||||
event_id: e.event_id,
|
||||
timestamp: e.timestamp,
|
||||
event_type: event_type_to_string(&e.event_type),
|
||||
message: event_to_message(&e.event_type),
|
||||
job_label,
|
||||
partition_ref,
|
||||
delegated_build_id,
|
||||
}
|
||||
}).collect(),
|
||||
Err(e) => {
|
||||
error!("Failed to get partition events: {}", e);
|
||||
|
|
@ -583,6 +595,28 @@ fn event_to_message(event_type: &Option<crate::build_event::EventType>) -> Strin
|
|||
}
|
||||
}
|
||||
|
||||
fn extract_navigation_data(event_type: &Option<crate::build_event::EventType>) -> (Option<String>, Option<String>, Option<String>) {
|
||||
match event_type {
|
||||
Some(crate::build_event::EventType::JobEvent(event)) => {
|
||||
let job_label = event.job_label.as_ref().map(|l| l.label.clone());
|
||||
(job_label, None, None)
|
||||
},
|
||||
Some(crate::build_event::EventType::PartitionEvent(event)) => {
|
||||
let partition_ref = event.partition_ref.as_ref().map(|r| r.str.clone());
|
||||
(None, partition_ref, None)
|
||||
},
|
||||
Some(crate::build_event::EventType::DelegationEvent(event)) => {
|
||||
let delegated_build_id = Some(event.delegated_to_build_request_id.clone());
|
||||
(None, None, delegated_build_id)
|
||||
},
|
||||
Some(crate::build_event::EventType::BuildRequestEvent(_)) => {
|
||||
// Build request events don't need navigation links (self-referential)
|
||||
(None, None, None)
|
||||
},
|
||||
None => (None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
// New handlers for list endpoints
|
||||
use axum::extract::Query;
|
||||
use std::collections::HashMap;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ pub struct BuildEventSummary {
|
|||
pub timestamp: i64,
|
||||
pub event_type: String,
|
||||
pub message: String,
|
||||
// Navigation-relevant fields (populated based on event type)
|
||||
pub job_label: Option<String>, // For job events
|
||||
pub partition_ref: Option<String>, // For partition events
|
||||
pub delegated_build_id: Option<String>, // For delegation events
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue