diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index a5a3d7c..54f8e53 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -31,6 +31,8 @@ const routes: Routes = [
{ path: 'settings', component: SettingsPageComponent },
{ path: 'extra/:templateTabId', component: PluginTabComponent },
{ path: 'extra/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
+ { path: 'extra/:path/:templateTabId', component: PluginTabComponent },
+ { path: 'extra/:path/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
{ path: 'experiments', component: ExperimentsPageComponent },
{ path: 'experiments/:experimentId', redirectTo: "info" },
{ path: 'experiments/:experimentId/info', component: ExperimentComponent },
@@ -43,6 +45,8 @@ const routes: Routes = [
{ 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', component: PluginTabComponent },
+ { path: 'experiments/:experimentId/extra/:path/:templateTabId/plugin/:pluginId', component: PluginTabComponent },
];
@NgModule({
diff --git a/src/app/components/plugin-tab/plugin-tab.component.html b/src/app/components/plugin-tab/plugin-tab.component.html
index c89bdb7..716366f 100644
--- a/src/app/components/plugin-tab/plugin-tab.component.html
+++ b/src/app/components/plugin-tab/plugin-tab.component.html
@@ -1,10 +1,40 @@
-
-
-
+
+
+
+
+ 1 && (currentPluginGroup?.collectionSize ?? 0) < 6">
+
+
+
+
+
+
+
+
diff --git a/src/app/components/plugin-tab/plugin-tab.component.sass b/src/app/components/plugin-tab/plugin-tab.component.sass
index b8a83e9..06a4437 100644
--- a/src/app/components/plugin-tab/plugin-tab.component.sass
+++ b/src/app/components/plugin-tab/plugin-tab.component.sass
@@ -1,10 +1,60 @@
.big-nav-tabs
- border-color: var(--border-color)
+ --mdc-tab-indicator-active-indicator-color: var(--primary-text)
+ --mat-tab-header-active-label-text-color: var(--primary-text)
+ --mat-tab-header-active-ripple-color: var(--primary-text)
+ --mat-tab-header-inactive-ripple-color: var(--primary-text)
+ --mat-tab-header-active-focus-label-text-color: var(--primary-text)
+ --mat-tab-header-active-hover-label-text-color: var(--primary-text)
+ --mat-tab-header-active-focus-indicator-color: var(--primary-text)
+ --mat-tab-header-active-hover-indicator-color: var(--primary-text)
+ border-block-end: 1px solid var(--border-color)
.big-nav-tabs a
color: var(--text)
+.link-label
+ display: inline-flex
+ align-items: center
+ gap: 0.3rem
+
+.main-content
+ display: flex
+ flex-direction: row
+ width: 100%
+
+.sidebar
+ position: sticky
+ top: 1rem
+ margin-block: 1rem
+ margin-inline: 1rem
+ min-width: 15rem
+ width: 25vw
+ max-width: 22rem
+ max-height: calc( 100vh - 2rem )
+
+.sidebar-header
+ padding-block: 1rem
+ height: 2rem
+
+.sidebar-content-wrapper
+ margin-inline: -16px
+ margin-block-end: -16px
+ height: calc(100% - 4rem)
+
+.sidebar-content
+ display: block
+ box-sizing: border-box
+ padding-inline: 16px
+ padding-block-end: 1rem
+ height: 100%
+ overflow-y: auto
+ scrollbar-width: thin
+
+.sidebar-content ::ng-deep > ul
+ margin-inline: -16px
+
.plugin-frame
+ flex-grow: 1
display: flex
flex-direction: column
min-height: 50vh
diff --git a/src/app/components/plugin-tab/plugin-tab.component.ts b/src/app/components/plugin-tab/plugin-tab.component.ts
index 34d031e..99bc305 100644
--- a/src/app/components/plugin-tab/plugin-tab.component.ts
+++ b/src/app/components/plugin-tab/plugin-tab.component.ts
@@ -1,10 +1,22 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
-import { ApiLink, PageApiObject } from 'src/app/services/api-data-types';
+import { ApiLink, CollectionApiObject, PageApiObject } from 'src/app/services/api-data-types';
import { CurrentExperimentService } from 'src/app/services/current-experiment.service';
import { PluginApiObject } from 'src/app/services/qhana-api-data-types';
import { PluginRegistryBaseService } from 'src/app/services/registry.service';
+import { TemplateApiObject, TemplatesService, TemplateTabApiObject } from 'src/app/services/templates.service';
+
+
+interface NavTab {
+ tabId: string;
+ name: string;
+ icon: string | null;
+ path: string;
+ isGroup: boolean;
+ link: string[];
+}
+
@Component({
selector: 'qhana-plugin-tab',
@@ -14,114 +26,222 @@ import { PluginRegistryBaseService } from 'src/app/services/registry.service';
export class PluginTabComponent implements OnInit, OnDestroy {
private routeParamsSubscription: Subscription | null = null;
+ private currentTemplateSubscription: Subscription | null = null;
+ private templateTabUpdatesSubscription: Subscription | null = null;
- currentExperimentId: string | null = null
+ private currentTemplate: TemplateApiObject | null = null;
+ private currentTemplateTab: TemplateTabApiObject | null = null;
+
+ currentExperimentId: string | null = null;
+ currentPath: string | null = null;
+ currentTemplateId: string | null = null;
+ routeTemplateId: string | null = null;
currentTabId: string | null = null;
currentPluginId: string | null = null;
- plugins: ApiLink[] = [];
+ navigationTabs: NavTab[][] = [];
+ navTabLinkPrefix: string[] = [];
+
+ currentPluginGroup: PageApiObject | null = null;
activePlugin: ApiLink | null = null;
+ highlightedPlugin: Set = new Set();
activePluginFrontendUrl: string | null = null;
- constructor(private route: ActivatedRoute, private router: Router, private registry: PluginRegistryBaseService, private experiment: CurrentExperimentService) { }
+ constructor(private route: ActivatedRoute, private router: Router, private registry: PluginRegistryBaseService, private templates: TemplatesService, private experiment: CurrentExperimentService) { }
ngOnInit(): void {
+ this.currentTemplateSubscription = this.templates.currentTemplate.subscribe(template => {
+ this.currentTemplate = template;
+ this.onParamsChanged();
+ });
this.routeParamsSubscription = this.route.params.subscribe(params => {
this.currentExperimentId = params?.experimentId ?? null;
this.experiment.setExperimentId(params?.experimentId ?? null);
- const tabId = params?.templateTabId ?? null;
- this.updatePlugins(tabId);
- const pluginId = params?.pluginId ?? null;
- this.updatePluginId(pluginId);
+ this.currentPath = params?.path ?? null;
+ this.currentTabId = params?.templateTabId ?? null;
+ let pluginId = params?.pluginId ?? null;
+ if (this.currentTabId == null) {
+ pluginId = null;
+ }
+ this.currentPluginId = pluginId;
+ this.onParamsChanged();
+ });
+ this.templateTabUpdatesSubscription = this.templates.currentTemplateTabsUpdates.subscribe(() => {
+ // this.updateGeneralExtraTabGroup();
+ // this.updateExperimentExtraTabGroup();
});
}
ngOnDestroy(): void {
this.routeParamsSubscription?.unsubscribe();
+ this.currentTemplateSubscription?.unsubscribe();
+ this.templateTabUpdatesSubscription?.unsubscribe();
+ }
+
+ private getNavigationGroups(tab: TemplateTabApiObject) {
+ let groupLocation = tab.location;
+ if (tab.groupKey) {
+ groupLocation = `${tab.location}.${tab.groupKey}`;
+ }
+ return this.currentTemplate?.groups?.filter(group => {
+ const groupKey = group.resourceKey?.["?group"] ?? null;
+ if (!groupKey?.includes(".")) {
+ return false;
+ }
+ if (groupKey && groupLocation.startsWith(groupKey)) {
+ return true;
+ }
+ return false;
+ }) ?? [];
}
- private async updatePlugins(templateTabId: string | null) {
- if (templateTabId === this.currentTabId) {
+ private async onParamsChanged() {
+ if (this.currentTabId == null || this.currentTemplate == null) {
+ this.currentTemplateTab = null;
+ this.navigationTabs = [];
+ this.currentPluginGroup = null;
+ this.onPluginGroupChanged();
return;
}
- this.currentTabId = templateTabId;
- if (templateTabId == null) {
- this.plugins = [];
+ if (this.currentExperimentId == null) {
+ this.navTabLinkPrefix = ["/extra"];
+ } else {
+ this.navTabLinkPrefix = ["/experiments", this.currentExperimentId, "extra"];
+ }
+
+ await this.loadTab();
+ await this.loadPluginGroup();
+ await this.loadPlugin();
+ }
+
+ private async loadTab() {
+ if (this.currentTemplateTab?.self?.resourceKey?.templateTabId === this.currentTabId) {
+ return;
+ }
+ if (this.currentTemplate == null) {
return;
}
- const plugins: ApiLink[] = [];
- this.plugins = plugins;
+ const templateResponse = await this.registry.getByApiLink(this.currentTemplate.self, null, false);
+ const tabsLink = templateResponse?.links?.find(link => link.resourceType === "ui-template-tab" && link.rel.some(r => r === "collection"));
+ if (tabsLink == null) {
+ return;
+ }
- const query = new URLSearchParams();
- query.set("template-tab", templateTabId);
- const pluginsResponse = await this.registry.getByRel([["plugin", "collection"]], query);
+ const allTabs = await this.registry.getByApiLink(tabsLink, null, true);
+ const tabLink = allTabs?.data?.items?.find(tab => tab.resourceKey?.uiTemplateTabId === this.currentTabId);
+ if (tabLink == null) {
+ return;
+ }
+ const tab = await this.registry.getByApiLink(tabLink, null, false);
+ this.currentTemplateTab = tab?.data ?? null;
- if ((pluginsResponse?.data?.collectionSize ?? 0) < 25) {
- pluginsResponse?.data?.items?.forEach(pluginLink => plugins.push(pluginLink));
- this.onPluginIdChanges(this.currentPluginId, true);
+ if (tab == null) {
+ return;
}
+
+ const groups = this.getNavigationGroups(tab.data);
+ groups.sort((a, b) => (a.resourceKey?.["?group"]?.length ?? 0) - (b.resourceKey?.["?group"]?.length ?? 0));
+
+ const navigationTabs: NavTab[][] = await Promise.all(groups.map(group => {
+ const filtered = allTabs?.data?.items?.filter(tab => tab.resourceKey?.["?group"] != null && tab.resourceKey["?group"] === group.resourceKey?.["?group"]) ?? [];
+ const promises = filtered.map(tabLink => {
+ return this.registry.getByApiLink(tabLink, null, false).then(tab => {
+ const link: string[] = [];
+ if (tab?.data?.location) {
+ link.push(tab?.data?.location);
+ }
+ link.push(tabLink.resourceKey?.uiTemplateTabId ?? "-1");
+ const t: NavTab = {
+ tabId: tab?.data?.self?.resourceKey?.uiTemplateTabId ?? "-1",
+ name: tab?.data?.name ?? tabLink.name ?? "UNNAMED TAB",
+ icon: tab?.data?.icon ?? null,
+ path: (tab?.data?.location ?? "") + ".",
+ isGroup: Boolean(tab?.data?.groupKey),
+ link: link,
+ }
+ return t;
+ });
+ });
+ return Promise.all(promises);
+ }));
+ this.navigationTabs = navigationTabs;
}
- private updatePluginId(pluginId: string | null) {
- if (pluginId === this.currentPluginId) {
+ private async loadPluginGroup() {
+ if (this.currentTemplateTab?.self?.resourceKey?.uiTemplateTabId == null || this.currentTemplateTab.groupKey) {
+ this.currentPluginGroup = null;
+ await this.onPluginGroupChanged();
return;
}
- this.currentPluginId = pluginId;
- this.onPluginIdChanges(pluginId);
+
+ const tabId = this.currentTemplateTab.self.resourceKey.uiTemplateTabId;
+ if (tabId === this.currentPluginGroup?.self?.resourceKey?.["?template-tab"]) {
+ return; // plugin group already loaded
+ }
+
+ const query = new URLSearchParams();
+ query.set("template-tab", tabId);
+ const pluginsResponse = await this.registry.getByRel([["plugin", "collection"]], query);
+ this.currentPluginGroup = pluginsResponse?.data ?? null;
+ await this.onPluginGroupChanged();
}
- private async onPluginIdChanges(pluginId: string | null, navigate = false) {
- if (pluginId == null) {
- if (this.plugins.length === 0) {
- this.updateActivePlugin(null, navigate);
- } else {
- this.updateActivePlugin(this.plugins[0], navigate);
- }
+ private async onPluginGroupChanged() {
+ if (this.currentPluginGroup?.collectionSize === 1) {
+ this.activePlugin = this.currentPluginGroup.items[0] ?? null;
+ await this.onActivePluginChanged();
return;
}
- let pluginLink = this.plugins.find(plugin => plugin.resourceKey?.pluginId === pluginId);
- if (pluginLink != null) {
- this.updateActivePlugin(pluginLink, navigate);
+ if (this.currentPluginId == null) {
+ this.activePlugin = null;
+ await this.onActivePluginChanged();
return;
}
+ }
- const query = new URLSearchParams();
- query.set("plugin-id", pluginId);
- if (this.currentTabId != null) {
- query.set("template-tab", this.currentTabId);
+ private async loadPlugin() {
+ if (this.currentPluginId == null || (this.activePlugin?.resourceKey?.pluginId ?? null) === this.currentPluginId) {
+ return;
}
- const pluginPageResponse = await this.registry.getByRel([["plugin", "collection"]], query);
- pluginLink = pluginPageResponse?.data?.items?.[0];
- if (pluginLink != null) {
- this.updateActivePlugin(pluginLink, navigate);
- return;
+ let pluginLink = this.currentPluginGroup?.items?.find(link => link.resourceKey?.pluginId === this.currentPluginId) ?? null;
+
+ if (pluginLink == null && this.currentPluginGroup != null) {
+ const query = new URLSearchParams();
+ query.set("plugin-id", this.currentPluginId);
+ const page = await this.registry.getByApiLink(this.currentPluginGroup.self, query);
+ if (page?.data?.items?.[0]?.resourceKey?.pluginId === this.currentPluginId) {
+ pluginLink = page.data.items[0];
+ }
}
- }
- private async updateActivePlugin(pluginLink: ApiLink | null, navigate: boolean = false) {
this.activePlugin = pluginLink;
+ await this.onActivePluginChanged();
+ }
- if (pluginLink == null) {
+ private async onActivePluginChanged() {
+ if (this.activePlugin == null) {
this.activePluginFrontendUrl = null;
- } else {
- const pluginResponse = await this.registry.getByApiLink(pluginLink);
- this.activePluginFrontendUrl = pluginResponse?.data?.entryPoint?.uiHref ?? null; // FIXME for relative URLs!
+ this.highlightedPlugin = new Set();
+ return;
}
- if (navigate) {
- const experimentPrefix = this.currentExperimentId != null ? ['/experiments', this.currentExperimentId] : [];
- if (pluginLink != null) {
- this.router.navigate([...experimentPrefix, 'extra', this.currentTabId, 'plugin', pluginLink.resourceKey?.pluginId], { queryParamsHandling: 'preserve' });
- } else {
- this.router.navigate([...experimentPrefix, 'extra', this.currentTabId], { queryParamsHandling: 'preserve' });
- }
- }
+ this.highlightedPlugin = new Set([this.activePlugin.resourceKey?.pluginId ?? ""]);
+
+ const pluginResponse = await this.registry.getByApiLink(this.activePlugin);
+ this.activePluginFrontendUrl = pluginResponse?.data?.entryPoint?.uiHref ?? null; // FIXME for relative URLs!
+ }
+
+ selectPlugin(plugin: ApiLink) {
+ this.activePlugin = plugin;
+ this.onActivePluginChanged();
+
+ this.router.navigate([...this.navTabLinkPrefix, this.currentTabId, 'plugin', plugin.resourceKey?.pluginId], { queryParamsHandling: 'preserve' });
}
}