From c891315f9730579da32959471b27b2468e5c6077 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Fri, 15 Mar 2019 15:34:57 +0100
Subject: [PATCH] feat: added oidc login

---
 deploy/auth/hbp-oidc.js                       | 38 +++++++++++
 deploy/auth/index.js                          | 36 ++++++++++
 deploy/auth/oidc.js                           | 39 +++++++++++
 deploy/catchError.js                          |  2 +-
 deploy/datasets/index.js                      |  9 ++-
 deploy/datasets/query.js                      | 46 ++++++++-----
 deploy/package.json                           |  3 +
 deploy/server.js                              | 66 +++++++++++++++----
 src/atlasViewer/atlasViewer.component.ts      |  1 +
 .../pluginUnit/pluginUnit.component.ts        |  3 +-
 .../pagination/pagination.component.ts        |  3 +-
 .../pagination/pagination.style.css           |  6 --
 src/css/darkBtns.css                          | 12 ++++
 src/main.module.ts                            |  8 +++
 src/res/css/extra_styles.css                  | 21 ++++++
 src/services/auth.service.ts                  | 52 +++++++++++++++
 src/ui/banner/banner.component.ts             | 24 ++++++-
 src/ui/banner/banner.style.css                |  2 +-
 src/ui/banner/banner.template.html            | 45 +++++++++++++
 src/ui/pluginBanner/pluginBanner.component.ts |  5 +-
 src/ui/pluginBanner/pluginBanner.style.css    |  6 --
 src/ui/ui.module.ts                           |  2 +
 22 files changed, 375 insertions(+), 54 deletions(-)
 create mode 100644 deploy/auth/hbp-oidc.js
 create mode 100644 deploy/auth/index.js
 create mode 100644 deploy/auth/oidc.js
 create mode 100644 src/css/darkBtns.css
 create mode 100644 src/services/auth.service.ts

