diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c5cec5a833039b574591fec54deb1a73a24bcc03..2806403724f1cbaf4a8adaadfee6baa225334e9b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -47,6 +47,7 @@ jobs: --env HBP_CLIENTID=${{ secrets.HBP_CLIENTID }} \ --env HBP_CLIENTSECRET=${{ secrets.HBP_CLIENTSECRET }} \ --env REFRESH_TOKEN=${{ secrets.REFRESH_TOKEN }} \ + --env PLUGIN_URLS=${{ env.ATLAS_URL }}/res/plugin_examples/plugin1/manifest.json \ -dit \ ${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG} - uses: actions/checkout@v1 diff --git a/e2e/src/layout/pluginInfo.e2e-spec.js b/e2e/src/layout/pluginInfo.e2e-spec.js new file mode 100644 index 0000000000000000000000000000000000000000..50bc4e83a8fed67fc087ef06559a2112474447c5 --- /dev/null +++ b/e2e/src/layout/pluginInfo.e2e-spec.js @@ -0,0 +1,35 @@ +const { LayoutPage } = require("../util") + +describe('> plugin dropdown', () => { + let layoutPage + + beforeEach(async () => { + layoutPage = new LayoutPage() + await layoutPage.init() + await layoutPage.goto() + await layoutPage.dismissModal() + }) + + it('> click on drop down btn shows drop down menu', async () => { + await layoutPage.showToolsMenu() + await layoutPage.wait(500) + const tools = await layoutPage.getVisibleTools() + expect(tools.length).toBeGreaterThan(0) + }) + + it('> click on info btn shows info', async () => { + await layoutPage.showToolsMenu() + await layoutPage.wait(500) + const tools = await layoutPage.getVisibleTools() + const exampleIndex = tools.findIndex(tool => /Example\ Plugin/.test(tool)) + expect(exampleIndex).toBeGreaterThanOrEqual(0) + await layoutPage.clickOnNthTool(exampleIndex, '[aria-label="About this plugin"]') + await layoutPage.wait(500) + const txt = await layoutPage.getModalText() + expect(txt).toContain('About Example Plugin (v0.0.1)') + expect(txt).toContain('description of example plugin') + expect(txt).toContain('http://HOSTNAME/home.html') + expect(txt).toContain('Xiaoyun Gui <x.gui@fz-juelich.de>') + + }) +}) diff --git a/e2e/src/util.js b/e2e/src/util.js index 00f524b8efafe85c114276d31988a51e6dad168c..45a504172ada61742cd1cc135ce46c49fc3605b8 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -244,6 +244,12 @@ class WdLayoutPage extends WdBase{ .findElements( By.tagName('button') ) } + async getModalText(){ + const el = await this._getModal() + const txt = await _getTextFromWebElement(el) + return txt + } + async getModalActions(){ const btns = await this._getModalBtns() @@ -414,6 +420,43 @@ class WdLayoutPage extends WdBase{ } // Signin banner + _getToolsIcon(){ + return this._driver + .findElement( By.css('[aria-label="Show tools and plugins"]') ) + } + + async showToolsMenu(){ + await this._getToolsIcon().click() + } + + _getToolsMenu(){ + return this._driver + .findElement( By.css('[aria-label="Tools and plugins menu"]') ) + } + + _getAllTools(){ + return this._getToolsMenu().findElements( By.css('[role="menuitem"]') ) + } + + async getVisibleTools(){ + // may throw if tools menu not visible + const menuItems = await this._getAllTools() + const returnArr = [] + for (const menuItem of menuItems){ + returnArr.push( + await _getTextFromWebElement(menuItem) + ) + } + return returnArr + } + + async clickOnNthTool(index, cssSelector){ + const menuItems = await this._getAllTools() + if (!menuItems[index]) throw new Error(`index out of bound: accessing index ${index} of length ${menuItems.length}`) + if (cssSelector) await menuItems[index].findElement( By.css(cssSelector) ).click() + else await menuItems[index].click() + } + _getFavDatasetIcon(){ return this._driver .findElement( By.css('[aria-label="Show pinned datasets"]') ) diff --git a/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts b/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts index aa46be49522122079e696d2f606629f8e3905032..756444017aa5bb9d1b178d21e24a13c54fd1c207 100644 --- a/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts +++ b/src/atlasViewer/pluginUnit/atlasViewer.pluginService.service.ts @@ -304,4 +304,10 @@ export interface IPluginManifest { initState?: any initStateUrl?: string persistency?: boolean + + description?: string + desc?: string + + homepage?: string + authors?: string } diff --git a/src/plugin_examples/README.md b/src/plugin_examples/README.md index c94e64a3d223507c0952d5115100b1c4d8f88ebc..7f967539c0997992f1dae6e391a72e084d5a33a5 100644 --- a/src/plugin_examples/README.md +++ b/src/plugin_examples/README.md @@ -27,11 +27,16 @@ The manifest JSON file describes the metadata associated with the plugin. } }, "initStateUrl": "http://LINK-TO-PLUGIN-STATE", - "persistency": false + "persistency": false, + + "description": "Human readable description of the plugin.", + "desc": "Same as description. If both present, description takes more priority.", + "homepage": "https://HOMEPAGE-URL-TO-YOUR-PLUGIN/doc.html", + "authors": "Author <author@example.com>, Author2 <author2@example.org>" } ``` *NB* -- Plugin name must be unique globally. To prevent plugin name clashing, please adhere to the convention of naming your package **AFFILIATION.AUTHORNAME.PACKAGENAME**. +- Plugin name must be unique globally. To prevent plugin name clashing, please adhere to the convention of naming your package **AFFILIATION.AUTHORNAME.PACKAGENAME\[.VERSION\]**. - the `initState` object and `initStateUrl` will be available prior to the evaluation of `script.js`, and will populate the objects `interactiveViewer.pluginControl[MANIFEST.name].initState` and `interactiveViewer.pluginControl[MANIFEST.name].initStateUrl` respectively. --- diff --git a/src/plugin_examples/plugin1/manifest.json b/src/plugin_examples/plugin1/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..0813f787c22dc71f22c3d14bbd20de2716d27068 --- /dev/null +++ b/src/plugin_examples/plugin1/manifest.json @@ -0,0 +1,18 @@ +{ + "name":"fzj.xg.exmaple.0_0_1", + "displayName": "Example Plugin (v0.0.1)", + "templateURL": "http://HOSTNAME/test.html", + "scriptURL": "http://HOSTNAME/script.js", + "initState": { + "key1": "val1", + "key2": { + "key21": "val21" + } + }, + "initStateUrl": "http://HOSTNAME/state?id=007", + "persistency": false, + "description": "description of example plugin", + "desc": "desc of example plugin", + "homepage": "http://HOSTNAME/home.html", + "authors": "Xiaoyun Gui <x.gui@fz-juelich.de>" +} \ No newline at end of file diff --git a/src/ui/pluginBanner/pluginBanner.component.ts b/src/ui/pluginBanner/pluginBanner.component.ts index 20b225c21ad0975620968c3d42b51b6c6f650e5a..c154f1b666a4aa1ba19cd4568eef5d91394f1888 100644 --- a/src/ui/pluginBanner/pluginBanner.component.ts +++ b/src/ui/pluginBanner/pluginBanner.component.ts @@ -1,5 +1,6 @@ -import { Component } from "@angular/core"; +import { Component, ViewChild, TemplateRef } from "@angular/core"; import { IPluginManifest, PluginServices } from "src/atlasViewer/pluginUnit"; +import { MatDialog } from "@angular/material/dialog"; @Component({ selector : 'plugin-banner', @@ -11,10 +12,26 @@ import { IPluginManifest, PluginServices } from "src/atlasViewer/pluginUnit"; export class PluginBannerUI { - constructor(public pluginServices: PluginServices) { + @ViewChild('pluginInfoTmpl', { read: TemplateRef }) + private pluginInfoTmpl: TemplateRef<any> + + constructor( + public pluginServices: PluginServices, + private matDialog: MatDialog, + ) { } public clickPlugin(plugin: IPluginManifest) { this.pluginServices.launchPlugin(plugin) } + + public showPluginInfo(manifest: IPluginManifest){ + this.matDialog.open( + this.pluginInfoTmpl, + { + data: manifest, + ariaLabel: `Additional information about a plugin` + } + ) + } } diff --git a/src/ui/pluginBanner/pluginBanner.style.css b/src/ui/pluginBanner/pluginBanner.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..232bed813d70dddf7b9f59d597334b2263ab5cda 100644 --- a/src/ui/pluginBanner/pluginBanner.style.css +++ b/src/ui/pluginBanner/pluginBanner.style.css @@ -0,0 +1,4 @@ +.sub +{ + transform: translate(25%, 25%); +} diff --git a/src/ui/pluginBanner/pluginBanner.template.html b/src/ui/pluginBanner/pluginBanner.template.html index 63a7079df80735543eb9b00b66db41c1e42131c7..6216ee8247a5ac011f3ec41acf7f58cb7e52d0d3 100644 --- a/src/ui/pluginBanner/pluginBanner.template.html +++ b/src/ui/pluginBanner/pluginBanner.template.html @@ -3,7 +3,70 @@ *ngFor="let plugin of pluginServices.fetchedPluginManifests" [matTooltip]="plugin.displayName ? plugin.displayName : plugin.name" (click)="clickPlugin(plugin)"> - <mat-icon fontSet="fas" fontIcon="fa-cube"></mat-icon> - {{ plugin.displayName ? plugin.displayName : plugin.name }} + <span mat-icon-button + aria-label="About this plugin" + class="mat-icon d-inline-flex align-items-center justify-content-center fa-stack" + (click)="showPluginInfo(plugin)" + iav-stop="click mousedown mouseup"> + <i class="fas fa-cube fa-stack-1x"></i> + <i class="fas fa-info-circle fa-stack-1x sub"></i> + </span> + <span> + {{ plugin.displayName ? plugin.displayName : plugin.name }} + </span> </button> </mat-action-list> + +<ng-template #pluginInfoTmpl let-manifest> + <h1 mat-dialog-title> + About {{ manifest.displayName || manifest.name }} + </h1> + + <div mat-dialog-content> + <mat-list> + <mat-list-item> + <span mat-list-icon class="d-inline-flex justify-content-center align-items-center"> + <i class="fas fa-info"></i> + </span> + <div mat-line> + Description + </div> + <div mat-line> + {{ manifest.description || manifest.desc || 'Not provided.' }} + </div> + </mat-list-item> + + <mat-list-item> + <span mat-list-icon class="d-inline-flex justify-content-center align-items-center"> + <i class="fas fa-users"></i> + </span> + <div mat-line> + Authors + </div> + <div mat-line> + {{ manifest.authors || 'Not provided' }} + </div> + </mat-list-item> + + <mat-list-item> + <span mat-list-icon class="d-inline-flex justify-content-center align-items-center"> + <i class="fas fa-globe-europe"></i> + </span> + <div mat-line> + Homepage + </div> + <div mat-line> + {{ manifest.homepage || 'Not provided' }} + </div> + </mat-list-item> + </mat-list> + + </div> + + <div mat-dialog-actions class="d-flex justify-content-center"> + <button mat-button mat-dialog-close> + close + </button> + </div> + +</ng-template> diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html index 32deee57d34cd6be014c47040a9471fd681fe133..9b50940756bb29f1a7d11beb6fef30e85124022d 100644 --- a/src/ui/signinBanner/signinBanner.template.html +++ b/src/ui/signinBanner/signinBanner.template.html @@ -22,6 +22,7 @@ <!-- plugin and tools --> <div class="btnWrapper" #pluginsAndToolsDiv> <button mat-icon-button + aria-label="Show tools and plugins" matTooltipPosition="below" [matTooltip]="pluginTooltipText" [matMenuTriggerFor]="pluginDropdownMenu" @@ -50,7 +51,8 @@ </div> <!-- plugin dropdownmenu --> -<mat-menu #pluginDropdownMenu> +<mat-menu #pluginDropdownMenu + [aria-label]="'Tools and plugins menu'"> <button mat-menu-item [disabled]="!parcellationIsSelected" (click)="takingScreenshot = true;"