diff --git a/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.html b/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.html index 496a84dd3..fe6f31a46 100644 --- a/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.html +++ b/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.html @@ -89,7 +89,7 @@
- +
{{ log.message }} diff --git a/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.ts b/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.ts index 0d0dbfae7..6c0daa0ab 100644 --- a/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.ts +++ b/apps/red-ui/src/app/screens/admin/audit-screen/audit-screen.component.ts @@ -5,7 +5,7 @@ import { AuditControllerService, AuditResponse, AuditSearchRequest } from '@reda import { TranslateService } from '@ngx-translate/core'; import { Moment } from 'moment'; -const PAGE_SIZE = 5; +const PAGE_SIZE = 50; @Component({ selector: 'redaction-audit-screen', diff --git a/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.html b/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.html index 01f33794d..dc0f181d7 100644 --- a/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.html +++ b/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.html @@ -53,7 +53,7 @@
-
+
- - - - -
+
+
+ {{ 'dictionary-overview.compare.compare' | translate }} +
+
+ + + {{ ruleSet === SELECT_RULESET ? (ruleSet.name | translate) : ruleSet.name }} + + +
+
+ + + {{ dictionary === SELECT_DICTIONARY ? (dictionary.label | translate) : dictionary.label }} + + +
+
@@ -93,13 +107,26 @@ [readOnly]="!permissionsService.isAdmin()" (textChanged)="textChanged($event)" [autoUpdateContent]="true" - [text]="dictionaryEntriesAsText" + class="ace-redaction" + > + +
+ + +
+
-
+
*:not(:first-child:last-child) { + width: 50%; + } + + > *:not(:last-child) { + border-radius: 8px 0 0 8px; + } + + > *:not(:first-child) { + border-radius: 0 8px 8px 0; + border-left: none; + } } .left-container { @@ -84,3 +98,40 @@ margin-right: 8px; } } + +.compare-form { + display: flex; + flex: 1; + justify-content: flex-end; + align-items: center; + .red-input-group { + margin-top: 0; + } +} + +.no-dictionary-selected { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 0 100px; + box-sizing: border-box; + text-align: center; + border: 1px solid $grey-5; + + > mat-icon { + height: 60px; + width: 60px; + opacity: 0.1; + margin-bottom: 24px; + } + + .heading-l { + color: $grey-7; + } +} + +.w-200 { + width: 200px; + max-width: 200px; +} diff --git a/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.ts b/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.ts index e36ab7b20..53288c6b3 100644 --- a/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.ts +++ b/apps/red-ui/src/app/screens/admin/dictionary-overview-screen/dictionary-overview-screen.component.ts @@ -1,5 +1,5 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { DictionaryControllerService, RuleSetModel, TypeValue } from '@redaction/red-ui-http'; import { DialogService } from '../../../dialogs/dialog.service'; import { AppStateService } from '../../../state/app-state.service'; import { PermissionsService } from '../../../common/service/permissions.service'; @@ -11,6 +11,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { saveAs } from 'file-saver'; import { ComponentHasChanges } from '../../../utils/can-deactivate.guard'; +import { FormBuilder, FormGroup } from '@angular/forms'; declare var ace; @@ -19,31 +20,30 @@ declare var ace; templateUrl: './dictionary-overview-screen.component.html', styleUrls: ['./dictionary-overview-screen.component.scss'] }) -export class DictionaryOverviewScreenComponent extends ComponentHasChanges { +export class DictionaryOverviewScreenComponent extends ComponentHasChanges implements OnInit { static readonly MIN_WORD_LENGTH: number = 2; - public compareActive = false; - - @ViewChild('editorComponent') - editorComponent: AceEditorComponent; activeEditMarkers: any[] = []; - activeSearchMarkers: any[] = []; - searchPositions: any[] = []; currentMatch = 1; - initialDictionaryEntries: string[] = []; currentDictionaryEntries: string[] = []; + compareDictionaryEntries: string[] = []; changedLines: number[] = []; - dictionaryEntriesAsText: string; aceOptions = { showPrintMargin: false }; searchText = ''; - processing = true; - @ViewChild('fileInput') - private _fileInput: ElementRef; + public SELECT_RULESET = { name: 'dictionary-overview.compare.select-ruleset' }; + public SELECT_DICTIONARY = { label: 'dictionary-overview.compare.select-dictionary' }; + public ruleSets: RuleSetModel[]; + public dictionaries: TypeValue[] = [this.SELECT_DICTIONARY]; + public compareForm: FormGroup; + + @ViewChild('editorComponent', { static: true }) private _editorComponent: AceEditorComponent; + @ViewChild('compareEditorComponent') private _compareEditorComponent: AceEditorComponent; + @ViewChild('fileInput') private _fileInput: ElementRef; constructor( public readonly permissionsService: PermissionsService, @@ -53,38 +53,73 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { private readonly _dialogService: DialogService, private readonly _router: Router, private readonly _activatedRoute: ActivatedRoute, - private readonly _appStateService: AppStateService + private readonly _appStateService: AppStateService, + private readonly _formBuilder: FormBuilder ) { super(_translateService); this._appStateService.activateDictionary(this._activatedRoute.snapshot.params.type, this._activatedRoute.snapshot.params.ruleSetId); - this._initialize(); - } - private _initialize() { - if (this.dictionary.type) - this._dictionaryControllerService.getDictionaryForType(this.dictionary.type, this.dictionary.ruleSetId).subscribe( - (data) => { - this.initialDictionaryEntries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); - this.revert(); - }, - () => { - this.processing = false; - } - ); + this.compareForm = this._formBuilder.group({ + active: [false], + ruleSet: [{ value: this.SELECT_RULESET, disabled: true }], + dictionary: [{ value: this.SELECT_DICTIONARY, disabled: true }] + }); + + this.compareForm.valueChanges.subscribe((value) => { + this._setFieldStatus('ruleSet', value.active); + this._setFieldStatus('dictionary', value.active && this.compareForm.get('ruleSet').value !== this.SELECT_RULESET); + this._loadDictionaries(); + }); + + this.ruleSets = [this.SELECT_RULESET, ...this._appStateService.ruleSets]; + + this._initializeEditor(); + + this.compareForm.controls.ruleSet.valueChanges.subscribe(() => { + this._onRuleSetChanged(); + }); + + this.compareForm.controls.dictionary.valueChanges.subscribe((dictionary) => { + this._onDictionaryChanged(dictionary); + }); } public get dictionary(): TypeValue { return this._appStateService.activeDictionary; } - openEditDictionaryDialog($event: any) { + public get hasChanges() { + return ( + this.currentDictionaryEntries.length && + (this.activeEditMarkers.length > 0 || + this.currentDictionaryEntries.filter((e) => e && e.trim().length > 0).length < this.initialDictionaryEntries.length) + ); + } + + private get _activeRow(): number { + return this._editorComponent.getEditor().selection.getCursor().row + 1; + } + + private static _setEditorValue(editor: AceEditorComponent, entries: string[]) { + const dictionaryEntriesAsText = entries.join('\n'); + editor.getEditor().setValue(dictionaryEntriesAsText); + editor.getEditor().gotoLine(1); + } + + ngOnInit(): void { + this._editorComponent.getEditor().selection.on('changeCursor', () => { + this._syncActiveLines(); + }); + } + + public openEditDictionaryDialog($event: any) { $event.stopPropagation(); this._dialogService.openAddEditDictionaryDialog(this.dictionary, this.dictionary.ruleSetId, async () => { await this._appStateService.loadDictionaryData(); }); } - openDeleteDictionaryDialog($event: any) { + public openDeleteDictionaryDialog($event: any) { this._dialogService.openDeleteDictionaryDialog($event, this.dictionary, this.dictionary.ruleSetId, async () => { await this._appStateService.loadDictionaryData(); this._router.navigate(['..']); @@ -92,50 +127,20 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { } @debounce() - searchChanged(text: string) { + public searchChanged(text: string) { this.searchText = text.toLowerCase(); this._applySearchMarkers(); this.currentMatch = 0; this.nextSearchMatch(); } - private _applySearchMarkers() { - this.searchPositions = this._getSearchPositions(); - this.activeSearchMarkers.forEach((am) => { - this.editorComponent.getEditor().getSession().removeMarker(am); - }); - this.activeSearchMarkers = []; - - const Range = ace.require('ace/range').Range; - for (const position of this.searchPositions) { - this.activeSearchMarkers.push( - this.editorComponent - .getEditor() - .getSession() - .addMarker(new Range(position.row, position.column, position.row, position.column + position.length), 'search-marker', 'text') - ); - } - } - - private _getSearchPositions() { - const lowerCaseSearchText = this.searchText.toLowerCase(); - return this.currentDictionaryEntries - .map((val, index) => { - const columnIndex = val.toLowerCase().indexOf(lowerCaseSearchText); - if (columnIndex >= 0) { - return { row: index, column: columnIndex, length: lowerCaseSearchText.length }; - } - }) - .filter((entry) => !!entry); - } - @debounce(500) - textChanged($event: any) { + public textChanged($event: any) { this._applySearchMarkers(); this.currentDictionaryEntries = $event.split('\n'); this.changedLines = []; this.activeEditMarkers.forEach((am) => { - this.editorComponent.getEditor().getSession().removeMarker(am); + this._editorComponent.getEditor().getSession().removeMarker(am); }); this.activeEditMarkers = []; @@ -151,23 +156,16 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { const entry = this.currentDictionaryEntries[i]; if (entry?.trim().length > 0) { // only mark non-empty lines - this.activeEditMarkers.push(this.editorComponent.getEditor().getSession().addMarker(new Range(i, 0, i, 1), 'changed-row-marker', 'fullLine')); + this.activeEditMarkers.push(this._editorComponent.getEditor().getSession().addMarker(new Range(i, 0, i, 1), 'changed-row-marker', 'fullLine')); } if (entry?.trim().length > 0 && entry.trim().length < DictionaryOverviewScreenComponent.MIN_WORD_LENGTH) { // show lines that are too short - this.activeEditMarkers.push(this.editorComponent.getEditor().getSession().addMarker(new Range(i, 0, i, 1), 'too-short-marker', 'fullLine')); + this.activeEditMarkers.push(this._editorComponent.getEditor().getSession().addMarker(new Range(i, 0, i, 1), 'too-short-marker', 'fullLine')); } } } - get hasChanges() { - return ( - this.activeEditMarkers.length > 0 || - this.currentDictionaryEntries.filter((e) => e && e.trim().length > 0).length < this.initialDictionaryEntries.length - ); - } - - async saveEntries() { + public async saveEntries() { let entriesToAdd = []; this.currentDictionaryEntries.forEach((currentEntry) => { entriesToAdd.push(currentEntry); @@ -187,7 +185,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { obs.subscribe( () => { - this._initialize(); + this._initializeEditor(); this._notificationService.showToastNotification( this._translateService.instant('dictionary-overview.success.generic'), null, @@ -210,19 +208,15 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { NotificationType.ERROR ); } - - // .ace_marker-layer .search-marker } - revert() { - this.dictionaryEntriesAsText = this.initialDictionaryEntries.join('\n'); - this.editorComponent.getEditor().setValue(this.dictionaryEntriesAsText); - this.editorComponent.getEditor().clearSelection(); + public revert() { + DictionaryOverviewScreenComponent._setEditorValue(this._editorComponent, this.initialDictionaryEntries); this.searchChanged(''); this.processing = false; } - nextSearchMatch() { + public nextSearchMatch() { // length = 3 if (this.searchPositions.length > 0) { this.currentMatch = this.currentMatch < this.searchPositions.length ? this.currentMatch + 1 : 1; @@ -230,21 +224,15 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { } } - previousSearchMatch() { + public previousSearchMatch() { if (this.searchPositions.length > 0) { this.currentMatch = this.currentMatch > 1 ? this.currentMatch - 1 : this.searchPositions.length; this._gotoLine(); } } - private _gotoLine() { - const position = this.searchPositions[this.currentMatch - 1]; - this.editorComponent.getEditor().scrollToLine(position.row, true, true, () => {}); - this.editorComponent.getEditor().gotoLine(position.row + 1, position.column, true); - } - public download(): void { - const content = this.editorComponent.getEditor().getValue(); + const content = this._editorComponent.getEditor().getValue(); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); @@ -257,10 +245,104 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges { if (file) { fileReader.onload = () => { - this.editorComponent.getEditor().setValue(fileReader.result); + this._editorComponent.getEditor().setValue(fileReader.result); this._fileInput.nativeElement.value = null; }; fileReader.readAsText(file); } } + + private _syncActiveLines() { + if (!!this._compareEditorComponent) { + this._compareEditorComponent.getEditor().gotoLine(this._activeRow); + } + } + + private _onRuleSetChanged() { + this._loadDictionaries(); + this.compareForm.patchValue({ dictionary: this.SELECT_DICTIONARY }); + } + + private _onDictionaryChanged(dictionary: TypeValue) { + if (dictionary !== this.SELECT_DICTIONARY) { + this._dictionaryControllerService.getDictionaryForType(dictionary.type, dictionary.ruleSetId).subscribe( + (data) => { + this.compareDictionaryEntries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); + DictionaryOverviewScreenComponent._setEditorValue(this._compareEditorComponent, this.compareDictionaryEntries); + this._syncActiveLines(); + }, + () => { + this.processing = false; + } + ); + } + } + + private _setFieldStatus(field: 'ruleSet' | 'dictionary', enabled: boolean) { + this.compareForm.get(field)[enabled ? 'enable' : 'disable']({ emitEvent: false }); + } + + private _loadDictionaries() { + const ruleSetId = this.compareForm.get('ruleSet').value.ruleSetId; + if (!ruleSetId) { + this.dictionaries = [this.SELECT_DICTIONARY]; + return; + } + const appStateDictionaryData = this._appStateService.dictionaryData[ruleSetId]; + this.dictionaries = [ + this.SELECT_DICTIONARY, + ...Object.keys(appStateDictionaryData) + .map((key) => appStateDictionaryData[key]) + .filter((d) => !d.virtual || d.type === 'false_positive') + ]; + } + + private _initializeEditor() { + if (this.dictionary.type) + this._dictionaryControllerService.getDictionaryForType(this.dictionary.type, this.dictionary.ruleSetId).subscribe( + (data) => { + this.initialDictionaryEntries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); + this.revert(); + }, + () => { + this.processing = false; + } + ); + } + + private _applySearchMarkers() { + this.searchPositions = this._getSearchPositions(); + this.activeSearchMarkers.forEach((am) => { + this._editorComponent.getEditor().getSession().removeMarker(am); + }); + this.activeSearchMarkers = []; + + const Range = ace.require('ace/range').Range; + for (const position of this.searchPositions) { + this.activeSearchMarkers.push( + this._editorComponent + .getEditor() + .getSession() + .addMarker(new Range(position.row, position.column, position.row, position.column + position.length), 'search-marker', 'text') + ); + } + } + + private _getSearchPositions() { + const lowerCaseSearchText = this.searchText.toLowerCase(); + return this.currentDictionaryEntries + .map((val, index) => { + const columnIndex = val.toLowerCase().indexOf(lowerCaseSearchText); + if (columnIndex >= 0) { + return { row: index, column: columnIndex, length: lowerCaseSearchText.length }; + } + }) + .filter((entry) => !!entry); + } + + private _gotoLine() { + const position = this.searchPositions[this.currentMatch - 1]; + this._editorComponent.getEditor().scrollToLine(position.row, true, true, () => {}); + this._editorComponent.getEditor().gotoLine(position.row + 1, position.column, true); + } } diff --git a/apps/red-ui/src/app/screens/downloads-list-screen/downloads-list-screen.component.html b/apps/red-ui/src/app/screens/downloads-list-screen/downloads-list-screen.component.html index 823a8cd38..a5447c1df 100644 --- a/apps/red-ui/src/app/screens/downloads-list-screen/downloads-list-screen.component.html +++ b/apps/red-ui/src/app/screens/downloads-list-screen/downloads-list-screen.component.html @@ -1,4 +1,3 @@ -\