Skip to content

Commit

Permalink
Merge pull request #77 from UST-QuAntiL/feature/extended-template-tabs
Browse files Browse the repository at this point in the history
UI Template Tab Improvements
  • Loading branch information
buehlefs authored Aug 5, 2024
2 parents 5ba8b1e + a3abb2e commit 6a6f3b8
Show file tree
Hide file tree
Showing 26 changed files with 649 additions and 156 deletions.
99 changes: 94 additions & 5 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RouterModule, Routes, UrlMatcher, UrlMatchResult, UrlSegment } from '@angular/router';
import { DataDetailComponent } from './components/data-detail/data-detail.component';
import { ExperimentDataComponent } from './components/experiment-data/experiment-data.component';
import { ExperimentTimelineComponent } from './components/experiment-timeline/experiment-timeline.component';
Expand All @@ -26,11 +26,98 @@ import { PluginTabComponent } from './components/plugin-tab/plugin-tab.component
import { SettingsPageComponent } from './components/settings-page/settings-page.component';
import { TimelineStepComponent } from './components/timeline-step/timeline-step.component';

const NUMBER_REGEX = /^[0-9]+$/;

const extraTabsMatcher: UrlMatcher = (segments: UrlSegment[], group, route): UrlMatchResult | null => {
const consumed: UrlSegment[] = [];
const params: { [props: string]: UrlSegment } = {};

let tabId: UrlSegment | null = null;
let pluginId: UrlSegment | null = null;

// match: /experiments/:experimentId
let index = 0;
if (segments[index]?.path === "experiments") {
consumed.push(segments[index]);
index += 1;
if ((segments[index]?.path ?? "").match(NUMBER_REGEX)) {
params.experimentId = segments[index];
consumed.push(segments[index]);
index += 1;
} else {
return null;
}
}

console.log(consumed, params)

// match: ./extra[/:path]/:templateTabId
if (segments[index]?.path !== "extra") {
return null;
}
consumed.push(segments[index]);
index += 1;
if (segments[index + 1]?.path.match(NUMBER_REGEX)) {
// push [/:path]
params.path = segments[index];
consumed.push(segments[index]);
index += 1;
// push /:templateTabId
tabId = segments[index];
consumed.push(segments[index]);
index += 1;
} else if (segments[index]?.path.match(NUMBER_REGEX)) {
// push /:templateTabId
tabId = segments[index];
consumed.push(segments[index]);
index += 1;
}

if (tabId == null) {
// sanity check
return null;
}
params.templateTabId = tabId;

console.log(consumed, params)

// found full match?
if (index === segments.length) {
return {
consumed: consumed,
posParams: params,
}
}

// match: ./plugin/:pluginId
if (segments[index]?.path !== "plugins") {
return null;
}
consumed.push(segments[index]);
index += 1;
if (segments[index]?.path.match(NUMBER_REGEX)) {
pluginId = segments[index];
consumed.push(segments[index]);
index += 1;
}

console.log(consumed, params)

// found full match?
if (index === segments.length && pluginId != null) {
params.pluginId = pluginId;
return {
consumed: consumed,
posParams: params,
}
}

return null;
}

