+
*: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 @@
-\