Просмотр исходного кода

feat(ui-devkit): Allow ui extensions to be launched in a new window

Michael Bromley 6 лет назад
Родитель
Сommit
71eb6a56b4

+ 3 - 0
packages/admin-ui/src/app/shared/components/extension-host/extension-host-config.ts

@@ -1,10 +1,13 @@
 export interface ExtensionHostOptions {
     extensionUrl: string;
+    openInNewTab?: boolean;
 }
 
 export class ExtensionHostConfig {
     public extensionUrl: string;
+    public openInNewTab: boolean;
     constructor(options: ExtensionHostOptions) {
         this.extensionUrl = options.extensionUrl;
+        this.openInNewTab = options.openInNewTab != null ? options.openInNewTab : false;
     }
 }

+ 20 - 1
packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.html

@@ -1 +1,20 @@
-<iframe [src]="extensionUrl" #extensionFrame></iframe>
+<ng-template [ngIf]="openInIframe" [ngIfElse]="launchExtension">
+    <iframe [src]="extensionUrl" #extensionFrame></iframe>
+</ng-template>
+<ng-template #launchExtension>
+    <div class="launch-button" [class.launched]="extensionWindowIsOpen">
+        <div>
+            <button
+                class="btn btn-lg btn-primary"
+                [disabled]="extensionWindowIsOpen"
+                (click)="launchExtensionWindow()"
+            >
+                <clr-icon shape="pop-out"></clr-icon>
+                {{ 'common.launch-extension' | translate }}
+            </button>
+            <h3 class="window-hint" [class.visible]="extensionWindowIsOpen">
+                {{ 'common.extension-running-in-separate-window' | translate }}
+            </h3>
+        </div>
+    </div>
+</ng-template>

+ 31 - 0
packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.scss

@@ -10,3 +10,34 @@ iframe {
     height: 100%;
     border: none;
 }
+
+.launch-button {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    width: 100%;
+    height: 100%;
+    border: none;
+    padding: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: background-color 0.3s;
+    text-align: center;
+    &.launched {
+        background-color: $color-grey-300;
+    }
+}
+
+.window-hint {
+    visibility: hidden;
+    opacity: 0;
+    transition: visibility 0.3s 0, opacity 0.3s;
+    &.visible {
+        visibility: visible;
+        opacity: 1;
+        transition: visibility 0, opacity 0.3s;
+    }
+}

+ 57 - 7
packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.ts

@@ -1,4 +1,12 @@
-import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import {
+    AfterViewInit,
+    ChangeDetectionStrategy,
+    Component,
+    ElementRef,
+    OnDestroy,
+    OnInit,
+    ViewChild,
+} from '@angular/core';
 import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
 import { ActivatedRoute } from '@angular/router';
 
@@ -12,9 +20,13 @@ import { ExtensionHostService } from './extension-host.service';
     changeDetection: ChangeDetectionStrategy.Default,
     providers: [ExtensionHostService],
 })
