diff --git a/apps/red-ui/src/app/guards/if-logged-in.guard.ts b/apps/red-ui/src/app/guards/if-logged-in.guard.ts index 52ac6bb1c..4d7ed1b64 100644 --- a/apps/red-ui/src/app/guards/if-logged-in.guard.ts +++ b/apps/red-ui/src/app/guards/if-logged-in.guard.ts @@ -40,8 +40,7 @@ export function ifLoggedIn(): AsyncGuard { logger.info('[KEYCLOAK] Keycloak init...'); await keycloakInitializer(tenant); - logger.info('[KEYCLOAK] Keycloak init done!'); - console.log({ tenant }); + logger.info('[KEYCLOAK] Keycloak init done for tenant: ', { tenant }); await tenantsService.selectTenant(tenant); await usersService.initialize(); await licenseService.loadLicenses(); diff --git a/apps/red-ui/src/app/modules/admin/admin.routes.ts b/apps/red-ui/src/app/modules/admin/admin.routes.ts index 233f15870..9d978cc6a 100644 --- a/apps/red-ui/src/app/modules/admin/admin.routes.ts +++ b/apps/red-ui/src/app/modules/admin/admin.routes.ts @@ -87,12 +87,8 @@ const dossierTemplateIdRoutes: IqserRoutes = [ multi: true, useFactory: () => { const service = inject(CopilotService); - return () => { - setTimeout(() => { - service.connect('/api/llm/llm-websocket'); - console.log('Copilot ready'); - }, 2000); - }; + console.log('Prepare copilot'); + return () => service.connectAsync('/api/llm/llm-websocket'); }, }, ], diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts index 79ecfa33a..6ccd48108 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts @@ -1,4 +1,4 @@ -import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { AsyncPipe, NgIf, NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; @@ -7,7 +7,6 @@ import { MatTooltip } from '@angular/material/tooltip'; import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component'; import { TenantsService } from '@common-ui/tenants'; import { getCurrentUser } from '@common-ui/users'; -import { NamePipe } from '@common-ui/users/name.pipe'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { Debounce, IqserTooltipPositions } from '@iqser/common-ui/lib/utils'; @@ -19,6 +18,7 @@ import { EditorThemeService } from '@services/editor-theme.service'; import { PermissionsService } from '@services/permissions.service'; import { DatePipe } from '@shared/pipes/date.pipe'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { RulesService } from '../../../services/rules.service'; import { rulesScreenTranslations } from '../../../translations/rules-screen-translations'; import ICodeEditor = monaco.editor.ICodeEditor; @@ -57,8 +57,6 @@ const RULE_VALIDATION_TIMEOUT = 2000; CircleButtonComponent, DatePipe, InputWithActionComponent, - NamePipe, - NgForOf, MatTooltip, ], }) @@ -102,16 +100,18 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv const username = this.#currentUser.id; const tenant = inject(TenantsService).activeTenantId; this.#copilotService - .listen('/user/' + username + '/queue/' + tenant + '/rules-copilot') - .pipe(takeUntilDestroyed()) + .listen<{ token?: string }>('/user/' + username + '/queue/' + tenant + '/rules-copilot') + .pipe( + takeUntilDestroyed(), + map(res => res?.token), + filter(Boolean), + ) .subscribe(response => { console.log('WS response: ' + response); + this.responses$.next([...this.responses$.value, { text: response, date: new Date().toISOString() }]); }); - this.#copilotService.publish({ - destination: '/app/rules-copilot', - body: JSON.stringify({ prompts: ['manageradmin'] }), - }); + this.#copilotService.send('manageradmin'); } set isLeavingPage(isLeaving: boolean) { @@ -136,11 +136,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv add(question: string) { console.log(question); - this.responses$.next([...this.responses$.value, { text: question, date: new Date().toISOString() }]); - this.#copilotService.publish({ - destination: '/app/rules-copilot', - body: JSON.stringify({ prompts: [question] }), - }); + this.#copilotService.send(question); } async ngOnInit() { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index c29f7fd53..f6faaf9fc 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -1,5 +1,5 @@ import { NgIf } from '@angular/common'; -import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, effect, inject, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core'; import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; @@ -90,7 +90,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni }) private readonly _filterTemplate: TemplateRef; #loadAllAnnotationsEnabled = false; - readonly #wsConnection$: Observable; + readonly #wsConnection$ = inject(WebSocketService) + .listen(WsTopics.ANALYSIS) + .pipe( + log('[WS] Analysis events'), + filter(event => event.analyseStatus === AnalyseStatuses.FINISHED), + switchMap(event => this._fileDataService.updateAnnotations(this.state.file(), event.analysisNumber)), + log('[WS] Annotations updated'), + ); #wsConnectionSub: Subscription; protected readonly isDocumine = getConfig().IS_DOCUMINE; readonly circleButtonTypes = CircleButtonTypes; @@ -135,7 +142,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _multiSelectService: MultiSelectService, private readonly _documentInfoService: DocumentInfoService, - private readonly _webSocketService: WebSocketService, ) { super(); effect(() => { @@ -151,13 +157,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } }); - this.#wsConnection$ = this._webSocketService.listen(WsTopics.ANALYSIS).pipe( - log('[WS] Analysis events'), - filter(event => event.analyseStatus === AnalyseStatuses.FINISHED), - switchMap(event => this._fileDataService.updateAnnotations(this.state.file(), event.analysisNumber)), - log('[CONNNEECCCCCTIIONSSS] Annotations updated'), - ); - const file = this.state.file(); console.log(file); console.log(this._fileDataService.annotations()); diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts index 95d6028c9..657544132 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts @@ -26,9 +26,7 @@ export default [ multi: true, useFactory: () => { const service = inject(WebSocketService); - return () => { - setTimeout(() => service.connect('/redaction-gateway-v1/websocket'), 2000); - }; + return () => service.connectAsync('/redaction-gateway-v1/websocket'); }, }, ], diff --git a/apps/red-ui/src/app/services/copilot.service.ts b/apps/red-ui/src/app/services/copilot.service.ts index 4b929105c..fdcf5d55f 100644 --- a/apps/red-ui/src/app/services/copilot.service.ts +++ b/apps/red-ui/src/app/services/copilot.service.ts @@ -1,11 +1,18 @@ import { Injectable } from '@angular/core'; -import { WebSocketService } from '@services/web-socket.service'; +import { StompService } from '@services/stomp.service'; @Injectable({ providedIn: 'root', }) -export class CopilotService extends WebSocketService { - get topicPrefix(): string { +export class CopilotService extends StompService { + override get topicPrefix(): string { return ''; } + + override send(message: string) { + this.publish({ + destination: '/app/rules-copilot', + body: JSON.stringify({ prompts: [message] }), + }); + } } diff --git a/apps/red-ui/src/app/services/stomp.service.ts b/apps/red-ui/src/app/services/stomp.service.ts new file mode 100644 index 000000000..a5b5b5d12 --- /dev/null +++ b/apps/red-ui/src/app/services/stomp.service.ts @@ -0,0 +1,74 @@ +import { inject, Injectable } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { KeycloakStatusService } from '@common-ui/tenants'; +import { log } from '@common-ui/utils'; +import { getConfig } from '@iqser/common-ui'; +import { IMessage, IWatchParams, RxStomp } from '@stomp/rx-stomp'; +import { StompHeaders } from '@stomp/stompjs'; + +import { NGXLogger } from 'ngx-logger'; +import { firstValueFrom, Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +@Injectable() +export class StompService extends RxStomp { + readonly #logger = inject(NGXLogger); + readonly #config = getConfig(); + readonly #keycloakStatusService = inject(KeycloakStatusService); + + constructor() { + super(); + this.connectionState$.pipe(log('[WS] Connection state'), takeUntilDestroyed()).subscribe(); + this.webSocketErrors$.pipe(log('[WS] Errors'), takeUntilDestroyed()).subscribe(); + this.stompErrors$.pipe(takeUntilDestroyed()).subscribe(frame => { + console.error(frame); + console.error('Broker reported error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + }); + this.#keycloakStatusService.token$.pipe(takeUntilDestroyed()).subscribe(token => { + this.#logger.info('[WS] Update connectHeaders'); + this.configure({ + connectHeaders: { Authorization: 'Bearer ' + token }, + }); + }); + } + + get topicPrefix() { + return ''; + } + + send(value: unknown) { + throw 'Not implemented'; + } + + override watch(opts: IWatchParams): Observable; + override watch(destination: string, headers?: StompHeaders): Observable; + override watch(opts: string | IWatchParams, headers?: StompHeaders): Observable { + if (typeof opts === 'string') { + return super.watch(this.topicPrefix + opts, headers); + } + + return super.watch(opts); + } + + listen(topic: string): Observable { + return this.watch(topic).pipe( + log('[WS] Listen to topic ' + topic), + tap(msg => console.log(msg.body)), + map(msg => JSON.parse(msg.body)), + ); + } + + connect(url: string) { + this.configure({ + debug: (msg: string) => this.#logger.debug(msg), + brokerURL: this.#config.API_URL + url, + }); + + this.activate(); + } + + connectAsync(url: string) { + return firstValueFrom(this.#keycloakStatusService.token$.pipe(map(() => this.connect(url)))); + } +} diff --git a/apps/red-ui/src/app/services/web-socket.service.ts b/apps/red-ui/src/app/services/web-socket.service.ts index 41b08684e..0b7c70f0d 100644 --- a/apps/red-ui/src/app/services/web-socket.service.ts +++ b/apps/red-ui/src/app/services/web-socket.service.ts @@ -1,72 +1,14 @@ -import { DestroyRef, inject, Injectable } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { inject, Injectable } from '@angular/core'; import { TenantsService } from '@common-ui/tenants'; -import { log } from '@common-ui/utils'; -import { getConfig } from '@iqser/common-ui'; -import { IMessage, IWatchParams, RxStomp } from '@stomp/rx-stomp'; -import { StompHeaders } from '@stomp/stompjs'; - -import { NGXLogger } from 'ngx-logger'; -import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { StompService } from '@services/stomp.service'; @Injectable({ providedIn: 'root', }) -export class WebSocketService extends RxStomp { - readonly #logger = inject(NGXLogger); - readonly #config = getConfig(); +export class WebSocketService extends StompService { readonly #tenantService = inject(TenantsService); - readonly #destroyRef = inject(DestroyRef); - constructor() { - super(); - } - - get topicPrefix() { + override get topicPrefix() { return '/topic/' + this.#tenantService.activeTenantId + '/'; } - - watch(opts: IWatchParams): Observable; - watch(destination: string, headers?: StompHeaders): Observable; - watch(opts: string | IWatchParams, headers?: StompHeaders): Observable { - if (typeof opts === 'string') { - return super.watch(this.topicPrefix + opts, headers); - } - - return super.watch(opts); - } - - listen(topic: string): Observable { - return this.watch(topic).pipe( - log('LISTEN LOG'), - tap(msg => console.log(msg.body)), - map(msg => JSON.parse(msg.body)), - ); - } - - connect(url: string) { - const headers = { Authorization: 'Bearer ' + localStorage.getItem('token') }; - console.log(headers); - this.configure({ - debug: (msg: string) => this.#logger.debug(msg), - brokerURL: this.#config.API_URL + url, - connectHeaders: headers, - }); - - this.connectionState$.pipe(log('[WS] Connection state')).subscribe(); - this.webSocketErrors$.pipe(log('[WS] Errors')).subscribe(); - this.stompErrors$ - .pipe( - tap(frame => { - console.error(frame); - console.error('Broker reported error: ' + frame.headers['message']); - console.error('Additional details: ' + frame.body); - }), - takeUntilDestroyed(this.#destroyRef), - ) - .subscribe(); - - this.activate(); - } } diff --git a/apps/red-ui/src/app/users/red-role.guard.ts b/apps/red-ui/src/app/users/red-role.guard.ts index ab0526c8e..d91daa5c5 100644 --- a/apps/red-ui/src/app/users/red-role.guard.ts +++ b/apps/red-ui/src/app/users/red-role.guard.ts @@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { IqserPermissionsService } from '@iqser/common-ui'; import { IqserRoleGuard } from '@iqser/common-ui/lib/users'; -import { UserService } from '@users/user.service'; import { NGXLogger } from 'ngx-logger'; @Injectable({ @@ -10,10 +9,9 @@ import { NGXLogger } from 'ngx-logger'; }) export class RedRoleGuard extends IqserRoleGuard { protected readonly _permissionsService = inject(IqserPermissionsService); - protected readonly _userService = inject(UserService); protected readonly _logger = inject(NGXLogger); - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + override async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { const currentUser = this._userService.currentUser; if (!currentUser?.hasAnyRole) { diff --git a/apps/red-ui/src/app/users/user.service.ts b/apps/red-ui/src/app/users/user.service.ts index aaf2a7074..5490603fd 100644 --- a/apps/red-ui/src/app/users/user.service.ts +++ b/apps/red-ui/src/app/users/user.service.ts @@ -1,10 +1,10 @@ import { inject, Injectable } from '@angular/core'; -import { User } from '@red/domain'; import { QueryParam } from '@iqser/common-ui'; -import { Roles } from '@users/roles'; -import { of } from 'rxjs'; import { IIqserUser, IqserUserService } from '@iqser/common-ui/lib/users'; import { List } from '@iqser/common-ui/lib/utils'; +import { User } from '@red/domain'; +import { Roles } from '@users/roles'; +import { of } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -13,7 +13,7 @@ export class UserService extends IqserUserService { protected readonly _defaultModelPath = 'user'; protected readonly _entityClass = User; - async loadCurrentUser(): Promise { + override async loadCurrentUser(): Promise { const currentUser = await super.loadCurrentUser(); this._permissionsService.add({ @@ -23,12 +23,12 @@ export class UserService extends IqserUserService { return currentUser; } - loadAll() { + override loadAll() { const canReadUsers = this._permissionsService.has(Roles.users.read); return canReadUsers ? super.loadAll() : of([]); } - getAll() { + override getAll() { const canReadAllUsers = this._permissionsService.has(Roles.users.readAll); const url = canReadAllUsers ? this._defaultModelPath : `${this._defaultModelPath}/red`; return super.getAll(url); @@ -39,8 +39,8 @@ export class UserService extends IqserUserService { return this._post(null, `${this._defaultModelPath}/profile/activate/${user.userId}`, queryParams); } - protected readonly _rolesFilter = (role: string) => role.startsWith('RED_'); - protected readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-'); + protected override readonly _rolesFilter = (role: string) => role.startsWith('RED_'); + protected override readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-'); } export function getCurrentUser() { diff --git a/libs/common-ui b/libs/common-ui index 17d2e8c53..ebaf1709b 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit 17d2e8c530700094d783ba912e9cb71c23621547 +Subproject commit ebaf1709b1138b6da5b329078362c6e34459e4a2 diff --git a/libs/red-domain/src/lib/trash/trash-dossier.model.ts b/libs/red-domain/src/lib/trash/trash-dossier.model.ts index c7a579f96..8213a3b45 100644 --- a/libs/red-domain/src/lib/trash/trash-dossier.model.ts +++ b/libs/red-domain/src/lib/trash/trash-dossier.model.ts @@ -19,9 +19,9 @@ export class TrashDossier extends TrashItem implements Partial { constructor( dossier: IDossier, - protected readonly _retentionHours: number, - readonly hasRestoreRights: boolean, - readonly hasHardDeleteRights: boolean, + protected override readonly _retentionHours: number, + override readonly hasRestoreRights: boolean, + override readonly hasHardDeleteRights: boolean, readonly ownerName: string, ) { super(_retentionHours, dossier.softDeletedTime || '-', hasRestoreRights, hasHardDeleteRights); diff --git a/libs/red-domain/src/lib/trash/trash-file.model.ts b/libs/red-domain/src/lib/trash/trash-file.model.ts index e12ea032f..e221ff205 100644 --- a/libs/red-domain/src/lib/trash/trash-file.model.ts +++ b/libs/red-domain/src/lib/trash/trash-file.model.ts @@ -1,6 +1,6 @@ -import { TrashItem } from './trash.item'; -import { File, IFile } from '../files'; import { FileAttributes } from '../file-attributes'; +import { File, IFile } from '../files'; +import { TrashItem } from './trash.item'; export class TrashFile extends TrashItem implements Partial { readonly type = 'file'; @@ -22,9 +22,9 @@ export class TrashFile extends TrashItem implements Partial { constructor( file: File, readonly dossierTemplateId: string, - protected readonly _retentionHours: number, - readonly hasRestoreRights: boolean, - readonly hasHardDeleteRights: boolean, + protected override readonly _retentionHours: number, + override readonly hasRestoreRights: boolean, + override readonly hasHardDeleteRights: boolean, readonly ownerName: string, readonly fileDossierName: string, ) { diff --git a/tsconfig.json b/tsconfig.json index e179555d9..c194700e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "useDefineForClassFields": false, "strictPropertyInitialization": false, "importHelpers": true, + // "noImplicitOverride": true, "target": "ES2022", "module": "ES2022", "typeRoots": ["node_modules/@types"],