diff --git a/deploy/auth/hbp-oidc.js b/deploy/auth/hbp-oidc.js
new file mode 100644
index 000000000..ab8ec12b7
--- /dev/null
+++ b/deploy/auth/hbp-oidc.js
@@ -0,0 +1,38 @@
+const passport = require('passport')
+const { configureAuth } = require('./oidc')
+
+const HOSTNAME = process.env.HOSTNAME || 'http://localhost:3000'
+const clientId = process.env.HBP_CLIENTID || 'no hbp id'
+const clientSecret = process.env.HBP_CLIENTSECRET || 'no hbp client secret'
+const discoveryUrl = 'https://services.humanbrainproject.eu/oidc'
+const redirectUri = `${HOSTNAME}/hbp-oidc/cb`
+const cb = (tokenset, {sub, given_name, family_name, ...rest}, done) => {
+  return done(null, {
+    id: `hbp-oidc:${sub}`,
+    name: `${given_name} ${family_name}`,
+    type: `hbp-oidc`,
+    tokenset,
+    rest
+  })
+}
+
+module.exports = async (app) => {
+  const { oidcStrategy } = await configureAuth({
+    clientId,
+    clientSecret,
+    discoveryUrl,
+    redirectUri,
+    cb,
+    clientConfig: {
+      redirect_uris: [ redirectUri ],
+      response_types: [ 'code' ]
+    }
+  })
+  
+  passport.use('hbp-oidc', oidcStrategy)
+  app.get('/hbp-oidc/auth', passport.authenticate('hbp-oidc'))
+  app.get('/hbp-oidc/cb', passport.authenticate('hbp-oidc', {
+    successRedirect: '/',
+    failureRedirect: '/'
+  }))
+}
diff --git a/deploy/auth/index.js b/deploy/auth/index.js
new file mode 100644
index 000000000..f76f6cc42
--- /dev/null
+++ b/deploy/auth/index.js
@@ -0,0 +1,36 @@
+const hbpOidc = require('./hbp-oidc')
+const passport = require('passport')
+const objStoreDb = new Map()
+
+module.exports = async (app) => {
+  app.use(passport.initialize())
+  app.use(passport.session())
+
+  passport.serializeUser((user, done) => {
+    objStoreDb.set(user.id, user)
+    done(null, user.id)
+  })
+
+  passport.deserializeUser((id, done) => {
+    const user = objStoreDb.get(id)
+    if (user) 
+      return done(null, user)
+    else
+      return done(null, false)
+  })
+
+  await hbpOidc(app)
+
+  app.get('/user', (req, res) => {
+    if (req.user) {
+      return res.status(200).send(JSON.stringify(req.user))
+    } else {
+      return res.sendStatus(401)
+    }
+  })
+
+  app.get('/logout', (req, res) => {
+    req.logout()
+    res.redirect('/')
+  })
+}
\ No newline at end of file
diff --git a/deploy/auth/oidc.js b/deploy/auth/oidc.js
new file mode 100644
index 000000000..856adc48b
--- /dev/null
+++ b/deploy/auth/oidc.js
@@ -0,0 +1,39 @@
+const { Issuer, Strategy } = require('openid-client')
+
+const defaultCb = (tokenset, {id, ...rest}, done) => {
+  return done(null, {
+    id: id || Date.now(),
+    ...rest
+  })
+}
+
+exports.configureAuth = async ({ discoveryUrl, clientId, clientSecret, redirectUri, clientConfig = {}, cb = defaultCb }) => {
+  if (!discoveryUrl)
+    throw new Error('discoveryUrl must be defined!')
+
+  if (!clientId)
+    throw new Error('clientId must be defined!')
+
+  if (!clientSecret)
+    throw new Error('clientSecret must be defined!')
+
+  if (!redirectUri)
+    throw new Error('redirectUri must be defined!')
+
+  const issuer = await Issuer.discover(discoveryUrl)
+
+  const client = new issuer.Client({
+    client_id: clientId,
+    client_secret: clientSecret,
+    ...clientConfig
+  })
+
+  const oidcStrategy = new Strategy({
+    client,
+    redirect_uri: redirectUri
+  }, cb)
+
+  return {
+    oidcStrategy
+  }
+}
\ No newline at end of file
diff --git a/deploy/catchError.js b/deploy/catchError.js
index f1e385c31..cf7c96612 100644
--- a/deploy/catchError.js
+++ b/deploy/catchError.js
@@ -2,5 +2,5 @@ module.exports = ({code = 500, error = 'an error had occured'}, req, res, next)
   /**
    * probably use more elaborate logging?
    */
-  res.status(code).send(error)
+  res.sendStatus(code)
 }
\ No newline at end of file
diff --git a/deploy/datasets/index.js b/deploy/datasets/index.js
index 1d9ac9c01..10eb068c5 100644
--- a/deploy/datasets/index.js
+++ b/deploy/datasets/index.js
@@ -2,7 +2,9 @@ const express = require('express')
 const datasetsRouter = express.Router()
 const { init, getDatasets } = require('./query')
 
