From 8a5c42faf8f30ab9004bb0f06044b81b83328993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:29:32 +0200 Subject: [PATCH 01/20] Fix subscriptions in growing list setup more than once --- .../growing-list/growing-list.component.ts | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/app/components/growing-list/growing-list.component.ts b/src/app/components/growing-list/growing-list.component.ts index 0720c79..dc3ede4 100644 --- a/src/app/components/growing-list/growing-list.component.ts +++ b/src/app/components/growing-list/growing-list.component.ts @@ -76,19 +76,39 @@ export class GrowingListComponent implements OnInit, OnDestroy { })).subscribe(); this.setupGrowingList(); + + + this.newItemsSubscription = this.registry.newApiObjectSubject + .pipe(filter(newObject => this.newItemRels != null && matchesLinkRel(newObject.new, this.newItemRels))) + .subscribe(newObject => this.updateQueue.next(() => this.onNewObjectQueued(newObject.new))); + this.changedItemsSubscription = this.registry.changedApiObjectSubject + .pipe(filter(changedObject => { + const isInList = this.items.some(item => item.href === changedObject.changed.href); + const maybeNew = this.newItemRels != null && matchesLinkRel(changedObject.changed, this.newItemRels); + return isInList || maybeNew; + })) + .subscribe(changedObject => this.updateQueue.next(() => this.onChangedObjectQueued(changedObject.changed))); + this.deletedItemsSubscription = this.registry.deletedApiObjectSubject + .pipe(filter(deletedObject => this.items.some(item => item.href === deletedObject.deleted.href))) + .subscribe(deletedObject => this.updateQueue.next(() => this.onDeletedObjectQueued(deletedObject.deleted))); } ngOnDestroy(): void { - this.unsubscribeAll(); + this.updateQueueSubscription?.unsubscribe(); + this.newItemsSubscription?.unsubscribe(); + this.changedItemsSubscription?.unsubscribe(); + this.deletedItemsSubscription?.unsubscribe(); } ngOnChanges(changes: SimpleChanges): void { - if (changes.apiLink?.previousValue || changes.rels?.previousValue || changes.query?.previousValue || changes.newItemRels?.previousValue) { - this.unsubscribeAll(); - this.newItemsSubscription = null; - this.changedItemsSubscription = null; - this.deletedItemsSubscription = null; - this.updateQueueSubscription = null; + let linkChanged = false; + if (changes.apiLink) { + // only consider link changed when href changes + if (changes.apiLink?.previousValue?.href !== changes.apiLink?.currentValue?.href) { + linkChanged = true; + } + } + if (linkChanged || changes.rels?.previousValue || changes.query?.previousValue) { this.startApiLink = null; this.startQueryArgs = null; this.loadMoreApiLink = null; @@ -100,13 +120,6 @@ export class GrowingListComponent implements OnInit, OnDestroy { } } - private unsubscribeAll(): void { - this.newItemsSubscription?.unsubscribe(); - this.changedItemsSubscription?.unsubscribe(); - this.deletedItemsSubscription?.unsubscribe(); - this.updateQueueSubscription?.unsubscribe(); - } - private setupGrowingList() { if (this.apiLink != null) { this.replaceApiLink(this.apiLink); @@ -116,24 +129,6 @@ export class GrowingListComponent implements OnInit, OnDestroy { this.registry.resolveRecursiveRels(rels).then((apiLink) => this.replaceApiLink(apiLink)); } } - - // handle api updates - const newItemRels = this.newItemRels; - if (newItemRels) { - this.newItemsSubscription = this.registry.newApiObjectSubject - .pipe(filter(newObject => matchesLinkRel(newObject.new, newItemRels))) - .subscribe(newObject => this.updateQueue.next(() => this.onNewObjectQueued(newObject.new))); - } - this.changedItemsSubscription = this.registry.changedApiObjectSubject - .pipe(filter(changedObject => { - const isInList = this.items.some(item => item.href === changedObject.changed.href); - const maybeNew = newItemRels != null && matchesLinkRel(changedObject.changed, newItemRels); - return isInList || maybeNew; - })) - .subscribe(changedObject => this.updateQueue.next(() => this.onChangedObjectQueued(changedObject.changed))); - this.deletedItemsSubscription = this.registry.deletedApiObjectSubject - .pipe(filter(deletedObject => this.items.some(item => item.href === deletedObject.deleted.href))) - .subscribe(deletedObject => this.updateQueue.next(() => this.onDeletedObjectQueued(deletedObject.deleted))); } replaceApiLink(newApiLink: ApiLink): void { From c1c8463c465bc9578f590a9a3ffef37f9183c0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:30:14 +0200 Subject: [PATCH 02/20] Add util methods to template service --- src/app/services/templates.service.ts | 29 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/services/templates.service.ts b/src/app/services/templates.service.ts index 319303b..66872cf 100644 --- a/src/app/services/templates.service.ts +++ b/src/app/services/templates.service.ts @@ -73,20 +73,33 @@ export class TemplatesService { } // need to fetch template resource from plugin registry + const defaultTemplate = await this.getTemplate(defaultTemplateId.toString(), true); + if (defaultTemplate != null) { + this.defaultTemplateSubject.next(defaultTemplate?.data ?? null); + } + } + + async addTemplate(newTemplate: TemplateApiObject) { + const createLink = await this.registry.searchResolveRels(["create", "ui-template"]); + return this.registry.submitByApiLink(createLink, newTemplate); + } + + async getTemplate(templateId: string, ignoreCache: boolean | "ignore-embedded" = true) { const query = new URLSearchParams(); - query.set("template-id", defaultTemplateId.toString()); - const templatePage = await this.registry.getByRel([["ui-template", "collection"]], query, true); + query.set("template-id", templateId.toString()); + const templatePage = await this.registry.getByRel([["ui-template", "collection"]], query, ignoreCache); if (templatePage?.data.collectionSize === 1) { // only expect one template since IDs are unique - const templateResponse = await this.registry.getByApiLink(templatePage.data.items[0], null, false); - this.defaultTemplateSubject.next(templateResponse?.data ?? null); + return await this.registry.getByApiLink(templatePage.data.items[0], null, false); } else { - console.warn(`Template API returned an ambiguous response for template id ${defaultTemplateId}`, templatePage); + console.warn(`Template API returned an ambiguous response for template id ${templateId}`, templatePage); + return null; } } - async addTemplate(newTemplate: TemplateApiObject) { - const createLink = await this.registry.searchResolveRels(["create", "ui-template"]); - return this.registry.submitByApiLink(createLink, newTemplate); + async getTemplateTabGroups(templateId: string, ignoreCache: boolean | "ignore-embedded" = false) { + const templateResponse = await this.getTemplate(templateId, ignoreCache); + console.log(templateResponse) + return templateResponse?.data?.groups ?? []; } } From 1c5090ea1e72d0a2b472a9b007eccc089477a6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:42:19 +0200 Subject: [PATCH 03/20] Remove unused test spec --- src/app/services/templates.service.spec.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/app/services/templates.service.spec.ts diff --git a/src/app/services/templates.service.spec.ts b/src/app/services/templates.service.spec.ts deleted file mode 100644 index 2203db7..0000000 --- a/src/app/services/templates.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { TemplatesService } from './templates.service'; - - -describe('TemplateService', () => { - let service: TemplatesService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(TemplatesService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); From ac97a9471a551fc183fb7a2ef91b4f881b26116b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:44:03 +0200 Subject: [PATCH 04/20] Track new ENV var in env service Auto-load DEFAULT_UI_TEMPLATE, which stores a ui template id. --- src/app/services/env.service.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/services/env.service.ts b/src/app/services/env.service.ts index be529ba..8436d7a 100644 --- a/src/app/services/env.service.ts +++ b/src/app/services/env.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { ApiObject, isApiObject } from './api-data-types'; +import { ApiObject, isApiObject, isDeletedApiObject } from './api-data-types'; import { PluginRegistryBaseService } from './registry.service'; @@ -51,15 +51,21 @@ export class EnvService { private lastMapStr: string | null = null; private urlMapSubject: BehaviorSubject | null> = new BehaviorSubject | null>(null); + private uiTemplateSubject: BehaviorSubject = new BehaviorSubject(null); public get urlMap(): Observable | null> { return this.urlMapSubject.asObservable(); } + public get uiTemplateId(): Observable { + return this.uiTemplateSubject.asObservable(); + } + constructor(private registryService: PluginRegistryBaseService) { this.subscribe(); // just kick off get, updates are handled by subscription setup above this.registryService.getByRel([["env", "collection"]], new URLSearchParams([["name", "UI_URL_MAP"]]), true); + this.registryService.getByRel([["env", "collection"]], new URLSearchParams([["name", "DEFAULT_UI_TEMPLATE"]]), true); } private unsubscribe() { @@ -69,12 +75,20 @@ export class EnvService { private subscribe() { this.registrySubscription = this.registryService.apiObjectSubject.subscribe((apiObject => { if (apiObject.self.resourceType !== "env") { - return; // only look at service api objects + return; // only look at env api objects } if (isEnvApiObject(apiObject)) { if (apiObject.name === "UI_URL_MAP") { this.updateUrlMap(apiObject.value); } + if (apiObject.name === "DEFAULT_UI_TEMPLATE") { + this.uiTemplateSubject.next(apiObject.value); + } + } + if (isDeletedApiObject(apiObject)) { + if (apiObject.deleted.name === "DEFAULT_UI_TEMPLATE") { + this.uiTemplateSubject.next(null); + } } })); } From 849f80d8ad17f8d7c40814eff17dd3dacc613f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:53:11 +0200 Subject: [PATCH 05/20] Refactor templates service, add tab group component Refactor templates service to include more utility methods to be used by other components using ui templates. Templates service now has subscriptions for the current and the default template. Templates service can get default template from Experiment and env vars. Refactor navbar to use the changed templates service. Refactor navbar to only show experiment navigation tabs when an experiment is active. Extract template tab details from plugin sidebar into dedicated component. --- src/app/app.module.ts | 2 + src/app/components/navbar/navbar.component.ts | 123 ++++++++----- .../plugin-sidebar.component.html | 27 ++- .../plugin-sidebar.component.ts | 34 ++-- .../tab-group-list.component.html | 6 + .../tab-group-list.component.sass | 7 + .../tab-group-list.component.spec.ts | 23 +++ .../tab-group-list.component.ts | 165 ++++++++++++++++++ src/app/services/templates.service.ts | 151 ++++++++++++++-- 9 files changed, 444 insertions(+), 94 deletions(-) create mode 100644 src/app/components/tab-group-list/tab-group-list.component.html create mode 100644 src/app/components/tab-group-list/tab-group-list.component.sass create mode 100644 src/app/components/tab-group-list/tab-group-list.component.spec.ts create mode 100644 src/app/components/tab-group-list/tab-group-list.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 56c65f4..9b93a4b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -82,6 +82,7 @@ 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 { TabGroupListComponent } from './components/tab-group-list/tab-group-list.component'; @NgModule({ declarations: [ @@ -124,6 +125,7 @@ import { ExperimentWorkspaceDetailComponent } from './components/experiment-work TemplateDetailsComponent, ExperimentWorkspaceDetailComponent, ImportExperimentComponent, + TabGroupListComponent, ], imports: [ BrowserModule, diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts index 0a32108..c2ba4fc 100644 --- a/src/app/components/navbar/navbar.component.ts +++ b/src/app/components/navbar/navbar.component.ts @@ -14,14 +14,14 @@ * limitations under the License. */ import { Component, Input, OnDestroy, OnInit, TrackByFunction } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { ApiLink, CollectionApiObject } from 'src/app/services/api-data-types'; import { CurrentExperimentService } from 'src/app/services/current-experiment.service'; -import { PluginRegistryBaseService } from 'src/app/services/registry.service'; -import { TemplateApiObject, TemplatesService } from 'src/app/services/templates.service'; import { DownloadsService } from 'src/app/services/downloads.service'; import { ExportResult, QhanaBackendService } from 'src/app/services/qhana-backend.service'; -import { ActivatedRoute } from '@angular/router'; +import { PluginRegistryBaseService } from 'src/app/services/registry.service'; +import { TemplateApiObject, TemplatesService } from 'src/app/services/templates.service'; @Component({ selector: 'qhana-navbar', @@ -39,15 +39,20 @@ export class NavbarComponent implements OnInit, OnDestroy { exportList: Observable | null = null; error: string | null = null; + generalExtraTabsGroupLink: ApiLink | null = null; + generalExtraTabs: ApiLink[] = []; + + experimentExtraTabsGroupLink: ApiLink | null = null; + experimentExtraTabs: ApiLink[] = []; + extraTabs: ApiLink[] = []; templateId: string | null = null; template: TemplateApiObject | null = null; + private defaultTemplateIdSubscription: Subscription | null = null; private defaultTemplateSubscription: Subscription | null = null; - private routeParamSubscription: Subscription | null = null; - private changedTemplateTabSubscription: Subscription | null = null; - private deletedTemplateTabSubscription: Subscription | null = null; + private templateTabUpdatesSubscription: Subscription | null = null; constructor(private route: ActivatedRoute, private experiment: CurrentExperimentService, private templates: TemplatesService, private registry: PluginRegistryBaseService, private backend: QhanaBackendService, private downloadService: DownloadsService) { this.currentExperiment = this.experiment.experimentName; @@ -55,44 +60,25 @@ export class NavbarComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.routeParamSubscription = this.route.queryParamMap.subscribe(async params => { - const templateId = params.get('template'); - if (templateId != null) { - this.registry.getByRel(["ui-template", "collection"], new URLSearchParams([["template-id", templateId]])).then(response => { - if (response?.data?.items?.length == 1) { - this.registry.getByApiLink(response.data.items[0]).then(template => { - this.template = template?.data ?? null; - this.onTemplateChanges(template?.data ?? null); - }); - } else { - this.onTemplateChanges(null); - } - }); - } - this.templateId = templateId; - }); this.registerSubscriptions(); this.downloadBadgeCounter = this.downloadService.getDownloadsCounter(); this.exportList = this.downloadService.getExportList(); } + ngOnDestroy(): void { + this.defaultTemplateIdSubscription?.unsubscribe(); + this.defaultTemplateSubscription?.unsubscribe(); + this.templateTabUpdatesSubscription?.unsubscribe(); + } + private registerSubscriptions() { - this.defaultTemplateSubscription = this.templates.defaultTemplate.subscribe(template => { + this.defaultTemplateIdSubscription = this.templates.currentTemplateId.subscribe(templateId => this.templateId = templateId); + this.defaultTemplateSubscription = this.templates.currentTemplate.subscribe(template => { this.onTemplateChanges(template); }); - this.changedTemplateTabSubscription = this.registry.changedApiObjectSubject.subscribe(async (apiObject) => { - if (apiObject.changed.resourceKey?.uiTemplateId === this.templateId) { - if (this.template != null) { - const response = await this.registry.getByApiLink(this.template?.self); - this.template = response?.data ?? null; - } - this.onTemplateChanges(this.template); - } - }); - this.deletedTemplateTabSubscription = this.registry.deletedApiObjectSubject.subscribe(async (apiObject) => { - if (apiObject.deleted.resourceKey?.uiTemplateId === this.templateId && apiObject.deleted.resourceKey?.["?group"] === "experiment-navigation") { - this.onTemplateChanges(this.template); - } + this.templateTabUpdatesSubscription = this.templates.currentTemplateTabsUpdates.subscribe(() => { + this.updateGeneralExtraTabGroup(); + this.updateExperimentExtraTabGroup(); }); } @@ -106,28 +92,71 @@ export class NavbarComponent implements OnInit, OnDestroy { return this.downloadBadgeCounter?.subscribe(); } - ngOnDestroy(): void { - this.defaultTemplateSubscription?.unsubscribe(); - this.routeParamSubscription?.unsubscribe(); - this.changedTemplateTabSubscription?.unsubscribe(); - this.deletedTemplateTabSubscription?.unsubscribe(); - } - private async onTemplateChanges(template: TemplateApiObject | null) { if (template == null) { this.extraTabs = []; return; } + const experimentNavGroup = template.groups.find(group => group.resourceKey?.["?group"] === "experiment-navigation") ?? null; + const experimentTabsLinkChanged = this.experimentExtraTabsGroupLink?.href !== experimentNavGroup?.href; + this.experimentExtraTabsGroupLink = experimentNavGroup; + + const generalNavGroup = template.groups.find(group => group.resourceKey?.["?group"] === "navigation") ?? null; + const generalTabsLinkChanged = this.experimentExtraTabsGroupLink?.href !== experimentNavGroup?.href; + this.generalExtraTabsGroupLink = generalNavGroup; + + if (experimentTabsLinkChanged) { + this.updateExperimentExtraTabGroup(); + } + if (generalTabsLinkChanged) { + this.updateGeneralExtraTabGroup(); + } + } + + private async updateExperimentExtraTabGroup() { + const groupLink = this.experimentExtraTabsGroupLink; + if (groupLink == null) { + this.experimentExtraTabs = []; + this.experimentExtraTabsGroupLink = null; + this.updateExtraTabs(); + return; + } + + const groupResponse = await this.registry.getByApiLink(groupLink, null, true); const extraTabs: ApiLink[] = []; - this.extraTabs = extraTabs; - const navGroup = template.groups.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); - if (navGroup == null) { + groupResponse?.data?.items?.forEach(tab => extraTabs.push(tab)); + + this.experimentExtraTabs = extraTabs; + this.updateExtraTabs(); + } + + private async updateGeneralExtraTabGroup() { + const groupLink = this.generalExtraTabsGroupLink; + if (groupLink == null) { + this.generalExtraTabs = []; + this.generalExtraTabsGroupLink = null; + this.updateExtraTabs(); return; } - const groupResponse = await this.registry.getByApiLink(navGroup); + + const groupResponse = await this.registry.getByApiLink(groupLink, null, true); + + const extraTabs: ApiLink[] = []; groupResponse?.data?.items?.forEach(tab => extraTabs.push(tab)); + + this.generalExtraTabs = extraTabs; + this.updateExtraTabs(); + } + + private updateExtraTabs() { + if (this.experimentId != null) { + // only show experiment navigatio tabs if an experiment is active + this.extraTabs = this.experimentExtraTabs; + } else { + this.extraTabs = this.generalExtraTabs; + } } } diff --git a/src/app/components/plugin-sidebar/plugin-sidebar.component.html b/src/app/components/plugin-sidebar/plugin-sidebar.component.html index a17ec9b..02ebdb5 100644 --- a/src/app/components/plugin-sidebar/plugin-sidebar.component.html +++ b/src/app/components/plugin-sidebar/plugin-sidebar.component.html @@ -31,8 +31,14 @@

- + + +
- - - - - - - - +
diff --git a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts index 24c59ad..328382c 100644 --- a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts +++ b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts @@ -40,8 +40,6 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { highlightedPlugins: Set = new Set(); - highlightedTemplateTabs: Set = new Set(); - defaultPluginGroups: PluginGroup[] = []; defaultTemplate: TemplateApiObject | null = null; @@ -49,6 +47,7 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { pluginGroups: PluginGroup[] = this.defaultPluginGroups; // route params + useDefaultTemplate: boolean = true; templateId: string | null = null; pluginId: string | null = null; tabId: string | null = null; @@ -65,7 +64,12 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { ngOnInit(): void { this.routeParamSubscription = this.route.queryParamMap.subscribe(params => { - this.templateId = params.get('template'); + let templateId = params.get('template'); + this.useDefaultTemplate = templateId !== "all-plugins"; + if (templateId === "all-plugins") { + templateId = null; + } + this.templateId = templateId; this.pluginId = params.get('plugin'); this.tabId = params.get('tab'); if (this.templateId != null) { @@ -79,12 +83,10 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { this.highlightedPlugins.clear(); } if (this.tabId != null) { - this.highlightedTemplateTabs = new Set([this.tabId]); if (this.activeArea !== "detail") { this.switchActiveArea("detail"); } } else { - this.highlightedTemplateTabs.clear(); if (this.activeArea === "detail") { this.switchActiveArea("plugins"); } @@ -108,7 +110,7 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { this.templates.defaultTemplate.subscribe(template => { this.defaultTemplate = template; - if (this.templateId == null) { + if (this.templateId == null && this.useDefaultTemplate) { this.switchActiveTemplateLink(template?.self ?? null); } }); @@ -210,11 +212,15 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { private switchActiveTemplateLink(activeTemplate: ApiLink | null) { if (activeTemplate == null) { - this.selectedTemplate = null; - this.selectedTemplateName = "All Plugins"; - this.activeArea = "plugins"; - this.pluginGroups = this.defaultPluginGroups; - return; + if (this.useDefaultTemplate && this.defaultTemplate != null) { + activeTemplate = this.defaultTemplate.self; + } else { + this.selectedTemplate = null; + this.selectedTemplateName = "All Plugins"; + this.activeArea = "plugins"; + this.pluginGroups = this.defaultPluginGroups; + return; + } } const tabId = this.route.snapshot.queryParamMap.get('tab'); this.selectedTemplate = activeTemplate; @@ -332,10 +338,12 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { } } - selectTemplate(templateLink: ApiLink | null) { + selectTemplate(templateLink: ApiLink | null, specialTemplateId: "all-plugins" | null = null) { + this.useDefaultTemplate = specialTemplateId == null; + this.switchActiveTemplateLink(templateLink); if (templateLink == null) { - this.navigate(null, null, null); + this.navigate(specialTemplateId, null, null); return; } this.navigate(templateLink.resourceKey?.uiTemplateId ?? null, null, null); diff --git a/src/app/components/tab-group-list/tab-group-list.component.html b/src/app/components/tab-group-list/tab-group-list.component.html new file mode 100644 index 0000000..77c3b54 --- /dev/null +++ b/src/app/components/tab-group-list/tab-group-list.component.html @@ -0,0 +1,6 @@ + + + + diff --git a/src/app/components/tab-group-list/tab-group-list.component.sass b/src/app/components/tab-group-list/tab-group-list.component.sass new file mode 100644 index 0000000..d4f2d7c --- /dev/null +++ b/src/app/components/tab-group-list/tab-group-list.component.sass @@ -0,0 +1,7 @@ +.sidebar-header + width: 100% + margin-left: 1rem + +.tab-list + display: block + margin-block-end: 3rem diff --git a/src/app/components/tab-group-list/tab-group-list.component.spec.ts b/src/app/components/tab-group-list/tab-group-list.component.spec.ts new file mode 100644 index 0000000..6279ce9 --- /dev/null +++ b/src/app/components/tab-group-list/tab-group-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabGroupListComponent } from './tab-group-list.component'; + +describe('TabGroupListComponent', () => { + let component: TabGroupListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TabGroupListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TabGroupListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/tab-group-list/tab-group-list.component.ts b/src/app/components/tab-group-list/tab-group-list.component.ts new file mode 100644 index 0000000..d276ccb --- /dev/null +++ b/src/app/components/tab-group-list/tab-group-list.component.ts @@ -0,0 +1,165 @@ +/* + * Copyright 2023 University of Stuttgart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { ApiLink } from 'src/app/services/api-data-types'; +import { PluginRegistryBaseService } from 'src/app/services/registry.service'; +import { TemplatesService } from 'src/app/services/templates.service'; + + +const TAB_GROUP_SORT_KEYS: { [group: string]: number } = { + "DEFAULT": 10000, + "workspace": 10, + "experiment-navigation": 20, +} + +const TAB_GROUP_NAME_OVERRIDES: { [group: string]: string } = { + "workspace": "Workspace Tabs (Sidebar)", + "experiment-navigation": "Experiment Navigation Tabs", +} + +@Component({ + selector: 'qhana-tab-group-list', + templateUrl: './tab-group-list.component.html', + styleUrls: ['./tab-group-list.component.sass'] +}) +export class TabGroupListComponent implements OnChanges, OnInit, OnDestroy { + + @Input() templateId: string | null = null; + @Input() selectedTab: string | number | null = null; + + @Output() clickedOnTab: EventEmitter = new EventEmitter(true); + + private newObjectsSubscription: Subscription | null = null; + private changedObjectsSubscritions: Subscription | null = null; + private deletedObjectsSubscription: Subscription | null = null; + + tabGroups: ApiLink[] = [] + + highlightedTemplateTabs: Set = new Set(); + + constructor(private templates: TemplatesService, private registry: PluginRegistryBaseService) { } + + ngOnInit(): void { + this.newObjectsSubscription = this.registry.newApiObjectSubject.subscribe(newApiObject => { + if (newApiObject.new.resourceType === "ui-template-tab") { + if (newApiObject.new.resourceKey?.uiTemplateId === this.templateId) { + this.reloadTabGroups(this.templateId); + } + } + }); + this.changedObjectsSubscritions = this.registry.changedApiObjectSubject.subscribe(changedApiObject => { + if (changedApiObject.changed.resourceType === "ui-template-tab" || changedApiObject.changed.resourceType === "ui-template") { + if (changedApiObject.changed.resourceKey?.uiTemplateId === this.templateId) { + this.reloadTabGroups(this.templateId); + } + } + }); + this.deletedObjectsSubscription = this.registry.deletedApiObjectSubject.subscribe(deletedApiObject => { + if (deletedApiObject.deleted.resourceType === "ui-template-tab") { + if (deletedApiObject.deleted.resourceKey?.uiTemplateId === this.templateId) { + this.reloadTabGroups(this.templateId); + } + } + if (deletedApiObject.deleted.resourceType === "ui-template") { + if (deletedApiObject.deleted.resourceKey?.uiTemplateId === this.templateId) { + this.reloadTabGroups(null); + } + } + }); + } + + ngOnDestroy(): void { + this.newObjectsSubscription?.unsubscribe(); + this.changedObjectsSubscritions?.unsubscribe(); + this.deletedObjectsSubscription?.unsubscribe(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.selectedTab != null) { + this.highlightedTemplateTabs.clear(); + if (this.selectedTab != null) { + this.highlightedTemplateTabs.add(this.selectedTab.toString()); + } + } + if (changes.templateId) { + this.reloadTabGroups(this.templateId); + } + } + + private async reloadTabGroups(templateId: string | null, ignoreCache: boolean = false) { + if (templateId == null) { + this.tabGroups = []; + return; + } + + const tabGroups = await this.templates.getTemplateTabGroups(templateId, ignoreCache); + tabGroups.sort((groupA, groupB) => { + const defaultSort = TAB_GROUP_SORT_KEYS["DEFAULT"]; + const a = TAB_GROUP_SORT_KEYS[groupA.resourceKey?.["?group"] ?? "DEFAULT"] ?? defaultSort; + const b = TAB_GROUP_SORT_KEYS[groupB.resourceKey?.["?group"] ?? "DEFAULT"] ?? defaultSort; + if (a !== b) { + return a - b; + } + const nameA = this.getTabName(groupA, groupA.href); + const nameB = this.getTabName(groupB, groupB.href); + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; + }); + this.tabGroups = tabGroups; + } + + + trackByHref(index: number, apiLink: ApiLink): string { + return apiLink.href; + } + + getTabName(tabGroupLink: ApiLink, fallback: string = ""): string { + const group = tabGroupLink.resourceKey?.['?group']; + if (group != null) { + const override = TAB_GROUP_NAME_OVERRIDES[group]; + if (override != null) { + return override; + } + } + return tabGroupLink.name ?? group ?? fallback; + } + + getTabFilter(groupLink: ApiLink) { + return (tabLink: ApiLink): boolean => { + if (tabLink.resourceKey == null) { + return false; + } + if (tabLink.resourceKey?.["?group"] !== groupLink.resourceKey?.["?group"]) { + return false; + } + if (tabLink.resourceKey?.templateId !== groupLink.resourceKey?.templateId) { + return false; + } + return true; + } + } + + selectTab(tabLink: ApiLink | null) { + this.clickedOnTab.emit(tabLink); + } + +} diff --git a/src/app/services/templates.service.ts b/src/app/services/templates.service.ts index 66872cf..296aa5c 100644 --- a/src/app/services/templates.service.ts +++ b/src/app/services/templates.service.ts @@ -15,11 +15,13 @@ */ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subject, Subscription, combineLatest } from 'rxjs'; import { ApiLink, ApiObject, PageApiObject } from './api-data-types'; import { CurrentExperimentService } from './current-experiment.service'; -import { ExperimentApiObject } from './qhana-backend.service'; import { PluginRegistryBaseService } from './registry.service'; +import { EnvService } from './env.service'; +import { distinctUntilChanged } from 'rxjs/operators'; +import { ActivatedRoute } from '@angular/router'; export interface TemplateApiObject extends ApiObject { // TODO check fields @@ -45,38 +47,153 @@ export interface TemplateTabApiObject extends ApiObject { // TODO check fields }) export class TemplatesService { + private envSubscription: Subscription | null = null; private currentExperimentSubscription: Subscription | null = null; + private routeSubscription: Subscription | null = null; + private newObjectsSubscription: Subscription | null = null; + private changedObjectsSubscritions: Subscription | null = null; + private deletedObjectsSubscription: Subscription | null = null; + + private envTemplateIdSubject: BehaviorSubject = new BehaviorSubject(null); + private experimentTemplateIdSubject: BehaviorSubject = new BehaviorSubject(null); + private routeTemplateIdSubject: BehaviorSubject = new BehaviorSubject(null); + + private defaultTemplateIdSubject: BehaviorSubject = new BehaviorSubject(null); + private currentTemplateIdSubject: BehaviorSubject = new BehaviorSubject(null); + + private defaultTemplateTabsUpdatesSubject: Subject = new Subject(); + private currentTemplateTabsUpdatesSubject: Subject = new Subject(); private defaultTemplateSubject: BehaviorSubject = new BehaviorSubject(null); + private currentTemplateSubject: BehaviorSubject = new BehaviorSubject(null); + + get defaultTemplateId() { + return this.defaultTemplateIdSubject.asObservable(); + } + + get currentTemplateId() { + return this.currentTemplateIdSubject.asObservable(); + } + + get defaultTemplateTabsUpdates() { + return this.defaultTemplateTabsUpdatesSubject.asObservable(); + } + + get currentTemplateTabsUpdates() { + return this.currentTemplateTabsUpdatesSubject.asObservable(); + } get defaultTemplate() { return this.defaultTemplateSubject.asObservable(); } - constructor(private currentExperiment: CurrentExperimentService, private registry: PluginRegistryBaseService) { + get currentTemplate() { + return this.currentTemplateSubject.asObservable(); + } + + constructor(private registry: PluginRegistryBaseService, private env: EnvService, private currentExperiment: CurrentExperimentService, private route: ActivatedRoute) { + this.envSubscription = env.uiTemplateId.subscribe((defaultTemplateId) => { + this.envTemplateIdSubject.next(defaultTemplateId); + }); this.currentExperimentSubscription = currentExperiment.experiment.subscribe(experiment => { - this.updateDefaultTemplateFromExperiment(experiment); + this.experimentTemplateIdSubject.next(experiment?.templateId?.toString() ?? null); + }); + this.routeSubscription = this.route.queryParamMap.subscribe(params => { + const templateId = params.get('template'); + this.routeTemplateIdSubject.next(templateId ?? null); }); - } - private async updateDefaultTemplateFromExperiment(experiment: ExperimentApiObject | null) { - const defaultTemplateId = experiment?.templateId ?? null; + // handle updates to ids: + combineLatest([ + this.envTemplateIdSubject.asObservable().pipe(distinctUntilChanged()), + this.experimentTemplateIdSubject.asObservable().pipe(distinctUntilChanged()), + this.routeTemplateIdSubject.asObservable().pipe(distinctUntilChanged()), + ]).subscribe(([envTemplateId, experimentTemplateId, routeTemplateId]) => { + //TODO + if (routeTemplateId != null) { + if (routeTemplateId === "all-plugins") { + // allow overriding the default templates using a special template id + this.currentTemplateIdSubject.next(null); + this.defaultTemplateIdSubject.next(experimentTemplateId ?? envTemplateId ?? null); + return; + } + this.currentTemplateIdSubject.next(routeTemplateId); + this.defaultTemplateIdSubject.next(experimentTemplateId ?? envTemplateId ?? null); + return; + } + if (experimentTemplateId != null) { + this.currentTemplateIdSubject.next(experimentTemplateId); + this.defaultTemplateIdSubject.next(experimentTemplateId); + return; + } + if (envTemplateId != null) { + this.currentTemplateIdSubject.next(envTemplateId); + this.defaultTemplateIdSubject.next(envTemplateId); + return; + } + this.currentTemplateIdSubject.next(null); + this.defaultTemplateIdSubject.next(null); + }); - const currentDefaultTemplateId = this.defaultTemplateSubject.value?.self?.resourceKey?.uiTemplateId; - if (defaultTemplateId === currentDefaultTemplateId) { - return // same ids, nothing to do + // update default and current template + this.defaultTemplateIdSubject.pipe(distinctUntilChanged()).subscribe(defaultTemplateId => { + this.updateDefaultTemplate(defaultTemplateId); + }); + this.currentTemplateIdSubject.pipe(distinctUntilChanged()).subscribe(currentTemplateId => { + this.updateCurrentTemplate(currentTemplateId); + }); + + + // subscribe to changes to create tab group changed signals + this.newObjectsSubscription = this.registry.newApiObjectSubject.subscribe(newApiObject => { + if (newApiObject.new.resourceType === "ui-template-tab") { + this.handlePotentialTabUpdates(newApiObject.new); + } + }); + this.changedObjectsSubscritions = this.registry.changedApiObjectSubject.subscribe(changedApiObject => { + if (changedApiObject.changed.resourceType === "ui-template-tab" || changedApiObject.changed.resourceType === "ui-template") { + this.handlePotentialTabUpdates(changedApiObject.changed); + } + }); + this.deletedObjectsSubscription = this.registry.deletedApiObjectSubject.subscribe(deletedApiObject => { + if (deletedApiObject.deleted.resourceType === "ui-template-tab") { + this.handlePotentialTabUpdates(deletedApiObject.deleted); + } + if (deletedApiObject.deleted.resourceType === "ui-template") { + // TODO + //if (deletedApiObject.deleted.resourceKey?.uiTemplateId === this.templateId) { + // this.reloadTabGroups(null); + //} + } + }); + } + + private handlePotentialTabUpdates(apiLink: ApiLink) { + const templateId = apiLink.resourceKey?.uiTemplateId; + if (templateId === this.defaultTemplateIdSubject.getValue()) { + this.defaultTemplateTabsUpdatesSubject.next(); + } + if (templateId === this.currentTemplateIdSubject.getValue()) { + this.currentTemplateTabsUpdatesSubject.next(); } + } + + private updateDefaultTemplate(templateId: string | null) { + this.updateTemplate(templateId, this.defaultTemplateSubject); + } + + private updateCurrentTemplate(templateId: string | null) { + this.updateTemplate(templateId, this.currentTemplateSubject); + } - if (defaultTemplateId == null) { - this.defaultTemplateSubject.next(null); + private async updateTemplate(templateId: string | null, subject: BehaviorSubject) { + if (templateId == null) { + subject.next(null); return; } - // need to fetch template resource from plugin registry - const defaultTemplate = await this.getTemplate(defaultTemplateId.toString(), true); - if (defaultTemplate != null) { - this.defaultTemplateSubject.next(defaultTemplate?.data ?? null); - } + const templateResponse = await this.getTemplate(templateId); + subject.next(templateResponse?.data ?? null); } async addTemplate(newTemplate: TemplateApiObject) { From d607380e08356eeddb95125a8bb72873f2d1314c Mon Sep 17 00:00:00 2001 From: infacc Date: Sun, 2 Jul 2023 12:32:25 +0200 Subject: [PATCH 06/20] refactor plugin groups to use template service methods --- .../plugin-sidebar.component.ts | 20 +++++++++++++------ src/app/services/templates.service.ts | 1 - 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts index 328382c..b9c66ba 100644 --- a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts +++ b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts @@ -128,9 +128,13 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { private async handleNewTemplateTab(newTabLink: ApiLink) { if ((this.workspaceTabsLink == null || this.experimentNavigationTabsLink == null) && this.selectedTemplate != null) { - const templateResponse = await this.registry.getByApiLink(this.selectedTemplate, null, true); - const workspaceGroupLink = templateResponse?.data?.groups?.find(group => group.resourceKey?.["?group"] === "workspace"); - const experimentNavigationGroupLink = templateResponse?.data?.groups?.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); + if (newTabLink.resourceKey?.uiTemplateId == null) { + console.warn("New tab has no uiTemplateId", newTabLink); + return; + } + const tabGroups = await this.templates.getTemplateTabGroups(newTabLink.resourceKey?.uiTemplateId); + const workspaceGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "workspace"); + const experimentNavigationGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); this.workspaceTabsLink = workspaceGroupLink ?? null; this.experimentNavigationTabsLink = experimentNavigationGroupLink ?? null; } @@ -230,12 +234,16 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { } private async loadPluginTemplate(activeTemplate: ApiLink) { + if (activeTemplate.resourceKey?.uiTemplateId == null) { + console.warn("No template id found in template link", activeTemplate); + return; + } const pluginGroups: PluginGroup[] = []; this.pluginGroups = pluginGroups; - const templateResponse = await this.registry.getByApiLink(activeTemplate); - const workspaceGroupLink = templateResponse?.data?.groups?.find(group => group.resourceKey?.["?group"] === "workspace"); + const tabGroups = await this.templates.getTemplateTabGroups(activeTemplate.resourceKey?.uiTemplateId); + const workspaceGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "workspace"); this.workspaceTabsLink = workspaceGroupLink ?? null; - const experimentNavigationGroupLink = templateResponse?.data?.groups?.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); + const experimentNavigationGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); this.experimentNavigationTabsLink = experimentNavigationGroupLink ?? null; if (workspaceGroupLink == null) { return; diff --git a/src/app/services/templates.service.ts b/src/app/services/templates.service.ts index 296aa5c..08cce59 100644 --- a/src/app/services/templates.service.ts +++ b/src/app/services/templates.service.ts @@ -216,7 +216,6 @@ export class TemplatesService { async getTemplateTabGroups(templateId: string, ignoreCache: boolean | "ignore-embedded" = false) { const templateResponse = await this.getTemplate(templateId, ignoreCache); - console.log(templateResponse) return templateResponse?.data?.groups ?? []; } } From 09014d8aa565a363b469efebf344b1d739151f19 Mon Sep 17 00:00:00 2001 From: infacc Date: Tue, 4 Jul 2023 17:08:19 +0200 Subject: [PATCH 07/20] show any template tab groups in workspace details --- ...experiment-workspace-detail.component.html | 22 +++++-------------- .../experiment-workspace-detail.component.ts | 21 +++++++++++++----- .../tab-group-list.component.ts | 4 ++-- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html index cb618b0..0785dfd 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html @@ -66,23 +66,10 @@

Create New Template Tab

-

Workspace Tabs

- - - - {{tab.name}} - - - {{templateTabObjects[tab.resourceKey?.uiTemplateTabId ?? '']?.description}} - - - - - -

Experiment Navigation Tabs

- +

{{getTabName(group.key)}}

+ @@ -95,5 +82,6 @@

Expe + diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts index 2dea209..490ef30 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts @@ -6,11 +6,13 @@ import { filter } from 'rxjs/operators'; import { DeleteDialog } from 'src/app/dialogs/delete-dialog/delete-dialog.dialog'; import { ApiLink, CollectionApiObject, PageApiObject } from 'src/app/services/api-data-types'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; -import { TemplateTabApiObject } from 'src/app/services/templates.service'; +import { TemplateTabApiObject, TemplatesService } from 'src/app/services/templates.service'; import { TemplateApiObject } from 'src/app/services/templates.service'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; import { Subscription } from 'rxjs'; +import { TAB_GROUP_NAME_OVERRIDES, TAB_GROUP_SORT_KEYS } from '../tab-group-list/tab-group-list.component'; +import { KeyValue } from '@angular/common'; @Component({ selector: 'qhana-experiment-workspace-detail', @@ -53,7 +55,7 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { private changedTabSubscription: Subscription | null = null; private routeParamSubscription: Subscription | null = null; - constructor(private route: ActivatedRoute, private router: Router, private registry: PluginRegistryBaseService, private fb: FormBuilder, private dialog: MatDialog) { } + constructor(private route: ActivatedRoute, private router: Router, private registry: PluginRegistryBaseService, private fb: FormBuilder, private dialog: MatDialog, private templates: TemplatesService) { } ngOnInit() { this.routeParamSubscription = this.route.queryParamMap.subscribe(async params => { @@ -194,9 +196,8 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { console.warn("Template not found"); return; } - const template = await this.registry.getByApiLink(this.templateLink); - this.templateObject = template?.data ?? null; - const groupLinks = template?.data?.groups; + this.templateObject = (await this.templates.getTemplate(templateId))?.data ?? null; + const groupLinks = await this.templates.getTemplateTabGroups(templateId); if (groupLinks == null) { console.warn("No group links found"); return; @@ -330,4 +331,14 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { this.cancelEditTemplate(); } + + getTabName(group: string): string { + return TAB_GROUP_NAME_OVERRIDES[group] ?? group; + } + + tabOrder = (a: KeyValue, b: KeyValue): number => { + const aSortKey = TAB_GROUP_SORT_KEYS[a.key] ?? 0; + const bSortKey = TAB_GROUP_SORT_KEYS[b.key] ?? 0; + return aSortKey - bSortKey; + } } diff --git a/src/app/components/tab-group-list/tab-group-list.component.ts b/src/app/components/tab-group-list/tab-group-list.component.ts index d276ccb..88a0f02 100644 --- a/src/app/components/tab-group-list/tab-group-list.component.ts +++ b/src/app/components/tab-group-list/tab-group-list.component.ts @@ -20,13 +20,13 @@ import { PluginRegistryBaseService } from 'src/app/services/registry.service'; import { TemplatesService } from 'src/app/services/templates.service'; -const TAB_GROUP_SORT_KEYS: { [group: string]: number } = { +export const TAB_GROUP_SORT_KEYS: { [group: string]: number } = { "DEFAULT": 10000, "workspace": 10, "experiment-navigation": 20, } -const TAB_GROUP_NAME_OVERRIDES: { [group: string]: string } = { +export const TAB_GROUP_NAME_OVERRIDES: { [group: string]: string } = { "workspace": "Workspace Tabs (Sidebar)", "experiment-navigation": "Experiment Navigation Tabs", } From f15ec574c6da8e0abe42adf9cab6a0dd43f58958 Mon Sep 17 00:00:00 2001 From: infacc Date: Wed, 5 Jul 2023 11:57:59 +0200 Subject: [PATCH 08/20] sync nav-icons in plugin sidebar on tab location change --- .../plugin-sidebar/plugin-sidebar.component.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts index b9c66ba..de36a06 100644 --- a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts +++ b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts @@ -170,10 +170,24 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { const tabIndex = this.pluginGroups.findIndex(group => group.link.resourceKey?.['?template-tab'] === tabId); this.pluginGroups[tabIndex] = { ...this.pluginGroups[tabIndex] }; const tabResponse = await this.registry.getByApiLink(changedObject.changed); - if (tabResponse) { + if (tabResponse == null) { + console.warn("Could not load template tab", changedObject.changed); + return; + } + if (tabIndex < 0) { + this.pluginGroups.push({ + name: tabResponse.data.name, + open: false, + description: tabResponse.data.description, + link: tabResponse.data.plugins, + }); + } else { this.pluginGroups[tabIndex].name = tabResponse.data.name; this.pluginGroups[tabIndex].description = tabResponse.data.description; } + if (tabResponse.data.location !== 'workspace') { + this.pluginGroups.splice(tabIndex, 1); + } }); // romove deleted template tabs From 2470af92c9d6bafd0de525164317ccada7a40d62 Mon Sep 17 00:00:00 2001 From: infacc Date: Wed, 5 Jul 2023 12:35:06 +0200 Subject: [PATCH 09/20] remove unused property (experimentNavigationTabsLink) --- .../components/plugin-sidebar/plugin-sidebar.component.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts index de36a06..1decef5 100644 --- a/src/app/components/plugin-sidebar/plugin-sidebar.component.ts +++ b/src/app/components/plugin-sidebar/plugin-sidebar.component.ts @@ -34,7 +34,6 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { selectedTemplate: ApiLink | null = null; selectedTemplateName: string | "All Plugins" = "All Plugins"; workspaceTabsLink: ApiLink | null = null; - experimentNavigationTabsLink: ApiLink | null = null; highlightedTemplates: Set = new Set(); @@ -127,16 +126,14 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { } private async handleNewTemplateTab(newTabLink: ApiLink) { - if ((this.workspaceTabsLink == null || this.experimentNavigationTabsLink == null) && this.selectedTemplate != null) { + if (this.workspaceTabsLink == null && this.selectedTemplate != null) { if (newTabLink.resourceKey?.uiTemplateId == null) { console.warn("New tab has no uiTemplateId", newTabLink); return; } const tabGroups = await this.templates.getTemplateTabGroups(newTabLink.resourceKey?.uiTemplateId); const workspaceGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "workspace"); - const experimentNavigationGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); this.workspaceTabsLink = workspaceGroupLink ?? null; - this.experimentNavigationTabsLink = experimentNavigationGroupLink ?? null; } // add plugins to corresponding group const tabResponse = await this.registry.getByApiLink(newTabLink); @@ -257,8 +254,6 @@ export class PluginSidebarComponent implements OnInit, OnDestroy { const tabGroups = await this.templates.getTemplateTabGroups(activeTemplate.resourceKey?.uiTemplateId); const workspaceGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "workspace"); this.workspaceTabsLink = workspaceGroupLink ?? null; - const experimentNavigationGroupLink = tabGroups.find(group => group.resourceKey?.["?group"] === "experiment-navigation"); - this.experimentNavigationTabsLink = experimentNavigationGroupLink ?? null; if (workspaceGroupLink == null) { return; } From 215f213a7e59b85bc975d37db18802ffc36a401a Mon Sep 17 00:00:00 2001 From: infacc Date: Wed, 5 Jul 2023 12:57:12 +0200 Subject: [PATCH 10/20] show navigation tabs in navbar if no experiment is selected --- src/app/app-routing.module.ts | 2 ++ .../template-details.component.html | 4 ++-- .../template-details.component.ts | 14 +++++++------- .../experiment-workspace-detail.component.ts | 5 ++++- src/app/components/navbar/navbar.component.html | 7 +++++++ src/app/components/navbar/navbar.component.ts | 16 +++++++++------- .../plugin-tab/plugin-tab.component.ts | 6 +++--- .../tab-group-list/tab-group-list.component.ts | 2 ++ 8 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 1d76dc6..a5a3d7c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -29,6 +29,8 @@ import { TimelineStepComponent } from './components/timeline-step/timeline-step. 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 }, 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 1ea9d40..300aed7 100644 --- a/src/app/components-small/template-details/template-details.component.html +++ b/src/app/components-small/template-details/template-details.component.html @@ -3,8 +3,8 @@

Template Tab

Location - - {{location.description}} + + {{location.value}} 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 7464e8e..7473ea0 100644 --- a/src/app/components-small/template-details/template-details.component.ts +++ b/src/app/components-small/template-details/template-details.component.ts @@ -3,6 +3,7 @@ import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms' import { ApiLink, ApiResponse } from 'src/app/services/api-data-types'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; import { TemplateApiObject, TemplateTabApiObject } from 'src/app/services/templates.service'; +import { TAB_GROUP_NAME_OVERRIDES } from "src/app/components/tab-group-list/tab-group-list.component"; export function isInSetValidator(validValues: any[]): Validators { return (control: FormControl): { [key: string]: any } | null => { @@ -28,17 +29,12 @@ export class TemplateDetailsComponent implements OnInit { @Input() templateLink: ApiLink | null = null; @Input() tabLink: ApiLink | null = null; - locations: Location[] = [ - { value: "workspace", description: "Workspace (appears in the plugin sidebar)" }, - { value: "experiment-navigation", description: "Experiment Navigation (appears in the top navigation bar)" } - ]; - private initialValues = { name: "", description: "", sortKey: 0, filterString: "{}", - location: this.locations[0].value + location: TAB_GROUP_NAME_OVERRIDES["workspace"] }; templateForm: FormGroup = this.fb.group({ @@ -46,7 +42,7 @@ export class TemplateDetailsComponent implements OnInit { 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(this.locations.map(location => location.value))]] + location: [this.initialValues.location, [Validators.required, isInSetValidator(Object.keys(TAB_GROUP_NAME_OVERRIDES))]] }); constructor(private registry: PluginRegistryBaseService, private fb: FormBuilder) { } @@ -93,4 +89,8 @@ export class TemplateDetailsComponent implements OnInit { } } } + + getLocations(): { [group: string]: string } { + return TAB_GROUP_NAME_OVERRIDES; + } } diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts index 490ef30..0472ec5 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts @@ -11,7 +11,7 @@ import { TemplateApiObject } from 'src/app/services/templates.service'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; import { Subscription } from 'rxjs'; -import { TAB_GROUP_NAME_OVERRIDES, TAB_GROUP_SORT_KEYS } from '../tab-group-list/tab-group-list.component'; +import { TAB_GROUP_NAME_OVERRIDES, TAB_GROUP_SORT_KEYS } from 'src/app/components/tab-group-list/tab-group-list.component'; import { KeyValue } from '@angular/common'; @Component({ @@ -175,6 +175,9 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { console.warn("changed tab has no group", changedObject.changed); return; } + if (!Object.hasOwn(this.templateTabLinks, group)) { + this.templateTabLinks[group] = []; + } if (!this.templateTabLinks[group].includes(changedObject.changed)) { for (const group in this.templateTabLinks) { this.templateTabLinks[group] = this.templateTabLinks[group].filter(link => link.href !== changedObject.changed.href); diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html index afa7564..e14fe8e 100644 --- a/src/app/components/navbar/navbar.component.html +++ b/src/app/components/navbar/navbar.component.html @@ -27,6 +27,13 @@ {{tab.name}} + + + {{tab.name}} + +
diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts index c2ba4fc..f4f14d2 100644 --- a/src/app/components/navbar/navbar.component.ts +++ b/src/app/components/navbar/navbar.component.ts @@ -102,7 +102,7 @@ export class NavbarComponent implements OnInit, OnDestroy { this.experimentExtraTabsGroupLink = experimentNavGroup; const generalNavGroup = template.groups.find(group => group.resourceKey?.["?group"] === "navigation") ?? null; - const generalTabsLinkChanged = this.experimentExtraTabsGroupLink?.href !== experimentNavGroup?.href; + const generalTabsLinkChanged = this.generalExtraTabsGroupLink?.href !== generalNavGroup?.href; this.generalExtraTabsGroupLink = generalNavGroup; if (experimentTabsLinkChanged) { @@ -152,11 +152,13 @@ export class NavbarComponent implements OnInit, OnDestroy { } private updateExtraTabs() { - if (this.experimentId != null) { - // only show experiment navigatio tabs if an experiment is active - this.extraTabs = this.experimentExtraTabs; - } else { - this.extraTabs = this.generalExtraTabs; - } + this.experimentId.subscribe(experimentId => { + if (experimentId != null) { + // only show experiment navigation tabs if an experiment is active + this.extraTabs = this.experimentExtraTabs; + } else { + this.extraTabs = this.generalExtraTabs; + } + }); } } diff --git a/src/app/components/plugin-tab/plugin-tab.component.ts b/src/app/components/plugin-tab/plugin-tab.component.ts index 0baf5b3..34d031e 100644 --- a/src/app/components/plugin-tab/plugin-tab.component.ts +++ b/src/app/components/plugin-tab/plugin-tab.component.ts @@ -63,7 +63,6 @@ export class PluginTabComponent implements OnInit, OnDestroy { if ((pluginsResponse?.data?.collectionSize ?? 0) < 25) { pluginsResponse?.data?.items?.forEach(pluginLink => plugins.push(pluginLink)); - console.log(this.plugins) this.onPluginIdChanges(this.currentPluginId, true); } } @@ -116,10 +115,11 @@ export class PluginTabComponent implements OnInit, OnDestroy { } if (navigate) { + const experimentPrefix = this.currentExperimentId != null ? ['/experiments', this.currentExperimentId] : []; if (pluginLink != null) { - this.router.navigate(['/experiments', this.currentExperimentId, 'extra', this.currentTabId, 'plugin', pluginLink.resourceKey?.pluginId], { queryParamsHandling: 'preserve' }); + this.router.navigate([...experimentPrefix, 'extra', this.currentTabId, 'plugin', pluginLink.resourceKey?.pluginId], { queryParamsHandling: 'preserve' }); } else { - this.router.navigate(['/experiments', this.currentExperimentId, 'extra', this.currentTabId], { queryParamsHandling: 'preserve' }); + this.router.navigate([...experimentPrefix, 'extra', this.currentTabId], { queryParamsHandling: 'preserve' }); } } } diff --git a/src/app/components/tab-group-list/tab-group-list.component.ts b/src/app/components/tab-group-list/tab-group-list.component.ts index 88a0f02..7ff0cd9 100644 --- a/src/app/components/tab-group-list/tab-group-list.component.ts +++ b/src/app/components/tab-group-list/tab-group-list.component.ts @@ -24,11 +24,13 @@ export const TAB_GROUP_SORT_KEYS: { [group: string]: number } = { "DEFAULT": 10000, "workspace": 10, "experiment-navigation": 20, + "navigation": 30, } export const TAB_GROUP_NAME_OVERRIDES: { [group: string]: string } = { "workspace": "Workspace Tabs (Sidebar)", "experiment-navigation": "Experiment Navigation Tabs", + "navigation": "Navigation Tabs", } @Component({ From fc49968d2daa6b8536f101e675f3e9530bf8cce4 Mon Sep 17 00:00:00 2001 From: infacc Date: Fri, 7 Jul 2023 13:42:32 +0200 Subject: [PATCH 11/20] move tab group constants to templates service --- .../template-details.component.ts | 2 +- .../experiment-workspace-detail.component.ts | 2 +- .../tab-group-list/tab-group-list.component.ts | 15 +-------------- src/app/services/templates.service.ts | 13 +++++++++++++ 4 files changed, 16 insertions(+), 16 deletions(-) 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 7473ea0..baa9c4d 100644 --- a/src/app/components-small/template-details/template-details.component.ts +++ b/src/app/components-small/template-details/template-details.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms' import { ApiLink, ApiResponse } from 'src/app/services/api-data-types'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; import { TemplateApiObject, TemplateTabApiObject } from 'src/app/services/templates.service'; -import { TAB_GROUP_NAME_OVERRIDES } from "src/app/components/tab-group-list/tab-group-list.component"; +import { TAB_GROUP_NAME_OVERRIDES } from 'src/app/services/templates.service'; export function isInSetValidator(validValues: any[]): Validators { return (control: FormControl): { [key: string]: any } | null => { diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts index 0472ec5..093e3e1 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts @@ -11,7 +11,7 @@ import { TemplateApiObject } from 'src/app/services/templates.service'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; import { Subscription } from 'rxjs'; -import { TAB_GROUP_NAME_OVERRIDES, TAB_GROUP_SORT_KEYS } from 'src/app/components/tab-group-list/tab-group-list.component'; +import { TAB_GROUP_NAME_OVERRIDES, TAB_GROUP_SORT_KEYS } from 'src/app/services/templates.service'; import { KeyValue } from '@angular/common'; @Component({ diff --git a/src/app/components/tab-group-list/tab-group-list.component.ts b/src/app/components/tab-group-list/tab-group-list.component.ts index 7ff0cd9..000ee36 100644 --- a/src/app/components/tab-group-list/tab-group-list.component.ts +++ b/src/app/components/tab-group-list/tab-group-list.component.ts @@ -18,20 +18,7 @@ import { Subscription } from 'rxjs'; import { ApiLink } from 'src/app/services/api-data-types'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; import { TemplatesService } from 'src/app/services/templates.service'; - - -export const TAB_GROUP_SORT_KEYS: { [group: string]: number } = { - "DEFAULT": 10000, - "workspace": 10, - "experiment-navigation": 20, - "navigation": 30, -} - -export const TAB_GROUP_NAME_OVERRIDES: { [group: string]: string } = { - "workspace": "Workspace Tabs (Sidebar)", - "experiment-navigation": "Experiment Navigation Tabs", - "navigation": "Navigation Tabs", -} +import { TAB_GROUP_NAME_OVERRIDES, TAB_GROUP_SORT_KEYS } from 'src/app/services/templates.service'; @Component({ selector: 'qhana-tab-group-list', diff --git a/src/app/services/templates.service.ts b/src/app/services/templates.service.ts index 08cce59..fb27dfd 100644 --- a/src/app/services/templates.service.ts +++ b/src/app/services/templates.service.ts @@ -41,6 +41,19 @@ export interface TemplateTabApiObject extends ApiObject { // TODO check fields plugins: ApiLink; } +export const TAB_GROUP_SORT_KEYS: { [group: string]: number } = { + "DEFAULT": 10000, + "workspace": 10, + "experiment-navigation": 20, + "navigation": 30, +} + +export const TAB_GROUP_NAME_OVERRIDES: { [group: string]: string } = { + "workspace": "Workspace Tabs (Sidebar)", + "experiment-navigation": "Experiment Navigation Tabs", + "navigation": "Navigation Tabs", +} + @Injectable({ providedIn: 'root' From 5eb6347d61b01b8309e43e2b65217fe9c2ba8694 Mon Sep 17 00:00:00 2001 From: infacc Date: Fri, 7 Jul 2023 13:45:34 +0200 Subject: [PATCH 12/20] fix bug: correct location value --- .../template-details/template-details.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 baa9c4d..4daa3cf 100644 --- a/src/app/components-small/template-details/template-details.component.ts +++ b/src/app/components-small/template-details/template-details.component.ts @@ -34,7 +34,7 @@ export class TemplateDetailsComponent implements OnInit { description: "", sortKey: 0, filterString: "{}", - location: TAB_GROUP_NAME_OVERRIDES["workspace"] + location: "workspace" }; templateForm: FormGroup = this.fb.group({ From a063ccaf4956112a5bed318879d6895610efb1f0 Mon Sep 17 00:00:00 2001 From: infacc Date: Fri, 7 Jul 2023 13:58:05 +0200 Subject: [PATCH 13/20] refactor: use objects instead of get function for name overrides --- .../template-details/template-details.component.html | 2 +- .../template-details/template-details.component.ts | 6 ++---- .../experiment-workspace-detail.component.html | 2 +- .../experiment-workspace-detail.component.ts | 6 ++---- 4 files changed, 6 insertions(+), 10 deletions(-) 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 300aed7..67e9f55 100644 --- a/src/app/components-small/template-details/template-details.component.html +++ b/src/app/components-small/template-details/template-details.component.html @@ -3,7 +3,7 @@

Template Tab

Location - + {{location.value}} 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 4daa3cf..f3c4a04 100644 --- a/src/app/components-small/template-details/template-details.component.ts +++ b/src/app/components-small/template-details/template-details.component.ts @@ -29,6 +29,8 @@ export class TemplateDetailsComponent implements OnInit { @Input() templateLink: ApiLink | null = null; @Input() tabLink: ApiLink | null = null; + tabGroupNameOverrides = {...TAB_GROUP_NAME_OVERRIDES}; + private initialValues = { name: "", description: "", @@ -89,8 +91,4 @@ export class TemplateDetailsComponent implements OnInit { } } } - - getLocations(): { [group: string]: string } { - return TAB_GROUP_NAME_OVERRIDES; - } } diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html index 0785dfd..9e73039 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html @@ -68,7 +68,7 @@

Create New Template Tab

-

{{getTabName(group.key)}}

+

{{tabGroupNameOverrides[group.key] ?? group.key}}

diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts index 093e3e1..120e22f 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts @@ -23,6 +23,8 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { readonly separatorKeysCodes = [ENTER, COMMA] as const; + tabGroupNameOverrides = {...TAB_GROUP_NAME_OVERRIDES}; + templateId: string | null = null; tabId: string | null = null; @@ -335,10 +337,6 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { this.cancelEditTemplate(); } - getTabName(group: string): string { - return TAB_GROUP_NAME_OVERRIDES[group] ?? group; - } - tabOrder = (a: KeyValue, b: KeyValue): number => { const aSortKey = TAB_GROUP_SORT_KEYS[a.key] ?? 0; const bSortKey = TAB_GROUP_SORT_KEYS[b.key] ?? 0; From 3e9b1e539b408e9164885e60977b5703952dbc5a Mon Sep 17 00:00:00 2001 From: infacc Date: Fri, 7 Jul 2023 14:01:17 +0200 Subject: [PATCH 14/20] refactor: remove extraTabs property --- .../components/navbar/navbar.component.html | 4 ++-- src/app/components/navbar/navbar.component.ts | 18 ------------------ 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html index e14fe8e..ab407ce 100644 --- a/src/app/components/navbar/navbar.component.html +++ b/src/app/components/navbar/navbar.component.html @@ -23,14 +23,14 @@ + routerLinkActive="active" *ngFor="let tab of experimentExtraTabs"> {{tab.name}}
+ routerLinkActive="active" *ngFor="let tab of generalExtraTabs"> {{tab.name}} diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts index f4f14d2..8d22e6d 100644 --- a/src/app/components/navbar/navbar.component.ts +++ b/src/app/components/navbar/navbar.component.ts @@ -45,8 +45,6 @@ export class NavbarComponent implements OnInit, OnDestroy { experimentExtraTabsGroupLink: ApiLink | null = null; experimentExtraTabs: ApiLink[] = []; - extraTabs: ApiLink[] = []; - templateId: string | null = null; template: TemplateApiObject | null = null; @@ -94,7 +92,6 @@ export class NavbarComponent implements OnInit, OnDestroy { private async onTemplateChanges(template: TemplateApiObject | null) { if (template == null) { - this.extraTabs = []; return; } const experimentNavGroup = template.groups.find(group => group.resourceKey?.["?group"] === "experiment-navigation") ?? null; @@ -118,7 +115,6 @@ export class NavbarComponent implements OnInit, OnDestroy { if (groupLink == null) { this.experimentExtraTabs = []; this.experimentExtraTabsGroupLink = null; - this.updateExtraTabs(); return; } @@ -129,7 +125,6 @@ export class NavbarComponent implements OnInit, OnDestroy { groupResponse?.data?.items?.forEach(tab => extraTabs.push(tab)); this.experimentExtraTabs = extraTabs; - this.updateExtraTabs(); } private async updateGeneralExtraTabGroup() { @@ -137,7 +132,6 @@ export class NavbarComponent implements OnInit, OnDestroy { if (groupLink == null) { this.generalExtraTabs = []; this.generalExtraTabsGroupLink = null; - this.updateExtraTabs(); return; } @@ -148,17 +142,5 @@ export class NavbarComponent implements OnInit, OnDestroy { groupResponse?.data?.items?.forEach(tab => extraTabs.push(tab)); this.generalExtraTabs = extraTabs; - this.updateExtraTabs(); - } - - private updateExtraTabs() { - this.experimentId.subscribe(experimentId => { - if (experimentId != null) { - // only show experiment navigation tabs if an experiment is active - this.extraTabs = this.experimentExtraTabs; - } else { - this.extraTabs = this.generalExtraTabs; - } - }); } } From 04a176573c39edb8507a2c7cb42abe6a3536fb3c Mon Sep 17 00:00:00 2001 From: infacc Date: Fri, 7 Jul 2023 14:24:35 +0200 Subject: [PATCH 15/20] create TrackByFunction for templateTabLinks --- .../experiment-workspace-detail.component.html | 2 +- .../experiment-workspace-detail.component.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html index 9e73039..17e0570 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.html @@ -67,7 +67,7 @@

Create New Template Tab

- +

{{tabGroupNameOverrides[group.key] ?? group.key}}

) => item.value.map(link => link.href).join(','); } From 1dd07defe113fb230e76c2f2530cd8dcb6d3a860 Mon Sep 17 00:00:00 2001 From: infacc Date: Sat, 8 Jul 2023 10:39:00 +0200 Subject: [PATCH 16/20] refactor TrackByFunction --- .../experiment-workspace-detail.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts index 868f3f2..875e5ba 100644 --- a/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts +++ b/src/app/components/experiment-workspace-detail/experiment-workspace-detail.component.ts @@ -343,5 +343,5 @@ export class ExperimentWorkspaceDetailComponent implements OnInit { return aSortKey - bSortKey; } - trackByTabLink = (index: number, item: KeyValue) => item.value.map(link => link.href).join(','); + trackByTabLink = (index: number, item: KeyValue) => item.key; } From 9814be2ebbceee58be74d43076a18b8eb7df30ca Mon Sep 17 00:00:00 2001 From: infacc Date: Mon, 10 Jul 2023 10:33:43 +0200 Subject: [PATCH 17/20] fix bug: update extra tabs --- src/app/components/navbar/navbar.component.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts index 8d22e6d..9e8ddd6 100644 --- a/src/app/components/navbar/navbar.component.ts +++ b/src/app/components/navbar/navbar.component.ts @@ -90,8 +90,13 @@ export class NavbarComponent implements OnInit, OnDestroy { return this.downloadBadgeCounter?.subscribe(); } - private async onTemplateChanges(template: TemplateApiObject | null) { + private onTemplateChanges(template: TemplateApiObject | null) { + console.log(template) if (template == null) { + this.experimentExtraTabsGroupLink = null; + this.generalExtraTabsGroupLink = null; + this.experimentExtraTabs = []; + this.generalExtraTabs = []; return; } const experimentNavGroup = template.groups.find(group => group.resourceKey?.["?group"] === "experiment-navigation") ?? null; From d3de68cb8b8169361569d34a5a179c135b5bc225 Mon Sep 17 00:00:00 2001 From: infacc Date: Mon, 10 Jul 2023 10:34:21 +0200 Subject: [PATCH 18/20] remove debug output --- src/app/components/navbar/navbar.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts index 9e8ddd6..2277ce6 100644 --- a/src/app/components/navbar/navbar.component.ts +++ b/src/app/components/navbar/navbar.component.ts @@ -91,7 +91,6 @@ export class NavbarComponent implements OnInit, OnDestroy { } private onTemplateChanges(template: TemplateApiObject | null) { - console.log(template) if (template == null) { this.experimentExtraTabsGroupLink = null; this.generalExtraTabsGroupLink = null; From 93df902b5e35a2de57074388355c42100c5453c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:25:29 +0200 Subject: [PATCH 19/20] Add function to get responses from cache only --- src/app/services/registry.service.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/services/registry.service.ts b/src/app/services/registry.service.ts index fd22fdb..fd0ee59 100644 --- a/src/app/services/registry.service.ts +++ b/src/app/services/registry.service.ts @@ -464,6 +464,23 @@ export class PluginRegistryBaseService { return await this.getByApiLink(link, searchParams, ignoreCache); } + public async getFromCacheByApiLink(link: ApiLink, searchParams: URLSearchParams | null = null, ignoreCache: false | "ignore-embedded" = false): Promise | null> { + const url = new URL(link.href) + searchParams?.forEach((value, key) => url.searchParams.append(key, value)); + const request = new Request(url.toString(), { headers: { Accept: "application/json" } }); + const response = await this._fetchCached(request, ignoreCache); + if (response != null) { + const responseData = await response.json(); + if (request.url === responseData.data.self.href) { + // prevent stale/incorrect cache results + return responseData as ApiResponse; + } else { + console.log("Wrong cached result!", request, responseData.data.self.href); + } + } + return null; + } + public async getByApiLink(link: ApiLink, searchParams: URLSearchParams | null = null, ignoreCache: boolean | "ignore-embedded" = false): Promise | null> { const url = new URL(link.href) searchParams?.forEach((value, key) => url.searchParams.append(key, value)); From 18b7de1b9b11866fbd27ac77f414f89dac9d9bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:58:02 +0200 Subject: [PATCH 20/20] Speed up display of plugin list items Load initial values from cache if available. Update with new values lazily. --- .../plugin-list-item.component.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/app/components-small/plugin-list-item/plugin-list-item.component.ts b/src/app/components-small/plugin-list-item/plugin-list-item.component.ts index a453670..4b5bd34 100644 --- a/src/app/components-small/plugin-list-item/plugin-list-item.component.ts +++ b/src/app/components-small/plugin-list-item/plugin-list-item.component.ts @@ -1,6 +1,5 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import { ApiLink } from 'src/app/services/api-data-types'; +import { ApiLink, ApiResponse } from 'src/app/services/api-data-types'; import { PluginApiObject } from 'src/app/services/qhana-api-data-types'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; @@ -39,15 +38,29 @@ export class PluginListItemComponent implements OnChanges { this.updateIsInSearch() return; } + + // load available data from cache immediately + const cachePromise = await this.registry.getFromCacheByApiLink(this.link); + const cacheResponse = await Promise.race([cachePromise, null]); + this.updatePluginDataFromApiResponse(cacheResponse); + this.updateIsInSearch(); + + + // load fresh data from the API const pluginResponse = await this.registry.getByApiLink(this.link); - this.plugin = pluginResponse?.data ?? null; - if (this.plugin != null) { - this.searchableString = `${this.plugin.identifier.toLowerCase()} ${this.plugin.title.toLowerCase()}${this.plugin.version} ${this.plugin.pluginType} ${this.plugin.tags.join(" ").toLowerCase()}`; + this.updatePluginDataFromApiResponse(pluginResponse); + this.updateIsInSearch(); + } + + private updatePluginDataFromApiResponse(pluginResponse: ApiResponse | null) { + const plugin = pluginResponse?.data ?? null; + this.plugin = plugin; + if (plugin != null) { + this.searchableString = `${plugin.identifier.toLowerCase()} ${plugin.title.toLowerCase()}${plugin.version} ${plugin.pluginType} ${plugin.tags.join(" ").toLowerCase()}`; } else { this.searchableString = ""; } - this.updateIsInSearch(); } updateIsInSearch() {