diff --git a/projects/step-core/src/lib/client/augmented/services/augmented-time-series.service.ts b/projects/step-core/src/lib/client/augmented/services/augmented-time-series.service.ts index 050450142..5437bd252 100644 --- a/projects/step-core/src/lib/client/augmented/services/augmented-time-series.service.ts +++ b/projects/step-core/src/lib/client/augmented/services/augmented-time-series.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { FetchBucketsRequest, TimeSeriesService } from '../../generated'; -import { map, Observable, of, OperatorFunction, switchMap } from 'rxjs'; +import { delay, map, Observable, of, OperatorFunction, switchMap } from 'rxjs'; import { TableApiWrapperService } from '../../table'; import { HttpOverrideResponseInterceptor } from '../shared/http-override-response-interceptor'; import { HttpOverrideResponseInterceptorService } from './http-override-response-interceptor.service'; diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-analytics/alt-execution-analytics.component.html b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-analytics/alt-execution-analytics.component.html index 0daecfb26..81d4bbcf2 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-analytics/alt-execution-analytics.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-analytics/alt-execution-analytics.component.html @@ -3,7 +3,7 @@ active.timeRangeSelectionChange$.pipe(withLatestFrom(active.execution$))), + ) + .subscribe(([tr, execution]) => { + if (!tr || !execution) { + return; + } + const timeRange = convertPickerSelectionToTimeRange(tr, execution); + this.dashboardComponent()?.updateFullTimeRange(timeRange!, { actionType: 'manual' }); + }); + + readonly autoRefreshTriggered = this._activeExecutionContext.activeExecution$ + .pipe( + takeUntilDestroyed(), + switchMap((active) => + active.execution$.pipe(map((execution) => [execution, active.getTimeRangeSelection()] as const)), + ), + ) + .subscribe(([execution, timeRangeSelection]) => { + if (!timeRangeSelection || !execution) { + return; + } + const timeRange = convertPickerSelectionToTimeRange(timeRangeSelection, execution); + this.dashboardComponent()?.updateFullTimeRange(timeRange!, { actionType: 'auto' }); + }); + handleDashboardSettingsChange(context: TimeSeriesContext) { this._urlParamsService.updateUrlParamsFromContext(context, this.activeTimeRangeSelection()!, undefined, false); } diff --git a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors/alt-execution-errors.component.html b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors/alt-execution-errors.component.html index f737f9f2d..3e0612a2f 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors/alt-execution-errors.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/alt-execution-errors/alt-execution-errors.component.html @@ -7,7 +7,7 @@ matSortActive="count" matSortDirection="desc" [visibleColumns]="displayColumns()" - [indicatorMode]="TableIndicatorMode.SKELETON_ON_INITIAL_LOAD" + [indicatorMode]="TableIndicatorMode.SPINNER" > Error Message @@ -20,7 +20,9 @@ @if (!element?.executionIds?.length) { -
Current time range is too large to display details.
+
+ Current time range is too large to display details. +
} @else { (); - private _router: Router = inject(Router); private _destroyRef = inject(DestroyRef); private _changeDetectorRef = inject(ChangeDetectorRef); @@ -34,6 +38,22 @@ export class LegacyExecutionViewComponent implements OnInit { private _authService = inject(AuthService); private _urlParamsService = inject(DashboardUrlParamsService); + private dashboardComponent = viewChild('dashboardComponent', { read: ExecutionDashboardComponent }); + + execution = input.required(); + + executionChangeEffect = effect(() => { + // handle autorefresh + let execution = this.execution(); + if (!execution) return; + + untracked(() => { + let timeRangeSelection = this.activeTimeRangeSelection(); + const timeRange = convertPickerSelectionToTimeRange(timeRangeSelection!, execution); + this.dashboardComponent()?.updateFullTimeRange(timeRange!, { actionType: 'auto' }); + }); + }); + readonly timeRangeOptions: TimeRangePickerSelection[] = [ { type: 'FULL' }, ...TimeSeriesConfig.EXECUTION_PAGE_TIME_SELECTION_OPTIONS, @@ -48,14 +68,9 @@ export class LegacyExecutionViewComponent implements OnInit { timeRange: Signal = computed(() => { const pickerSelection = this.activeTimeRangeSelection(); - if (pickerSelection) { - const execution = this.execution(); - const end = execution.endTime || new Date().getTime(); - if (pickerSelection.type === 'FULL') { - return { from: execution.startTime!, to: end }; - } else { - return TimeSeriesUtils.convertSelectionToTimeRange(pickerSelection, end); - } + const execution = this.execution(); + if (pickerSelection && execution) { + return convertPickerSelectionToTimeRange(pickerSelection, this.execution()); } else { return undefined; } @@ -113,13 +128,10 @@ export class LegacyExecutionViewComponent implements OnInit { ); } - handleTimeRangeChange(pickerSelection: TimeRangePickerSelection) { + handleTimeRangeSelectionChange(pickerSelection: TimeRangePickerSelection) { this.activeTimeRangeSelection.set(pickerSelection); - } - - triggerRefresh() { - let rangeSelection = this.activeTimeRangeSelection()!; - this.activeTimeRangeSelection.set({ ...rangeSelection }); + let timeRange = convertPickerSelectionToTimeRange(pickerSelection, this.execution()); + this.dashboardComponent()?.updateFullTimeRange(timeRange!, { actionType: 'manual' }); } private subscribeToUrlNavigation() { diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard-state.ts b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard-state.ts index 7e819c9f1..75d887bbd 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard-state.ts +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard-state.ts @@ -10,6 +10,7 @@ import { Subject, switchMap, take, + tap, } from 'rxjs'; import { TimeRangePickerSelection } from '../../../../timeseries/modules/_common/types/time-selection/time-range-picker-selection'; import { @@ -49,6 +50,8 @@ interface EntityWithKeywordsStats { statuses: Record; } +type KeywordsChartState = { chartSettings: TSChartSettings; lastExecutions: Execution[] }; + export type CrossExecutionViewType = 'task' | 'plan'; export abstract class CrossExecutionDashboardState { @@ -57,11 +60,13 @@ export abstract class CrossExecutionDashboardState { protected _executionService = inject(ExecutionsService); protected _timeSeriesService = inject(AugmentedTimeSeriesService); protected _statusColors = inject(STATUS_COLORS); - private _uPlotUtils = inject(UPlotUtilsService); private readonly fetchLastExecutionTrigger$ = new Subject(); readonly task = signal(undefined); readonly plan = signal(undefined); + readonly lastRefreshTrigger = signal<'manual' | 'auto'>('manual'); + readonly onRefreshTriggered = new Subject(); + readonly onTimeSelectionChanged = new Subject(); // view settings activeTimeRangeSelection = signal(undefined); @@ -74,8 +79,19 @@ export abstract class CrossExecutionDashboardState { abstract readonly executionsTableFilter: Record; abstract getViewType(): CrossExecutionViewType; - updateTimeRangeSelection(selection: TimeRangePickerSelection) { + executionsChartLoading = signal(false); + summaryWidgetLoading = signal(false); + testCasesCountChartLoading = signal(false); + keywordsCountChartLoading = signal(false); + errorsTableLoading = signal(false); + successRateValueLoading = signal(false); + averageDurationValueLoading = signal(false); + totalExecutionsValueLoading = signal(false); + + public updateTimeRangeSelection(selection: TimeRangePickerSelection) { + this.lastRefreshTrigger.set('manual'); this.activeTimeRangeSelection.set(selection); + this.onTimeSelectionChanged.next(this.convertSelectionToTimeRange(selection)); } updateRefreshInterval(interval: number): void { @@ -86,18 +102,20 @@ export abstract class CrossExecutionDashboardState { filter((value): value is TimeRangePickerSelection => value != null), ); + public triggerRefresh() { + this.lastRefreshTrigger.set('auto'); + this.activeTimeRangeSelection.set({ ...this.activeTimeRangeSelection()! }); + this.fetchLastExecutionTrigger$.next(); + let timeRangeSelection = this.activeTimeRangeSelection(); + if (!timeRangeSelection) { + return; + } + const timeRange = this.convertSelectionToTimeRange(timeRangeSelection); + this.onRefreshTriggered.next(timeRange); + } + readonly timeRange$: Observable = this.timeRangeSelection$.pipe( - map((rangeSelection) => { - switch (rangeSelection.type) { - case 'FULL': - throw new Error('Full range is not supported'); - case 'ABSOLUTE': - return rangeSelection.absoluteSelection!; - case 'RELATIVE': - const endTime = new Date().getTime(); - return { from: endTime - rangeSelection.relativeSelection!.timeInMs, to: endTime }; - } - }), + map(this.convertSelectionToTimeRange), filter((range): range is TimeRange => range !== undefined), shareReplay(1), ) as Observable; @@ -109,10 +127,26 @@ export abstract class CrossExecutionDashboardState { shareReplay(1), ); + private convertSelectionToTimeRange(rangeSelection: TimeRangePickerSelection) { + switch (rangeSelection.type) { + case 'FULL': + throw new Error('Full range is not supported'); + case 'ABSOLUTE': + return rangeSelection.absoluteSelection!; + case 'RELATIVE': + const endTime = new Date().getTime(); + return { from: endTime - rangeSelection.relativeSelection!.timeInMs, to: endTime }; + } + } + // charts readonly executionsDurationTimeSeriesData = this.timeRange$.pipe( switchMap((timeRange) => { + this.summaryWidgetLoading.set(true); + this.successRateValueLoading.set(true); + this.totalExecutionsValueLoading.set(true); + this.averageDurationValueLoading.set(true); const oql = new OQLBuilder() .open('and') .append('attributes.metricType = "executions/duration"') @@ -138,12 +172,14 @@ export abstract class CrossExecutionDashboardState { items[keyAttributes['result'] as string] = bucket.count; total += bucket.count; }); + this.summaryWidgetLoading.set(false); return { items: items, total: total }; }), ); executionsChartSettings$ = this.timeRange$.pipe( switchMap((timeRange) => { + this.executionsChartLoading.set(true); const statusAttribute = 'result'; const oql = new OQLBuilder() .open('and') @@ -225,6 +261,7 @@ export abstract class CrossExecutionDashboardState { }, }, ]; + this.executionsChartLoading.set(false); return { title: '', showLegend: false, @@ -260,6 +297,10 @@ export abstract class CrossExecutionDashboardState { ); readonly lastExecutionsSorted$ = this.timeRange$.pipe( + tap(() => { + this.testCasesCountChartLoading.set(true); + this.keywordsCountChartLoading.set(true); + }), switchMap((timeRange) => this.fetchLastExecutions(timeRange)), map((executions) => { executions.sort((a, b) => a.startTime! - b.startTime!); @@ -268,7 +309,7 @@ export abstract class CrossExecutionDashboardState { shareReplay(1), ); - keywordsChartSettings$ = this.lastExecutionsSorted$.pipe( + keywordsChartSettings$: Observable = this.lastExecutionsSorted$.pipe( switchMap((executions) => { return this.timeRange$.pipe( take(1), @@ -276,6 +317,7 @@ export abstract class CrossExecutionDashboardState { const statusAttribute = 'status'; const executionIdAttribute = 'executionId'; if (executions.length === 0) { + this.keywordsCountChartLoading.set(false); return of(this.createKeywordsChart([], [])); } else { const executionsIdsJoined = @@ -338,7 +380,9 @@ export abstract class CrossExecutionDashboardState { return s; }); this.cumulateSeriesData(series); - return this.createKeywordsChart(executions, series); + let chartSettings = this.createKeywordsChart(executions, series); + this.keywordsCountChartLoading.set(false); + return chartSettings; }), ); } @@ -355,6 +399,7 @@ export abstract class CrossExecutionDashboardState { take(1), switchMap((timeRange) => { if (executions.length === 0) { + this.testCasesCountChartLoading.set(false); return of({ chart: this.createTestCasesChart([], []), hasData: false, lastExecutions: [] }); } else { const executionsIdsJoined = executions.map((e) => `attributes.executionId = ${e.id!}`).join(' or '); @@ -421,6 +466,7 @@ export abstract class CrossExecutionDashboardState { return s; }); this.cumulateSeriesData(series); + this.testCasesCountChartLoading.set(false); return { chart: this.createTestCasesChart(executions, series), lastExecutions: executions, @@ -440,7 +486,7 @@ export abstract class CrossExecutionDashboardState { let filterItem = this.getDashboardFilter(); // this is working only with searchEntities for now. extend it if needed const filter = { [filterItem.attributeName]: filterItem.searchEntities[0]?.searchValue }; - this.errorsDataSource.reload({ request: { timeRange: timeRange, ...filter } }); + this.errorsDataSource.reload({ request: { timeRange: timeRange, ...filter }, hideProgress: false }); }); private getDefaultBands(count: number, skipSeries = 0): Band[] { diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard.component.html b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard.component.html index 5c89c0ce9..db0e7d80a 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/cross-execution-dashboard.component.html @@ -50,13 +50,13 @@

@if (_state.activeTimeRangeSelection()) {
+ @if (isLoading() && _state.lastRefreshTrigger() !== 'auto') { +
+ +
+ } @if (defaultDateRange(); as dateRange) {
>(); readonly defaultDateRange = input(); + readonly isLoading = signal(false); + protected readonly dashletTitle = computed(() => { let heatmapType = this.heatmapType(); const lastExecutionLabel = ` (last ${this._state.LAST_EXECUTIONS_TO_DISPLAY} executions)`; @@ -114,6 +116,7 @@ export class CrossExecutionHeatmapComponent implements OnInit, OnDestroy { protected switchType(newType: HeatMapChartType) { this.heatmapType.set(newType); + this._state.lastRefreshTrigger.set('manual'); } readonly heatMapData$: Observable = combineLatest([ @@ -122,6 +125,7 @@ export class CrossExecutionHeatmapComponent implements OnInit, OnDestroy { this.heatmapType$, ]).pipe( switchMap(([executions, timeRange, heatmapType]) => { + this.isLoading.set(true); const isKeywordsHeatmap = heatmapType === 'keywords'; const executionIdAttribute = 'eId'; @@ -200,7 +204,7 @@ export class CrossExecutionHeatmapComponent implements OnInit, OnDestroy { }); }); }); - + this.isLoading.set(false); return { data: this.convertToTableData(executions, Object.values(itemsMap)), truncated: false }; }), ); diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/heatmap/heatmap.component.scss b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/heatmap/heatmap.component.scss index 923aa5cd9..bcbdf87db 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/heatmap/heatmap.component.scss +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/heatmap/heatmap.component.scss @@ -116,6 +116,5 @@ step-heatmap { .heatmap-col-header { font-weight: normal; top: 0; - color: var.$white; } } diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/performance/scheduler-performance-view.component.html b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/performance/scheduler-performance-view.component.html index fde129971..e5d7c3cbc 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/performance/scheduler-performance-view.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/performance/scheduler-performance-view.component.html @@ -1,8 +1,9 @@ @if (dashboardId) { @if (!isLoading) { (undefined); + + private dashboardComponent = viewChild('dashboardComponent', { read: DashboardComponent }); + isLoading = false; dashboardId?: string; @@ -35,6 +39,12 @@ export class SchedulerPerformanceViewComponent implements OnInit { ngOnInit(): void { this.subscribeToUrlNavigation(); + this._state.onRefreshTriggered.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((timeRange) => { + this.dashboardComponent()?.updateFullTimeRange(timeRange, { actionType: 'auto' }); + }); + this._state.onTimeSelectionChanged.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((timeRange) => { + this.dashboardComponent()?.updateFullTimeRange(timeRange, { actionType: 'manual' }); + }); } handleDashboardSettingsChange(context: TimeSeriesContext) { diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.html b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.html index 4f98cdc15..d16bde17f 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.html @@ -3,11 +3,13 @@
@if (successRateValue$ | async; as data) { - {{ data }} + @if (_state.successRateValueLoading() && displayLoadingOnProgress()) { + + } @else { + {{ data }} + } } @else { -
- -
+ }
@@ -17,11 +19,13 @@
@if (averageExecutionDurationLabel$ | async; as data) { - {{ data }} + @if (_state.averageDurationValueLoading() && displayLoadingOnProgress()) { + + } @else { + {{ data }} + } } @else { -
- -
+ }
@@ -31,13 +35,21 @@
@if ((totalExecutionsCount$ | async) === null) { -
- -
+ } @else { - {{ totalExecutionsCount$ | async | bigNumber }} + @if (_state.totalExecutionsValueLoading() && displayLoadingOnProgress()) { + + } @else { + {{ totalExecutionsCount$ | async | bigNumber }} + } }
+ + +
+ +
+
diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.scss b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.scss index 1b50d16d2..ee3fd7d49 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.scss +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.scss @@ -13,6 +13,6 @@ .card-value { font-weight: bold; font-size: 2rem; - margin-bottom: 2rem; + height: 3.6rem; } } diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.ts b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.ts index b310ba22e..1246c0485 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.ts +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/header/report-view-header.component.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, OnInit } from '@angular/core'; import { CrossExecutionDashboardState } from '../../cross-execution-dashboard-state'; -import { map, Observable, of, shareReplay, switchMap } from 'rxjs'; +import { finalize, map, Observable, of, pipe, shareReplay, switchMap, tap } from 'rxjs'; import { ReportNodeSummary } from '../../../../../shared/report-node-summary'; import { FilterUtils, OQLBuilder, TimeSeriesConfig } from '../../../../../../timeseries/modules/_common'; import { BucketResponse } from '@exense/step-core'; @@ -14,6 +14,10 @@ import { BucketResponse } from '@exense/step-core'; export class ReportViewHeaderComponent { readonly _state = inject(CrossExecutionDashboardState); + displayLoadingOnProgress = computed(() => { + return this._state.lastRefreshTrigger() === 'manual'; + }); + successRateValue$: Observable = this._state.summaryData$.pipe( map((summaryData: ReportNodeSummary) => { const passed = summaryData.items['PASSED'] || 0; @@ -22,6 +26,9 @@ export class ReportViewHeaderComponent { } return ((passed / summaryData.total) * 100).toFixed(2) + '%'; }), + tap(() => { + this._state.successRateValueLoading.set(false); + }), shareReplay(1), ); @@ -41,6 +48,9 @@ export class ReportViewHeaderComponent { return TimeSeriesConfig.AXES_FORMATTING_FUNCTIONS.time(totalDuration / totalCount); } }), + tap(() => { + this._state.averageDurationValueLoading.set(false); + }), shareReplay(1), ); @@ -53,6 +63,9 @@ export class ReportViewHeaderComponent { }); return totalCount; }), + tap(() => { + this._state.totalExecutionsValueLoading.set(false); + }), shareReplay(1), ); } diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.html b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.html index a105f558e..38fafca30 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.html @@ -3,19 +3,19 @@
@if (_state.summaryData$ | async; as summary) { + @if (_state.summaryWidgetLoading()) { + + } - } @else { - - - - - }
@if (_state.executionsChartSettings$ | async; as executionsChartSettings) { + @if (_state.executionsChartLoading()) { + + }
- +
@if (reportNodesChartType() === 'keywords') { + @if (_state.keywordsCountChartLoading()) { + + } @if (_state.keywordsChartSettings$ | async; as settings) { @@ -65,7 +68,13 @@ } } @else if (reportNodesChartType() === 'testcases') { + @if (_state.testCasesCountChartLoading()) { + + } @if (_state.testCasesChartSettings$ | async; as testCasesData) { + @if (_state.testCasesCountChartLoading()) { + + }
@@ -90,6 +99,9 @@
+ @if (_state.errorsTableLoading()) { + + } -
- -
+ @if (_state.lastRefreshTrigger() !== 'auto') { +
+ +
+ }
diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.scss b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.scss index 0b6212f9c..a128aecde 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.scss +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.scss @@ -4,6 +4,16 @@ padding-top: 0; } +.spinner-container { + display: flex; + justify-content: center; + position: absolute; + width: 100%; + height: 100%; + align-items: center; + z-index: 100; +} + .header { display: flex; align-items: center; @@ -49,6 +59,7 @@ .piechart-container { width: 44rem; min-width: 44rem; + position: relative; } .row { @@ -121,12 +132,3 @@ .wide-menu { max-width: 400px; } - -.spinner-container { - display: flex; - width: 100%; - height: 100%; - flex-grow: 1; - align-items: center; - justify-content: center; -} diff --git a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.ts b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.ts index 9948c0a8a..50d8339a0 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.ts +++ b/projects/step-frontend/src/lib/modules/execution/components/schedule-overview/cross-execution-dashboard/report/scheduler-report-view.component.ts @@ -52,10 +52,16 @@ export class SchedulerReportViewComponent implements OnInit { ); switchReportNodesChart(type: ReportNodesChartType) { + this._state.lastRefreshTrigger.set('manual'); + if (type === 'keywords') { + this._state.keywordsCountChartLoading.set(true); + } else { + this._state.testCasesCountChartLoading.set(true); + } this.reportNodesChartType.set(type); } - readonly byExecutionChartTitle = computed(() => { + readonly countChartTitle = computed(() => { const label = this.reportNodesChartType() === 'keywords' ? 'Keyword calls count' : 'Test cases count'; return `${label} (last ${this._state.LAST_EXECUTIONS_TO_DISPLAY} executions)`; }); @@ -103,6 +109,7 @@ export class SchedulerReportViewComponent implements OnInit { } handleMainChartZoom(timeRange: TimeRange) { + this._state.lastRefreshTrigger.set('manual'); this._state.executionsChartSettings$.pipe(take(1)).subscribe((chartSettings) => { const base = chartSettings.xAxesSettings.values[0]; const interval = chartSettings.xAxesSettings.values[1] - chartSettings.xAxesSettings.values[0]; diff --git a/projects/step-frontend/src/lib/modules/execution/services/execution-commands.service.ts b/projects/step-frontend/src/lib/modules/execution/services/execution-commands.service.ts index 7df8268ff..92cc51bac 100644 --- a/projects/step-frontend/src/lib/modules/execution/services/execution-commands.service.ts +++ b/projects/step-frontend/src/lib/modules/execution/services/execution-commands.service.ts @@ -80,7 +80,6 @@ export class ExecutionCommandsService implements OnDestroy { const executionsParameters$ = this.buildExecutionParams(false, false); return executionsParameters$.pipe( map((executionsParameters) => { - console.log('EXECUTION PARAMS', executionsParameters); const name = executionsParameters.description ?? ''; return { attributes: { name }, diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/chart-dashlet/chart-dashlet.component.html b/projects/step-frontend/src/lib/modules/timeseries/components/chart-dashlet/chart-dashlet.component.html index 5039ad034..5499f79ef 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/chart-dashlet/chart-dashlet.component.html +++ b/projects/step-frontend/src/lib/modules/timeseries/components/chart-dashlet/chart-dashlet.component.html @@ -2,6 +2,11 @@ @if (!_internalSettings()) { } @else { + @if (isLoading() && showLoadingSpinnerWhileLoading()) { +
+ +
+ } = { TooltipContentDirective, ChartStandardTooltipComponent, ], + standalone: true, }) export class ChartDashletComponent extends ChartDashlet implements OnInit { private readonly stepped = uPlot.paths.stepped; // this is a function from uplot wich allows to draw 'stepped' or 'stairs like' lines @@ -111,12 +113,15 @@ export class ChartDashletComponent extends ChartDashlet implements OnInit { readonly height = input.required(); readonly editMode = input(false); readonly showExecutionLinks = input(false); + readonly showLoadingSpinnerWhileLoading = input(true); readonly remove = output(); readonly shiftLeft = output(); readonly shiftRight = output(); readonly zoomReset = output(); + readonly isLoading = signal(false); + groupingSelection: MetricAttributeSelection[] = []; selectedAggregate!: ChartAggregation; selectedAggregatePcl?: number; @@ -133,6 +138,7 @@ export class ChartDashletComponent extends ChartDashlet implements OnInit { firstEffectTriggered = false; + readonly itemChangeEffect = effect(() => { const item = this.item(); if (this.firstEffectTriggered) { @@ -479,6 +485,7 @@ export class ChartDashletComponent extends ChartDashlet implements OnInit { truncated: response.truncated, }; }), + tap(() => this.isLoading.set(false)), ); } @@ -541,6 +548,7 @@ export class ChartDashletComponent extends ChartDashlet implements OnInit { } private fetchDataAndCreateChartSettings(): Observable { + this.isLoading.set(true); const groupDimensions = this.getGroupDimensions(); const oqlFilter = this.composeRequestFilter(); this.requestOql = oqlFilter; diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard-page/dashboard-page.component.html b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard-page/dashboard-page.component.html index 48133a6d8..51ac93086 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard-page/dashboard-page.component.html +++ b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard-page/dashboard-page.component.html @@ -41,8 +41,9 @@ @if (timeRange() && dashboard()?.id && !isLoading()) { = signal(undefined); @@ -112,7 +114,7 @@ export class DashboardPageComponent implements OnInit { } handleDashboardUpdate(dashboard: DashboardView) { - // this will make sure there are no conflicts between the dashboard entity shared across this page and actual dashboard component + // the dashboard is editable. this will make sure there are no conflicts between the dashboard entity shared across this page and actual dashboard component const mergedDashboard: DashboardView = { ...dashboard, attributes: this.dashboard!()!.attributes, @@ -139,10 +141,6 @@ export class DashboardPageComponent implements OnInit { ); } - handleTimeRangeChange(pickerSelection: TimeRangePickerSelection) { - this.activeTimeRangeSelection.set(pickerSelection); - } - handleDashboardSettingsChange(context: TimeSeriesContext) { this._urlParamsService.updateUrlParamsFromContext( context, @@ -160,9 +158,16 @@ export class DashboardPageComponent implements OnInit { ); } + handleTimeRangeChange(pickerSelection: TimeRangePickerSelection) { + this.activeTimeRangeSelection.set(pickerSelection); + let timeRange = TimeSeriesUtils.convertSelectionToTimeRange(pickerSelection); + this.dashboardComponent()?.updateFullTimeRange(timeRange, { actionType: 'manual' }); + } + triggerRefresh() { - let rangeSelection = this.activeTimeRangeSelection()!; - this.activeTimeRangeSelection.set({ ...rangeSelection }); + let pickerSelection = this.activeTimeRangeSelection()!; + let timeRange = TimeSeriesUtils.convertSelectionToTimeRange(pickerSelection); + this.dashboardComponent()?.updateFullTimeRange(timeRange, { actionType: 'auto' }); } private subscribeToUrlNavigation() { diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state-engine.ts b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state-engine.ts index 70552e534..54619218c 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state-engine.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state-engine.ts @@ -52,6 +52,7 @@ export class DashboardStateEngine { fullRange: timeRange, selectedRange: timeRange, }; + this.state.lastChangeType = 'manual'; this.state.context.updateTimeRangeSettings(timeRangeSettings); } diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state.ts b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state.ts index 3300b1667..5dd74f188 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard-state.ts @@ -9,6 +9,7 @@ export interface DashboardState { getDashlets: () => QueryList; getFilterBar: () => DashboardFilterBarComponent; getRanger: () => PerformanceViewTimeSelectionComponent; + lastChangeType: 'auto' | 'manual'; refreshInProgress: boolean; refreshSubscription?: Subscription; } diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard.component.html b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard.component.html index 82eec6b74..3127cd938 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard.component.html +++ b/projects/step-frontend/src/lib/modules/timeseries/components/dashboard/dashboard.component.html @@ -29,7 +29,11 @@
- +
@if (compareModeEnabled) {
} @else {
@if (!compareModeEnabled) { @@ -84,8 +90,10 @@ #compareFilterBar [editMode]="editMode" [context]="compareEngine!.state.context" - [compactView]="compareModeEnabled" - (fullRangeChange)="handleCompareFullRangeChange($event)" + [compactView]="true" + (fullRangeRequestChange)="handleCompareFullRangeChange($event)" + (groupingChange)="handleGroupingChange(compareEngine!, $event)" + (filtersChange)="handleFiltersChange(compareEngine!, $event)" />
} @@ -101,6 +109,7 @@ #chart [editMode]="editMode" [showExecutionLinks]="showExecutionLinks" + [showLoadingSpinnerWhileLoading]="mainEngine.state.lastChangeType === 'manual'" [item]="dashlet" [context]="mainEngine.state.context" [height]="DASHLET_HEIGHT" @@ -115,6 +124,7 @@
Compare
(); + initialTimeRange = input.required(); timeRangeOptions = TimeSeriesConfig.ANALYTICS_TIME_SELECTION_OPTIONS; fullRangeSelected: boolean = true; - timeRangeChangeEffect = effect(() => { - const timeRange = this.timeRange()!; - this.mainEngine?.state.context.updateFullTimeRange(timeRange); - // this.compareEngine?.state.context.updateFullTimeRange(timeRange); - this.refresh(); - }); - /** @Output **/ readonly contextSettingsChanged = output(); // used to detect any change, useful for url updates readonly contextSettingsInit = output(); // emit only first time when the context is created @@ -141,6 +134,19 @@ export class DashboardComponent implements OnInit, OnDestroy { mainEngine!: DashboardStateEngine; compareEngine?: DashboardStateEngine; + compareModeChangesSubscription?: Subscription; + + public updateFullTimeRange( + timeRange: TimeRange, + opts: { actionType: 'manual' | 'auto'; resetSelection?: boolean }, + ): void { + this.mainEngine.state.lastChangeType = opts.actionType; + this.mainEngine?.state.context.updateFullTimeRange(timeRange, opts.resetSelection); + } + + public getSelectedTimeRange(): TimeRange { + return this.mainEngine.state.context.timeRangeSettings.selectedRange; + } ngOnInit(): void { const dashboardId = this.id(); @@ -156,21 +162,22 @@ export class DashboardComponent implements OnInit, OnDestroy { }); } - updateTimeRangeFromSelection() { + /** + * This method is used to notify the parent component that the user wants to change the full time-range, to his current sub-selection + * @protected + */ + protected emitFullRangeUpdateRequest() { if (!this.fullRangeSelected) { + this.mainEngine.state.lastChangeType = 'manual'; this.fullRangeUpdateRequest.emit(this.mainEngine.state.context.getSelectedTimeRange()); } } - public getSelectedTimeRange(): TimeRange { - return this.mainEngine.state.context.timeRangeSettings.selectedRange; - } - - handleMainFullRangeChange(range: TimeRange) { + protected handleMainFullRangeChangeRequest(range: TimeRange) { this.fullRangeUpdateRequest.emit(range); } - handleCompareFullRangeChange(range: TimeRange) { + protected handleCompareFullRangeChange(range: TimeRange) { this.compareEngine?.state.context.updateFullRangeAndSelection(range); } @@ -180,7 +187,7 @@ export class DashboardComponent implements OnInit, OnDestroy { * 2. Stored state * 3. Dashboard object */ - initState(urlParams: DashboardUrlParams, dashboard: DashboardView): Observable { + private initState(urlParams: DashboardUrlParams, dashboard: DashboardView): Observable { this.dashboard = dashboard; const existingContext = this.storageId ? this._timeSeriesContextFactory.getContext(this.storageId) : undefined; const context$: Observable = existingContext @@ -201,7 +208,17 @@ export class DashboardComponent implements OnInit, OnDestroy { ); } - handleZoomReset() { + protected handleGroupingChange(engine: DashboardStateEngine, groupDimensions: string[]) { + engine.state.lastChangeType = 'manual'; + engine.state.context.updateGrouping(groupDimensions); + } + + protected handleFiltersChange(engine: DashboardStateEngine, filters: TsFilteringSettings) { + engine.state.lastChangeType = 'manual'; + engine.state.context.setFilteringSettings(filters); + } + + protected handleZoomReset() { this.zoomReset.emit(); this.mainEngine.state.context.resetZoom(); } @@ -214,6 +231,7 @@ export class DashboardComponent implements OnInit, OnDestroy { getDashlets: () => this.dashlets, getRanger: () => this.timeRanger!, refreshInProgress: false, + lastChangeType: 'auto', }; this.mainEngine = new DashboardStateEngine(state); this.mainEngine.subscribeForContextChange(); @@ -223,36 +241,32 @@ export class DashboardComponent implements OnInit, OnDestroy { } } - public refresh() { - if (!this.compareModeEnabled && this.mainEngine) { - this.mainEngine.triggerRefresh(false); - this.compareEngine?.triggerRefresh(false); - } - } - - handleResolutionChange(resolution: number) { + protected handleResolutionChange(resolution: number) { if (resolution > 0 && resolution < 1000) { // minimum value should be one second return; } + this.mainEngine.state.lastChangeType = 'manual'; this.mainEngine.state.context.updateChartsResolution(resolution); - this.compareEngine?.state.context.updateChartsResolution(resolution); + if (this.compareEngine) { + this.compareEngine.state.lastChangeType = 'manual'; + this.compareEngine.state.context.updateChartsResolution(resolution); + } } - enableEditMode() { + protected enableEditMode() { this.dashboardBackup = JSON.parse(JSON.stringify(this.dashboard)); this.editMode = true; } - cancelEditMode() { + protected cancelEditMode() { this.dashboard = { ...this.dashboardBackup }; this.editMode = false; } - saveEditChanges() { + protected saveEditChanges() { this.editMode = false; this.dashboard.grouping = this.mainEngine.state.context.getGroupDimensions(); - // this.dashboard.timeRange = this.mainEngine.state.context.getTimeRangeSettings().pickerSelection; this.dashboard.resolution = this.resolution; this.dashboard.filters = this.filterBar?._internalFilters.map((item) => { @@ -262,11 +276,9 @@ export class DashboardComponent implements OnInit, OnDestroy { }) || []; this.dashboardUpdate.emit(this.dashboard); - // this._dashboardService.saveDashboard(this.dashboard).subscribe((response) => {}); - // this.mainEngine.refreshAllCharts(false, true); } - addTableDashlet(metric: MetricType) { + protected addTableDashlet(metric: MetricType) { let tableItem: DashboardItem = { id: 'table-' + new Date().getTime(), type: 'TABLE', @@ -306,7 +318,7 @@ export class DashboardComponent implements OnInit, OnDestroy { ]; } - addChartDashlet(metric: MetricType) { + protected addChartDashlet(metric: MetricType) { const newDashlet: DashboardItem = { id: 'chart-' + new Date().getTime(), name: metric.displayName, @@ -348,13 +360,13 @@ export class DashboardComponent implements OnInit, OnDestroy { return existingSettings!; } else { return { - fullRange: this.timeRange()!, - selectedRange: urlParams.selectedTimeRange || this.timeRange()!, + fullRange: this.initialTimeRange()!, + selectedRange: urlParams.selectedTimeRange || this.initialTimeRange()!, }; } } - createContext( + private createContext( dashboard: DashboardView, urlParams: DashboardUrlParams, existingContext?: TimeSeriesContext, @@ -503,7 +515,7 @@ export class DashboardComponent implements OnInit, OnDestroy { ); } - handleChartDelete(index: number) { + protected handleChartDelete(index: number) { const itemToDelete = this.dashboard.dashlets[index]; this.dashboard.dashlets.splice(index, 1); this.mainEngine.state.context.updateAttributes(this.collectAllAttributes()); @@ -515,7 +527,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.mainEngine.state.context.updateDashlets(this.dashboard.dashlets); } - handleChartShiftLeft(index: number) { + protected handleChartShiftLeft(index: number) { const listLength = this.dashboard.dashlets.length; let swapIndex = index - 1; if (index === 0) { @@ -528,7 +540,7 @@ export class DashboardComponent implements OnInit, OnDestroy { ]; } - handleChartShiftRight(index: number) { + protected handleChartShiftRight(index: number) { const listLength = this.dashboard.dashlets.length; let swapIndex = index + 1; if (index === listLength - 1) { @@ -541,7 +553,7 @@ export class DashboardComponent implements OnInit, OnDestroy { ]; } - toggleCompareMode() { + protected toggleCompareMode() { this.compareModeEnabled = !this.compareModeEnabled; if (this.compareModeEnabled) { this.enableCompareMode(); @@ -550,7 +562,11 @@ export class DashboardComponent implements OnInit, OnDestroy { } } - disableCompareMode() { + private disableCompareMode() { + if (this.compareModeChangesSubscription) { + this.compareModeChangesSubscription.unsubscribe(); + this.compareModeChangesSubscription = undefined; + } this.mainEngine.state.context.disableCompareMode(); this.dashlets.forEach((d) => { if (d.getType() === 'TABLE') { @@ -579,8 +595,13 @@ export class DashboardComponent implements OnInit, OnDestroy { return clonedSettings; } - enableCompareMode() { + private enableCompareMode() { + if (this.compareModeChangesSubscription) { + this.compareModeChangesSubscription.unsubscribe(); + this.compareModeChangesSubscription = undefined; + } const mainState = this.mainEngine.state; + mainState.lastChangeType = 'manual'; const mainTimeSettings = mainState.context.getTimeRangeSettings(); const compareModeContext = this._timeSeriesContextFactory.createContext({ dashlets: JSON.parse(JSON.stringify(this.dashboard.dashlets)), // clone @@ -604,14 +625,18 @@ export class DashboardComponent implements OnInit, OnDestroy { getFilterBar: () => this.compareFilterBar!, getRanger: () => this.compareTimeRanger!, refreshInProgress: false, + lastChangeType: 'auto', }; - compareModeContext.settingsChange$.subscribe(() => { + + this.compareModeChangesSubscription = compareModeContext.settingsChange$.subscribe((x) => { + console.log('settings have changed', x); this.dashlets.forEach((d) => { if (d.getType() === 'TABLE') { (d as TableDashletComponent).refreshCompareData().subscribe(); } }); }); + this.compareEngine = new DashboardStateEngine(state); this.compareEngine.subscribeForContextChange(); mainState.context.enableCompareMode(compareModeContext); @@ -622,11 +647,11 @@ export class DashboardComponent implements OnInit, OnDestroy { }); } - collectAllAttributes(): MetricAttribute[] { + private collectAllAttributes(): MetricAttribute[] { return this.dashboard.dashlets.flatMap((d) => d.attributes); } - removeOneTimeUrlParams() { + private removeOneTimeUrlParams() { const currentParams = { ...this._route.snapshot.queryParams }; currentParams[TimeSeriesConfig.DASHBOARD_URL_PARAMS_PREFIX + 'edit'] = null; @@ -638,7 +663,7 @@ export class DashboardComponent implements OnInit, OnDestroy { }); } - resetDashboard() { + protected resetDashboard() { this._timeSeriesContextFactory.destroyContext(this.storageId); this.dashboard = undefined as any; this.dashboardBackup = undefined as any; @@ -654,7 +679,7 @@ export class DashboardComponent implements OnInit, OnDestroy { }); } - exportRawData(): void { + protected exportRawData(): void { if (this.exportInProgress || !this.mainEngine.state.context) { return; } @@ -668,6 +693,7 @@ export class DashboardComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.mainEngine?.destroy(); this.compareEngine?.destroy(); + this.compareModeChangesSubscription?.unsubscribe(); if (!this.storageId) { this.mainEngine?.state?.context?.destroy?.(); this.compareEngine?.state?.context?.destroy?.(); diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/execution-page/execution-dashboard.component.html b/projects/step-frontend/src/lib/modules/timeseries/components/execution-page/execution-dashboard.component.html index 15082997a..8b556bac9 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/execution-page/execution-dashboard.component.html +++ b/projects/step-frontend/src/lib/modules/timeseries/components/execution-page/execution-dashboard.component.html @@ -1,7 +1,8 @@ -@if (isInitialized && timeRange()) { +@if (isInitialized && initialTimeRange()) { (); - timeRange = input.required(); + initialTimeRange = input.required(); readonly fullRangeUpdateRequest = output(); readonly contextSettingsChanged = output(); // used to detect any change, useful for url updates readonly contextSettingsInit = output(); // emit only first time when the context is created - @ViewChild(DashboardComponent) dashboard!: DashboardComponent; + private dashboardComponent = viewChild('dashboardComponent', { read: DashboardComponent }); private _authService = inject(AuthService); protected _executionViewModeService = inject(ExecutionViewModeService); @@ -64,6 +65,13 @@ export class ExecutionDashboardComponent implements OnInit, OnChanges { private _destroyRef = inject(DestroyRef); + public updateFullTimeRange( + timeRange: TimeRange, + opts: { actionType: 'manual' | 'auto'; resetSelection?: boolean }, + ): void { + this.dashboardComponent()?.updateFullTimeRange(timeRange, opts); + } + ngOnInit(): void { if (!this.execution) { throw new Error('Execution input is mandatory'); @@ -96,19 +104,20 @@ export class ExecutionDashboardComponent implements OnInit, OnChanges { } public getSelectedTimeRange() { - return this.dashboard.getSelectedTimeRange(); + return this.dashboardComponent()!.getSelectedTimeRange(); } - getExecutionRange(execution: Execution): Partial { - return { from: execution.startTime, to: execution.endTime }; + getExecutionRange(execution: Execution): TimeRange { + return { from: execution.startTime!, to: execution.endTime || new Date().getTime() }; } ngOnChanges(changes: SimpleChanges): void { + // we do the refresh based on execution change const executionChange = changes['execution']; if (executionChange?.currentValue !== executionChange?.previousValue && !executionChange?.firstChange) { this.executionMode = this._executionViewModeService.getExecutionMode(this.execution()); - this.executionRange = this.getExecutionRange(this.execution()); - this.dashboard?.refresh(); + // const timeRange = this.getExecutionRange(this.execution()); + // this.dashboardComponent()?.updateFullTimeRange(timeRange, { actionType: 'auto' }); } } diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.html b/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.html index cda269398..3be7a6634 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.html +++ b/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.html @@ -95,13 +95,12 @@
- + @if (isLoading() && showLoadingSpinnerWhileLoading()) { +
+ +
+ } +
diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.scss b/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.scss index 1c502f9ff..3376cd584 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.scss +++ b/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.scss @@ -2,6 +2,16 @@ @use 'projects/step-core/styles/core-mixins' as core; step-table-dashlet { + .spinner-container { + display: flex; + justify-content: center; + position: absolute; + width: 100%; + height: 100%; + align-items: center; + z-index: 100; + } + .header-container { display: flex; justify-content: space-between; diff --git a/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.ts b/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.ts index 7ffbd04cf..e8d6d7470 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/components/table-dashlet/table-dashlet.component.ts @@ -1,12 +1,15 @@ import { ChangeDetectorRef, Component, + effect, EventEmitter, inject, + input, Input, OnChanges, OnInit, Output, + signal, SimpleChanges, ViewEncapsulation, } from '@angular/core'; @@ -24,7 +27,7 @@ import { } from '@exense/step-core'; import { TsComparePercentagePipe } from './ts-compare-percentage.pipe'; import { TableColumnType } from '../../modules/_common/types/table-column-type'; -import { BehaviorSubject, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'; +import { BehaviorSubject, finalize, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'; import { ChartDashlet } from '../../modules/_common/types/chart-dashlet'; import { MatDialog } from '@angular/material/dialog'; import { TableDashletSettingsComponent } from '../table-dashlet-settings/table-dashlet-settings.component'; @@ -84,6 +87,7 @@ interface ProcessedBucketResponse { styleUrls: ['./table-dashlet.component.scss'], encapsulation: ViewEncapsulation.None, imports: [COMMON_IMPORTS, TsComparePercentagePipe, TableEntryFormatPipe, MatTooltip], + standalone: true, }) export class TableDashletComponent extends ChartDashlet implements OnInit, OnChanges { readonly COMPARE_COLUMN_ID_SUFFIX = '_comp'; @@ -92,11 +96,14 @@ export class TableDashletComponent extends ChartDashlet implements OnInit, OnCha @Input() item!: DashboardItem; @Input() context!: TimeSeriesContext; @Input() editMode = false; + showLoadingSpinnerWhileLoading = input(false); @Output() remove = new EventEmitter(); @Output() shiftLeft = new EventEmitter(); @Output() shiftRight = new EventEmitter(); + isLoading = signal(true); + private _timeSeriesService = inject(TimeSeriesService); private _matDialog = inject(MatDialog); private _timeSeriesEntityService = inject(TimeSeriesEntityService); @@ -104,7 +111,6 @@ export class TableDashletComponent extends ChartDashlet implements OnInit, OnCha tableData$ = new BehaviorSubject([]); tableDataSource: TableLocalDataSource | undefined; - tableIsLoading = true; columnsDefinition: TableColumn[] = []; visibleColumnsIds: string[] = ['name']; @@ -128,20 +134,32 @@ export class TableDashletComponent extends ChartDashlet implements OnInit, OnCha } this.prepareState(); this.tableDataSource = new TableLocalDataSource(this.tableData$, this.getDatasourceConfig()); - this.fetchBaseData().subscribe(() => this.updateTableData()); + this.fetchBaseData() + .pipe( + switchMap(() => this.updateTableData()), + finalize(() => this.isLoading.set(false)), + ) + .subscribe(); } - refresh(blur?: boolean): Observable { - return this.fetchBaseData().pipe(tap(() => this.updateTableData())); + public refresh(blur?: boolean): Observable { + console.log('refreshing'); + this.isLoading.set(true); + return this.fetchBaseData().pipe( + switchMap(() => this.updateTableData()), + finalize(() => this.isLoading.set(false)), + ); } - refreshCompareData(): Observable { + public refreshCompareData(): Observable { + this.isLoading.set(true); return this.fetchData(true).pipe( tap((response) => { this.compareBuckets = response.buckets; this.truncated = response.truncated; - this.updateTableData(); }), + switchMap(() => this.updateTableData()), + finalize(() => this.isLoading.set(false)), ); } @@ -213,12 +231,16 @@ export class TableDashletComponent extends ChartDashlet implements OnInit, OnCha } enableCompareMode(context: TimeSeriesContext) { + console.log('enabling compare mode in table dashlet'); this.compareModeEnabled = true; this.compareContext = context; this.compareBuckets = this.baseBuckets; this.compareRequestOql = this.baseRequestOql; this.updateVisibleColumns(); - this.updateTableData(); + this.isLoading.set(true); + this.updateTableData() + .pipe(finalize(() => this.isLoading.set(false))) + .subscribe(); } disableCompareMode() { @@ -226,7 +248,10 @@ export class TableDashletComponent extends ChartDashlet implements OnInit, OnCha this.compareContext = undefined; this.compareBuckets = []; this.updateVisibleColumns(); - this.updateTableData(); + this.isLoading.set(true); + this.updateTableData() + .pipe(finalize(() => this.isLoading.set(false))) + .subscribe(); } updateVisibleColumns(): void { @@ -372,10 +397,11 @@ export class TableDashletComponent extends ChartDashlet implements OnInit, OnCha private updateTableData() { const tableEntries = this.mergeBaseAndCompareData(); - this.fetchLegendEntities(tableEntries).subscribe((updatedData) => { - this.tableData$.next(updatedData); - this.tableIsLoading = false; - }); + return this.fetchLegendEntities(tableEntries).pipe( + tap((updatedData) => { + this.tableData$.next(updatedData); + }), + ); } private processResponse(response: TimeSeriesAPIResponse, context: TimeSeriesContext): ProcessedBucketResponse { diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/grouping/ts-grouping.component.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/grouping/ts-grouping.component.ts index 8c6ad0c5b..7bd112450 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/grouping/ts-grouping.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/grouping/ts-grouping.component.ts @@ -9,6 +9,7 @@ const EMPTY_DIMENSIONS_LABEL = 'Empty'; templateUrl: './ts-grouping.component.html', styleUrls: ['./ts-grouping.component.scss'], imports: [COMMON_IMPORTS], + standalone: true, }) export class TsGroupingComponent implements OnInit, OnChanges { readonly NO_GROUPING_OPTION = { label: 'Empty', attributes: [] }; diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.scss b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.scss index 940edb71b..6a4fae23d 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.scss +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.scss @@ -150,6 +150,7 @@ &.compact-btn { height: 40px; + min-width: unset; padding: 0 4px; width: 40px; display: flex; diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.ts index f0096ff8d..66362e6dc 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/components/time-range-picker/time-range-picker.component.ts @@ -28,6 +28,7 @@ import { TIME_UNIT_DICTIONARY, TimeConvertersFactoryService, TimeRange, TimeUnit templateUrl: './time-range-picker.component.html', styleUrls: ['./time-range-picker.component.scss'], imports: [COMMON_IMPORTS], + standalone: true, }) export class TimeRangePickerComponent implements OnInit { timeUnitOptions = [TimeUnit.MINUTE, TimeUnit.HOUR, TimeUnit.DAY, TimeUnit.WEEK, TimeUnit.MONTH, TimeUnit.YEAR]; diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/types/time-series/time-series-context.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/types/time-series/time-series-context.ts index 3bde3119d..15b91917a 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/_common/types/time-series/time-series-context.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/_common/types/time-series/time-series-context.ts @@ -75,7 +75,6 @@ export class TimeSeriesContext { this.chartsResolution$ = new BehaviorSubject(params.resolution || 0); params.metrics?.forEach((m) => (this.indexedMetrics[m.name] = m)); - // any specific context change will trigger the main stateChange this.settingsChange$ = merge( this.compareModeChange$.pipe(skip(1)), this.inProgress$.pipe(skip(1)), // TODO @@ -110,9 +109,9 @@ export class TimeSeriesContext { * If there is a selection before changing the full range, and it fits inside it, the selection will not be reset. Otherwise it does. * @param range */ - updateFullTimeRange(range: TimeRange) { + updateFullTimeRange(range: TimeRange, resetSelection?: boolean) { range = TimeSeriesUtils.removeFloatingDigits(range); - const isFullRangeSelected = this.isFullRangeSelected(); + const isFullRangeSelected = this.isFullRangeSelected() || resetSelection; const previousSelection = this.timeRangeSettings.selectedRange; if (isFullRangeSelected || !TimeSeriesUtils.intervalIsInside(range, previousSelection)) { // reset it diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/chart/components/time-series-chart/time-series-chart.component.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/chart/components/time-series-chart/time-series-chart.component.ts index 09e6afe99..268b58d0d 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/chart/components/time-series-chart/time-series-chart.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/chart/components/time-series-chart/time-series-chart.component.ts @@ -75,8 +75,16 @@ export class TimeSeriesChartComponent implements OnInit, OnChanges, OnDestroy, T @Output() lockStateChange = new EventEmitter(); lockState = signal(false); // the state does not change when unlocking from a synced chart + private lockEffectFirstRun = true; + lockEffect = effect(() => { - let locked = this.lockState(); + const locked = this.lockState(); + + if (this.lockEffectFirstRun) { + this.lockEffectFirstRun = false; + return; // skip init + } + this.lockStateChange.emit(locked); }); diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/dashboard-filter-bar/dashboard-filter-bar.component.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/dashboard-filter-bar/dashboard-filter-bar.component.ts index 90e46d478..ee5a7593d 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/dashboard-filter-bar/dashboard-filter-bar.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/dashboard-filter-bar/dashboard-filter-bar.component.ts @@ -60,6 +60,7 @@ const ATTRIBUTES_REMOVAL_FUNCTION = (field: string) => { styleUrls: ['./dashboard-filter-bar.component.scss'], encapsulation: ViewEncapsulation.None, imports: [COMMON_IMPORTS, TsGroupingComponent, FilterBarItemComponent, MatDivider], + standalone: true, }) export class DashboardFilterBarComponent implements OnInit, OnDestroy { context = input.required(); @@ -69,14 +70,18 @@ export class DashboardFilterBarComponent implements OnInit, OnDestroy { @Input() compactView = false; @Input() editMode = false; - compareModeEnabled = false; - - readonly fullRangeChange = output(); + /** + * Output used to notify when the full time range of the dashboard should be changed. This is currently triggered only via "use selected execution's time range" filters. + */ + readonly fullRangeRequestChange = output(); @ViewChild(PerformanceViewTimeSelectionComponent) timeSelection?: PerformanceViewTimeSelectionComponent; @ViewChildren(FilterBarItemComponent) filterComponents?: QueryList; @ViewChildren('appliedFilter', { read: ElementRef }) appliedFilters?: QueryList>; + filtersChange = output(); + groupingChange = output(); + private _destroyRef = inject(DestroyRef); filterOptions: MetricAttribute[] = []; // dashboard attributes that are not used in filters yet @@ -123,9 +128,6 @@ export class DashboardFilterBarComponent implements OnInit, OnDestroy { if (contextInput.getGroupDimensions()) { this.activeGrouping = contextInput.getGroupDimensions(); } - contextInput.compareModeChange$ - .pipe(takeUntilDestroyed(this._destroyRef)) - .subscribe((settings) => (this.compareModeEnabled = settings.enabled)); let contextFiltering = contextInput.getFilteringSettings(); this.oqlValue = contextFiltering.oql || ''; this.activeMode = contextFiltering.mode; @@ -231,7 +233,8 @@ export class DashboardFilterBarComponent implements OnInit, OnDestroy { this.rawMeasurementsModeActive = response.hasUnknownFields; // for grouping change, we will trigger refresh automatically. otherwise grouping and filters will change together if (!this.rawMeasurementsModeActive) { - this.context().updateGrouping(dimensions); + this.groupingChange.emit(dimensions); + // this.context().updateGrouping(dimensions); } else { // wait for the manual apply } @@ -245,12 +248,13 @@ export class DashboardFilterBarComponent implements OnInit, OnDestroy { hiddenFilters: this.context().getFilteringSettings().hiddenFilters, oql: this.oqlValue, }; - this.context().setFilteringSettings(settings); + this.filtersChange.emit(settings); } manuallyApplyFilters() { if (this.haveNewGrouping()) { - this.context().updateGrouping(this.activeGrouping); + this.groupingChange.emit(this.activeGrouping); + // this.context().updateGrouping(this.activeGrouping); } if (this.activeMode === TsFilteringMode.STANDARD) { this.emitFiltersChange(); @@ -286,7 +290,7 @@ export class DashboardFilterBarComponent implements OnInit, OnDestroy { if (item.updateTimeSelectionOnFilterChange && item.searchEntities.length > 0) { // calculate the new time range. if all the entities were deleted, keep the last range. const newRange = this.getExecutionsTimeRange(item); - this.fullRangeChange.emit(newRange); + this.fullRangeRequestChange.emit(newRange); } this.emitFilterChange$.next(); } diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/filter-bar-item/filter-bar-item.component.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/filter-bar-item/filter-bar-item.component.ts index 9b9ae860f..b13266b05 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/filter-bar-item/filter-bar-item.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/filter-bar-item/filter-bar-item.component.ts @@ -25,6 +25,7 @@ import { Execution, ExecutiontTaskParameters, Plan } from '@exense/step-core'; templateUrl: './filter-bar-item.component.html', styleUrls: ['./filter-bar-item.component.scss'], imports: [COMMON_IMPORTS, FilterBarPlanItemComponent, FilterBarTaskItemComponent, FilterBarExecutionItemComponent], + standalone: true, }) export class FilterBarItemComponent implements OnInit, OnChanges, AfterViewInit { @Input() item!: FilterBarItem; // should not make edits on it diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.html b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.html index 9f5257fe7..85ce52628 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.html +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.html @@ -11,6 +11,11 @@ (rangeChange)="onRangerSelectionChange($event)" (zoomReset)="onRangerZoomReset()" /> + @if (showSpinnerWhileLoading() && isLoading()) { +
+ +
+ } }
diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.scss b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.scss index d5a178b08..6321a550e 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.scss +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.scss @@ -49,4 +49,15 @@ step-execution-time-selection { .time-picker-container { margin-right: 0.2rem; } + + .spinner-container { + display: flex; + justify-content: center; + position: absolute; + width: 100%; + height: 62px; + align-items: center; + z-index: 100; + top: 0; + } } diff --git a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.ts b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.ts index 26c4b0707..ef116ed57 100644 --- a/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.ts +++ b/projects/step-frontend/src/lib/modules/timeseries/modules/filter-bar/components/perfomance-view-time-selection/performance-view-time-selection.component.ts @@ -4,6 +4,7 @@ import { EventEmitter, inject, input, + Input, OnInit, output, Output, @@ -25,6 +26,7 @@ import { TSRangerSettings } from '../ranger/ts-ranger-settings'; import { TSRangerComponent } from '../ranger/ts-ranger.component'; import { FindBucketsRequestBuilder } from '../../types/find-buckets-request-builder'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { sign } from 'chart.js/helpers'; @Component({ selector: 'step-execution-time-selection', @@ -32,9 +34,11 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; styleUrls: ['./performance-view-time-selection.component.scss'], encapsulation: ViewEncapsulation.None, imports: [COMMON_IMPORTS, TSRangerComponent], + standalone: true, }) export class PerformanceViewTimeSelectionComponent implements OnInit { readonly context = input.required(); + readonly showSpinnerWhileLoading = input(false); readonly rangerLoaded = output(); @@ -46,6 +50,8 @@ export class PerformanceViewTimeSelectionComponent implements OnInit { private _timeSeriesService = inject(TimeSeriesService); private _destroyRef = inject(DestroyRef); + isLoading = signal(false); + ngOnInit(): void { const context = this.context(); if (!context) { @@ -76,7 +82,9 @@ export class PerformanceViewTimeSelectionComponent implements OnInit { } createRanger(context: TimeSeriesContext): Observable { + this.isLoading.set(true); const customFiltering = JSON.parse(JSON.stringify(this.context().getFilteringSettings())) as TsFilteringSettings; + customFiltering.filterItems = []; // ignore visible filters. const request = new FindBucketsRequestBuilder() .withRange(context.getFullTimeRange()) @@ -100,11 +108,11 @@ export class PerformanceViewTimeSelectionComponent implements OnInit { label: 'Response Time', labelItems: ['Response Time'], data: avgData, - // value: (self, x) => Math.trunc(x) + ' ms', legendName: 'Ranger', }, ], }); + this.isLoading.set(false); }), ); }