Skip to content

Commit e326413

Browse files
committed
Add dialog to show Serval build info
1 parent 733461b commit e326413

21 files changed

+940
-303
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -357,19 +357,15 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
357357
return await this.onlineInvoke<QueryResults<EventMetric>>('eventMetrics', { projectId, pageIndex, pageSize });
358358
}
359359

360-
async onlineAllEventMetricsForConstructionDraftJobs(
360+
async onlineAllEventMetricsForConstructingDraftJobs(
361+
eventTypes: string[],
361362
projectId?: string,
362363
daysBack?: number
363364
): Promise<QueryResults<EventMetric> | undefined> {
364365
const params: any = {
365366
projectId: projectId ?? null,
366367
scopes: [3], // Drafting scope
367-
eventTypes: [
368-
'StartPreTranslationBuildAsync',
369-
'BuildProjectAsync',
370-
'RetrievePreTranslationStatusAsync',
371-
'CancelPreTranslationBuildAsync'
372-
]
368+
eventTypes
373369
};
374370

375371
if (daysBack != null) {

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/_draft-jobs-theme.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
--sf-draft-jobs-table-row-background: #{mat.get-theme-color($theme, neutral, if($is-dark, 10, 100))};
77
--sf-draft-jobs-project-link-color: #{mat.get-theme-color($theme, primary, if($is-dark, 70, 40))};
88
--sf-draft-jobs-book-count-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 70, 40))};
9+
--sf-draft-jobs-disabled-link-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 50, 50))};
910
}
1011

