Skip to content

Commit 7967398

Browse files
committed
Add Serval builds tab
1 parent 8edcc79 commit 7967398

File tree

46 files changed

+5734
-201
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5734
-201
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
"omnisharp.enableRoslynAnalyzers": true,
118118
"python.formatting.provider": "black",
119119
"deno.enablePaths": ["./src/SIL.XForge.Scripture/ClientApp/e2e", "./scripts/"],
120-
"deno.disablePaths": ["./scripts/db_tools/"],
120+
"deno.disablePaths": ["./scripts/db_tools/", "./src"],
121121
"typescript.tsdk": "./src/SIL.XForge.Scripture/ClientApp/node_modules/typescript/lib",
122122
"typescript.enablePromptUseWorkspaceTsdk": true,
123123
"typescript.preferences.importModuleSpecifier": "relative",

AGENTS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ This repository contains three interconnected applications:
3939
- Localizations that a Community Checker user might see should be created or edited in src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/checking_en.json. Only localizations that a Community Checker user will not see can be created or edited in src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json.
4040
- Even if something is a system-wide feature that isn't specific to community checking functionality, it should still be placed in checking_en.json if a community checking user would POSSIBLY see it.
4141

42+
# Frontend styling
43+
44+
- Avoid making up and using hard-coded or new color values. Where feasible, use color values from [Material Design](https://material.angular.dev/guide/theming-your-components). For example, `--mat-sys-surface` and `--mat-sys-on-primary`. If you can't get close to what you want from Material Design colors, you can use a defined color in `_variables.scss` or `material-styles.scss`. Follow patterns in existing `_foo-theme.scss` files.
45+
46+
# Frontend user interface
47+
48+
- Use Sentence case for user interface elements, per Material Design. Do not use Title Case for user interface elements. For example, use "Project settings" rather than "Project Settings".
49+
4250
# Frontend testing
4351

4452
- Write unit tests for new components and services
@@ -95,11 +103,34 @@ This repository contains three interconnected applications:
95103
`const buildEvent: EventMetric | undefined = buildEvents[0];`.
96104
- Prefer to use `null` to express a deliberate absence of a value, and `undefined` to express a value that has not been set yet.
97105
- Observables (including subclasses such as Subject or BehaviorSubject) should have names that end with a `$`.
106+
- Do not use the `!` non-null assertion operator. For example, do not write `foo!.baz()`. If you need to dereference `foo`, first prove to the type system that `foo` is not null either by using a type guard or checking for null. You may use the `!` non-null assertion operator in spec test files.
107+
- Do not use the `as` type assertion operator. For example, do not write `const foo: SomeType = someValue as SomeType`. If you need to treat `someValue` as `SomeType`, first prove to the type system that `someValue` is of type `SomeType` such as by using a type guard. You may use the `as` type assertion operator in spec test files.
108+
- Do not use object property shorthand when creating objects. For example, do not write `const obj = { foo, bar };`. Instead, write `const obj = { foo: foo, bar: bar };`.
109+
- Do not reorder existing fields and methods to comply with this, but when creating new fields and methods in TypeScript classes, use this order:
110+
1. public static fields
111+
2. protected static fields
112+
3. private static fields
113+
4. @Input, @Output, and @ViewChild fields
114+
5. public instance fields
115+
6. protected instance fields
116+
7. private instance fields
117+
8. constructor
118+
9. getters and setters
119+
10. public static methods
120+
11. protected static methods
121+
12. private static methods
122+
13. public instance methods
123+
14. protected instance methods
124+
15. private instance methods
98125

99126
# Angular templates
100127

101128
- Use `@if {}` syntax rather than `*ngIf` syntax.
102129

130+
# C# language
131+
132+
- Do not use the `!` null-forgiving operator. Instead, check for null and cause a specific outcome if it is null, or make use of the `NotNullWhen` attribute. You may use the `!` null-forgiving operator in test files.
133+
103134
# Code
104135

105136
- All code that you write should be able to pass eslint linting tests for TypeScript, or csharpier for C#.
@@ -108,6 +139,8 @@ This repository contains three interconnected applications:
108139
- It is better to explicitly check for and handle problems, or prevent problems from happening, than to assume problems will not happen.
109140
- Corner-cases happen. They should be handled in code.
110141
- 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.
142+
- Avoid magic numbers where not obvious. Use a named constant for the value instead, which can be defined right before usage.
143+
- Don't write useless comments. For example, for field `translationEngineId`, comment "The translation engine ID." adds no additional information than the name of the field already says.
111144

112145
# Frontend code
113146

@@ -117,3 +150,4 @@ This repository contains three interconnected applications:
117150

118151
- 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'`
119152
- If you need to run all frontend tests, you can run them in the `src/SIL.XForge.Scripture/ClientApp` directory with command `npm run test:headless -- --watch=false`
153+
- If you run backend dotnet tests, run them in the repository root directory with a command such as `dotnet test`.

src/SIL.XForge.Scripture/ClientApp/src/app/machine-api/build-dto.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,21 @@ export interface BuildDto extends ResourceDto {
1010
state: BuildStates;
1111
queueDepth: number;
1212
additionalInfo?: ServalBuildAdditionalInfo;
13+
/** The Serval deployment version that executed this build. */
14+
deploymentVersion?: string;
15+
/** Execution data from the Serval build, including training/pretranslation counts and language tags. */
16+
executionData?: BuildExecutionData;
1317
}
1418

19+
/** Execution data from a Serval translation build. */
20+
export interface BuildExecutionData {
21+
trainCount: number;
22+
pretranslateCount: number;
23+
sourceLanguageTag?: string;
24+
targetLanguageTag?: string;
25+
}
26+
27+
/** Additional information about a Serval build. */
1528
export interface ServalBuildAdditionalInfo {
1629
buildId: string;
1730
corporaIds?: string[];

src/SIL.XForge.Scripture/ClientApp/src/app/machine-api/remote-translation-engine.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,19 @@ describe('RemoteTranslationEngine', () => {
8686
env.addCreateBuild();
8787

8888
when(env.mockedHttpClient.post<BuildDto>('translation/builds', JSON.stringify('project01'))).thenReturn(
89-
of({ status: 200 })
89+
of({
90+
status: 200,
91+
data: {
92+
id: 'build01',
93+
href: 'translation/builds/id:build01',
94+
revision: 0,
95+
engine: { id: 'engine01', href: 'translation/engines/id:engine01' },
96+
percentCompleted: 0,
97+
message: '',
98+
state: BuildStates.Pending,
99+
queueDepth: 0
100+
}
101+
})
90102
);
91103

92104
await env.client.startTraining();

src/SIL.XForge.Scripture/ClientApp/src/app/machine-api/remote-translation-engine.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,21 @@ export class RemoteTranslationEngine implements InteractiveTranslationEngine {
164164
}
165165

166166
private getEngine(projectId: string): Observable<EngineDto> {
167-
return this.httpClient
168-
.get<EngineDto>(`translation/engines/project:${projectId}`)
169-
.pipe(map(res => res.data as EngineDto));
167+
return this.httpClient.get<EngineDto>(`translation/engines/project:${projectId}`).pipe(
168+
map(res => {
169+
if (res.data == null) throw new Error('Unexpectedly received no engine data.');
170+
return res.data;
171+
})
172+
);
170173
}
171174

172175
private createBuild(engineId: string): Observable<BuildDto> {
173-
return this.httpClient
174-
.post<BuildDto>('translation/builds', JSON.stringify(engineId))
175-
.pipe(map(res => res.data as BuildDto));
176+
return this.httpClient.post<BuildDto>('translation/builds', JSON.stringify(engineId)).pipe(
177+
map(res => {
178+
if (res.data == null) throw new Error('Unexpectedly received no build data.');
179+
return res.data;
180+
})
181+
);
176182
}
177183

178184
private pollBuildProgress(locator: string, minRevision: number): Observable<ProgressStatus> {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@use 'sass:color';
2+
@use '@angular/material' as mat;
3+
@use '../../_variables' as vars;
4+
5+
/**
6+
* Theme for the Serval Builds component.
7+
*/
8+
@mixin color($theme) {
9+
$is-dark: mat.get-theme-type($theme) == dark;
10+
11+
// Status icon colors
12+
--sf-serval-builds-status-userrequested: #{mat.get-theme-color($theme, neutral, if($is-dark, 40, 60))};
13+
--sf-serval-builds-status-submittedtoserval: #{mat.get-theme-color($theme, neutral, if($is-dark, 40, 60))};
14+
--sf-serval-builds-status-queued: #{mat.get-theme-color($theme, neutral, if($is-dark, 40, 60))};
15+
--sf-serval-builds-status-pending: #{mat.get-theme-color($theme, neutral, if($is-dark, 40, 60))};
16+
--sf-serval-builds-status-active: #{if($is-dark, #4fa3ff, #007fff)};
17+
--sf-serval-builds-status-finishing: #{if($is-dark, #4fa3ff, #007fff)};
18+
--sf-serval-builds-status-completed: #{if($is-dark, lightGreen, darkGreen)};
19+
--sf-serval-builds-status-faulted: #{mat.get-theme-color($theme, error, if($is-dark, 70, 40))};
20+
--sf-serval-builds-status-canceled: #{mat.get-theme-color($theme, neutral, if($is-dark, 60, 50))};
21+
22+
// Problem/warning indicator
23+
--sf-serval-builds-problem-color: #{if($is-dark, #dbdb00, #ffcc00)};
24+
--sf-serval-builds-problem-border-color: var(--mat-table-background-color);
25+
26+
// Table
27+
--sf-serval-builds-table-odd-row-background: #{if(
28+
$is-dark,
29+
var(--mat-sys-surface-container),
30+
var(--mat-sys-surface-container-high)
31+
)};
32+
--sf-serval-builds-table-odd-row-expanded-background: #{if(
33+
$is-dark,
34+
var(--mat-sys-surface-container),
35+
var(--mat-sys-surface-container-high)
36+
)};
37+
--sf-serval-builds-table-odd-row-detail-card-background: #{if(
38+
$is-dark,
39+
var(--mat-sys-surface-container-highest),
40+
var(--mat-sys-surface-dim)
41+
)};
42+
--sf-serval-builds-table-even-row-detail-card-background: #{if(
43+
$is-dark,
44+
var(--mat-sys-surface-container-low),
45+
var(--mat-sys-surface-container)
46+
)};
47+
48+
// Links and text
49+
--sf-serval-builds-project-link-color: #{mat.get-theme-color($theme, primary, if($is-dark, 70, 40))};
50+
--sf-serval-builds-subdued-text: #{mat.get-theme-color($theme, neutral, if($is-dark, 70, 50))};
51+
--sf-serval-builds-label-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 80, 50))};
52+
--sf-serval-builds-duration-bar-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 40, 80))};
53+
54+
// Deleted project indicator
55+
--sf-serval-builds-deleted-project-color: #{mat.get-theme-color($theme, neutral, if($is-dark, 60, 50))};
56+
--sf-serval-builds-deleted-icon-color: #{mat.get-theme-color($theme, error, if($is-dark, 70, 40))};
57+
}
58+
59+
@mixin theme($theme) {
60+
@if mat.theme-has($theme, color) {
61+
@include color($theme);
62+
}
63+
}

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/date-range-picker.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class DateRangePickerComponent implements OnInit {
6868
end: FormControl<Date | null>;
6969
}>;
7070

