improve file preview load performance

This commit is contained in:
Dan Percic 2022-04-04 15:24:02 +03:00
parent a66427cd4f
commit 90877c7077
15 changed files with 71 additions and 92 deletions

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { Component, ViewContainerRef } from '@angular/core';
import { RouterHistoryService } from '@services/router-history.service';
import { UserService } from '@services/user.service';

View File

@ -120,7 +120,10 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
level: NgxLoggerLevel.DEBUG,
},
FILTERS: {
enabled: true,
enabled: false,
},
PDF: {
enabled: false,
},
},
} as ILoggerConfig,

View File

@ -1,16 +0,0 @@
import { ManualRedactionEntryWrapper } from './manual-redaction-entry.wrapper';
import { IManualAddResponse } from '@red/domain';
export class ManualAnnotationResponse {
annotationId;
commentId;
constructor(public manualRedactionEntryWrapper: ManualRedactionEntryWrapper, public manualAddResponse: IManualAddResponse) {
this.annotationId = manualAddResponse?.annotationId ? manualAddResponse.annotationId : new Date().getTime();
this.commentId = manualAddResponse?.commentId ? manualAddResponse.commentId : new Date().getTime();
}
get dictionary() {
return this.manualRedactionEntryWrapper.manualRedactionEntry.type;
}
}

View File

