Compare commits
2 Commits
master
...
RulesLogge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4435752ac | ||
|
|
df64a73adc |
@ -24,6 +24,7 @@ import { AuditService } from './services/audit.service';
|
||||
import { DigitalSignatureService } from './services/digital-signature.service';
|
||||
import { RulesService } from './services/rules.service';
|
||||
import { SmtpConfigService } from './services/smtp-config.service';
|
||||
import { RulesLoggerService } from '@services/rules-logger.service';
|
||||
|
||||
const entityRoutes: IqserRoutes = [
|
||||
{
|
||||
@ -95,6 +96,19 @@ const dossierTemplateIdRoutes: IqserRoutes = [
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: () => {
|
||||
const service = inject(RulesLoggerService);
|
||||
return () => {
|
||||
setTimeout(() => {
|
||||
service.connect('/api/rules-logging/rulesocket');
|
||||
console.log('Rules logger ready');
|
||||
}, 2000);
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,87 +1,127 @@
|
||||
<div class="header-container">
|
||||
<div [translate]="translations[type()]['title']" class="heading-l"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex" style="height: 100%">
|
||||
<ngx-monaco-editor (init)="onCodeEditorInit($event)" [(ngModel)]="codeEditorText" [options]="editorOptions"></ngx-monaco-editor>
|
||||
|
||||
<div [class.collapsed]="collapsed()" class="right-container flex-column">
|
||||
<div class="collapsed-wrapper">
|
||||
<ng-container *ngTemplateOutlet="collapsible; context: { action: 'expand', tooltip: ('copilot' | translate) }"></ng-container>
|
||||
<div class="all-caps-label" translate="dossier-details.title"></div>
|
||||
<div class="rules-screen-container">
|
||||
<!-- Main content area -->
|
||||
<div class="main-content">
|
||||
<div class="header-container">
|
||||
<div [translate]="translations[type()]['title']" class="heading-l"></div>
|
||||
</div>
|
||||
|
||||
<div class="header-wrapper flex mt-8">
|
||||
<div class="heading-xl flex-1" id="dossierDetailsDossierName">Copilot</div>
|
||||
<ng-container *ngTemplateOutlet="collapsible; context: { action: 'collapse', tooltip: ('copilot' | translate) }"></ng-container>
|
||||
</div>
|
||||
<div class="flex" style="height: 100%">
|
||||
<ngx-monaco-editor (init)="onCodeEditorInit($event)" [(ngModel)]="codeEditorText" [options]="editorOptions"></ngx-monaco-editor>
|
||||
|
||||
<div class="mt-24">
|
||||
@for (comment of responses$ | async; track comment) {
|
||||
<div class="comment">
|
||||
<div class="comment-details-wrapper">
|
||||
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
|
||||
{{ comment.date | date: 'sophisticatedDate' }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="comment-actions">-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<div>{{ comment.text }}</div>
|
||||
<div [class.collapsed]="collapsed()" class="right-container flex-column">
|
||||
<div class="collapsed-wrapper">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="collapsible; context: { action: 'expand', tooltip: ('copilot' | translate) }"
|
||||
></ng-container>
|
||||
<div class="all-caps-label" translate="dossier-details.title"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<iqser-input-with-action
|
||||
(action)="add($event)"
|
||||
[placeholder]="'comments.add-comment' | translate"
|
||||
autocomplete="off"
|
||||
icon="iqser:collapse"
|
||||
width="full"
|
||||
></iqser-input-with-action>
|
||||
<div class="header-wrapper flex mt-8">
|
||||
<div class="heading-xl flex-1" id="dossierDetailsDossierName">Copilot</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="collapsible; context: { action: 'collapse', tooltip: ('copilot' | translate) }"
|
||||
></ng-container>
|
||||
</div>
|
||||
|
||||
<div class="mt-24">
|
||||
@for (comment of responses$ | async; track comment) {
|
||||
<div class="comment">
|
||||
<div class="comment-details-wrapper">
|
||||
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
|
||||
{{ comment.date | date: 'sophisticatedDate' }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="comment-actions">-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<div>{{ comment.text }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<iqser-input-with-action
|
||||
(action)="add($event)"
|
||||
[placeholder]="'comments.add-comment' | translate"
|
||||
autocomplete="off"
|
||||
icon="iqser:collapse"
|
||||
width="full"
|
||||
></iqser-input-with-action>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #collapsible let-action="action" let-tooltip="tooltip">
|
||||
<iqser-circle-button
|
||||
(action)="collapsed.set(!collapsed())"
|
||||
[icon]="'iqser:' + action"
|
||||
[tooltipPosition]="IqserTooltipPositions.before"
|
||||
[tooltip]="tooltip"
|
||||
></iqser-circle-button>
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="changed && permissionsService.canEditRules() && !isLeaving" class="changes-box">
|
||||
<div (click)="goToErrors()" *ngIf="numberOfErrors() || numberOfWarnings()" class="errors">
|
||||
<span>
|
||||
<mat-icon *ngIf="numberOfErrors()" [svgIcon]="'iqser:alert-circle'" class="icon"></mat-icon>
|
||||
<div class="found-errors">
|
||||
<span
|
||||
*ngIf="numberOfErrors()"
|
||||
[translateParams]="{ errors: numberOfErrors() }"
|
||||
[translate]="translations[type()]['errors-found']"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
*ngIf="numberOfWarnings()"
|
||||
[class.only-warning]="!numberOfErrors()"
|
||||
[translateParams]="{ warnings: numberOfWarnings() }"
|
||||
[translate]="translations[type()]['warnings-found']"
|
||||
class="warning"
|
||||
></span>
|
||||
</div>
|
||||
</span>
|
||||
<mat-icon [svgIcon]="'iqser:expand'" class="icon face-up"></mat-icon>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<iqser-icon-button
|
||||
(action)="save()"
|
||||
[label]="translations[type()]['save-changes'] | translate"
|
||||
[type]="iconButtonTypes.primary"
|
||||
icon="iqser:check"
|
||||
></iqser-icon-button>
|
||||
|
||||
<div (click)="revert()" [translate]="translations[type()]['revert-changes']" class="all-caps-label cancel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rules-logger" [class.collapsed]="isLogCollapsed">
|
||||
<div class="logger-header" (click)="toggleLogCollapse()">
|
||||
<h3>Rules Log</h3>
|
||||
<button class="collapse-button">
|
||||
<!--todo: add proper icon -->
|
||||
<mat-icon>{{ isLogCollapsed ? 'x' : 'x' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="logger-messages" *ngIf="!isLogCollapsed">
|
||||
<div *ngFor="let msg of messages" class="log-entry">
|
||||
<div class="log-header">
|
||||
<span class="log-level">{{ msg.logLevel }}</span>
|
||||
<span class="log-time">{{ msg.parsedTimeStamp | date: 'short' }}</span>
|
||||
</div>
|
||||
<div class="log-message">
|
||||
{{ msg.message }}
|
||||
</div>
|
||||
<div class="log-details">
|
||||
<!--todo: add translation -->
|
||||
<div><strong>Tenant ID:</strong> {{ msg.tenantId }}</div>
|
||||
<div><strong>File ID:</strong> {{ msg.fileId }}</div>
|
||||
<div><strong>Dossier ID:</strong> {{ msg.dossierId }}</div>
|
||||
<div><strong>Rule Version:</strong> {{ msg.ruleVersion }}</div>
|
||||
<div><strong>Analysis Number:</strong> {{ msg.analysisNumber }}</div>
|
||||
<div><strong>Timestamp:</strong> {{ msg.parsedTimeStamp }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #collapsible let-action="action" let-tooltip="tooltip">
|
||||
<iqser-circle-button
|
||||
(action)="collapsed.set(!collapsed())"
|
||||
[icon]="'iqser:' + action"
|
||||
[tooltipPosition]="IqserTooltipPositions.before"
|
||||
[tooltip]="tooltip"
|
||||
></iqser-circle-button>
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="changed && permissionsService.canEditRules() && !isLeaving" class="changes-box">
|
||||
<div (click)="goToErrors()" *ngIf="numberOfErrors() || numberOfWarnings()" class="errors">
|
||||
<span>
|
||||
<mat-icon *ngIf="numberOfErrors()" [svgIcon]="'iqser:alert-circle'" class="icon"></mat-icon>
|
||||
<div class="found-errors">
|
||||
<span
|
||||
*ngIf="numberOfErrors()"
|
||||
[translateParams]="{ errors: numberOfErrors() }"
|
||||
[translate]="translations[type()]['errors-found']"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
*ngIf="numberOfWarnings()"
|
||||
[class.only-warning]="!numberOfErrors()"
|
||||
[translateParams]="{ warnings: numberOfWarnings() }"
|
||||
[translate]="translations[type()]['warnings-found']"
|
||||
class="warning"
|
||||
></span>
|
||||
</div>
|
||||
</span>
|
||||
<mat-icon [svgIcon]="'iqser:expand'" class="icon face-up"></mat-icon>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<iqser-icon-button
|
||||
(action)="save()"
|
||||
[label]="translations[type()]['save-changes'] | translate"
|
||||
[type]="iconButtonTypes.primary"
|
||||
icon="iqser:check"
|
||||
></iqser-icon-button>
|
||||
|
||||
<div (click)="revert()" [translate]="translations[type()]['revert-changes']" class="all-caps-label cancel"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -96,3 +96,94 @@ ngx-monaco-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rules-screen-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh; /* Adjust based on your layout */
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.rules-logger {
|
||||
flex-shrink: 0;
|
||||
transition: max-height 0.3s ease;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #ccc;
|
||||
background-color: #fafafa;
|
||||
|
||||
&.collapsed {
|
||||
max-height: 50px; /* Height of the header when collapsed */
|
||||
}
|
||||
|
||||
&:not(.collapsed) {
|
||||
max-height: 300px; /* Maximum height when expanded */
|
||||
}
|
||||
|
||||
.logger-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.collapse-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
mat-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logger-messages {
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 50px); /* Subtract header height */
|
||||
padding: 10px;
|
||||
|
||||
.log-entry {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.log-level {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.log-message {
|
||||
margin-bottom: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.log-details {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
|
||||
div {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,8 @@ import { rulesScreenTranslations } from '../../../translations/rules-screen-tran
|
||||
import ICodeEditor = monaco.editor.ICodeEditor;
|
||||
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
|
||||
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
|
||||
import { RulesLoggerService } from '@services/rules-logger.service';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
|
||||
interface SyntaxError {
|
||||
line: number;
|
||||
@ -38,6 +40,19 @@ interface UploadResponse {
|
||||
deprecatedWarnings: SyntaxError[];
|
||||
}
|
||||
|
||||
interface RulesLogMessage {
|
||||
tenantId: string;
|
||||
fileId: string;
|
||||
dossierId: string;
|
||||
dossierTemplateId: string;
|
||||
ruleVersion: number;
|
||||
analysisNumber: number;
|
||||
timeStamp: string;
|
||||
logLevel: string;
|
||||
message: string;
|
||||
parsedTimeStamp?: Date;
|
||||
}
|
||||
|
||||
const RULE_VALIDATION_TIMEOUT = 2000;
|
||||
|
||||
@Component({
|
||||
@ -60,6 +75,7 @@ const RULE_VALIDATION_TIMEOUT = 2000;
|
||||
NamePipe,
|
||||
NgForOf,
|
||||
MatTooltip,
|
||||
ScrollingModule,
|
||||
],
|
||||
})
|
||||
export default class RulesScreenComponent implements OnInit, ComponentCanDeactivate {
|
||||
@ -69,6 +85,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
|
||||
readonly #errors = signal<SyntaxError[]>([]);
|
||||
#ruleValidationTimeout: number = null;
|
||||
readonly #copilotService = inject(CopilotService);
|
||||
readonly #rulesLoggerService = inject(RulesLoggerService);
|
||||
readonly #currentUser = getCurrentUser();
|
||||
protected readonly collapsed = signal(true);
|
||||
protected readonly IqserTooltipPositions = IqserTooltipPositions;
|
||||
@ -86,10 +103,12 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
|
||||
initialLines: string[] = [];
|
||||
currentLines: string[] = [];
|
||||
isLeaving = false;
|
||||
messages: RulesLogMessage[] = [];
|
||||
readonly type = input.required<IRules['ruleFileType']>();
|
||||
readonly numberOfErrors = computed(() => this.#errors().filter(e => !e.warning).length);
|
||||
readonly numberOfWarnings = computed(() => this.#errors().filter(e => e.warning).length);
|
||||
readonly responses$ = new BehaviorSubject<{ text: string; date: string }[]>([]);
|
||||
isLogCollapsed = true;
|
||||
|
||||
constructor(
|
||||
readonly permissionsService: PermissionsService,
|
||||
@ -112,6 +131,15 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
|
||||
destination: '/app/rules-copilot',
|
||||
body: JSON.stringify({ prompts: ['manageradmin'] }),
|
||||
});
|
||||
|
||||
this.#rulesLoggerService
|
||||
.listen<RulesLogMessage>('/topic/' + tenant + '/rule-log-events')
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(message => {
|
||||
message.parsedTimeStamp = this.parseTimestamp(message.timeStamp);
|
||||
this.messages.push(message);
|
||||
this._changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
set isLeavingPage(isLeaving: boolean) {
|
||||
@ -350,4 +378,23 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
|
||||
() => this._loadingService.stop(),
|
||||
);
|
||||
}
|
||||
|
||||
parseTimestamp(timestamp: string): Date {
|
||||
let adjustedTimestamp = timestamp.replace('Z', '');
|
||||
|
||||
const dotIndex = adjustedTimestamp.indexOf('.');
|
||||
if (dotIndex !== -1) {
|
||||
adjustedTimestamp = adjustedTimestamp.substring(0, dotIndex);
|
||||
}
|
||||
|
||||
return new Date(adjustedTimestamp);
|
||||
}
|
||||
|
||||
trackByMessageId(index: number, msg: RulesLogMessage): string {
|
||||
return msg.timeStamp; // Assuming timeStamp is unique
|
||||
}
|
||||
|
||||
toggleLogCollapse() {
|
||||
this.isLogCollapsed = !this.isLogCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,13 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { List, OnChange } from '@iqser/common-ui/lib/utils';
|
||||
import { MonacoEditorModule, MonacoStandaloneCodeEditor, MonacoStandaloneDiffEditor } from '@materia-ui/ngx-monaco-editor';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgIf } from '@angular/common';
|
||||
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
|
||||
import IDiffEditor = monaco.editor.IDiffEditor;
|
||||
import ICodeEditor = monaco.editor.ICodeEditor;
|
||||
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
|
||||
import ILineChange = monaco.editor.ILineChange;
|
||||
import IEditorMouseEvent = monaco.editor.IEditorMouseEvent;
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
const MIN_WORD_LENGTH = 2;
|
||||
const lineChangeToDecoration = ({ originalEndLineNumber, originalStartLineNumber }: ILineChange) =>
|
||||
|
||||
11
apps/red-ui/src/app/services/rules-logger.service.ts
Normal file
11
apps/red-ui/src/app/services/rules-logger.service.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { WebSocketService } from '@services/web-socket.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RulesLoggerService extends WebSocketService {
|
||||
get topicPrefix(): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,8 @@ export class WebSocketService extends RxStomp {
|
||||
readonly #tenantService = inject(TenantsService);
|
||||
readonly #destroyRef = inject(DestroyRef);
|
||||
|
||||
url: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@ -46,16 +48,11 @@ export class WebSocketService extends RxStomp {
|
||||
}
|
||||
|
||||
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.url = url;
|
||||
this.configureConnection();
|
||||
|
||||
this.connectionState$.pipe(log('[WS] Connection state')).subscribe();
|
||||
this.webSocketErrors$.pipe(log('[WS] Errors')).subscribe();
|
||||
this.webSocketErrors$.pipe(log('[WS] Errors')).subscribe(frame => this.configureConnection());
|
||||
this.stompErrors$
|
||||
.pipe(
|
||||
tap(frame => {
|
||||
@ -69,4 +66,14 @@ export class WebSocketService extends RxStomp {
|
||||
|
||||
this.activate();
|
||||
}
|
||||
|
||||
private configureConnection() {
|
||||
const headers = { Authorization: 'Bearer ' + localStorage.getItem('token') };
|
||||
console.log(headers);
|
||||
this.configure({
|
||||
debug: (msg: string) => this.#logger.debug(msg),
|
||||
brokerURL: this.#config.API_URL + this.url,
|
||||
connectHeaders: headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"ADMIN_CONTACT_NAME": null,
|
||||
"ADMIN_CONTACT_URL": null,
|
||||
"API_URL": "https://dan1.iqser.cloud",
|
||||
"API_URL": "https://dom1.iqser.cloud",
|
||||
"APP_NAME": "RedactManager",
|
||||
"IS_DOCUMINE": false,
|
||||
"RULE_EDITOR_DEV_ONLY": false,
|
||||
@ -13,7 +13,7 @@
|
||||
"MAX_RETRIES_ON_SERVER_ERROR": 3,
|
||||
"OAUTH_CLIENT_ID": "redaction",
|
||||
"OAUTH_IDP_HINT": null,
|
||||
"OAUTH_URL": "https://dan1.iqser.cloud/auth",
|
||||
"OAUTH_URL": "https://dom1.iqser.cloud/auth",
|
||||
"RECENT_PERIOD_IN_HOURS": 24,
|
||||
"SELECTION_MODE": "structural",
|
||||
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user