71-
private readonly defaultDaysBack = 14;
71+
private readonly defaultDaysBack = 5;
7272

7373
/** Event emitted when the date range changes with a valid normalized range */
7474
@Output() dateRangeChange = new EventEmitter<NormalizedDateRange>();

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

Lines changed: 32 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,38 @@ import { getTestTranslocoModule } from 'xforge-common/test-utils';
44
import { DraftJobsExportService, SpreadsheetRow } from './draft-jobs-export.service';
55

66
describe('DraftJobsExportService', () => {
7-
describe('createSpreadsheetRows', () => {
8-
it('should export with correct headers and data', () => {
9-
const env = new TestEnvironment();
10-
const spreadsheetRows: SpreadsheetRow[] = (env.service as any).createSpreadsheetRows([
11-
{
12-
job: {
13-
buildId: 'build-123',
14-
startTime: new Date('2025-01-15T10:00:00Z'),
15-
finishTime: new Date('2025-01-15T11:00:00Z'),
16-
duration: 3600000 // 1 hour in milliseconds
17-
},
18-
projectId: 'project1',
19-
projectName: 'Test Project',
20-
status: 'Success',
21-
userId: 'user123',
22-
trainingBooks: [{ projectId: 'project1', books: ['GEN'] }],
23-
translationBooks: [{ projectId: 'project1', books: ['MAT'] }]
24-
}
25-
]);
26-
27-
expect(spreadsheetRows.length).toEqual(1);
28-
expect(spreadsheetRows[0]).toEqual({
29-
servalBuildId: 'build-123',
30-
startTime: '2025-01-15T10:00:00.000Z',
31-
endTime: '2025-01-15T11:00:00.000Z',
32-
durationMinutes: '60',
33-
status: 'Success',
34-
sfProjectId: 'project1',
35-
projectName: 'Test Project',
36-
sfUserId: 'user123',
37-
trainingBooks: 'project1: GEN',
38-
translationBooks: 'project1: MAT'
39-
});
40-
});
41-
});
42-
43-
describe('createSpreadsheetRowsWithStatistics', () => {
7+
describe('addStatistics', () => {
448
it('should append blank row and statistics rows with mean and max duration', () => {
459
const env = new TestEnvironment();
46-
const testRows = [
10+
const testRows: SpreadsheetRow[] = [
4711
{
48-
job: {
49-
buildId: 'build-1',
50-
startTime: new Date('2025-01-15T10:00:00Z'),
51-
finishTime: new Date('2025-01-15T10:30:00Z'),
52-
duration: 1800000 // 30 minutes in milliseconds
53-
},
54-
projectId: 'project1',
55-
projectName: 'Project 1',
12+
servalBuildId: 'build-1',
13+
startTime: '2025-01-15T10:00:00.000Z',
14+
endTime: '2025-01-15T10:30:00.000Z',
15+
durationMinutes: '30',
5616
status: 'Success',
57-
userId: 'user1',
58-
trainingBooks: [],
59-
translationBooks: []
17+
sfProjectId: 'project1',
18+
projectName: 'Project 1',
19+
sfUserId: 'user1',
20+
trainingBooks: '',
21+
translationBooks: ''
6022
},
6123
{
62-
job: {
63-
buildId: 'build-2',
64-
startTime: new Date('2025-01-15T11:00:00Z'),
65-
finishTime: new Date('2025-01-15T12:00:00Z'),
66-
duration: 3600000 // 60 minutes in milliseconds
67-
},
68-
projectId: 'project1',
69-
projectName: 'Project 1',
24+
servalBuildId: 'build-2',
25+
startTime: '2025-01-15T11:00:00.000Z',
26+
endTime: '2025-01-15T12:00:00.000Z',
27+
durationMinutes: '60',
7028
status: 'Success',
71-
userId: 'user2',
72-
trainingBooks: [],
73-
translationBooks: []
29+
sfProjectId: 'project1',
30+
projectName: 'Project 1',
31+
sfUserId: 'user2',
32+
trainingBooks: '',
33+
translationBooks: ''
7434
}
7535
];
7636

7737
// SUT: create rows with the test data
78-
const spreadsheetRows: SpreadsheetRow[] = (env.service as any).createSpreadsheetRowsWithStatistics(
79-
testRows,
80-
2700000,
81-
3600000
82-
);
38+
const spreadsheetRows: SpreadsheetRow[] = (env.service as any).addStatistics(testRows, 2700000, 3600000);
8339

8440
// Should have 2 data rows + 1 blank row + 2 statistics rows = 5 total
8541
expect(spreadsheetRows.length).toEqual(5);
@@ -102,7 +58,7 @@ describe('DraftJobsExportService', () => {
10258
it('should handle empty rows array', () => {
10359
const env = new TestEnvironment();
10460
// SUT
105-
const spreadsheetRows: SpreadsheetRow[] = (env.service as any).createSpreadsheetRowsWithStatistics([], 0, 0);
61+
const spreadsheetRows: SpreadsheetRow[] = (env.service as any).addStatistics([], 0, 0);
10662

10763
// Should have only blank row + 2 statistics rows = 3 total
10864
expect(spreadsheetRows.length).toEqual(3);
@@ -116,6 +72,16 @@ describe('DraftJobsExportService', () => {
11672
expect(spreadsheetRows[2].startTime).toBe('0');
11773
});
11874
});
75+
76+
describe('getExportFilename', () => {
77+
it('should use the provided filename prefix', () => {
78+
const env = new TestEnvironment();
79+
const dateRange = { start: new Date(2025, 0, 15), end: new Date(2025, 1, 28) };
80+
81+
const filename: string = (env.service as any).getExportFilename(dateRange, 'csv', 'my-prefix');
82+
expect(filename).toBe('my-prefix_2025-01-15_2025-02-28.csv');
83+
});
84+
});
11985
});
12086

12187
class TestEnvironment {

0 commit comments

Comments
 (0)