1112
@mixin theme($theme) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@use '@angular/material' as mat;
2+
3+
@mixin color($theme) {
4+
$is-dark: mat.get-theme-type($theme) == dark;
5+
6+
// Summary section colors
7+
--job-details-summary-background: #{mat.get-theme-color($theme, neutral, if($is-dark, 17, 98))};
8+
--job-details-summary-item-background: #{mat.get-theme-color($theme, neutral, if($is-dark, 22, 100))};
9+
--job-details-label-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 80, 50))};
10+
--job-details-value-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 90, 10))};
11+
12+
// Language pair maybe indicator
13+
--job-details-maybe-color: #{mat.get-theme-color($theme, tertiary, if($is-dark, 70, 50))};
14+
15+
// ClearML link colors
16+
--job-details-link-color: #{mat.get-theme-color($theme, primary, if($is-dark, 70, 40))};
17+
--job-details-link-hover-color: #{mat.get-theme-color($theme, primary, if($is-dark, 80, 30))};
18+
19+
// Loading and error states
20+
--job-details-loading-text-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 80, 50))};
21+
--job-details-error-background: #{mat.get-theme-color($theme, error, if($is-dark, 20, 95))};
22+
--job-details-error-color: #{mat.get-theme-color($theme, error, if($is-dark, 80, 40))};
23+
--job-details-no-data-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 60, 60))};
24+
25+
// Event timeline colors
26+
--job-details-event-background: #{mat.get-theme-color($theme, neutral, if($is-dark, 17, 98))};
27+
--job-details-event-timestamp-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 70, 50))};
28+
--job-details-event-content-background: #{mat.get-theme-color($theme, surface)};
29+
--job-details-event-content-text: #{mat.get-theme-color($theme, neutral, if($is-dark, 90, 10))};
30+
--job-details-event-code-background: #{mat.get-theme-color($theme, neutral, if($is-dark, 20, 95))};
31+
32+
// Exception styling
33+
--job-details-exception-border: #{mat.get-theme-color($theme, error, if($is-dark, 60, 50))};
34+
--job-details-exception-background: #{mat.get-theme-color($theme, error, if($is-dark, 20, 95))};
35+
}
36+
37+
@mixin theme($theme) {
38+
@if mat.theme-has($theme, color) {
39+
@include color($theme);
40+
}
41+
}

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/_job-events-dialog-theme.scss

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/draft-jobs.component.html

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<th mat-header-cell *matHeaderCellDef>Training Books</th>
7474
<td mat-cell *matCellDef="let row">
7575
@for (projectBook of row.trainingBooks; track projectBook.projectId) {
76-
<div class="project-books-line">
76+
<div>
7777
<a [routerLink]="['/serval-administration', projectBook.projectId]" class="project-link">{{
7878
getProjectShortName(projectBook.projectId)
7979
}}</a
@@ -95,7 +95,7 @@
9595
<th mat-header-cell *matHeaderCellDef>Translation Books</th>
9696
<td mat-cell *matCellDef="let row">
9797
@for (projectBook of row.translationBooks; track projectBook.projectId) {
98-
<div class="project-books-line">
98+
<div>
9999
<a [routerLink]="['/serval-administration', projectBook.projectId]" class="project-link">{{
100100
getProjectShortName(projectBook.projectId)
101101
}}</a
@@ -142,22 +142,19 @@
142142
></app-owner>
143143
</td>
144144
</ng-container>
145-
<ng-container matColumnDef="buildId">
146-
<th mat-header-cell *matHeaderCellDef>Build ID & ClearML link</th>
145+
<ng-container matColumnDef="buildDetails">
146+
<th mat-header-cell *matHeaderCellDef>Build Details</th>
147147
<td mat-cell *matCellDef="let row">
148-
<a [href]="row.clearmlUrl" target="_blank" rel="noopener noreferrer" matTooltip="Open in ClearML">
149-
{{ row.job.buildId }}
148+
<a
149+
(click)="openJobDetailsDialog(row.job)"
150+
class="build-id-link"
151+
[class.disabled]="!row.job.buildId"
152+
matTooltip="View complete job details including build information and events"
153+
>
154+
{{ row.job.buildId || "N/A" }}
150155
</a>
151156
</td>
152157
</ng-container>
153-
<ng-container matColumnDef="details">
154-
<th mat-header-cell *matHeaderCellDef>Events</th>
155-
<td mat-cell *matCellDef="let row">
156-
<button mat-stroked-button (click)="openDetailsDialog(row.job)" color="primary" class="details-button">
157-
<mat-icon class="mirror-rtl">list</mat-icon> Events
158-
</button>
159-
</td>
160-
</ng-container>
161158
</table>
162159
</div>
163160

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/draft-jobs.component.scss

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,22 @@
99
background-color: var(--sf-draft-jobs-table-row-background);
1010
}
1111

12-
app-owner,
13-
.details-button {
12+
app-owner {
1413
white-space: nowrap;
1514
}
1615

16+
.build-id-link {
17+
text-decoration: underline;
18+
cursor: pointer;
19+
white-space: nowrap;
20+
21+
&.disabled {
22+
color: var(--sf-draft-jobs-disabled-link-color);
23+
text-decoration: none;
24+
cursor: default;
25+
}
26+
}
27+
1728
.status-cell {
1829
display: flex;
1930
align-items: center;

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/draft-jobs.component.ts

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { SFProjectService } from '../core/sf-project.service';
1616
import { EventMetric } from '../event-metrics/event-metric';
1717
import { NoticeComponent } from '../shared/notice/notice.component';
1818
import { projectLabel } from '../shared/utils';
19-
import { JobEventsDialogComponent } from './job-events-dialog.component';
19+
import { JobDetailsDialogComponent } from './job-details-dialog.component';
2020
import { ServalAdministrationService } from './serval-administration.service';
2121

2222
/**
@@ -36,6 +36,7 @@ interface DraftJob {
3636
finishEvent?: EventMetric;
3737
cancelEvent?: EventMetric;
3838
events: EventMetric[];
39+
additionalEvents: EventMetric[]; // Events with same build ID that weren't included in the main job tracking
3940
status: 'running' | 'success' | 'failed' | 'cancelled' | 'incomplete';
4041
startTime: Date | null;
4142
finishTime: Date | null;
@@ -61,6 +62,14 @@ interface Row {
6162
clearmlUrl?: string;
6263
}
6364

65+
const DRAFTING_EVENTS = [
66+
'StartPreTranslationBuildAsync',
67+
'BuildProjectAsync',
68+
'RetrievePreTranslationStatusAsync',
69+
'ExecuteWebhookAsync',
70+
'CancelPreTranslationBuildAsync'
71+
];
72+
6473
@Component({
6574
selector: 'app-draft-jobs',
6675
templateUrl: './draft-jobs.component.html',
@@ -75,8 +84,7 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
7584
'translationBooks',
7685
'startTime',
7786
'duration',
78-
'author',
79-
'buildId'
87+
'author'
8088
];
8189
rows: Row[] = [];
8290

@@ -102,7 +110,7 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
102110
this.daysBack$.next(value);
103111
}
104112

105-
private eventMetrics?: EventMetric[];
113+
private draftEvents?: EventMetric[];
106114
private draftJobs: DraftJob[] = [];
107115
private projectNames = new Map<string, string | null>(); // Cache for project names
108116
private projectShortNames = new Map<string, string | null>(); // Cache for project short names
@@ -124,16 +132,16 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
124132
}
125133

126134
get isLoading(): boolean {
127-
return this.eventMetrics == null;
135+
return this.draftEvents == null;
128136
}
129137

130138
ngOnInit(): void {
131139
if (
132-
!this.columnsToDisplay.includes('details') &&
140+
!this.columnsToDisplay.includes('buildDetails') &&
133141
(this.authService.currentUserRoles.includes(SystemRole.ServalAdmin) ||
134142
this.authService.currentUserRoles.includes(SystemRole.SystemAdmin))
135143
) {
136-
this.columnsToDisplay.push('details');
144+
this.columnsToDisplay.push('buildDetails');
137145
}
138146
this.loadingStarted();
139147

@@ -159,14 +167,15 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
159167
}
160168

161169
if (isOnline) {
162-
const queryResults = await this.projectService.onlineAllEventMetricsForConstructionDraftJobs(
170+
const queryResults = await this.projectService.onlineAllEventMetricsForConstructingDraftJobs(
171+
DRAFTING_EVENTS,
163172
projectFilterId ?? undefined,
164173
daysBack === 'all_time' ? undefined : daysBack
165174
);
166175
if (Array.isArray(queryResults?.results)) {
167-
this.eventMetrics = queryResults.results as EventMetric[];
176+
this.draftEvents = queryResults.results as EventMetric[];
168177
} else {
169-
this.eventMetrics = [];
178+
this.draftEvents = [];
170179
}
171180
this.processDraftJobs();
172181
await this.loadProjectNames();
@@ -178,51 +187,73 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
178187
.subscribe();
179188
}
180189

181-
openDetailsDialog(job: DraftJob): void {
190+
openJobDetailsDialog(job: DraftJob): void {
191+
if (job.buildId == null) {
192+
void this.noticeService.show('No build ID available for this job');
193+
return;
194+
}
195+
196+
// Format event-based duration if available
197+
const eventBasedDuration = job.duration != null ? this.formatDuration(job.duration) : undefined;
198+
199+
// Format start time if available
200+
const startTime = job.startTime != null ? this.i18n.formatDate(job.startTime, { showTimeZone: true }) : undefined;
201+
182202
// Collect all events that were used to create this job
183203
const events = [job.startEvent, job.buildEvent, job.finishEvent, job.cancelEvent]
184204
.filter((event): event is EventMetric => event != null)
185205
.sort((a, b) => new Date(a.timeStamp).getTime() - new Date(b.timeStamp).getTime());
186206

207+
// Get ClearML URL
208+
const clearmlUrl = job.buildId
209+
? `https://app.sil.hosted.allegro.ai/projects?gq=${job.buildId}&tab=tasks`
210+
: undefined;
211+
212+
// Count how many builds have started on this project since this build started
213+
let buildsStartedSince = 0;
214+
if (job.startTime != null) {
215+
const jobStartTime = job.startTime;
216+
buildsStartedSince = this.draftJobs.filter(
217+
otherJob =>
218+
otherJob.projectId === job.projectId && otherJob.startTime != null && otherJob.startTime > jobStartTime
219+
).length;
220+
}
221+
187222
const dialogData = {
223+
buildId: job.buildId,
188224
projectId: job.projectId,
189225
jobStatus: this.getStatusDisplay(job.status),
190-
events
226+
events,
227+
additionalEvents: job.additionalEvents,
228+
eventBasedDuration,
229+
startTime,
230+
clearmlUrl,
231+
buildsStartedSince
191232
};
192233

193-
const dialogConfig: MatDialogConfig<any> = { data: dialogData, width: '800px', maxHeight: '80vh' };
194-
this.dialogService.openMatDialog(JobEventsDialogComponent, dialogConfig);
234+
const dialogConfig: MatDialogConfig<any> = { data: dialogData, width: '1000px', maxHeight: '80vh' };
235+
this.dialogService.openMatDialog(JobDetailsDialogComponent, dialogConfig);
195236
}
196237

197238
clearProjectFilter(): void {
198-
this.router.navigate([], {
239+
void this.router.navigate([], {
199240
relativeTo: this.route,
200241
queryParams: { projectId: null },
201242
queryParamsHandling: 'merge'
202243
});
203244
}
204245

205246
private processDraftJobs(): void {
206-
if (this.eventMetrics == null) {
247+
if (this.draftEvents == null) {
207248
this.draftJobs = [];
208249
this.generateRows();
209250
return;
210251
}
211252

212-
// Filter draft-related events
213-
const draftEvents = this.eventMetrics.filter(
214-
event =>
215-
event.eventType === 'StartPreTranslationBuildAsync' ||
216-
event.eventType === 'BuildProjectAsync' ||
217-
event.eventType === 'RetrievePreTranslationStatusAsync' ||
218-
event.eventType === 'ExecuteWebhookAsync' ||
219-
event.eventType === 'CancelPreTranslationBuildAsync'
220-
);
221-
222253
const jobs: DraftJob[] = [];
223254

224255
// Step 1: Find all build events (BuildProjectAsync) - these are our anchors
225-
const buildEvents = draftEvents.filter(event => event.eventType === 'BuildProjectAsync');
256+
const buildEvents = this.draftEvents.filter(event => event.eventType === 'BuildProjectAsync');
226257

227258
// Step 2: For each build event, find the nearest preceding start event
228259
for (const buildEvent of buildEvents) {
@@ -234,7 +265,7 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
234265
const buildTime = new Date(buildEvent.timeStamp);
235266

236267
// Find all StartPreTranslationBuildAsync events for this project that precede the build
237-
const candidateStartEvents = draftEvents.filter(
268+
const candidateStartEvents = this.draftEvents.filter(
238269
event =>
239270
event.eventType === 'StartPreTranslationBuildAsync' &&
240271
event.projectId === buildEvent.projectId &&
@@ -252,7 +283,7 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
252283
});
253284

254285
// Step 3: Find the first completion event after the build
255-
const candidateCompletionEvents = draftEvents.filter(event => {
286+
const candidateCompletionEvents = this.draftEvents.filter(event => {
256287
if (event.projectId !== buildEvent.projectId) return false;
257288
if (new Date(event.timeStamp) <= buildTime) return false;
258289

@@ -295,6 +326,7 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
295326
finishEvent: undefined,
296327
cancelEvent: undefined,
297328
events: [],
329+
additionalEvents: [],
298330
startTime: new Date(startEvent.timeStamp),
299331
userId: startEvent.userId,
300332
trainingBooks,
@@ -314,6 +346,22 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
314346
}
315347
}
316348

349+
// Step 4: Collect additional events (any events with same build ID that weren't already included)
350+
const additionalEvents = this.draftEvents.filter(event => {
351+
// Don't include events we've already tracked
352+
if (event === startEvent || event === buildEvent || event === completionEvent) {
353+
return false;
354+
}
355+
356+
// Check if this event has the same build ID
357+
const eventBuildId = this.extractBuildIdFromEvent(event);
358+
return eventBuildId === buildId;
359+
});
360+
361+
job.additionalEvents = additionalEvents.sort(
362+
(a, b) => new Date(a.timeStamp).getTime() - new Date(b.timeStamp).getTime()
363+
);
364+
317365
jobs.push(job);
318366
}
319367

0 commit comments

Comments
 (0)