Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/plugin filter editor #48

Merged
merged 31 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
01bf1bb
add plugin filter editor
infacc Jun 19, 2023
e55a407
show plugin editor when no tab link is provided
infacc Jul 3, 2023
8b1fe43
hide mat-card if filter is empty
infacc Jul 4, 2023
5616886
refactor: improve filter editor usability
infacc Jul 11, 2023
76d7744
refactor: avoid ViewChild decorator in filter editor
infacc Jul 12, 2023
4e2edb3
simplify isLeaf check
infacc Jul 12, 2023
38879c2
rectify warning message
infacc Jul 12, 2023
e71700b
refactor: simplify filter setup
infacc Jul 12, 2023
d81143a
Merge branch 'feature/plugin-filter-editor' of github.com:UST-QuAntiL…
infacc Jul 12, 2023
fae56c0
handle filter deletion by parent
infacc Jul 13, 2023
4ceb6d4
catch JSON paring errors
infacc Jul 13, 2023
183ced2
separate ternary operator to improve readability
infacc Jul 13, 2023
a1fe552
add inverted indication for and/or filters
infacc Jul 13, 2023
05d026b
remove index property
infacc Jul 17, 2023
f62ff31
store info whether filter is empty in property
infacc Jul 17, 2023
334189a
refactor: separate input and output objects
infacc Jul 18, 2023
c0ee392
check for empty filter
infacc Jul 18, 2023
51171f8
fix bug: keep input focus when typing
infacc Jul 18, 2023
fff9496
fix bug: use JSON editor input on tab change
infacc Jul 18, 2023
be07ea4
add active warning for filters with multiple attributes
infacc Jul 18, 2023
9622400
set filter string via template expression
infacc Jul 18, 2023
ec041cf
refactor: simplify type check for filter keys
infacc Jul 18, 2023
62344c6
Merge branch 'main' into feature/plugin-filter-editor
infacc Jul 18, 2023
b8b6cf6
rename HTML class to avoid confusion
infacc Jul 18, 2023
16dd6a5
use deep copies to emit filters
infacc Jul 19, 2023
8ea5eea
remove filter string from template form
infacc Jul 20, 2023
af7ad6b
add reload filter button
infacc Jul 20, 2023
37526f4
remove unused location interface
infacc Jul 20, 2023
6a33202
add JSON validator to filter string textarea input
infacc Jul 20, 2023
f5423d5
rename revert filter button
infacc Jul 21, 2023
eb4ebca
hide button from screen readers
infacc Jul 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,57 @@
<div class="filter-editor mat-elevation-z0">
<h3 class="editor-description">Filter Editor</h3>
<mat-slide-toggle [(ngModel)]="showEditor">
Editor Mode {{ showEditor ? '(UI)' : '(JSON)' }}
</mat-slide-toggle>
<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-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,16 @@
.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
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,68 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl, Validators } 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';

@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 = "{}";
filterControl = new FormControl(this.filterString, [Validators.required, Validators.minLength(2)]); // TODO: Add validator for JSON

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