+
Statuses
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/components/statuses/form-statuses-group.component.spec.ts b/src/app/components/statuses/form-statuses-group.component.spec.ts
index 97db9ca38..aa36d1da1 100644
--- a/src/app/components/statuses/form-statuses-group.component.spec.ts
+++ b/src/app/components/statuses/form-statuses-group.component.spec.ts
@@ -1,40 +1,34 @@
import { TaskStatus } from '@aneoconsultingfr/armonik.api.angular';
-import { AbstractControl, FormArray } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { FormStatusesGroupComponent } from './form-statuses-group.component';
describe('FormStatusesGroupComponent', () => {
- let component: FormStatusesGroupComponent;
+ const component = new FormStatusesGroupComponent();
+
+ const statuses: { name: string, value: string }[] = [
+ { name: 'Completed', value: `${TaskStatus.TASK_STATUS_COMPLETED}` },
+ { name: 'Cancelled', value: `${TaskStatus.TASK_STATUS_CANCELLED}` },
+ { name: 'Processed', value: `${TaskStatus.TASK_STATUS_PROCESSED}` }
+ ];
beforeEach(() => {
- component = new FormStatusesGroupComponent();
+ component.statuses = statuses;
component.group = {
name: 'status',
- color: 'green',
statuses: [
TaskStatus.TASK_STATUS_CANCELLED,
TaskStatus.TASK_STATUS_COMPLETED
]
};
+ component.ngOnInit();
});
it('should create', () => {
expect(component).toBeTruthy();
});
- describe('ngOnInit', () => {
- it('should init', () => {
- component.ngOnInit();
- expect(component.groupForm.value).toEqual({
- name: 'status',
- color: 'green',
- statuses: [8, 4]
- });
- });
-
- it('should init without color', () => {
- component.group = {name: 'status', statuses: [TaskStatus.TASK_STATUS_CANCELLED, TaskStatus.TASK_STATUS_COMPLETED]};
- component.ngOnInit();
+ describe('on init', () => {
+ it('should complete form', () => {
expect(component.groupForm.value).toEqual({
name: 'status',
color: null,
@@ -43,17 +37,6 @@ describe('FormStatusesGroupComponent', () => {
});
});
- it('should init without statuses', () => {
- const spyGroupFormGet = jest.spyOn(component.groupForm, 'get');
- spyGroupFormGet.mockImplementationOnce(() => null);
- component.ngOnInit();
- expect(component.groupForm.value).toEqual({
- name: 'status',
- color: 'green',
- statuses: []
- });
- });
-
it('should return true if it is checked', () => {
expect(component.isChecked({name: 'status', value: '4'})).toBeTruthy();
});
@@ -67,103 +50,82 @@ describe('FormStatusesGroupComponent', () => {
expect(component.isChecked({name: 'status', value: '4'})).toBeFalsy();
});
- it('should update on uncheked', () => {
- const initialObject = {
- controls: [
- {
- value: 'item1'
- },
- {
- value: 'item2'
- },
- {
- value: 'item3'
+ describe('onCheckboxChange', () => {
+ it('should update on uncheked', () => {
+ const groupFormStatuses = [
+ TaskStatus.TASK_STATUS_PROCESSED,
+ TaskStatus.TASK_STATUS_COMPLETED,
+ TaskStatus.TASK_STATUS_CANCELLED
+ ];
+ component.groupForm.patchValue({ statuses: groupFormStatuses });
+ const event = {
+ checked: false,
+ source: {
+ value: `${TaskStatus.TASK_STATUS_COMPLETED}`
}
- ],
- removeAt: jest.fn()
- } as unknown as AbstractControl;
- const spyGroupFormGet = jest.spyOn(component.groupForm, 'get');
- spyGroupFormGet.mockImplementationOnce(() => initialObject);
-
- const event = {
- checked: false,
- source: {
- value: 'item2'
- }
- } as unknown as MatCheckboxChange;
-
- component.onCheckboxChange(event);
- expect((initialObject as FormArray).removeAt).toHaveBeenCalledWith(1);
- });
+ } as unknown as MatCheckboxChange;
+
+ component.onCheckboxChange(event);
+ expect(component.groupForm.value.statuses?.length).toEqual(2);
+ });
- it('should update on check', () => {
- const initialObject = {
- controls: [
- {
- value: 'item1'
- },
- {
- value: 'item2'
- },
- {
- value: 'item3'
+ it('should update on check', () => {
+ const groupFormStatuses = [
+ TaskStatus.TASK_STATUS_COMPLETED,
+ TaskStatus.TASK_STATUS_CANCELLED
+ ];
+ component.groupForm.patchValue({ statuses: groupFormStatuses });
+
+ const event = {
+ checked: true,
+ source: {
+ value: `${TaskStatus.TASK_STATUS_PROCESSED}`
}
- ],
- push: jest.fn()
- } as unknown as AbstractControl;
-
- const event = {
- checked: true,
- source: {
- value: 'item4'
- }
- } as unknown as MatCheckboxChange;
-
- const spyGroupFormGet = jest.spyOn(component.groupForm, 'get');
- spyGroupFormGet.mockImplementationOnce(() => initialObject);
-
- component.onCheckboxChange(event);
- expect((initialObject as FormArray).push).toHaveBeenCalled();
- });
+ } as unknown as MatCheckboxChange;
- it('should not update on check if there is no status', () => {
- const event = {
- checked: true,
- source: {
- value: 'item4'
- }
- } as unknown as MatCheckboxChange;
-
- const spyGroupFormGet = jest.spyOn(component.groupForm, 'get');
- spyGroupFormGet.mockImplementationOnce(() => null);
- expect(component.onCheckboxChange(event)).toEqual(undefined);
+ component.onCheckboxChange(event);
+ expect(component.groupForm.value.statuses?.length).toEqual(3);
+ });
+
+ it('should set the group name as the first selected status', () => {
+ component.groupForm.patchValue({name: undefined, statuses: []});
+ const event = {
+ checked: true,
+ source: {
+ value: `${TaskStatus.TASK_STATUS_PROCESSED}`
+ }
+ } as unknown as MatCheckboxChange;
+
+ component.onCheckboxChange(event);
+ expect(component.groupForm.value.name).toEqual('Processed');
+ });
});
it('should emit on submit', () => {
- component.groupForm.value.color = 'green';
- component.groupForm.value.name = 'name';
- component.groupForm.value.statuses = ['0', '1'];
+ const newGroup = {
+ name: 'name',
+ color: 'green',
+ statuses: [TaskStatus.TASK_STATUS_UNSPECIFIED, TaskStatus.TASK_STATUS_CREATING]
+ };
+ component.groupForm.setValue(newGroup);
const spySubmit = jest.spyOn(component.submitChange, 'emit');
-
component.onSubmit();
- expect(spySubmit).toHaveBeenCalledWith({
- name: 'name',
- color: 'green',
- statuses: [TaskStatus.TASK_STATUS_UNSPECIFIED, TaskStatus.TASK_STATUS_CREATING]
- });
+ expect(spySubmit).toHaveBeenCalledWith(newGroup);
});
it('should emit on submit even without values', () => {
- component.groupForm.value.color = undefined;
- component.groupForm.value.name = undefined;
- component.groupForm.value.statuses = undefined;
+ const undefinedGroup = {
+ name: null,
+ color: null,
+ statuses: null
+ };
+ component.groupForm.setValue(undefinedGroup);
const spySubmit = jest.spyOn(component.submitChange, 'emit');
component.onSubmit();
-
expect(spySubmit).toHaveBeenCalledWith({
name: '',
color: '',
diff --git a/src/app/components/statuses/form-statuses-group.component.ts b/src/app/components/statuses/form-statuses-group.component.ts
index 73920c45d..fa056be1d 100644
--- a/src/app/components/statuses/form-statuses-group.component.ts
+++ b/src/app/components/statuses/form-statuses-group.component.ts
@@ -1,6 +1,6 @@
import { TaskStatus } from '@aneoconsultingfr/armonik.api.angular';
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
-import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
@@ -41,7 +41,8 @@ mat-dialog-content {
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule
- ]
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormStatusesGroupComponent implements OnInit {
@Input() group: TasksStatusesGroup | null = null;
@@ -51,58 +52,42 @@ export class FormStatusesGroupComponent implements OnInit {
@Output() submitChange = new EventEmitter
();
groupForm = new FormGroup({
- name: new FormControl('', [
+ name: new FormControl(null, [
Validators.required,
]),
- color: new FormControl(''),
- statuses: new FormArray>([]),
+ color: new FormControl(''),
+ statuses: new FormControl([])
});
ngOnInit() {
if(this.group) {
- this.groupForm.setValue({
+ this.groupForm.patchValue({
name: this.group.name,
color: this.group.color ?? null,
- statuses: []
+ statuses: [...this.group.statuses],
});
- const statuses = this.groupForm.get('statuses') as FormArray | null;
- if (!statuses) {
- return;
- }
-
- for (const status of this.group.statuses) {
- statuses.push(new FormControl(status));
- }
}
}
isChecked(status: StatusLabeled): boolean {
- if (!this.group) {
- return false;
- }
-
- return this.group.statuses.includes(Number(status.value) as TaskStatus);
+ return this.group?.statuses.includes(Number(status.value) as TaskStatus) ?? false;
}
-
onCheckboxChange(e: MatCheckboxChange) {
- const statuses = this.groupForm.get('statuses') as FormArray | null;
-
- if (!statuses) {
- return;
- }
+ const statuses = this.groupForm.get('statuses') as FormControl;
+ const status = Number(e.source.value) as TaskStatus;
if (e.checked) {
- statuses.push(new FormControl(e.source.value));
- } else {
- let i = 0;
- (statuses.controls as FormControl[]).forEach((item: FormControl) => {
- if (item.value == e.source.value) {
- statuses.removeAt(i);
- return;
+ statuses.value.push(status);
+ if (!this.groupForm.value.name && statuses.value.length === 1) {
+ const status = this.statuses.find(status => status.value === e.source.value);
+ if (status) {
+ this.groupForm.patchValue({name: status.name});
}
- i++;
- });
+ }
+ } else {
+ const index = statuses.value.findIndex(s => s === status);
+ statuses.value.splice(index, 1);
}
}
@@ -110,7 +95,7 @@ export class FormStatusesGroupComponent implements OnInit {
const result: TasksStatusesGroup = {
name: this.groupForm.value.name ?? '',
color: this.groupForm.value.color ?? '',
- statuses: this.groupForm.value.statuses?.map((status: string) => Number(status) as TaskStatus) ?? []
+ statuses: this.groupForm.value.statuses ?? []
};
this.submitChange.emit(result);
diff --git a/src/app/components/statuses/manage-groups-dialog.component.spec.ts b/src/app/components/statuses/manage-groups-dialog.component.spec.ts
index b0b34889a..72a324bad 100644
--- a/src/app/components/statuses/manage-groups-dialog.component.spec.ts
+++ b/src/app/components/statuses/manage-groups-dialog.component.spec.ts
@@ -49,7 +49,6 @@ describe('ManageGroupsDialogComponent', () => {
}}
]
}).inject(ManageGroupsDialogComponent);
- component.ngOnInit();
});
it('should run', () => {
diff --git a/src/app/components/statuses/manage-groups-dialog.component.ts b/src/app/components/statuses/manage-groups-dialog.component.ts
index a8df9cf8a..4d87d9cf7 100644
--- a/src/app/components/statuses/manage-groups-dialog.component.ts
+++ b/src/app/components/statuses/manage-groups-dialog.component.ts
@@ -1,6 +1,6 @@
import { TaskStatus } from '@aneoconsultingfr/armonik.api.angular';
import { CdkDragDrop, DragDropModule, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
-import { Component, Inject, OnInit, inject } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Inject, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@@ -126,10 +126,15 @@ ul {
MatIconModule,
DragDropModule,
MatTooltipModule,
- ]
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
-export class ManageGroupsDialogComponent implements OnInit {
- groups: TasksStatusesGroup[] = [];
+export class ManageGroupsDialogComponent {
+ private _groups = signal([]);
+
+ get groups(): TasksStatusesGroup[] {
+ return this._groups();
+ }
#dialog = inject(MatDialog);
#iconsServices = inject(IconsService);
@@ -138,10 +143,8 @@ export class ManageGroupsDialogComponent implements OnInit {
constructor(
public _dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) public data: ManageGroupsDialogData,
- ) {}
-
- ngOnInit(): void {
- this.groups = this.data.groups;
+ ) {
+ this._groups.set(this.data.groups);
}
getIcon(name: string): string {
@@ -174,7 +177,7 @@ export class ManageGroupsDialogComponent implements OnInit {
dialogRef.afterClosed().subscribe((result) => {
if (result) {
- this.groups.push(result);
+ this._groups.update(groups => [...groups, result]);
}
});
}
@@ -190,9 +193,7 @@ export class ManageGroupsDialogComponent implements OnInit {
dialogRef.afterClosed().subscribe((result) => {
if (result) {
const index = this.groups.indexOf(group);
- if (index > -1) {
- this.groups[index] = result;
- }
+ this._groups.update(groups => groups.map((group, i) => i === index ? result : group));
}
});
}
@@ -200,7 +201,7 @@ export class ManageGroupsDialogComponent implements OnInit {
onDelete(group: TasksStatusesGroup): void {
const index = this.groups.indexOf(group);
if (index > -1) {
- this.groups.splice(index, 1);
+ this._groups.update(groups => groups.filter((group, i) => i !== index));
}
}
diff --git a/src/app/partitions/services/partitions-inspection.service.ts b/src/app/partitions/services/partitions-inspection.service.ts
index 4d1685c80..3ea69ec5d 100644
--- a/src/app/partitions/services/partitions-inspection.service.ts
+++ b/src/app/partitions/services/partitions-inspection.service.ts
@@ -1,3 +1,4 @@
+import { PartitionRawEnumField } from '@aneoconsultingfr/armonik.api.angular';
import { Field } from '@app/types/column.type';
import { InspectionService } from '@app/types/services/inspectionService';
import { PartitionRaw } from '../types';
@@ -23,6 +24,10 @@ export class PartitionsInspectionService extends InspectionService
];
readonly arrays: Field[] = [
- { key: 'parentPartitionIds', link: 'partitions', queryParams: '0-root-1-0' }
+ {
+ key: 'parentPartitionIds',
+ link: 'partitions',
+ queryParams: `0-root-${PartitionRawEnumField.PARTITION_RAW_ENUM_FIELD_ID}-0`
+ }
];
}
\ No newline at end of file
diff --git a/src/app/results/services/results-filters.service.ts b/src/app/results/services/results-filters.service.ts
index c1c98ac52..038f9eaae 100644
--- a/src/app/results/services/results-filters.service.ts
+++ b/src/app/results/services/results-filters.service.ts
@@ -18,6 +18,7 @@ export class ResultsFiltersService implements FiltersServiceInterface = {
[ResultRawEnumField.RESULT_RAW_ENUM_FIELD_COMPLETED_AT]: $localize`Completed at`,
[ResultRawEnumField.RESULT_RAW_ENUM_FIELD_CREATED_AT]: $localize`Created at`,
+ [ResultRawEnumField.RESULT_RAW_ENUM_FIELD_CREATED_BY]: $localize`Created By`,
[ResultRawEnumField.RESULT_RAW_ENUM_FIELD_NAME]: $localize`Name`,
[ResultRawEnumField.RESULT_RAW_ENUM_FIELD_OWNER_TASK_ID]: $localize`Owner Task ID`,
[ResultRawEnumField.RESULT_RAW_ENUM_FIELD_RESULT_ID]: $localize`Result ID`,
@@ -48,6 +49,11 @@ export class ResultsFiltersService implements FiltersServiceInterface {
sortable: true,
link: '/tasks',
},
+ {
+ name: $localize`Created By`,
+ key: 'createdBy',
+ sortable: true,
+ },
{
name: $localize`Created at`,
key: 'createdAt',
diff --git a/src/app/results/services/results-inspection.service.ts b/src/app/results/services/results-inspection.service.ts
index 0573d3802..29f170190 100644
--- a/src/app/results/services/results-inspection.service.ts
+++ b/src/app/results/services/results-inspection.service.ts
@@ -19,6 +19,9 @@ export class ResultsInspectionService extends InspectionService {
key: 'createdAt',
type: 'date'
},
+ {
+ key: 'createdBy',
+ },
{
key: 'completedAt',
type: 'date'
diff --git a/src/app/services/default-config.service.ts b/src/app/services/default-config.service.ts
index b208c0d10..8fd3899f6 100644
--- a/src/app/services/default-config.service.ts
+++ b/src/app/services/default-config.service.ts
@@ -117,7 +117,9 @@ export class DefaultConfigService {
lockColumns: false,
columns: [
'id',
- 'count'
+ 'podReserved',
+ 'priority',
+ 'count',
],
options: {
pageIndex: 0,
@@ -136,6 +138,8 @@ export class DefaultConfigService {
lockColumns: false,
columns: [
'sessionId',
+ 'status',
+ 'createdAt',
'count',
'actions',
],
@@ -156,7 +160,9 @@ export class DefaultConfigService {
lockColumns: false,
columns: [
'resultId',
+ 'name',
'sessionId',
+ 'createdAt',
],
options: {
pageIndex: 0,
diff --git a/src/app/sessions/services/sessions-inspection.service.ts b/src/app/sessions/services/sessions-inspection.service.ts
index 5a8219552..3217a4370 100644
--- a/src/app/sessions/services/sessions-inspection.service.ts
+++ b/src/app/sessions/services/sessions-inspection.service.ts
@@ -1,3 +1,4 @@
+import { PartitionRawEnumField } from '@aneoconsultingfr/armonik.api.angular';
import { Field } from '@app/types/column.type';
import { InspectionService } from '@app/types/services/inspectionService';
import { SessionRaw } from '../types';
@@ -40,7 +41,7 @@ export class SessionsInspectionService extends InspectionService {
{
key: 'partitionIds',
link: 'partitions',
- queryParams: '0-root-1-0'
+ queryParams: `0-root-${PartitionRawEnumField.PARTITION_RAW_ENUM_FIELD_ID}-0`
}
];
}
\ No newline at end of file
diff --git a/src/app/sessions/show.component.spec.ts b/src/app/sessions/show.component.spec.ts
index 67c114d11..71260eb31 100644
--- a/src/app/sessions/show.component.spec.ts
+++ b/src/app/sessions/show.component.spec.ts
@@ -2,7 +2,7 @@ import { GetSessionResponse, SessionStatus } from '@aneoconsultingfr/armonik.api
import { TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { GrpcStatusEvent } from '@ngx-grpc/common';
-import { Timestamp } from '@ngx-grpc/well-known-types';
+import { Duration, Timestamp } from '@ngx-grpc/well-known-types';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { TasksInspectionService } from '@app/tasks/services/tasks-inspection.service';
import { FiltersService } from '@services/filters.service';
@@ -189,10 +189,10 @@ describe('AppShowComponent', () => {
component.lowerDate = taskCreatedAt.date;
component.upperDate = taskEndedAt.date;
component.computeDuration$.next();
- expect(component.data()?.duration).toEqual({
+ expect(component.data()?.duration).toEqual(new Duration({
seconds: '1000',
nanos: 0
- });
+ }));
});
});
diff --git a/src/app/sessions/show.component.ts b/src/app/sessions/show.component.ts
index 7f69d49a0..88129b154 100644
--- a/src/app/sessions/show.component.ts
+++ b/src/app/sessions/show.component.ts
@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject } from '@
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Params, Router, RouterModule } from '@angular/router';
-import { Timestamp } from '@ngx-grpc/well-known-types';
+import { Duration, Timestamp } from '@ngx-grpc/well-known-types';
import { Subject, map, switchMap } from 'rxjs';
import { TasksFiltersService } from '@app/tasks/services/tasks-filters.service';
import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service';
@@ -171,10 +171,10 @@ export class ShowComponent extends AppShowComponent {
const data = this.data();
if (data && this.lowerDate && this.upperDate) {
- data.duration = {
+ data.duration = new Duration({
seconds: (Number(this.upperDate.seconds) - Number(this.lowerDate.seconds)).toString(),
nanos: Math.abs(this.upperDate.nanos - this.lowerDate.nanos)
- };
+ });
this.data.set(data);
}
});
diff --git a/src/app/settings/index.component.html b/src/app/settings/index.component.html
index fa53c0bd4..a83fd13b0 100644
--- a/src/app/settings/index.component.html
+++ b/src/app/settings/index.component.html
@@ -20,7 +20,7 @@
-
+
diff --git a/src/app/settings/index.component.spec.ts b/src/app/settings/index.component.spec.ts
new file mode 100644
index 000000000..ad9193aa7
--- /dev/null
+++ b/src/app/settings/index.component.spec.ts
@@ -0,0 +1,458 @@
+import { CdkDragDrop } from '@angular/cdk/drag-drop';
+import { HttpClient } from '@angular/common/http';
+import { TestBed } from '@angular/core/testing';
+import { MatCheckboxChange } from '@angular/material/checkbox';
+import { MatDialog } from '@angular/material/dialog';
+import { of } from 'rxjs';
+import { Key } from '@app/types/config';
+import { Sidebar, SidebarItem } from '@app/types/navigation';
+import { IconsService } from '@services/icons.service';
+import { NavigationService } from '@services/navigation.service';
+import { NotificationService } from '@services/notification.service';
+import { StorageService } from '@services/storage.service';
+import { IndexComponent } from './index.component';
+
+class FakeFileReader extends FileReader {
+ _result: string;
+
+ override set result(entry: string) {
+ this._result = entry;
+ }
+
+ override get result() {
+ return this._result;
+ }
+
+ constructor(result: string) {
+ super();
+ this.result = result;
+ }
+
+ override onload = jest.fn();
+
+ override readAsText() {
+ this.onload();
+ }
+}
+
+describe('IndexComponent', () => {
+ let component: IndexComponent;
+
+ const mockNotificationService = {
+ success: jest.fn(),
+ error: jest.fn(),
+ };
+
+ const mockSideBar: Sidebar[] = ['profile', 'dashboard', 'sessions', 'tasks'];
+
+ const mockSidebarItems: SidebarItem[] = [
+ {
+ type: 'link',
+ id: 'profile',
+ display: $localize`Profile`,
+ route: '/profile',
+ },
+ {
+ type: 'link',
+ id: 'dashboard',
+ display: $localize`Dashboard`,
+ route: '/dashboard',
+ },
+ {
+ type: 'link',
+ id: 'applications',
+ display: $localize`Applications`,
+ route: '/applications',
+ },
+ {
+ type: 'link',
+ id: 'partitions',
+ display: $localize`Partitions`,
+ route: '/partitions',
+ },
+ {
+ type: 'link',
+ id: 'sessions',
+ display: $localize`Sessions`,
+ route: '/sessions',
+ },
+ {
+ type: 'link',
+ id: 'tasks',
+ display: $localize`Tasks`,
+ route: '/tasks',
+ },
+ {
+ type: 'link',
+ id: 'results',
+ display: $localize`Results`,
+ route: '/results',
+ },
+ {
+ type: 'divider',
+ id: 'divider',
+ display: $localize`Divider`,
+ route: null,
+ },
+ ];
+
+ const mockNavigationService = {
+ restoreSidebar: jest.fn(() => [...mockSideBar]),
+ saveSidebar: jest.fn(),
+ updateSidebar: jest.fn(),
+ defaultSidebar: [...mockSideBar],
+ sidebarItems: [...mockSidebarItems]
+ };
+
+ const mockKeys: Key[] = ['applications-columns', 'dashboard-lines', 'language'];
+
+ const mockStorageService = {
+ restoreKeys: jest.fn(() => new Set([...mockKeys])),
+ removeItem: jest.fn(),
+ importData: jest.fn(),
+ exportData: jest.fn(),
+ };
+
+ let dialogResult: boolean;
+
+ const mockDialog = {
+ open: jest.fn(() => {
+ return {
+ afterClosed: jest.fn(() => of(dialogResult))
+ };
+ })
+ };
+
+ const mockHttpClient = {
+ get: jest.fn()
+ };
+
+ beforeEach(() => {
+ component = TestBed.configureTestingModule({
+ providers: [
+ IndexComponent,
+ IconsService,
+ { provide: NotificationService, useValue: mockNotificationService },
+ { provide: NavigationService, useValue: mockNavigationService },
+ { provide: StorageService, useValue: mockStorageService },
+ { provide: MatDialog, useValue: mockDialog },
+ { provide: HttpClient, useValue: mockHttpClient },
+ ]
+ }).inject(IndexComponent);
+ component.ngOnInit();
+ });
+
+ it('should run', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('initialisation', () => {
+ it('should set keys', () => {
+ expect(component.keys).toEqual(new Set(mockKeys));
+ });
+
+ it('should set sidebar', () => {
+ expect(component.sidebar).toEqual(mockSideBar);
+ });
+ });
+
+ it('should retrieve icons', () => {
+ expect(component.getIcon('heart')).toEqual('favorite');
+ });
+
+ it('should reset sidebar', () => {
+ component.onRestoreSidebar();
+ expect(mockNavigationService.restoreSidebar).toHaveBeenCalled();
+ });
+
+ it('should clear side bar', () => {
+ component.sidebar = ['applications', 'divider', 'partitions'];
+ dialogResult = true;
+ component.onClearSideBar();
+ expect(component.sidebar).toEqual(mockSideBar);
+ });
+
+ describe('onSaveSideBar', () => {
+ it('should save sidebar', () => {
+ component.onSaveSidebar();
+ expect(mockNavigationService.saveSidebar).toHaveBeenCalledWith(component.sidebar);
+ });
+
+ it('should restore keys', () => {
+ component.keys = new Set();
+ component.onSaveSidebar();
+ expect(component.keys).toEqual(new Set(mockKeys));
+ });
+ });
+
+ it('should remove an item of the sidebar according to its index', () => {
+ component.onRemoveSidebarItem(0);
+ expect(component.sidebar).toEqual(['dashboard', 'sessions', 'tasks']);
+ });
+
+ it('should add a sidebar item at the end of the set', () => {
+ component.onAddSidebarItem();
+ expect(component.sidebar).toEqual([...mockSideBar, 'dashboard']);
+ });
+
+ it('should return the sidebar items', () => {
+ expect(component.getSidebarItems()).toEqual(mockSidebarItems.map(item => {
+ return {
+ name: item.display,
+ value: item.id
+ };
+ }));
+ });
+
+ describe('findSidebarItem', () => {
+ it('should find a sidebar item', () => {
+ expect(component.findSidebarItem('applications')).toEqual({
+ type: 'link',
+ id: 'applications',
+ display: $localize`Applications`,
+ route: '/applications',
+ });
+ });
+
+ it('should throw an Error if it does not find any item', () => {
+ expect(() => component.findSidebarItem('notExisting' as Sidebar)).toThrow();
+ });
+ });
+
+ it('should update the sidebar item on change', () => {
+ component.onSidebarItemChange(0, 'results');
+ expect(component.sidebar[0]).toEqual('results');
+ });
+
+ describe('updateKeySelection', () => {
+ const keys: Key[] = ['dashboard-lines', 'applications-columns'];
+
+ beforeEach(() => {
+ component.selectedKeys = new Set(keys);
+ });
+
+ it('should delete the key from the selection', () => {
+ const event = {
+ source: {
+ name: 'dashboard-lines' as Key
+ }
+ } as MatCheckboxChange;
+ component.updateKeySelection(event);
+ expect(component.selectedKeys).toEqual(new Set(['applications-columns']));
+ });
+
+ it('should add the key to the selection', () => {
+ const event = {
+ source: {
+ name: 'language' as Key
+ }
+ } as MatCheckboxChange;
+ component.updateKeySelection(event);
+ expect(component.selectedKeys).toEqual(new Set([...keys, 'language']));
+ });
+ });
+
+ describe('onSubmitStorage', () => {
+ const keys: Key[] = ['dashboard-lines', 'applications-columns'];
+
+ beforeEach(() => {
+ component.selectedKeys = new Set(keys);
+ const event = {
+ preventDefault: jest.fn()
+ } as unknown as SubmitEvent;
+ component.onSubmitStorage(event);
+ });
+
+ it('should delete all selected keys', () => {
+ expect(component.keys).toEqual(new Set(['language']));
+ });
+
+ it('should remove all selected keys from the storage', () => {
+ expect(mockStorageService.removeItem).toHaveBeenCalledTimes(keys.length);
+ });
+
+ it('should clear selectedKeys', () => {
+ expect(component.selectedKeys).toEqual(new Set([]));
+ });
+
+ it('should notify on success', () => {
+ expect(mockNotificationService.success).toHaveBeenCalled();
+ });
+ });
+
+ describe('clear All', () => {
+ const serverReturn = JSON.stringify({language: 'en'});
+ mockHttpClient.get.mockReturnValue(of(serverReturn));
+ dialogResult = true;
+
+ beforeEach(() => {
+ component.clearAll();
+ });
+
+ it('should delete all keys', () => {
+ expect(component.keys).toEqual(new Set());
+ });
+
+ it('should remove all items from storageService', () => {
+ expect(mockStorageService.removeItem).toHaveBeenCalledTimes(mockKeys.length);
+ });
+
+ it('should retrieve server config', () => {
+ expect(mockStorageService.importData).toHaveBeenCalledWith(serverReturn as string, false, false);
+ });
+
+ it('should notify on success', () => {
+ expect(mockNotificationService.success).toHaveBeenCalled();
+ });
+ });
+
+ describe('exportData', () => {
+ const anchor = {
+ href: '',
+ download: '',
+ click: jest.fn()
+ };
+
+ global.document.createElement = jest.fn().mockReturnValue(anchor);
+
+ global.URL.createObjectURL = jest.fn();
+
+ beforeEach(() => {
+ component.exportData();
+ });
+
+ it('should download the settings', () => {
+ expect(anchor.download).toContain('settings.json');
+ });
+
+ it('should click the anchor', () => {
+ expect(anchor.click).toHaveBeenCalled();
+ });
+
+ it('should notify on success', () => {
+ expect(mockNotificationService.success).toHaveBeenCalled();
+ });
+ });
+
+ describe('onSubmitImport', () => {
+ const newSideBar: Sidebar[] = ['results', 'partitions'];
+ const data = {'navigation-sidebar': newSideBar};
+
+ const file = new File([JSON.stringify(data)], 'settings', {type: 'application/json'});
+ const target = {
+ querySelector: jest.fn().mockReturnValue({files: [file]}),
+ reset: jest.fn()
+ };
+
+ const event = {
+ target: target,
+ preventDefault: jest.fn()
+ } as unknown as SubmitEvent;
+
+ jest.spyOn(global, 'FileReader').mockReturnValue(new FakeFileReader(JSON.stringify(data)));
+
+ beforeEach(() => {
+ mockStorageService.restoreKeys.mockReturnValueOnce(new Set(Object.keys(data) as Key[]));
+ mockNavigationService.restoreSidebar.mockReturnValueOnce(data['navigation-sidebar']);
+ });
+
+ it('should not accept undefined forms', () => {
+ expect(component.onSubmitImport({target: undefined, preventDefault: jest.fn()} as unknown as SubmitEvent)).toBeUndefined();
+ });
+
+ it('should not accept invalid input', () => {
+ target.querySelector.mockReturnValueOnce(undefined);
+ expect(component.onSubmitImport(event)).toBeUndefined();
+ });
+
+ it('should not accept empty files input', () => {
+ target.querySelector.mockReturnValueOnce({files: []});
+ expect(component.onSubmitImport(event)).toBeUndefined();
+ });
+
+ it('should notify on empty file', () => {
+ target.querySelector.mockReturnValueOnce({files: []});
+ component.onSubmitImport(event);
+ expect(mockNotificationService.error).toHaveBeenCalled();
+ });
+
+ it('should not accept not json file', () => {
+ target.querySelector.mockReturnValueOnce({files: [{type: 'application/txt'}]});
+ component.onSubmitImport(event);
+ expect(component.onSubmitImport(event)).toBeUndefined();
+ });
+
+ it('should notify on wrong file type', () => {
+ target.querySelector.mockReturnValueOnce({files: [{type: 'application/txt'}]});
+ component.onSubmitImport(event);
+ expect(mockNotificationService.error).toHaveBeenCalled();
+ });
+
+ it('should reset the form on wrong file type', () => {
+ target.querySelector.mockReturnValueOnce({files: [{type: 'application/txt'}]});
+ component.onSubmitImport(event);
+ expect(target.reset).toHaveBeenCalled();
+ });
+
+ it('should import data in storage', async () => {
+ component.onSubmitImport(event);
+ expect(mockStorageService.importData).toHaveBeenCalledWith(JSON.stringify(data));
+ });
+
+ it('should set keys', () => {
+ component.onSubmitImport(event);
+ expect(component.keys).toEqual(new Set(Object.keys(data) as Key[]));
+ });
+
+ it('should set sidebar', () => {
+ component.onSubmitImport(event);
+ expect(mockNavigationService.updateSidebar).toHaveBeenCalledWith(data['navigation-sidebar']);
+ });
+
+ it('should notify on success', () => {
+ component.onSubmitImport(event);
+ expect(mockNotificationService.success).toHaveBeenCalled();
+ });
+
+ it('should console warn in case of reading error', () => {
+ console.warn = jest.fn().mockImplementation(() => {});
+ mockStorageService.importData.mockImplementationOnce(() => {throw new Error();});
+ component.onSubmitImport(event);
+ expect(console.warn).toHaveBeenCalled();
+ });
+
+ it('should notify on error', () => {
+ console.warn = jest.fn().mockImplementation(() => {});
+ mockStorageService.importData.mockImplementationOnce(() => {throw new Error();});
+ component.onSubmitImport(event);
+ expect(mockNotificationService.error).toHaveBeenCalled();
+ });
+
+ it('should reset the form', () => {
+ component.onSubmitImport(event);
+ expect(target.reset).toHaveBeenCalled();
+ });
+ });
+
+ it('should update position on drop', () => {
+ const event = {
+ previousIndex: 0,
+ currentIndex: 1
+ } as CdkDragDrop;
+ component.drop(event);
+ expect(component.sidebar).toEqual(['dashboard', 'profile', 'sessions', 'tasks']);
+ });
+
+ it('should retrieve the config file name', () => {
+ const fileName = 'settings';
+ const event = {
+ target: {
+ files: {
+ item: jest.fn().mockReturnValue({name: fileName})
+ }
+ }
+ } as unknown as Event;
+ component.addConfigFile(event);
+ expect(component.fileName).toEqual(fileName);
+ });
+});
\ No newline at end of file
diff --git a/src/app/settings/index.component.ts b/src/app/settings/index.component.ts
index a5bd121ac..1b5ef2222 100644
--- a/src/app/settings/index.component.ts
+++ b/src/app/settings/index.component.ts
@@ -154,23 +154,23 @@ export class IndexComponent implements OnInit {
sidebar: Sidebar[] = [];
readonly dialog = inject(MatDialog);
- #iconsService = inject(IconsService);
- #notificationService = inject(NotificationService);
- #navigationService = inject(NavigationService);
- #storageService = inject(StorageService);
- httpClient = inject(HttpClient);
+ private readonly iconsService = inject(IconsService);
+ private readonly notificationService = inject(NotificationService);
+ private readonly navigationService = inject(NavigationService);
+ private readonly storageService = inject(StorageService);
+ private readonly httpClient = inject(HttpClient);
ngOnInit(): void {
- this.keys = this.#sortKeys(this.#storageService.restoreKeys());
- this.sidebar = this.#navigationService.restoreSidebar();
+ this.keys = this.sortKeys(this.storageService.restoreKeys());
+ this.sidebar = this.navigationService.restoreSidebar();
}
getIcon(name: string | null): string {
- return this.#iconsService.getIcon(name);
+ return this.iconsService.getIcon(name);
}
- onResetSidebar(): void {
- this.sidebar = this.#navigationService.restoreSidebar();
+ onRestoreSidebar(): void {
+ this.sidebar = this.navigationService.restoreSidebar();
}
onClearSideBar(): void {
@@ -182,13 +182,13 @@ export class IndexComponent implements OnInit {
});
}
- clearSideBar(): void {
- this.sidebar = Array.from(this.#navigationService.defaultSidebar);
+ private clearSideBar(): void {
+ this.sidebar = Array.from(this.navigationService.defaultSidebar);
}
onSaveSidebar(): void {
- this.#navigationService.saveSidebar(this.sidebar);
- this.keys = this.#sortKeys(this.#storageService.restoreKeys());
+ this.navigationService.saveSidebar(this.sidebar);
+ this.keys = this.sortKeys(this.storageService.restoreKeys());
}
onRemoveSidebarItem(index: number): void {
@@ -200,14 +200,14 @@ export class IndexComponent implements OnInit {
}
getSidebarItems(): { name: string, value: Sidebar }[] {
- return this.#navigationService.sidebarItems.map(item => ({
+ return this.navigationService.sidebarItems.map(item => ({
name: item.display,
value: item.id as Sidebar,
}));
}
findSidebarItem(id: Sidebar): SidebarItem {
- const item = this.#navigationService.sidebarItems.find(item => item.id === id);
+ const item = this.navigationService.sidebarItems.find(item => item.id === id);
if (!item) {
throw new Error(`Sidebar item with id "${id}" not found`);
@@ -233,12 +233,12 @@ export class IndexComponent implements OnInit {
for (const key of this.selectedKeys) {
this.keys.delete(key);
- this.#storageService.removeItem(key);
+ this.storageService.removeItem(key);
}
this.selectedKeys.clear();
- this.#notificationService.success('Data cleared');
+ this.notificationService.success('Data cleared');
}
clearAll(): void {
@@ -246,30 +246,30 @@ export class IndexComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
if (result) {
- this.#clearAll();
- this.#getServerConfig();
- this.#notificationService.success('All data cleared');
+ this.clearAllKeys();
+ this.getServerConfig();
+ this.notificationService.success('All data cleared');
}
});
}
- #clearAll(): void {
+ private clearAllKeys(): void {
for (const key of this.keys) {
this.keys.delete(key);
- this.#storageService.removeItem(key);
+ this.storageService.removeItem(key);
}
}
- #getServerConfig() {
+ private getServerConfig() {
this.httpClient.get('/static/gui_configuration').subscribe(data => {
if (data && Object.keys(data).length !== 0) {
- this.#storageService.importData(data as string, false, false);
+ this.storageService.importData(data as string, false, false);
}
});
}
exportData(): void {
- const data = JSON.stringify(this.#storageService.exportData());
+ const data = JSON.stringify(this.storageService.exportData());
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
@@ -281,7 +281,7 @@ export class IndexComponent implements OnInit {
anchor.download = `${date}-${id}-settings.json`;
anchor.click();
- this.#notificationService.success('Settings exported');
+ this.notificationService.success('Settings exported');
}
onSubmitImport(event: SubmitEvent): void {
@@ -302,12 +302,12 @@ export class IndexComponent implements OnInit {
const file = fileInput.files?.[0];
if (!file) {
- this.#notificationService.error('No file selected');
+ this.notificationService.error('No file selected');
return;
}
- if( file.type !== 'application/json' ) {
- this.#notificationService.error(`'${file.name}' is not a JSON file`);
+ if(file.type !== 'application/json') {
+ this.notificationService.error(`'${file.name}' is not a JSON file`);
form.reset();
return;
}
@@ -317,26 +317,25 @@ export class IndexComponent implements OnInit {
reader.onload = () => {
const data = reader.result as string;
try {
- this.#storageService.importData(data);
- this.keys = this.#sortKeys(this.#storageService.restoreKeys());
+ this.storageService.importData(data);
+ this.keys = this.sortKeys(this.storageService.restoreKeys());
const hasSidebarKey = this.keys.has('navigation-sidebar');
// Update sidebar
if (hasSidebarKey) {
- this.sidebar = this.#navigationService.restoreSidebar();
- this.#navigationService.updateSidebar(this.sidebar);
+ this.sidebar = this.navigationService.restoreSidebar();
+ this.navigationService.updateSidebar(this.sidebar);
}
- this.#notificationService.success('Settings imported');
+ this.notificationService.success('Settings imported');
} catch (e) {
console.warn(e);
- this.#notificationService.error('Settings could not be imported.');
+ this.notificationService.error('Settings could not be imported.');
}
form.reset();
};
-
reader.readAsText(file);
}
@@ -344,7 +343,7 @@ export class IndexComponent implements OnInit {
moveItemInArray(this.sidebar, event.previousIndex, event.currentIndex);
}
- #sortKeys(keys: Set): Set {
+ private sortKeys(keys: Set): Set {
return new Set([...keys].sort((a, b) => a.localeCompare(b)));
}
diff --git a/src/app/tasks/services/tasks-filters.service.ts b/src/app/tasks/services/tasks-filters.service.ts
index 34b12f086..2f2e6e0f1 100644
--- a/src/app/tasks/services/tasks-filters.service.ts
+++ b/src/app/tasks/services/tasks-filters.service.ts
@@ -20,6 +20,7 @@ export class TasksFiltersService implements FiltersServiceOptionsInterface {
key: 'processingToEndDuration',
type: 'duration'
},
+ {
+ key: 'createdBy',
+ },
{
key: 'createdAt',
type: 'date'
@@ -107,22 +111,22 @@ export class TasksInspectionService extends InspectionService {
{
key: 'dataDependencies',
link: 'results',
- queryParams: '0-root-7-0'
+ queryParams: `0-root-${ResultRawEnumField.RESULT_RAW_ENUM_FIELD_RESULT_ID}-0`
},
{
key: 'expectedOutputIds',
link: 'results',
- queryParams: '0-root-7-0'
+ queryParams: `0-root-${ResultRawEnumField.RESULT_RAW_ENUM_FIELD_RESULT_ID}-0`
},
{
key: 'parentTaskIds',
link: 'tasks',
- queryParams: '0-root-1-0'
+ queryParams: `0-root-${TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_TASK_ID}-0`
},
{
key: 'retryOfIds',
link: 'tasks',
- queryParams: '0-root-1-0'
+ queryParams: `0-root-${TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_TASK_ID}-0`
}
];
}
\ No newline at end of file
diff --git a/src/app/tasks/show.component.spec.ts b/src/app/tasks/show.component.spec.ts
index 43a337250..a4c3e324e 100644
--- a/src/app/tasks/show.component.spec.ts
+++ b/src/app/tasks/show.component.spec.ts
@@ -34,13 +34,19 @@ describe('AppShowComponent', () => {
const returnedTask = {
id: 'taskId-12345',
+ sessionId: 'sessionId',
options: {
partitionId: 'partitionId'
},
- status: TaskStatus.TASK_STATUS_PROCESSING
+ status: TaskStatus.TASK_STATUS_PROCESSING,
+ parentTaskIds: [
+ 'sessionId',
+ 'taskId-789'
+ ]
} as TaskRaw;
+
const mockTasksGrpcService = {
- get$: jest.fn((): Observable => of({task: returnedTask} as GetTaskResponse)),
+ get$: jest.fn((): Observable => of({ task: returnedTask } as GetTaskResponse)),
cancel$: jest.fn(() => of({}))
};
@@ -104,7 +110,7 @@ describe('AppShowComponent', () => {
describe('get status', () => {
it('should return undefined if there is no data', () => {
mockTasksGrpcService.get$.mockReturnValueOnce(of(null));
- jest.spyOn(console, 'error').mockImplementation(() => {});
+ jest.spyOn(console, 'error').mockImplementation(() => { });
component.refresh.next();
expect(component.status).toEqual(undefined);
});
@@ -133,11 +139,15 @@ describe('AppShowComponent', () => {
});
it('should set resultsQueryParams', () => {
- expect(component.resultsQueryParams).toEqual({'0-root-3-0': returnedTask.id});
+ expect(component.resultsQueryParams).toEqual({ '0-root-3-0': returnedTask.id });
+ });
+
+ it('should filter the sessionID from the parent tasks IDs', () => {
+ expect(component.data()?.parentTaskIds).toEqual(returnedTask.parentTaskIds.filter(taskId => taskId !== returnedTask.sessionId));
});
it('should catch errors', () => {
- jest.spyOn(console, 'error').mockImplementation(() => {});
+ jest.spyOn(console, 'error').mockImplementation(() => { });
mockTasksGrpcService.get$.mockReturnValueOnce(throwError(() => new Error()));
const spy = jest.spyOn(component, 'handleError');
component.refresh.next();
@@ -147,15 +157,15 @@ describe('AppShowComponent', () => {
describe('Handle errors', () => {
it('should log errors', () => {
- const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
const errorMessage = 'ErrorMessage';
- component.handleError({statusMessage: errorMessage} as GrpcStatusEvent);
+ component.handleError({ statusMessage: errorMessage } as GrpcStatusEvent);
expect(errorSpy).toHaveBeenCalled();
});
it('should notify the error', () => {
const errorMessage = 'ErrorMessage';
- component.handleError({statusMessage: errorMessage} as GrpcStatusEvent);
+ component.handleError({ statusMessage: errorMessage } as GrpcStatusEvent);
expect(mockNotificationService.error).toHaveBeenCalledWith('Could not retrieve data.');
});
});
@@ -200,7 +210,7 @@ describe('AppShowComponent', () => {
});
it('should log errors', () => {
- const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
mockTasksGrpcService.cancel$.mockReturnValueOnce(throwError(() => new Error()));
component.cancel();
expect(errorSpy).toHaveBeenCalled();
diff --git a/src/app/tasks/show.component.ts b/src/app/tasks/show.component.ts
index 4c2c094b8..7dfa63a5a 100644
--- a/src/app/tasks/show.component.ts
+++ b/src/app/tasks/show.component.ts
@@ -89,21 +89,22 @@ export class ShowComponent extends AppShowComponent im
}
getDataFromResponse(data: GetTaskResponse): TaskRaw | undefined {
- return data.task;
+ return data.task;
}
afterDataFetching(): void {
const data = this.data();
this.status = data?.status;
if (data) {
+ data.parentTaskIds = data.parentTaskIds.filter(taskId => taskId !== data.sessionId);
this.createResultQueryParams();
this.canCancel = !this.tasksStatusesService.taskNotEnded(data.status);
}
}
-
+
cancel(): void {
const data = this.data();
- if(data) {
+ if (data) {
this.grpcService.cancel$([data.id]).subscribe({
complete: () => {
this.success('Task canceled');
diff --git a/src/app/types/navigation.ts b/src/app/types/navigation.ts
index 7ef00aadc..df8eaa136 100644
--- a/src/app/types/navigation.ts
+++ b/src/app/types/navigation.ts
@@ -3,7 +3,7 @@
*
* A sidebar is build using a list of SidebarItems.
*/
-const ALL_SIDEBAR_LINKS = ['profile', 'dashboard', 'applications', 'sessions', 'partitions', 'results', 'tasks', 'healthcheck', 'divider'] as const;
+const ALL_SIDEBAR_LINKS = ['profile', 'dashboard', 'applications', 'sessions', 'partitions', 'results', 'tasks', 'divider'] as const;
export type Sidebar = typeof ALL_SIDEBAR_LINKS[number];