From 40b9a0f283423a5bf866f4fab810f73ad99eaa2d Mon Sep 17 00:00:00 2001 From: Faust1 Date: Fri, 21 Jun 2024 15:13:14 +0200 Subject: [PATCH 1/4] chore: rework show component --- .../show-card-content.component.html | 27 ++- .../components/show-card-content.component.ts | 89 +++++---- src/app/components/show-card.component.ts | 13 +- src/app/components/show-page.component.html | 2 +- src/app/components/show-page.component.ts | 8 +- src/app/partitions/show.component.ts | 53 +++--- src/app/pipes/duration.pipe.ts | 3 +- src/app/results/show.component.ts | 61 +++---- src/app/sessions/show.component.ts | 171 ++++++++++-------- src/app/tasks/show.component.ts | 76 ++++---- src/app/types/components/show.ts | 61 +++++-- 11 files changed, 306 insertions(+), 258 deletions(-) diff --git a/src/app/components/show-card-content.component.html b/src/app/components/show-card-content.component.html index e334ff98e..65b53c6ad 100644 --- a/src/app/components/show-card-content.component.html +++ b/src/app/components/show-card-content.component.html @@ -1,27 +1,25 @@ @if (!isArray(data) && keys.length > 0) { @for(key of keys; track key) { - @if (!isArray(findValue(key)) && !isObject(findValue(key))) { + @if (!isArray(data[key]) && !isObject(key)) {

- {{ pretty(key) }} - : + {{ pretty(key) }}: - @if (isString(findValue(key))) { - {{ findValue(key) }} - } @else if (isNumber(findValue(key)) && !isStatus(key)) { - {{ findValue(key) }} - } @else if (isStatus(key)) { - {{ statusToLabel(key) }} - } @else if (isTimestamp(findValue(key))) { - {{ toTimestamp(key) }} - } @else if (isDuration(findValue(key))) { - {{ toTime(key) }} + @if (isStatus(key)) { + {{ statusToLabel(key) | emptyCell }} + } @else if (isString(key) || isNumber(key)) { + {{ data[key] | emptyCell }} + } @else if (isTimestamp(key)) { + {{ toDate(key) | date: 'yyyy-MM-dd  HH:mm:ss' | emptyCell}} + } @else if (isDuration(key)) { + {{ toDuration(key) | duration | emptyCell }} } @else { - }

- } @else if (isArray(findValue(key))) { + } @else if (isArray(data[key])) { + {{ pretty(key) }}: } @else { + {{ pretty(key) }}: } } diff --git a/src/app/components/show-card-content.component.ts b/src/app/components/show-card-content.component.ts index 9f5819b12..1d266b84d 100644 --- a/src/app/components/show-card-content.component.ts +++ b/src/app/components/show-card-content.component.ts @@ -1,5 +1,8 @@ +import { DatePipe } from '@angular/common'; import { Component, Input, OnChanges } from '@angular/core'; import { Duration, Timestamp } from '@ngx-grpc/well-known-types'; +import { DurationPipe } from '@pipes/duration.pipe'; +import { EmptyCellPipe } from '@pipes/empty-cell.pipe'; type Data = { [key: string]: string | string[] | Data; @@ -21,13 +24,25 @@ app-show-card-content { margin: 0; } `], - standalone: true, + imports: [ + DurationPipe, + DatePipe, + EmptyCellPipe, + ], + standalone: true }) export class ShowCardContentComponent implements OnChanges { - @Input({ required: true }) data: T | T[] | null = null; + @Input({ required: true }) set data(entry: T | T[] | null) { + this._data = entry as Data; + } @Input({ required: true }) statuses: Record = []; keys: string[] = []; + private _data: Data; + + get data(): Data { + return this._data; + } ngOnChanges() { if (this.data) { @@ -46,12 +61,12 @@ export class ShowCardContentComponent implements OnChanges { return key.replaceAll('_', '').replace(/(? str.toUpperCase()); } - isString(value: unknown): boolean { - return typeof value === 'string'; + isString(key: string): boolean { + return typeof this.data[key] === 'string'; } - isNumber(value: unknown): boolean { - return typeof value === 'number'; + isNumber(key: string): boolean { + return typeof this.data[key] === 'number'; } isStatus(key: string): boolean { @@ -62,6 +77,10 @@ export class ShowCardContentComponent implements OnChanges { return Array.isArray(value); } + isTimestamp(key: string): boolean { + return this.data[key] instanceof Timestamp; + } + hasLength(value: unknown): boolean { return value != undefined && (value as unknown[]).length > 0; } @@ -70,35 +89,25 @@ export class ShowCardContentComponent implements OnChanges { return value as unknown[]; } - isObject(value: unknown): boolean { - return typeof value === 'object' && !this.isArray(value) && !this.isDuration(value) && !this.isTimestamp(value); + isObject(key: string): boolean { + return typeof this.data[key] === 'object' && !this.isArray(this.data[key]) && !this.isDuration(key) && !this.isTimestamp(key); } - isDuration(value: unknown): boolean { - return value instanceof Duration; + isDuration(key: string): boolean { + const duration = this.data[key] as unknown as Duration; + return !Number.isNaN(duration?.nanos) && !!duration?.seconds; } - isTimestamp(value: unknown): boolean { - return value instanceof Timestamp; - } - - /** - * Search for a string in the component JSON data. - * @param key the key of the JSON data you are looking for. - * @returns the string, or "-" if not found. - */ - findValue(key: string) { - if (!this.data) { - return '-'; - } - - const value = (this.data as unknown as Data)[key]; - - if (value === null || value === undefined) { - return '-'; + toDate(key: string): Date | undefined { + if (this.data) { + const timestamp = this.data[key] as unknown as Timestamp; + return timestamp.toDate(); } + return undefined; + } - return value; + toDuration(key: string) { + return this.data[key] as unknown as Duration ?? null; } /** @@ -111,7 +120,7 @@ export class ShowCardContentComponent implements OnChanges { return []; } - const value = (this.data as unknown as Data)[key]; + const value = this.data[key]; if (value === null || value === undefined) { return []; @@ -130,7 +139,7 @@ export class ShowCardContentComponent implements OnChanges { return {}; } - const value = (this.data as unknown as Data)[key]; + const value = this.data[key]; if (value === null || value === undefined) { return {}; @@ -150,7 +159,7 @@ export class ShowCardContentComponent implements OnChanges { return '-'; } - const value = (this.data as unknown as Data)[key] as unknown as Duration; + const value = this.data[key] as unknown as Duration; if (!value || value.seconds === undefined || value.nanos === undefined || (value.seconds === '0' && value.nanos === 0)) { @@ -171,7 +180,7 @@ export class ShowCardContentComponent implements OnChanges { return '-'; } - const value = new Timestamp((this.data as unknown as Data)[key] as Data); + const value = new Timestamp(this.data[key] as Data); if (value.seconds === undefined || value.nanos === undefined || (value.seconds === '0' && value.nanos === 0)) { @@ -186,13 +195,13 @@ export class ShowCardContentComponent implements OnChanges { * @param key the key of the status * @returns the label if found, "-" if not */ - statusToLabel(key: string): string { - if (!this.data || !this.statuses) { - return '-'; + statusToLabel(key: string) { + if (this.data && this.statuses) { + const label = this.statuses[Number(this.data[key])]; + if (label) { + return label; + } } - - const status = Number((this.data as unknown as Data)[key]); - - return this.statuses[status] ? this.statuses[status] : '-'; + return null; } } diff --git a/src/app/components/show-card.component.ts b/src/app/components/show-card.component.ts index 26da9bb1b..b7227a747 100644 --- a/src/app/components/show-card.component.ts +++ b/src/app/components/show-card.component.ts @@ -1,7 +1,6 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { Subject } from 'rxjs'; import { DataRaw } from '@app/types/data'; import { ShowCardContentComponent } from './show-card-content.component'; @@ -20,13 +19,7 @@ pre { MatProgressSpinnerModule ] }) -export class ShowCardComponent implements OnInit { - @Input({ required: true }) data$: Subject; +export class ShowCardComponent { + @Input({ required: true }) data: T | null; @Input() statuses: Record = []; - - data: T | null = null; - - ngOnInit(): void { - this.data$.subscribe(data => this.data = data); - } } diff --git a/src/app/components/show-page.component.html b/src/app/components/show-page.component.html index 78945742b..2baed7130 100644 --- a/src/app/components/show-page.component.html +++ b/src/app/components/show-page.component.html @@ -8,4 +8,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/components/show-page.component.ts b/src/app/components/show-page.component.ts index a9a192451..91dae72bd 100644 --- a/src/app/components/show-page.component.ts +++ b/src/app/components/show-page.component.ts @@ -1,10 +1,9 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Subject } from 'rxjs'; import { ShowActionButton } from '@app/types/components/show'; import { DataRaw } from '@app/types/data'; import { NotificationService } from '@services/notification.service'; @@ -33,11 +32,12 @@ span { MatIconModule, ClipboardModule, MatButtonModule - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ShowPageComponent{ @Input({ required: true }) id: string | null = null; - @Input({ required: true }) data$: Subject; + @Input({required: true }) data: T | null; @Input() statuses: Record = []; @Input() sharableURL: string | null = null; @Input({ required: true }) actionsButton: ShowActionButton[]; diff --git a/src/app/partitions/show.component.ts b/src/app/partitions/show.component.ts index b34ff3d83..0cf2d326d 100644 --- a/src/app/partitions/show.component.ts +++ b/src/app/partitions/show.component.ts @@ -1,8 +1,7 @@ -import { FilterArrayOperator, FilterStringOperator, PartitionRawEnumField, SessionRawEnumField, TaskOptionEnumField } from '@aneoconsultingfr/armonik.api.angular'; -import { AfterViewInit, Component, OnInit, inject } from '@angular/core'; +import { FilterArrayOperator, FilterStringOperator, GetPartitionResponse, SessionRawEnumField, TaskOptionEnumField } from '@aneoconsultingfr/armonik.api.angular'; +import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { catchError, map, switchMap } from 'rxjs'; import { AppShowComponent, ShowActionButton, ShowActionInterface } from '@app/types/components/show'; import { ShowPageComponent } from '@components/show-page.component'; import { FiltersService } from '@services/filters.service'; @@ -15,12 +14,12 @@ import { TableService } from '@services/table.service'; import { UtilsService } from '@services/utils.service'; import { PartitionsFiltersService } from './services/partitions-filters.service'; import { PartitionsGrpcService } from './services/partitions-grpc.service'; -import { PartitionRaw, PartitionRawFieldKey, PartitionRawListOptions } from './types'; +import { PartitionRaw } from './types'; @Component({ selector: 'app-partitions-show', template: ` - + Partition @@ -46,9 +45,9 @@ import { PartitionRaw, PartitionRawFieldKey, PartitionRawListOptions } from './t MatIconModule ] }) -export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface { - protected override _grpcService = inject(PartitionsGrpcService); - private _filtersService = inject(FiltersService); +export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface, OnDestroy { + readonly grpcService = inject(PartitionsGrpcService); + private readonly filtersService = inject(FiltersService); actionButtons: ShowActionButton[] = [ { @@ -74,31 +73,31 @@ export class ShowComponent extends AppShowComponent { - return this._grpcService.get$(this.id); - }), - map((data) => { - return data.partition ?? null; - }), - catchError(error => this.handleError(error)) - ).subscribe((data) => { - if (data) { - this.data = data; - this._filtersService.createFilterQueryParams(this.actionButtons, 'sessions', this.partitionsKey, this.data.id); - this._filtersService.createFilterQueryParams(this.actionButtons, 'tasks', this.tasksKey, this.data.id); - this.data$.next(data); - } - }); - + this.subscribeToData(); this.getIdByRoute(); } + ngOnDestroy(): void { + this.unsubscribe(); + } + + getDataFromResponse(data: GetPartitionResponse): PartitionRaw | undefined { + return data.partition; + } + + afterDataFetching(): void { + const data = this.data(); + if (data) { + this.filtersService.createFilterQueryParams(this.actionButtons, 'sessions', this.partitionsKey, data.id); + this.filtersService.createFilterQueryParams(this.actionButtons, 'tasks', this.tasksKey, data.id); + } + } + get partitionsKey() { - return this._filtersService.createQueryParamsKey(0, 'root', FilterArrayOperator.FILTER_ARRAY_OPERATOR_CONTAINS, SessionRawEnumField.SESSION_RAW_ENUM_FIELD_PARTITION_IDS); + return this.filtersService.createQueryParamsKey(0, 'root', FilterArrayOperator.FILTER_ARRAY_OPERATOR_CONTAINS, SessionRawEnumField.SESSION_RAW_ENUM_FIELD_PARTITION_IDS); } get tasksKey() { - return this._filtersService.createQueryParamsKey(0, 'options', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, TaskOptionEnumField.TASK_OPTION_ENUM_FIELD_PARTITION_ID); + return this.filtersService.createQueryParamsKey(0, 'options', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, TaskOptionEnumField.TASK_OPTION_ENUM_FIELD_PARTITION_ID); } } diff --git a/src/app/pipes/duration.pipe.ts b/src/app/pipes/duration.pipe.ts index 502a90edd..cdd997992 100644 --- a/src/app/pipes/duration.pipe.ts +++ b/src/app/pipes/duration.pipe.ts @@ -3,7 +3,7 @@ import { Duration } from '@ngx-grpc/well-known-types'; @Pipe({ name: 'duration', standalone: true }) export class DurationPipe implements PipeTransform { - transform(value: Duration | null) { + transform(value: Duration | null | undefined) { if (!value) { return null; } @@ -23,7 +23,6 @@ export class DurationPipe implements PipeTransform { const minutesStr = minutes > 0 ? `${minutes}m ` : ''; const secondsStr = secondsRemaining > 0 ? `${secondsRemaining}s ` : ''; const millisStr = millis > 0 ? `${Math.trunc(millis)}ms` : ''; - return `${hoursStr}${minutesStr}${secondsStr}${millisStr}`.trim(); } } diff --git a/src/app/results/show.component.ts b/src/app/results/show.component.ts index d9b28da0c..542b5fcf6 100644 --- a/src/app/results/show.component.ts +++ b/src/app/results/show.component.ts @@ -1,8 +1,7 @@ -import { ResultRawEnumField } from '@aneoconsultingfr/armonik.api.angular'; -import { AfterViewInit, Component, OnInit, inject } from '@angular/core'; +import { GetResultResponse } from '@aneoconsultingfr/armonik.api.angular'; +import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { catchError, map, switchMap } from 'rxjs'; import { AppShowComponent, ShowActionButton, ShowActionInterface } from '@app/types/components/show'; import { ShowPageComponent } from '@components/show-page.component'; import { NotificationService } from '@services/notification.service'; @@ -14,13 +13,13 @@ import { TableService } from '@services/table.service'; import { UtilsService } from '@services/utils.service'; import { ResultsFiltersService } from './services/results-filters.service'; import { ResultsGrpcService } from './services/results-grpc.service'; -import { ResultsStatusesService } from './services/results-statuses.service';import { ResultRaw, ResultRawFieldKey, ResultRawListOptions } from './types'; - +import { ResultsStatusesService } from './services/results-statuses.service'; +import { ResultRaw } from './types'; @Component({ selector: 'app-result-show', template: ` - + Result @@ -46,10 +45,10 @@ import { ResultsStatusesService } from './services/results-statuses.service';imp MatIconModule, ] }) -export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface { +export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface, OnDestroy { - protected override _grpcService = inject(ResultsGrpcService); - private _resultsStatusesService = inject(ResultsStatusesService); + readonly grpcService = inject(ResultsGrpcService); + private readonly resultsStatusesService = inject(ResultsStatusesService); actionButtons: ShowActionButton[] = [ { @@ -71,32 +70,32 @@ export class ShowComponent extends AppShowComponent { - return this._grpcService.get$(this.id); - }), - map((data) => { - return data.result ?? null; - }), - catchError(error => this.handleError(error)) - ).subscribe((data) => { - if (data) { - this.data = data; - this.setLink('session', 'sessions', data.sessionId); - if(data.sessionId === data.ownerTaskId) { - this.actionButtons = this.actionButtons.filter(element => element.id !== 'task'); - } else { - this.setLink('task', 'tasks', data.ownerTaskId); - } - this.data$.next(data); - } - }); - + this.subscribeToData(); this.getIdByRoute(); } + ngOnDestroy() { + this.unsubscribe(); + } + + getDataFromResponse(data: GetResultResponse): ResultRaw | undefined { + return data.result; + } + + afterDataFetching(): void { + const data = this.data(); + if (data) { + this.setLink('session', 'sessions', data.sessionId); + if(!data.ownerTaskId || data.sessionId === data.ownerTaskId) { + this.actionButtons = this.actionButtons.filter(element => element.id !== 'task'); + } else { + this.setLink('task', 'tasks', data.ownerTaskId); + } + } + } + get statuses() { - return this._resultsStatusesService.statuses; + return this.resultsStatusesService.statuses; } setLink(actionId: string, baseLink: string, id: string) { diff --git a/src/app/sessions/show.component.ts b/src/app/sessions/show.component.ts index aadd7ce36..c005f9c04 100644 --- a/src/app/sessions/show.component.ts +++ b/src/app/sessions/show.component.ts @@ -1,10 +1,10 @@ -import { FilterStringOperator, ResultRawEnumField, SessionRawEnumField, SessionTaskOptionEnumField, TaskSummaryEnumField } from '@aneoconsultingfr/armonik.api.angular'; -import { AfterViewInit, Component, OnInit, inject } from '@angular/core'; +import { FilterStringOperator, GetSessionResponse, ResultRawEnumField, TaskSummaryEnumField } from '@aneoconsultingfr/armonik.api.angular'; +import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { Timestamp } from '@ngx-grpc/well-known-types'; -import { Subject, catchError, map, switchMap } from 'rxjs'; +import { Subject, map, switchMap } from 'rxjs'; import { TasksFiltersService } from '@app/tasks/services/tasks-filters.service'; import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service'; import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service'; @@ -23,12 +23,12 @@ import { SessionsFiltersService } from './services/sessions-filters.service'; import { SessionsGrpcService } from './services/sessions-grpc.service'; import { SessionsIndexService } from './services/sessions-index.service'; import { SessionsStatusesService } from './services/sessions-statuses.service'; -import { SessionRaw, SessionRawFieldKey, SessionRawListOptions } from './types'; +import { SessionRaw } from './types'; @Component({ selector: 'app-sessions-show', template: ` - + Session @@ -60,8 +60,8 @@ import { SessionRaw, SessionRawFieldKey, SessionRawListOptions } from './types'; MatIconModule, ] }) -export class ShowComponent extends AppShowComponent - implements OnInit, AfterViewInit, ShowActionInterface, ShowCancellableInterface, ShowClosableInterface { +export class ShowComponent extends AppShowComponent + implements OnInit, AfterViewInit, ShowActionInterface, ShowCancellableInterface, ShowClosableInterface, OnDestroy { cancel$ = new Subject(); pause$ = new Subject(); @@ -75,11 +75,10 @@ export class ShowComponent extends AppShowComponent(); canResume$ = new Subject(); @@ -159,86 +158,106 @@ export class ShowComponent extends AppShowComponent { - return this._grpcService.get$(this.id); - }), - map((data) => { - return data.session ?? null; - }), - catchError(error => this.handleError(error)) - ).subscribe((data) => { - if (data) { - this.data = data; - this.lowerDuration$.next(); - this.upperDuration$.next(); - this.data$.next(data); - this._filtersService.createFilterPartitionQueryParams(this.actionButtons, this.data.partitionIds); - this._filtersService.createFilterQueryParams(this.actionButtons, 'results', this.resultsKey, this.data.sessionId); - this._filtersService.createFilterQueryParams(this.actionButtons, 'tasks', this.tasksKey, this.data.sessionId); - this.canPause$.next(!this._sessionsStatusesService.canPause(this.data.status)); - this.canResume$.next(!this._sessionsStatusesService.canResume(this.data.status)); - this.canCancel$.next(!this._sessionsStatusesService.canCancel(this.data.status)); - this.canClose$.next(!this._sessionsStatusesService.canClose(this.data.status)); - } - }); - + this.subscribeToData(); + this.subscribeToDuration(); + this.subscribeToInteractions(); this.getIdByRoute(); - this.cancel$.subscribe(() => { - this.cancel(); - }); + } - this.pause$.subscribe(() => { - this.pause(); - }); + ngOnDestroy(): void { + this.unsubscribe(); + } - this.resume$.subscribe(() => { - this.resume(); - }); + getDataFromResponse(data: GetSessionResponse): SessionRaw | undefined { + return data.session; + } - this.delete$.subscribe(() => { - this.delete(); - }); + afterDataFetching(): void { + const data = this.data(); + if (data) { + this.lowerDuration$.next(); + this.upperDuration$.next(); + this.filtersService.createFilterPartitionQueryParams(this.actionButtons, data.partitionIds); + this.filtersService.createFilterQueryParams(this.actionButtons, 'results', this.resultsKey(), data.sessionId); + this.filtersService.createFilterQueryParams(this.actionButtons, 'tasks', this.tasksKey(), data.sessionId); + this.canPause$.next(!this.sessionsStatusesService.canPause(data.status)); + this.canResume$.next(!this.sessionsStatusesService.canResume(data.status)); + this.canCancel$.next(!this.sessionsStatusesService.canCancel(data.status)); + this.canClose$.next(!this.sessionsStatusesService.canClose(data.status)); + } + } - this.close$.subscribe(() => { - this.close(); - }); + get statuses() { + return this.sessionsStatusesService.statuses; + } - this.lowerDuration$.pipe( + subscribeToDuration() { + const lowerDurationSubscription = this.lowerDuration$.pipe( switchMap(() => { - return this._grpcService.getTaskData$(this.id, 'createdAt', 'asc').pipe(map(d => d.date)); + return this.grpcService.getTaskData$(this.id, 'createdAt', 'asc').pipe(map(d => d.date)); }) ).subscribe((data) => { this.lowerDate = data; this.computeDuration$.next(); }); - this.upperDuration$.pipe( + const upperDurationSubscription = this.upperDuration$.pipe( switchMap(() => { - return this._grpcService.getTaskData$(this.id, 'endedAt', 'desc').pipe(map(d => d.date)); + return this.grpcService.getTaskData$(this.id, 'endedAt', 'desc').pipe(map(d => d.date)); }) ).subscribe((data) => { this.upperDate = data; this.computeDuration$.next(); }); - this.computeDuration$.subscribe(() => { - if (this.data && this.lowerDate && this.upperDate) { - this.data.duration = { + const computeDurationSubscription = this.computeDuration$.subscribe(() => { + const data = this.data(); + if (data && this.lowerDate && this.upperDate) { + data.duration = { seconds: (Number(this.upperDate.seconds) - Number(this.lowerDate.seconds)).toString(), nanos: Math.abs(this.upperDate.nanos - this.lowerDate.nanos) }; + this.data.set(data); } }); + + this.subscriptions.add(lowerDurationSubscription); + this.subscriptions.add(upperDurationSubscription); + this.subscriptions.add(computeDurationSubscription); } - get statuses() { - return this._sessionsStatusesService.statuses; + subscribeToInteractions() { + const cancelSubscription = this.cancel$.subscribe(() => { + this.cancel(); + }); + + const pauseSubscription = this.pause$.subscribe(() => { + this.pause(); + }); + + const resumeSubscription = this.resume$.subscribe(() => { + this.resume(); + }); + + const deleteSubscription = this.delete$.subscribe(() => { + this.delete(); + }); + + const closeSubscription = this.close$.subscribe(() => { + this.close(); + }); + + this.subscriptions.add(cancelSubscription); + this.subscriptions.add(pauseSubscription); + this.subscriptions.add(resumeSubscription); + this.subscriptions.add(deleteSubscription); + this.subscriptions.add(closeSubscription); } cancel(): void { - if(this.data?.sessionId) { - this._grpcService.cancel$(this.data.sessionId).subscribe({ + const data = this.data(); + if(data?.sessionId) { + this.grpcService.cancel$(data.sessionId).subscribe({ complete: () => { this.success('Session canceled'); this.refresh.next(); @@ -252,8 +271,9 @@ export class ShowComponent extends AppShowComponent { this.success('Session paused'); this.refresh.next(); @@ -267,8 +287,9 @@ export class ShowComponent extends AppShowComponent { this.success('Session resumed'); this.refresh.next(); @@ -282,8 +303,9 @@ export class ShowComponent extends AppShowComponent { this.success('Session closed'); this.refresh.next(); @@ -297,8 +319,9 @@ export class ShowComponent extends AppShowComponent { this.success('Session deleted'); this.router.navigate(['/sessions']); @@ -311,11 +334,11 @@ export class ShowComponent extends AppShowComponent(0, 'root', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, ResultRawEnumField.RESULT_RAW_ENUM_FIELD_SESSION_ID); + resultsKey() { + return this.filtersService.createQueryParamsKey(0, 'root', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, ResultRawEnumField.RESULT_RAW_ENUM_FIELD_SESSION_ID); } - get tasksKey() { - return this._filtersService.createQueryParamsKey(0, 'root', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_SESSION_ID); + tasksKey() { + return this.filtersService.createQueryParamsKey(0, 'root', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, TaskSummaryEnumField.TASK_SUMMARY_ENUM_FIELD_SESSION_ID); } } diff --git a/src/app/tasks/show.component.ts b/src/app/tasks/show.component.ts index d9ad100cf..09e79a9be 100644 --- a/src/app/tasks/show.component.ts +++ b/src/app/tasks/show.component.ts @@ -1,8 +1,8 @@ -import { FilterStringOperator, ResultRawEnumField, TaskOptionEnumField, TaskSummaryEnumField } from '@aneoconsultingfr/armonik.api.angular'; -import { AfterViewInit, Component, OnInit, inject } from '@angular/core'; +import { FilterStringOperator, GetTaskResponse, ResultRawEnumField } from '@aneoconsultingfr/armonik.api.angular'; +import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Subject, catchError, map, switchMap } from 'rxjs'; +import { Subject } from 'rxjs'; import { AppShowComponent, ShowActionButton, ShowActionInterface, ShowCancellableInterface } from '@app/types/components/show'; import { ShowPageComponent } from '@components/show-page.component'; import { FiltersService } from '@services/filters.service'; @@ -18,12 +18,12 @@ import { UtilsService } from '@services/utils.service'; import { TasksFiltersService } from './services/tasks-filters.service'; import { TasksGrpcService } from './services/tasks-grpc.service'; import { TasksStatusesService } from './services/tasks-statuses.service'; -import { TaskRaw, TaskSummaryFieldKey, TaskSummaryListOptions } from './types'; +import { TaskRaw } from './types'; @Component({ selector: 'app-tasks-show', template: ` - + Task @@ -52,13 +52,13 @@ import { TaskRaw, TaskSummaryFieldKey, TaskSummaryListOptions } from './types'; MatIconModule, ] }) -export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowCancellableInterface, ShowActionInterface { +export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowCancellableInterface, ShowActionInterface, OnDestroy { cancel$ = new Subject(); - private _tasksStatusesService = inject(TasksStatusesService); - private _filtersService = inject(FiltersService); - protected override _grpcService = inject(TasksGrpcService); + private readonly tasksStatusesService = inject(TasksStatusesService); + private readonly filtersService = inject(FiltersService); + readonly grpcService = inject(TasksGrpcService); canCancel$ = new Subject(); @@ -101,34 +101,36 @@ export class ShowComponent extends AppShowComponent { - return this._grpcService.get$(this.id); - }), - map((data) => { - return data.task ?? null; - }), - catchError(error => this.handleError(error)) - ).subscribe((data) => { - if (data) { - this.data = data; - this.data$.next(data); - this.setLink('session', 'sessions', data.sessionId); - if (data.options) { - this.setLink('partition', 'partitions', data.options.partitionId); - } - this._filtersService.createFilterQueryParams(this.actionButtons, 'results', this.resultsKey, data.id); - this.canCancel$.next(this._tasksStatusesService.taskNotEnded(this.data.status)); - } - }); - + this.subscribeToData(); this.getIdByRoute(); - this.cancel$.subscribe(() => this.cancel()); - } + const cancelSubscription = this.cancel$.subscribe(() => this.cancel()); + this.subscriptions.add(cancelSubscription); + } + + ngOnDestroy(): void { + this.unsubscribe(); + } + + getDataFromResponse(data: GetTaskResponse): TaskRaw | undefined { + return data.task; + } + + afterDataFetching(): void { + const data = this.data(); + if (data) { + this.setLink('session', 'sessions', data.sessionId); + if (data.options) { + this.setLink('partition', 'partitions', data.options.partitionId); + } + this.filtersService.createFilterQueryParams(this.actionButtons, 'results', this.resultsKey(), data.id); + this.canCancel$.next(this.tasksStatusesService.taskNotEnded(data.status)); + } + } cancel(): void { - if(this.data) { - this._grpcService.cancel$([this.data.id]).subscribe({ + const data = this.data(); + if(data) { + this.grpcService.cancel$([data.id]).subscribe({ complete: () => { this.success('Task canceled'); this.refresh.next(); @@ -140,8 +142,8 @@ export class ShowComponent extends AppShowComponent(1, 'root', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, ResultRawEnumField.RESULT_RAW_ENUM_FIELD_OWNER_TASK_ID); + resultsKey() { + return this.filtersService.createQueryParamsKey(1, 'root', FilterStringOperator.FILTER_STRING_OPERATOR_EQUAL, ResultRawEnumField.RESULT_RAW_ENUM_FIELD_OWNER_TASK_ID); } setLink(actionId: string, baseLink: string, id: string) { @@ -152,6 +154,6 @@ export class ShowComponent extends AppShowComponent { +export abstract class AppShowComponent { id: string; sharableURL: string = ''; refresh = new Subject(); - data: T | null; - data$: Subject = new Subject(); + data = signal(null); + subscriptions = new Subscription(); - private _iconsService = inject(IconsService); - protected _grpcService: GrpcTableService; - private _shareURLService = inject(ShareUrlService); - private _notificationService = inject(NotificationService); - private _route = inject(ActivatedRoute); + private readonly iconsService = inject(IconsService); + abstract readonly grpcService: GrpcGetInterface; + private readonly shareURLService = inject(ShareUrlService); + private readonly notificationService = inject(NotificationService); + private readonly route = inject(ActivatedRoute); getIcon(name: string): string { - return this._iconsService.getIcon(name); + return this.iconsService.getIcon(name); } onRefresh() { this.refresh.next(); } + subscribeToData() { + const refreshSubscription = this.refresh.pipe( + switchMap(() => { + return this.get$(); + }), + map((data) => this.getDataFromResponse(data) ?? null), + catchError((error) => this.handleError(error)) + ).subscribe((data) => { + this.data.set(data); + this.afterDataFetching(); + }); + this.subscriptions.add(refreshSubscription); + } + + private get$(): Observable { + return this.grpcService.get$(this.id); + } + + unsubscribe() { + this.subscriptions.unsubscribe(); + } + + abstract getDataFromResponse(data: R): T | undefined; + + abstract afterDataFetching(): void; + getIdByRoute() { - this._route.params.pipe( + this.route.params.pipe( map(params => params['id']), ).subscribe(id => { this.id = id; @@ -73,14 +98,14 @@ export abstract class AppShowComponent Date: Fri, 21 Jun 2024 15:24:00 +0200 Subject: [PATCH 2/4] added onPush change strategy --- src/app/components/show-page.component.ts | 5 ++--- src/app/partitions/show.component.ts | 10 ++++++---- src/app/results/show.component.ts | 10 ++++++---- src/app/sessions/show.component.ts | 12 +++++++----- src/app/tasks/show.component.ts | 8 +++++--- src/app/types/components/show.ts | 1 - 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/app/components/show-page.component.ts b/src/app/components/show-page.component.ts index 91dae72bd..4e9f70ac0 100644 --- a/src/app/components/show-page.component.ts +++ b/src/app/components/show-page.component.ts @@ -1,6 +1,6 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; @@ -32,8 +32,7 @@ span { MatIconModule, ClipboardModule, MatButtonModule - ], - changeDetection: ChangeDetectionStrategy.OnPush + ] }) export class ShowPageComponent{ @Input({ required: true }) id: string | null = null; diff --git a/src/app/partitions/show.component.ts b/src/app/partitions/show.component.ts index 0cf2d326d..f707ac742 100644 --- a/src/app/partitions/show.component.ts +++ b/src/app/partitions/show.component.ts @@ -1,5 +1,5 @@ import { FilterArrayOperator, FilterStringOperator, GetPartitionResponse, SessionRawEnumField, TaskOptionEnumField } from '@aneoconsultingfr/armonik.api.angular'; -import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; import { AppShowComponent, ShowActionButton, ShowActionInterface } from '@app/types/components/show'; @@ -19,7 +19,7 @@ import { PartitionRaw } from './types'; @Component({ selector: 'app-partitions-show', template: ` - + Partition @@ -43,7 +43,8 @@ import { PartitionRaw } from './types'; imports: [ ShowPageComponent, MatIconModule - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface, OnDestroy { readonly grpcService = inject(PartitionsGrpcService); @@ -69,12 +70,13 @@ export class ShowComponent extends AppShowComponent + Result @@ -43,7 +43,8 @@ import { ResultRaw } from './types'; imports: [ ShowPageComponent, MatIconModule, - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface, OnDestroy { @@ -66,12 +67,13 @@ export class ShowComponent extends AppShowComponent + Session @@ -58,7 +58,8 @@ import { SessionRaw } from './types'; imports: [ ShowPageComponent, MatIconModule, - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowActionInterface, ShowCancellableInterface, ShowClosableInterface, OnDestroy { @@ -154,6 +155,7 @@ export class ShowComponent extends AppShowComponent + Task @@ -50,7 +50,8 @@ import { TaskRaw } from './types'; imports: [ ShowPageComponent, MatIconModule, - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ShowComponent extends AppShowComponent implements OnInit, AfterViewInit, ShowCancellableInterface, ShowActionInterface, OnDestroy { @@ -105,6 +106,7 @@ export class ShowComponent extends AppShowComponent im this.getIdByRoute(); const cancelSubscription = this.cancel$.subscribe(() => this.cancel()); this.subscriptions.add(cancelSubscription); + this.refresh.next(); } ngOnDestroy(): void { diff --git a/src/app/types/components/show.ts b/src/app/types/components/show.ts index 78bec227f..ab1fd7200 100644 --- a/src/app/types/components/show.ts +++ b/src/app/types/components/show.ts @@ -87,7 +87,6 @@ export abstract class AppShowComponent map(params => params['id']), ).subscribe(id => { this.id = id; - this.refresh.next(); }); } From 458245d389e15a8f7d31c4dc425348f7b03d0989 Mon Sep 17 00:00:00 2001 From: Faust1 Date: Fri, 21 Jun 2024 16:16:04 +0200 Subject: [PATCH 3/4] test: updates --- .../show-card-content.component.spec.ts | 284 ++++++------------ .../components/show-card-content.component.ts | 128 +++----- .../components/show-card.component.spec.ts | 26 +- 3 files changed, 139 insertions(+), 299 deletions(-) diff --git a/src/app/components/show-card-content.component.spec.ts b/src/app/components/show-card-content.component.spec.ts index 92703a283..ea6032199 100644 --- a/src/app/components/show-card-content.component.spec.ts +++ b/src/app/components/show-card-content.component.spec.ts @@ -7,19 +7,44 @@ import { ShowCardContentComponent } from './show-card-content.component'; * is why it is call SandBox. */ type SandBox = { - [key: string]: string | string[] | number | {seconds: string | number | undefined, nanos: string | number | undefined}; + [key: string]: string | (string | number)[] | number | undefined | {seconds: string | number | undefined, nanos: string | number | undefined} | SandBox; }; type Data = { [key: string]: string | string[] | Data; }; -describe('ShowCardContentComponent', () => { +const time: Timestamp = new Timestamp({ + seconds: '179543301', + nanos: 0 +}); + +const duration = new Duration({ + seconds: '3600', + nanos: 10 +}); + +const data: SandBox = { + string: 'valid-string', + empty: '', + number: 1, + undefined: undefined, + array: [1, 2, 3], + emptyArray: [], + status: 'some-false-status', + object: { + value: 'some-value' + }, + time: time, + duration: duration, +}; +describe('ShowCardContentComponent', () => { let component: ShowCardContentComponent; beforeEach(() => { component = new ShowCardContentComponent(); + component.data = data; }); it('Should run', () => { @@ -28,184 +53,177 @@ describe('ShowCardContentComponent', () => { describe('isString', () => { it('Should return true when a string is provided', () => { - expect(component.isString('mystring')).toBeTruthy(); + expect(component.isString('string')).toBeTruthy(); }); it('Should return true when a empty string is provided', () => { - expect(component.isString('')).toBeTruthy(); + expect(component.isString('empty')).toBeTruthy(); }); it('Should return false when the given parameter is not a string', () => { - expect(component.isString(1234)).toBeFalsy(); + expect(component.isString('number')).toBeFalsy(); }); it('Should return false when the given parameter is undefined', () => { - expect(component.isString(undefined)).toBeFalsy(); + expect(component.isString('undefined')).toBeFalsy(); }); it('Should return false when an array is provided', () => { - const myArray: Array = ['1', '2', '3']; - expect(component.isString(myArray)).toBeFalsy(); + expect(component.isString('array')).toBeFalsy(); }); }); describe('isNumber', () => { it('Should return true when the given parameter is a number', () => { - expect(component.isNumber(1)); + expect(component.isNumber('number')); }); it('Should return false when the given parameter is not a number', () => { - expect(component.isNumber('Some string')).toBeFalsy(); + expect(component.isNumber('string')).toBeFalsy(); }); it('Should return false when the given parameter is undefined', () => { - expect(component.isNumber(undefined)).toBeFalsy(); + expect(component.isNumber('undefined')).toBeFalsy(); }); it('Should return false when an array is provided', () => { - const myArray: Array = [1, 2, 3, 4, 5]; - expect(component.isNumber(myArray)).toBeFalsy(); + expect(component.isNumber('array')).toBeFalsy(); }); }); describe('isStatus', () => { it('Should return true when a string contains "status"', () => { - expect(component.isStatus('a string with status')).toBeTruthy(); + expect(component.isStatus('status')).toBeTruthy(); }); it('Should return false when the given string does not have "status"', () => { - expect(component.isStatus('a string with statu-s')).toBeFalsy(); + expect(component.isStatus('string')).toBeFalsy(); }); it('Should return false when the given parameter is an empty string', () => { - expect(component.isStatus('')).toBeFalsy(); + expect(component.isStatus('empty')).toBeFalsy(); }); }); describe('isArray', () => { it('Should return true when an array is provided', () => { - const myArray: Array = [1, 2, 3, 4, 5]; - expect(component.isArray(myArray)).toBeTruthy(); + expect(component.isArray(data['array'])).toBeTruthy(); }); it('Should return true when an empty array is provided', () => { - const myArray: Array = []; - expect(component.isArray(myArray)).toBeTruthy(); + expect(component.isArray(data['emptyArray'])).toBeTruthy(); }); it('Should return false when a number is provided', () => { - expect(component.isArray(1)).toBeFalsy(); + expect(component.isArray(data['number'])).toBeFalsy(); }); it('Should return false when a string is provided', () => { - expect(component.isArray('my string')).toBeFalsy(); + expect(component.isArray(data['string'])).toBeFalsy(); }); it('Should return false when undefined is provided', () => { - expect(component.isArray(undefined)).toBeFalsy(); + expect(component.isArray(data['undefined'])).toBeFalsy(); }); }); describe('isObject', () => { it('Should return true when an object is provided', () => { - expect(component.isObject(new Object())).toBeTruthy(); + expect(component.isObject('object')).toBeTruthy(); }); it('Should return false when a timestamp is provided', () => { - expect(component.isObject(new Timestamp())).toBeFalsy; + expect(component.isObject('time')).toBeFalsy; }); it('Should return false when a duration is provided', () => { - expect(component.isObject(new Duration())).toBeFalsy(); + expect(component.isObject('duration')).toBeFalsy(); }); it('Should return false when an array is provided', () => { - const myArray: Array = [1, 2, 3, 4, 5]; - expect(component.isObject(myArray)).toBeFalsy(); + expect(component.isObject('array')).toBeFalsy(); }); it('Should return false when a string is provided', () => { - expect(component.isObject('a string')).toBeFalsy(); + expect(component.isObject('string')).toBeFalsy(); }); it('Should return false when a number is provided', () => { - expect(component.isObject(1)).toBeFalsy(); + expect(component.isObject('number')).toBeFalsy(); }); it('Should return false when undefinded is provided', () => { - expect(component.isObject(undefined)).toBeFalsy(); + expect(component.isObject('undefined')).toBeFalsy(); }); }); describe('isDuration', () => { it('Should return true when a duration is provided', () => { - expect(component.isDuration(new Duration())).toBeTruthy(); + expect(component.isDuration('duration')).toBeTruthy(); }); it('Should return false when a number is provided', () => { - expect(component.isDuration(1)).toBeFalsy(); + expect(component.isDuration('number')).toBeFalsy(); }); it('Should return false when a string is provided', () => { - expect(component.isDuration('my string')).toBeFalsy(); + expect(component.isDuration('string')).toBeFalsy(); }); it('Should return false when an array is provided', () => { - const myArray: Array = [1, 2, 3, 4, 5]; - expect(component.isDuration(myArray)).toBeFalsy(); + expect(component.isDuration('array')).toBeFalsy(); }); it('Should return false when undefined is provided', () => { - expect(component.isDuration(undefined)).toBeFalsy(); + expect(component.isDuration('undefined')).toBeFalsy(); }); }); describe('isTimestamp', () => { it('Should return true when a timestamp is provided', () => { - expect(component.isTimestamp(new Timestamp())).toBeTruthy(); + expect(component.isTimestamp('time')).toBeTruthy(); }); it('Should return false when a number is provided', () => { - expect(component.isTimestamp(1)).toBeFalsy(); + expect(component.isTimestamp('number')).toBeFalsy(); }); it('Should return false when a string is provided', () => { - expect(component.isTimestamp('my string')).toBeFalsy(); + expect(component.isTimestamp('string')).toBeFalsy(); }); it('Should return false when an array is provided', () => { - const myArray: Array = [1, 2, 3, 4, 5]; - expect(component.isTimestamp(myArray)).toBeFalsy(); + expect(component.isTimestamp('array')).toBeFalsy(); }); it('Should return false when undefined is provided', () => { - expect(component.isTimestamp(undefined)).toBeFalsy(); + expect(component.isTimestamp('undefined')).toBeFalsy(); }); }); describe('hasLength', () => { it('should return true when an non-empty array is provided', () => { - expect(component.hasLength([1])).toBeTruthy(); + expect(component.hasLength(data['array'])).toBeTruthy(); }); it('should return false when an empty array is provided', () => { - expect(component.hasLength([])).toBeFalsy(); + expect(component.hasLength(data['emptyArray'])).toBeFalsy(); }); it('should return true when an non-empty string is provided', () => { - expect(component.hasLength('az')).toBeTruthy(); + expect(component.hasLength(data['string'])).toBeTruthy(); }); it('should return false when an empty string is provided', () => { - expect(component.hasLength('')).toBeFalsy(); + expect(component.hasLength(data['empty'])).toBeFalsy(); }); it('should return false when an number is provided', () => { - expect(component.hasLength(123)).toBeFalsy(); + expect(component.hasLength(data['number'])).toBeFalsy(); }); it('should return false when undefined is provided', () => { - expect(component.hasLength(undefined)).toBeFalsy(); + expect(component.hasLength(data['undefined'])).toBeFalsy(); }); }); @@ -215,40 +233,7 @@ describe('ShowCardContentComponent', () => { }); }); - describe('findValue', () => { - - it('Should return a value if the key is correct', () => { - component.data = { - first_key: 'first_value', - second_key: 'second_value', - third_key: ['third_value', 'fourth_value'] - }; - expect(component.findValue('first_key')).toEqual('first_value'); - expect(component.findValue('third_key')).toEqual(['third_value', 'fourth_value']); - }); - - it('Should return "-" if the key is incorrect', () => { - component.data = { - first_key: 'first_value', - second_key: 'second_value', - third_key: ['third_value', 'fourth_value'] - }; - expect(component.findValue('some_incorrect_key')).toEqual('-'); - }); - - it('Should return "-" if there is no data', () => { - component.data = []; - expect(component.findValue('first_key')).toEqual('-'); - }); - - it('Should return "-" if data is null', () => { - component.data = null; - expect(component.findValue('first_key')).toEqual('-'); - }); - }); - describe('findArray', () => { - it('Should return a value if the key is correct', () => { component.data = { first_key: 'first_value', @@ -280,7 +265,6 @@ describe('ShowCardContentComponent', () => { }); describe('findObject', () => { - it('Should return a value if the key is correct', () => { component.data = { first_key: 'first_value', @@ -307,92 +291,7 @@ describe('ShowCardContentComponent', () => { it('Should return an empty array if data is null', () => { component.data = null; - expect(component.findObject('first_key')).toEqual({}); - }); - }); - - describe('toTime', () => { - it('Should return a string value if the data is correct', () => { - component.data = { - first_key: { seconds: '1234', nanos: '456'}, - second_key: { seconds: 1234, nanos: '456'}, - third_key: { seconds: '1234', nanos: 456} - }; - expect(component.toTime('first_key')).toEqual('1234s 456ns'); - expect(component.toTime('second_key')).toEqual('1234s 456ns'); - expect(component.toTime('third_key')).toEqual('1234s 456ns'); - }); - - it('Should return "-" if time is equal to 0', () => { - component.data = { - first_key: { seconds: '0', nanos: 0}, - second_key: { seconds: '0', nanos: undefined}, - third_key: '' - }; - expect(component.toTime('first_key')).toEqual('-'); - expect(component.toTime('second_key')).toEqual('-'); - }); - - it('Should return "-" if the data is invalid', () => { - component.data = { - first_key: '1234,456', - second_key: 'some string', - third_key: ['45', '62'] - }; - expect(component.toTime('first_key')).toEqual('-'); - expect(component.toTime('second_key')).toEqual('-'); - expect(component.toTime('third_key')).toEqual('-'); - }); - - it('Should return "-" if the data is empty',() => { - expect(component.toTime('first_key')).toEqual('-'); - expect(component.toTime('some_randow_key')).toEqual('-'); - }); - }); - - describe('toTimestamp', () => { - it('Should return a timestamp value if the data is correct', () => { - component.data = { - first_key: { seconds: '1234', nanos: '456'}, - second_key: { seconds: 1234, nanos: '456'}, - third_key: { seconds: '1234', nanos: 456} - }; - - const expectedResult: Date = new Timestamp({ - seconds: '1234', - nanos: 456 - }).toDate(); - - expect(component.toTimestamp('first_key')).toEqual(expectedResult); - expect(component.toTimestamp('second_key')).toEqual(expectedResult); - expect(component.toTimestamp('third_key')).toEqual(expectedResult); - }); - - it('Should return "-" if time is equal to 0', () => { - component.data = { - first_key: { seconds: '0', nanos: 0}, - second_key: { seconds: 0, nanos: undefined}, - third_key: {seconds: undefined, nanos: undefined} - }; - expect(component.toTimestamp('first_key')).toEqual('-'); - expect(component.toTimestamp('second_key')).toEqual('-'); - expect(component.toTimestamp('third_key')).toEqual('-'); - }); - - it('Should return "-" if the data is invalid', () => { - component.data = { - first_key: '1234,456', - second_key: 'some string', - third_key: ['45', '62'] - }; - expect(component.toTimestamp('first_key')).toEqual('-'); - expect(component.toTimestamp('second_key')).toEqual('-'); - expect(component.toTimestamp('third_key')).toEqual('-'); - }); - - it('Should return "-" if the data is empty',() => { - expect(component.toTimestamp('first_key')).toEqual('-'); - expect(component.toTimestamp('some_randow_key')).toEqual('-'); + expect(component.findObject('')).toEqual({}); }); }); @@ -410,21 +309,21 @@ describe('ShowCardContentComponent', () => { expect(component.statusToLabel('second_key')).toEqual('second_label'); }); - it('Should return "-" if no data is provided', () => { + it('Should return "null" if no data is provided', () => { component.statuses = {1: 'first_label', 2: 'second_label'}; - expect(component.statusToLabel('first_key')).toEqual('-'); + expect(component.statusToLabel('first_key')).toBeNull(); }); - it('Should return "-" if no statuses are provided', () => { + it('Should return "null" if no statuses are provided', () => { component.data = { first_key: '1', second_key: 2, third_key: ['45', '62'] }; - expect(component.statusToLabel('first_key')).toEqual('-'); + expect(component.statusToLabel('first_key')).toBeNull(); }); - it('Should return "-" if the data key is invalid', () => { + it('Should return "null" if the data key is invalid', () => { component.statuses = {1: 'first_label', 2: 'second_label'}; component.data = { @@ -432,10 +331,10 @@ describe('ShowCardContentComponent', () => { second_key: 2, third_key: ['45', '62'] }; - expect(component.statusToLabel('some_random_key')).toEqual('-'); + expect(component.statusToLabel('some_random_key')).toBeNull(); }); - it('Should return "-" if the data value is invalid', () => { + it('Should return "null" if the data value is invalid', () => { component.statuses = {1: 'first_label', 2: 'second_label'}; component.data = { @@ -443,8 +342,8 @@ describe('ShowCardContentComponent', () => { second_key: 3, third_key: ['45', '62'] }; - expect(component.statusToLabel('first_key')).toEqual('-'); - expect(component.statusToLabel('second_key')).toEqual('-'); + expect(component.statusToLabel('first_key')).toBeNull(); + expect(component.statusToLabel('second_key')).toBeNull(); }); }); @@ -454,20 +353,25 @@ describe('ShowCardContentComponent', () => { expect(component.pretty('My_String_Should_Change_.')).toEqual('My String Should Change.'); }); - describe('ngOnChange', () => { - it('Should sort data keys and store it into keys on change', () => { - component.data = { - first_key: 'first_value', - second_key: 'second_value', - third_key: ['third_value', 'fourth_value'] - }; - component.ngOnChanges(); - expect(component.keys).toEqual(['first_key', 'second_key', 'third_key']); + describe('toDate', () => { + it ('should return a date from a timestamp', () => { + expect(component.toDate('time')).toEqual(new Date(179543301000)); + }); + + it('should return undefined if there is no data', () => { + component.data = undefined as unknown as SandBox; + expect(component.toDate('time')).toBeUndefined(); + }); + }); + + describe('toDuration', () => { + it('should return the data as a duration', () => { + expect(component.toDuration('duration') instanceof Duration).toBeTruthy(); }); - it('Should not do anything if there is no data', () => { - component.ngOnChanges(); - expect(component.keys).toEqual([]); + it('should return null if the data is undefined', () => { + component.data = undefined as unknown as SandBox; + expect(component.toDuration('duration')).toBeNull(); }); }); }); \ No newline at end of file diff --git a/src/app/components/show-card-content.component.ts b/src/app/components/show-card-content.component.ts index 1d266b84d..27a5e8b9a 100644 --- a/src/app/components/show-card-content.component.ts +++ b/src/app/components/show-card-content.component.ts @@ -1,5 +1,5 @@ import { DatePipe } from '@angular/common'; -import { Component, Input, OnChanges } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { Duration, Timestamp } from '@ngx-grpc/well-known-types'; import { DurationPipe } from '@pipes/duration.pipe'; import { EmptyCellPipe } from '@pipes/empty-cell.pipe'; @@ -31,25 +31,22 @@ app-show-card-content { ], standalone: true }) -export class ShowCardContentComponent implements OnChanges { +export class ShowCardContentComponent { @Input({ required: true }) set data(entry: T | T[] | null) { this._data = entry as Data; + if (entry) { + this.keys = Object.keys(this.data).sort((a, b) => a.toString().localeCompare(b.toString())); + } } @Input({ required: true }) statuses: Record = []; - keys: string[] = []; + keys: (keyof Data)[] = []; private _data: Data; get data(): Data { return this._data; } - ngOnChanges() { - if (this.data) { - this.keys = Object.keys(this.data).sort((a, b) => a.toString().localeCompare(b.toString())); - } - } - /** * Changes the syntax of a camelCase string by removing "_", place a * space between each UpperCase character, and put this character in lowercase, @@ -57,57 +54,62 @@ export class ShowCardContentComponent implements OnChanges { * @param key string to format * @returns formatted string */ - pretty(key: string): string { - return key.replaceAll('_', '').replace(/(? str.toUpperCase()); + pretty(key: keyof Data): string { + return key.toString().replaceAll('_', '').replace(/(? str.toUpperCase()); } - isString(key: string): boolean { + isString(key: keyof Data): boolean { return typeof this.data[key] === 'string'; } - isNumber(key: string): boolean { + isNumber(key: keyof Data): boolean { return typeof this.data[key] === 'number'; } - isStatus(key: string): boolean { - return key.toLowerCase().includes('status'); + isStatus(key: keyof Data): boolean { + return key.toString().toLowerCase().includes('status'); } isArray(value: unknown): boolean { return Array.isArray(value); } - isTimestamp(key: string): boolean { + isTimestamp(key: keyof Data): boolean { return this.data[key] instanceof Timestamp; } hasLength(value: unknown): boolean { - return value != undefined && (value as unknown[]).length > 0; + return (value as unknown[])?.length > 0; } toArray(value: unknown): unknown[] { return value as unknown[]; } - isObject(key: string): boolean { + isObject(key: keyof Data): boolean { return typeof this.data[key] === 'object' && !this.isArray(this.data[key]) && !this.isDuration(key) && !this.isTimestamp(key); } - isDuration(key: string): boolean { + isDuration(key: keyof Data): boolean { const duration = this.data[key] as unknown as Duration; return !Number.isNaN(duration?.nanos) && !!duration?.seconds; } - toDate(key: string): Date | undefined { + toDate(key: keyof Data): Date | undefined { if (this.data) { const timestamp = this.data[key] as unknown as Timestamp; - return timestamp.toDate(); + if (timestamp) { + return timestamp.toDate(); + } } return undefined; } - toDuration(key: string) { - return this.data[key] as unknown as Duration ?? null; + toDuration(key: keyof Data) { + if (this.data?.[key]) { + return new Duration(this.data[key] as Partial); + } + return null; } /** @@ -115,18 +117,14 @@ export class ShowCardContentComponent implements OnChanges { * @param key the key of the JSON data you are looking for. * @returns the array, or an empty array if not found. */ - findArray(key: string): string[] { - if (!this.data) { - return []; - } - - const value = this.data[key]; - - if (value === null || value === undefined) { - return []; + findArray(key: keyof Data): string[] { + if (this.data) { + const value = this.data[key]; + if (value !== null && value !== undefined) { + return value as string[]; + } } - - return value as string[]; + return []; } /** @@ -134,60 +132,14 @@ export class ShowCardContentComponent implements OnChanges { * @param key the key of the JSON data you are looking for. * @returns the object, or an empty object if not found. */ - findObject(key: string): Data { - if (!this.data) { - return {}; - } - - const value = this.data[key]; - - if (value === null || value === undefined) { - return {}; - } - - return value as Data; - } - - /** - * Turns a stored JSON data into a string of Time. - * The JSON data is of type Duration. - * @param key the key of the JSON time you are looking for. - * @returns "seconds+s nanos+s" if found, "-" if not. - */ - toTime(key: string): string { - if (!this.data) { - return '-'; - } - - const value = this.data[key] as unknown as Duration; - - if (!value || value.seconds === undefined || value.nanos === undefined - || (value.seconds === '0' && value.nanos === 0)) { - return '-'; - } - - return `${value.seconds}s ${value.nanos}ns`; - } - - /** - * Turns a stored JSON data into a TimeStamp. - * The JSON data is of type Duration. - * @param key the key of the JSON time you are looking for. - * @returns a Date if found, "-" if not. - */ - toTimestamp(key: string): string | Date { - if (!this.data) { - return '-'; - } - - const value = new Timestamp(this.data[key] as Data); - - if (value.seconds === undefined || value.nanos === undefined - || (value.seconds === '0' && value.nanos === 0)) { - return '-'; + findObject(key: keyof Data): Data { + if (this.data) { + const value = this.data[key]; + if (value !== null && value !== undefined) { + return value as Data; + } } - - return value.toDate(); + return {}; } /** @@ -195,7 +147,7 @@ export class ShowCardContentComponent implements OnChanges { * @param key the key of the status * @returns the label if found, "-" if not */ - statusToLabel(key: string) { + statusToLabel(key: keyof Data) { if (this.data && this.statuses) { const label = this.statuses[Number(this.data[key])]; if (label) { diff --git a/src/app/components/show-card.component.spec.ts b/src/app/components/show-card.component.spec.ts index 35b70e3d4..68c571b8a 100644 --- a/src/app/components/show-card.component.spec.ts +++ b/src/app/components/show-card.component.spec.ts @@ -1,32 +1,16 @@ -import { Subject } from 'rxjs'; import { ResultRaw } from '@app/results/types'; import { ShowCardComponent } from './show-card.component'; describe('ShowCardComponent', () => { - let component: ShowCardComponent; + const statuses = { 1: 'status' }; + const data: ResultRaw = {} as ResultRaw; + const component = new ShowCardComponent(); - const subject = new Subject(); - - beforeEach(() => { - component = new ShowCardComponent(); - component.data$ = subject; - component.statuses = { 1: 'status' }; - component.ngOnInit(); - }); + component.data = data; + component.statuses = statuses; it('should create', () => { expect(component).toBeTruthy(); }); - - it('should subscribe to data$', () => { - expect(subject.observed).toBeTruthy(); - }); - - it('should update data', () => { - const result = { name: 'result' } as ResultRaw; - subject.next(result); - expect(component.data).toEqual(result); - }); - }); \ No newline at end of file From 1d34386d1451d21082bd3f141be7f620d80162d1 Mon Sep 17 00:00:00 2001 From: Faust1 Date: Fri, 21 Jun 2024 16:23:39 +0200 Subject: [PATCH 4/4] tests: update pages tests --- src/app/partitions/show.component.spec.ts | 17 ++++++++++++----- src/app/results/show.component.spec.ts | 7 ++----- src/app/sessions/show.component.spec.ts | 21 ++++++++++++++------- src/app/tasks/show.component.spec.ts | 22 ++++++++++++++-------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/app/partitions/show.component.spec.ts b/src/app/partitions/show.component.spec.ts index 61eefc448..8c4bccd69 100644 --- a/src/app/partitions/show.component.spec.ts +++ b/src/app/partitions/show.component.spec.ts @@ -86,17 +86,14 @@ describe('ShowComponent', () => { }); it('should update data on success', () => { - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(component.data).toEqual(returnedPartition); - expect(spy).toHaveBeenCalledWith(returnedPartition); + expect(component.data()).toEqual(returnedPartition); }); it('should not update data if there is none', () => { mockPartitionsGrpcService.get$.mockImplementationOnce(() => of({})); - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(spy).not.toHaveBeenCalled(); + expect(component.data()).toEqual(null); }); it('should catch errors', () => { @@ -134,4 +131,14 @@ describe('ShowComponent', () => { expect(mockNotificationService.error).toHaveBeenCalledWith(error); }); }); + + describe('on destroy', () => { + beforeEach(() => { + component.ngOnDestroy(); + }); + + it('should unsubscribe from subjects', () => { + expect(component.subscriptions.closed).toBeTruthy(); + }); + }); }); \ No newline at end of file diff --git a/src/app/results/show.component.spec.ts b/src/app/results/show.component.spec.ts index 3c81e7297..f2594b812 100644 --- a/src/app/results/show.component.spec.ts +++ b/src/app/results/show.component.spec.ts @@ -97,10 +97,8 @@ describe('ShowComponent', () => { }); it('should update data on success', () => { - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(component.data).toEqual(returnedResult); - expect(spy).toHaveBeenCalledWith(returnedResult); + expect(component.data()).toEqual(returnedResult); }); it(('should set link if sessionId is not the same as ownerTaskId'), () => { @@ -118,9 +116,8 @@ describe('ShowComponent', () => { it('should not update data if there is none', () => { mockResultsGrpcService.get$.mockImplementationOnce(() => of({})); - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(spy).not.toHaveBeenCalled(); + expect(component.data()).toEqual(null); }); it('should catch errors', () => { diff --git a/src/app/sessions/show.component.spec.ts b/src/app/sessions/show.component.spec.ts index 9bab02609..66a761f22 100644 --- a/src/app/sessions/show.component.spec.ts +++ b/src/app/sessions/show.component.spec.ts @@ -121,17 +121,14 @@ describe('AppShowComponent', () => { }); it('should update data on success', () => { - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(component.data).toEqual(returnedSession); - expect(spy).toHaveBeenCalledWith(returnedSession); + expect(component.data()).toEqual(returnedSession); }); it('should not update data if there is none', () => { mockSessionsGrpcService.get$.mockImplementationOnce(() => of({})); - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(spy).not.toHaveBeenCalled(); + expect(component.data()).toEqual(null); }); it('should catch errors', () => { @@ -185,7 +182,7 @@ describe('AppShowComponent', () => { component.lowerDate = taskCreatedAt.date; component.upperDate = taskEndedAt.date; component.computeDuration$.next(); - expect(component.data?.duration).toEqual({ + expect(component.data()?.duration).toEqual({ seconds: '1000', nanos: 0 }); @@ -249,7 +246,7 @@ describe('AppShowComponent', () => { }); it('should get resultKeys', () => { - expect(component.resultsKey).toEqual('0-root-1-0'); + expect(component.resultsKey()).toEqual('0-root-1-0'); }); @@ -372,4 +369,14 @@ describe('AppShowComponent', () => { expect(mockNotificationService.error).toHaveBeenCalled(); }); }); + + describe('on destroy', () => { + beforeEach(() => { + component.ngOnDestroy(); + }); + + it('should unsubscribe from subjects', () => { + expect(component.subscriptions.closed).toBeTruthy(); + }); + }); }); \ 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 bb441fd79..5cab0e46f 100644 --- a/src/app/tasks/show.component.spec.ts +++ b/src/app/tasks/show.component.spec.ts @@ -97,17 +97,13 @@ describe('AppShowComponent', () => { }); it('should update data on success', () => { - const spy = jest.spyOn(component.data$, 'next'); - component.refresh.next(); - expect(component.data).toEqual(returnedTask); - expect(spy).toHaveBeenCalledWith(returnedTask); + expect(component.data()).toEqual(returnedTask); }); - it('should not update data if there is none', () => { + it('should not update data if none is fetched', () => { mockTasksGrpcService.get$.mockImplementationOnce(() => of({})); - const spy = jest.spyOn(component.data$, 'next'); component.refresh.next(); - expect(spy).not.toHaveBeenCalled(); + expect(component.data()).toEqual(null); }); it('should catch errors', () => { @@ -176,7 +172,7 @@ describe('AppShowComponent', () => { }); it('should get resultKeys', () => { - expect(component.resultsKey).toEqual('1-root-3-0'); + expect(component.resultsKey()).toEqual('1-root-3-0'); }); describe('actions', () => { @@ -185,4 +181,14 @@ describe('AppShowComponent', () => { expect(mockTasksGrpcService.cancel$).toHaveBeenCalledWith([returnedTask.id]); }); }); + + describe('on destroy', () => { + beforeEach(() => { + component.ngOnDestroy(); + }); + + it('should unsubscribe from subjects', () => { + expect(component.subscriptions.closed).toBeTruthy(); + }); + }); }); \ No newline at end of file