diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html index e725780fa..172710bbd 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html @@ -55,86 +55,11 @@ -
-
-
- - -
-
- -
-
-
- {{ currentMatch + '/' + searchPositions.length }} -
- - - -
-
-
-
-
- {{ 'dictionary-overview.compare.compare' | translate }} -
-
- - - {{ ruleSet === selectRuleSet ? (ruleSet.name | translate) : ruleSet.name }} - - -
-
- - - {{ dictionary === selectDictionary ? (dictionary.label | translate) : dictionary.label }} - - -
-
-
- -
- - -
- - -
- - -
- -
- -
-
-
+
@@ -146,7 +71,7 @@
- {{ initialDictionaryEntries?.length }} + {{ dictionaryManager.initialDictionaryEntries?.length }}
diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.scss index 242fa4bdc..4d87564aa 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.scss @@ -1,74 +1,6 @@ @import '../../../../../assets/styles/red-variables'; @import '../../../../../assets/styles/red-mixins'; -.editor-container { - height: calc(100% - 50px); - display: flex; - - > *: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; - } -} - -.content-container { - padding: 15px; - - .actions-bar { - display: flex; - align-items: center; - margin-bottom: 16px; - - .mr-32 { - margin-right: 32px; - } - - .mr-16 { - margin-right: 16px; - //opacity: 0; // TODO: Hidden for now - } - - .red-input-group { - input { - padding-right: 32px; - - &.with-matches { - padding-right: 108px; - } - } - - .input-icons { - position: absolute; - right: 12px; - top: 8px; - color: $grey-1; - - .with-input { - display: flex; - justify-content: center; - align-items: center; - - .search-match-text { - } - } - - mat-icon { - max-width: 14px; - margin-left: 8px; - } - } - } - } -} - .right-container { width: 353px; min-width: 353px; @@ -96,36 +28,3 @@ 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; - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts index 53200eafb..13e9a81a1 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts @@ -1,19 +1,17 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { DictionaryControllerService, RuleSetModel, TypeValue } from '@redaction/red-ui-http'; +import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http'; import { AppStateService } from '@state/app-state.service'; import { PermissionsService } from '@services/permissions.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { AceEditorComponent } from 'ng2-ace-editor'; -import { debounce } from '@utils/debounce'; import { NotificationService, NotificationType } from '@services/notification.service'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { saveAs } from 'file-saver'; import { ComponentHasChanges } from '@guards/can-deactivate.guard'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder } from '@angular/forms'; import { AdminDialogService } from '../../services/admin-dialog.service'; +import { DictionaryManagerComponent } from '../../../shared/components/dictionary-manager/dictionary-manager.component'; -declare let ace; const MIN_WORD_LENGTH = 2; @Component({ @@ -22,26 +20,10 @@ const MIN_WORD_LENGTH = 2; styleUrls: ['./dictionary-overview-screen.component.scss'] }) export class DictionaryOverviewScreenComponent extends ComponentHasChanges implements OnInit { - activeEditMarkers: any[] = []; - activeSearchMarkers: any[] = []; - searchPositions: any[] = []; - currentMatch = 1; - initialDictionaryEntries: string[] = []; - currentDictionaryEntries: string[] = []; - compareDictionaryEntries: string[] = []; - changedLines: number[] = []; - aceOptions = { showPrintMargin: false }; - searchText = ''; - processing = true; + processing = false; + entries: string[] = []; - selectRuleSet = { name: 'dictionary-overview.compare.select-ruleset' }; - selectDictionary = { label: 'dictionary-overview.compare.select-dictionary' }; - ruleSets: RuleSetModel[]; - dictionaries: TypeValue[] = [this.selectDictionary]; - compareForm: FormGroup; - - @ViewChild('editorComponent', { static: true }) private _editorComponent: AceEditorComponent; - @ViewChild('compareEditorComponent') private _compareEditorComponent: AceEditorComponent; + @ViewChild('dictionaryManager', { static: false }) private _dictionaryManager: DictionaryManagerComponent; @ViewChild('fileInput') private _fileInput: ElementRef; constructor( @@ -57,30 +39,24 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple ) { super(_translateService); this._appStateService.activateDictionary(this._activatedRoute.snapshot.params.type, this._activatedRoute.snapshot.params.ruleSetId); + } - this.compareForm = this._formBuilder.group({ - active: [false], - ruleSet: [{ value: this.selectRuleSet, disabled: true }], - dictionary: [{ value: this.selectDictionary, disabled: true }] - }); + ngOnInit(): void { + this._loadEntries(); + } - this.compareForm.valueChanges.subscribe((value) => { - this._setFieldStatus('ruleSet', value.active); - this._setFieldStatus('dictionary', value.active && this.compareForm.get('ruleSet').value !== this.selectRuleSet); - this._loadDictionaries(); - }); - - this.ruleSets = [this.selectRuleSet, ...this._appStateService.ruleSets]; - - this._initializeEditor(); - - this.compareForm.controls.ruleSet.valueChanges.subscribe(() => { - this._onRuleSetChanged(); - }); - - this.compareForm.controls.dictionary.valueChanges.subscribe((dictionary) => { - this._onDictionaryChanged(dictionary); - }); + private _loadEntries() { + this.processing = true; + this._dictionaryControllerService.getDictionaryForType(this.dictionary.ruleSetId, this.dictionary.type).subscribe( + (data) => { + this.processing = false; + this.entries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); + }, + () => { + this.processing = false; + this.entries = []; + } + ); } get dictionary(): TypeValue { @@ -88,27 +64,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple } 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(); - }); + return this._dictionaryManager.hasChanges; } openEditDictionaryDialog($event: any) { @@ -125,48 +81,9 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple }); } - @debounce() - searchChanged(text: string) { - this.searchText = text.toLowerCase(); - this._applySearchMarkers(); - this.currentMatch = 0; - this.nextSearchMatch(); - } - - @debounce(500) - textChanged($event: any) { - this._applySearchMarkers(); - this.currentDictionaryEntries = $event.split('\n'); - this.changedLines = []; - this.activeEditMarkers.forEach((am) => { - this._editorComponent.getEditor().getSession().removeMarker(am); - }); - this.activeEditMarkers = []; - - for (let i = 0; i < this.currentDictionaryEntries.length; i++) { - const currentEntry = this.currentDictionaryEntries[i]; - if (this.initialDictionaryEntries.indexOf(currentEntry) < 0) { - this.changedLines.push(i); - } - } - - const range = ace.require('ace/range').Range; - for (const i of this.changedLines) { - 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')); - } - if (entry?.trim().length > 0 && entry.trim().length < 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')); - } - } - } - - async saveEntries() { + async saveEntries(entries: string[]) { let entriesToAdd = []; - this.currentDictionaryEntries.forEach((currentEntry) => { + entries.forEach((currentEntry) => { entriesToAdd.push(currentEntry); }); // remove empty lines @@ -177,14 +94,15 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple this.processing = true; let obs: Observable; if (entriesToAdd.length > 0) { - obs = this._dictionaryControllerService.addEntry(entriesToAdd, this.dictionary.type, this.dictionary.ruleSetId, null, true); + obs = this._dictionaryControllerService.addEntry(entriesToAdd, this.dictionary.ruleSetId, this.dictionary.type, null, true); } else { - obs = this._dictionaryControllerService.deleteEntries(this.initialDictionaryEntries, this.dictionary.type, this.dictionary.ruleSetId); + obs = this._dictionaryControllerService.deleteEntries(this.entries, this.dictionary.ruleSetId, this.dictionary.type); } obs.subscribe( () => { - this._initializeEditor(); + // TODO + this._loadEntries(); this._notificationService.showToastNotification( this._translateService.instant('dictionary-overview.success.generic'), null, @@ -209,29 +127,8 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple } } - revert() { - DictionaryOverviewScreenComponent._setEditorValue(this._editorComponent, this.initialDictionaryEntries); - this.searchChanged(''); - this.processing = false; - } - - nextSearchMatch() { - // length = 3 - if (this.searchPositions.length > 0) { - this.currentMatch = this.currentMatch < this.searchPositions.length ? this.currentMatch + 1 : 1; - this._gotoLine(); - } - } - - previousSearchMatch() { - if (this.searchPositions.length > 0) { - this.currentMatch = this.currentMatch > 1 ? this.currentMatch - 1 : this.searchPositions.length; - this._gotoLine(); - } - } - download(): void { - const content = this._editorComponent.getEditor().getValue(); + const content = this._dictionaryManager.editorValue; const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); @@ -244,106 +141,10 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple if (file) { fileReader.onload = () => { - this._editorComponent.getEditor().setValue(fileReader.result); + this._dictionaryManager.editorValue = 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.selectDictionary }); - } - - private _onDictionaryChanged(dictionary: TypeValue) { - if (dictionary !== this.selectDictionary) { - this._dictionaryControllerService.getDictionaryForType(dictionary.ruleSetId, dictionary.type).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.selectDictionary]; - return; - } - const appStateDictionaryData = this._appStateService.dictionaryData[ruleSetId]; - this.dictionaries = [ - this.selectDictionary, - ...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.ruleSetId, this.dictionary.type).subscribe( - (data) => { - this.initialDictionaryEntries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); - this.revert(); - }, - () => { - this.initialDictionaryEntries = []; - 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/modules/shared/components/dictionary-manager/dictionary-manager.component.html b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html new file mode 100644 index 000000000..7a2d7513b --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html @@ -0,0 +1,80 @@ +
+
+
+ + +
+
+ +
+
+
+ {{ currentMatch + '/' + searchPositions.length }} +
+ + + +
+
+
+
+
+ {{ 'dictionary-overview.compare.compare' | translate }} +
+
+ + + {{ ruleSet === selectRuleSet ? (ruleSet.name | translate) : ruleSet.name }} + + +
+
+ + + {{ dictionary === selectDictionary ? (dictionary.label | translate) : dictionary.label }} + + +
+
+
+ +
+ + +
+ + +
+ + +
+ +
+ +
+
+
diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.scss b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.scss new file mode 100644 index 000000000..7a084f902 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.scss @@ -0,0 +1,110 @@ +@import '../../../../../assets/styles/red-variables'; +@import '../../../../../assets/styles/red-mixins'; + +:host { + width: 100%; + height: 100%; +} + +.compare-form { + display: flex; + flex: 1; + justify-content: flex-end; + align-items: center; + + .red-input-group { + margin-top: 0; + } +} + +.editor-container { + height: calc(100% - 50px); + display: flex; + + > *: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; + } +} + +.content-container { + padding: 15px; + height: calc(100% - 30px); + width: calc(100% - 30px); + + .actions-bar { + display: flex; + align-items: center; + margin-bottom: 16px; + + .mr-32 { + margin-right: 32px; + } + + .mr-16 { + margin-right: 16px; + //opacity: 0; // TODO: Hidden for now + } + + .red-input-group { + input { + padding-right: 32px; + + &.with-matches { + padding-right: 108px; + } + } + + .input-icons { + position: absolute; + right: 12px; + top: 8px; + color: $grey-1; + + .with-input { + display: flex; + justify-content: center; + align-items: center; + + .search-match-text { + } + } + + mat-icon { + max-width: 14px; + margin-left: 8px; + } + } + } + } +} + +.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; + } +} diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts new file mode 100644 index 000000000..70a1aadaf --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts @@ -0,0 +1,258 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; +import { DictionaryControllerService, RuleSetModel, TypeValue } from '@redaction/red-ui-http'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { AceEditorComponent } from 'ng2-ace-editor'; +import { PermissionsService } from '@services/permissions.service'; +import { NotificationService } from '@services/notification.service'; +import { TranslateService } from '@ngx-translate/core'; +import { AdminDialogService } from '../../../admin/services/admin-dialog.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AppStateService } from '@state/app-state.service'; +import { debounce } from '@utils/debounce'; + +declare let ace; +const MIN_WORD_LENGTH = 2; + +@Component({ + selector: 'redaction-dictionary-manager', + templateUrl: './dictionary-manager.component.html', + styleUrls: ['./dictionary-manager.component.scss'] +}) +export class DictionaryManagerComponent implements OnInit, OnChanges { + @Input() + initialDictionaryEntries: string[]; + + @Output() + saveDictionary = new EventEmitter(); + + activeEditMarkers: any[] = []; + activeSearchMarkers: any[] = []; + searchPositions: any[] = []; + currentMatch = 1; + currentDictionaryEntries: string[] = []; + compareDictionaryEntries: string[] = []; + changedLines: number[] = []; + aceOptions = { showPrintMargin: false }; + searchText = ''; + + selectRuleSet = { name: 'dictionary-overview.compare.select-ruleset' }; + selectDictionary = { label: 'dictionary-overview.compare.select-dictionary' }; + ruleSets: RuleSetModel[]; + dictionaries: TypeValue[] = [this.selectDictionary]; + compareForm: FormGroup; + + @ViewChild('editorComponent', { static: true }) private _editorComponent: AceEditorComponent; + @ViewChild('compareEditorComponent') private _compareEditorComponent: AceEditorComponent; + + constructor( + readonly permissionsService: PermissionsService, + private readonly _notificationService: NotificationService, + protected readonly _translateService: TranslateService, + private readonly _dictionaryControllerService: DictionaryControllerService, + private readonly _dialogService: AdminDialogService, + private readonly _router: Router, + private readonly _activatedRoute: ActivatedRoute, + private readonly _appStateService: AppStateService, + private readonly _formBuilder: FormBuilder + ) { + this.compareForm = this._formBuilder.group({ + active: [false], + ruleSet: [{ value: this.selectRuleSet, disabled: true }], + dictionary: [{ value: this.selectDictionary, disabled: true }] + }); + + this.compareForm.valueChanges.subscribe((value) => { + this._setFieldStatus('ruleSet', value.active); + this._setFieldStatus('dictionary', value.active && this.compareForm.get('ruleSet').value !== this.selectRuleSet); + this._loadDictionaries(); + }); + + this.ruleSets = [this.selectRuleSet, ...this._appStateService.ruleSets]; + + this.compareForm.controls.ruleSet.valueChanges.subscribe(() => { + this._onRuleSetChanged(); + }); + + this.compareForm.controls.dictionary.valueChanges.subscribe((dictionary) => { + this._onDictionaryChanged(dictionary); + }); + } + + private static _setEditorValue(editor: AceEditorComponent, entries: string[]) { + const dictionaryEntriesAsText = entries.join('\n'); + editor.getEditor().setValue(dictionaryEntriesAsText); + editor.getEditor().gotoLine(1); + } + + get editorValue() { + return this._editorComponent.getEditor().getValue(); + } + + set editorValue(value: any) { + this._editorComponent.getEditor().setValue(value); + } + + ngOnInit(): void { + this._editorComponent.getEditor().selection.on('changeCursor', () => { + this._syncActiveLines(); + }); + } + + revert() { + DictionaryManagerComponent._setEditorValue(this._editorComponent, this.initialDictionaryEntries); + this.searchChanged(''); + } + + @debounce() + searchChanged(text: string) { + this.searchText = text.toLowerCase(); + this._applySearchMarkers(); + this.currentMatch = 0; + this.nextSearchMatch(); + } + + @debounce(500) + textChanged($event: any) { + this._applySearchMarkers(); + this.currentDictionaryEntries = $event.split('\n'); + this.changedLines = []; + this.activeEditMarkers.forEach((am) => { + this._editorComponent.getEditor().getSession().removeMarker(am); + }); + this.activeEditMarkers = []; + + for (let i = 0; i < this.currentDictionaryEntries.length; i++) { + const currentEntry = this.currentDictionaryEntries[i]; + if (this.initialDictionaryEntries.indexOf(currentEntry) < 0) { + this.changedLines.push(i); + } + } + + const range = ace.require('ace/range').Range; + for (const i of this.changedLines) { + 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')); + } + if (entry?.trim().length > 0 && entry.trim().length < 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')); + } + } + } + + get hasChanges() { + return ( + this.currentDictionaryEntries.length && + (this.activeEditMarkers.length > 0 || + this.currentDictionaryEntries.filter((e) => e && e.trim().length > 0).length < this.initialDictionaryEntries.length) + ); + } + + nextSearchMatch() { + // length = 3 + if (this.searchPositions.length > 0) { + this.currentMatch = this.currentMatch < this.searchPositions.length ? this.currentMatch + 1 : 1; + this._gotoLine(); + } + } + + previousSearchMatch() { + if (this.searchPositions.length > 0) { + this.currentMatch = this.currentMatch > 1 ? this.currentMatch - 1 : this.searchPositions.length; + this._gotoLine(); + } + } + + 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.selectDictionary]; + return; + } + const appStateDictionaryData = this._appStateService.dictionaryData[ruleSetId]; + this.dictionaries = [ + this.selectDictionary, + ...Object.keys(appStateDictionaryData) + .map((key) => appStateDictionaryData[key]) + .filter((d) => !d.virtual || d.type === 'false_positive') + ]; + } + + 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); + } + + private _onRuleSetChanged() { + this._loadDictionaries(); + this.compareForm.patchValue({ dictionary: this.selectDictionary }); + } + + private _onDictionaryChanged(dictionary: TypeValue) { + if (dictionary !== this.selectDictionary) { + this._dictionaryControllerService.getDictionaryForType(dictionary.ruleSetId, dictionary.type).subscribe( + (data) => { + this.compareDictionaryEntries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); + DictionaryManagerComponent._setEditorValue(this._compareEditorComponent, this.compareDictionaryEntries); + this._syncActiveLines(); + }, + () => {} + ); + } + } + + private _syncActiveLines() { + if (this._compareEditorComponent) { + this._compareEditorComponent.getEditor().gotoLine(this._activeRow); + } + } + + private get _activeRow(): number { + return this._editorComponent.getEditor().selection.getCursor().row + 1; + } + + saveEntries() { + this.saveDictionary.emit(this.currentDictionaryEntries); + } + + ngOnChanges(): void { + this.revert(); + } +} diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index edb7b8ac6..5a570d5f7 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -32,6 +32,8 @@ import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/materia import { MomentDateAdapter } from '@angular/material-moment-adapter'; import { SelectComponent } from './components/select/select.component'; import { NavigateLastProjectsScreenDirective } from './directives/navigate-last-projects-screen.directive'; +import { DictionaryManagerComponent } from './components/dictionary-manager/dictionary-manager.component'; +import { AceEditorModule } from 'ng2-ace-editor'; const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent]; @@ -61,9 +63,9 @@ const utils = [HumanizePipe, SyncWidthDirective, HasScrollbarDirective, Navigate const modules = [MatConfigModule, TranslateModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule]; @NgModule({ - declarations: [...components, ...utils], - imports: [CommonModule, ...modules], - exports: [...modules, ...components, ...utils], + declarations: [...components, ...utils, DictionaryManagerComponent], + imports: [CommonModule, ...modules, AceEditorModule], + exports: [...modules, ...components, ...utils, DictionaryManagerComponent], providers: [ { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] }, {