Skip to content

Commit 47ee299

Browse files
authored
Draft jobs duration mean: only include successful jobs (#3611)
1 parent 7e567d4 commit 47ee299

File tree

13 files changed

+108
-76
lines changed

13 files changed

+108
-76
lines changed

.github/copilot-instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ This repository contains three interconnected applications:
106106
- Corner-cases happen. They should be handled in code.
107107
- Please don't change existing code without good justification. Existing code largely works and changing it will cause work for code review. Leave existing code as is when possible.
108108

109+
# Frontend code
110+
111+
- Pay attention to available types and type guards in src/SIL.XForge.Scripture/ClientApp/src/type-utils.ts.
112+
109113
# Running commands
110114

111115
- If you run frontend tests, run them in the `src/SIL.XForge.Scripture/ClientApp` directory with a command such as `npm run test:headless -- --watch=false --include '**/text.component.spec.ts' --include '**/settings.component.spec.ts'`

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
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))};
99
--sf-draft-jobs-disabled-link-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 50, 50))};
10-
--sf-draft-jobs-statistics-background: #{mat.get-theme-color($theme, neutral-variant, if($is-dark, 30, 90))};
1110
--sf-draft-jobs-statistics-text: #{mat.get-theme-color($theme, on-surface)};
1211
}
1312

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/draft-jobs-export.service.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ describe('DraftJobsExportService', () => {
9090

9191
// Check mean duration row (index 3) - (30 + 60) / 2 = 45 minutes
9292
// Label in servalBuildId, value in startTime
93-
expect(spreadsheetRows[3].servalBuildId).toBe('Mean duration minutes');
93+
expect(spreadsheetRows[3].servalBuildId).toMatch(/Mean/);
9494
expect(spreadsheetRows[3].startTime).toBe('45');
9595

9696
// Check max duration row (index 4) - max(30, 60) = 60 minutes
9797
// Label in servalBuildId, value in startTime
98-
expect(spreadsheetRows[4].servalBuildId).toBe('Max duration minutes');
98+
expect(spreadsheetRows[4].servalBuildId).toMatch(/Max/);
9999
expect(spreadsheetRows[4].startTime).toBe('60');
100100
});
101101