-export class ExtensionHostComponent implements OnInit {
+export class ExtensionHostComponent implements OnInit, AfterViewInit, OnDestroy {
     extensionUrl: SafeResourceUrl;
-    @ViewChild('extensionFrame', { static: true }) private extensionFrame: ElementRef<HTMLIFrameElement>;
+    openInIframe = true;
+    extensionWindowIsOpen = false;
+    private config: ExtensionHostConfig;
+    private extensionWindow?: Window;
+    @ViewChild('extensionFrame', { static: false }) private extensionFrame: ElementRef<HTMLIFrameElement>;
 
     constructor(
         private route: ActivatedRoute,
@@ -29,15 +41,53 @@ export class ExtensionHostComponent implements OnInit {
                 `Expected an ExtensionHostConfig object, got ${JSON.stringify(data.extensionHostConfig)}`,
             );
         }
+        this.config = data.extensionHostConfig;
+        this.openInIframe = !this.config.openInNewTab;
         this.extensionUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
-            data.extensionHostConfig.extensionUrl || 'about:blank',
+            this.config.extensionUrl || 'about:blank',
         );
-        const { contentWindow } = this.extensionFrame.nativeElement;
-        if (contentWindow) {
-            this.extensionHostService.init(contentWindow);
+    }
+
+    ngAfterViewInit() {
+        if (this.openInIframe) {
+            const extensionWindow = this.extensionFrame.nativeElement.contentWindow;
+            if (extensionWindow) {
+                this.extensionHostService.init(extensionWindow);
+            }
+        }
+    }
+
+    ngOnDestroy(): void {
+        if (this.extensionWindow) {
+            this.extensionWindow.close();
         }
     }
 
+    launchExtensionWindow() {
+        const extensionWindow = window.open(this.config.extensionUrl);
+        if (!extensionWindow) {
+            return;
+        }
+        this.extensionHostService.init(extensionWindow);
+        this.extensionWindowIsOpen = true;
+        this.extensionWindow = extensionWindow;
+
+        let timer: number;
+        function pollWindowState(extwindow: Window, onClosed: () => void) {
+            if (extwindow.closed) {
+                window.clearTimeout(timer);
+                onClosed();
+            } else {
+                timer = window.setTimeout(() => pollWindowState(extwindow, onClosed), 250);
+            }
+        }
+
+        pollWindowState(extensionWindow, () => {
+            this.extensionWindowIsOpen = false;
+            this.extensionHostService.destroy();
+        });
+    }
+
     private isExtensionHostConfig(input: any): input is ExtensionHostConfig {
         return input.hasOwnProperty('extensionUrl');
     }

+ 5 - 1
packages/admin-ui/src/app/shared/components/extension-host/extension-host.service.ts

@@ -21,11 +21,15 @@ export class ExtensionHostService implements OnDestroy {
         window.addEventListener('message', this.handleMessage);
     }
 
-    ngOnDestroy(): void {
+    destroy() {
         window.removeEventListener('message', this.handleMessage);
         this.destroyMessage$.next();
     }
 
+    ngOnDestroy(): void {
+        this.destroy();
+    }
+
     private handleMessage = (message: MessageEvent) => {
         const { data, origin } = message;
         if (this.isExtensionMessage(data)) {

+ 4 - 1
packages/admin-ui/src/i18n-messages/en.json

@@ -140,10 +140,12 @@
     "edit": "Edit",
     "edit-field": "Edit field",
     "enabled": "Enabled",
+    "extension-running-in-separate-window": "Extension is running in a separate window",
     "guest": "Guest",
     "items-per-page-option": "{ count } per page",
     "jobs-in-progress": "{ count } {count, plural, one {job} other {jobs}} in progress",
     "language": "Language",
+    "launch-extension": "Launch extension",
     "log-out": "Log out",
     "login": "Log in",
     "more": "More...",
@@ -555,6 +557,7 @@
     "catalog": "Catalog",
     "channel": "Channel",
     "channel-token": "Channel token",
+    "confirm-delete-role": "Delete role?",
     "create": "Create",
     "create-new-channel": "Create new channel",
     "create-new-country": "Create new country",
@@ -604,4 +607,4 @@
     "update": "Update",
     "zone": "Zone"
   }
-}
+}

+ 1 - 0
packages/dev-server/ui-plugin/extensions/ui-plugin.module.ts

@@ -56,6 +56,7 @@ export class TestComponent {
                     ],
                     extensionHostConfig: new ExtensionHostConfig({
                         extensionUrl: './assets/vue-app/index.html',
+                        openInNewTab: true,
                     }),
                 },
             },

+ 3 - 2
packages/ui-devkit/src/devkit-api.ts

@@ -96,6 +96,7 @@ function sendMessage<T extends ExtensionMesssage>(type: T['type'], data: T['data
     };
 
     return new Observable<any>(subscriber => {
+        const hostWindow = window.opener || window.parent;
         const handleReply = (event: MessageEvent) => {
             const response: MessageResponse = event.data;
             if (response && response.requestId === requestId) {
@@ -113,10 +114,10 @@ function sendMessage<T extends ExtensionMesssage>(type: T['type'], data: T['data
             }
         };
         const tearDown = () => {
-            window.parent.postMessage({ requestId, type: 'cancellation', data: null }, targetOrigin);
+            hostWindow.postMessage({ requestId, type: 'cancellation', data: null }, targetOrigin);
         };
         window.addEventListener('message', handleReply);
-        window.parent.postMessage(message, targetOrigin);
+        hostWindow.postMessage(message, targetOrigin);
 
         return tearDown;
     });