diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 9b93a4b..b6fe4e0 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -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';
@@ -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({
@@ -125,6 +128,8 @@ import { TabGroupListComponent } from './components/tab-group-list/tab-group-lis
TemplateDetailsComponent,
ExperimentWorkspaceDetailComponent,
ImportExperimentComponent,
+ PluginFilterNodeComponent,
+ PluginFilterEditorComponent,
TabGroupListComponent,
],
imports: [
@@ -138,6 +143,7 @@ import { TabGroupListComponent } from './components/tab-group-list/tab-group-lis
MatCardModule,
MatButtonModule,
MatButtonToggleModule,
+ MatSlideToggleModule,
MatCommonModule,
MatTabsModule,
MatRippleModule,
diff --git a/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.html b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.html
new file mode 100644
index 0000000..2f10e4a
--- /dev/null
+++ b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.html
@@ -0,0 +1,64 @@
+
+
Filter Editor
+
+
+
+
+ Filter String:
+
+
+ Invalid JSON
+
+
+ Filter String Info
+
+ Enter a filter string as JSON object. Filter strings have the following keys:
+
+ - name
- Represents the name of a plugin.
+ - tag
- Allows filtering elements by their assigned tags.
+ - version
- Uses PEP 440 version specifier to filter elements based on specific versions or version ranges.
+ - not
- Specifies a filter string to exclude certain elements.
+ - and
- Includes multiple filter strings, with elements passing all conditions included in the filtered results (intersection).
+ - or
- Includes multiple filter strings, with elements meeting at least one condition included in the filtered results (union).
+
+
+ Examples:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.sass b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.sass
new file mode 100644
index 0000000..8540462
--- /dev/null
+++ b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.sass
@@ -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
\ No newline at end of file
diff --git a/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.spec.ts b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.spec.ts
new file mode 100644
index 0000000..74ce911
--- /dev/null
+++ b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ PluginFilterEditorComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PluginFilterEditorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.ts b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.ts
new file mode 100644
index 0000000..51eb9b0
--- /dev/null
+++ b/src/app/components-small/plugin-filter-editor/plugin-filter-editor.component.ts
@@ -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 = new EventEmitter();
+
+ 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(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();
+ }
+}
diff --git a/src/app/components-small/plugin-filter-node/plugin-filter-node.component.html b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.html
new file mode 100644
index 0000000..88c1d46
--- /dev/null
+++ b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Not:
+
+
+
+ Not:
+
+ Filter Type:
+
+ Name
+ Tag
+ Version
+
+
+
+ Filter String:
+
+
+
+
+ Invert - Negate this filter
+
+
+
\ No newline at end of file
diff --git a/src/app/components-small/plugin-filter-node/plugin-filter-node.component.sass b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.sass
new file mode 100644
index 0000000..f255b64
--- /dev/null
+++ b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.sass
@@ -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
diff --git a/src/app/components-small/plugin-filter-node/plugin-filter-node.component.spec.ts b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.spec.ts
new file mode 100644
index 0000000..a2a4b2e
--- /dev/null
+++ b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ PluginFilterNodeComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PluginFilterNodeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components-small/plugin-filter-node/plugin-filter-node.component.ts b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.ts
new file mode 100644
index 0000000..c69b9e6
--- /dev/null
+++ b/src/app/components-small/plugin-filter-node/plugin-filter-node.component.ts
@@ -0,0 +1,159 @@
+import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core';
+
+// Define filter types ('not' excluded)
+// The PluginFilterNodeComponent component is designed to encapsulate a filter object and the information wether the filter is inverted ('not').
+// When the filter is inverted, the filter object is wrapped in a 'not' object and the 'inverted' property is set to true.
+const filterTypes = ['and', 'or', 'name', 'tag', 'version'] as const;
+type FilterType = (typeof filterTypes)[number];
+const isFilterType = (x: any): x is FilterType => filterTypes.includes(x);
+
+@Component({
+ selector: 'qhana-plugin-filter-node',
+ templateUrl: './plugin-filter-node.component.html',
+ styleUrls: ['./plugin-filter-node.component.sass']
+})
+export class PluginFilterNodeComponent implements OnInit {
+ @Input() filterIn: any;
+ @Input() depth: number = 0;
+ @Output() filterOut = new EventEmitter();
+ @Output() delete = new EventEmitter();
+
+ filterObject: any = {};
+ type: FilterType | null = null;
+ children: any[] | null = null;
+ value: string | null = null;
+ inverted: boolean = false;
+ isEmpty: boolean = true;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ this.setupFilter();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.filterIn.previousValue != null) {
+ this.setupFilter();
+ }
+ }
+
+ setupFilter() {
+ this.isEmpty = Object.keys(this.filterIn).length === 0;
+ this.filterObject = JSON.parse(JSON.stringify(this.filterIn));
+ const filter = this.filterObject.not ?? this.filterObject;
+ this.inverted = this.filterObject.not != null;
+ if (filter == null) {
+ console.warn("No filter object provided to plugin filter node component");
+ return;
+ }
+ if (this.isEmpty) {
+ return;
+ }
+ const filterKeys = Object.keys(filter)
+ if (filterKeys.length != 1) {
+ console.error("Filters with more than one attribute are not supported! ", filterKeys);
+ }
+ const type = filterKeys[0];
+ if (!isFilterType(type)) {
+ console.warn("Invalid filter type provided to plugin filter node component: ", type);
+ return;
+ }
+ this.type = type as FilterType;
+ const isLeaf = filter.and == null && filter.or == null;
+ if (isLeaf) {
+ this.value = filter[this.type];
+ } else {
+ this.children = filter[this.type];
+ }
+ }
+
+ updateFilterObject() {
+ if (this.type == null) {
+ console.warn("No type provided to plugin filter node component");
+ return;
+ }
+ const filter = JSON.parse(JSON.stringify(this.filterObject));
+ if (this.inverted) {
+ filter.not[this.type] = this.children ?? this.value;
+ } else {
+ filter[this.type] = this.children ?? this.value;
+ }
+ this.filterOut.emit(filter);
+ }
+
+ setFilter(type: FilterType) {
+ const isLeaf = type !== 'and' && type !== 'or';
+ this.type = type;
+ this.children = isLeaf ? null : [];
+ this.value = isLeaf ? "" : null;
+ this.updateFilterObject();
+ this.isEmpty = false;
+ }
+
+ deleteChild(index: number) {
+ if (this.children == null) {
+ console.warn("Cannot delete child filter: Filter has no children!");
+ return;
+ }
+ this.children.splice(index, 1);
+ this.updateFilterObject();
+ }
+
+ addFilter(type: FilterType = 'name') {
+ if (this.children == null) {
+ console.warn("Cannot add child filter because the plugin filter node component is a leaf node!");
+ return;
+ }
+ const filterValue = (type === 'and' || type === 'or') ? [] : "";
+ this.children.push({ [type]: filterValue });
+ this.updateFilterObject();
+ }
+
+ updateChild(value: any, index: number) {
+ if (this.children == null) {
+ console.warn("No children provided to plugin filter node component");
+ return;
+ }
+ if (value == null) {
+ this.children.splice(index, 1);
+ } else {
+ this.children[index] = value;
+ }
+ this.updateFilterObject();
+ }
+
+ changeType(type: FilterType) {
+ if (type == null || this.type === type) {
+ return;
+ }
+ const filter = this.filterObject.not ?? this.filterObject;
+ const isLeaf = type !== 'and' && type !== 'or';
+ if (this.type != null) {
+ delete filter[this.type]
+ }
+ this.type = type;
+ if (isLeaf) {
+ this.children = null;
+ filter[type] = this.value ?? "";
+ } else {
+ this.value = null;
+ filter[type] = this.children ?? [];
+ }
+ this.filterOut.emit(JSON.parse(JSON.stringify(this.filterObject)));
+ }
+
+ updateFilterString(event: any) {
+ this.value = event.target.value;
+ this.updateFilterObject();
+ }
+
+ invertFilter(event: any) {
+ this.inverted = event.checked;
+ if (this.inverted) {
+ this.filterObject = { not: this.filterObject };
+ } else {
+ this.filterObject = this.filterObject.not;
+ }
+ this.filterOut.emit(JSON.parse(JSON.stringify(this.filterObject)));
+ }
+}
diff --git a/src/app/components-small/template-details/template-details.component.html b/src/app/components-small/template-details/template-details.component.html
index 67e9f55..c3ea833 100644
--- a/src/app/components-small/template-details/template-details.component.html
+++ b/src/app/components-small/template-details/template-details.component.html
@@ -20,50 +20,7 @@ Template Tab
Sort Key:
-
- Filter String:
-
-
-
- Filter String Info
-
- Enter a filter string as JSON object. Filter strings have the following keys:
-
- - name
- Represents the name of a plugin.
- - tag
- Allows filtering elements by their assigned tags.
- - version
- Uses PEP 440 version specifier to filter elements based on specific versions or version ranges.
- - not
- Specifies a filter string to exclude certain elements.
- - and
- Includes multiple filter strings, with elements passing all conditions included in the filtered results (intersection).
- - or
- Includes multiple filter strings, with elements meeting at least one condition included in the filtered results (union).
-
-
- Examples:
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/app/components-small/template-details/template-details.component.ts b/src/app/components-small/template-details/template-details.component.ts
index f3c4a04..932622b 100644
--- a/src/app/components-small/template-details/template-details.component.ts
+++ b/src/app/components-small/template-details/template-details.component.ts
@@ -14,12 +14,6 @@ export function isInSetValidator(validValues: any[]): Validators {
};
}
-export interface Location {
- value: string;
- description: string;
-}
-
-
@Component({
selector: 'qhana-template-details',
templateUrl: './template-details.component.html',
@@ -29,13 +23,14 @@ export class TemplateDetailsComponent implements OnInit {
@Input() templateLink: ApiLink | null = null;
@Input() tabLink: ApiLink | null = null;
- tabGroupNameOverrides = {...TAB_GROUP_NAME_OVERRIDES};
+ filterString: string = "{}";
+
+ tabGroupNameOverrides = { ...TAB_GROUP_NAME_OVERRIDES };
private initialValues = {
name: "",
description: "",
sortKey: 0,
- filterString: "{}",
location: "workspace"
};
@@ -43,7 +38,6 @@ export class TemplateDetailsComponent implements OnInit {
name: [this.initialValues.name, Validators.required],
description: this.initialValues.description,
sortKey: this.initialValues.sortKey,
- filterString: [this.initialValues.filterString, Validators.minLength(2)], // TODO: validate using JSON schema
location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]]
});
@@ -56,7 +50,6 @@ export class TemplateDetailsComponent implements OnInit {
name: response?.data?.name ?? this.initialValues.name,
description: response?.data?.description ?? this.initialValues.description,
sortKey: response?.data?.sortKey ?? this.initialValues.sortKey,
- filterString: response?.data?.filterString ?? this.initialValues.filterString,
location: response?.data?.location ?? this.initialValues.location
});
});
@@ -82,7 +75,7 @@ export class TemplateDetailsComponent implements OnInit {
name: this.templateForm.value.name,
description: this.templateForm.value.description,
sortKey: this.templateForm.value.sortKey,
- filterString: this.templateForm.value.filterString,
+ filterString: this.filterString,
location: this.templateForm.value.location
});
if (this.templateLink != null) {