-init()
+init().catch(e => {
+  console.warn(`dataset init failed`, e)
+})
 
 datasetsRouter.get('/templateName/:templateName', (req, res, next) => {
   const { templateName } = req.params
@@ -20,9 +22,10 @@ datasetsRouter.get('/templateName/:templateName', (req, res, next) => {
 
 
 
-datasetsRouter.get('/parcellationName/:parcellationName', (req, res) => {
+datasetsRouter.get('/parcellationName/:parcellationName', (req, res, next) => {
   const { parcellationName } = req.params
-  getDatasets({ parcellationName })
+  const { user } = req
+  getDatasets({ parcellationName, user })
     .then(ds => {
       res.status(200).send(JSON.stringify(ds))
     })
diff --git a/deploy/datasets/query.js b/deploy/datasets/query.js
index 816603ae5..192be5953 100644
--- a/deploy/datasets/query.js
+++ b/deploy/datasets/query.js
@@ -4,23 +4,35 @@ const path = require('path')
 
 let cachedData = null
 let otherQueryResult = null
-const queryUrl = process.env.KG_DATASET_QUERY_URL || `https://kg-int.humanbrainproject.org/query/minds/core/dataset/v1.0.0/interactiveViewerKgQuery/instances/public?size=450&vocab=https%3A%2F%2Fschema.hbp.eu%2FmyQuery%2F`
+const queryUrl = process.env.KG_DATASET_QUERY_URL || `https://kg-int.humanbrainproject.org/query/minds/core/dataset/v1.0.0/interactiveViewerKgQuery/instances?size=450&vocab=https%3A%2F%2Fschema.hbp.eu%2FmyQuery%2F`
 const timeout = process.env.TIMEOUT || 5000
 
-const fetchDatasetFromKg = () => new Promise((resolve, reject) => {
-  request(queryUrl, (err, resp, body) => {
+const fetchDatasetFromKg = (arg) => new Promise((resolve, reject) => {
+  const accessToken = arg && arg.user && arg.user.tokenset && arg.user.tokenset.access_token
+  const option = accessToken
+    ? {
+        auth: {
+          'bearer': accessToken
+        }
+      }
+    : {}
+  request(queryUrl, option, (err, resp, body) => {
     if (err)
       return reject(err)
-    try {
-      const json = JSON.parse(body)
-      return resolve(json)
-    } catch (e) {
-      return reject(e)
-    }
+    if (resp.statusCode >= 400)
+      return reject(resp.statusCode)
+    const json = JSON.parse(body)
+    return resolve(json)
   })
 })
 
-const getDs = () => Promise.race([
+const cacheData = ({results, ...rest}) => {
+  cachedData = results
+  otherQueryResult = rest
+  return cachedData
+}
+
+const getPublicDs = () => Promise.race([
   new Promise((rs, rj) => {
     setTimeout(() => {
       if (cachedData) {
@@ -32,14 +44,14 @@ const getDs = () => Promise.race([
       }
     }, timeout)
   }),
-  fetchDatasetFromKg()
-    .then(({results, ...rest}) => {
-      cachedData = results
-      otherQueryResult = rest
-      return cachedData
-    })
+  fetchDatasetFromKg().then(cacheData)
 ])
 
+
+const getDs = ({ user }) => user
+  ? fetchDatasetFromKg({ user }).then(({results}) => results)
+  : getPublicDs()
+
 /**
  * Needed by filter by parcellation
  */
@@ -116,7 +128,7 @@ exports.init = () => fetchDatasetFromKg()
     cachedData = json
   })
 
-exports.getDatasets = ({ templateName, parcellationName }) => getDs()
+exports.getDatasets = ({ templateName, parcellationName, user }) => getDs({ user })
     .then(json => filter(json, {templateName, parcellationName}))
 
 
diff --git a/deploy/package.json b/deploy/package.json
index 0dfc57457..5916cb005 100644
--- a/deploy/package.json
+++ b/deploy/package.json
@@ -11,7 +11,10 @@
   "license": "ISC",
   "dependencies": {
     "express": "^4.16.4",
+    "express-session": "^1.15.6",
+    "memorystore": "^1.6.1",
     "openid-client": "^2.4.5",
+    "passport": "^0.4.0",
     "request": "^2.88.0"
   },
   "devDependencies": {
diff --git a/deploy/server.js b/deploy/server.js
index d740f1487..8f3a55817 100644
--- a/deploy/server.js
+++ b/deploy/server.js
@@ -1,31 +1,69 @@
 const path = require('path')
 const express = require('express')
 const app = express()
+const session = require('express-session')
+const MemoryStore = require('memorystore')(session)
+
 
 app.disable('x-powered-by')
 
 if (process.env.NODE_ENV !== 'production') {
   require('dotenv').config()
   app.use(require('cors')())
+  process.on('unhandledRejection', (err, p) => {
+    console.log({err, p})
+  })
 }
 
-const templateRouter = require('./templates')
-const nehubaConfigRouter = require('./nehubaConfig')
-const datasetRouter = require('./datasets')
-const catchError = require('./catchError')
+/**
+ * load env first, then load other modules
+ */
+
+const configureAuth = require('./auth')
 
-const publicPath = process.env.NODE_ENV === 'production'
-  ? path.join(__dirname, 'public')
-  : path.join(__dirname, '..', 'dist', 'aot')
+const store = new MemoryStore({
+  checkPeriod: 86400000
+})
 
-app.use('/templates', templateRouter)
-app.use('/nehubaConfig', nehubaConfigRouter)
-app.use('/datasets', datasetRouter)
+const SESSIONSECRET = process.env.SESSIONSECRET || 'this is not really a random session secret'
 
-app.use(catchError)
+/**
+ * passport application of oidc requires session
+ */
+app.use(session({
+  secret: SESSIONSECRET,
+  resave: true,
+  saveUninitialized: false,
+  store
+}))
 
-app.use(express.static(publicPath))
+const startServer = async (app) => {
+  try{
+    await configureAuth(app)
+  }catch (e) {
+    console.log('error during configureAuth', e)
+  }
 
-const PORT = process.env.PORT || 3000
+  const templateRouter = require('./templates')
+  const nehubaConfigRouter = require('./nehubaConfig')
+  const datasetRouter = require('./datasets')
+  const catchError = require('./catchError')
+  
+  const publicPath = process.env.NODE_ENV === 'production'
+    ? path.join(__dirname, 'public')
+    : path.join(__dirname, '..', 'dist', 'aot')
+  
+  app.use('/templates', templateRouter)
+  app.use('/nehubaConfig', nehubaConfigRouter)
+  app.use('/datasets', datasetRouter)
+  
+  app.use(catchError)
+  
+  app.use(express.static(publicPath))
+  
+  const PORT = process.env.PORT || 3000
+  
+  app.listen(PORT, () => console.log(`listening on port ${PORT}`))
+}
 
-app.listen(PORT, () => console.log(`listening on port ${PORT}`))
\ No newline at end of file
+startServer(app)
\ No newline at end of file
diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts
index 014c570dc..2211af270 100644
--- a/src/atlasViewer/atlasViewer.component.ts
+++ b/src/atlasViewer/atlasViewer.component.ts
@@ -34,6 +34,7 @@ export class AtlasViewer implements OnDestroy, OnInit {
   @ViewChild('floatingMouseContextualContainer', { read: ViewContainerRef }) floatingMouseContextualContainer: ViewContainerRef
   @ViewChild('helpComponent', {read: TemplateRef}) helpComponent : TemplateRef<any>
   @ViewChild('viewerConfigComponent', {read: TemplateRef}) viewerConfigComponent : TemplateRef<any>
+  @ViewChild('loginComponent', {read: TemplateRef}) loginComponent: TemplateRef <any>
   @ViewChild(LayoutMainSide) layoutMainSide: LayoutMainSide
 
   @ViewChild(NehubaContainer) nehubaContainer: NehubaContainer
diff --git a/src/atlasViewer/pluginUnit/pluginUnit.component.ts b/src/atlasViewer/pluginUnit/pluginUnit.component.ts
index 8afdef862..5d849eab7 100644
--- a/src/atlasViewer/pluginUnit/pluginUnit.component.ts
+++ b/src/atlasViewer/pluginUnit/pluginUnit.component.ts
@@ -17,7 +17,8 @@ export class PluginUnit implements OnDestroy{
   }
 
   ngOnDestroy(){
-    console.log('plugin being destroyed')
+    if (!PRODUCTION)
+      console.log('plugin being destroyed')
   }
 
 }
\ No newline at end of file
diff --git a/src/components/pagination/pagination.component.ts b/src/components/pagination/pagination.component.ts
index 1723d4540..0e5ff89ee 100644
--- a/src/components/pagination/pagination.component.ts
+++ b/src/components/pagination/pagination.component.ts
@@ -4,7 +4,8 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'
   selector : 'pagination-component',
   templateUrl : './pagination.template.html',
   styleUrls : [
-    './pagination.style.css'
+    './pagination.style.css',
+    '../../css/darkBtns.css'
   ]
 })
 
diff --git a/src/components/pagination/pagination.style.css b/src/components/pagination/pagination.style.css
index 57325bbb9..ca46999f9 100644
--- a/src/components/pagination/pagination.style.css
+++ b/src/components/pagination/pagination.style.css
@@ -44,12 +44,6 @@ div.btn:hover:before
   opacity : 0.3;
 }
 
-:host-context([darktheme="true"]) div.btn.btn-default
-{
-  background-color:rgba(128,128,128,0.5);
-  color:rgba(240,240,240,0.9);
-}
-
 div.btn.btn-primary
 {
   transform:translateY(3%);
diff --git a/src/css/darkBtns.css b/src/css/darkBtns.css
new file mode 100644
index 000000000..35e4e24ea
--- /dev/null
+++ b/src/css/darkBtns.css
@@ -0,0 +1,12 @@
+
+:host-context([darktheme="true"]) .btn.btn-default
+{
+  background-color:rgba(128,128,128,0.5);
+  color:rgba(240,240,240,0.9);
+  border: none;
+}
+
+:host-context([darktheme="true"]) .btn.btn-default > *
+{
+  font-size: 100%;
+}
\ No newline at end of file
diff --git a/src/main.module.ts b/src/main.module.ts
index 911962ffb..94d90d087 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -32,6 +32,7 @@ import { DockedContainerDirective } from "./util/directives/dockedContainer.dire
 import { FloatingContainerDirective } from "./util/directives/floatingContainer.directive";
 import { PluginFactoryDirective } from "./util/directives/pluginFactory.directive";
 import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive";
+import { AuthService } from "./services/auth.service";
 
 @NgModule({
   imports : [
@@ -94,6 +95,7 @@ import { FloatingMouseContextualContainerDirective } from "./util/directives/flo
     AtlasViewerAPIServices,
     ToastService,
     AtlasWorkerService,
+    AuthService
   ],
   bootstrap : [
     AtlasViewer
@@ -101,4 +103,10 @@ import { FloatingMouseContextualContainerDirective } from "./util/directives/flo
 })
 
 export class MainModule{
+  
+  constructor(
+    authServce: AuthService
+  ){
+    authServce.authReloadState()
+  }
 }
\ No newline at end of file
diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css
index 96f68781b..5481f94d0 100644
--- a/src/res/css/extra_styles.css
+++ b/src/res/css/extra_styles.css
@@ -244,4 +244,25 @@ markdown-dom pre code
 {
   background-color: rgba(70, 70 , 70, 1.0);
   color: rgba(255, 255, 255, 1.0);
+}
+
+.popoverContainer .popover-header
+{
+  border: none;
+}
+
+[darktheme="true"] .popoverContainer.popover
+{
+  background-color: rgba(50,50,50);
+  color: white;
+}
+
+[darktheme="true"] .popoverContainer.popover .popover-header
+{
+  background-color: rgba(75,75,75);
+}
+
+[darktheme="true"] .popoverContainer.popover.bottom .popover-arrow.arrow::after
+{
+  border-bottom-color: rgb(75,75,75);
 }
\ No newline at end of file
diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts
new file mode 100644
index 000000000..20a8f38d1
--- /dev/null
+++ b/src/services/auth.service.ts
@@ -0,0 +1,52 @@
+import { Injectable } from "@angular/core";
+
+const IV_REDIRECT_TOKEN = `IV_REDIRECT_TOKEN`
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class AuthService{
+  public user: User | null
+
+  public logoutHref: String = 'logout'
+
+  /**
+   * TODO build it dynamically, or at least possible to configure via env var
+   */
+  public loginMethods : AuthMethod[] = [{
+    name: 'HBP OIDC',
+    href: 'hbp-oidc/auth'
+  }]
+
+  constructor() {
+    fetch('user')
+      .then(res => res.json())
+      .then(user => this.user = user)
+      .catch(e => {
+        if (!PRODUCTION)
+          console.log(`auth failed`, e)
+      })
+  }
+
+  authSaveState() {
+    window.localStorage.setItem(IV_REDIRECT_TOKEN, window.location.href)
+  }
+
+  authReloadState() {
+
+    const redirect = window.localStorage.getItem(IV_REDIRECT_TOKEN)
+    window.localStorage.removeItem(IV_REDIRECT_TOKEN)
+    if (redirect) window.location.href = redirect
+  }
+}
+
+export interface User {
+  name: String
+  id: String
+}
+
+export interface AuthMethod{
+  href: String
+  name: String
+}
diff --git a/src/ui/banner/banner.component.ts b/src/ui/banner/banner.component.ts
index fde833e0f..f69c931ab 100644
--- a/src/ui/banner/banner.component.ts
+++ b/src/ui/banner/banner.component.ts
@@ -6,12 +6,14 @@ import { map, filter, debounceTime, buffer, distinctUntilChanged } from "rxjs/op
 import { FilterNameBySearch } from "../../util/pipes/filterNameBySearch.pipe";
 import { regionAnimation } from "./regionPopover.animation";
 import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"
+import { AuthService, User, AuthMethod } from "src/services/auth.service";
 
 @Component({
   selector: 'atlas-banner',
   templateUrl: './banner.template.html',
   styleUrls: [
-    `./banner.style.css`
+    `./banner.style.css`,
+    '../../css/darkBtns.css'
   ],
   animations: [
     regionAnimation
@@ -42,7 +44,8 @@ export class AtlasBanner implements OnDestroy, OnInit {
 
   constructor(
     private store: Store<ViewerStateInterface>,
-    private constantService: AtlasViewerConstantsServices
+    private constantService: AtlasViewerConstantsServices,
+    private authService: AuthService
   ) {
     this.loadedTemplates$ = this.store.pipe(
       select('viewerState'),
@@ -139,6 +142,18 @@ export class AtlasBanner implements OnDestroy, OnInit {
     this.regionsLabelIndexMap = getLabelIndexMap(parcellation.regions)
   }
 
+  get user() : User | null {
+    return this.authService.user
+  }
+
+  get loginMethods(): AuthMethod[] {
+    return this.authService.loginMethods
+  }
+
+  get logoutHref(): String {
+    return this.authService.logoutHref
+  }
+
   selectTemplate(template: any) {
     if (this.selectedTemplate === template) {
       return
@@ -292,6 +307,11 @@ export class AtlasBanner implements OnDestroy, OnInit {
     this.constantService.showConfigSubject$.next()
   }
 
+  loginBtnOnclick() {
+    this.authService.authSaveState()
+    return true
+  }
+
   get toastDuration() {
     return this.constantService.citationToastDuration
   }
diff --git a/src/ui/banner/banner.style.css b/src/ui/banner/banner.style.css
index 407d7383f..4a47b703b 100644
--- a/src/ui/banner/banner.style.css
+++ b/src/ui/banner/banner.style.css
@@ -106,4 +106,4 @@ citations-component
 .help-container
 {
   margin:1em;
-}
\ No newline at end of file
+}
diff --git a/src/ui/banner/banner.template.html b/src/ui/banner/banner.template.html
index 7028ad640..92118772e 100644
--- a/src/ui/banner/banner.template.html
+++ b/src/ui/banner/banner.template.html
@@ -96,3 +96,48 @@
   <i (click)="showConfig()" class="glyphicon glyphicon-cog"></i>
 </div>
 <i *ngIf="!isMobile" (click)="showConfig()" class="glyphicon glyphicon-cog"></i>
+
+<!-- login btn -->
+<div *ngIf="isMobile" class="help-container">
+  <i
+    placement="auto"
+    triggers="click"
+    [outsideClick]="true"
+    [popover]="loginPopover"
+    [popoverTitle]="user ? 'Hi, ' + user.name : 'Login'"
+    [ngClass]="user ? 'glyphicon-user' : 'glyphicon-log-in'"
+    class="glyphicon"
+    containerClass="popoverContainer"></i>
+</div>
+<i
+  *ngIf="!isMobile"
+  placement="auto"
+  triggers="click"
+  [outsideClick]="true"
+  [popover]="loginPopover"
+  [popoverTitle]="user ? 'Hi, ' + user.name : 'Login with'"
+  [ngClass]="user ? 'glyphicon-user' : 'glyphicon-log-in'"
+  class="glyphicon"
+  containerClass="popoverContainer"></i>
+
+<ng-template #loginPopover>
+  <a
+    (click)="loginBtnOnclick()"
+    class="btn btn-sm btn-default"
+    [href]="logoutHref"
+    *ngIf="user">
+    <i class="glyphicon glyphicon-log-out"></i>
+    Logout
+  </a>
+  <div
+    *ngIf="!user"
+    class="btn-group-vertical">
+    <a
+      (click)="loginBtnOnclick()"
+      class="btn btn-sm btn-default"
+      *ngFor="let m of loginMethods"
+      [href]="m.href">
+      {{ m.name }}
+    </a>
+  </div>
+</ng-template>
\ No newline at end of file
diff --git a/src/ui/pluginBanner/pluginBanner.component.ts b/src/ui/pluginBanner/pluginBanner.component.ts
index cef22dab9..11c044191 100644
--- a/src/ui/pluginBanner/pluginBanner.component.ts
+++ b/src/ui/pluginBanner/pluginBanner.component.ts
@@ -1,12 +1,13 @@
 import { Component } from "@angular/core";
-import { PluginServices, PluginManifest } from "../../atlasViewer/atlasViewer.pluginService.service";
+import { PluginServices, PluginManifest } from "src/atlasViewer/atlasViewer.pluginService.service";
 
 
 @Component({
   selector : 'plugin-banner',
   templateUrl : './pluginBanner.template.html',
   styleUrls : [
-    `./pluginBanner.style.css`
+    `./pluginBanner.style.css`,
+    '../../css/darkBtns.css'
   ]
 })
 
diff --git a/src/ui/pluginBanner/pluginBanner.style.css b/src/ui/pluginBanner/pluginBanner.style.css
index 208e7f758..2bb0a1504 100644
--- a/src/ui/pluginBanner/pluginBanner.style.css
+++ b/src/ui/pluginBanner/pluginBanner.style.css
@@ -27,12 +27,6 @@
   cursor: default;
 }
 
-:host-context([darktheme="true"]) .btn-default
-{
-  background-color:rgba(80,80,80,0.8);
-  color:white;
-}
-
 .btn.btn-disabled.btn.btn-disabled.btn.btn-disabled.btn.btn-disabled
 {
   opacity:0.5;
diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts
index aa4d5a440..b8b80de74 100644
--- a/src/ui/ui.module.ts
+++ b/src/ui/ui.module.ts
@@ -45,6 +45,7 @@ import { HelpComponent } from "./help/help.component";
 import { ConfigComponent } from './config/config.component'
 import { FlatmapArrayPipe } from "src/util/pipes/flatMapArray.pipe";
 import { FilterDataEntriesByRegion } from "src/util/pipes/filterDataEntriesByRegion.pipe";
+import { PopoverModule } from 'ngx-bootstrap/popover'
 
 
 @NgModule({
@@ -55,6 +56,7 @@ import { FilterDataEntriesByRegion } from "src/util/pipes/filterDataEntriesByReg
     LayoutModule,
     ComponentsModule,
 
+    PopoverModule.forRoot(),
     TooltipModule.forRoot()
   ],
   declarations : [
-- 
GitLab