Browse Source

Colorize the listed extensions

Eric Charles 5 years ago
parent
commit
f71aaf643c

+ 4 - 0
examples/listings/Makefile

@@ -0,0 +1,4 @@
+start:
+	@exec echo open http://localhost:8888/listings/blacklist.json
+	echo open http://localhost:8888/listings/whitelist.json
+	@exec python main.py --dev-mode --watch

+ 1 - 24
examples/listings/README.md

@@ -1,26 +1,3 @@
-# JupyterLab Black/White Listings Example
-
 ```bash
-pip install flask flask_cors && \
-  yarn && \
-  yarn start
-```
-
-```bash
-open http://localhost:8080/lists/blacklist.json
-open http://localhost:8080/lists/whitelist.json
-```
-
-```json
-{
-  // Extension Manager
-  // @jupyterlab/extensionmanager-extension:plugin
-  // Extension manager settings.
-  // *********************************************
-
-  // Enabled Status
-  // Enables extension manager (requires Node.js/npm).
-  // WARNING: installing untrusted extensions may be unsafe.
-  "enabled": true
-}
+make start
 ```

+ 54 - 0
examples/listings/index.js

@@ -0,0 +1,54 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { PageConfig } from '@jupyterlab/coreutils';
+// eslint-disable-next-line
+__webpack_public_path__ = PageConfig.getOption('fullStaticUrl') + '/';
+
+// This must be after the public path is set.
+// This cannot be extracted because the public path is dynamic.
+require('./build/imports.css');
+
+window.addEventListener('load', async function() {
+  var JupyterLab = require('@jupyterlab/application').JupyterLab;
+
+  var mods = [
+    require('@jupyterlab/application-extension'),
+    require('@jupyterlab/apputils-extension'),
+    require('@jupyterlab/codemirror-extension'),
+    require('@jupyterlab/completer-extension'),
+    require('@jupyterlab/console-extension'),
+    require('@jupyterlab/csvviewer-extension'),
+    require('@jupyterlab/docmanager-extension'),
+    require('@jupyterlab/extensionmanager-extension'),
+    require('@jupyterlab/fileeditor-extension'),
+    require('@jupyterlab/filebrowser-extension'),
+    require('@jupyterlab/help-extension'),
+    require('@jupyterlab/imageviewer-extension'),
+    require('@jupyterlab/inspector-extension'),
+    require('@jupyterlab/launcher-extension'),
+    require('@jupyterlab/mainmenu-extension'),
+    require('@jupyterlab/markdownviewer-extension'),
+    require('@jupyterlab/mathjax2-extension'),
+    require('@jupyterlab/notebook-extension'),
+    require('@jupyterlab/rendermime-extension'),
+    require('@jupyterlab/running-extension'),
+    require('@jupyterlab/settingeditor-extension'),
+    require('@jupyterlab/shortcuts-extension'),
+    require('@jupyterlab/statusbar-extension'),
+    require('@jupyterlab/tabmanager-extension'),
+    require('@jupyterlab/terminal-extension'),
+    require('@jupyterlab/theme-dark-extension'),
+    require('@jupyterlab/theme-light-extension'),
+    require('@jupyterlab/tooltip-extension'),
+    require('@jupyterlab/ui-components-extension')
+  ];
+  var lab = new JupyterLab();
+  lab.registerPluginModules(mods);
+  /* eslint-disable no-console */
+  console.log('Starting app');
+  await lab.start();
+  console.log('App started, waiting for restore');
+  await lab.restored;
+  console.log('Example started!');
+});

BIN
examples/listings/jupyter.png


+ 2 - 1
examples/listings/lists/blacklist.json

@@ -3,6 +3,7 @@
     "@jupyterlab-examples/launcher",
     "@tpoff/jupyterlab-tpoff_xkcd",
     "@mohansrk/test",
-    "custom-git"
+    "custom-git",
+    "jupyterlabtd_git"
   ]
 }

+ 50 - 17
examples/listings/main.py

@@ -1,27 +1,60 @@
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
 
-from flask import Flask, g, send_from_directory
-from flask_cors import CORS
+from jupyterlab.labapp import LabApp
+from jupyterlab_server import LabServerApp, LabConfig
+from notebook.base.handlers import IPythonHandler, FileFindHandler
+from notebook.utils import url_path_join as ujoin
+import json
+import os
+from traitlets import Unicode
 
-PORT = 8080
+HERE = os.path.dirname(__file__)
 
-ROOT_FOLDER='./'
+# Turn off the Jupyter configuration system so configuration files on disk do
+# not affect this app. This helps this app to truly be standalone.
+os.environ["JUPYTER_NO_CONFIG"]="1"
 
