Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ class TestEnvironment {

this.fixture = TestBed.createComponent(DateRangePickerComponent);
this.component = this.fixture.componentInstance;
this.component.startWithEmptyRange = startWithEmptyRange;
// Spy before the first change detection so that emissions from ngOnInit are recorded.
// Spy before setting the input, since setting it can emit the initial range.
this.emitSpy = spyOn(this.component.dateRangeChange, 'emit').and.callThrough();
this.component.startWithEmptyRange = startWithEmptyRange;
this.fixture.detectChanges();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,34 @@ export class DateRangePickerComponent implements OnInit {
@Input() showReset: boolean = false;

/** When true, initialize the control with null as the selected default range. */
@Input() startWithEmptyRange: boolean = false;
@Input() set startWithEmptyRange(value: boolean | null) {
if (!!value) {
this.dateRangeForm.setValue({ start: null, end: null }, { emitEvent: false });
this.defaultRange = undefined;
this.isDefaultRange = true;
} else {
// Set initial range
const initialEndDate = new Date(this.maxSelectableDate);
const initialStartDate = new Date(this.maxSelectableDate);
initialStartDate.setDate(initialStartDate.getDate() - this.defaultDaysBack);

const normalizedStart = this.beginningOfTheDayOf(initialStartDate);
const normalizedEnd = this.endOfTheDayOf(initialEndDate);

this.dateRangeForm.setValue({ start: normalizedStart, end: normalizedEnd }, { emitEvent: false });
this.defaultRange = {
start: new Date(normalizedStart),
end: new Date(normalizedEnd)
};
this.isDefaultRange = true;

// Tell parent of the initial range.
this.dateRangeChange.emit({
start: new Date(normalizedStart),
end: new Date(normalizedEnd)
});
}
}

/** Maximum selectable date (today) */
readonly maxSelectableDate: Date;
Expand Down Expand Up @@ -115,33 +142,6 @@ export class DateRangePickerComponent implements OnInit {
}

ngOnInit(): void {
if (this.startWithEmptyRange) {
this.dateRangeForm.setValue({ start: null, end: null }, { emitEvent: false });
this.defaultRange = undefined;
this.isDefaultRange = true;
} else {
// Set initial range
const initialEndDate = new Date(this.maxSelectableDate);
const initialStartDate = new Date(this.maxSelectableDate);
initialStartDate.setDate(initialStartDate.getDate() - this.defaultDaysBack);

const normalizedStart = this.beginningOfTheDayOf(initialStartDate);
const normalizedEnd = this.endOfTheDayOf(initialEndDate);

this.dateRangeForm.setValue({ start: normalizedStart, end: normalizedEnd }, { emitEvent: false });
this.defaultRange = {
start: new Date(normalizedStart),
end: new Date(normalizedEnd)
};
this.isDefaultRange = true;

// Tell parent of the initial range.
this.dateRangeChange.emit({
start: new Date(normalizedStart),
end: new Date(normalizedEnd)
});
}

// Update format hint based on current locale
this.updateLocaleSensitiveSettings(this.i18nService.localeCode);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class DraftJobsExportService extends BaseExportService {
*/
exportCsv(
rows: SpreadsheetRow[],
dateRangeForFilename: NormalizedDateRange,
dateRangeForFilename: NormalizedDateRange | undefined,
meanDuration: number,
maxDuration: number,
filenamePrefix: string
Expand All @@ -64,7 +64,7 @@ export class DraftJobsExportService extends BaseExportService {
*/
exportTsv(
rows: SpreadsheetRow[],
dateRangeForFilename: NormalizedDateRange,
dateRangeForFilename: NormalizedDateRange | undefined,
meanDuration: number,
maxDuration: number,
filenamePrefix: string
Expand All @@ -86,7 +86,7 @@ export class DraftJobsExportService extends BaseExportService {
*/
exportRsv(
rows: SpreadsheetRow[],
dateRangeForFilename: NormalizedDateRange,
dateRangeForFilename: NormalizedDateRange | undefined,
meanDuration: number,
maxDuration: number,
filenamePrefix: string
Expand Down Expand Up @@ -184,7 +184,7 @@ export class DraftJobsExportService extends BaseExportService {

private exportSeparatedValues(
rows: SpreadsheetRow[],
dateRangeForFilename: NormalizedDateRange,
dateRangeForFilename: NormalizedDateRange | undefined,
meanDuration: number,
maxDuration: number,
filenamePrefix: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

@if (isOnline) {
<div class="filter-controls">
<app-date-range-picker (dateRangeChange)="onDateRangeChange($event)"></app-date-range-picker>
<app-date-range-picker
[startWithEmptyRange]="false"
(dateRangeChange)="onDateRangeChange($event)"
></app-date-range-picker>
<div class="export-button-group">
<button mat-raised-button color="primary" class="export-csv-button" (click)="exportCsv()">
<mat-icon>download</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
OnboardingRequestService
} from '../../translate/draft-generation/onboarding-request.service';
import { OnboardingRequestAssigneeSelectComponent } from '../onboarding-request-assignee-select/onboarding-request-assignee-select.component';
import { ServalAdminParams } from '../serval-administration.component';
import { ServalAdministrationService } from '../serval-administration.service';
import {
ApproveRequestDialogComponent,
Expand Down Expand Up @@ -123,7 +124,8 @@ export class OnboardingRequestDetailComponent extends DataLoadingComponent imple
void this.loadRequest(requestId);
} else {
this.noticeService.showError('No request ID provided');
void this.router.navigate(['/serval-administration'], { queryParams: { tab: 'onboarding-requests' } });
const queryParams: ServalAdminParams = { tab: 'onboarding-requests' };
void this.router.navigate(['/serval-administration'], { queryParams });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { OnboardingRequestsComponent } from './onboarding-requests/onboarding-re
import { ServalBuildsComponent } from './serval-builds.component';
import { ServalProjectsComponent } from './serval-projects.component';

/**
* Represents the possible valid query params for the Serval Administration page
*/
export interface ServalAdminParams {
tab?: string;
q?: string;
projectId?: string;
}

/**
* Main serval administration component with tabbed interface.
* Supports URL parameters for filtering by project ID and selecting specific tabs.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
@if (isOnline) {
<div class="filter-controls">
<div class="filter-group">
<app-date-range-picker [showReset]="true" (dateRangeChange)="onDateRangeChange($event)"></app-date-range-picker>
<app-date-range-picker
[showReset]="true"
[startWithEmptyRange]="useUnspecifiedDateRange$ | async"
(dateRangeChange)="onDateRangeChange($event)"
></app-date-range-picker>
<div class="duration-area-toggle">
<mat-slide-toggle [checked]="includeDeleted" (change)="onIncludeDeletedChange($event.checked)">
Include deleted projects
Expand Down Expand Up @@ -48,6 +52,18 @@
</button>
</mat-menu>
</div>
@if (currentProjectFilter) {
<app-notice icon="filter_alt" mode="fill-dark">
<div class="filter-notice-content">
<span>
Showing Serval builds for project <strong>{{ filteredProjectName }}</strong>
</span>
<button mat-icon-button class="clear-filter-btn" (click)="clearProjectFilter()">
<mat-icon>clear</mat-icon>
</button>
</div>
</app-notice>
}

@if (summaryDisplayItems.length > 0) {
<div class="summary-area">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,33 @@ a {
}
}

app-notice {
margin-top: 1rem;
margin-bottom: 1rem;
}

.filter-notice-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}

.clear-filter-btn {
margin-left: 8px;
opacity: 0.8;

&:hover {
opacity: 1;
}

mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}

.status-icon.status-is-userrequested {
color: var(--sf-serval-builds-status-userrequested);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { configureTestingModule, getTestTranslocoModule } from 'xforge-common/test-utils';
import { UserService } from 'xforge-common/user.service';
import { notNull } from '../../type-utils';
import { SFProjectProfileDoc } from '../core/models/sf-project-profile-doc';
import { SF_TYPE_REGISTRY } from '../core/models/sf-type-registry';
import { BuildDto } from '../machine-api/build-dto';
import { BuildStates } from '../machine-api/build-states';
Expand All @@ -23,6 +24,7 @@
import { BuildConfidencesExportService } from './build-confidences-export.service';
import { NormalizedDateRange } from './date-range-picker.component';
import { DraftJobsExportService, SpreadsheetRow } from './draft-jobs-export.service';
import { ServalAdministrationService } from './serval-administration.service';
import { ServalBuildProblemsDialog } from './serval-build-problems-dialog.component';
import {
buildProjectDisplayName,
Expand All @@ -49,6 +51,7 @@
const mockUserService = mock(UserService);
const mockI18nService = mock(I18nService);
const mockedActivatedRoute = mock(ActivatedRoute);
const mockServalAdministrationService = mock(ServalAdministrationService);
const mockedConsole: MockConsole = MockConsole.install();

describe('ServalBuildsComponent', () => {
Expand All @@ -67,7 +70,8 @@
{ provide: UserService, useMock: mockUserService },
{ provide: I18nService, useMock: mockI18nService },
{ provide: ActivatedRoute, useMock: mockedActivatedRoute },
{ provide: ServalAdministrationService, useMock: mockServalAdministrationService },
provideNoopAnimations()

Check warning on line 74 in src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-builds.component.spec.ts

View workflow job for this annotation

GitHub Actions / Lint and Prettier (22.23.4, 11.11.0, 12.11.2)

`provideNoopAnimations` is deprecated. 20.2 Use `animate.enter` or `animate.leave` instead. Intent to remove in v23
]
}));

Expand Down Expand Up @@ -378,17 +382,50 @@
const env = new TestEnvironment();
const matchingRow: ServalBuildRow = env.createRow({ projectId: 'sf-project-123' });
const otherRow: ServalBuildRow = env.createRow({ projectId: 'sf-project-999' });
env.component['allRows'] = [matchingRow, otherRow];

// SUT
env.component.ngOnInit();
env.waitForRowUpdate();
env.component['allRows'] = [matchingRow, otherRow];
env.queryParams$.next({ q: 'project-123' });
env.waitForRowUpdate();

expect(env.component['searchControl'].value).toBe('project-123');
expect(env.component['rows']).toEqual([matchingRow]);
}));

it('requests only builds for the project in the URL', fakeAsync(() => {
const env = new TestEnvironment();
when(mockServalAdministrationService.get('sf-project-123')).thenResolve({
data: { shortName: 'P123', name: 'Project 123' }
} as SFProjectProfileDoc);

env.queryParams$.next({ projectId: 'sf-project-123' });

// SUT
env.component.ngOnInit();
env.waitForRowUpdate();

verify(mockDraftGenerationService.getBuildsSince(anything(), 'sf-project-123')).once();
let useUnspecifiedDateRange: boolean | undefined;
env.component.useUnspecifiedDateRange$.pipe(take(1)).subscribe(v => (useUnspecifiedDateRange = v));
expect(useUnspecifiedDateRange).toBe(true);
}));

it('loads the filtered project name from the project doc', fakeAsync(() => {
const env = new TestEnvironment();
when(mockServalAdministrationService.get('sf-filtered-project')).thenResolve({
data: { shortName: 'FProj', name: 'Filtered Project' }
} as SFProjectProfileDoc);
env.queryParams$.next({ projectId: 'sf-filtered-project' });

// SUT
env.component.ngOnInit();
env.waitForRowUpdate();

expect(env.component['filteredProjectName']).toBe('FProj - Filtered Project');
}));

it('updates query param q when search text changes', fakeAsync(() => {
const env = new TestEnvironment();
const navigateSpy = spyOn(env.component['router'], 'navigate').and.resolveTo(true);
Expand Down Expand Up @@ -2158,7 +2195,7 @@

when(mockNoticeService.loadingStarted(anything())).thenReturn(undefined);
when(mockNoticeService.loadingFinished(anything())).thenReturn(undefined);
when(mockDraftGenerationService.getBuildsSince(anything())).thenReturn(this.builds$);
when(mockDraftGenerationService.getBuildsSince(anything(), anything())).thenReturn(this.builds$);
when(mockDialogService.openMatDialog(anything(), anything())).thenReturn(undefined as never);
when(mockDialogService.openGenericDialog(anything())).thenReturn({
dialogRef: undefined as never,
Expand Down
Loading
Loading