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`)