Skip to content

Commit

Permalink
Merge pull request #48 from UST-QuAntiL/feature/plugin-filter-editor
Browse files Browse the repository at this point in the history
Feature/plugin filter editor
  • Loading branch information
infacc authored Jul 21, 2023
2 parents dcdb986 + eb4ebca commit 647d08a
Show file tree
Hide file tree
Showing 11 changed files with 510 additions and 55 deletions.
6 changes: 6 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { FormsModule } from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
Expand Down Expand Up @@ -82,6 +83,8 @@ import { ChangeUiTemplateComponent } from './dialogs/change-ui-template/change-u
import { ReactiveFormsModule } from '@angular/forms';
import { TemplateDetailsComponent } from './components-small/template-details/template-details.component';
import { ExperimentWorkspaceDetailComponent } from './components/experiment-workspace-detail/experiment-workspace-detail.component';
import { PluginFilterNodeComponent } from './components-small/plugin-filter-node/plugin-filter-node.component';
import { PluginFilterEditorComponent } from './components-small/plugin-filter-editor/plugin-filter-editor.component';
import { TabGroupListComponent } from './components/tab-group-list/tab-group-list.component';

@NgModule({
Expand Down Expand Up @@ -125,6 +128,8 @@ import { TabGroupListComponent } from './components/tab-group-list/tab-group-lis
TemplateDetailsComponent,
ExperimentWorkspaceDetailComponent,
ImportExperimentComponent,
PluginFilterNodeComponent,
PluginFilterEditorComponent,
TabGroupListComponent,
],
imports: [
Expand All @@ -138,6 +143,7 @@ import { TabGroupListComponent } from './components/tab-group-list/tab-group-lis
MatCardModule,
MatButtonModule,
MatButtonToggleModule,
MatSlideToggleModule,
MatCommonModule,
MatTabsModule,
MatRippleModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<div class="filter-editor mat-elevation-z0">
<h3 class="editor-description">Filter Editor</h3>
<div class="editor-header">
<mat-slide-toggle [(ngModel)]="showEditor">
Editor Mode {{ showEditor ? '(UI)' : '(JSON)' }}
</mat-slide-toggle>
<button mat-raised-button type="button" (click)="ngOnInit()">
<mat-icon aria-hidden="true">refresh</mat-icon>
Revert Changes
</button>
</div>
<qhana-plugin-filter-node *ngIf="filterObject && showEditor" [filterIn]="filterObject" (filterOut)="updateFilter($event)" (delete)="deleteFilter()"></qhana-plugin-filter-node>
<ng-container *ngIf="!showEditor">
<mat-form-field class="form-field">
<mat-label>Filter String:</mat-label>
<textarea matInput [formControl]="filterControl" cdkTextareaAutosize #autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1" cdkAutosizeMaxRows="20" (focusout)="updateFilterEditor()">
</textarea>
<button mat-raised-button class="copy-button" type="button" (click)="copyFilterString()">
<mat-icon>content_copy</mat-icon>
</button>
<mat-error *ngIf="filterControl.hasError('invalidJSON')">Invalid JSON</mat-error>
</mat-form-field>
<details>
<summary>Filter String Info</summary>

<i>Enter a filter string as JSON object. Filter strings have the following keys:</i>
<dl>
<dt>name</dt> <dd>Represents the name of a plugin.</dd>
<dt>tag</dt> <dd>Allows filtering elements by their assigned tags.</dd>
<dt>version</dt> <dd>Uses PEP 440 version specifier to filter elements based on specific versions or version ranges.</dd>
<dt>not</dt> <dd>Specifies a filter string to exclude certain elements.</dd>
<dt>and</dt> <dd>Includes multiple filter strings, with elements passing all conditions included in the filtered results (intersection).</dd>
<dt>or</dt> <dd>Includes multiple filter strings, with elements meeting at least one condition included in the filtered results (union).</dd>
</dl>
<mat-divider></mat-divider>
<p>Examples:</p>
<mat-form-field class="form-field">
<textarea class="filter-example" matInput disabled rows="3">
{
"name": "hello-world"
}
</textarea>
</mat-form-field>
<mat-form-field class="form-field">
<textarea class="filter-example" matInput disabled rows="3">
{
"not": { "name": "hello-world" }
}
</textarea>
</mat-form-field>
<mat-form-field class="form-field">
<textarea class="filter-example" matInput disabled rows="6">
{
"and": [
{ "tag": "data-loading" },
{ "version": ">=0.2.0" }
]
}
</textarea>
</mat-form-field>
</details>
</ng-container>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.form-field
width: 100%

.copy-button
position: absolute
top: 0
right: 0

.filter-editor
display: flex
flex-direction: column
gap: 1rem
padding-bottom: 0px

.editor-description
margin: 0

.editor-header
display: flex
justify-content: space-between
align-items: center
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { PluginFilterEditorComponent } from './plugin-filter-editor.component';

describe('PluginFilterEditorComponent', () => {
let component: PluginFilterEditorComponent;
let fixture: ComponentFixture<PluginFilterEditorComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PluginFilterEditorComponent ]
})
.compileComponents();

fixture = TestBed.createComponent(PluginFilterEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormControl, Validators, ValidatorFn } from '@angular/forms';
import { ApiLink } from 'src/app/services/api-data-types';
import { PluginRegistryBaseService } from 'src/app/services/registry.service';
import { TemplateTabApiObject } from 'src/app/services/templates.service';

export function isJSONValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
try {
JSON.parse(control.value);
return null;
} catch (e) {
return { invalidJSON: true };
}
};
}

