From 55f5a7bc1f6885fde4ac9bd65daa2e7692414405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BChler?= <17296905+buehlefs@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:35:40 +0200 Subject: [PATCH] Start implementing autofill for plugin uis in workspace Add button popup to plugin ui frame Add maximize button that always works Fix css for fullscreen plugin displays Make plugin ui frame context aware (WIP) --- .../experiment-workspace.component.html | 4 +- .../plugin-uiframe.component.html | 14 +++ .../plugin-uiframe.component.sass | 39 ++++++-- .../plugin-uiframe.component.ts | 90 +++++++++++++++++-- src/app/services/qhana-backend.service.ts | 6 ++ 5 files changed, 140 insertions(+), 13 deletions(-) diff --git a/src/app/components/experiment-workspace/experiment-workspace.component.html b/src/app/components/experiment-workspace/experiment-workspace.component.html index cb89ce7..697f6ba 100644 --- a/src/app/components/experiment-workspace/experiment-workspace.component.html +++ b/src/app/components/experiment-workspace/experiment-workspace.component.html @@ -42,7 +42,9 @@

Experiment Workspace

- + diff --git a/src/app/components/plugin-uiframe/plugin-uiframe.component.html b/src/app/components/plugin-uiframe/plugin-uiframe.component.html index 0293c97..d1263a7 100644 --- a/src/app/components/plugin-uiframe/plugin-uiframe.component.html +++ b/src/app/components/plugin-uiframe/plugin-uiframe.component.html @@ -1,4 +1,18 @@
+
+ + + + +
diff --git a/src/app/components/plugin-uiframe/plugin-uiframe.component.sass b/src/app/components/plugin-uiframe/plugin-uiframe.component.sass index e0ed25c..271e2c5 100644 --- a/src/app/components/plugin-uiframe/plugin-uiframe.component.sass +++ b/src/app/components/plugin-uiframe/plugin-uiframe.component.sass @@ -26,12 +26,41 @@ background-color: var(--background) color: var(--warn-text) -.fullscreen +.floating-buttons + display: none position: absolute - top: 0 - left: 0 - right: 0 - min-height: 97% + height: 2.5rem + top: calc(-2.5rem - 3px) + right: 0.3rem + color: var(--text-card) + background-color: var(--background-card) + border: solid + border-width: 2px + border-radius: 2px + border-color: var(--border-color) + z-index: 2 + +.floating-buttons.left + right: unset + left: 0.3rem + +.microfrontend-container:focus-within, .microfrontend-container:focus, .microfrontend-container:hover + .floating-buttons + display: flex + +.fullscreen + .floating-buttons + display: flex + top: 2px + +.fullscreen + position: fixed + top: 0px + left: 0px + right: 0px + bottom: 0px + background-color: var(--background) + z-index: 1 .fsbutton position: absolute diff --git a/src/app/components/plugin-uiframe/plugin-uiframe.component.ts b/src/app/components/plugin-uiframe/plugin-uiframe.component.ts index c4c3770..086f64a 100644 --- a/src/app/components/plugin-uiframe/plugin-uiframe.component.ts +++ b/src/app/components/plugin-uiframe/plugin-uiframe.component.ts @@ -3,10 +3,10 @@ import { MatDialog } from '@angular/material/dialog'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { Observable, of } from 'rxjs'; -import { concatAll, filter, map, mergeAll, toArray } from 'rxjs/operators'; +import { catchError, concatAll, filter, map, mergeAll, mergeMap, toArray } from 'rxjs/operators'; import { ChooseDataDialog } from 'src/app/dialogs/choose-data/choose-data.dialog'; import { ChoosePluginDialog } from 'src/app/dialogs/choose-plugin/choose-plugin.dialog'; -import { CollectionApiObject } from 'src/app/services/api-data-types'; +import { ApiLink, CollectionApiObject } from 'src/app/services/api-data-types'; import { PluginApiObject } from 'src/app/services/qhana-api-data-types'; import { ApiObjectList, ExperimentDataApiObject, QhanaBackendService } from 'src/app/services/qhana-backend.service'; import { PluginRegistryBaseService } from 'src/app/services/registry.service'; @@ -20,6 +20,12 @@ export interface FormSubmitData { resultUrl: string; } +export interface PluginUiContext { + experimentId?: string | number; + stepId?: string | number; + data?: Array<{ downloadUrl: string, dataType: string, contentType: string }>; +} + function isFormSubmitData(data: any): data is FormSubmitData { if (data == null) { return false; @@ -161,6 +167,9 @@ export class PluginUiframeComponent implements OnChanges, OnDestroy { @Input() url: string | null = null; @Output() formDataSubmit: EventEmitter = new EventEmitter(); + @Input() plugin: ApiLink | null = null; + @Input() context: PluginUiContext | null = null; + blank: SafeResourceUrl; pluginOrigin: string | null = null; @@ -171,11 +180,16 @@ export class PluginUiframeComponent implements OnChanges, OnDestroy { hasFullscreenMode: boolean = false; fullscreen: boolean = false; + buttonsLeft: boolean = false; + loading: boolean = true; error: { code: number, status: string } | null = null; + autofillData: { value: string, encoding: string } | null = null; + private dialogActive = false; + listenerFunction = (event: MessageEvent) => this.handleMicroFrontendEvent(event); constructor(private sanitizer: DomSanitizer, private dialog: MatDialog, private backend: QhanaBackendService, private registry: PluginRegistryBaseService, private route: ActivatedRoute) { @@ -202,11 +216,65 @@ export class PluginUiframeComponent implements OnChanges, OnDestroy { this.frontendHeight = 100; return; } - this.loading = true; - this.pluginOrigin = (new URL(url)).origin; - this.frontendHeight = 100; - this.frontendUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); - this.hasFullscreenMode = false; + if (changes.url != null) { + this.loading = true; + this.pluginOrigin = (new URL(url)).origin; + this.frontendHeight = 100; + this.frontendUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); + this.hasFullscreenMode = false; + } + if (changes.plugin != null || changes.context != null) { + this.processContext(); + } + } + + async autofillLatest() { + if (this.autofillData != null) { + this.sendAutofillData(this.autofillData.value, this.autofillData.encoding); + } + } + + private async processContext() { + if (this.plugin == null) { + this.autofillData = null; + return; + } + const plugin = (await this.registry.getByApiLink(this.plugin))?.data ?? null; + if (plugin == null) { + this.autofillData = null; + return; + } + if (this.context?.experimentId != null) { + this.backend.getTimelineStepsPage(this.context.experimentId, { sort: -1, pluginName: plugin.identifier, version: plugin.version, itemCount: 1 }).pipe( + map(steps => { + if (steps.items.length == 1) { + const step = steps.items[0]; + return { + parametersUrl: step.parameters, + encoding: step.parametersContentType, + }; + } + return null; + }), + mergeMap(params => { + if (params == null) { + return of(null); + } + return this.backend.getTimelineStepParameters(params.parametersUrl).pipe(map(value => { + return { value: value, encoding: params.encoding }; + })) + }), + catchError((err) => { + console.log(err) + return of(null); + }), + ).subscribe(result => { + this.autofillData = result; + }); + return; + } + this.autofillData = null; + return; } private selectPlugin(request: PluginUrlRequest) { @@ -299,6 +367,14 @@ export class PluginUiframeComponent implements OnChanges, OnDestroy { } } + private sendAutofillData(value: string, encoding: string) { + this.sendMessage({ + type: "autofill-response", + value: value, + encoding: encoding, + }); + } + private sendMessage(message: any) { const iframe: HTMLIFrameElement | null = this.uiframe?.nativeElement ?? null; iframe?.contentWindow?.postMessage?.(message, this.pluginOrigin ?? "*"); diff --git a/src/app/services/qhana-backend.service.ts b/src/app/services/qhana-backend.service.ts index c794373..ddfc0bc 100644 --- a/src/app/services/qhana-backend.service.ts +++ b/src/app/services/qhana-backend.service.ts @@ -474,6 +474,12 @@ export class QhanaBackendService { ); } + public getTimelineStepParameters(url: string): Observable { + return this.callWithRootUrl( + rootUrl => this.http.get(url, { responseType: "text" }) + ); + } + public getTimelineStepNotes(experimentId: number | string, step: number | string): Observable { return this.callWithRootUrl( rootUrl => this.http.get(`${rootUrl}/experiments/${experimentId}/timeline/${step}/notes`)