Browse Source

More license updates (#9779)

* bump license year, remove extra package licenses

* add licenses API/CLI

* bump license-webpack-plugin to federated-module-aware minimum version

* resolve yarn locks

* hoist licenses config to new plugin, emit JSON

* remove extra import

* bump lwp even more

* resolve envs, again

* revert jupyterlab directory, concentrate on dev_mode

* add path handling for dev_mode, report parsing

* rework reports with csv, md, json

* step back from schema for now

* add federated stuff

* remove license plumbing, put behind conditionals (leave CLI)

* tune up license output to be more spdx-like

* linting

* start grid-based UI for licenses in help

* more work on help - licenses

* mostly working splitpanel viewer

* add report download toolbar, theme grid

* shed most of the licenses CLI

* check for LicensesApp instead of LicensesManager

* bump lwpp version, resolve, remove special-case stuff

* restore special case for self

* make toolbar prettier, add refresh

* add licensesTracker

* use spacer, move refresh to the right

* add license to ap package.jsons

* update staging yarn.lock

* linting

* restore licenses as plugin

* start removing datagrid from licenses

* more work on license grid, docs

* more work on license grid style

* adapt to top-level bundles key, more work on table

* ensure tab selection triggers stateChanged

* remove some ids to better support multiple license views

* add filters

* more work on widget restoring

* try some more restorer stuff

* relax some min-widths

* reference to proper upstream

* fix other reference to jupyter_server

* fix lint, add JSX to eslint globals

* more return type annotations
Nicholas Bollweg 3 years ago
parent
commit
4817dbc6b1

+ 3 - 0
.eslintrc.js

@@ -6,6 +6,9 @@ module.exports = {
     node: true,
     'jest/globals': true
   },
+  globals: {
+    JSX: 'readonly'
+  },
   root: true,
   extends: [
     'eslint:recommended',

+ 1 - 2
LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) 2015 Project Jupyter Contributors
+Copyright (c) 2015-2021 Project Jupyter Contributors
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
@@ -31,4 +31,3 @@ Semver File License
 
 The semver.py file is from https://github.com/podhmo/python-semver
 which is licensed under the "MIT" license.  See the semver.py file for details.
-

+ 1 - 1
builder/package.json

@@ -53,7 +53,7 @@
     "file-loader": "~6.0.0",
     "fs-extra": "^9.0.1",
     "glob": "~7.1.6",
-    "license-webpack-plugin": "^2.3.11",
+    "license-webpack-plugin": "^2.3.14",
     "mini-css-extract-plugin": "~1.3.2",
     "path-browserify": "^1.0.0",
     "process": "^0.11.10",

+ 2 - 4
builder/src/extensionConfig.ts

@@ -4,7 +4,7 @@
 import * as path from 'path';
 import * as webpack from 'webpack';
 import { Build } from './build';
-import { LicenseWebpackPlugin } from 'license-webpack-plugin';
+import { WPPlugin } from './webpack-plugins';
 import { merge } from 'webpack-merge';
 import * as fs from 'fs-extra';
 import * as glob from 'glob';
@@ -237,9 +237,7 @@ function generateConfig({
 
   if (mode === 'production') {
     plugins.push(
-      new LicenseWebpackPlugin({
-        perChunkOutput: false,
-        outputFilename: 'third-party-licenses.txt',
+      new WPPlugin.JSONLicenseWebpackPlugin({
         excludedPackageTest: packageName => packageName === data.name
       })
     );

+ 85 - 0
builder/src/webpack-plugins.ts

@@ -6,6 +6,9 @@
 import DuplicatePackageCheckerPlugin from 'duplicate-package-checker-webpack-plugin';
 import * as fs from 'fs-extra';
 import * as webpack from 'webpack';
+import { LicenseWebpackPlugin } from 'license-webpack-plugin';
+import { LicenseIdentifiedModule } from 'license-webpack-plugin/dist/LicenseIdentifiedModule';
+import { PluginOptions } from 'license-webpack-plugin/dist/PluginOptions';
 
 // From
 // https://github.com/webpack/webpack/blob/95120bdf98a01649740b104bebc426b0123651ce/lib/WatchIgnorePlugin.js
@@ -166,4 +169,86 @@ export namespace WPPlugin {
 
     options: DuplicatePackageCheckerPlugin.Options;
   }
+
+  /**
+   * A top-level report of the licenses for all code included in a bundle
+   *
+   * ### Note
+   *
+   * This is roughly informed by the terms defined in the SPDX spec, though is not
+   * an SPDX Document, since there seem to be several (incompatible) specs
+   * in that repo.
+   *
+   * @see https://github.com/spdx/spdx-spec/blob/development/v2.2.1/schemas/spdx-schema.json
+   **/
+  export interface ILicenseReport {
+    packages: IPackageLicenseInfo[];
+  }
+
+  /**
+   * A best-effort single bundled package's information.
+   *
+   * ### Note
+   *
+   * This is roughly informed by SPDX `packages` and `hasExtractedLicenseInfos`,
+   * as making it conformant would vastly complicate the structure.
+   *
+   * @see https://github.com/spdx/spdx-spec/blob/development/v2.2.1/schemas/spdx-schema.json
+   **/
+  export interface IPackageLicenseInfo {
+    /** the name of the package as it appears in node_modules */
+    name: string;
+    /** the version of the package, or an empty string if unknown */
+    versionInfo: string;
+    /** an SPDX license or LicenseRef, or an empty string if unknown */
+    licenseId: string;
+    /** the verbatim extracted text of the license, or an empty string if unknown */
+    extractedText: string;
+  }
+
+  /**
+   * A well-known filename for third-party license information.
+   *
+   * ### Note
+   * If an alternate JupyterLab-based ecosystem wanted to implement a different
+   * name, they may _still_ need to handle the presence of this file if reusing
+   * any core files or extensions.
+   *
+   * If multiple files are found by `jupyterlab_server, their `packages` will
+   * be concatenated.
+   */
+  export const DEFAULT_LICENSE_REPORT_FILENAME = 'third-party-licenses.json';
+
+  /**
+   * a plugin that creates a predictable, machine-readable report of licenses for
+   * all modules included in this build
+   */
+  export class JSONLicenseWebpackPlugin extends LicenseWebpackPlugin {
+    constructor(pluginOptions: PluginOptions = {}) {
+      super({
+        outputFilename: DEFAULT_LICENSE_REPORT_FILENAME,
+        ...pluginOptions,
+        renderLicenses: modules => this.renderLicensesJSON(modules),
+        perChunkOutput: false
+      });
+    }
+
+    /** render an SPDX-like record */
+    renderLicensesJSON(modules: LicenseIdentifiedModule[]): string {
+      const report: ILicenseReport = { packages: [] };
+
+      modules.sort((left, right) => (left.name < right.name ? -1 : 1));
+
+      for (const mod of modules) {
+        report.packages.push({
+          name: mod.name || '',
+          versionInfo: mod.packageJson.version || '',
+          licenseId: mod.licenseId || '',
+          extractedText: mod.licenseText || ''
+        });
+      }
+
+      return JSON.stringify(report, null, 2);
+    }
+  }
 }

+ 8 - 0
buildutils/src/ensure-package.ts

@@ -552,6 +552,14 @@ export async function ensurePackage(
     }
   }
 
+  // Ensure extra LICENSE is not packaged (always use repo license)
+  let licenseFile = path.join(pkgPath, 'LICENSE');
+
+  if (fs.existsSync(licenseFile)) {
+    messages.push('Removed LICENSE (prefer top-level)');
+    await fs.unlink(licenseFile);
+  }
+
   if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
     messages.push('Updated package.json');
   }

+ 2 - 1
dev_mode/package.json

@@ -2,6 +2,7 @@
   "name": "@jupyterlab/application-top",
   "version": "3.1.0-alpha.10",
   "private": true,
+  "license": "BSD-3-Clause",
   "scripts": {
     "build": "npm run clean && webpack",
     "build:dev": "npm run build",
@@ -180,7 +181,7 @@
     "handlebars": "^4.5.3",
     "html-loader": "~1.3.0",
     "html-webpack-plugin": "^5.0.0-beta.6",
-    "license-webpack-plugin": "^2.3.11",
+    "license-webpack-plugin": "^2.3.14",
     "mini-css-extract-plugin": "~1.3.2",
     "raw-loader": "~4.0.0",
     "rimraf": "~3.0.0",

+ 2 - 5
dev_mode/webpack.prod.config.js

@@ -1,7 +1,6 @@
 const merge = require('webpack-merge').default;
 const config = require('./webpack.config');
-const LicenseWebpackPlugin = require('license-webpack-plugin')
-  .LicenseWebpackPlugin;
+const WPPlugin = require('@jupyterlab/builder').WPPlugin;
 
 config[0] = merge(config[0], {
   mode: 'production',
@@ -15,9 +14,7 @@ config[0] = merge(config[0], {
     minimize: false
   },
   plugins: [
-    new LicenseWebpackPlugin({
-      perChunkOutput: false,
-      outputFilename: 'third-party-licenses.txt',
+    new WPPlugin.JSONLicenseWebpackPlugin({
       excludedPackageTest: packageName =>
         packageName === '@jupyterlab/application-top'
     })

+ 2 - 5
dev_mode/webpack.prod.minimize.config.js

@@ -1,7 +1,6 @@
 const TerserPlugin = require('terser-webpack-plugin');
 const merge = require('webpack-merge').default;
-const LicenseWebpackPlugin = require('license-webpack-plugin')
-  .LicenseWebpackPlugin;
+const WPPlugin = require('@jupyterlab/builder').WPPlugin;
 const config = require('./webpack.config');
 
 config[0] = merge(config[0], {
@@ -33,9 +32,7 @@ config[0] = merge(config[0], {
     ]
   },
   plugins: [
-    new LicenseWebpackPlugin({
-      perChunkOutput: false,
-      outputFilename: 'third-party-licenses.txt',
+    new WPPlugin.JSONLicenseWebpackPlugin({
       excludedPackageTest: packageName =>
         packageName === '@jupyterlab/application-top'
     })

+ 54 - 1
jupyterlab/labapp.py

@@ -14,6 +14,7 @@ from jupyter_core.application import JupyterApp, NoStart, base_aliases, base_fla
 from jupyter_server._version import version_info as jpserver_version_info
 from jupyter_server.serverapp import flags
 from jupyter_server.utils import url_path_join as ujoin
+
 from jupyterlab_server import WORKSPACE_EXTENSION, LabServerApp, slugify
 from nbclassic.shim import NBClassicConfigShimMixin
 from traitlets import Bool, Instance, Unicode, default
@@ -32,6 +33,12 @@ from .handlers.error_handler import ErrorHandler
 from .handlers.extension_manager_handler import ExtensionHandler, ExtensionManager, extensions_handler_path
 from .handlers.yjs_echo_ws import YJSEchoWS
 
+# TODO: remove when oldest compatible jupyterlab_server contains license tooling
+try:
+    from jupyterlab_server import LicensesApp
+except ImportError:
+    LicensesApp = None
+
 DEV_NOTE = """You're running JupyterLab from source.
 If you're working on the TypeScript sources of JupyterLab, try running
 
@@ -414,6 +421,46 @@ class LabWorkspaceApp(JupyterApp):
         self.exit(0)
 
 
+if LicensesApp is not None:
+    class LabLicensesApp(LicensesApp):
+        version = version
+
+        dev_mode = Bool(
+            False,
+            config=True,
+            help="""Whether to start the app in dev mode. Uses the unpublished local
+            JavaScript packages in the `dev_mode` folder.  In this case JupyterLab will
+            show a red stripe at the top of the page.  It can only be used if JupyterLab
+            is installed as `pip install -e .`.
+            """,
+        )
+
+        app_dir = Unicode(
+            "", config=True, help="The app directory for which to show licenses"
+        )
+
+        aliases = {
+            **LicensesApp.aliases,
+            "app-dir": "LabLicensesApp.app_dir",
+        }
+
+        flags = {
+            **LicensesApp.flags,
+            "dev-mode": (
+                {"LabLicensesApp": {"dev_mode": True}},
+                "Start the app in dev mode for running from source.",
+            ),
+        }
+
+        @default('app_dir')
+        def _default_app_dir(self):
+            return get_app_dir()
+
+        @default('static_dir')
+        def _default_static_dir(self):
+            return pjoin(self.app_dir, 'static')
+
+
 aliases = dict(base_aliases)
 aliases.update({
     'ip': 'ServerApp.ip',
@@ -511,6 +558,12 @@ class LabApp(NBClassicConfigShimMixin, LabServerApp):
         workspaces=(LabWorkspaceApp, LabWorkspaceApp.description.splitlines()[0])
     )
 
+    # TODO: remove when oldest compatible jupyterlab_server contains license tooling
+    if LicensesApp is not None:
+        subcommands.update(
+            licenses=(LabLicensesApp, LabLicensesApp.description.splitlines()[0])
+        )
+
     default_url = Unicode('/lab', config=True,
         help="The default URL to redirect to from `/`")
 
@@ -557,7 +610,7 @@ class LabApp(NBClassicConfigShimMixin, LabServerApp):
 
     expose_app_in_browser = Bool(False, config=True,
         help="Whether to expose the global app instance to browser via window.jupyterlab")
-    
+
     collaborative = Bool(False, config=True,
         help="Whether to enable collaborative mode.")
 

+ 2 - 1
jupyterlab/staging/package.json

@@ -1,5 +1,6 @@
 {
   "name": "@jupyterlab/application-top",
+  "license": "BSD-3-Clause",
   "version": "3.1.0-alpha.10",
   "private": true,
   "scripts": {
@@ -179,7 +180,7 @@
     "handlebars": "^4.5.3",
     "html-loader": "~1.3.0",
     "html-webpack-plugin": "^5.0.0-beta.6",
-    "license-webpack-plugin": "^2.3.11",
+    "license-webpack-plugin": "^2.3.14",
     "mini-css-extract-plugin": "~1.3.2",
     "raw-loader": "~4.0.0",
     "rimraf": "~3.0.0",

+ 1 - 8
jupyterlab/staging/webpack.prod.config.js

@@ -15,14 +15,7 @@ config[0] = merge(config[0], {
   optimization: {
     minimize: false
   },
-  plugins: [
-    new LicenseWebpackPlugin({
-      perChunkOutput: false,
-      outputFilename: 'third-party-licenses.txt',
-      excludedPackageTest: packageName =>
-        packageName === '@jupyterlab/application-top'
-    })
-  ]
+  plugins: [new LicenseWebpackPlugin()]
 });
 
 module.exports = config;

+ 1 - 8
jupyterlab/staging/webpack.prod.minimize.config.js

@@ -33,14 +33,7 @@ config[0] = merge(config[0], {
       })
     ]
   },
-  plugins: [
-    new LicenseWebpackPlugin({
-      perChunkOutput: false,
-      outputFilename: 'third-party-licenses.txt',
-      excludedPackageTest: packageName =>
-        packageName === '@jupyterlab/application-top'
-    })
-  ]
+  plugins: [new LicenseWebpackPlugin()]
 });
 
 module.exports = config;

+ 3 - 0
packages/help-extension/package.json

@@ -46,6 +46,9 @@
     "@jupyterlab/services": "^6.1.0-alpha.10",
     "@jupyterlab/translation": "^3.1.0-alpha.10",
     "@jupyterlab/ui-components": "^3.1.0-alpha.10",
+    "@lumino/coreutils": "^1.5.3",
+    "@lumino/signaling": "^1.4.3",
+    "@lumino/virtualdom": "^1.8.0",
     "@lumino/widgets": "^1.19.0",
     "react": "^17.0.1"
   },

+ 197 - 3
packages/help-extension/src/index.tsx

@@ -17,7 +17,9 @@ import {
   IFrame,
   MainAreaWidget,
   showDialog,
-  WidgetTracker
+  WidgetTracker,
+  CommandToolbarButton,
+  Toolbar
 } from '@jupyterlab/apputils';
 
 import { PageConfig, URLExt } from '@jupyterlab/coreutils';
@@ -30,11 +32,19 @@ import { KernelMessage } from '@jupyterlab/services';
 
 import { ITranslator } from '@jupyterlab/translation';
 
-import { jupyterIcon, jupyterlabWordmarkIcon } from '@jupyterlab/ui-components';
+import { Licenses } from './licenses';
+
+import {
+  jupyterIcon,
+  jupyterlabWordmarkIcon,
+  copyrightIcon,
+  refreshIcon
+} from '@jupyterlab/ui-components';
 
 import { Menu } from '@lumino/widgets';
 
 import * as React from 'react';
+import { ReadonlyJSONObject } from '@lumino/coreutils';
 
 /**
  * The command IDs used by the help plugin.
@@ -53,6 +63,12 @@ namespace CommandIDs {
   export const hide = 'help:hide';
 
   export const launchClassic = 'help:launch-classic-notebook';
+
+  export const licenses = 'help:licenses';
+
+  export const licenseReport = 'help:license-report';
+
+  export const refreshLicenses = 'help:licenses-refresh';
 }
 
 /**
@@ -448,5 +464,183 @@ const resources: JupyterFrontEndPlugin<void> = {
   }
 };
 
-const plugins: JupyterFrontEndPlugin<any>[] = [about, launchClassic, resources];
+/**
+ * A plugin to add a licenses reporting tools.
+ */
+const licenses: JupyterFrontEndPlugin<void> = {
+  id: '@jupyterlab/help-extension:licenses',
+  autoStart: true,
+  requires: [ITranslator],
+  optional: [IMainMenu, ICommandPalette, ILayoutRestorer],
+  activate: (
+    app: JupyterFrontEnd,
+    translator: ITranslator,
+    menu: IMainMenu | null,
+    palette: ICommandPalette | null,
+    restorer: ILayoutRestorer | null
+  ) => {
+    // bail if no license API is available from the server
+    if (!PageConfig.getOption('licensesUrl')) {
+      return;
+    }
+
+    const { commands, shell } = app;
+    const trans = translator.load('jupyterlab');
+
+    // translation strings
+    const category = trans.__('Help');
+    const downloadAsText = trans.__('Download All Licenses as');
+    const licensesText = trans.__('Licenses');
+    const refreshLicenses = trans.__('Refresh Licenses');
+
+    // an incrementer for license widget ids
+    let counter = 0;
+
+    const licensesUrl =
+      URLExt.join(
+        PageConfig.getBaseUrl(),
+        PageConfig.getOption('licensesUrl')
+      ) + '/';
+
+    const licensesNamespace = 'help-licenses';
+    const licensesTracker = new WidgetTracker<MainAreaWidget<Licenses>>({
+      namespace: licensesNamespace
+    });
+
+    /**
+     * Return a full report format based on a format name
+     */
+    function formatOrDefault(format: string): Licenses.IReportFormat {
+      return (
+        Licenses.REPORT_FORMATS[format] ||
+        Licenses.REPORT_FORMATS[Licenses.DEFAULT_FORMAT]
+      );
+    }
+
+    /**
+     * Create a MainAreaWidget for a toolbar item
+     */
+    function createLicenseWidget(args: Licenses.ICreateArgs) {
+      const licensesModel = new Licenses.Model({ licensesUrl, trans, ...args });
+      const content = new Licenses({ model: licensesModel });
+      content.id = `${licensesNamespace}-${++counter}`;
+      content.title.label = licensesText;
+      content.title.icon = copyrightIcon;
+      const main = new MainAreaWidget({
+        content,
+        reveal: licensesModel.licensesReady
+      });
+
+      main.toolbar.addItem(
+        'refresh-licenses',
+        new CommandToolbarButton({
+          id: CommandIDs.refreshLicenses,
+          args: { noLabel: 1 },
+          commands
+        })
+      );
+
+      main.toolbar.addItem('spacer', Toolbar.createSpacerItem());
+
+      for (const format of Object.keys(Licenses.REPORT_FORMATS)) {
+        const button = new CommandToolbarButton({
+          id: CommandIDs.licenseReport,
+          args: { format, noLabel: 1 },
+          commands
+        });
+        main.toolbar.addItem(`download-${format}`, button);
+      }
+
+      return main;
+    }
+
+    // register license-related commands
+    commands.addCommand(CommandIDs.licenses, {
+      label: licensesText,
+      execute: (args: any) => {
+        const licenseMain = createLicenseWidget(args as Licenses.ICreateArgs);
+        shell.add(licenseMain, 'main');
+
+        // add to tracker so it can be restored, and update when choices change
+        void licensesTracker.add(licenseMain);
+        licenseMain.content.model.trackerDataChanged.connect(() => {
+          void licensesTracker.save(licenseMain);
+        });
+        return licenseMain;
+      }
+    });
+
+    commands.addCommand(CommandIDs.refreshLicenses, {
+      label: args => (args.noLabel ? '' : refreshLicenses),
+      caption: refreshLicenses,
+      icon: refreshIcon,
+      execute: async () => {
+        return licensesTracker.currentWidget?.content.model.initLicenses();
+      }
+    });
+
+    commands.addCommand(CommandIDs.licenseReport, {
+      label: args => {
+        if (args.noLabel) {
+          return '';
+        }
+        const format = formatOrDefault(`${args.format}`);
+        return `${downloadAsText} ${format.title}`;
+      },
+      caption: args => {
+        const format = formatOrDefault(`${args.format}`);
+        return `${downloadAsText} ${format.title}`;
+      },
+      icon: args => {
+        const format = formatOrDefault(`${args.format}`);
+        return format.icon;
+      },
+      execute: async args => {
+        const format = formatOrDefault(`${args.format}`);
+        return await licensesTracker.currentWidget?.content.model.download({
+          format: format.id
+        });
+      }
+    });
+
+    // handle optional integrations
+    if (palette) {
+      palette.addItem({ command: CommandIDs.licenses, category });
+    }
+
+    if (menu) {
+      const helpMenu = menu.helpMenu;
+      helpMenu.addGroup([{ command: CommandIDs.licenses }], 0);
+    }
+
+    if (restorer) {
+      void restorer.restore(licensesTracker, {
+        command: CommandIDs.licenses,
+        name: widget => 'licenses',
+        args: widget => {
+          const {
+            currentBundleName,
+            currentPackageIndex,
+            packageFilter
+          } = widget.content.model;
+
+          const args: Licenses.ICreateArgs = {
+            currentBundleName,
+            currentPackageIndex,
+            packageFilter
+          };
+          return args as ReadonlyJSONObject;
+        }
+      });
+    }
+  }
+};
+
+const plugins: JupyterFrontEndPlugin<any>[] = [
+  about,
+  launchClassic,
+  resources,
+  licenses
+];
+
 export default plugins;

+ 729 - 0
packages/help-extension/src/licenses.tsx

@@ -0,0 +1,729 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import * as React from 'react';
+
+import { Panel, SplitPanel, TabBar, Widget } from '@lumino/widgets';
+import { ReadonlyJSONObject, PromiseDelegate } from '@lumino/coreutils';
+import { ISignal, Signal } from '@lumino/signaling';
+
+import { VirtualElement, h } from '@lumino/virtualdom';
+
+import { ServerConnection } from '@jupyterlab/services';
+import { TranslationBundle } from '@jupyterlab/translation';
+import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
+import {
+  spreadsheetIcon,
+  jsonIcon,
+  markdownIcon,
+  LabIcon
+} from '@jupyterlab/ui-components';
+
+/**
+ * A license viewer
+ */
+export class Licenses extends SplitPanel {
+  readonly model: Licenses.Model;
+
+  constructor(options: Licenses.IOptions) {
+    super();
+    this.addClass('jp-Licenses');
+    this.model = options.model;
+    this.initLeftPanel();
+    this.initFilters();
+    this.initBundles();
+    this.initGrid();
+    this.initLicenseText();
+    this.setRelativeSizes([1, 2, 3]);
+    void this.model.initLicenses().then(() => this._updateBundles());
+    this.model.trackerDataChanged.connect(() => {
+      this.title.label = this.model.title;
+    });
+  }
+
+  /**
+   * Handle disposing of the widget
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._bundles.currentChanged.disconnect(this.onBundleSelected, this);
+    this.model.dispose();
+    super.dispose();
+  }
+
+  /**
+   * Initialize the left area for filters and bundles
+   */
+  protected initLeftPanel(): void {
+    this._leftPanel = new Panel();
+    this._leftPanel.addClass('jp-Licenses-FormArea');
+    this.addWidget(this._leftPanel);
+    SplitPanel.setStretch(this._leftPanel, 1);
+  }
+
+  /**
+   * Initialize the filters
+   */
+  protected initFilters(): void {
+    this._filters = new Licenses.Filters(this.model);
+    SplitPanel.setStretch(this._filters, 1);
+    this._leftPanel.addWidget(this._filters);
+  }
+
+  /**
+   * Initialize the listing of available bundles
+   */
+  protected initBundles(): void {
+    this._bundles = new TabBar({
+      orientation: 'vertical',
+      renderer: new Licenses.BundleTabRenderer(this.model)
+    });
+    this._bundles.addClass('jp-Licenses-Bundles');
+    SplitPanel.setStretch(this._bundles, 1);
+    this._leftPanel.addWidget(this._bundles);
+    this._bundles.currentChanged.connect(this.onBundleSelected, this);
+    this.model.stateChanged.connect(() => this._bundles.update());
+  }
+
+  /**
+   * Initialize the listing of packages within the current bundle
+   */
+  protected initGrid(): void {
+    this._grid = new Licenses.Grid(this.model);
+    SplitPanel.setStretch(this._grid, 1);
+    this.addWidget(this._grid);
+  }
+
+  /**
+   * Initialize the full text of the current package
+   */
+  protected initLicenseText(): void {
+    this._licenseText = new Licenses.FullText(this.model);
+    SplitPanel.setStretch(this._grid, 1);
+    this.addWidget(this._licenseText);
+  }
+
+  /**
+   * Event handler for updating the model with the current bundle
+   */
+  protected onBundleSelected(): void {
+    if (this._bundles.currentTitle?.label) {
+      this.model.currentBundleName = this._bundles.currentTitle.label;
+    }
+  }
+
+  /**
+   * Update the bundle tabs.
+   */
+  protected _updateBundles(): void {
+    this._bundles.clearTabs();
+    let i = 0;
+    const { currentBundleName } = this.model;
+    let currentIndex = 0;
+    for (const bundle of this.model.bundleNames) {
+      const tab = new Widget();
+      tab.title.label = bundle;
+      if (bundle === currentBundleName) {
+        currentIndex = i;
+      }
+      this._bundles.insertTab(++i, tab.title);
+    }
+    this._bundles.currentIndex = currentIndex;
+  }
+
+  /**
+   * An area for selecting licenses by bundle and filters
+   */
+  protected _leftPanel: Panel;
+
+  /**
+   * Filters on visible licenses
+   */
+  protected _filters: Licenses.Filters;
+
+  /**
+   * Tabs reflecting available bundles
+   */
+  protected _bundles: TabBar<Widget>;
+
+  /**
+   * A grid of the current bundle's packages' license metadata
+   */
+  protected _grid: Licenses.Grid;
+
+  /**
+   * The currently-selected package's full license text
+   */
+  protected _licenseText: Licenses.FullText;
+}
+
+/** A namespace for license components */
+export namespace Licenses {
+  /** The information about a license report format  */
+  export interface IReportFormat {
+    title: string;
+    icon: LabIcon;
+    id: string;
+  }
+
+  /**
+   * License report formats understood by the server (once lower-cased)
+   */
+  export const REPORT_FORMATS: Record<string, IReportFormat> = {
+    markdown: {
+      id: 'markdown',
+      title: 'Markdown',
+      icon: markdownIcon
+    },
+    csv: {
+      id: 'csv',
+      title: 'CSV',
+      icon: spreadsheetIcon
+    },
+    json: {
+      id: 'csv',
+      title: 'JSON',
+      icon: jsonIcon
+    }
+  };
+
+  /**
+   * The default format (most human-readable)
+   */
+  export const DEFAULT_FORMAT = 'markdown';
+
+  /**
+   * Options for instantiating a license viewer
+   */
+  export interface IOptions {
+    model: Model;
+  }
+  /**
+   * Options for instantiating a license model
+   */
+  export interface IModelOptions extends ICreateArgs {
+    licensesUrl: string;
+    serverSettings?: ServerConnection.ISettings;
+    trans: TranslationBundle;
+  }
+
+  /**
+   * The JSON response from the API
+   */
+  export interface ILicenseResponse {
+    bundles: {
+      [key: string]: ILicenseBundle;
+    };
+  }
+
+  /**
+   * A top-level report of the licenses for all code included in a bundle
+   *
+   * ### Note
+   *
+   * This is roughly informed by the terms defined in the SPDX spec, though is not
+   * an SPDX Document, since there seem to be several (incompatible) specs
+   * in that repo.
+   *
+   * @see https://github.com/spdx/spdx-spec/blob/development/v2.2.1/schemas/spdx-schema.json
+   **/
+  export interface ILicenseBundle extends ReadonlyJSONObject {
+    packages: IPackageLicenseInfo[];
+  }
+
+  /**
+   * A best-effort single bundled package's information.
+   *
+   * ### Note
+   *
+   * This is roughly informed by SPDX `packages` and `hasExtractedLicenseInfos`,
+   * as making it conformant would vastly complicate the structure.
+   *
+   * @see https://github.com/spdx/spdx-spec/blob/development/v2.2.1/schemas/spdx-schema.json
+   **/
+  export interface IPackageLicenseInfo extends ReadonlyJSONObject {
+    /**
+     * the name of the package as it appears in package.json
+     */
+    name: string;
+    /**
+     * the version of the package, or an empty string if unknown
+     */
+    versionInfo: string;
+    /**
+     * an SPDX license identifier or LicenseRef, or an empty string if unknown
+     */
+    licenseId: string;
+    /**
+     * the verbatim extracted text of the license, or an empty string if unknown
+     */
+    extractedText: string;
+  }
+
+  /**
+   * The format information for a download
+   */
+  export interface IDownloadOptions {
+    format: string;
+  }
+
+  /**
+   * The fields which can be filtered
+   */
+  export type TFilterKey = 'name' | 'versionInfo' | 'licenseId';
+
+  export interface ICreateArgs {
+    currentBundleName?: string | null;
+    packageFilter?: Partial<IPackageLicenseInfo> | null;
+    currentPackageIndex?: number | null;
+  }
+
+  /**
+   * A model for license data
+   */
+  export class Model extends VDomModel implements ICreateArgs {
+    constructor(options: IModelOptions) {
+      super();
+      this._trans = options.trans;
+      this._licensesUrl = options.licensesUrl;
+      this._serverSettings =
+        options.serverSettings || ServerConnection.makeSettings();
+      if (options.currentBundleName) {
+        this._currentBundleName = options.currentBundleName;
+      }
+      if (options.packageFilter) {
+        this._packageFilter = options.packageFilter;
+      }
+      if (options.currentPackageIndex) {
+        this._currentPackageIndex = options.currentPackageIndex;
+      }
+    }
+
+    /**
+     * Handle the initial request for the licenses from the server.
+     */
+    async initLicenses(): Promise<void> {
+      try {
+        const response = await ServerConnection.makeRequest(
+          this._licensesUrl,
+          {},
+          this._serverSettings
+        );
+        this._serverResponse = await response.json();
+        this._licensesReady.resolve();
+        this.stateChanged.emit(void 0);
+      } catch (err) {
+        this._licensesReady.reject(err);
+      }
+    }
+
+    /**
+     * Create a temporary download link, and emulate clicking it to trigger a named
+     * file download.
+     */
+    async download(options: IDownloadOptions): Promise<void> {
+      const url = `${this._licensesUrl}?format=${options.format}&download=1`;
+      const element = document.createElement('a');
+      element.href = url;
+      element.download = '';
+      document.body.appendChild(element);
+      element.click();
+      document.body.removeChild(element);
+      return void 0;
+    }
+
+    /**
+     * A promise that resolves when the licenses from the server change
+     */
+    get selectedPackageChanged(): ISignal<Model, void> {
+      return this._selectedPackageChanged;
+    }
+
+    /**
+     * A promise that resolves when the trackable data changes
+     */
+    get trackerDataChanged(): ISignal<Model, void> {
+      return this._trackerDataChanged;
+    }
+
+    /**
+     * The names of the license bundles available
+     */
+    get bundleNames(): string[] {
+      return Object.keys(this._serverResponse?.bundles || {});
+    }
+
+    /**
+     * The current license bundle
+     */
+    get currentBundleName(): string | null {
+      if (this._currentBundleName) {
+        return this._currentBundleName;
+      }
+      if (this.bundleNames.length) {
+        return this.bundleNames[0];
+      }
+      return null;
+    }
+
+    /**
+     * Set the current license bundle, and reset the selected index
+     */
+    set currentBundleName(currentBundleName: string | null) {
+      if (this._currentBundleName !== currentBundleName) {
+        this._currentBundleName = currentBundleName;
+        this.stateChanged.emit(void 0);
+        this._trackerDataChanged.emit(void 0);
+      }
+    }
+
+    /**
+     * A promise that resolves when the licenses are available from the server
+     */
+    get licensesReady(): Promise<void> {
+      return this._licensesReady.promise;
+    }
+
+    /**
+     * All the license bundles, keyed by the distributing packages
+     */
+    get bundles(): null | { [key: string]: ILicenseBundle } {
+      return this._serverResponse?.bundles || {};
+    }
+
+    /**
+     * The index of the currently-selected package within its license bundle
+     */
+    get currentPackageIndex(): number | null {
+      return this._currentPackageIndex;
+    }
+
+    /**
+     * Update the currently-selected package within its license bundle
+     */
+    set currentPackageIndex(currentPackageIndex: number | null) {
+      if (this._currentPackageIndex === currentPackageIndex) {
+        return;
+      }
+      this._currentPackageIndex = currentPackageIndex;
+      this._selectedPackageChanged.emit(void 0);
+      this.stateChanged.emit(void 0);
+      this._trackerDataChanged.emit(void 0);
+    }
+
+    /**
+     * The license data for the currently-selected package
+     */
+    get currentPackage(): IPackageLicenseInfo | null {
+      if (
+        this.currentBundleName &&
+        this.bundles &&
+        this._currentPackageIndex != null
+      ) {
+        return this.getFilteredPackages(
+          this.bundles[this.currentBundleName]?.packages || []
+        )[this._currentPackageIndex];
+      }
+
+      return null;
+    }
+
+    /**
+     * A translation bundle
+     */
+    get trans(): TranslationBundle {
+      return this._trans;
+    }
+
+    get title(): string {
+      return `${this._currentBundleName || ''} ${this._trans.__(
+        'Licenses'
+      )}`.trim();
+    }
+
+    /**
+     * The current package filter
+     */
+    get packageFilter(): Partial<IPackageLicenseInfo> {
+      return this._packageFilter;
+    }
+
+    set packageFilter(packageFilter: Partial<IPackageLicenseInfo>) {
+      this._packageFilter = packageFilter;
+      this.stateChanged.emit(void 0);
+      this._trackerDataChanged.emit(void 0);
+    }
+
+    /**
+     * Get filtered packages from current bundle where at least one token of each
+     * key is present.
+     */
+    getFilteredPackages(allRows: IPackageLicenseInfo[]): IPackageLicenseInfo[] {
+      let rows: IPackageLicenseInfo[] = [];
+      let filters: [string, string[]][] = Object.entries(this._packageFilter)
+        .filter(([k, v]) => v && `${v}`.trim().length)
+        .map(([k, v]) => [k, `${v}`.toLowerCase().trim().split(' ')]);
+      for (const row of allRows) {
+        let keyHits = 0;
+        for (const [key, bits] of filters) {
+          let bitHits = 0;
+          let rowKeyValue = `${row[key]}`.toLowerCase();
+          for (const bit of bits) {
+            if (rowKeyValue.includes(bit)) {
+              bitHits += 1;
+            }
+          }
+          if (bitHits) {
+            keyHits += 1;
+          }
+        }
+        if (keyHits === filters.length) {
+          rows.push(row);
+        }
+      }
+      return Object.values(rows);
+    }
+
+    private _selectedPackageChanged: Signal<Model, void> = new Signal(this);
+    private _trackerDataChanged: Signal<Model, void> = new Signal(this);
+    private _serverResponse: ILicenseResponse | null;
+    private _licensesUrl: string;
+    private _serverSettings: ServerConnection.ISettings;
+    private _currentBundleName: string | null;
+    private _trans: TranslationBundle;
+    private _currentPackageIndex: number | null = 0;
+    private _licensesReady = new PromiseDelegate<void>();
+    private _packageFilter: Partial<IPackageLicenseInfo> = {};
+  }
+
+  /**
+   * A filter form for limiting the packages displayed
+   */
+  export class Filters extends VDomRenderer<Model> {
+    constructor(model: Model) {
+      super(model);
+      this.addClass('jp-Licenses-Filters');
+      this.addClass('jp-RenderedHTMLCommon');
+    }
+
+    protected render(): JSX.Element {
+      const { trans } = this.model;
+      return (
+        <div>
+          <label>
+            <strong>{trans.__('Filter Licenses By')}</strong>
+          </label>
+          <ul>
+            <li>
+              <label>{trans.__('Package')}</label>
+              {this.renderFilter('name')}
+            </li>
+            <li>
+              <label>{trans.__('Version')}</label>
+              {this.renderFilter('versionInfo')}
+            </li>
+            <li>
+              <label>{trans.__('License')}</label>
+              {this.renderFilter('licenseId')}
+            </li>
+          </ul>
+          <label>
+            <strong>{trans.__('Distributions')}</strong>
+          </label>
+        </div>
+      );
+    }
+
+    /**
+     * Render a filter input
+     */
+    protected renderFilter = (key: TFilterKey): JSX.Element => {
+      const value = this.model.packageFilter[key] || '';
+      return (
+        <input
+          type="text"
+          name={key}
+          defaultValue={value}
+          className="jp-mod-styled"
+          onInput={this.onFilterInput}
+        />
+      );
+    };
+
+    /**
+     * Handle a filter input changing
+     */
+    protected onFilterInput = (
+      evt: React.ChangeEvent<HTMLInputElement>
+    ): void => {
+      const input = evt.currentTarget;
+      const { name, value } = input;
+      this.model.packageFilter = { ...this.model.packageFilter, [name]: value };
+    };
+  }
+
+  /**
+   * A fancy bundle renderer with the package count
+   */
+  export class BundleTabRenderer extends TabBar.Renderer {
+    /**
+     * A model of the state of license viewing as well as the underlying data
+     */
+    model: Model;
+
+    readonly closeIconSelector = '.lm-TabBar-tabCloseIcon';
+
+    constructor(model: Model) {
+      super();
+      this.model = model;
+    }
+
+    /**
+     * Render a full bundle
+     */
+    renderTab(data: TabBar.IRenderData<Widget>): VirtualElement {
+      let title = data.title.caption;
+      let key = this.createTabKey(data);
+      let style = this.createTabStyle(data);
+      let className = this.createTabClass(data);
+      let dataset = this.createTabDataset(data);
+      return h.li(
+        { key, className, title, style, dataset },
+        this.renderIcon(data),
+        this.renderLabel(data),
+        this.renderCountBadge(data)
+      );
+    }
+
+    /**
+     * Render the package count
+     */
+    renderCountBadge(data: TabBar.IRenderData<Widget>): VirtualElement {
+      const bundle = data.title.label;
+      const { bundles } = this.model;
+      const packages = this.model.getFilteredPackages(
+        (bundles && bundle ? bundles[bundle].packages : []) || []
+      );
+      return h.label({}, `${packages.length}`);
+    }
+  }
+
+  /**
+   * A grid of licenses
+   */
+  export class Grid extends VDomRenderer<Licenses.Model> {
+    constructor(model: Licenses.Model) {
+      super(model);
+      this.addClass('jp-Licenses-Grid');
+      this.addClass('jp-RenderedHTMLCommon');
+    }
+
+    /**
+     * Render a grid of package license information
+     */
+    protected render(): JSX.Element {
+      const { bundles, currentBundleName, trans } = this.model;
+      const filteredPackages = this.model.getFilteredPackages(
+        bundles && currentBundleName
+          ? bundles[currentBundleName]?.packages || []
+          : []
+      );
+      if (!filteredPackages.length) {
+        return (
+          <blockquote>
+            <em>{trans.__('No Packages found')}</em>
+          </blockquote>
+        );
+      }
+      return (
+        <form>
+          <table>
+            <thead>
+              <tr>
+                <td></td>
+                <th>{trans.__('Package')}</th>
+                <th>{trans.__('Version')}</th>
+                <th>{trans.__('License')}</th>
+              </tr>
+            </thead>
+            <tbody>{filteredPackages.map(this.renderRow)}</tbody>
+          </table>
+        </form>
+      );
+    }
+
+    /**
+     * Render a single package's license information
+     */
+    protected renderRow = (
+      row: Licenses.IPackageLicenseInfo,
+      index: number
+    ): JSX.Element => {
+      const selected = index === this.model.currentPackageIndex;
+      const onCheck = () => (this.model.currentPackageIndex = index);
+      return (
+        <tr
+          key={row.name}
+          className={selected ? 'jp-mod-selected' : ''}
+          onClick={onCheck}
+        >
+          <td>
+            <input
+              type="radio"
+              name="show-package-license"
+              value={index}
+              onChange={onCheck}
+              checked={selected}
+            />
+          </td>
+          <th>{row.name}</th>
+          <td>
+            <code>{row.versionInfo}</code>
+          </td>
+          <td>
+            <code>{row.licenseId}</code>
+          </td>
+        </tr>
+      );
+    };
+  }
+
+  /**
+   * A package's full license text
+   */
+  export class FullText extends VDomRenderer<Model> {
+    constructor(model: Model) {
+      super(model);
+      this.addClass('jp-Licenses-Text');
+      this.addClass('jp-RenderedHTMLCommon');
+      this.addClass('jp-RenderedMarkdown');
+    }
+
+    /**
+     * Render the license text, or a null state if no package is selected
+     */
+    protected render(): JSX.Element[] {
+      const { currentPackage, trans } = this.model;
+      let head = '';
+      let quote = trans.__('No Package selected');
+      let code = '';
+      if (currentPackage) {
+        const { name, versionInfo, licenseId, extractedText } = currentPackage;
+        head = `${name} v${versionInfo}`;
+        quote = `${trans.__('License')}: ${
+          licenseId || trans.__('No License ID found')
+        }`;
+        code = extractedText || trans.__('No License Text found');
+      }
+      return [
+        <h1 key="h1">{head}</h1>,
+        <blockquote key="quote">
+          <em>{quote}</em>
+        </blockquote>,
+        <code key="code">{code}</code>
+      ];
+    }
+  }
+}

+ 170 - 1
packages/help-extension/style/base.css

@@ -6,7 +6,7 @@
 .jp-Help {
   min-width: 240px;
   min-height: 240px;
-  background: white;
+  background: var(--jp-layout-color0);
   outline: none;
 }
 
@@ -82,3 +82,172 @@
   flex-direction: column;
   margin-left: 16px;
 }
+
+/* licenses */
+.jp-Licenses {
+  display: flex;
+  flex-direction: row;
+  align-items: stretch;
+  background-color: var(--jp-layout-color0);
+}
+
+.jp-Licenses-FormArea {
+  display: flex;
+  flex-direction: column;
+  min-width: calc(10 * var(--jp-ui-font-size1));
+  width: calc(18 * var(--jp-ui-font-size1));
+}
+
+.jp-Licenses .lm-SplitPanel-handle:hover {
+  background-color: var(--jp-brand-color2);
+}
+
+/* filters */
+.jp-Licenses-Filters {
+  padding: var(--jp-ui-font-size1) calc(var(--jp-ui-font-size1) / 2) 0
+    var(--jp-ui-font-size1);
+}
+
+.jp-Licenses-Filters label {
+  display: block;
+}
+
+.jp-Licenses-Filters label strong {
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 1px;
+  font-size: var(--jp-ui-font-size0);
+}
+
+.jp-RenderedHTMLCommon.jp-Licenses-Filters ul,
+.jp-RenderedHTMLCommon.jp-Licenses-Filters li {
+  list-style: none;
+}
+
+.jp-Licenses-Filters input {
+  width: 100%;
+}
+
+.jp-RenderedHTMLCommon.jp-Licenses-Filters ul {
+  padding: 0 0 var(--jp-ui-font-size1) 0;
+  margin: 0;
+  padding-bottom: var(--jp-ui-font-size1);
+}
+
+/* bundles */
+.jp-Licenses-Bundles {
+  background-color: var(--jp-layout-color2);
+  overflow-y: auto;
+  flex: 1;
+}
+
+.jp-Licenses-Bundles .lm-TabBar-tab {
+  padding: calc(var(--jp-ui-font-size1) / 2);
+  background-color: var(--jp-layout-color1);
+  color: var(--jp-ui-font-color1);
+}
+
+.jp-Licenses-Bundles .lm-TabBar-tab label {
+  background-color: var(--jp-layout-color2);
+  border-radius: var(--jp-ui-font-size1);
+  width: calc(2.5 * var(--jp-ui-font-size1));
+  padding: 0 calc(var(--jp-ui-font-size1) / 2);
+  text-align: center;
+  margin-left: var(--jp-ui-font-size1);
+}
+
+.jp-Licenses-Bundles .lm-TabBar-tab.lm-mod-current {
+  background-color: var(--jp-brand-color1);
+  color: #fff;
+}
+
+.jp-Licenses-Bundles .lm-TabBar-tab.lm-mod-current label {
+  background-color: #fff;
+  color: var(--jp-brand-color1);
+}
+
+/* license grid */
+.jp-Licenses-Grid.jp-RenderedHTMLCommon {
+  min-width: calc(var(--jp-ui-font-size1) * 10);
+  display: flex;
+  flex-direction: column;
+  padding: 0;
+}
+
+.jp-Licenses-Grid.jp-RenderedHTMLCommon form {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow-y: scroll;
+  margin: 0;
+  padding: 0;
+}
+
+.jp-RenderedHTMLCommon.jp-Licenses-Grid table {
+  flex: 1;
+  max-width: 100%;
+  border: solid var(--jp-border-width) var(--jp-border-color2);
+  border-top: 0;
+  border-bottom: 0;
+  margin: 0;
+}
+
+.jp-Licenses-Grid.jp-RenderedHTMLCommon td,
+.jp-Licenses-Grid.jp-RenderedHTMLCommon th {
+  text-align: left;
+}
+
+.jp-Licenses-Grid table td:nth-child(1) {
+  max-width: calc(2 * var(--jp-ui-font-size1));
+}
+
+.jp-Licenses-Grid label {
+  width: 100%;
+}
+
+.jp-Licenses-Grid.jp-RenderedHTMLCommon code {
+  background-color: transparent;
+}
+
+.jp-Licenses-Grid table tr.jp-mod-selected {
+  background-color: var(--jp-brand-color1);
+  color: #fff;
+}
+
+.jp-Licenses-Grid.jp-RenderedHTMLCommon .jp-mod-selected code {
+  color: #fff;
+}
+
+/* license text */
+.jp-Licenses-Text {
+  min-width: calc(10 * var(--jp-ui-font-size1));
+  padding: 0 0 0 var(--jp-ui-font-size1);
+  display: flex;
+  flex-direction: column;
+}
+
+.jp-Licenses-Text h1 {
+  flex: initial;
+  margin-bottom: 0;
+}
+
+.jp-Licenses-Text h1:empty {
+  display: none;
+}
+
+.jp-Licenses-Text blockquote {
+  flex: initial;
+}
+
+.jp-Licenses-Text.jp-RenderedHTMLCommon code {
+  overflow-wrap: anywhere;
+  overflow-y: auto;
+  flex: 1;
+  padding-right: var(--jp-ui-font-size1);
+  margin-bottom: 0;
+  padding-bottom: var(--jp-ui-font-size1);
+}
+
+.jp-Licenses-Text code:empty {
+  display: none;
+}

+ 0 - 28
packages/translation-extension/LICENSE

@@ -1,28 +0,0 @@
-BSD 3-Clause License
-
-Copyright (c) 2020, Project Jupyter All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 0 - 28
packages/translation/LICENSE

@@ -1,28 +0,0 @@
-BSD 3-Clause License
-
-Copyright (c) 2020, Project Jupyter All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

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

@@ -27,6 +27,7 @@ import closeSvgstr from '../../style/icons/toolbar/close.svg';
 import codeSvgstr from '../../style/icons/toolbar/code.svg';
 import consoleSvgstr from '../../style/icons/filetype/console.svg';
 import copySvgstr from '../../style/icons/toolbar/copy.svg';
+import copyrightSvgstr from '../../style/icons/licenses/copyright.svg';
 import cutSvgstr from '../../style/icons/toolbar/cut.svg';
 import downloadSvgstr from '../../style/icons/toolbar/download.svg';
 import editSvgstr from '../../style/icons/toolbar/edit.svg';
@@ -105,6 +106,7 @@ export const closeIcon = new LabIcon({ name: 'ui-components:close', svgstr: clos
 export const codeIcon = new LabIcon({ name: 'ui-components:code', svgstr: codeSvgstr });
 export const consoleIcon = new LabIcon({ name: 'ui-components:console', svgstr: consoleSvgstr });
 export const copyIcon = new LabIcon({ name: 'ui-components:copy', svgstr: copySvgstr });
+export const copyrightIcon = new LabIcon({ name: 'ui-components:copyright', svgstr: copyrightSvgstr });
 export const cutIcon = new LabIcon({ name: 'ui-components:cut', svgstr: cutSvgstr });
 export const downloadIcon = new LabIcon({ name: 'ui-components:download', svgstr: downloadSvgstr });
 export const editIcon = new LabIcon({ name: 'ui-components:edit', svgstr: editSvgstr });

+ 4 - 0
packages/ui-components/style/deprecated.css

@@ -31,6 +31,7 @@
   --jp-icon-code: url('icons/toolbar/code.svg');
   --jp-icon-console: url('icons/filetype/console.svg');
   --jp-icon-copy: url('icons/toolbar/copy.svg');
+  --jp-icon-copyright: url('icons/licenses/copyright.svg');
   --jp-icon-cut: url('icons/toolbar/cut.svg');
   --jp-icon-download: url('icons/toolbar/download.svg');
   --jp-icon-edit: url('icons/toolbar/edit.svg');
@@ -149,6 +150,9 @@
 .jp-CopyIcon {
   background-image: var(--jp-icon-copy);
 }
+.jp-CopyrightIcon {
+  background-image: var(--jp-icon-copyright);
+}
 .jp-CutIcon {
   background-image: var(--jp-icon-cut);
 }

+ 5 - 0
packages/ui-components/style/icons/licenses/copyright.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24">
+  <g class="jp-icon3" fill="#616161">
+    <path d="M11.88,9.14c1.28,0.06,1.61,1.15,1.63,1.66h1.79c-0.08-1.98-1.49-3.19-3.45-3.19C9.64,7.61,8,9,8,12.14 c0,1.94,0.93,4.24,3.84,4.24c2.22,0,3.41-1.65,3.44-2.95h-1.79c-0.03,0.59-0.45,1.38-1.63,1.44C10.55,14.83,10,13.81,10,12.14 C10,9.25,11.28,9.16,11.88,9.14z M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8 s3.59-8,8-8s8,3.59,8,8S16.41,20,12,20z"/>
+  </g>
+</svg>

+ 4 - 4
yarn.lock

@@ -10430,10 +10430,10 @@ lib0@^0.2.31, lib0@^0.2.41, lib0@^0.2.42:
   dependencies:
     isomorphic.js "^0.2.4"
 
-license-webpack-plugin@^2.3.11:
-  version "2.3.11"
-  resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz"
-  integrity sha512-0iVGoX5vx0WDy8dmwTTpOOMYiGqILyUbDeVMFH52AjgBlS58lHwOlFMSoqg5nY8Kxl6+FRKyUZY/UdlQaOyqDw==
+license-webpack-plugin@^2.3.14:
+  version "2.3.19"
+  resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz#f02720b2b0bcd9ae27fb63f0bd908d9ac9335d6c"
+  integrity sha512-z/izhwFRYHs1sCrDgrTUsNJpd+Xsd06OcFWSwHz/TiZygm5ucweVZi1Hu14Rf6tOj/XAl1Ebyc7GW6ZyyINyWA==
   dependencies:
     "@types/webpack-sources" "^0.1.5"
     webpack-sources "^1.2.0"