mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-49653 Code improvements
This commit is contained in:
@@ -31,17 +31,13 @@ export interface FilterCountersQuerySort {
|
|||||||
isProcessVariable: boolean;
|
isProcessVariable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A single query of the batched count request, holding the criteria of one filter. */
|
||||||
* A single query of the batched count request, holding the criteria of one filter. The counter
|
|
||||||
* resolved for the query is keyed by its `requestId` in the response.
|
|
||||||
*/
|
|
||||||
export interface FilterCountersQuery {
|
export interface FilterCountersQuery {
|
||||||
/** Identifies the query, so that its counter can be read back from the response. */
|
/** Identifies the query, so its counter can be read back from the response. */
|
||||||
requestId: string;
|
requestId: string;
|
||||||
status?: string[];
|
status?: string[];
|
||||||
assignee?: string[];
|
assignee?: string[];
|
||||||
sort?: FilterCountersQuerySort;
|
sort?: FilterCountersQuerySort;
|
||||||
/** Every other criteria of the filter the query was built from. */
|
|
||||||
[criteria: string]: unknown;
|
[criteria: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,34 +48,23 @@ export type FilterCountersRequest = {
|
|||||||
[entityType in FilterCounterEntityType]?: FilterCountersQuery[];
|
[entityType in FilterCounterEntityType]?: FilterCountersQuery[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** Shape of a task or process filter the counters are resolved for. Its key is the `requestId`. */
|
||||||
* Shape of a task or process filter the counters are resolved for. The key of the filter is used
|
|
||||||
* as the `requestId` of its query, so that its counter can be read back from the response. A filter
|
|
||||||
* without a key holds no identity for the batched request, so its counter is fetched on its own.
|
|
||||||
*/
|
|
||||||
export interface FilterCounterCandidate {
|
export interface FilterCounterCandidate {
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
showCounter?: boolean;
|
showCounter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts returned by the batched count request, keyed by entity type and then by the `requestId`
|
* Counts returned by the batched count request, keyed by entity type and then by `requestId`.
|
||||||
* of the query the count was resolved for.
|
* e.g. `{ TASK: { 'my-tasks': 5 }, PROCESS_INSTANCE: { 'running-processes': 5 } }`
|
||||||
* e.g. `{ TASK: { 'my-tasks': 5, 'queued-tasks': 0 }, PROCESS_INSTANCE: { 'running-processes': 5 } }`
|
|
||||||
*/
|
*/
|
||||||
export type FilterCounters = {
|
export type FilterCounters = {
|
||||||
[entityType in FilterCounterEntityType]?: { [requestId: string]: number };
|
[entityType in FilterCounterEntityType]?: { [requestId: string]: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Counters of the filters of one entity type, keyed by the key of the filter they were resolved for.
|
|
||||||
*/
|
|
||||||
export interface FilterCountersResult {
|
export interface FilterCountersResult {
|
||||||
/** Counters keyed by filter key. Empty when the batched count endpoint is not available. */
|
/** Counters keyed by filter key. Empty when the batched count endpoint is not available. */
|
||||||
counters: { [filterKey: string]: number };
|
counters: { [filterKey: string]: number };
|
||||||
/**
|
/** When `false`, the backend holds no batched count endpoint: count one filter at a time. */
|
||||||
* Whether the counters were resolved by the batched count endpoint. When `false`, the endpoint is
|
|
||||||
* not available on the backend of the app and the counters are to be resolved one filter at a time.
|
|
||||||
*/
|
|
||||||
batched: boolean;
|
batched: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -520,13 +520,11 @@ describe('ProcessFiltersCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the counters of the filters the batch left out on their own', async () => {
|
it('should resolve the counters of the filters the batch left out on their own', async () => {
|
||||||
/* A filter without a key, or one the query cannot be built for, is left out of the batch. */
|
|
||||||
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
|
||||||
await bindAppName('mock-app-name');
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
/* The model holds no status for the filter targeting every status. */
|
|
||||||
expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']);
|
expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -901,13 +899,11 @@ describe('ProcessFiltersCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the counters of the filters the batch left out on their own', async () => {
|
it('should resolve the counters of the filters the batch left out on their own', async () => {
|
||||||
/* A filter without a key, or one the query cannot be built for, is left out of the batch. */
|
|
||||||
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
getFilterCountersSpy.and.returnValue(of({ counters: { FakeRunningProcesses: 9 }, batched: true }));
|
||||||
|
|
||||||
await bindAppName('mock-app-name');
|
await bindAppName('mock-app-name');
|
||||||
|
|
||||||
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
expect(component.counters['FakeRunningProcesses']).toBe(9);
|
||||||
/* The model holds no status for the filter targeting every status. */
|
|
||||||
expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']);
|
expect(getProcessCounterSpy.calls.allArgs().map(([, status]) => status)).toEqual([null, 'COMPLETED']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+22
-42
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||||
import { defer, EMPTY, Observable, Subscription } from 'rxjs';
|
import { combineLatest, defer, EMPTY, Observable, Subscription } from 'rxjs';
|
||||||
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
|
||||||
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model';
|
||||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||||
@@ -48,8 +48,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
/**
|
/**
|
||||||
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count.
|
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the process count.
|
||||||
*
|
*
|
||||||
* @deprecated the counters are resolved by `POST /query/v1/count`, which requires Activiti 8.7.0
|
* @deprecated only used by the backends without `POST /query/v1/count`. It will be removed,
|
||||||
* forward. This input is only used by the backends without that endpoint and will be removed,
|
|
||||||
* along with the 'GET' method, in ADF 10.0.0.
|
* along with the 'GET' method, in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
@@ -141,12 +140,14 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
this.initFilterCounters();
|
this.initFilterCounters();
|
||||||
this.selectFilterAndEmit(this.filterParam);
|
this.selectFilterAndEmit(this.filterParam);
|
||||||
this.success.emit(res);
|
this.success.emit(res);
|
||||||
this.loadFilterCounters(appName);
|
|
||||||
},
|
},
|
||||||
error: (err: unknown) => {
|
error: (err: unknown) => {
|
||||||
this.error.emit(err);
|
this.error.emit(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Read along with the filters, not once they arrive, so both components share one request. */
|
||||||
|
this.loadFilterCounters(appName, filters$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,20 +254,18 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keeps the counters in sync with the engine events of the app.
|
* @deprecated `getFilterCounters` keeps the counters in sync with the engine events, so nothing is
|
||||||
*
|
|
||||||
* @deprecated the counters stream keeps itself in sync with the engine events, so nothing is
|
|
||||||
* subscribed here anymore. It will be removed in ADF 10.0.0.
|
* subscribed here anymore. It will be removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
initProcessNotification(): void {
|
initProcessNotification(): void {
|
||||||
/* Kept for backwards compatibility: `getFilterCounters` subscribes to the engine events. */
|
/* Kept for backwards compatibility. */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterate over filters and update counters
|
* Iterate over filters and update counters
|
||||||
*
|
*
|
||||||
* @deprecated the counters are resolved by the batched count request. This resolves them one
|
* @deprecated resolves the counters one filter at a time, for the backends without the batched
|
||||||
* filter at a time, for the backends without the batched count endpoint.
|
* count endpoint. It will be removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
updateFilterCounters(): void {
|
updateFilterCounters(): void {
|
||||||
this.filters.forEach((filter) => this.updateFilterCounter(filter));
|
this.filters.forEach((filter) => this.updateFilterCounter(filter));
|
||||||
@@ -276,8 +275,8 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
* Get current value for filter and check if value has changed
|
* Get current value for filter and check if value has changed
|
||||||
*
|
*
|
||||||
* @param filter filter
|
* @param filter filter
|
||||||
* @deprecated the counters are resolved by the batched count request. This resolves the counter
|
* @deprecated resolves the counter of one filter, for the backends without the batched count
|
||||||
* of one filter, for the backends without the batched count endpoint.
|
* endpoint. It will be removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
updateFilterCounter(filter: ProcessFilterCloudModel): void {
|
updateFilterCounter(filter: ProcessFilterCloudModel): void {
|
||||||
const filterKey = filter?.showCounter ? filter.key : undefined;
|
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||||
@@ -285,8 +284,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* `defer` turns the query building of the count request into a failure of the stream, so that a
|
/* `defer` keeps a query that cannot be built from breaking the counters of the other filters. */
|
||||||
filter the counter cannot be resolved for is left without one instead of breaking the others. */
|
|
||||||
defer(() => this.fetchProcessFilterCounter(filter))
|
defer(() => this.fetchProcessFilterCounter(filter))
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(() => EMPTY),
|
catchError(() => EMPTY),
|
||||||
@@ -306,9 +304,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Flags the counter of a filter as read whenever the filter is refreshed elsewhere */
|
||||||
* Flags the counter of a filter as read whenever the filter is refreshed by an external action
|
|
||||||
*/
|
|
||||||
getFilterKeysAfterExternalRefreshing(): void {
|
getFilterKeysAfterExternalRefreshing(): void {
|
||||||
this.processFilterCloudService.filterKeyToBeRefreshed$
|
this.processFilterCloudService.filterKeyToBeRefreshed$
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
@@ -341,18 +337,15 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
this.currentFilter = undefined;
|
this.currentFilter = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private loadFilterCounters(appName: string, filters$: Observable<ProcessFilterCloudModel[]>): void {
|
||||||
* Resolves the counters of the filters, with one request shared with the task filters. The
|
|
||||||
* counters of a backend without the batched count endpoint are resolved one filter at a time.
|
|
||||||
*
|
|
||||||
* @param appName application name
|
|
||||||
*/
|
|
||||||
private loadFilterCounters(appName: string): void {
|
|
||||||
this.countersSubscription?.unsubscribe();
|
this.countersSubscription?.unsubscribe();
|
||||||
this.countersSubscription = this.filterCountersCloudService
|
/* Counters are keyed by filter key, so they are applied once the filters are known. */
|
||||||
.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE)
|
this.countersSubscription = combineLatest([
|
||||||
|
filters$.pipe(catchError(() => EMPTY)),
|
||||||
|
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE)
|
||||||
|
])
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe(({ counters, batched }) => {
|
.subscribe(([, { counters, batched }]) => {
|
||||||
this.batchedCounters = batched;
|
this.batchedCounters = batched;
|
||||||
if (batched) {
|
if (batched) {
|
||||||
this.applyFilterCounters(counters);
|
this.applyFilterCounters(counters);
|
||||||
@@ -362,16 +355,9 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the counters resolved by the batched count request, which are keyed by filter key. The
|
|
||||||
* counter of a filter the request holds none for is resolved on its own: a filter without a key,
|
|
||||||
* or one the count query cannot be built for, is left out of the batch.
|
|
||||||
*
|
|
||||||
* @param counters counters keyed by filter key
|
|
||||||
*/
|
|
||||||
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
||||||
this.filters.forEach((filter) => {
|
this.filters.forEach((filter) => {
|
||||||
/* A filter without a key holds no request id, so no counter can be keyed by it. */
|
/* A filter without a key holds no request id. */
|
||||||
const filterKey = filter?.showCounter ? filter.key : undefined;
|
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||||
if (!filterKey) {
|
if (!filterKey) {
|
||||||
return;
|
return;
|
||||||
@@ -379,7 +365,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
const counter = counters[filterKey];
|
const counter = counters[filterKey];
|
||||||
if (counter === undefined) {
|
if (counter === undefined) {
|
||||||
/* The batch holds no counter for a filter the count query cannot be built for. */
|
/* Left out of the batch: counted on its own. */
|
||||||
this.updateFilterCounter(filter);
|
this.updateFilterCounter(filter);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -389,12 +375,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the counters again, with one request for every filter of the app. The counter of the
|
|
||||||
* given filter alone is resolved when the batched count endpoint is not available.
|
|
||||||
*
|
|
||||||
* @param filter filter that was clicked
|
|
||||||
*/
|
|
||||||
private refreshFilterCounter(filter?: ProcessFilterCloudModel): void {
|
private refreshFilterCounter(filter?: ProcessFilterCloudModel): void {
|
||||||
if (this.batchedCounters) {
|
if (this.batchedCounters) {
|
||||||
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||||
|
|||||||
+2
-2
@@ -405,8 +405,8 @@ export class ProcessFilterCloudService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use FilterCountersCloudService.getEngineEvents instead, which shares a single
|
* @deprecated use FilterCountersCloudService.getEngineEvents instead.
|
||||||
* subscription with the task filters and provides a debounced engine-event stream used to drive counter refreshes.
|
*
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
* @returns Process engine events
|
* @returns Process engine events
|
||||||
*/
|
*/
|
||||||
|
|||||||
+180
-61
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
||||||
import { firstValueFrom, Observable, of, Subject, throwError } from 'rxjs';
|
import { combineLatest, firstValueFrom, Observable, of, Subject, throwError } from 'rxjs';
|
||||||
import { ApolloTestingModule } from 'apollo-angular/testing';
|
import { ApolloTestingModule } from 'apollo-angular/testing';
|
||||||
import { FilterCountersCloudService } from './filter-counters-cloud.service';
|
import { FilterCountersCloudService } from './filter-counters-cloud.service';
|
||||||
import { NotificationCloudService } from './notification-cloud.service';
|
import { NotificationCloudService } from './notification-cloud.service';
|
||||||
@@ -47,18 +47,18 @@ describe('FilterCountersCloudService', () => {
|
|||||||
let service: FilterCountersCloudService;
|
let service: FilterCountersCloudService;
|
||||||
let notificationCloudService: NotificationCloudService;
|
let notificationCloudService: NotificationCloudService;
|
||||||
let appConfigService: AppConfigService;
|
let appConfigService: AppConfigService;
|
||||||
let engineEvents$: Subject<EngineEventsResult>;
|
let taskEvents$: Subject<EngineEventsResult>;
|
||||||
|
let processEvents$: Subject<EngineEventsResult>;
|
||||||
let makeGQLQuerySpy: jasmine.Spy;
|
let makeGQLQuerySpy: jasmine.Spy;
|
||||||
let postSpy: jasmine.Spy;
|
let postSpy: jasmine.Spy;
|
||||||
/** Payload the batched count endpoint was called with. */
|
|
||||||
const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1];
|
|
||||||
const countUrl = (): string => postSpy.calls.mostRecent().args[0];
|
|
||||||
/** Queries the batched count endpoint was called with for an entity type. */
|
|
||||||
const countQueries = (entityType: FilterCounterEntityType): FilterCountersQuery[] => countRequest()[entityType] ?? [];
|
|
||||||
const countRequestIds = (entityType: FilterCounterEntityType): string[] => countQueries(entityType).map((query) => query.requestId);
|
|
||||||
let getTaskListFiltersSpy: jasmine.Spy;
|
let getTaskListFiltersSpy: jasmine.Spy;
|
||||||
let getProcessFiltersSpy: jasmine.Spy;
|
let getProcessFiltersSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1];
|
||||||
|
const countUrl = (): string => postSpy.calls.mostRecent().args[0];
|
||||||
|
const countQueries = (entityType: FilterCounterEntityType): FilterCountersQuery[] => countRequest()[entityType] ?? [];
|
||||||
|
const countRequestIds = (entityType: FilterCounterEntityType): string[] => countQueries(entityType).map((query) => query.requestId);
|
||||||
|
|
||||||
const countersMock: FilterCounters = {
|
const countersMock: FilterCounters = {
|
||||||
TASK: { 'my-tasks': 5, 'queued-tasks': 0 },
|
TASK: { 'my-tasks': 5, 'queued-tasks': 0 },
|
||||||
PROCESS_INSTANCE: { 'running-processes': 5 }
|
PROCESS_INSTANCE: { 'running-processes': 5 }
|
||||||
@@ -79,8 +79,23 @@ describe('FilterCountersCloudService', () => {
|
|||||||
processFilter({ key: 'all-processes', status: '', showCounter: false })
|
processFilter({ key: 'all-processes', status: '', showCounter: false })
|
||||||
];
|
];
|
||||||
|
|
||||||
const emitEvent = (eventType = 'TASK_CREATED') =>
|
const engineEvents = (eventType: string): EngineEventsResult => ({
|
||||||
engineEvents$.next({ data: { engineEvents: [{ eventType, entity: {} } as TaskCloudEngineEvent] } });
|
data: { engineEvents: [{ eventType, entity: {} } as TaskCloudEngineEvent] }
|
||||||
|
});
|
||||||
|
const emitTaskEvent = (eventType = 'TASK_CREATED') => taskEvents$.next(engineEvents(eventType));
|
||||||
|
const emitProcessEvent = (eventType = 'PROCESS_STARTED') => processEvents$.next(engineEvents(eventType));
|
||||||
|
|
||||||
|
const counters = (entityType: FilterCounterEntityType, appName = 'mock-app') => firstValueFrom(service.getFilterCounters(appName, entityType));
|
||||||
|
const taskCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.TASK, appName);
|
||||||
|
const processCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.PROCESS_INSTANCE, appName);
|
||||||
|
/** As read when both filter components are on screen. */
|
||||||
|
const bothCounters = (appName = 'mock-app') =>
|
||||||
|
firstValueFrom(
|
||||||
|
combineLatest([
|
||||||
|
service.getFilterCounters(appName, FilterCounterEntityType.TASK),
|
||||||
|
service.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -96,9 +111,14 @@ describe('FilterCountersCloudService', () => {
|
|||||||
appConfigService = TestBed.inject(AppConfigService);
|
appConfigService = TestBed.inject(AppConfigService);
|
||||||
appConfigService.config.bpmHost = 'https://fake-bpm-host.com';
|
appConfigService.config.bpmHost = 'https://fake-bpm-host.com';
|
||||||
|
|
||||||
engineEvents$ = new Subject<EngineEventsResult>();
|
taskEvents$ = new Subject<EngineEventsResult>();
|
||||||
makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery').and.returnValue(engineEvents$.asObservable());
|
processEvents$ = new Subject<EngineEventsResult>();
|
||||||
/* `post` is protected on BaseCloudService, so it is reached through the shape it is spied on. */
|
makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery');
|
||||||
|
/* Every entity type holds its own subscription. */
|
||||||
|
makeGQLQuerySpy.and.callFake((_appName: string, query: string) =>
|
||||||
|
(query.includes('TASK_CREATED') ? taskEvents$ : processEvents$).asObservable()
|
||||||
|
);
|
||||||
|
/* `post` is protected, so it is reached through the shape it is spied on. */
|
||||||
postSpy = spyOn(service as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock));
|
postSpy = spyOn(service as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock));
|
||||||
getTaskListFiltersSpy = spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock));
|
getTaskListFiltersSpy = spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock));
|
||||||
getProcessFiltersSpy = spyOn(TestBed.inject(ProcessFilterCloudService), 'getProcessFilters').and.returnValue(of(processFiltersMock));
|
getProcessFiltersSpy = spyOn(TestBed.inject(ProcessFilterCloudService), 'getProcessFilters').and.returnValue(of(processFiltersMock));
|
||||||
@@ -129,7 +149,7 @@ describe('FilterCountersCloudService', () => {
|
|||||||
|
|
||||||
it('should share the filters with the batched count request', async () => {
|
it('should share the filters with the batched count request', async () => {
|
||||||
await firstValueFrom(service.getTaskFilters('mock-app'));
|
await firstValueFrom(service.getTaskFilters('mock-app'));
|
||||||
await firstValueFrom(service.getFilterCounters('mock-app', FilterCounterEntityType.TASK));
|
await taskCounters();
|
||||||
|
|
||||||
expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1);
|
expect(getTaskListFiltersSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -142,10 +162,6 @@ describe('FilterCountersCloudService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getFilterCounters', () => {
|
describe('getFilterCounters', () => {
|
||||||
const taskCounters = (appName = 'mock-app') => firstValueFrom(service.getFilterCounters(appName, FilterCounterEntityType.TASK));
|
|
||||||
const processCounters = (appName = 'mock-app') =>
|
|
||||||
firstValueFrom(service.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE));
|
|
||||||
|
|
||||||
it('should return EMPTY when appName is not set', () => {
|
it('should return EMPTY when appName is not set', () => {
|
||||||
let completed = false;
|
let completed = false;
|
||||||
service.getFilterCounters('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) });
|
service.getFilterCounters('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) });
|
||||||
@@ -154,28 +170,23 @@ describe('FilterCountersCloudService', () => {
|
|||||||
expect(postSpy).not.toHaveBeenCalled();
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the counters of both entity types with a single request', () => {
|
it('should resolve the counters of both entity types with a single request', async () => {
|
||||||
const results: FilterCountersResult[] = [];
|
expect(await bothCounters()).toEqual([
|
||||||
/* Both filter components hold their subscription, so one request resolves the counters of both. */
|
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result));
|
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe((result) => results.push(result));
|
|
||||||
|
|
||||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(results).toEqual([
|
|
||||||
{ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true },
|
{ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true },
|
||||||
{ counters: { 'running-processes': 5 }, batched: true }
|
{ counters: { 'running-processes': 5 }, batched: true }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send the queries of both entity types to the batched count endpoint', async () => {
|
it('should call the batched count endpoint of the app', async () => {
|
||||||
await taskCounters();
|
await taskCounters();
|
||||||
|
|
||||||
expect(countUrl()).toBe('https://fake-bpm-host.com/mock-app/query/v1/count');
|
expect(countUrl()).toBe('https://fake-bpm-host.com/mock-app/query/v1/count');
|
||||||
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should identify the query of every filter by the key of the filter', async () => {
|
it('should identify the query of every filter by the key of the filter', async () => {
|
||||||
await taskCounters();
|
await bothCounters();
|
||||||
|
|
||||||
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
||||||
expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['running-processes']);
|
expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['running-processes']);
|
||||||
@@ -209,7 +220,7 @@ describe('FilterCountersCloudService', () => {
|
|||||||
it('should omit an entity type without filters with a counter enabled', async () => {
|
it('should omit an entity type without filters with a counter enabled', async () => {
|
||||||
getProcessFiltersSpy.and.returnValue(of([]));
|
getProcessFiltersSpy.and.returnValue(of([]));
|
||||||
|
|
||||||
await taskCounters();
|
await bothCounters();
|
||||||
|
|
||||||
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -227,15 +238,14 @@ describe('FilterCountersCloudService', () => {
|
|||||||
it('should leave out a filter without a key, since it holds no request id', async () => {
|
it('should leave out a filter without a key, since it holds no request id', async () => {
|
||||||
getProcessFiltersSpy.and.returnValue(of([processFilter({ key: null, status: 'RUNNING', showCounter: true })]));
|
getProcessFiltersSpy.and.returnValue(of([processFilter({ key: null, status: 'RUNNING', showCounter: true })]));
|
||||||
|
|
||||||
await processCounters();
|
expect(await processCounters()).toEqual({ counters: {}, batched: true });
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => {
|
it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => {
|
||||||
getProcessFiltersSpy.and.returnValue(throwError(() => new Error('filters failed')));
|
getProcessFiltersSpy.and.returnValue(throwError(() => new Error('filters failed')));
|
||||||
|
|
||||||
await taskCounters();
|
await bothCounters();
|
||||||
|
|
||||||
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']);
|
||||||
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
expect(countRequest().PROCESS_INSTANCE).toBeUndefined();
|
||||||
@@ -278,35 +288,81 @@ describe('FilterCountersCloudService', () => {
|
|||||||
expect(await taskCounters()).toEqual({ counters: {}, batched: false });
|
expect(await taskCounters()).toEqual({ counters: {}, batched: false });
|
||||||
|
|
||||||
postSpy.and.returnValue(of(countersMock));
|
postSpy.and.returnValue(of(countersMock));
|
||||||
service.refreshFilterCounters('mock-app');
|
|
||||||
|
|
||||||
expect(await taskCounters()).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
expect(await taskCounters()).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true });
|
||||||
expect(postSpy).toHaveBeenCalledTimes(2);
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('counters scoped to the entity types on screen', () => {
|
||||||
|
it('should send the queries of the entity type on screen alone', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load the filters of an entity type that is not on screen', async () => {
|
||||||
|
await taskCounters();
|
||||||
|
|
||||||
|
expect(getTaskListFiltersSpy).toHaveBeenCalled();
|
||||||
|
expect(getProcessFiltersSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the queries of both entity types when both are on screen', async () => {
|
||||||
|
await bothCounters();
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the counters again when an entity type joins the ones on screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK]);
|
||||||
|
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should stop covering an entity type once its counters hold no subscriber', fakeAsync(() => {
|
||||||
|
const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
taskSubscription.unsubscribe();
|
||||||
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.PROCESS_INSTANCE]);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('refreshFilterCounters', () => {
|
describe('refreshFilterCounters', () => {
|
||||||
it('should resolve the counters again with a single request', fakeAsync(() => {
|
it('should resolve the counters again with a single request', fakeAsync(() => {
|
||||||
const results: FilterCountersResult[] = [];
|
const results: FilterCountersResult[] = [];
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result));
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result));
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
service.refreshFilterCounters('mock-app');
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
expect(postSpy).toHaveBeenCalledTimes(2);
|
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(results.length).toBe(2);
|
expect(results.length).toBe(2);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should not resolve the counters of an app without subscribers', () => {
|
it('should not resolve the counters of an app without subscribers', fakeAsync(() => {
|
||||||
service.refreshFilterCounters('mock-app');
|
service.refreshFilterCounters('mock-app');
|
||||||
|
tick(0);
|
||||||
|
|
||||||
expect(postSpy).not.toHaveBeenCalled();
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when only one of the two filter families is wired', () => {
|
describe('when only one of the two filter families is wired', () => {
|
||||||
/* An app holding only the task filters provides the task preferences service alone. */
|
|
||||||
const configureTasksOnly = () => {
|
const configureTasksOnly = () => {
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -344,53 +400,68 @@ describe('FilterCountersCloudService', () => {
|
|||||||
describe('getEngineEvents', () => {
|
describe('getEngineEvents', () => {
|
||||||
it('should return EMPTY when appName is not set', () => {
|
it('should return EMPTY when appName is not set', () => {
|
||||||
let completed = false;
|
let completed = false;
|
||||||
service.getEngineEvents('').subscribe({ complete: () => (completed = true) });
|
service.getEngineEvents('', FilterCounterEntityType.TASK).subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
expect(completed).toBeTrue();
|
expect(completed).toBeTrue();
|
||||||
expect(makeGQLQuerySpy).not.toHaveBeenCalled();
|
expect(makeGQLQuerySpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open a single subscription for multiple subscribers of the same app', () => {
|
it('should subscribe to the events of the task entity type alone', () => {
|
||||||
service.getEngineEvents('mock-app').subscribe();
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
service.getEngineEvents('mock-app').subscribe();
|
|
||||||
|
|
||||||
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should subscribe to both the task and the process engine events', () => {
|
|
||||||
service.getEngineEvents('mock-app').subscribe();
|
|
||||||
|
|
||||||
const [appName, query] = makeGQLQuerySpy.calls.mostRecent().args;
|
const [appName, query] = makeGQLQuerySpy.calls.mostRecent().args;
|
||||||
expect(appName).toBe('mock-app');
|
expect(appName).toBe('mock-app');
|
||||||
expect(query).toContain('TASK_CREATED');
|
expect(query).toContain('TASK_CREATED');
|
||||||
|
expect(query).not.toContain('PROCESS_STARTED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should subscribe to the events of the process entity type alone', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
|
||||||
|
const [, query] = makeGQLQuerySpy.calls.mostRecent().args;
|
||||||
expect(query).toContain('PROCESS_STARTED');
|
expect(query).toContain('PROCESS_STARTED');
|
||||||
|
expect(query).not.toContain('TASK_CREATED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a single subscription for multiple subscribers of the same entity type', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a separate subscription per entity type', () => {
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getEngineEvents('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open a separate subscription per app', () => {
|
it('should open a separate subscription per app', () => {
|
||||||
service.getEngineEvents('mock-app').subscribe();
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
service.getEngineEvents('other-app').subscribe();
|
service.getEngineEvents('other-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
|
||||||
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the debounced batch of events', fakeAsync(() => {
|
it('should emit the debounced batch of events', fakeAsync(() => {
|
||||||
const batches: TaskCloudEngineEvent[][] = [];
|
const batches: TaskCloudEngineEvent[][] = [];
|
||||||
service.getEngineEvents('mock-app').subscribe((events) => batches.push(events));
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe((events) => batches.push(events));
|
||||||
|
|
||||||
emitEvent('TASK_CREATED');
|
emitTaskEvent('TASK_CREATED');
|
||||||
emitEvent('PROCESS_STARTED');
|
emitTaskEvent('TASK_ASSIGNED');
|
||||||
tick(3000);
|
tick(3000);
|
||||||
|
|
||||||
expect(batches.length).toBe(1);
|
expect(batches.length).toBe(1);
|
||||||
expect(batches[0][0].eventType).toBe('PROCESS_STARTED');
|
expect(batches[0][0].eventType).toBe('TASK_ASSIGNED');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should debounce the events using the configured debounce time', fakeAsync(() => {
|
it('should debounce the events using the configured debounce time', fakeAsync(() => {
|
||||||
spyOnProperty(service, 'notificationDebounceTime', 'get').and.returnValue(5000);
|
spyOnProperty(service, 'notificationDebounceTime', 'get').and.returnValue(5000);
|
||||||
let emitted = false;
|
let emitted = false;
|
||||||
service.getEngineEvents('mock-app').subscribe(() => (emitted = true));
|
service.getEngineEvents('mock-app', FilterCounterEntityType.TASK).subscribe(() => (emitted = true));
|
||||||
|
|
||||||
emitEvent();
|
emitTaskEvent();
|
||||||
tick(3000);
|
tick(3000);
|
||||||
expect(emitted).toBeFalse();
|
expect(emitted).toBeFalse();
|
||||||
|
|
||||||
@@ -400,13 +471,28 @@ describe('FilterCountersCloudService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('counters driven by the engine events', () => {
|
describe('counters driven by the engine events', () => {
|
||||||
it('should make a single count request for a batch of events received by both entity types', fakeAsync(() => {
|
it('should make a single count request for a batch of events of both entity types', fakeAsync(() => {
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
tick(0);
|
||||||
postSpy.calls.reset();
|
postSpy.calls.reset();
|
||||||
|
|
||||||
emitEvent('TASK_CREATED');
|
emitTaskEvent();
|
||||||
emitEvent('PROCESS_STARTED');
|
emitProcessEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should make a single count request for the events of both entity types arriving apart', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
tick(0);
|
||||||
|
postSpy.calls.reset();
|
||||||
|
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(1000);
|
||||||
|
emitProcessEvent();
|
||||||
tick(3000);
|
tick(3000);
|
||||||
|
|
||||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||||
@@ -415,15 +501,48 @@ describe('FilterCountersCloudService', () => {
|
|||||||
it('should emit the counters resolved for the batch of events', fakeAsync(() => {
|
it('should emit the counters resolved for the batch of events', fakeAsync(() => {
|
||||||
const results: FilterCountersResult[] = [];
|
const results: FilterCountersResult[] = [];
|
||||||
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result));
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result));
|
||||||
|
tick(0);
|
||||||
|
|
||||||
postSpy.and.returnValue(of({ TASK: { 'my-tasks': 9 } }));
|
postSpy.and.returnValue(of({ TASK: { 'my-tasks': 9 } }));
|
||||||
emitEvent();
|
emitTaskEvent();
|
||||||
tick(3000);
|
tick(3000);
|
||||||
|
|
||||||
expect(results.length).toBe(2);
|
expect(results.length).toBe(2);
|
||||||
expect(results[1]).toEqual({ counters: { 'my-tasks': 9 }, batched: true });
|
expect(results[1]).toEqual({ counters: { 'my-tasks': 9 }, batched: true });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should not subscribe to the events of an entity type that is not on screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(makeGQLQuerySpy.calls.mostRecent().args[1]).toContain('TASK_CREATED');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not resolve the counters again on the events of an entity type that is not on screen', fakeAsync(() => {
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
tick(0);
|
||||||
|
postSpy.calls.reset();
|
||||||
|
|
||||||
|
emitProcessEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should stop resolving the counters on the events of an entity type that left the screen', fakeAsync(() => {
|
||||||
|
const taskSubscription = service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
|
||||||
|
service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
taskSubscription.unsubscribe();
|
||||||
|
postSpy.calls.reset();
|
||||||
|
emitTaskEvent();
|
||||||
|
tick(3000);
|
||||||
|
|
||||||
|
expect(postSpy).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should not subscribe to the engine events when notifications are disabled', fakeAsync(() => {
|
it('should not subscribe to the engine events when notifications are disabled', fakeAsync(() => {
|
||||||
appConfigService.config.notifications = false;
|
appConfigService.config.notifications = false;
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { inject, Injectable, Injector } from '@angular/core';
|
import { inject, Injectable, Injector } from '@angular/core';
|
||||||
import { combineLatest, defer, EMPTY, merge, Observable, of, Subject } from 'rxjs';
|
import { asapScheduler, combineLatest, defer, EMPTY, merge, Observable, of, Subject, Subscription } from 'rxjs';
|
||||||
import { catchError, debounceTime, map, shareReplay, switchMap, take } from 'rxjs/operators';
|
import { catchError, debounceTime, finalize, map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||||
import { BaseCloudService } from './base-cloud.service';
|
import { BaseCloudService } from './base-cloud.service';
|
||||||
import { NotificationCloudService } from './notification-cloud.service';
|
import { NotificationCloudService } from './notification-cloud.service';
|
||||||
import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model';
|
import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model';
|
||||||
@@ -38,24 +38,20 @@ import {
|
|||||||
FilterCountersResult
|
FilterCountersResult
|
||||||
} from '../models/filter-counters-cloud.model';
|
} from '../models/filter-counters-cloud.model';
|
||||||
|
|
||||||
/**
|
|
||||||
* Single subscription covering both the task and the process engine events, so that a batch of
|
|
||||||
* events results in one call to the batched count endpoint.
|
|
||||||
*/
|
|
||||||
const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501];
|
const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501];
|
||||||
|
|
||||||
/** Filters of both entity types, to resolve the counters of both filter components with one request. */
|
|
||||||
interface FilterCountersFilters {
|
interface FilterCountersFilters {
|
||||||
[FilterCounterEntityType.TASK]: TaskFilterCloudModel[];
|
[FilterCounterEntityType.TASK]: TaskFilterCloudModel[];
|
||||||
[FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[];
|
[FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Data selected by the engine event subscription. */
|
|
||||||
interface EngineEventsData {
|
interface EngineEventsData {
|
||||||
engineEvents?: TaskCloudEngineEvent[];
|
engineEvents?: TaskCloudEngineEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = `
|
/** One subscription per entity type, so an app showing one of them is not notified of the other. */
|
||||||
|
const ENGINE_EVENTS_SUBSCRIPTION_QUERIES: Record<FilterCounterEntityType, string> = {
|
||||||
|
[FilterCounterEntityType.TASK]: `
|
||||||
subscription {
|
subscription {
|
||||||
engineEvents(eventType: [
|
engineEvents(eventType: [
|
||||||
TASK_COMPLETED
|
TASK_COMPLETED
|
||||||
@@ -64,6 +60,15 @@ const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = `
|
|||||||
TASK_SUSPENDED
|
TASK_SUSPENDED
|
||||||
TASK_CANCELLED
|
TASK_CANCELLED
|
||||||
TASK_CREATED
|
TASK_CREATED
|
||||||
|
]) {
|
||||||
|
eventType
|
||||||
|
entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
[FilterCounterEntityType.PROCESS_INSTANCE]: `
|
||||||
|
subscription {
|
||||||
|
engineEvents(eventType: [
|
||||||
PROCESS_CANCELLED
|
PROCESS_CANCELLED
|
||||||
PROCESS_COMPLETED
|
PROCESS_COMPLETED
|
||||||
PROCESS_CREATED
|
PROCESS_CREATED
|
||||||
@@ -75,29 +80,28 @@ const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = `
|
|||||||
entity
|
entity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central place handling the filter counters of the task and the process filter components: it owns
|
* Resolves the counters of the task and the process filters with one batched count request, covering
|
||||||
* the filters of both components and a single engine event subscription, debounced into one batched
|
* the entity types whose counters are subscribed.
|
||||||
* count request.
|
|
||||||
*
|
|
||||||
* Loading the filters here is what keeps the counters of both components resolved by one request,
|
|
||||||
* both on load and on every batch of engine events.
|
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class FilterCountersCloudService extends BaseCloudService {
|
export class FilterCountersCloudService extends BaseCloudService {
|
||||||
private readonly notificationCloudService = inject(NotificationCloudService);
|
private readonly notificationCloudService = inject(NotificationCloudService);
|
||||||
private readonly taskListCloudService = inject(TaskListCloudService);
|
private readonly taskListCloudService = inject(TaskListCloudService);
|
||||||
private readonly processListCloudService = inject(ProcessListCloudService);
|
private readonly processListCloudService = inject(ProcessListCloudService);
|
||||||
/**
|
/** The filter services are resolved on demand: an app showing one family must not wire the other. */
|
||||||
* The filter services are resolved on demand, so that an app holding only one of the two filter
|
|
||||||
* components is not forced to provide the preferences service of the other one.
|
|
||||||
*/
|
|
||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
|
|
||||||
private readonly eventsPerApp = new Map<string, Observable<TaskCloudEngineEvent[]>>();
|
private readonly eventsPerEntityType = new Map<string, Observable<TaskCloudEngineEvent[]>>();
|
||||||
private readonly refreshPerApp = new Map<string, Subject<void>>();
|
private readonly rawEventsPerEntityType = new Map<string, Observable<TaskCloudEngineEvent[]>>();
|
||||||
|
private readonly recountPerApp = new Map<string, Subject<void>>();
|
||||||
|
private readonly eventRecountPerApp = new Map<string, Subject<void>>();
|
||||||
|
private readonly activeEntityTypesPerApp = new Map<string, Set<FilterCounterEntityType>>();
|
||||||
|
private readonly subscribersPerEntityType = new Map<string, number>();
|
||||||
|
private readonly eventSubscriptionsPerEntityType = new Map<string, Subscription>();
|
||||||
private readonly appsWithoutBatchedCounters = new Set<string>();
|
private readonly appsWithoutBatchedCounters = new Set<string>();
|
||||||
private readonly taskFiltersPerApp = new Map<string, Observable<TaskFilterCloudModel[]>>();
|
private readonly taskFiltersPerApp = new Map<string, Observable<TaskFilterCloudModel[]>>();
|
||||||
private readonly processFiltersPerApp = new Map<string, Observable<ProcessFilterCloudModel[]>>();
|
private readonly processFiltersPerApp = new Map<string, Observable<ProcessFilterCloudModel[]>>();
|
||||||
@@ -108,8 +112,7 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task filters of the app, loaded once and shared between the task filter component and the
|
* Task filters of the app, loaded once and shared with the batched count request.
|
||||||
* batched count request, so that one place owns the filters the counters are resolved for.
|
|
||||||
*
|
*
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
* @returns Task filters of the app
|
* @returns Task filters of the app
|
||||||
@@ -119,8 +122,7 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process filters of the app, loaded once and shared between the process filter component and
|
* Process filters of the app, loaded once and shared with the batched count request.
|
||||||
* the batched count request.
|
|
||||||
*
|
*
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
* @returns Process filters of the app
|
* @returns Process filters of the app
|
||||||
@@ -130,53 +132,74 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counters of the filters of an entity type, resolved by the request shared with the filters of
|
* Counters of the filters of an entity type, resolved on subscription and kept in sync with the
|
||||||
* the other entity type: once on subscription, then on every debounced batch of engine events
|
* engine events of the app. Subscribers of both entity types share one request.
|
||||||
* and on every `refreshFilterCounters` call.
|
|
||||||
*
|
*
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
* @param entityType Entity type the counters are read for
|
* @param entityType Entity type the counters are read for
|
||||||
* @returns Counters of the filters of the entity type, keyed by filter key
|
* @returns Counters keyed by filter key
|
||||||
*/
|
*/
|
||||||
getFilterCounters(appName: string, entityType: FilterCounterEntityType): Observable<FilterCountersResult> {
|
getFilterCounters(appName: string, entityType: FilterCounterEntityType): Observable<FilterCountersResult> {
|
||||||
if (!appName) {
|
if (!appName) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getCounters(appName).pipe(map(({ counters, batched }) => ({ counters: counters[entityType] ?? {}, batched })));
|
return defer(() => {
|
||||||
|
this.activateEntityType(appName, entityType);
|
||||||
|
|
||||||
|
return this.getCounters(appName);
|
||||||
|
}).pipe(
|
||||||
|
map(({ counters, batched }) => ({ counters: counters[entityType] ?? {}, batched })),
|
||||||
|
finalize(() => this.deactivateEntityType(appName, entityType))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the counters of the filters of the app again, for both entity types with one request.
|
* Resolves the counters of the app again, with one request.
|
||||||
*
|
*
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
*/
|
*/
|
||||||
refreshFilterCounters(appName: string): void {
|
refreshFilterCounters(appName: string): void {
|
||||||
this.getRefreshTrigger(appName).next();
|
this.recount(appName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced batches of engine events of the app, shared between all the subscribers of the app.
|
* Debounced batches of the engine events of an entity type, shared between its subscribers.
|
||||||
*
|
*
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
|
* @param entityType Entity type the events are read for
|
||||||
* @returns Debounced batches of engine events
|
* @returns Debounced batches of engine events
|
||||||
*/
|
*/
|
||||||
getEngineEvents(appName: string): Observable<TaskCloudEngineEvent[]> {
|
getEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable<TaskCloudEngineEvent[]> {
|
||||||
if (!appName) {
|
if (!appName) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
let events$ = this.eventsPerApp.get(appName);
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
let events$ = this.eventsPerEntityType.get(key);
|
||||||
|
if (!events$) {
|
||||||
|
events$ = this.rawEngineEvents(appName, entityType).pipe(
|
||||||
|
debounceTime(this.notificationDebounceTime),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
);
|
||||||
|
this.eventsPerEntityType.set(key, events$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return events$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private rawEngineEvents(appName: string, entityType: FilterCounterEntityType): Observable<TaskCloudEngineEvent[]> {
|
||||||
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
let events$ = this.rawEventsPerEntityType.get(key);
|
||||||
if (!events$) {
|
if (!events$) {
|
||||||
events$ = defer(() =>
|
events$ = defer(() =>
|
||||||
this.notificationCloudService.makeGQLQuery<EngineEventsData>(appName, FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY)
|
this.notificationCloudService.makeGQLQuery<EngineEventsData>(appName, ENGINE_EVENTS_SUBSCRIPTION_QUERIES[entityType])
|
||||||
).pipe(
|
).pipe(
|
||||||
map((result) => result.data?.engineEvents ?? []),
|
map((result) => result.data?.engineEvents ?? []),
|
||||||
debounceTime(this.notificationDebounceTime),
|
|
||||||
catchError(() => EMPTY),
|
catchError(() => EMPTY),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
);
|
);
|
||||||
this.eventsPerApp.set(appName, events$);
|
this.rawEventsPerEntityType.set(key, events$);
|
||||||
}
|
}
|
||||||
|
|
||||||
return events$;
|
return events$;
|
||||||
@@ -186,18 +209,75 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
return this.appConfigService.get('notifications', true);
|
return this.appConfigService.get('notifications', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private activateEntityType(appName: string, entityType: FilterCounterEntityType): void {
|
||||||
* Filters of both entity types, to resolve the counters of both filter components with one
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
* request. The filters of an entity type that fails to load are left out, so that the counters
|
const subscribers = (this.subscribersPerEntityType.get(key) ?? 0) + 1;
|
||||||
* of the other entity type are still resolved.
|
this.subscribersPerEntityType.set(key, subscribers);
|
||||||
*
|
|
||||||
* @param appName Name of the target app
|
if (subscribers > 1) {
|
||||||
* @returns Task and process filters of the app
|
return;
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
const activeEntityTypes = this.activeEntityTypes(appName);
|
||||||
|
const joinsResolvedCounters = activeEntityTypes.size > 0;
|
||||||
|
activeEntityTypes.add(entityType);
|
||||||
|
|
||||||
|
if (this.notificationsEnabled) {
|
||||||
|
this.eventSubscriptionsPerEntityType.set(
|
||||||
|
key,
|
||||||
|
this.rawEngineEvents(appName, entityType).subscribe(() => this.eventRecountTrigger(appName).next())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (joinsResolvedCounters) {
|
||||||
|
this.recount(appName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private deactivateEntityType(appName: string, entityType: FilterCounterEntityType): void {
|
||||||
|
const key = this.entityTypeKey(appName, entityType);
|
||||||
|
const subscribers = (this.subscribersPerEntityType.get(key) ?? 1) - 1;
|
||||||
|
|
||||||
|
if (subscribers > 0) {
|
||||||
|
this.subscribersPerEntityType.set(key, subscribers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscribersPerEntityType.delete(key);
|
||||||
|
this.activeEntityTypes(appName).delete(entityType);
|
||||||
|
this.eventSubscriptionsPerEntityType.get(key)?.unsubscribe();
|
||||||
|
this.eventSubscriptionsPerEntityType.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private activeEntityTypes(appName: string): Set<FilterCounterEntityType> {
|
||||||
|
let activeEntityTypes = this.activeEntityTypesPerApp.get(appName);
|
||||||
|
if (!activeEntityTypes) {
|
||||||
|
activeEntityTypes = new Set<FilterCounterEntityType>();
|
||||||
|
this.activeEntityTypesPerApp.set(appName, activeEntityTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeEntityTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private entityTypeKey(appName: string, entityType: FilterCounterEntityType): string {
|
||||||
|
return `${appName}|${entityType}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private recount(appName: string): void {
|
||||||
|
this.recountTrigger(appName).next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The filters of an entity type that fails to load are left out, so the other one is still counted.
|
||||||
private getFiltersForCounters(appName: string): Observable<FilterCountersFilters> {
|
private getFiltersForCounters(appName: string): Observable<FilterCountersFilters> {
|
||||||
|
const activeEntityTypes = this.activeEntityTypes(appName);
|
||||||
|
|
||||||
return combineLatest({
|
return combineLatest({
|
||||||
[FilterCounterEntityType.TASK]: this.getTaskFilters(appName).pipe(catchError(() => of([]))),
|
[FilterCounterEntityType.TASK]: activeEntityTypes.has(FilterCounterEntityType.TASK)
|
||||||
[FilterCounterEntityType.PROCESS_INSTANCE]: this.getProcessFilters(appName).pipe(catchError(() => of([])))
|
? this.getTaskFilters(appName).pipe(catchError(() => of([])))
|
||||||
|
: of([]),
|
||||||
|
[FilterCounterEntityType.PROCESS_INSTANCE]: activeEntityTypes.has(FilterCounterEntityType.PROCESS_INSTANCE)
|
||||||
|
? this.getProcessFilters(appName).pipe(catchError(() => of([])))
|
||||||
|
: of([])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,23 +291,10 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
return filters$;
|
return filters$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Counters of both entity types, resolved by one request shared between the filter components.
|
|
||||||
* The request is sent on subscription and on every trigger of the app: a batch of engine events,
|
|
||||||
* or a refresh.
|
|
||||||
*
|
|
||||||
* @param appName Name of the target app
|
|
||||||
* @returns Counters of both entity types
|
|
||||||
*/
|
|
||||||
private getCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
private getCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
||||||
let counters$ = this.countersPerApp.get(appName);
|
let counters$ = this.countersPerApp.get(appName);
|
||||||
if (!counters$) {
|
if (!counters$) {
|
||||||
const triggers: Observable<unknown>[] = [of(undefined), this.getRefreshTrigger(appName)];
|
counters$ = this.recounts(appName).pipe(
|
||||||
if (this.notificationsEnabled) {
|
|
||||||
triggers.push(this.getEngineEvents(appName));
|
|
||||||
}
|
|
||||||
|
|
||||||
counters$ = merge(...triggers).pipe(
|
|
||||||
switchMap(() => this.resolveCounters(appName)),
|
switchMap(() => this.resolveCounters(appName)),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
);
|
);
|
||||||
@@ -237,14 +304,6 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
return counters$;
|
return counters$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends one count request for the filters of both entity types. A backend without the batched
|
|
||||||
* count endpoint resolves no counter, so that the filter components fall back to the counters
|
|
||||||
* resolved one filter at a time.
|
|
||||||
*
|
|
||||||
* @param appName Name of the target app
|
|
||||||
* @returns Counters of both entity types
|
|
||||||
*/
|
|
||||||
private resolveCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
private resolveCounters(appName: string): Observable<{ counters: FilterCounters; batched: boolean }> {
|
||||||
if (this.appsWithoutBatchedCounters.has(appName)) {
|
if (this.appsWithoutBatchedCounters.has(appName)) {
|
||||||
return of({ counters: {}, batched: false });
|
return of({ counters: {}, batched: false });
|
||||||
@@ -256,7 +315,6 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
map((counters) => ({ counters, batched: true })),
|
map((counters) => ({ counters, batched: true })),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
if (BATCHED_COUNTERS_UNAVAILABLE_STATUSES.includes(error?.status)) {
|
if (BATCHED_COUNTERS_UNAVAILABLE_STATUSES.includes(error?.status)) {
|
||||||
/* The backend of the app holds no batched count endpoint, so it is not asked again. */
|
|
||||||
this.appsWithoutBatchedCounters.add(appName);
|
this.appsWithoutBatchedCounters.add(appName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,22 +323,35 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRefreshTrigger(appName: string): Subject<void> {
|
private recounts(appName: string): Observable<unknown> {
|
||||||
let refresh$ = this.refreshPerApp.get(appName);
|
return merge(
|
||||||
if (!refresh$) {
|
/* Reads landing in the same task are merged, so both filter components share one request. */
|
||||||
refresh$ = new Subject<void>();
|
merge(of(undefined), this.recountTrigger(appName)).pipe(debounceTime(0, asapScheduler)),
|
||||||
this.refreshPerApp.set(appName, refresh$);
|
/* One debounce over every entity type, so a batch of events also results in one request. */
|
||||||
|
this.eventRecountTrigger(appName).pipe(debounceTime(this.notificationDebounceTime))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private recountTrigger(appName: string): Subject<void> {
|
||||||
|
let recount$ = this.recountPerApp.get(appName);
|
||||||
|
if (!recount$) {
|
||||||
|
recount$ = new Subject<void>();
|
||||||
|
this.recountPerApp.set(appName, recount$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recount$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private eventRecountTrigger(appName: string): Subject<void> {
|
||||||
|
let eventRecount$ = this.eventRecountPerApp.get(appName);
|
||||||
|
if (!eventRecount$) {
|
||||||
|
eventRecount$ = new Subject<void>();
|
||||||
|
this.eventRecountPerApp.set(appName, eventRecount$);
|
||||||
}
|
}
|
||||||
|
|
||||||
return refresh$;
|
return eventRecount$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the payload of the batched count request from the filters with a counter enabled.
|
|
||||||
*
|
|
||||||
* @param filters Task and process filters of the app
|
|
||||||
* @returns Payload of the count request
|
|
||||||
*/
|
|
||||||
private buildRequest(filters: FilterCountersFilters): FilterCountersRequest {
|
private buildRequest(filters: FilterCountersFilters): FilterCountersRequest {
|
||||||
const request: FilterCountersRequest = {};
|
const request: FilterCountersRequest = {};
|
||||||
|
|
||||||
@@ -309,23 +380,15 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
.filter((filter) => filter?.showCounter && this.isCounterBatched(filter))
|
.filter((filter) => filter?.showCounter && this.isCounterBatched(filter))
|
||||||
.map((filter) => {
|
.map((filter) => {
|
||||||
try {
|
try {
|
||||||
/* Only the filters holding a key reach this point, so every query is identified by one. */
|
|
||||||
return { ...buildQuery(filter), requestId: filter.key as string };
|
return { ...buildQuery(filter), requestId: filter.key as string };
|
||||||
} catch {
|
} catch {
|
||||||
/* A filter the query cannot be built for is left out of the batch and counted on its own. */
|
/* Left out of the batch and counted on its own. */
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((query): query is FilterCountersQuery => !!query);
|
.filter((query): query is FilterCountersQuery => !!query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the counters of the given queries with a single request.
|
|
||||||
*
|
|
||||||
* @param appName Name of the target app
|
|
||||||
* @param request Payload of the count request
|
|
||||||
* @returns Counters keyed by entity type and status
|
|
||||||
*/
|
|
||||||
private fetchFilterCounters(appName: string, request: FilterCountersRequest): Observable<FilterCounters> {
|
private fetchFilterCounters(appName: string, request: FilterCountersRequest): Observable<FilterCounters> {
|
||||||
if (!Object.keys(request).length) {
|
if (!Object.keys(request).length) {
|
||||||
return of({});
|
return of({});
|
||||||
@@ -336,13 +399,7 @@ export class FilterCountersCloudService extends BaseCloudService {
|
|||||||
return this.post<FilterCountersRequest, FilterCounters>(queryUrl, request).pipe(map((counters) => counters || {}));
|
return this.post<FilterCountersRequest, FilterCounters>(queryUrl, request).pipe(map((counters) => counters || {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// A filter without a key holds no `requestId` its counter could be keyed by.
|
||||||
* Whether the counter of a filter is resolved by the batched count request. A filter without a
|
|
||||||
* key holds no `requestId` its counter could be keyed by, so it is left to be fetched on its own.
|
|
||||||
*
|
|
||||||
* @param filter Filter with a counter enabled
|
|
||||||
* @returns `true` when the counter of the filter is resolved by the batched request, otherwise `false`
|
|
||||||
*/
|
|
||||||
private isCounterBatched(filter: FilterCounterCandidate): boolean {
|
private isCounterBatched(filter: FilterCounterCandidate): boolean {
|
||||||
return !!filter?.key;
|
return !!filter?.key;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-3
@@ -19,7 +19,7 @@ import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core';
|
|||||||
import { Component, SimpleChange } from '@angular/core';
|
import { Component, SimpleChange } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { first, of, Subject, throwError } from 'rxjs';
|
import { first, NEVER, of, Subject, throwError } from 'rxjs';
|
||||||
import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
import { PROCESS_FILTERS_SERVICE_TOKEN, TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
|
||||||
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
|
||||||
import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
import { defaultTaskFiltersMock, fakeGlobalFilter, taskNotifications } from '../../mock/task-filters-cloud.mock';
|
||||||
@@ -331,7 +331,7 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(getEngineEventsSpy).toHaveBeenCalledWith('my-app-1');
|
expect(getEngineEventsSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the events of the debounced batch', fakeAsync(() => {
|
it('should emit the events of the debounced batch', fakeAsync(() => {
|
||||||
@@ -694,6 +694,28 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Batched counters', () => {
|
describe('Batched counters', () => {
|
||||||
|
it('should read the counters without waiting for the filters', async () => {
|
||||||
|
getTaskListFiltersSpy.and.returnValue(NEVER);
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
|
||||||
|
expect(getFilterCountersSpy).toHaveBeenCalledWith('my-app-1', FilterCounterEntityType.TASK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hold the counters until the filters they belong to arrive', async () => {
|
||||||
|
const filters$ = new Subject<TaskFilterCloudModel[]>();
|
||||||
|
getTaskListFiltersSpy.and.returnValue(filters$.asObservable());
|
||||||
|
getFilterCountersSpy.and.returnValue(of({ counters: { 'fake-involved-tasks': 9 }, batched: true }));
|
||||||
|
|
||||||
|
await bindAppName();
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBeUndefined();
|
||||||
|
|
||||||
|
filters$.next(fakeGlobalFilter);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.counters['fake-involved-tasks']).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
it('should read the counters of the task filters of the bound app', async () => {
|
it('should read the counters of the task filters of the bound app', async () => {
|
||||||
await bindAppName();
|
await bindAppName();
|
||||||
|
|
||||||
@@ -718,7 +740,6 @@ describe('TaskFiltersCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the counter of a filter the batch left out on its own', async () => {
|
it('should resolve the counter of a filter the batch left out on its own', async () => {
|
||||||
/* A filter without a key, or one the query cannot be built for, is left out of the batch. */
|
|
||||||
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: true }));
|
getFilterCountersSpy.and.returnValue(of({ counters: {}, batched: true }));
|
||||||
|
|
||||||
await bindAppName();
|
await bindAppName();
|
||||||
|
|||||||
+20
-38
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||||
import { defer, EMPTY, Observable, of, Subscription } from 'rxjs';
|
import { combineLatest, defer, EMPTY, Observable, of, Subscription } from 'rxjs';
|
||||||
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
import { TaskFilterCloudService } from '../../services/task-filter-cloud.service';
|
||||||
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
import { FilterParamsModel, TaskFilterCloudModel } from '../../models/filter-cloud.model';
|
||||||
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
import { AppConfigService, IconModule, TranslationService } from '@alfresco/adf-core';
|
||||||
@@ -47,8 +47,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
/**
|
/**
|
||||||
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count.
|
* (optional) From Activiti 8.7.0 forward, use the 'POST' method to get the task count.
|
||||||
*
|
*
|
||||||
* @deprecated the counters are resolved by `POST /query/v1/count`, which requires Activiti 8.7.0
|
* @deprecated only used by the backends without `POST /query/v1/count`. It will be removed,
|
||||||
* forward. This input is only used by the backends without that endpoint and will be removed,
|
|
||||||
* along with the 'GET' method, in ADF 10.0.0.
|
* along with the 'GET' method, in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
@@ -125,13 +124,15 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
this.filters = res || [];
|
this.filters = res || [];
|
||||||
this.initFilterCounters();
|
this.initFilterCounters();
|
||||||
this.selectFilterAndEmit(this.filterParam);
|
this.selectFilterAndEmit(this.filterParam);
|
||||||
this.loadFilterCounters(appName);
|
|
||||||
this.success.emit(res);
|
this.success.emit(res);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.error.emit(err);
|
this.error.emit(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Read along with the filters, not once they arrive, so both components share one request. */
|
||||||
|
this.loadFilterCounters(appName, filters$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,8 +145,8 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
/**
|
/**
|
||||||
* Iterate over filters and update counters
|
* Iterate over filters and update counters
|
||||||
*
|
*
|
||||||
* @deprecated the counters are resolved by the batched count request. This resolves them one
|
* @deprecated resolves the counters one filter at a time, for the backends without the batched
|
||||||
* filter at a time, for the backends without the batched count endpoint.
|
* count endpoint. It will be removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
updateFilterCounters(): void {
|
updateFilterCounters(): void {
|
||||||
this.filters.forEach((filter) => this.updateFilterCounter(filter));
|
this.filters.forEach((filter) => this.updateFilterCounter(filter));
|
||||||
@@ -155,16 +156,15 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
* Get current value for filter and check if value has changed
|
* Get current value for filter and check if value has changed
|
||||||
*
|
*
|
||||||
* @param filter filter
|
* @param filter filter
|
||||||
* @deprecated the counters are resolved by the batched count request. This resolves the counter
|
* @deprecated resolves the counter of one filter, for the backends without the batched count
|
||||||
* of one filter, for the backends without the batched count endpoint.
|
* endpoint. It will be removed in ADF 10.0.0.
|
||||||
*/
|
*/
|
||||||
updateFilterCounter(filter: TaskFilterCloudModel): void {
|
updateFilterCounter(filter: TaskFilterCloudModel): void {
|
||||||
if (!filter?.showCounter) {
|
if (!filter?.showCounter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* `defer` turns the query building of the count request into a failure of the stream, so that a
|
/* `defer` keeps a query that cannot be built from breaking the counters of the other filters. */
|
||||||
filter the counter cannot be resolved for is left without one instead of breaking the others. */
|
|
||||||
defer(() => this.fetchTaskFilterCounter(filter))
|
defer(() => this.fetchTaskFilterCounter(filter))
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(() => EMPTY),
|
catchError(() => EMPTY),
|
||||||
@@ -186,7 +186,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.filterCountersCloudService
|
this.filterCountersCloudService
|
||||||
.getEngineEvents(this.appName)
|
.getEngineEvents(this.appName, FilterCounterEntityType.TASK)
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((events) => {
|
.subscribe((events) => {
|
||||||
events.forEach((taskEvent) => this.checkFilterCounter(taskEvent.entity));
|
events.forEach((taskEvent) => this.checkFilterCounter(taskEvent.entity));
|
||||||
@@ -283,27 +283,22 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Flags the counter of a filter as read whenever the filter is refreshed elsewhere */
|
||||||
* Flags the counter of a filter as read whenever the filter is refreshed by an external action
|
|
||||||
*/
|
|
||||||
getFilterKeysAfterExternalRefreshing(): void {
|
getFilterKeysAfterExternalRefreshing(): void {
|
||||||
this.taskFilterCloudService.filterKeyToBeRefreshed$
|
this.taskFilterCloudService.filterKeyToBeRefreshed$
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((filterKey: string) => this.updatedCountersSet.delete(filterKey));
|
.subscribe((filterKey: string) => this.updatedCountersSet.delete(filterKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private loadFilterCounters(appName: string, filters$: Observable<TaskFilterCloudModel[]>): void {
|
||||||
* Resolves the counters of the filters, with one request shared with the process filters. The
|
|
||||||
* counters of a backend without the batched count endpoint are resolved one filter at a time.
|
|
||||||
*
|
|
||||||
* @param appName application name
|
|
||||||
*/
|
|
||||||
private loadFilterCounters(appName: string): void {
|
|
||||||
this.countersSubscription?.unsubscribe();
|
this.countersSubscription?.unsubscribe();
|
||||||
this.countersSubscription = this.filterCountersCloudService
|
/* Counters are keyed by filter key, so they are applied once the filters are known. */
|
||||||
.getFilterCounters(appName, FilterCounterEntityType.TASK)
|
this.countersSubscription = combineLatest([
|
||||||
|
filters$.pipe(catchError(() => of([]))),
|
||||||
|
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.TASK)
|
||||||
|
])
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe(({ counters, batched }) => {
|
.subscribe(([, { counters, batched }]) => {
|
||||||
this.batchedCounters = batched;
|
this.batchedCounters = batched;
|
||||||
if (batched) {
|
if (batched) {
|
||||||
this.applyFilterCounters(counters);
|
this.applyFilterCounters(counters);
|
||||||
@@ -313,16 +308,9 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the counters resolved by the batched count request, which are keyed by filter key. The
|
|
||||||
* counter of a filter the request holds none for is resolved on its own: a filter without a key,
|
|
||||||
* or one the count query cannot be built for, is left out of the batch.
|
|
||||||
*
|
|
||||||
* @param counters counters keyed by filter key
|
|
||||||
*/
|
|
||||||
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
private applyFilterCounters(counters: { [filterKey: string]: number }): void {
|
||||||
this.filters.forEach((filter) => {
|
this.filters.forEach((filter) => {
|
||||||
/* A filter without a key holds no request id, so no counter can be keyed by it. */
|
/* A filter without a key holds no request id. */
|
||||||
const filterKey = filter?.showCounter ? filter.key : undefined;
|
const filterKey = filter?.showCounter ? filter.key : undefined;
|
||||||
if (!filterKey) {
|
if (!filterKey) {
|
||||||
return;
|
return;
|
||||||
@@ -353,12 +341,6 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
|
|||||||
this.currentFilter = undefined;
|
this.currentFilter = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the counters again, with one request for every filter of the app. The counter of the
|
|
||||||
* given filter alone is resolved when the batched count endpoint is not available.
|
|
||||||
*
|
|
||||||
* @param filter filter that was selected
|
|
||||||
*/
|
|
||||||
private refreshFilterCounter(filter: TaskFilterCloudModel): void {
|
private refreshFilterCounter(filter: TaskFilterCloudModel): void {
|
||||||
if (this.batchedCounters) {
|
if (this.batchedCounters) {
|
||||||
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
this.filterCountersCloudService.refreshFilterCounters(this.appName);
|
||||||
|
|||||||
+1
-2
@@ -362,8 +362,7 @@ export class TaskFilterCloudService extends BaseCloudService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use FilterCountersCloudService.getEngineEvents instead, which shares a single
|
* @deprecated use FilterCountersCloudService.getEngineEvents instead.
|
||||||
* subscription with the process filters and resolves the counters with a single request.
|
|
||||||
* @param appName Name of the target app
|
* @param appName Name of the target app
|
||||||
* @returns Task engine events
|
* @returns Task engine events
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user