extracted dictionary manager component
This commit is contained in:
parent
2413074b33
commit
bdbb62c524
@ -55,86 +55,11 @@
|
||||
|
||||
<redaction-side-nav type="project-templates"></redaction-side-nav>
|
||||
|
||||
<div class="content-container">
|
||||
<div class="actions-bar">
|
||||
<div class="red-input-group w-450 mr-32">
|
||||
<input
|
||||
#inputElement
|
||||
(keyup)="searchChanged(searchText)"
|
||||
[(ngModel)]="searchText"
|
||||
[class.with-matches]="searchText.length > 0"
|
||||
placeholder="{{ 'dictionary-overview.search' | translate }}"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<div class="input-icons">
|
||||
<div *ngIf="searchText.length === 0" class="no-input">
|
||||
<mat-icon svgIcon="red:search"></mat-icon>
|
||||
</div>
|
||||
<div *ngIf="searchText.length > 0" class="with-input">
|
||||
<div class="search-match-text">
|
||||
{{ currentMatch + '/' + searchPositions.length }}
|
||||
</div>
|
||||
<mat-icon (click)="previousSearchMatch()" class="pointer" svgIcon="red:arrow-up"></mat-icon>
|
||||
<mat-icon (click)="nextSearchMatch()" class="pointer" svgIcon="red:arrow-down"></mat-icon>
|
||||
<mat-icon (click)="searchChanged(''); inputElement.focus()" class="pointer" svgIcon="red:close"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="compareForm" class="compare-form">
|
||||
<div class="red-input-group mr-16">
|
||||
<mat-checkbox color="primary" formControlName="active"> {{ 'dictionary-overview.compare.compare' | translate }} </mat-checkbox>
|
||||
</div>
|
||||
<div class="red-input-group w-200 mr-8">
|
||||
<mat-select formControlName="ruleSet">
|
||||
<mat-option *ngFor="let ruleSet of ruleSets" [value]="ruleSet">
|
||||
{{ ruleSet === selectRuleSet ? (ruleSet.name | translate) : ruleSet.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<div class="red-input-group w-200">
|
||||
<mat-select formControlName="dictionary">
|
||||
<mat-option *ngFor="let dictionary of dictionaries" [value]="dictionary">
|
||||
{{ dictionary === selectDictionary ? (dictionary.label | translate) : dictionary.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<ace-editor
|
||||
#editorComponent
|
||||
(textChanged)="textChanged($event)"
|
||||
[autoUpdateContent]="true"
|
||||
[mode]="'text'"
|
||||
[options]="aceOptions"
|
||||
[readOnly]="!permissionsService.isAdmin()"
|
||||
[theme]="'eclipse'"
|
||||
class="ace-redaction"
|
||||
>
|
||||
</ace-editor>
|
||||
<div *ngIf="compareForm.get('active').value && compareForm.get('dictionary').value === selectDictionary" class="no-dictionary-selected">
|
||||
<mat-icon svgIcon="red:dictionary"></mat-icon>
|
||||
<span class="heading-l" translate="dictionary-overview.select-dictionary"></span>
|
||||
</div>
|
||||
<ace-editor
|
||||
#compareEditorComponent
|
||||
*ngIf="compareForm.get('active').value && compareForm.get('dictionary').value !== selectDictionary"
|
||||
[mode]="'text'"
|
||||
[options]="aceOptions"
|
||||
[readOnly]="true"
|
||||
[theme]="'eclipse'"
|
||||
class="ace-redaction"
|
||||
>
|
||||
</ace-editor>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasChanges && permissionsService.isAdmin()" [class.offset]="compareForm.get('active').value" class="changes-box">
|
||||
<redaction-icon-button (action)="saveEntries()" icon="red:check" text="dictionary-overview.save-changes" type="primary"></redaction-icon-button>
|
||||
<div (click)="revert()" class="all-caps-label cancel" translate="dictionary-overview.revert-changes"></div>
|
||||
</div>
|
||||
</div>
|
||||
<redaction-dictionary-manager
|
||||
[initialDictionaryEntries]="entries"
|
||||
(saveDictionary)="saveEntries($event)"
|
||||
#dictionaryManager
|
||||
></redaction-dictionary-manager>
|
||||
|
||||
<div class="right-container">
|
||||
<div class="dictionary-header">
|
||||
@ -146,7 +71,7 @@
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:entries"></mat-icon>
|
||||
{{ initialDictionaryEntries?.length }}
|
||||
{{ dictionaryManager.initialDictionaryEntries?.length }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="small-label stats-subtitle">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<any>;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
<div class="content-container">
|
||||
<div class="actions-bar">
|
||||
<div class="red-input-group w-450 mr-32">
|
||||
<input
|
||||
#inputElement
|
||||
(keyup)="searchChanged(searchText)"
|
||||
[(ngModel)]="searchText"
|
||||
[class.with-matches]="searchText.length > 0"
|
||||
placeholder="{{ 'dictionary-overview.search' | translate }}"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<div class="input-icons">
|
||||
<div *ngIf="searchText.length === 0" class="no-input">
|
||||
<mat-icon svgIcon="red:search"></mat-icon>
|
||||
</div>
|
||||
<div *ngIf="searchText.length > 0" class="with-input">
|
||||
<div class="search-match-text">
|
||||
{{ currentMatch + '/' + searchPositions.length }}
|
||||
</div>
|
||||
<mat-icon (click)="previousSearchMatch()" class="pointer" svgIcon="red:arrow-up"></mat-icon>
|
||||
<mat-icon (click)="nextSearchMatch()" class="pointer" svgIcon="red:arrow-down"></mat-icon>
|
||||
<mat-icon (click)="searchChanged(''); inputElement.focus()" class="pointer" svgIcon="red:close"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="compareForm" class="compare-form">
|
||||
<div class="red-input-group mr-16">
|
||||
<mat-checkbox color="primary" formControlName="active"> {{ 'dictionary-overview.compare.compare' | translate }} </mat-checkbox>
|
||||
</div>
|
||||
<div class="red-input-group w-200 mr-8">
|
||||
<mat-select formControlName="ruleSet">
|
||||
<mat-option *ngFor="let ruleSet of ruleSets" [value]="ruleSet">
|
||||
{{ ruleSet === selectRuleSet ? (ruleSet.name | translate) : ruleSet.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<div class="red-input-group w-200">
|
||||
<mat-select formControlName="dictionary">
|
||||
<mat-option *ngFor="let dictionary of dictionaries" [value]="dictionary">
|
||||
{{ dictionary === selectDictionary ? (dictionary.label | translate) : dictionary.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<ace-editor
|
||||
#editorComponent
|
||||
(textChanged)="textChanged($event)"
|
||||
[autoUpdateContent]="true"
|
||||
[mode]="'text'"
|
||||
[options]="aceOptions"
|
||||
[readOnly]="!permissionsService.isAdmin()"
|
||||
[theme]="'eclipse'"
|
||||
class="ace-redaction"
|
||||
>
|
||||
</ace-editor>
|
||||
<div *ngIf="compareForm.get('active').value && compareForm.get('dictionary').value === selectDictionary" class="no-dictionary-selected">
|
||||
<mat-icon svgIcon="red:dictionary"></mat-icon>
|
||||
<span class="heading-l" translate="dictionary-overview.select-dictionary"></span>
|
||||
</div>
|
||||
<ace-editor
|
||||
#compareEditorComponent
|
||||
*ngIf="compareForm.get('active').value && compareForm.get('dictionary').value !== selectDictionary"
|
||||
[mode]="'text'"
|
||||
[options]="aceOptions"
|
||||
[readOnly]="true"
|
||||
[theme]="'eclipse'"
|
||||
class="ace-redaction"
|
||||
>
|
||||
</ace-editor>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasChanges && permissionsService.isAdmin()" [class.offset]="compareForm.get('active').value" class="changes-box">
|
||||
<redaction-icon-button (action)="saveEntries()" icon="red:check" text="dictionary-overview.save-changes" type="primary"></redaction-icon-button>
|
||||
<div (click)="revert()" class="all-caps-label cancel" translate="dictionary-overview.revert-changes"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<string[]>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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] },
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user