@Component({
selector: 'qhana-plugin-filter-editor',
templateUrl: './plugin-filter-editor.component.html',
styleUrls: ['./plugin-filter-editor.component.sass']
})
export class PluginFilterEditorComponent implements OnInit {
@Input() tabLink: ApiLink | null = null;
@Output() filterEmitter: EventEmitter<string> = new EventEmitter<string>();

filterString: string = "{}";
// TODO: Add JSON validator for filter strings
filterControl = new FormControl(this.filterString, [isJSONValidator()]);

filterObject: any = {};

showEditor: boolean = true;

constructor(private registry: PluginRegistryBaseService) { }

ngOnInit(): void {
if (this.tabLink == null) {
return;
}
this.registry.getByApiLink<TemplateTabApiObject>(this.tabLink).then(response => {
this.filterString = response?.data?.filterString ?? this.filterString;
this.filterObject = JSON.parse(this.filterString);
this.updateFilter(this.filterObject);
});
}

updateFilter(value: any) {
this.filterObject = value;
this.filterString = JSON.stringify(this.filterObject, null, 2);
this.filterControl.setValue(this.filterString);
this.filterEmitter.emit(this.filterString);
}

copyFilterString() {
navigator.clipboard.writeText(this.filterString);
}

updateFilterEditor() {
const filterValue = this.filterControl.value;
if (filterValue == null) {
return;
}
if (this.filterControl.valid) {
try {
this.filterObject = JSON.parse(filterValue);
this.filterString = filterValue;
this.filterEmitter.emit(this.filterString);
} catch (e) {
console.warn("Invalid filter string", this.filterObject, "\nError:", e);
}
}
}

deleteFilter() {
this.filterString = "{}";
this.filterControl.setValue(this.filterString);
this.updateFilterEditor();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<div>
<div *ngIf="isEmpty" class="set-buttons">
<button mat-raised-button type="button" (click)="setFilter('and')" color="primary">
<mat-icon>add</mat-icon>
AND
</button>
<button mat-raised-button type="button" (click)="setFilter('or')" color="primary">
<mat-icon>add</mat-icon>
OR
</button>
<button mat-raised-button type="button" (click)="setFilter('name')" color="primary">
<mat-icon>add</mat-icon>
Filter
</button>
</div>
<mat-card *ngIf="!isEmpty" class="filter-node mat-elevation-z0" [ngClass]="{'filter-leaf' : type !== 'and' && type !== 'or'}">
<mat-card-header>
<ng-container *ngIf="children != null">
<div class="config-header">
<mat-button-toggle-group [value]="type" (valueChange)="changeType($event)">
<mat-button-toggle value="and">AND</mat-button-toggle>
<mat-button-toggle value="or">OR</mat-button-toggle>
</mat-button-toggle-group>
<div class="config-header-buttons">
<button mat-raised-button *ngIf="depth < 2" type="button" (click)="addFilter('and')" color="primary">
<mat-icon>add</mat-icon>
AND
</button>
<button mat-raised-button *ngIf="depth < 2" type="button" (click)="addFilter('or')" color="primary">
<mat-icon>add</mat-icon>
OR
</button>
<button mat-raised-button type="button" (click)="addFilter('name')" color="primary">
<mat-icon>add</mat-icon>
Filter
</button>
<button mat-raised-button type="button" (click)="delete.emit()">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</ng-container>
</mat-card-header>
<mat-card-content>
<ng-container *ngIf="children != null">
<span *ngIf="inverted" class="t-subheading">Not:</span>
<qhana-plugin-filter-node *ngFor="let child of children; let i = index" [filterIn]="children[i]"
[depth]="depth + 1" (filterOut)="updateChild($event, i)" (delete)="deleteChild(i)"></qhana-plugin-filter-node>
</ng-container>
<div class="config-filter" *ngIf="value != null">
<span *ngIf="inverted" class="t-subheading">Not:</span>
<mat-form-field class="inner-form-field">
<mat-label>Filter Type:</mat-label>
<mat-select [value]="type" (valueChange)="changeType($event)">
<mat-option value="name">Name</mat-option>
<mat-option value="tag">Tag</mat-option>
<mat-option value="version">Version</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Filter String:</mat-label>
<textarea matInput [value]="value" (focusout)="updateFilterString($event)" cdkTextareaAutosize #autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1" cdkAutosizeMaxRows="15"></textarea>
</mat-form-field>
<button mat-raised-button type="button" (click)="delete.emit()">
<mat-icon>delete</mat-icon>
</button>
</div>
<mat-checkbox [checked]="inverted" (change)="invertFilter($event)">Invert - <i>Negate this filter</i></mat-checkbox>
</mat-card-content>
</mat-card>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.set-buttons
display: flex
flex-direction: row
justify-content: flex-start
gap: 10px

.form-field
width: 100%

.filter-node
border: 1px solid #7b7b7b

.filter-node, .filter-node > mat-card-content
display: flex
flex-direction: column
justify-content: flex-start
gap: 10px
font-size: 12px

.filter-leaf
padding: 8px 16px

::ng-deep .mat-card-header-text
margin: 0px

.config-header
width: 100%
display: flex
flex-direction: row
justify-content: space-between

.config-header-buttons
display: flex
flex-direction: row
justify-content: flex-end
gap: 10px

.config-header > mat-button-toggle-group
height: 32px

.config-header > mat-button-toggle-group > mat-button-toggle
height: 32px
font-size: 12px
display: flex
align-items: center

.config-filter
display: flex
flex-direction: row
justify-content: space-between
align-items: center
gap: 10px
font-size: 12px

::ng-deep .config-filter > mat-form-field > .mat-form-field-wrapper
margin: 0px
padding: 0px
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { PluginFilterNodeComponent } from './plugin-filter-node.component';

describe('PluginFilterNodeComponent', () => {
let component: PluginFilterNodeComponent;
let fixture: ComponentFixture<PluginFilterNodeComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PluginFilterNodeComponent ]
})
.compileComponents();

fixture = TestBed.createComponent(PluginFilterNodeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading

0 comments on commit 647d08a

Please sign in to comment.