@@ -109,10 +109,10 @@ describe('DraftJobsExportService', () => {
109109

110110
// Check statistics are 0 when no data
111111
// Label in servalBuildId, value in startTime
112-
expect(spreadsheetRows[1].servalBuildId).toBe('Mean duration minutes');
112+
expect(spreadsheetRows[1].servalBuildId).toMatch(/Mean/);
113113
expect(spreadsheetRows[1].startTime).toBe('0');
114114

115-
expect(spreadsheetRows[2].servalBuildId).toBe('Max duration minutes');
115+
expect(spreadsheetRows[2].servalBuildId).toMatch(/Max/);
116116
expect(spreadsheetRows[2].startTime).toBe('0');
117117
});
118118
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class DraftJobsExportService {
124124

125125
// Append mean duration row. This is not in a 'correct column', but puts some desired data into the output.
126126
const meanRow: SpreadsheetRow = {
127-
servalBuildId: 'Mean duration minutes',
127+
servalBuildId: 'Mean successful duration minutes',
128128
startTime: meanDuration != null ? this.msToMinutes(meanDuration).toFixed(0) : '0',
129129
status: '',
130130
sfProjectId: '',
@@ -135,7 +135,7 @@ export class DraftJobsExportService {
135135

136136
// Append max duration row
137137
const maxRow: SpreadsheetRow = {
138-
servalBuildId: 'Max duration minutes',
138+
servalBuildId: 'Max all durations minutes',
139139
startTime: maxDuration != null ? this.msToMinutes(maxDuration).toFixed(0) : '0',
140140
status: '',
141141
sfProjectId: '',

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@
1616
<span>Export RSV</span>
1717
</button>
1818
</mat-menu>
19-
@if (meanDurationFormatted != null || maxDurationFormatted != null) {
20-
<div class="duration-statistics">
21-
@if (meanDurationFormatted != null) {
22-
<span class="stat-item">Mean duration: {{ meanDurationFormatted }}</span>
23-
}
24-
@if (maxDurationFormatted != null) {
25-
<span class="stat-item">Max duration: {{ maxDurationFormatted }}</span>
26-
}
19+
<div class="duration-area">
20+
<div class="duration-area-stat">
21+
<span class="duration-area-stat-label"> Mean successful duration </span>
22+
<span> {{ meanDurationFormatted ?? "n/a" }} </span>
2723
</div>
28-
}
24+
<div class="duration-area-stat">
25+
<span class="duration-area-stat-label"> Max all durations </span
26+
><span> {{ maxDurationFormatted ?? "n/a" }} </span>
27+
</div>
28+
</div>
2929
<div class="grouping-toggle">
3030
<div class="grouping-toggle-label">
3131
Determination

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,19 +116,21 @@
116116
}
117117
}
118118

119-
.duration-statistics {
119+
.duration-area {
120+
padding: 1rem;
120121
display: flex;
121-
gap: 16px;
122-
padding: 8px 16px;
123-
background-color: var(--sf-draft-jobs-statistics-background);
124-
border-radius: 4px;
125-
font-size: 14px;
126-
margin-bottom: 2rem;
127-
128-
.stat-item {
129-
color: var(--sf-draft-jobs-statistics-text);
130-
font-weight: 500;
131-
}
122+
flex-direction: column;
123+
gap: 0.5rem;
124+
}
125+
126+
.duration-area-stat {
127+
display: flex;
128+
justify-content: space-between;
129+
gap: 1rem;
130+
}
131+
132+
.duration-area-stat-label {
133+
font-weight: 500;
132134
}
133135

134136
.grouping-toggle {

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { configureTestingModule, getTestTranslocoModule } from 'xforge-common/te
2121
import { SF_TYPE_REGISTRY } from '../core/models/sf-type-registry';
2222
import { SFProjectService } from '../core/sf-project.service';
2323
import { EventMetric, EventScope } from '../event-metrics/event-metric';
24-
import { DraftJob, DraftJobsComponent } from './draft-jobs.component';
24+
import { DraftJob, DraftJobsComponent, DraftJobsTableRow, DraftJobStatus } from './draft-jobs.component';
2525
import { JobDetailsDialogComponent } from './job-details-dialog.component';
2626
import sampleEvents from './sample-events.json';
2727
import { ServalAdministrationService } from './serval-administration.service';
@@ -523,6 +523,19 @@ describe('DraftJobsComponent', () => {
523523
// Mean: (3600000 + 7200000) / 2 = 5400000
524524
expect(env.component.meanDuration).toBe(5400000);
525525
}));
526+
527+
it('should ignore non-successful jobs when calculating mean', fakeAsync(() => {
528+
const env = new TestEnvironment({ hasEvents: false });
529+
env.wait();
530+
env.component.rows = [
531+
env.createRowWithDuration(3600000, 'success'), // counts
532+
env.createRowWithDuration(5400000, 'running'), // excluded because not success
533+
env.createRowWithDuration(7200000, 'failed') // excluded because not success
534+
];
535+
536+
// Mean: only the successful job's duration should be counted
537+
expect(env.component.meanDuration).toBe(3600000);
538+
}));
526539
});
527540

528541
describe('maxDuration', () => {
@@ -727,12 +740,13 @@ class TestEnvironment {
727740
this.component.onGroupingModeChange(mode);
728741
}
729742

730-
createRowWithDuration(duration: number | undefined): any {
743+
createRowWithDuration(duration: number | undefined, statusOverride?: DraftJobStatus): DraftJobsTableRow {
744+
const jobStatus: DraftJobStatus = statusOverride ?? (duration != null ? 'success' : 'running');
731745
return {
732746
job: {
733747
buildId: 'build-123',
734748
projectId: 'project1',
735-
status: duration != null ? 'success' : 'running',
749+
status: jobStatus,
736750
startTime: new Date('2025-01-15T10:00:00Z'),
737751
finishTime: duration != null ? new Date('2025-01-15T11:00:00Z') : undefined,
738752
duration: duration,
@@ -747,7 +761,7 @@ class TestEnvironment {
747761
projectDeleted: false,
748762
startTimeStamp: '2025-01-15 10:00 UTC',
749763
duration: duration != null ? '1h 0m 0s' : undefined,
750-
status: duration != null ? 'Success' : 'Running',
764+
status: this.component['getStatusDisplay'](jobStatus),
751765
userId: 'user123',
752766
trainingBooks: [],
753767
translationBooks: []

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ import { I18nService } from 'xforge-common/i18n.service';
2929
import { NoticeService } from 'xforge-common/notice.service';
3030
import { OnlineStatusService } from 'xforge-common/online-status.service';
3131
import { OwnerComponent } from 'xforge-common/owner/owner.component';
32-
import { isPopulatedString } from 'xforge-common/utils';
33-
import { hasElements, notNull, PopulatedArray } from '../../type-utils';
32+
import { hasElements, isPopulatedString, notNull, PopulatedArray } from '../../type-utils';
3433
import { SFProjectService } from '../core/sf-project.service';
3534
import { EventMetric } from '../event-metrics/event-metric';
3635
import { InfoComponent } from '../shared/info/info.component';
@@ -40,6 +39,10 @@ import { DateRangePickerComponent, NormalizedDateRange } from './date-range-pick
4039
import { DraftJobsExportService } from './draft-jobs-export.service';
4140
import { JobDetailsDialogComponent } from './job-details-dialog.component';
4241
import { ServalAdministrationService } from './serval-administration.service';
42+
43+
/** Outcome or in-progress situation of a draft generation request job. */
44+
export type DraftJobStatus = 'running' | 'success' | 'failed' | 'cancelled' | 'incomplete';
45+
4346
interface ProjectBooks {
4447
projectId: string;
4548
books: string[];
@@ -60,7 +63,7 @@ export interface DraftJob {
6063
events: EventMetric[];
6164
/** Events with the same build ID that weren't included in the main job tracking */
6265
additionalEvents: EventMetric[];
63-
status: 'running' | 'success' | 'failed' | 'cancelled' | 'incomplete';
66+
status: DraftJobStatus;
6467
startTime: Date | undefined;
6568
finishTime: Date | undefined;
6669
duration: number | undefined;
@@ -183,9 +186,12 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
183186

184187
/** Of jobs in the table. */
185188
get meanDuration(): number | undefined {
186-
const durations = this.rows.map(r => r.job.duration).filter((d): d is number => d != null);
187-
if (durations.length === 0) return undefined;
188-
return durations.reduce((sum, d) => sum + d, 0) / durations.length;
189+
const successfulDurations: number[] = this.rows
190+
.filter(row => row.job.status === 'success')
191+
.map(row => row.job.duration)
192+
.filter(notNull);
193+
if (successfulDurations.length === 0) return undefined;
194+
return successfulDurations.reduce((sum, duration) => sum + duration, 0) / successfulDurations.length;
189195
}
190196

191197
get maxDuration(): number | undefined {
@@ -660,7 +666,7 @@ export class DraftJobsComponent extends DataLoadingComponent implements OnInit {
660666
}
661667

662668
private finalizeJobStatus(job: DraftJob): void {
663-
let status: 'running' | 'success' | 'failed' | 'cancelled' | 'incomplete';
669+
let status: DraftJobStatus;
664670
let finishTime: Date | undefined = undefined;
665671
let duration: number | undefined = undefined;
666672
let errorMessage: string | undefined;

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/job-details-dialog.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { MatTab, MatTabGroup } from '@angular/material/tabs';
1313
import { MatTooltip } from '@angular/material/tooltip';
1414
import { I18nService } from 'xforge-common/i18n.service';
1515
import { L10nNumberPipe } from 'xforge-common/l10n-number.pipe';
16-
import { isPopulatedString } from 'xforge-common/utils';
16+
import { isPopulatedString } from '../../type-utils';
1717
import { EventMetric } from '../event-metrics/event-metric';
1818
import { JsonViewerComponent } from '../shared/json-viewer/json-viewer.component';
1919
import { NoticeComponent } from '../shared/notice/notice.component';

src/SIL.XForge.Scripture/ClientApp/src/type-utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,10 @@ export type PopulatedArray<T> = [T, ...T[]] | [...T[], T] | [T, ...T[], T];
131131
export function hasElements<T>(array: T[]): array is PopulatedArray<T> {
132132
return array.length > 0;
133133
}
134+
135+
/** The input is a string, and it is not empty (''). It is not null or undefined. This method could alternatively be known as "isNonEmptyString".
136+
*
137+
* If the negation of this is true, then the input is '' or null or undefined or another non-string. */
138+
export function isPopulatedString(value: unknown): value is Exclude<string, ''> {
139+
return typeof value === 'string' && value !== '';
140+
}

0 commit comments

Comments
 (0)