Explorar o código

Tree handler (#3396)

* Use absolute paths for themes.

* Add scaffolding for a URL router plugin.

* Clean up.

* Create a router with instantiation options.

* Initial routing implementation.

* Update router plugin.

* Pass command arguments to matching routes.

* Initial implementation of tree routing command.

* Support passing paths in at the command line.

* Add main file browser navigate command.

* Update router navigate command.

* Update the settings service to account for a dynamic page URL.

* Fix navigate command.

* Clean up file browser commands.

* Fix URL handling and add URL command for tree.

* Add sharing functionality.

* Add link icons.

* Add settings editor to command palette.

* Fix extra slash in path.

* Synchronously create URL for sharing.

* Add and use PageConfig.getTreeUrl()

* Add a custom not found handler for JupyterLab.

* Fix doc string.
Afshin Darian %!s(int64=7) %!d(string=hai) anos
pai
achega
658f0e7a21

+ 15 - 15
docs/extensions_dev.md

@@ -84,7 +84,7 @@ jupyter labextension install  # install the current directory as an extension
 ```
 
 This causes the builder to re-install the source folder before building
-the application files.  You can re-build at any time using `jupyter lab build` and it will reinstall these packages.  You can also link other local npm packages that you are working on simultaneously using 
+the application files.  You can re-build at any time using `jupyter lab build` and it will reinstall these packages.  You can also link other local npm packages that you are working on simultaneously using
 `jupyter labextension link`; they will be re-installed but not
 considered as extensions.  Local extensions and linked packages are included in `jupyter labextension list`.
 
@@ -95,8 +95,8 @@ jupyter lab --watch
 ```
 
 This will cause the application to incrementally rebuild when one of the
-linked packages changes.  Note that only compiled JavaScript files (and the 
-CSS files) are watched by the WebPack process.  
+linked packages changes.  Note that only compiled JavaScript files (and the
+CSS files) are watched by the WebPack process.
 
 Note that the application is built against **released** versions of the
 core JupyterLab extensions.  If your extension depends on JupyterLab
@@ -167,10 +167,10 @@ key in its `package.json` with `"mimeExtension"` metadata.  The value can be
 module (e.g. `"lib/foo"`).
 
 The mime renderer can update its data by calling `.setData()` on the model
-it is given to render.  This can be used for example to add a `png` 
-representation of a dynamic figure, which will be picked up by a notebook 
-model and added to the notebook document.  
-When using `IDocumentWidgetFactoryOptions`, you can update the document model by calling `.setData()` with updated data for the rendered MIME type.  The 
+it is given to render.  This can be used for example to add a `png`
+representation of a dynamic figure, which will be picked up by a notebook
+model and added to the notebook document.
+When using `IDocumentWidgetFactoryOptions`, you can update the document model by calling `.setData()` with updated data for the rendered MIME type.  The
 document can then be saved by the user in the usual manner.
 
 
@@ -178,8 +178,8 @@ document can then be saved by the user in the usual manner.
 A theme is a JupyterLab extension that uses a `ThemeManager` and can be
 loaded and unloaded dynamically.  The package must include all static assets
 that are referenced by `url()` in its CSS files.  Local URLs can be used
-to reference files relative to the location of the referring CSS file in the theme directory.  For example `url('images/foo.png')` or 
-`url('../foo/bar.css')`can be used to refer local files in the theme.  
+to reference files relative to the location of the referring CSS file in the theme directory.  For example `url('images/foo.png')` or
+`url('../foo/bar.css')`can be used to refer local files in the theme.
 Absolute URLs (starting with a `/`) or external URLs (e.g. `https:`) can be used to refer to external assets.
 The path to the theme  assets is specified `package.json` under the
 `"jupyterlab"` key as `"themeDir"`. See the [JupyterLab Light Theme](https://github.com/jupyterlab/jupyterlab/tree/master/packages/theme-light-extension)
@@ -187,14 +187,14 @@ for an example.  Ensure that the theme files are included in the
 `"files"` metadata in package.json.  A theme can optionally specify
 an `embed.css` file that can be consumed outside of a JupyterLab application.
 
-To quickly create a theme based on the JupyterLab Light Theme, follow the 
-instructions in the [contributing guide](https://github.com/jupyterlab/jupyterlab/blob/master/CONTRIBUTING.md#setting-up-a-development-environment) 
-and then run `jlpm run create:theme` from the repository root directory.  
-Once you select a name, title and a description, a new theme folder will be 
-created  in the current directory.  You can move that new folder to a location 
+To quickly create a theme based on the JupyterLab Light Theme, follow the
+instructions in the [contributing guide](https://github.com/jupyterlab/jupyterlab/blob/master/CONTRIBUTING.md#setting-up-a-development-environment)
+and then run `jlpm run create:theme` from the repository root directory.
+Once you select a name, title and a description, a new theme folder will be
+created  in the current directory.  You can move that new folder to a location
 of your choice, and start making desired changes.
 
-The theme extension is installed the same as a regular extension (see 
+The theme extension is installed the same as a regular extension (see
 [extension authoring](#Extension Authoring)).
 
 ## Standard (General-Purpose) Extensions

+ 7 - 1
jupyterlab/extension.py

@@ -30,7 +30,7 @@ def load_jupyter_server_extension(nbapp):
     """
     # Delay imports to speed up jlpmapp
     from jupyterlab_launcher import add_handlers, LabConfig
-    from notebook.utils import url_path_join as ujoin
+    from notebook.utils import url_path_join as ujoin, url_escape
     from tornado.ioloop import IOLoop
     from .build_handler import build_path, Builder, BuildHandler
     from .commands import (
@@ -82,6 +82,12 @@ def load_jupyter_server_extension(nbapp):
     page_config['token'] = nbapp.token
     page_config['devMode'] = dev_mode
 
+    if nbapp.file_to_run:
+        relpath = os.path.relpath(nbapp.file_to_run, nbapp.notebook_dir)
+        uri = url_escape(ujoin('/lab/tree', *relpath.split(os.sep)))
+        nbapp.default_url = uri
+        nbapp.file_to_run = ''
+
     if core_mode:
         app_dir = HERE
         logger.info(CORE_NOTE.strip())

+ 82 - 4
packages/application-extension/src/index.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  JupyterLab, JupyterLabPlugin, ILayoutRestorer, LayoutRestorer
+  JupyterLab, JupyterLabPlugin, ILayoutRestorer, IRouter, LayoutRestorer, Router
 } from '@jupyterlab/application';
 
 import {
@@ -10,7 +10,7 @@ import {
 } from '@jupyterlab/apputils';
 
 import {
-  IStateDB
+  IStateDB, PageConfig, URLExt
 } from '@jupyterlab/coreutils';
 
 import {
@@ -42,6 +42,12 @@ namespace CommandIDs {
 
   export
   const toggleRightArea: string = 'application:toggle-right-area';
+
+  export
+  const tree: string = 'router:tree';
+
+  export
+  const url: string = 'router:tree-url';
 }
 
 
@@ -136,13 +142,15 @@ const layout: JupyterLabPlugin<ILayoutRestorer> = {
   activate: (app: JupyterLab, state: IStateDB) => {
     const first = app.started;
     const registry = app.commands;
-    let restorer = new LayoutRestorer({ first, registry, state });
+    const restorer = new LayoutRestorer({ first, registry, state });
+
     restorer.fetch().then(saved => {
       app.shell.restoreLayout(saved);
       app.shell.layoutModified.connect(() => {
         restorer.save(app.shell.saveLayout());
       });
     });
+
     return restorer;
   },
   autoStart: true,
@@ -150,12 +158,82 @@ const layout: JupyterLabPlugin<ILayoutRestorer> = {
 };
 
 
+/**
+ * The default URL router provider.
+ */
+const router: JupyterLabPlugin<IRouter> = {
+  id: '@jupyterlab/application-extension:router',
+  activate: (app: JupyterLab) => {
+    const { commands } = app;
+    const base = URLExt.join(
+      PageConfig.getBaseUrl(),
+      PageConfig.getOption('pageUrl')
+    );
+    const tree = PageConfig.getTreeUrl();
+    const router = new Router({ base, commands });
+
+    commands.addCommand(CommandIDs.tree, {
+      execute: (args: IRouter.ICommandArgs) => {
+        const path = (args.path as string).replace('/tree', '');
+
+        // Change the URL back to the base application URL.
+        window.history.replaceState({ }, '', base);
+
+        return commands.execute('filebrowser:navigate-main', { path });
+      }
+    });
+
+    commands.addCommand(CommandIDs.url, {
+      execute: args => URLExt.join(tree, (args.path as string))
+    });
+
+    app.restored.then(() => { router.route(window.location.href); });
+    router.register(/^\/tree\/.+/, CommandIDs.tree);
+
+    return router;
+  },
+  autoStart: true,
+  provides: IRouter
+};
+
+
+/**
+ * The default URL not found extension.
+ */
+const notfound: JupyterLabPlugin<void> = {
+  id: '@jupyterlab/application-extension:notfound',
+  activate: (app: JupyterLab) => {
+    const bad = PageConfig.getOption('notFoundUrl');
+
+    if (!bad) {
+      return;
+    }
+
+    const base = URLExt.join(
+      PageConfig.getBaseUrl(),
+      PageConfig.getOption('pageUrl')
+    );
+
+    // Change the URL back to the base application URL.
+    window.history.replaceState({ }, '', base);
+
+    showDialog({
+      title: 'Path Not Found',
+      body: `The path: ${bad} was not found. JupyterLab redirected to: ${base}`,
+      buttons: [Dialog.okButton()]
+    });
+  },
+  autoStart: true
+};
+
+
 /**
  * Add the main application commands.
  */
 function addCommands(app: JupyterLab, palette: ICommandPalette): void {
   const category = 'Main Area';
   let command = CommandIDs.activateNextTab;
+
   app.commands.addCommand(command, {
     label: 'Activate Next Tab',
     execute: () => { app.shell.activateNextTab(); }
@@ -240,6 +318,6 @@ function addCommands(app: JupyterLab, palette: ICommandPalette): void {
 /**
  * Export the plugins as default.
  */
-const plugins: JupyterLabPlugin<any>[] = [main, layout];
+const plugins: JupyterLabPlugin<any>[] = [main, layout, router, notfound];
 
 export default plugins;

+ 1 - 0
packages/application/src/index.ts

@@ -38,6 +38,7 @@ import {
 
 export { ApplicationShell } from './shell';
 export { ILayoutRestorer, LayoutRestorer } from './layoutrestorer';
+export { IRouter, Router } from './router';
 
 
 /**

+ 8 - 8
packages/application/src/layoutrestorer.ts

@@ -3,6 +3,14 @@
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
+import {
+  InstanceTracker
+} from '@jupyterlab/apputils';
+
+import {
+  IStateDB
+} from '@jupyterlab/coreutils';
+
 import {
   CommandRegistry
 } from '@phosphor/commands';
@@ -19,14 +27,6 @@ import {
   DockPanel, Widget
 } from '@phosphor/widgets';
 
-import {
-  InstanceTracker
-} from '@jupyterlab/apputils';
-
-import {
-  IStateDB
-} from '@jupyterlab/coreutils';
-
 import {
   ApplicationShell
 } from './shell';

+ 182 - 0
packages/application/src/router.ts

@@ -0,0 +1,182 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import {
+  URLExt
+} from '@jupyterlab/coreutils';
+
+import {
+  CommandRegistry
+} from '@phosphor/commands';
+
+import {
+  ReadonlyJSONObject, Token
+} from '@phosphor/coreutils';
+
+import {
+  IDisposable, DisposableDelegate
+} from '@phosphor/disposable';
+
+
+/* tslint:disable */
+/**
+ * The URL Router token.
+ */
+export
+const IRouter = new Token<IRouter>('@jupyterlab/application:IRouter');
+/* tslint:enable */
+
+
+/**
+ * A static class that routes URLs within the application.
+ */
+export
+interface IRouter {
+  /**
+   * The base URL for the router.
+   */
+  readonly base: string;
+
+  /**
+   * The command registry used by the router.
+   */
+  readonly commands: CommandRegistry;
+
+  /**
+   * Register to route a path pattern to a command.
+   *
+   * @param pattern - The regular expression that will be matched against URLs.
+   *
+   * @param command - The command string that will be invoked upon matching.
+   *
+   * @returns A disposable that removes the registered rul from the router.
+   */
+  register(pattern: RegExp, command: string): IDisposable;
+
+  /**
+   * Route a specific path to an action.
+   *
+   * @param url - The URL string that will be routed.
+   *
+   * #### Notes
+   * If a pattern is matched, its command will be invoked with arguments that
+   * match the `IRouter.ICommandArgs` interface.
+   */
+  route(url: string): void;
+}
+
+
+/**
+ * A namespace for the `IRouter` specification.
+ */
+export
+namespace IRouter {
+  /**
+   * The arguments passed into a command execution when a path is routed.
+   */
+  export
+  interface ICommandArgs extends ReadonlyJSONObject {
+    /**
+     * The path that matched a routing pattern.
+     */
+    path: string;
+
+    /**
+     * The search element, including leading question mark (`'?'`), if any,
+     * of the path.
+     */
+    search: string;
+  }
+}
+
+
+/**
+ * A static class that routes URLs within the application.
+ */
+export
+class Router implements IRouter {
+  /**
+   * Create a URL router.
+   */
+  constructor(options: Router.IOptions) {
+    this.base = options.base;
+    this.commands = options.commands;
+  }
+
+  /**
+   * The base URL for the router.
+   */
+  readonly base: string;
+
+  /**
+   * The command registry used by the router.
+   */
+  readonly commands: CommandRegistry;
+
+  /**
+   * Register to route a path pattern to a command.
+   *
+   * @param pattern - The regular expression that will be matched against URLs.
+   *
+   * @param command - The command string that will be invoked upon matching.
+   *
+   * @returns A disposable that removes the registered rul from the router.
+   */
+  register(pattern: RegExp, command: string): IDisposable {
+    const rules = this._rules;
+
+    rules.set(pattern, command);
+
+    return new DisposableDelegate(() => { rules.delete(pattern); });
+  }
+
+  /**
+   * Route a specific path to an action.
+   *
+   * @param url - The URL string that will be routed.
+   *
+   * #### Notes
+   * If a pattern is matched, its command will be invoked with arguments that
+   * match the `IRouter.ICommandArgs` interface.
+   */
+  route(url: string): void {
+    const { base } = this;
+    const parsed = URLExt.parse(url.replace(base, ''));
+    const path = parsed.pathname;
+    const search = parsed.search;
+    const rules = this._rules;
+
+    rules.forEach((command, pattern) => {
+      if (path.match(pattern)) {
+        this.commands.execute(command, { path, search });
+      }
+    });
+  }
+
+  private _rules = new Map<RegExp, string>();
+}
+
+
+/**
+ * A namespace for `Router` class statics.
+ */
+export
+namespace Router {
+  /**
+   * The options for instantiating a JupyterLab URL router.
+   */
+  export
+  interface IOptions {
+    /**
+     * The fully qualified base URL for the router.
+     */
+    base: string;
+
+    /**
+     * The command registry used by the router.
+     */
+    commands: CommandRegistry;
+  }
+}

+ 5 - 0
packages/application/style/icons.css

@@ -138,6 +138,11 @@
 }
 
 
+.jp-LinkIcon {
+  background-image: var(--jp-icon-link);
+}
+
+
 .jp-MainAreaLandscapeIcon {
   background-repeat: no-repeat;
   margin-right: 2px;

+ 8 - 0
packages/coreutils/src/pageconfig.ts

@@ -120,6 +120,14 @@ namespace PageConfig {
     return URLExt.parse(baseUrl).toString();
   }
 
+  /**
+   * Get the tree url for a JupyterLab application.
+   */
+  export
+  function getTreeUrl(): string {
+    return URLExt.join(getBaseUrl(), getOption('pageUrl'), 'tree');
+  }
+
   /**
    * Get the base websocket url for a Jupyter application.
    */

+ 92 - 47
packages/filebrowser-extension/src/index.ts

@@ -6,11 +6,11 @@ import {
 } from '@jupyterlab/application';
 
 import {
-  InstanceTracker, ToolbarButton
+  Clipboard, InstanceTracker, ToolbarButton
 } from '@jupyterlab/apputils';
 
 import {
-  IStateDB
+  IStateDB, PageConfig, PathExt, URLExt
 } from '@jupyterlab/coreutils';
 
 import {
@@ -34,7 +34,7 @@ import {
 } from '@jupyterlab/services';
 
 import {
-  each
+  map, toArray
 } from '@phosphor/algorithm';
 
 import {
@@ -53,6 +53,10 @@ namespace CommandIDs {
   export
   const copy = 'filebrowser:copy';
 
+  // For main browser only.
+  export
+  const createLauncher = 'filebrowser:create-main-launcher';
+
   export
   const cut = 'filebrowser:cut';
 
@@ -65,8 +69,13 @@ namespace CommandIDs {
   export
   const duplicate = 'filebrowser:duplicate';
 
+  // For main browser only.
+  export
+  const hideBrowser = 'filebrowser:hide-main';
+
+  // For main browser only.
   export
-  const hideBrowser = 'filebrowser:hide-main'; // For main browser only.
+  const navigate = 'filebrowser:navigate-main';
 
   export
   const open = 'filebrowser:open';
@@ -77,17 +86,20 @@ namespace CommandIDs {
   export
   const rename = 'filebrowser:rename';
 
+  // For main browser only.
   export
-  const showBrowser = 'filebrowser:activate-main'; // For main browser only.
+  const share = 'filebrowser:share-main';
 
+  // For main browser only.
   export
-  const shutdown = 'filebrowser:shutdown';
+  const showBrowser = 'filebrowser:activate-main';
 
   export
-  const toggleBrowser = 'filebrowser:toggle-main'; // For main browser only.
+  const shutdown = 'filebrowser:shutdown';
 
+  // For main browser only.
   export
-  const createLauncher = 'filebrowser:create-main-launcher'; // For main browser only.
+  const toggleBrowser = 'filebrowser:toggle-main';
 }
 
 
@@ -227,11 +239,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.del, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.delete();
+      if (widget) {
+        return widget.delete();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-CloseIcon',
     label: 'Delete',
@@ -241,11 +252,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.copy, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.copy();
+      if (widget) {
+        return widget.copy();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-CopyIcon',
     label: 'Copy',
@@ -255,11 +265,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.cut, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.cut();
+      if (widget) {
+        return widget.cut();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-CutIcon',
     label: 'Cut'
@@ -268,11 +277,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.download, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.download();
+      if (widget) {
+        return widget.download();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-DownloadIcon',
     label: 'Download'
@@ -281,11 +289,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.duplicate, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.duplicate();
+      if (widget) {
+        return widget.duplicate();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-CopyIcon',
     label: 'Duplicate'
@@ -299,34 +306,59 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
     }
   });
 
+  commands.addCommand(CommandIDs.navigate, {
+    execute: args => {
+      const path = args.path as string || '';
+      const services = app.serviceManager;
+      const open = 'docmanager:open';
+      const failure = (reason: any) => {
+        console.warn(`${CommandIDs.navigate} failed to open: ${path}`, reason);
+      };
+
+      return services.ready
+        .then(() => services.contents.get(path))
+        .then(value => {
+          const { model } = browser;
+          const { restored } = model;
+
+          if (value.type === 'directory') {
+            return restored.then(() => model.cd(`/${path}`));
+          }
+
+          return restored.then(() => model.cd(`/${PathExt.dirname(path)}`))
+            .then(() => commands.execute(open, { path }));
+        }).catch(failure);
+    }
+  });
+
   commands.addCommand(CommandIDs.open, {
     execute: () => {
       const widget = tracker.currentWidget;
+
       if (!widget) {
         return;
       }
 
-      each(widget.selectedItems(), item => {
+      return Promise.all(toArray(map(widget.selectedItems(), item => {
         if (item.type === 'directory') {
-          widget.model.cd(item.path);
-        } else {
-          commands.execute('docmanager:open', { path: item.path });
+          return widget.model.cd(item.path);
         }
-      });
+
+        return commands.execute('docmanager:open', { path: item.path });
+      })));
     },
     iconClass: 'jp-MaterialIcon jp-OpenFolderIcon',
     label: 'Open',
-    mnemonic: 0,
+    mnemonic: 0
   });
 
   commands.addCommand(CommandIDs.paste, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.paste();
+      if (widget) {
+        return widget.paste();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-PasteIcon',
     label: 'Paste',
@@ -336,16 +368,28 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.rename, {
     execute: (args) => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
+
+      if (widget) {
+        return widget.rename();
       }
-      return widget.rename();
     },
     iconClass: 'jp-MaterialIcon jp-EditIcon',
     label: 'Rename',
     mnemonic: 0
   });
 
+  commands.addCommand(CommandIDs.share, {
+    execute: () => {
+      const path = browser.selectedItems().next().path;
+      const tree = PageConfig.getTreeUrl();
+
+      Clipboard.copyToSystem(URLExt.join(tree, (path as string)));
+    },
+    isVisible: () => toArray(browser.selectedItems()).length === 1,
+    iconClass: 'jp-MaterialIcon jp-LinkIcon',
+    label: 'Copy Shareable Link'
+  });
+
   commands.addCommand(CommandIDs.showBrowser, {
     execute: () => { app.shell.activateById(browser.id); }
   });
@@ -353,11 +397,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
   commands.addCommand(CommandIDs.shutdown, {
     execute: () => {
       const widget = tracker.currentWidget;
-      if (!widget) {
-        return;
-      }
 
-      return widget.shutdownKernels();
+      if (widget) {
+        return widget.shutdownKernels();
+      }
     },
     iconClass: 'jp-MaterialIcon jp-StopIcon',
     label: 'Shutdown Kernel'
@@ -367,9 +410,9 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, bro
     execute: () => {
       if (browser.isHidden) {
         return commands.execute(CommandIDs.showBrowser, void 0);
-      } else {
-        return commands.execute(CommandIDs.hideBrowser, void 0);
       }
+
+      return commands.execute(CommandIDs.hideBrowser, void 0);
     }
   });
 
@@ -422,6 +465,8 @@ function createContextMenu(model: Contents.IModel, commands: CommandRegistry, re
     menu.addItem({ command: CommandIDs.shutdown });
   }
 
+  menu.addItem({ command: CommandIDs.share });
+
   return menu;
 }
 

+ 6 - 0
packages/services/src/serverconnection.ts

@@ -62,6 +62,11 @@ namespace ServerConnection {
       */
      readonly baseUrl: string;
 
+     /**
+      * The page url of the JupyterLab application.
+      */
+     readonly pageUrl: string;
+
      /**
       * The base ws url of the server.
       */
@@ -177,6 +182,7 @@ namespace ServerConnection {
   export
   const defaultSettings: ServerConnection.ISettings = {
     baseUrl: PageConfig.getBaseUrl(),
+    pageUrl: PageConfig.getOption('pageUrl'),
     wsUrl: PageConfig.getWsUrl(),
     token: PageConfig.getToken(),
     init: { 'cache': 'no-store', 'credentials': 'same-origin' },

+ 5 - 3
packages/services/src/setting/index.ts

@@ -13,7 +13,7 @@ import {
 /**
  * The url for the lab settings service.
  */
-const SERVICE_SETTINGS_URL = 'lab/api/settings';
+const SERVICE_SETTINGS_URL = 'api/settings';
 
 
 /**
@@ -43,8 +43,9 @@ class SettingManager {
    * with a `ServerConnection.IError`.
    */
   fetch(id: string): Promise<ISettingRegistry.IPlugin> {
-    const base = this.serverSettings.baseUrl;
     const { serverSettings } = this;
+    const { baseUrl, pageUrl } = serverSettings;
+    const base = baseUrl + pageUrl;
     const url = Private.url(base, id);
     const promise = ServerConnection.makeRequest(url, {}, serverSettings);
 
@@ -68,8 +69,9 @@ class SettingManager {
    * with a `ServerConnection.IError`.
    */
   save(id: string, raw: string): Promise<void> {
-    const base = this.serverSettings.baseUrl;
     const { serverSettings } = this;
+    const { baseUrl, pageUrl } = serverSettings;
+    const base = baseUrl + pageUrl;
     const url = Private.url(base, id);
     const init = {
       body: raw,

+ 12 - 4
packages/settingeditor-extension/src/index.ts

@@ -8,7 +8,7 @@ import {
 } from '@jupyterlab/application';
 
 import {
-  InstanceTracker
+  ICommandPalette, InstanceTracker
 } from '@jupyterlab/apputils';
 
 import {
@@ -51,7 +51,14 @@ namespace CommandIDs {
  */
 const plugin: JupyterLabPlugin<ISettingEditorTracker> = {
   id: '@jupyterlab/settingeditor-extension:plugin',
-  requires: [ILayoutRestorer, ISettingRegistry, IEditorServices, IStateDB, IRenderMimeRegistry],
+  requires: [
+    ILayoutRestorer,
+    ISettingRegistry,
+    IEditorServices,
+    IStateDB,
+    IRenderMimeRegistry,
+    ICommandPalette
+  ],
   autoStart: true,
   provides: ISettingEditorTracker,
   activate
@@ -61,7 +68,7 @@ const plugin: JupyterLabPlugin<ISettingEditorTracker> = {
 /**
  * Activate the setting editor extension.
  */
-function activate(app: JupyterLab, restorer: ILayoutRestorer, registry: ISettingRegistry, editorServices: IEditorServices, state: IStateDB, rendermime: IRenderMimeRegistry): ISettingEditorTracker {
+function activate(app: JupyterLab, restorer: ILayoutRestorer, registry: ISettingRegistry, editorServices: IEditorServices, state: IStateDB, rendermime: IRenderMimeRegistry, palette: ICommandPalette): ISettingEditorTracker {
   const { commands, shell } = app;
   const namespace = 'setting-editor';
   const factoryService = editorServices.factoryService;
@@ -118,8 +125,9 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, registry: ISetting
       shell.addToMainArea(editor);
       shell.activateById(editor.id);
     },
-    label: 'Advanced Settings'
+    label: 'Advanced Settings Editor'
   });
+  palette.addItem({ category: 'Settings', command: CommandIDs.open });
 
   commands.addCommand(CommandIDs.revert, {
     execute: () => { tracker.currentWidget.revert(); },

+ 4 - 0
packages/theme-dark-extension/style/icons/md/link.svg

@@ -0,0 +1,4 @@
+<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
+</svg>

+ 2 - 0
packages/theme-dark-extension/style/urls.css

@@ -9,6 +9,7 @@
 
   These images are included with JupyterLab for showing the Project Jupyter logo
   */
+
   --jp-image-jupyter: url('images/jupyter.svg');
   --jp-image-jupyterlab: url('images/jupyterlab.svg');
   --jp-image-jupyterlab-workmark: url('images/jupyterlab-wordmark.svg');
@@ -74,6 +75,7 @@
   --jp-icon-home: url('icons/md/home.svg');
   --jp-icon-jupyter: url('icons/jupyter/jupyter.svg');
   --jp-icon-launcher: url('icons/jupyter/launcher.svg');
+  --jp-icon-link: url('icons/md/link.svg');
   --jp-icon-more: url('icons/md/more-horiz.svg');
   --jp-icon-run: url('icons/md/run.svg');
   --jp-icon-save: url('icons/md/save.svg');

+ 4 - 0
packages/theme-light-extension/style/icons/md/link.svg

@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
+</svg>

+ 1 - 0
packages/theme-light-extension/style/urls.css

@@ -75,6 +75,7 @@
   --jp-icon-home: url('icons/md/home.svg');
   --jp-icon-jupyter: url('icons/jupyter/jupyter.svg');
   --jp-icon-launcher: url('icons/jupyter/launcher.svg');
+  --jp-icon-link: url('icons/md/link.svg');
   --jp-icon-more: url('icons/md/more-horiz.svg');
   --jp-icon-run: url('icons/md/run.svg');
   --jp-icon-save: url('icons/md/save.svg');