-app = Flask(__name__, static_folder = ROOT_FOLDER)
+with open(os.path.join(HERE, 'package.json')) as fid:
+    version = json.load(fid)['version']
 
-CORS(app)
+class ExampleApp(LabApp):
+    base_url = '/'
+    default_url = Unicode('/lab',
+                          help='The default URL to redirect to from `/`')
 
-@app.route('/listings/<path:path>', defaults = {'listings': 'listingws'})
-def res(folder, path):
-    return send_from_directory(ROOT_FOLDER + listings, path)
+    lab_config = LabConfig(
+        app_name = 'JupyterLab Example App',
+        app_settings_dir = os.path.join(HERE, 'build', 'application_settings'),
+        app_version = version,
+        app_url = '/lab',
+        schemas_dir = os.path.join(HERE, 'build', 'schemas'),
+        static_dir = os.path.join(HERE, 'build'),
+        templates_dir = os.path.join(HERE, 'templates'),
+        themes_dir = os.path.join(HERE, 'build', 'themes'),
+        user_settings_dir = os.path.join(HERE, 'build', 'user_settings'),
+        workspaces_dir = os.path.join(HERE, 'build', 'workspaces'),
+    )
+
+    def init_webapp(self):
+        """initialize tornado webapp and httpserver.
+        """
+        super().init_webapp()
+        default_handlers = [
+            (
+                ujoin(self.base_url, r"/listings/(.*)"), FileFindHandler,
+                 {'path': os.path.join(HERE, 'lists')}
+            )
+        ]
+        self.web_app.add_handlers('.*$', default_handlers)
+
+    def start(self):
+        settings = self.web_app.settings
+
+        # By default, make terminals available.
+        settings.setdefault('terminals_available', True)
+
+        super().start()
 
 if __name__ == '__main__':
-    print('http://localhost:8080/lists/blacklist.json')
-    print('http://localhost:8080/lists/whitelist.json')
-    app.run(
-        host='0.0.0.0',
-        port = PORT,
-        threaded = True,
-        processes = 1,
-        )
+    ExampleApp.launch_instance()

+ 52 - 4
examples/listings/package.json

@@ -5,11 +5,59 @@
   "scripts": {
     "build": "webpack",
     "clean": "rimraf build",
-    "prepublishOnly": "npm run build",
-    "start": "concurrently \"jupyter lab --dev-mode\" \"python main.py\""
+    "prepublishOnly": "npm run build"
+  },
+  "dependencies": {
+    "@jupyterlab/application": "^2.0.0",
+    "@jupyterlab/application-extension": "^2.0.0",
+    "@jupyterlab/apputils-extension": "^2.0.0",
+    "@jupyterlab/buildutils": "^2.0.0",
+    "@jupyterlab/codemirror-extension": "^2.0.0",
+    "@jupyterlab/completer-extension": "^2.0.0",
+    "@jupyterlab/console-extension": "^2.0.0",
+    "@jupyterlab/csvviewer-extension": "^2.0.0",
+    "@jupyterlab/docmanager-extension": "^2.0.0",
+    "@jupyterlab/filebrowser-extension": "^2.0.0",
+    "@jupyterlab/extensionmanager-extension": "^2.0.0",
+    "@jupyterlab/fileeditor-extension": "^2.0.0",
+    "@jupyterlab/help-extension": "^2.0.0",
+    "@jupyterlab/imageviewer-extension": "^2.0.0",
+    "@jupyterlab/inspector-extension": "^2.0.0",
+    "@jupyterlab/launcher-extension": "^2.0.0",
+    "@jupyterlab/mainmenu-extension": "^2.0.0",
+    "@jupyterlab/markdownviewer-extension": "^2.0.0",
+    "@jupyterlab/mathjax2-extension": "^2.0.0",
+    "@jupyterlab/notebook-extension": "^2.0.0",
+    "@jupyterlab/rendermime-extension": "^2.0.0",
+    "@jupyterlab/running-extension": "^2.0.0",
+    "@jupyterlab/settingeditor-extension": "^2.0.0",
+    "@jupyterlab/shortcuts-extension": "^2.0.0",
+    "@jupyterlab/statusbar-extension": "^2.0.0",
+    "@jupyterlab/tabmanager-extension": "^2.0.0",
+    "@jupyterlab/terminal-extension": "^2.0.0",
+    "@jupyterlab/theme-dark-extension": "^2.0.0",
+    "@jupyterlab/theme-light-extension": "^2.0.0",
+    "@jupyterlab/tooltip-extension": "^2.0.0",
+    "@jupyterlab/ui-components-extension": "^2.0.0",
+    "es6-promise": "~4.2.8",
+    "react": "~16.9.0",
+    "react-dom": "~16.9.0"
   },
