Pārlūkot izejas kodu

Merge branch 'master' of https://github.com/jupyterlab/jupyterlab

Steven Silvester 7 gadi atpakaļ
vecāks
revīzija
680e70230d

+ 1 - 2
docs/environment.yml

@@ -5,5 +5,4 @@ dependencies:
 - python=3.5
 - sphinx>=1.6
 - sphinx_rtd_theme
-- pip:
-  - recommonmark==0.4.0
+- recommonmark

+ 175 - 56
packages/apputils-extension/src/index.ts

@@ -8,7 +8,7 @@ import {
 } from '@jupyterlab/application';
 
 import {
-  ICommandPalette, IThemeManager, ThemeManager, ISplashScreen
+  Dialog, ICommandPalette, IThemeManager, ThemeManager, ISplashScreen
 } from '@jupyterlab/apputils';
 
 import {
@@ -49,6 +49,11 @@ import '../style/index.css';
  */
 const WORKSPACE_SAVE_DEBOUNCE_INTERVAL = 2000;
 
+/**
+ * The interval in milliseconds before recover options appear during splash.
+ */
+const SPLASH_RECOVER_TIMEOUT = 12000;
+
 
 /**
  * The command IDs used by the apputils plugin.
@@ -63,6 +68,9 @@ namespace CommandIDs {
   export
   const loadState = 'apputils:load-statedb';
 
+  export
+  const recoverState = 'apputils:recover-statedb';
+
   export
   const saveState = 'apputils:save-statedb';
 }
@@ -209,7 +217,16 @@ const splash: JupyterLabPlugin<ISplashScreen> = {
   id: '@jupyterlab/apputils-extension:splash',
   autoStart: true,
   provides: ISplashScreen,
-  activate: app => ({ show: () => Private.showSplash(app.restored) })
+  activate: app => {
+    return {
+      show: () => {
+        const { commands, restored } = app;
+        const recovery = () => { commands.execute(CommandIDs.recoverState); };
+
+        return Private.showSplash(restored, recovery);
+      }
+    };
+  }
 };
 
 
@@ -243,6 +260,7 @@ const state: JupyterLabPlugin<IStateDB> = {
       // If the request that was routed did not contain a workspace,
       // leave the database intact.
       if (!resolved) {
+        resolved = true;
         transform.resolve({ type: 'cancel', contents: null });
       }
     };
@@ -253,17 +271,44 @@ const state: JupyterLabPlugin<IStateDB> = {
       execute: () => state.clear()
     });
 
+    command = CommandIDs.recoverState;
+    commands.addCommand(command, {
+      execute: () => {
+        const immediate = true;
+        const silent = true;
+
+        // Clear the state silenty so that the state changed signal listener
+        // will not be triggered as it causes a save state, but the save state
+        // promise is lost and cannot be used to reload the application.
+        return state.clear(silent)
+          .then(() => commands.execute(CommandIDs.saveState, { immediate }))
+          .then(() => { document.location.reload(); })
+          .catch(() => { document.location.reload(); });
+      }
+    });
+
+    // Hold a reference to each outstanding promise delegate in order to resolve
+    // when debouncing occurs.
+    let outstanding: PromiseDelegate<void> | null = null;
+
     command = CommandIDs.saveState;
     commands.addCommand(command, {
       label: () => `Save Workspace (${workspace})`,
       isEnabled: () => !!workspace,
-      execute: () => {
+      execute: args => {
         if (!workspace) {
           return;
         }
 
+        const timeout = args.immediate ? 0 : WORKSPACE_SAVE_DEBOUNCE_INTERVAL;
         const id = workspace;
         const metadata = { id };
+        const delegate = new PromiseDelegate<void>();
+
+        if (outstanding) {
+          outstanding.resolve(delegate.promise);
+        }
+        outstanding = delegate;
 
         if (debouncer) {
           window.clearTimeout(debouncer);
@@ -272,44 +317,61 @@ const state: JupyterLabPlugin<IStateDB> = {
         debouncer = window.setTimeout(() => {
           state.toJSON()
             .then(data => workspaces.save(id, { data, metadata }))
+            .then(() => {
+              outstanding.resolve(undefined);
+              outstanding = null;
+            })
             .catch(reason => {
-              console.warn(`Saving workspace (${id}) failed.`, reason);
+              outstanding.reject(reason);
+              outstanding = null;
             });
-        }, WORKSPACE_SAVE_DEBOUNCE_INTERVAL);
+        }, timeout);
+
+        return delegate.promise;
       }
     });
 
     command = CommandIDs.loadState;
     disposables.add(commands.addCommand(command, {
       execute: (args: IRouter.ICommandArgs) => {
-        // Irrespective of whether the workspace exists, the state database's
-        // initial data transormation resolves if this command is executed.
-        resolved = true;
-
         // Populate the workspace placeholder.
         workspace = decodeURIComponent((args.path.match(pattern)[1]));
 
+        // This command only runs once, when the page loads.
+        if (resolved) {
+          console.warn(`${command} was called after state resolution.`);
+          return;
+        }
+
         // If there is no workspace, leave the state database intact.
         if (!workspace) {
+          resolved = true;
           transform.resolve({ type: 'cancel', contents: null });
           return;
         }
 
         // Any time the local state database changes, save the workspace.
-        state.changed.connect(() => {
+        state.changed.connect((sender: any, change: StateDB.Change) => {
           commands.execute(CommandIDs.saveState);
         });
 
         // Fetch the workspace and overwrite the state database.
         return workspaces.fetch(workspace).then(session => {
-          transform.resolve({ type: 'overwrite', contents: session.data });
+          if (!resolved) {
+            resolved = true;
+            transform.resolve({ type: 'overwrite', contents: session.data });
+          }
         }).catch(reason => {
           console.warn(`Fetching workspace (${workspace}) failed.`, reason);
 
           // If the workspace does not exist, cancel the data transformation and
           // save a workspace with the current user state data.
-          transform.resolve({ type: 'cancel', contents: null });
-          commands.execute(CommandIDs.saveState);
+          if (!resolved) {
+            resolved = true;
+            transform.resolve({ type: 'cancel', contents: null });
+          }
+
+          return commands.execute(CommandIDs.saveState);
         });
       }
     }));
@@ -337,10 +399,83 @@ export default plugins;
  * The namespace for module private data.
  */
 namespace Private {
+  /**
+   * Create a splash element.
+   */
+  function createSplash(): HTMLElement {
+      const splash = document.createElement('div');
+      const galaxy = document.createElement('div');
+      const logo = document.createElement('div');
+
+      splash.id = 'jupyterlab-splash';
+      galaxy.id = 'galaxy';
+      logo.id = 'main-logo';
+
+      galaxy.appendChild(logo);
+      ['1', '2', '3'].forEach(id => {
+        const moon = document.createElement('div');
+        const planet = document.createElement('div');
+
+        moon.id = `moon${id}`;
+        moon.className = 'moon orbit';
+        planet.id = `planet${id}`;
+        planet.className = 'planet';
+
+        moon.appendChild(planet);
+        galaxy.appendChild(moon);
+      });
+
+      splash.appendChild(galaxy);
+
+      return splash;
+  }
+
+  /**
+   * A debouncer for recovery attempts.
+   */
+  let debouncer = 0;
+
+  /**
+   * The recovery dialog.
+   */
+  let dialog: Dialog<any>;
+
+  /**
+   * Allows the user to clear state if splash screen takes too long.
+   */
+  function recover(fn: () => void): void {
+    if (dialog) {
+      return;
+    }
+
+    dialog = new Dialog({
+      title: 'Loading...',
+      body: `The loading screen is taking a long time.
+        Would you like to clear the workspace or keep waiting?`,
+      buttons: [
+        Dialog.cancelButton({ label: 'Keep Waiting' }),
+        Dialog.warnButton({ label: 'Clear Workspace' })
+      ]
+    });
+
+    dialog.launch().then(result => {
+      if (result.button.accept) {
+        return fn();
+      }
+
+      dialog.dispose();
+      dialog = null;
+
+      debouncer = window.setTimeout(() => {
+        recover(fn);
+      }, SPLASH_RECOVER_TIMEOUT);
+    });
+  }
+
   /**
    * The splash element.
    */
-  let splash: HTMLElement | null;
+  const splash = createSplash();
 
   /**
    * The splash screen counter.
@@ -349,56 +484,40 @@ namespace Private {
 
   /**
    * Show the splash element.
+   *
+   * @param ready - A promise that must be resolved before splash disappears.
+   *
+   * @param recovery - A function that recovers from a hanging splash.
    */
   export
-  function showSplash(ready: Promise<any>): IDisposable {
-    if (!splash) {
-      splash = document.createElement('div');
-      splash.id = 'jupyterlab-splash';
-
-      let galaxy = document.createElement('div');
-      galaxy.id = 'galaxy';
-      splash.appendChild(galaxy);
+  function showSplash(ready: Promise<any>, recovery: () => void): IDisposable {
+    splash.classList.remove('splash-fade');
+    splashCount++;
 
-      let mainLogo = document.createElement('div');
-      mainLogo.id = 'main-logo';
-
-      let planet = document.createElement('div');
-      let planet2 = document.createElement('div');
-      let planet3 = document.createElement('div');
-      planet.className = 'planet';
-      planet2.className = 'planet';
-      planet3.className = 'planet';
-
-      let moon1 = document.createElement('div');
-      moon1.id = 'moon1';
-      moon1.className = 'moon orbit';
-      moon1.appendChild(planet);
-
-      let moon2 = document.createElement('div');
-      moon2.id = 'moon2';
-      moon2.className = 'moon orbit';
-      moon2.appendChild(planet2);
-
-      let moon3 = document.createElement('div');
-      moon3.id = 'moon3';
-      moon3.className = 'moon orbit';
-      moon3.appendChild(planet3);
-
-      galaxy.appendChild(mainLogo);
-      galaxy.appendChild(moon1);
-      galaxy.appendChild(moon2);
-      galaxy.appendChild(moon3);
+    if (debouncer) {
+      window.clearTimeout(debouncer);
     }
-    splash.classList.remove('splash-fade');
+    debouncer = window.setTimeout(() => {
+      recover(recovery);
+    }, SPLASH_RECOVER_TIMEOUT);
+
     document.body.appendChild(splash);
-    splashCount++;
+
     return new DisposableDelegate(() => {
       ready.then(() => {
-        splashCount = Math.max(splashCount - 1, 0);
-        if (splashCount === 0 && splash) {
+        if (--splashCount === 0) {
+          if (debouncer) {
+            window.clearTimeout(debouncer);
+            debouncer = 0;
+          }
+
+          if (dialog) {
+            dialog.dispose();
+            dialog = null;
+          }
+
           splash.classList.add('splash-fade');
-          setTimeout(() => { document.body.removeChild(splash); }, 500);
+          window.setTimeout(() => { document.body.removeChild(splash); }, 500);
         }
       });
     });

+ 3 - 3
packages/apputils-extension/style/splash.css

@@ -132,10 +132,10 @@ bottom: 0;
 
 
 @keyframes sk-bounce {
-  0%, 100% { 
+  0%, 100% {
     transform: scale(0.0);
     -webkit-transform: scale(0.0);
-  } 50% { 
+  } 50% {
     transform: scale(1.0);
     -webkit-transform: scale(1.0);
   }
@@ -176,4 +176,4 @@ bottom: 0;
   100% {
     opacity: 0;
   }
-}
+}

+ 1 - 1
packages/apputils/src/dialog.ts

@@ -69,7 +69,7 @@ class Dialog<T> extends Widget {
    *
    * @param options - The dialog setup options.
    */
-  constructor(options: Partial<Dialog.IOptions<T>>={}) {
+  constructor(options: Partial<Dialog.IOptions<T>> = { }) {
     super();
     this.addClass('jp-Dialog');
     let normalized = Private.handleOptions(options);

+ 4 - 2
packages/cells/src/widget.ts

@@ -370,8 +370,10 @@ class Cell extends Widget {
       return;
     }
     // Handle read only state.
-    this.editor.setOption('readOnly', this._readOnly);
-    this.toggleClass(READONLY_CLASS, this._readOnly);
+    if (this.editor.getOption('readOnly') !== this._readOnly) {
+      this.editor.setOption('readOnly', this._readOnly);
+      this.toggleClass(READONLY_CLASS, this._readOnly);
+    }
   }
 
   private _readOnly = false;

+ 15 - 4
packages/coreutils/src/statedb.ts

@@ -150,8 +150,16 @@ class StateDB implements IStateDB {
   /**
    * Clear the entire database.
    */
-  clear(): Promise<void> {
-    return this._ready.then(() => { this._clear(); });
+  clear(silent = false): Promise<void> {
+    return this._ready.then(() => {
+      this._clear();
+
+      if (silent) {
+        return;
+      }
+
+      this._changed.emit({ id: null, type: 'clear' });
+    });
   }
 
   /**
@@ -374,13 +382,16 @@ namespace StateDB {
   type Change = {
     /**
      * The key of the database item that was changed.
+     *
+     * #### Notes
+     * This field is set to `null` for global changes (i.e. `clear`).
      */
-    id: string;
+    id: string | null;
 
     /**
      * The type of change.
      */
-    type: 'remove' | 'save'
+    type: 'clear' | 'remove' | 'save'
   };
 
   /**

+ 19 - 10
packages/docregistry/src/mimedocument.ts

@@ -1,6 +1,18 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import {
+  showErrorMessage
+} from '@jupyterlab/apputils';
+
+import {
+  ActivityMonitor, PathExt
+} from '@jupyterlab/coreutils';
+
+import {
+  IRenderMime, RenderMimeRegistry, MimeModel
+} from '@jupyterlab/rendermime';
+
 import {
   JSONObject, PromiseDelegate
 } from '@phosphor/coreutils';
@@ -13,14 +25,6 @@ import {
   BoxLayout, Widget
 } from '@phosphor/widgets';
 
-import {
-  ActivityMonitor, PathExt
-} from '@jupyterlab/coreutils';
-
-import {
-  IRenderMime, RenderMimeRegistry, MimeModel
-} from '@jupyterlab/rendermime';
-
 import {
   ABCWidgetFactory
 } from './default';
@@ -67,7 +71,7 @@ class MimeDocument extends Widget implements DocumentRegistry.IReadyWidget {
       if (this.isDisposed) {
         return;
       }
-      return this._render().then();
+      return this._render();
     }).then(() => {
       // Throttle the rendering rate of the widget.
       this._monitor = new ActivityMonitor({
@@ -164,8 +168,13 @@ class MimeDocument extends Widget implements DocumentRegistry.IReadyWidget {
       this._hasRendered = true;
       this._isRendering = false;
       if (this._renderRequested) {
-        this._render();
+        return this._render();
       }
+    }).catch(reason => {
+      // Dispose the document if rendering fails.
+      requestAnimationFrame(() => { this.dispose(); });
+
+      showErrorMessage(`Renderer Failure: ${context.path}`, reason);
     });
   }
 

+ 1 - 1
packages/filebrowser-extension/src/index.ts

@@ -441,7 +441,7 @@ function createContextMenu(model: Contents.IModel, commands: CommandRegistry, re
     if (path && factories.length > 1) {
       const command =  'docmanager:open';
       const openWith = new Menu({ commands });
-      openWith.title.label = 'Open With...';
+      openWith.title.label = 'Open With';
       factories.forEach(factory => {
         openWith.addItem({ args: { factory, path }, command });
       });

+ 4 - 0
packages/vega2-extension/src/index.ts

@@ -103,6 +103,10 @@ class RenderedVega extends Widget implements IRenderMime.IRenderer {
     return Private.ensureMod().then(embedFunc => {
       return new Promise<void>((resolve, reject) => {
         embedFunc(this.node, embedSpec, (error: any, result: any): any => {
+          if (error) {
+            return; /*reject(error);*/
+          }
+
           // Save png data in MIME bundle along with original MIME data.
           if (!model.data['image/png']) {
             let imageData = result.view.toImageURL().split(',')[1] as JSONValue;