const routes: Routes = [
{ path: '', component: ExperimentsPageComponent },
{ path: 'settings', component: SettingsPageComponent },
{ path: 'extra/:templateTabId', component: PluginTabComponent },
{ path: 'extra/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
{ path: 'experiments', component: ExperimentsPageComponent },
{ path: 'experiments/:experimentId', redirectTo: "info" },
{ path: 'experiments/:experimentId/info', component: ExperimentComponent },
Expand All @@ -41,8 +128,10 @@ const routes: Routes = [
{ path: 'experiments/:experimentId/timeline', component: ExperimentTimelineComponent },
{ path: 'experiments/:experimentId/timeline/:step', component: TimelineStepComponent },
{ path: 'experiments/:experimentId/timeline/:step/:stepTabId', component: TimelineStepComponent },
{ path: 'experiments/:experimentId/extra/:templateTabId', component: PluginTabComponent },
{ path: 'experiments/:experimentId/extra/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
{/* path: '[experiments/:experimentId/]extra/[:path/]:templateTabId/[plugins/:pluginId]' */
matcher: extraTabsMatcher,
component: PluginTabComponent,
},
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export class PluginFilterEditorComponent implements OnInit {
}
this.registry.getByApiLink<TemplateTabApiObject>(this.tabLink).then(response => {
this.filterString = response?.data?.filterString ?? this.filterString;
if (!this.filterString) {
this.filterString = "{}";
}
this.filterObject = JSON.parse(this.filterString);
this.updateFilter(this.filterObject);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Filter String:</mat-label>
<input matInput [value]="value" (focusout)="updateFilterString($event)" list="plugin-types">
<input matInput [value]="value" (focusout)="updateFilterString($event)" [attr.list]="type === 'type' ? 'plugin-types' : null">
<datalist *ngIf="type === 'type'" id="plugin-types">
<option value="processing"></option>
<option value="visualization"></option>
Expand All @@ -80,7 +80,10 @@
Examples: "processing", "conversion", "dataloader", "visualization", "interaction"
</mat-hint>
</mat-form-field>
<button mat-raised-button type="button" (click)="delete.emit()">
<button mat-raised-button class="config-filter-button" (click)="openPluginChooser()" *ngIf="type === 'name' || type === 'id'">
choose
</button>
<button mat-raised-button class="config-filter-button" (click)="delete.emit()">
<mat-icon>delete</mat-icon>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,16 @@
display: flex
flex-direction: row
justify-content: space-between
align-items: center
align-items: flex-start
gap: 10px
font-size: 12px

.config-filter-button
display: inline-flex
align-items: center
justify-content: center
min-height: 56px

::ng-deep .config-filter > mat-form-field > .mat-mdc-form-field-wrapper
margin: 0px
padding: 0px
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ChoosePluginDialog } from 'src/app/dialogs/choose-plugin/choose-plugin.dialog';
import { PluginApiObject } from 'src/app/services/qhana-api-data-types';

// Define filter types ('not' excluded)
// The PluginFilterNodeComponent component is designed to encapsulate a filter object and the information wether the filter is inverted ('not').
Expand All @@ -25,7 +28,7 @@ export class PluginFilterNodeComponent implements OnInit {
inverted: boolean = false;
isEmpty: boolean = true;

constructor() { }
constructor(private dialog: MatDialog) { }

ngOnInit(): void {
this.setupFilter();
Expand Down Expand Up @@ -156,4 +159,22 @@ export class PluginFilterNodeComponent implements OnInit {
}
this.filterOut.emit(JSON.parse(JSON.stringify(this.filterObject)));
}

openPluginChooser() {
const dialogRef = this.dialog.open(ChoosePluginDialog, {});
dialogRef.afterClosed().subscribe((result: PluginApiObject | null) => {
if (result == null) {
return; // nothing was selected
}
if (this.type === "id") {
this.value = result.identifier;
this.updateFilterObject();
}
if (this.type === "name") {
this.value = result.title;
this.updateFilterObject();
}
});

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ <h2>Template Tab</h2>
</mat-radio-button>
</mat-radio-group>
</div>
<mat-form-field class="form-field" [hidden]="!templateForm.value.location || templateForm.value.location === 'workspace'">
<mat-label>Tab Group:</mat-label>
<input matInput formControlName="locationExtra">
<mat-hint>The group key of the tab this tab should be grouped under. (Use '.' to separate group keys for nested groups.)</mat-hint>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Name:</mat-label>
<input matInput formControlName="name">
Expand All @@ -22,8 +27,20 @@ <h2>Template Tab</h2>
<mat-label>Sort Key:</mat-label>
<input matInput type="number" formControlName="sortKey">
</mat-form-field>
<div class="icon-field-wrapper">
<mat-form-field class="form-field">
<mat-label>Icon:</mat-label>
<input matInput formControlName="icon">
</mat-form-field>
<mat-icon class="icon-preview">{{templateForm.value.icon || "extension"}}</mat-icon>
</div>
<mat-form-field class="form-field">
<mat-label>Group Key:</mat-label>
<input matInput pattern="[^\.]*" formControlName="groupKey">
<mat-hint>Establish this tab as its own tab group. (Cannot contain '.')</mat-hint>
</mat-form-field>
<qhana-plugin-filter-editor [tabLink]="tabLink"
(filterEmitter)="filterString = $event"></qhana-plugin-filter-editor>
<br>
<button mat-raised-button type="submit" color="primary">{{templateLink ? 'Create' : 'Update'}} Tab</button>
(filterEmitter)="filterString = $event" [hidden]="templateForm.value.groupKey"></qhana-plugin-filter-editor>
<p class="form-error" *ngIf="templateForm?.errors?.groupKeyForbidden">Group key cannot be used in experiment workspace tabs!</p>
<button mat-raised-button class="submit-button" type="submit" color="primary" [disabled]="templateForm.errors">{{templateLink ? 'Create' : 'Update'}} Tab</button>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@
.location-chooser
margin-block-end: 1rem

details
margin-left: 2rem
.icon-field-wrapper
display: flex
gap: 1rem
align-items: baseline

summary
margin-left: -2rem
.icon-preview
font-size: 24px
margin-inline: 1rem

dt
font-weight: bold
.submit-button
margin-block-start: 2rem

dl,
dd
font-size: 0.9rem

dd
margin-bottom: 1em
.form-error
color: var(--warn-text)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
import { ApiLink, ApiResponse } from 'src/app/services/api-data-types';
import { PluginRegistryBaseService } from 'src/app/services/registry.service';
import { TAB_GROUP_NAME_OVERRIDES, TemplateApiObject, TemplateTabApiObject } from 'src/app/services/templates.service';
Expand Down Expand Up @@ -40,27 +40,50 @@ export class TemplateDetailsComponent implements OnInit {
private initialValues = {
name: "",
description: "",
icon: null,
sortKey: 0,
location: "workspace"
location: "workspace",
locationExtra: "",
groupKey: "",
};

templateForm: FormGroup = this.fb.group({
name: [this.initialValues.name, [Validators.required, Validators.minLength(1)]],
description: this.initialValues.description,
icon: [this.initialValues.locationExtra, [Validators.maxLength(64)]],
sortKey: this.initialValues.sortKey,
location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]]
location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]],
locationExtra: [this.initialValues.locationExtra],
groupKey: [this.initialValues.locationExtra, [Validators.maxLength(32)]],
});

constructor(private registry: PluginRegistryBaseService, private fb: FormBuilder) { }

ngOnInit() {
this.templateForm.addValidators((control): ValidationErrors | null => {
const loc = control.get("location")?.getRawValue() ?? "";
if (loc === "workspace") {
const groupKey = control.get("groupKey")?.getRawValue() ?? "";
if (groupKey) {
return {
groupKeyForbidden: true,
};
}
}
return null;
});
if (this.tabLink != null) {
this.registry.getByApiLink<TemplateTabApiObject>(this.tabLink).then(response => {
const location = response?.data?.location ?? this.initialValues.location
const [baseLocation, locationExtra] = location.split(".", 2);
this.templateForm.setValue({
name: response?.data?.name ?? this.initialValues.name,
description: response?.data?.description ?? this.initialValues.description,
icon: response?.data?.icon ?? this.initialValues.icon,
sortKey: response?.data?.sortKey ?? this.initialValues.sortKey,
location: response?.data?.location ?? this.initialValues.location
groupKey: response?.data?.groupKey ?? this.initialValues.groupKey,
location: baseLocation,
locationExtra: locationExtra ?? "",
});
});
}
Expand All @@ -84,12 +107,23 @@ export class TemplateDetailsComponent implements OnInit {
}
const link = response?.links?.find(link => link.rel.some(rel => rel === findString) && link.resourceType == "ui-template-tab") ?? null;
if (link != null) {
let iconValue = this.templateForm.value.icon;
if (!iconValue) {
iconValue = null;
}
const location = [this.templateForm.value.location]
if (this.templateForm.value.location !== "workspace" && this.templateForm.value.locationExtra) {
location.push(this.templateForm.value.locationExtra);
}
const groupKey = this.templateForm.value.groupKey;
this.registry.submitByApiLink<TemplateTabApiObject>(link, {
name: this.templateForm.value.name,
description: this.templateForm.value.description,
icon: iconValue,
sortKey: this.templateForm.value.sortKey,
filterString: this.filterString,
location: this.templateForm.value.location
groupKey: groupKey,
filterString: Boolean(groupKey) ? "" : this.filterString,
location: location.join("."),
});
if (this.templateLink != null) {
this.templateForm.reset(this.initialValues);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,19 @@ <h3 mat-subheader>Create New Template Tab</h3>
</mat-expansion-panel>
<mat-divider></mat-divider>

<ng-container *ngFor="let group of templateTabLinks | keyvalue:tabOrder; trackBy:trackByTabLink">
<h3 mat-subheader>{{tabGroupNameOverrides[group.key] ?? group.key}}</h3>
<mat-expansion-panel *ngFor="let tab of group.value" [expanded]="tabId == tab.resourceKey?.uiTemplateTabId"
(afterExpand)="selectTab(tab.resourceKey?.uiTemplateTabId ?? 'null')" (closed)="deselectTab(tab.resourceKey?.uiTemplateTabId ?? null)"
<ng-container *ngFor="let group of templateTabLinks; trackBy:trackByTabLink">
<h3 mat-subheader>{{tabGroupNameOverrides[group.group] ?? group.name}}</h3>
<mat-expansion-panel *ngFor="let tab of group.tabs" [expanded]="tabId == tab.resourceKey?.uiTemplateTabId"
(afterExpand)="selectTab(tab.resourceKey?.uiTemplateTabId ?? 'null')" (closed)="deselectTab(tab.resourceKey?.uiTemplateTabId ?? null)"
[id]="'tab-' + tab.resourceKey?.uiTemplateTabId">
<mat-expansion-panel-header>
<mat-panel-title>
{{tab.name}}
<span>
{{tab.name}}
<span *ngIf="templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.groupKey">
(Group: {{templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.groupKey}})
</span>
</span>
</mat-panel-title>
<mat-panel-description>
{{templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.description}}
Expand Down
Loading

0 comments on commit 6a6f3b8

Please sign in to comment.