Compare dictionaries

This commit is contained in:
Adina Țeudan 2021-03-26 02:59:13 +02:00
parent 7d2e9a7b7a
commit 9455e2cf39
8 changed files with 284 additions and 104 deletions

View File

@ -89,7 +89,7 @@
<div class="scrollbar-placeholder"></div>
</div>
<cdk-virtual-scroll-viewport [itemSize]="100" redactionHasScrollbar>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div class="table-item pointer" *cdkVirtualFor="let log of logs?.data">
<div>
{{ log.message }}

View File

@ -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',

View File

@ -53,7 +53,7 @@
<div class="flex red-content-inner">
<div class="left-container">
<div class="actions-bar">
<div class="red-input-group flex-1 mr-32">
<div class="red-input-group w-450 mr-32">
<input
[class.with-matches]="searchText.length > 0"
type="text"
@ -77,11 +77,25 @@
</div>
</div>
</div>
<!-- Not yet used-->
<!-- <div class="red-input-group mr-16">-->
<!-- <mat-checkbox [(ngModel)]="compareActive" color="primary"> {{ 'dictionary-overview.compare' | translate }} </mat-checkbox>-->
<!-- </div>-->
<div class="flex-1"></div>
<form class="compare-form" [formGroup]="compareForm">
<div class="red-input-group mr-16">
<mat-checkbox formControlName="active" color="primary"> {{ '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 === SELECT_RULESET ? (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 === SELECT_DICTIONARY ? (dictionary.label | translate) : dictionary.label }}
</mat-option>
</mat-select>
</div>
</form>
</div>
<div class="editor-container">
@ -93,13 +107,26 @@
[readOnly]="!permissionsService.isAdmin()"
(textChanged)="textChanged($event)"
[autoUpdateContent]="true"
[text]="dictionaryEntriesAsText"
class="ace-redaction"
>
</ace-editor>
<div class="no-dictionary-selected" *ngIf="compareForm.get('active').value && compareForm.get('dictionary').value === SELECT_DICTIONARY">
<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 !== SELECT_DICTIONARY"
[mode]="'text'"
[theme]="'eclipse'"
[options]="aceOptions"
[readOnly]="true"
class="ace-redaction"
>
</ace-editor>
</div>
<div class="changes-box" *ngIf="hasChanges && permissionsService.isAdmin()">
<div class="changes-box" *ngIf="hasChanges && permissionsService.isAdmin()" [class.offset]="compareForm.get('active').value">
<redaction-icon-button
icon="red:check-alt"
(action)="saveEntries()"

View File

@ -3,6 +3,20 @@
.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;
}
}
.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;
}

View File

@ -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);
}
}

View File

@ -1,4 +1,3 @@
\
<section>
<div class="page-header">
<div class="actions flex-1">

View File

@ -641,10 +641,15 @@
"search": "Search...",
"save-changes": "Save Changes",
"revert-changes": "Revert",
"compare": "Compare",
"dictionary-details": {
"description": "Description"
}
},
"compare": {
"compare": "Compare",
"select-ruleset": "Select Project Template",
"select-dictionary": "Select Dictionary"
},
"select-dictionary": "Select a dictionary above to compare with the current one."
},
"dictionary-listing": {
"action": {

View File

@ -28,6 +28,10 @@
border-right: none;
}
.ace_gutter-cell:after {
content: '.';
}
.ace_active-line {
background: $grey-6;
}
@ -50,7 +54,7 @@
}
.ace_gutter-active-line {
background-color: $grey-4;
background-color: $grey-6;
}
.ace_marker-layer .ace_selected-word {
@ -60,6 +64,14 @@
.ace_invisible {
color: $grey-4;
}
&[ng-reflect-read-only='true'] {
background-color: $grey-2;
*:not(.ace_scrollbar) {
pointer-events: none;
}
}
}
.editor-container {
@ -92,4 +104,8 @@
> *:not(:last-child) {
margin-right: 24px;
}
&.offset {
right: calc(50% + 40px);
}
}