@ -8,11 +8,7 @@ export const ManualRedactionEntryTypes = {
export type ManualRedactionEntryType = keyof typeof ManualRedactionEntryTypes;
export class ManualRedactionEntryWrapper {
constructor(
readonly quads: any,
readonly manualRedactionEntry: IManualRedactionEntry,
readonly type: ManualRedactionEntryType,
readonly rectId?: string,
) {}
export interface ManualRedactionEntryWrapper {
readonly manualRedactionEntry: IManualRedactionEntry;
readonly type: ManualRedactionEntryType;
}

View File

@ -286,9 +286,7 @@ export class FileActionsComponent implements OnChanges {
: null,
denyText: this.file.analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.denyText') : null,
}),
async () => {
await this._setFileApproved();
},
() => this._setFileApproved(),
);
}
@ -300,12 +298,8 @@ export class FileActionsComponent implements OnChanges {
return;
}
await firstValueFrom(this._redactionImportService.importRedactions(this.file.dossierId, this.file.fileId, fileToImport)).catch(
error => {
this._toaster.error(_('error.http.generic'), { params: error });
},
);
// reload file
const import$ = this._redactionImportService.importRedactions(this.file.dossierId, this.file.fileId, fileToImport);
await firstValueFrom(import$).catch(error => this._toaster.error(_('error.http.generic'), { params: error }));
}
forceReanalysisAction($event: LongPressEvent) {

View File

@ -415,11 +415,11 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0];
const activePage = activeAnnotation.getPageNumber();
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
const manualRedaction = this._getManualRedaction({ [activePage]: quads });
const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads });
this._cleanUpSelectionAndButtonState();
this.pdfViewer.deleteAnnotations([activeAnnotation.Id]);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(quads, manualRedaction, 'REDACTION', activeAnnotation.Id));
this.manualAnnotationRequested.emit({ manualRedactionEntry, type: 'REDACTION' });
}
private _cleanUpSelectionAndButtonState() {
@ -487,8 +487,8 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
const text = this.documentViewer.getSelectedText();
const manualRedaction = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit({ manualRedactionEntry, type });
}
private async _handleCustomActions() {

View File

@ -39,7 +39,6 @@
class="red-tab"
iqserHelpMode="highlights_view"
>
<!-- iqserHelpMode="text_highlights_view"-->
{{ 'file-preview.text-highlights' | translate }}
</button>
</ng-container>

View File

@ -1,4 +1,6 @@
<ng-container *ngIf="stateService.dossier$ | async as dossier">
<ng-container *ngIf="state.dossierFileChange$ | async"></ng-container>
<ng-container *ngIf="state.dossier$ | async as dossier">
<section *ngIf="file$ | async as file" [class.fullscreen]="fullScreen">
<div class="page-header">
<div class="flex flex-1">
@ -112,14 +114,14 @@
<ng-template #annotationFilterTemplate let-filter="filter">
<redaction-type-filter
*ngIf="filter.topLevelFilter"
[dossierTemplateId]="stateService.dossierTemplateId"
[dossierTemplateId]="state.dossierTemplateId"
[filter]="filter"
></redaction-type-filter>
<ng-container *ngIf="!filter.topLevelFilter">
<redaction-dictionary-annotation-icon
[dictionaryKey]="filter.id"
[dossierTemplateId]="stateService.dossierTemplateId"
[dossierTemplateId]="state.dossierTemplateId"
></redaction-dictionary-annotation-icon>
{{ filter.label | humanize: false }}

View File

@ -22,14 +22,14 @@ import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from '../dossier/services/annotation-processing.service';
import { File, ViewMode } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { combineLatest, firstValueFrom, Observable, of, pairwise, timer } from 'rxjs';
import { combineLatest, firstValueFrom, Observable, of, pairwise } from 'rxjs';
import { UserPreferenceService } from '@services/user-preference.service';
import { clearStamps, download, handleFilterDelta, stampPDFPage } from '../../utils';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
import { TranslateService } from '@ngx-translate/core';
import { FilesService } from '@services/entity-services/files.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { catchError, debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators';
import { catchError, debounceTime, map, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { WatermarkService } from '@shared/services/watermark.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
@ -66,9 +66,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
selectedAnnotations: AnnotationWrapper[] = [];
displayPdfViewer = false;
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly fileId = this.stateService.fileId;
readonly dossierId = this.stateService.dossierId;
readonly file$ = this.stateService.file$.pipe(tap(() => this._fileDataService.loadAnnotations()));
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly file$ = this.state.file$.pipe(tap(() => this._fileDataService.loadAnnotations()));
ready = false;
private _lastPage: string;
@ -86,7 +86,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _logger: NGXLogger,
private readonly _filesService: FilesService,
private readonly _errorService: ErrorService,
readonly stateService: FilePreviewStateService,
readonly state: FilePreviewStateService,
private readonly _filterService: FilterService,
readonly permissionsService: PermissionsService,
readonly multiSelectService: MultiSelectService,
@ -128,7 +128,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private get _canPerformAnnotationActions$() {
const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect()));
return combineLatest([this.stateService.file$, this.stateService.dossier$, viewMode$, this._viewModeService.compareMode$]).pipe(
return combineLatest([this.state.file$, this.state.dossier$, viewMode$, this._viewModeService.compareMode$]).pipe(
map(
([file, dossier, viewMode]) =>
this.permissionsService.canPerformAnnotationActions(file, dossier) && viewMode === 'STANDARD',
@ -200,7 +200,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<boolean> {
const file = await this.stateService.file;
const file = await this.state.file;
if (!file.canBeOpened) {
this._navigateToDossier();
return;
@ -215,7 +215,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnInit(): Promise<void> {
const file = await this.stateService.file;
const file = await this.state.file;
if (!file) {
this._handleDeletedFile();
@ -295,10 +295,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
'manualAnnotation',
null,
{ manualRedactionEntryWrapper, dossierId: this.dossierId },
async ({ manualRedactionEntry }: ManualRedactionEntryWrapper) => {
const addAnnotation$ = this._manualRedactionService.addAnnotation([manualRedactionEntry], this.dossierId, this.fileId);
await firstValueFrom(addAnnotation$.pipe(catchError(() => of(undefined))));
await this._fileDataService.loadAnnotations();
({ manualRedactionEntry }: ManualRedactionEntryWrapper) => {
const add$ = this._manualRedactionService.addAnnotation([manualRedactionEntry], this.dossierId, this.fileId);
const addAndReload$ = add$.pipe(
withLatestFrom(this.state.file$),
switchMap(([, file]) => this._filesService.reload(this.dossierId, file)),
);
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
},
);
});
@ -365,7 +368,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const pageNumber: string = this._lastPage || this._activatedRoute.snapshot.queryParams.page;
if (pageNumber) {
setTimeout(async () => {
const file = await this.stateService.file;
const file = await this.state.file;
let page = parseInt(pageNumber, 10);
if (page < 1 || Number.isNaN(page)) {
page = 1;
@ -386,7 +389,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
async annotationsChangedByReviewAction() {
this.multiSelectService.deactivate();
const file = await this.stateService.file;
const file = await this.state.file;
await firstValueFrom(this._filesService.reload(this.dossierId, file));
}
@ -521,7 +524,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return;
}
const file = await this.stateService.file;
const file = await this.state.file;
const allPages = [...Array(file.numberOfPages).keys()].map(page => page + 1);
try {
@ -532,7 +535,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
if (this._viewModeService.isRedacted) {
const dossier = await this.stateService.dossier;
const dossier = await this.state.dossier;
if (dossier.watermarkPreviewEnabled) {
await this._stampPreview(pdfDoc, dossier.dossierTemplateId);
}
@ -579,14 +582,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private _subscribeToFileUpdates(): void {
this.addActiveScreenSubscription = this.loadAnnotations().subscribe();
// TODO With changes monitoring, this should not be necessary
this.addActiveScreenSubscription = timer(0, 5000)
.pipe(
switchMap(() => this.stateService.file$),
switchMap(file => this._filesService.reload(this.dossierId, file)),
)
.subscribe();
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this._handleDeletedDossier()))

View File

@ -82,10 +82,6 @@ export class FileDataService {
return firstValueFrom(this.annotations$.pipe(map(dict => Object.values(dict))));
}
get textHighlights() {
return this.#textHighlights$.value;
}
get #annotations$() {
return this.#redactionLog$.pipe(
withLatestFrom(this._state.file$),
@ -106,7 +102,7 @@ export class FileDataService {
return;
}
this._logger.debug('[ANNOTATIONS] Load annotations');
this._logger.info('[ANNOTATIONS] Load annotations');
await this.loadViewedPages(file);
await this.loadRedactionLog();

View File

@ -10,12 +10,15 @@ import { FileManagementService } from '@services/entity-services/file-management
import { DOSSIER_ID, FILE_ID } from '@utils/constants';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { wipeFilesCache } from '@red/cache';
import { DossiersService } from '../../../services/dossiers/dossiers.service';
import { FilesService } from '../../../services/entity-services/files.service';
@Injectable()
export class FilePreviewStateService {
readonly file$: Observable<File>;
readonly blob$: Observable<Blob>;
readonly dossier$: Observable<Dossier>;
readonly dossierFileChange$: Observable<File[]>;
readonly isReadonly$: Observable<boolean>;
readonly isWritable$: Observable<boolean>;
@ -28,6 +31,8 @@ export class FilePreviewStateService {
private readonly _injector: Injector,
private readonly _route: ActivatedRoute,
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
private readonly _filesService: FilesService,
private readonly _permissionsService: PermissionsService,
) {
const dossiersService = dossiersServiceResolver(this._injector);
@ -44,6 +49,7 @@ export class FilePreviewStateService {
);
this.blob$ = this.#blob$;
this.dossierFileChange$ = this.#dossierFilesChange$();
}
get file(): Promise<File> {
@ -67,6 +73,13 @@ export class FilePreviewStateService {
);
}
#dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
switchMap(dossierId => this._filesService.loadAll(dossierId, this._dossiersService.routerPath)),
);
}
#downloadOriginalFile(cacheIdentifier?: string): Observable<Blob> {
const downloadFile = this._fileManagementService.downloadOriginalFile(this.dossierId, this.fileId, 'body', cacheIdentifier);
return from(wipeFilesCache()).pipe(switchMap(() => downloadFile));

View File

@ -4,12 +4,14 @@ import { DossierStats, IDossierStats } from '@red/domain';
import { DOSSIER_ID } from '@utils/constants';
import { Observable, of } from 'rxjs';
import { UserService } from '../user.service';
import { NGXLogger } from 'ngx-logger';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DossierStatsService extends StatsService<DossierStats, IDossierStats> {
constructor(protected readonly _injector: Injector, private readonly _userService: UserService) {
constructor(protected readonly _injector: Injector, private readonly _userService: UserService, private readonly _logger: NGXLogger) {
super(_injector, DOSSIER_ID, DossierStats, 'dossier-stats');
}
@ -19,6 +21,6 @@ export class DossierStatsService extends StatsService<DossierStats, IDossierStat
return of([]);
}
return super.getFor(ids);
return super.getFor(ids).pipe(tap(stats => this._logger.info('[STATS] Loaded', stats)));
}
}

View File

@ -1,13 +1,14 @@
import { EntitiesService, List, mapEach, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui';
import { Dossier, DossierStats, IChangesDetails, IDossier, IDossierChanges, IDossierRequest } from '@red/domain';
import { combineLatest, EMPTY, forkJoin, Observable, of, Subject, throwError } from 'rxjs';
import { catchError, filter, map, mapTo, pluck, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, map, pluck, switchMap, tap } from 'rxjs/operators';
import { Injector } from '@angular/core';
import { DossierStatesService } from '../entity-services/dossier-states.service';
import { DossierStatsService } from './dossier-stats.service';
import { IDossiersStats } from './active-dossiers.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { NGXLogger } from 'ngx-logger';
const CONFLICT_MSG = _('add-dossier-dialog.errors.dossier-already-exists');
const GENERIC_MSG = _('add-dossier-dialog.errors.generic');
@ -18,6 +19,7 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
protected readonly _dossierStatsService = this._injector.get(DossierStatsService);
protected readonly _dossierStateService = this._injector.get(DossierStatesService);
protected readonly _toaster = this._injector.get(Toaster);
protected readonly _logger = this._injector.get(NGXLogger);
protected constructor(protected readonly _injector: Injector, protected readonly _path: string, readonly routerPath: string) {
super(_injector, Dossier, _path);
@ -50,7 +52,8 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
changes.map(change => this._load(change.dossierId).pipe(removeIfNotFound(change.dossierId)));
return this.hasChangesDetails$().pipe(
switchMap(dossierChanges => forkJoin(load(dossierChanges)).pipe(mapTo(dossierChanges))),
tap(changes => this._logger.info('[CHANGES] ', changes)),
switchMap(dossierChanges => forkJoin(load(dossierChanges)).pipe(map(() => dossierChanges))),
tap(() => this._updateLastChanged()),
);
}
@ -60,8 +63,8 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
return this.getAll().pipe(
mapEach(entity => new Dossier(entity, this.routerPath)),
/* Load stats before updating entities */
switchMap(dossiers => this._dossierStatsService.getFor(dossierIds(dossiers)).pipe(mapTo(dossiers))),
switchMap(dossiers => this._dossierStateService.loadAllForAllTemplates().pipe(mapTo(dossiers))),
switchMap(dossiers => this._dossierStatsService.getFor(dossierIds(dossiers)).pipe(map(() => dossiers))),
switchMap(dossiers => this._dossierStateService.loadAllForAllTemplates().pipe(map(() => dossiers))),
tap(dossiers => this.setEntities(dossiers)),
);
}

View File

@ -4,7 +4,7 @@ import { File, IFile } from '@red/domain';
import { Observable } from 'rxjs';
import { UserService } from '../user.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { map, mapTo, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { NGXLogger } from 'ngx-logger';
@ -26,17 +26,18 @@ export class FilesService extends EntitiesService<File, IFile> {
loadAll(dossierId: string, routerPath: string) {
const files$ = this.getFor(dossierId).pipe(
mapEach(file => new File(file, this._userService.getNameForId(file.assignee), routerPath)),
tap(() => this._logger.info('[FILE] Loaded')),
);
const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(mapTo(files))));
const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(map(() => files))));
return loadStats$.pipe(tap(files => this._filesMapService.set(dossierId, files)));
}
reload(dossierId: string, file: File): Observable<boolean> {
return super._getOne([dossierId, file.id]).pipe(
map(_file => new File(_file, this._userService.getNameForId(_file.assignee), file.routerPath)),
switchMap(_file => this._dossierStatsService.getFor([dossierId]).pipe(mapTo(_file))),
switchMap(_file => this._dossierStatsService.getFor([dossierId]).pipe(map(() => _file))),
map(_file => this._filesMapService.replace([_file])),
tap(() => this._logger.info('[FILE] Loaded')),
tap(() => this._logger.info('[FILE] Reloaded')),
);
}
@ -66,9 +67,6 @@ export class FilesService extends EntitiesService<File, IFile> {
);
}
/**
* Assigns a reviewer for a list of files.
*/
@Validate()
setReviewerFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string, assigneeId: string) {
const url = `${this._defaultModelPath}/under-review/${dossierId}/bulk`;
@ -79,9 +77,6 @@ export class FilesService extends EntitiesService<File, IFile> {
);
}
/**
* Sets the status APPROVED for a list of files.
*/
@Validate()
setApprovedFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
const fileIds = files.map(f => f.id);
@ -91,9 +86,6 @@ export class FilesService extends EntitiesService<File, IFile> {
);
}
/**
* Sets the status UNDER_REVIEW for a list of files.
*/
@Validate()
setUnderReviewFor(@RequiredParam() files: List<IRouterPath>, @RequiredParam() dossierId: string) {
const fileIds = files.map(f => f.id);

@ -1 +1 @@
Subproject commit 9596338ed8493dce2759f23695ba7c2adb32c345
Subproject commit 8e15298919c2ae3b0abb243c3c6912eb97d6db68