-  "dependencies": {},
   "devDependencies": {
-    "concurrently": "5.1.0"
+    "css-loader": "~3.2.0",
+    "file-loader": "~5.0.2",
+    "fs-extra": "^8.1.0",
+    "glob": "~7.1.6",
+    "mini-css-extract-plugin": "~0.8.0",
+    "raw-loader": "~4.0.0",
+    "read-package-tree": "^5.3.1",
+    "rimraf": "~3.0.0",
+    "style-loader": "~1.0.1",
+    "svg-url-loader": "~3.0.3",
+    "url-loader": "~3.0.0",
+    "watch": "~1.0.2",
+    "webpack": "^4.41.2",
+    "webpack-cli": "^3.3.10",
+    "whatwg-fetch": "^3.0.0"
   }
 }

+ 59 - 0
examples/listings/templates/error.html

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) Jupyter Development Team.
+Distributed under the terms of the Modified BSD License.
+-->
+<html>
+
+<head>
+  <meta charset="utf-8">
+
+  <title>{% block title %}{{page_title | e}}{% endblock %}</title>
+
+  {% block favicon %}<link rel="shortcut icon" type="image/x-icon" href="/static/base/images/favicon.ico">{% endblock %}
+
+</head>
+
+<body>
+
+{% block stylesheet %}
+<style type="text/css">
+/* disable initial hide */
+div#header, div#site {
+    display: block;
+}
+</style>
+{% endblock %}
+{% block site %}
+
+<div class="error">
+    {% block h1_error %}
+    <h1>{{status_code | e}} : {{status_message | e}}</h1>
+    {% endblock h1_error %}
+    {% block error_detail %}
+    {% if message %}
+    <p>The error was:</p>
+    <div class="traceback-wrapper">
+    <pre class="traceback">{{message | e}}</pre>
+    </div>
+    {% endif %}
+    {% endblock %}
+</header>
+
+{% endblock %}
+
+{% block script %}
+<script type='text/javascript'>
+window.onload = function () {
+  var tb = document.getElementsByClassName('traceback')[0];
+  tb.scrollTop = tb.scrollHeight;
+  {% if message %}
+  console.error("{{message | e}}")
+  {% endif %}
+};
+</script>
+{% endblock script %}
+
+</body>
+
+</html>

+ 29 - 0
examples/listings/templates/index.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>{{page_config['appName'] | e}}</title>
+</head>
+<body>
+    {# Copy so we do not modify the page_config with updates. #}
+    {% set page_config_full = page_config.copy() %}
+    
+    {# Set a dummy variable - we just want the side effect of the update. #}
+    {% set _ = page_config_full.update(baseUrl=base_url, wsUrl=ws_url) %}
+    
+      <script id="jupyter-config-data" type="application/json">
+        {{ page_config_full | tojson }}
+      </script>
+  <script src="{{page_config['fullStaticUrl'] | e}}/bundle.js" main="index"></script>
+
+  <script type="text/javascript">
+    /* Remove token from URL. */
+    (function () {
+      var parsedUrl = new URL(window.location.href);
+      if (parsedUrl.searchParams.get('token')) {
+        parsedUrl.searchParams.delete('token');
+        window.history.replaceState({ }, '', parsedUrl.href);
+      }
+    })();
+  </script>
+</body>
+</html>

+ 70 - 0
examples/listings/webpack.config.js

@@ -0,0 +1,70 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+var data = require('./package.json');
+var Build = require('@jupyterlab/buildutils').Build;
+
+var names = Object.keys(data.dependencies).filter(function(name) {
+  var packageData = require(name + '/package.json');
+  return packageData.jupyterlab !== undefined;
+});
+
+var extras = Build.ensureAssets({
+  packageNames: names,
+  output: './build'
+});
+
+module.exports = [
+  {
+    entry: ['whatwg-fetch', './index.js'],
+    output: {
+      path: __dirname + '/build',
+      filename: 'bundle.js'
+    },
+    node: {
+      fs: 'empty'
+    },
+    bail: true,
+    devtool: 'source-map',
+    mode: 'development',
+    module: {
+      rules: [
+        { test: /\.css$/, use: ['style-loader', 'css-loader'] },
+        { test: /\.html$/, use: 'file-loader' },
+        { test: /\.md$/, use: 'raw-loader' },
+        { test: /\.(jpg|png|gif)$/, use: 'file-loader' },
+        { test: /\.js.map$/, use: 'file-loader' },
+        {
+          test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
+          use: 'url-loader?limit=10000&mimetype=application/font-woff'
+        },
+        {
+          test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
+          use: 'url-loader?limit=10000&mimetype=application/font-woff'
+        },
+        {
+          test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
+          use: 'url-loader?limit=10000&mimetype=application/octet-stream'
+        },
+        { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' },
+        {
+          // In .css files, svg is loaded as a data URI.
+          test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+          issuer: { test: /\.css$/ },
+          use: {
+            loader: 'svg-url-loader',
+            options: { encoding: 'none', limit: 10000 }
+          }
+        },
+        {
+          // In .ts and .tsx files (both of which compile to .js), svg files
+          // must be loaded as a raw string instead of data URIs.
+          test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+          issuer: { test: /\.js$/ },
+          use: {
+            loader: 'raw-loader'
+          }
+        }
+      ]
+    }
+  }
+].concat(extras);

+ 1 - 1
packages/extensionmanager/src/index.ts

@@ -2,6 +2,6 @@
 // Distributed under the terms of the Modified BSD License.
 
 export * from './model';
-export * from './query';
+export * from './npm';
 export * from './listings';
 export * from './widget';

+ 25 - 4
packages/extensionmanager/src/listings.ts

@@ -7,10 +7,15 @@
  */
 export interface IListResult {
   /**
-   * A collection of search results.
+   * A collection of back listed extensions.
    */
   blacklist: string[];
 
+  /**
+   * A collection of white listed extensions.
+   */
+  whitelist: string[];
+
   /**
    * Timestamp of the search result creation.
    */
@@ -29,15 +34,15 @@ export class Lister {
    * @param whiteListUri The URI of the CDN to use for fetching full package data.
    */
   constructor(
-    blackListUri = 'http://localhost:8080/lists/blacklist.json',
-    whiteListUri = 'http://localhost:8080/lists/whitelist.json'
+    blackListUri = 'http://localhost:8888/listings/blacklist.json',
+    whiteListUri = 'http://localhost:8888/listings/whitelist.json'
   ) {
     this.blackListUri = blackListUri;
     this.whiteListUri = whiteListUri;
   }
 
   /**
-   * Search for a jupyterlab extension.
+   * Get the black list.
    *
    * @param page The page of results to fetch.
    * @param pageination The pagination size to use. See registry API documentation for acceptable values.
@@ -52,6 +57,22 @@ export class Lister {
     });
   }
 
+  /**
+   * Get the white list.
+   *
+   * @param page The page of results to fetch.
+   * @param pageination The pagination size to use. See registry API documentation for acceptable values.
+   */
+  getWhiteList(): Promise<IListResult> {
+    const uri = new URL('', this.whiteListUri);
+    return fetch(uri.toString()).then((response: Response) => {
+      if (response.ok) {
+        return response.json();
+      }
+      return [];
+    });
+  }
+
   /**
    * The URI of the black listing registry to use.
    */

+ 66 - 15
packages/extensionmanager/src/model.ts

@@ -23,7 +23,7 @@ import {
 
 import { reportInstallError } from './dialog';
 
-import { Searcher, ISearchResult, isJupyterOrg } from './query';
+import { Searcher, ISearchResult, isJupyterOrg } from './npm';
 
 import { Lister, IListResult } from './listings';
 
@@ -72,8 +72,13 @@ export interface IEntry {
   installed_version: string;
 
   isBlacklisted: boolean;
+
+  isWhitelisted: boolean;
 }
 
+/***
+ * Information about a listed entry.
+ */
 export interface IListEntry {
   /**
    * The name of the extension.
@@ -411,9 +416,9 @@ export class ListModel extends VDomModel {
    */
   protected async translateSearchResult(
     res: Promise<ISearchResult>,
-    blacklistMap: Map<string, IListEntry>
+    blacklistMap: Map<string, IListEntry>,
+    whitelistMap: Map<string, IListEntry>
   ): Promise<{ [key: string]: IEntry }> {
-    console.log('::::', typeof blacklistMap);
     let entries: { [key: string]: IEntry } = {};
     for (let obj of (await res).objects) {
       let pkg = obj.package;
@@ -434,13 +439,14 @@ export class ListModel extends VDomModel {
         status: null,
         latest_version: pkg.version,
         installed_version: '',
-        isBlacklisted: blacklistMap.has(pkg.name)
+        isBlacklisted: blacklistMap.has(pkg.name),
+        isWhitelisted: whitelistMap.has(pkg.name)
       };
     }
     return entries;
   }
 
-  protected async translateBlacklistResult(
+  protected async translateBlacklistingResult(
     res: Promise<IListResult>
   ): Promise<Map<string, IListEntry>> {
     let entries: Map<string, IListEntry> = new Map();
@@ -450,6 +456,16 @@ export class ListModel extends VDomModel {
     return entries;
   }
 
+  protected async translateWhitelistingResult(
+    res: Promise<IListResult>
+  ): Promise<Map<string, IListEntry>> {
+    let entries: Map<string, IListEntry> = new Map();
+    for (let obj of (await res).whitelist) {
+      entries.set(obj, { name: obj });
+    }
+    return entries;
+  }
+
   /**
    * Translate installed extensions information from the server into entries.
    *
@@ -457,7 +473,8 @@ export class ListModel extends VDomModel {
    */
   protected async translateInstalled(
     res: Promise<IInstalledEntry[]>,
-    blacklistMap: Map<string, IListEntry>
+    blacklistMap: Map<string, IListEntry>,
+    whitelistMap: Map<string, IListEntry>
   ): Promise<{ [key: string]: IEntry }> {
     const promises = [];
     const entries: { [key: string]: IEntry } = {};
@@ -473,7 +490,8 @@ export class ListModel extends VDomModel {
             status: pkg.status,
             latest_version: pkg.latest_version,
             installed_version: pkg.installed_version,
-            isBlacklisted: blacklistMap.has(pkg.name)
+            isBlacklisted: blacklistMap.has(pkg.name),
+            isWhitelisted: whitelistMap.has(pkg.name)
           };
         })
       );
@@ -523,7 +541,8 @@ export class ListModel extends VDomModel {
    * @returns {Promise<{ [key: string]: IEntry; }>} The search result as a map of entries.
    */
   protected async performSearch(
-    blacklistingMap: Map<string, IListEntry>
+    blacklistingMap: Map<string, IListEntry>,
+    whitelistingMap: Map<string, IListEntry>
   ): Promise<{ [key: string]: IEntry }> {
     if (this.query === null) {
       this._searchResult = [];
@@ -538,7 +557,11 @@ export class ListModel extends VDomModel {
       this.page,
       this.pagination
     );
-    let searchMapPromise = this.translateSearchResult(search, blacklistingMap);
+    let searchMapPromise = this.translateSearchResult(
+      search,
+      blacklistingMap,
+      whitelistingMap
+    );
 
     let searchMap: { [key: string]: IEntry };
     try {
@@ -553,9 +576,9 @@ export class ListModel extends VDomModel {
   }
 
   protected async performGetBlacklist(): Promise<Map<string, IListEntry>> {
-    // Start the search without waiting for it:
+    // Start the fetch without waiting for it:
     let blacklist = this.lister.getBlackList();
-    let blacklistMapPromise = this.translateBlacklistResult(blacklist);
+    let blacklistMapPromise = this.translateBlacklistingResult(blacklist);
 
     let blacklistMap: Map<string, IListEntry>;
     try {
@@ -569,6 +592,23 @@ export class ListModel extends VDomModel {
     return blacklistMap;
   }
 
+  protected async performGetWhitelist(): Promise<Map<string, IListEntry>> {
+    // Start the fetch without waiting for it:
+    let whitelist = this.lister.getWhiteList();
+    let whitelistMapPromise = this.translateWhitelistingResult(whitelist);
+
+    let whitelisttMap: Map<string, IListEntry>;
+    try {
+      whitelisttMap = await whitelistMapPromise;
+      this.blacklistError = null;
+    } catch (reason) {
+      whitelisttMap = new Map();
+      this.blacklistError = reason.toString();
+    }
+
+    return whitelisttMap;
+  }
+
   /**
    * Query the installed extensions.
    *
@@ -578,13 +618,15 @@ export class ListModel extends VDomModel {
    */
   protected async queryInstalled(
     refreshInstalled: boolean,
-    blacklistMap: Map<string, IListEntry>
+    blacklistMap: Map<string, IListEntry>,
+    whitelisttMap: Map<string, IListEntry>
   ): Promise<{ [key: string]: IEntry }> {
     let installedMap;
     try {
       installedMap = await this.translateInstalled(
         this.fetchInstalled(refreshInstalled),
-        blacklistMap
+        blacklistMap,
+        whitelisttMap
       );
       this.installedError = null;
     } catch (reason) {
@@ -603,12 +645,21 @@ export class ListModel extends VDomModel {
    */
   protected async update(refreshInstalled = false) {
     // Start both queries before awaiting:
+
     const blacklistingPromise = this.performGetBlacklist();
     const blacklistingMap = await blacklistingPromise;
-    const searchMapPromise = this.performSearch(blacklistingMap);
+
+    const whitelistingPromise = this.performGetWhitelist();
+    const whitelistingMap = await whitelistingPromise;
+
+    const searchMapPromise = this.performSearch(
+      blacklistingMap,
+      whitelistingMap
+    );
     const installedMapPromise = this.queryInstalled(
       refreshInstalled,
-      blacklistingMap
+      blacklistingMap,
+      whitelistingMap
     );
 
     // Await results:

+ 0 - 0
packages/extensionmanager/src/query.ts → packages/extensionmanager/src/npm.ts


+ 32 - 18
packages/extensionmanager/src/widget.tsx

@@ -9,10 +9,12 @@ import {
   caretRightIcon,
   Collapse,
   InputGroup,
-  //  notTrustedIcon as blacklistIcon,
-  bugIcon as blacklistIcon,
+  Checkbox,
+  Switch,
+  blacklistedIcon,
   jupyterIcon,
-  refreshIcon
+  refreshIcon,
+  whitelistedIcon
 } from '@jupyterlab/ui-components';
 
 import { Message } from '@lumino/messaging';
@@ -20,7 +22,7 @@ import * as React from 'react';
 import ReactPaginate from 'react-paginate';
 
 import { ListModel, IEntry, Action } from './model';
-import { isJupyterOrg } from './query';
+import { isJupyterOrg } from './npm';
 
 // TODO: Replace pagination with lazy loading of lower search results
 
@@ -64,6 +66,12 @@ export class SearchBar extends React.Component<
           value={this.state.value}
           rightIcon="search"
         />
+        <br />
+        <Switch
+          checked={false}
+          label="I understand that extensions managed through this interface run arbitrary code that may be dangerous."
+        />
+        <Checkbox label="I understand that extensions managed through this interface run arbitrary code that may be dangerous." />
       </div>
     );
   }
@@ -157,11 +165,12 @@ function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
   let title = entry.name;
   if (isJupyterOrg(entry.name)) {
     flagClasses.push(`jp-extensionmanager-entry-mod-whitelisted`);
-    title = `${entry.name} (Developed by Project Jupyter)`;
+  }
+  if (entry.isWhitelisted) {
+    flagClasses.push(`jp-extensionmanager-entry-is-whitelisted`);
   }
   if (entry.isBlacklisted) {
-    flagClasses.push(`jp-extensionmanager-entry-mod-blacklisted`);
-    title = `${entry.name} is blacklisted`;
+    flagClasses.push(`jp-extensionmanager-entry-is-blacklisted`);
   }
   return (
     <li
@@ -174,17 +183,22 @@ function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
             {entry.name}
           </a>
         </div>
-        <jupyterIcon.react
-          className="jp-extensionmanager-entry-jupyter-org"
-          top="1px"
-          height="auto"
-          width="1em"
-        />
-        {entry.isBlacklisted === true && (
-          <blacklistIcon.react
-            className="jp-extensionmanager-entry-blacklisted"
-            top="1px"
-            kind="menuItem"
+        {isJupyterOrg(entry.name) && (
+          <ToolbarButtonComponent
+            icon={jupyterIcon}
+            iconLabel={entry.name + ' (Developed by Project Jupyter)'}
+          />
+        )}
+        {entry.isBlacklisted && (
+          <ToolbarButtonComponent
+            icon={blacklistedIcon}
+            iconLabel={entry.name + ' is blacklisted'}
+          />
+        )}
+        {entry.isWhitelisted && (
+          <ToolbarButtonComponent
+            icon={whitelistedIcon}
+            iconLabel={entry.name + ' is whitelisted'}
           />
         )}
       </div>

+ 6 - 3
packages/extensionmanager/style/base.css

@@ -164,9 +164,12 @@
   display: inline;
 }
 
-.jp-extensionmanager-entry.jp-extensionmanager-entry-mod-blacklisted
-  .jp-extensionmanager-entry-blacklisted {
-  display: inline;
+.jp-extensionmanager-entry.jp-extensionmanager-entry-is-blacklisted {
+  background-color: var(--jp-error-color3);
+}
+
+.jp-extensionmanager-entry.jp-extensionmanager-entry-is-whitelisted {
+  background-color: var(--jp-success-color3);
 }
 
 /* Precedence order update/error/warning matters! */

+ 16 - 0
packages/ui-components/src/blueprint.tsx

@@ -22,6 +22,14 @@ import {
   Select as BPSelect,
   ISelectProps
 } from '@blueprintjs/select/lib/cjs/components/select/select';
+import {
+  Checkbox as BPCheckbox,
+  ICheckboxProps
+} from '@blueprintjs/core/lib/cjs/components/forms/controls';
+import {
+  Switch as BPSwitch,
+  ISwitchProps
+} from '@blueprintjs/core/lib/cjs/components/forms/controls';
 export { Intent } from '@blueprintjs/core/lib/cjs/common/intent';
 
 import { classes } from './utils';
@@ -77,3 +85,11 @@ export const Collapse = (props: ICollapseProps & CommonProps<any>) => (
 export const Select = (props: ISelectProps<any> & CommonProps<any>) => (
   <BPSelect {...props} className={classes(props.className, 'jp-Select')} />
 );
+
+export const Checkbox = (props: ICheckboxProps & CommonProps<any>) => (
+  <BPCheckbox {...props} className={classes(props.className, 'jp-Checkbox')} />
+);
+
+export const Switch = (props: ISwitchProps & CommonProps<any>) => (
+  <BPSwitch {...props} className={classes(props.className, 'jp-Switch')} />
+);

+ 4 - 0
packages/ui-components/src/icon/iconimports.ts

@@ -9,6 +9,7 @@ import { LabIcon } from './labicon';
 
 // icon svg import statements
 import addSvgstr from '../../style/icons/toolbar/add.svg';
+import blacklistedSvgstr from '../../style/icons/listing/blacklisted.svg';
 import bugSvgstr from '../../style/icons/toolbar/bug.svg';
 import buildSvgstr from '../../style/icons/sidebar/build.svg';
 import caretDownEmptySvgstr from '../../style/icons/arrow/caret-down-empty.svg';
@@ -72,10 +73,12 @@ import textEditorSvgstr from '../../style/icons/filetype/text-editor.svg';
 import trustedSvgstr from '../../style/icons/statusbar/trusted.svg';
 import undoSvgstr from '../../style/icons/toolbar/undo.svg';
 import vegaSvgstr from '../../style/icons/filetype/vega.svg';
+import whitelistedSvgstr from '../../style/icons/listing/whitelisted.svg';
 import yamlSvgstr from '../../style/icons/filetype/yaml.svg';
 
 // LabIcon instance construction
 export const addIcon = new LabIcon({ name: 'ui-components:add', svgstr: addSvgstr });
+export const blacklistedIcon = new LabIcon({ name: 'ui-components:blaklisted', svgstr: blacklistedSvgstr });
 export const bugIcon = new LabIcon({ name: 'ui-components:bug', svgstr: bugSvgstr });
 export const buildIcon = new LabIcon({ name: 'ui-components:build', svgstr: buildSvgstr });
 export const caretDownEmptyIcon = new LabIcon({ name: 'ui-components:caret-down-empty', svgstr: caretDownEmptySvgstr });
@@ -139,4 +142,5 @@ export const textEditorIcon = new LabIcon({ name: 'ui-components:text-editor', s
 export const trustedIcon = new LabIcon({ name: 'ui-components:trusted', svgstr: trustedSvgstr });
 export const undoIcon = new LabIcon({ name: 'ui-components:undo', svgstr: undoSvgstr });
 export const vegaIcon = new LabIcon({ name: 'ui-components:vega', svgstr: vegaSvgstr });
+export const whitelistedIcon = new LabIcon({ name: 'ui-components:whitelisted', svgstr: whitelistedSvgstr });
 export const yamlIcon = new LabIcon({ name: 'ui-components:yaml', svgstr: yamlSvgstr });

+ 1 - 0
packages/ui-components/style/icons/listing/blacklisted.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="3 3 16 16"><defs><linearGradient gradientUnits="userSpaceOnUse" y2="-2.623" x2="0" y1="986.67"><stop stop-color="#ffce3b"/><stop offset="1" stop-color="#ffd762"/></linearGradient><linearGradient id="0" gradientUnits="userSpaceOnUse" y1="986.67" x2="0" y2="-2.623"><stop stop-color="#ffce3b"/><stop offset="1" stop-color="#fef4ab"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" x2="1" x1="0" xlink:href="#0"/></defs><g transform="matrix(2 0 0 2-11-2071.72)"><path transform="translate(7 1037.36)" d="m4 0c-2.216 0-4 1.784-4 4 0 2.216 1.784 4 4 4 2.216 0 4-1.784 4-4 0-2.216-1.784-4-4-4" fill="#da4453"/><path d="m11.906 1041.46l.99-.99c.063-.062.094-.139.094-.229 0-.09-.031-.166-.094-.229l-.458-.458c-.063-.062-.139-.094-.229-.094-.09 0-.166.031-.229.094l-.99.99-.99-.99c-.063-.062-.139-.094-.229-.094-.09 0-.166.031-.229.094l-.458.458c-.063.063-.094.139-.094.229 0 .09.031.166.094.229l.99.99-.99.99c-.063.062-.094.139-.094.229 0 .09.031.166.094.229l.458.458c.063.063.139.094.229.094.09 0 .166-.031.229-.094l.99-.99.99.99c.063.063.139.094.229.094.09 0 .166-.031.229-.094l.458-.458c.063-.062.094-.139.094-.229 0-.09-.031-.166-.094-.229l-.99-.99" fill="#fff"/></g></svg>

+ 36 - 0
packages/ui-components/style/icons/listing/whitelisted.svg

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   id="Layer_3"
+   x="0px"
+   y="0px"
+   viewBox="0 0 111.2 111.2"
+   xml:space="preserve"
+   width="111.2"
+   height="111.2"
+   >
+   <style
+     type="text/css"
+     id="style2">
+	.st0{fill:#53245E;}
+	.st1{fill:#C6AAC8;}
+	.st2{fill:#8E548D;}
+  </style>
+  <path
+     class="st0"
+     d="m 69.2,5.6000031 h -7.4 c -0.8,0 -1.6,-0.5 -2,-1.1 -1.7,-2.7 -4.7,-4.5000000482422 -8.2,-4.5000000482422 -3.5,0 -6.4,1.9000000482422 -8.2,4.5000000482422 -0.5,0.6 -1.1,1.1 -2,1.1 h -7.3 c -4.1,0 -7.7,3.1 -7.8,7.0999999 -0.3,4.4 3.3,8.2 7.7,8.2 h 35.4 c 4.4,0 7.8,-3.8 7.7,-8.2 C 76.9,8.7000031 73.3,5.6000031 69.2,5.6000031 Z"
+     id="path4"
+     style="fill:#53245e" /><path
+     class="st1"
+     d="M 90.4,14.400003 H 83 c -0.6,0 -1.1,0.5 -1.3,1.1 -0.5,2.4 -1.4,4.5 -3.1,6.4 -2.4,2.5 -5.6,3.9 -8.9,3.9 H 34.1 c -3.4,0 -6.6,-1.4 -8.9,-3.9 -1.7,-1.7 -2.7,-4.1 -3.1,-6.4 -0.2,-0.6 -0.6,-1.1 -1.3,-1.1 h -7.5 c -0.6,0 -1.3,0.5 -1.3,1.1 V 108.9 c 0,0.6 0.6,1.3 1.3,1.3 h 58 c 1.1,0 1.6,-1.3 0.9,-2 -2.8,-3.4 -4.5,-8 -4.5,-12.899997 0,-11.3 9.2,-20.5 20.5,-20.5 0.6,0 1.3,0 1.9,0.2 0.8,0 1.4,-0.5 1.4,-1.3 v -58.2 c 0.1,-0.7 -0.5,-1.1 -1.1,-1.1 z m -40.3,78.5 H 26.4 c -1.3,0 -2.4,-1.1 -2.4,-2.4 0,-1.3 1.1,-2.4 2.4,-2.4 h 23.7 c 1.3,0 2.4,1.1 2.4,2.4 0,1.3 -1,2.4 -2.4,2.4 z m 27.1,-20.4 H 26.4 c -1.3,0 -2.4,-1.1 -2.4,-2.4 0,-1.3 1.1,-2.4 2.4,-2.4 h 50.8 c 1.3,0 2.4,1.1 2.4,2.4 0,1.3 -1.1,2.4 -2.4,2.4 z m 0,-14.499999 H 26.4 c -1.3,0 -2.4,-1.1 -2.4,-2.4 0,-1.3 1.1,-2.4 2.4,-2.4 h 50.8 c 1.3,0 2.4,1.1 2.4,2.4 0,1.4 -1.1,2.4 -2.4,2.4 z m 0,-14.5 H 26.4 c -1.3,0 -2.4,-1.1 -2.4,-2.400001 0,-1.3 1.1,-2.4 2.4,-2.4 h 50.8 c 1.3,0 2.4,1.1 2.4,2.4 0,1.300001 -1.1,2.400001 -2.4,2.400001 z"
+     id="path6"
+     style="fill:#c6aac8" /><path
+     class="st2"
+     d="m 88.4,79.600003 c -8.8,0 -15.8,7.1 -15.8,15.8 C 72.6,104.1 79.7,111.2 88.4,111.2 c 8.7,0 15.8,-7.1 15.8,-15.799997 0,-8.7 -7.2,-15.8 -15.8,-15.8 z m 9.7,12.1 L 86.3,103.6 c -0.5,0.5 -1.1,0.6 -1.7,0.6 0,0 0,0 -0.2,0 -0.6,0 -1.3,-0.3 -1.7,-0.8 l -5.5,-6.599997 c -0.8,-0.9 -0.8,-2.5 0.3,-3.3 0.9,-0.8 2.5,-0.8 3.3,0.3 l 3,3.6 c 0.5,0.5 1.3,0.6 1.7,0 l 8.9,-9.1 c 0.9,-0.9 2.4,-0.9 3.3,0 1.2,1 1.2,2.4 0.4,3.4 z"
+     id="path8"
+     style="fill:#8e548d" />
+  </svg>
+