123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663 |
- /*-----------------------------------------------------------------------------
- | Copyright (c) Jupyter Development Team.
- | Distributed under the terms of the Modified BSD License.
- |----------------------------------------------------------------------------*/
- import {
- ILayoutRestorer, IRouter, JupyterLab, JupyterLabPlugin
- } from '@jupyterlab/application';
- import {
- Dialog, ICommandPalette, ISplashScreen, IThemeManager, ThemeManager
- } from '@jupyterlab/apputils';
- import {
- DataConnector, ISettingRegistry, IStateDB, IWindowResolver, SettingRegistry,
- StateDB, URLExt, WindowResolver
- } from '@jupyterlab/coreutils';
- import {
- IMainMenu
- } from '@jupyterlab/mainmenu';
- import {
- ServiceManager
- } from '@jupyterlab/services';
- import {
- PromiseDelegate
- } from '@phosphor/coreutils';
- import {
- DisposableDelegate, IDisposable
- } from '@phosphor/disposable';
- import {
- Menu
- } from '@phosphor/widgets';
- import {
- activatePalette, restorePalette
- } from './palette';
- import {
- createRedirectForm
- } from './redirect';
- import '../style/index.css';
- /**
- * The interval in milliseconds that calls to save a workspace are debounced
- * to allow for multiple quickly executed state changes to result in a single
- * workspace save operation.
- */
- const WORKSPACE_SAVE_DEBOUNCE_INTERVAL = 1500;
- /**
- * The interval in milliseconds before recover options appear during splash.
- */
- const SPLASH_RECOVER_TIMEOUT = 12000;
- /**
- * The command IDs used by the apputils plugin.
- */
- namespace CommandIDs {
- export
- const changeTheme = 'apputils:change-theme';
- export
- const loadState = 'apputils:load-statedb';
- export
- const recoverState = 'apputils:recover-statedb';
- export
- const reset = 'apputils:reset';
- export
- const resetOnLoad = 'apputils:reset-on-load';
- export
- const saveState = 'apputils:save-statedb';
- }
- /**
- * The routing regular expressions used by the apputils plugin.
- */
- namespace Patterns {
- export
- const loadState = /^\/workspaces\/([^?]+)/;
- export
- const resetOnLoad = /(\?reset|\&reset)($|&)/;
- }
- /**
- * A data connector to access plugin settings.
- */
- class SettingsConnector extends DataConnector<ISettingRegistry.IPlugin, string> {
- /**
- * Create a new settings connector.
- */
- constructor(manager: ServiceManager) {
- super();
- this._manager = manager;
- }
- /**
- * Retrieve a saved bundle from the data connector.
- */
- fetch(id: string): Promise<ISettingRegistry.IPlugin> {
- return this._manager.settings.fetch(id).then(data => {
- // Replace the server ID with the original unmodified version.
- data.id = id;
- return data;
- });
- }
- /**
- * Save the user setting data in the data connector.
- */
- save(id: string, raw: string): Promise<void> {
- return this._manager.settings.save(id, raw);
- }
- private _manager: ServiceManager;
- }
- /**
- * The default commmand palette extension.
- */
- const palette: JupyterLabPlugin<ICommandPalette> = {
- activate: activatePalette,
- id: '@jupyterlab/apputils-extension:palette',
- provides: ICommandPalette,
- autoStart: true
- };
- /**
- * The default commmand palette's restoration extension.
- *
- * #### Notes
- * The command palette's restoration logic is handled separately from the
- * command palette provider extension because the layout restorer dependency
- * causes the command palette to be unavailable to other extensions earlier
- * in the application load cycle.
- */
- const paletteRestorer: JupyterLabPlugin<void> = {
- activate: restorePalette,
- id: '@jupyterlab/apputils-extension:palette-restorer',
- requires: [ILayoutRestorer],
- autoStart: true
- };
- /**
- * The default setting registry provider.
- */
- const settings: JupyterLabPlugin<ISettingRegistry> = {
- id: '@jupyterlab/apputils-extension:settings',
- activate: (app: JupyterLab): ISettingRegistry => {
- const connector = new SettingsConnector(app.serviceManager);
- return new SettingRegistry({ connector });
- },
- autoStart: true,
- provides: ISettingRegistry
- };
- /**
- * The default theme manager provider.
- */
- const themes: JupyterLabPlugin<IThemeManager> = {
- id: '@jupyterlab/apputils-extension:themes',
- requires: [ISettingRegistry, ISplashScreen],
- optional: [ICommandPalette, IMainMenu],
- activate: (app: JupyterLab, settings: ISettingRegistry, splash: ISplashScreen, palette: ICommandPalette | null, mainMenu: IMainMenu | null): IThemeManager => {
- const host = app.shell;
- const commands = app.commands;
- const url = app.info.urls.themes;
- const key = themes.id;
- const manager = new ThemeManager({ key, host, settings, splash, url });
- // Keep a synchronously set reference to the current theme,
- // since the asynchronous setting of the theme in `changeTheme`
- // can lead to an incorrect toggle on the currently used theme.
- let currentTheme: string;
- commands.addCommand(CommandIDs.changeTheme, {
- label: args => {
- const theme = args['theme'] as string;
- return args['isPalette'] ? `Use ${theme} Theme` : theme;
- },
- isToggled: args => args['theme'] === currentTheme,
- execute: args => {
- const theme = args['theme'] as string;
- if (theme === manager.theme) {
- return;
- }
- currentTheme = theme;
- manager.setTheme(theme);
- commands.notifyCommandChanged(CommandIDs.changeTheme);
- }
- });
- // If we have a main menu, add the theme manager to the settings menu.
- if (mainMenu) {
- const themeMenu = new Menu({ commands });
- themeMenu.title.label = 'JupyterLab Theme';
- app.restored.then(() => {
- const command = CommandIDs.changeTheme;
- const isPalette = false;
- manager.themes.forEach(theme => {
- themeMenu.addItem({ command, args: { isPalette, theme } });
- });
- });
- mainMenu.settingsMenu.addGroup([{
- type: 'submenu' as Menu.ItemType, submenu: themeMenu
- }], 0);
- }
- // If we have a command palette, add theme switching options to it.
- if (palette) {
- app.restored.then(() => {
- const category = 'Settings';
- const command = CommandIDs.changeTheme;
- const isPalette = true;
- currentTheme = manager.theme;
- manager.themes.forEach(theme => {
- palette.addItem({ command, args: { isPalette, theme }, category });
- });
- });
- }
- return manager;
- },
- autoStart: true,
- provides: IThemeManager
- };
- /**
- * The default window name resolver provider.
- */
- const resolver: JupyterLabPlugin<IWindowResolver> = {
- id: '@jupyterlab/apputils-extension:resolver',
- autoStart: true,
- provides: IWindowResolver,
- requires: [IRouter],
- activate: (app: JupyterLab, router: IRouter) => {
- const candidate = Private.getWorkspace(router) || '';
- const resolver = new WindowResolver();
- return resolver.resolve(candidate)
- .catch(reason => {
- console.log('Window resolution failed.', reason);
- return Private.redirect(router);
- })
- .then(() => resolver);
- }
- };
- /**
- * The default splash screen provider.
- */
- const splash: JupyterLabPlugin<ISplashScreen> = {
- id: '@jupyterlab/apputils-extension:splash',
- autoStart: true,
- provides: ISplashScreen,
- activate: app => {
- return {
- show: () => {
- const { commands, restored } = app;
- const recovery = () => { commands.execute(CommandIDs.reset); };
- // If the reset command is available, show the recovery UI.
- if (commands.hasCommand(CommandIDs.reset)) {
- return Private.showSplash(restored, recovery);
- }
- // If the reset command is unavailable, showing the UI is superfluous.
- return new DisposableDelegate(() => { /* no-op */ });
- }
- };
- }
- };
- /**
- * The default state database for storing application state.
- */
- const state: JupyterLabPlugin<IStateDB> = {
- id: '@jupyterlab/apputils-extension:state',
- autoStart: true,
- provides: IStateDB,
- requires: [IRouter, IWindowResolver],
- activate: (app: JupyterLab, router: IRouter, resolver: IWindowResolver) => {
- let debouncer: number;
- let resolved = false;
- const { commands, info, serviceManager } = app;
- const { workspaces } = serviceManager;
- const transform = new PromiseDelegate<StateDB.DataTransform>();
- const state = new StateDB({
- namespace: info.namespace,
- transform: transform.promise,
- windowName: resolver.name
- });
- commands.addCommand(CommandIDs.recoverState, {
- execute: () => {
- const immediate = true;
- const silent = true;
- // Clear the state silently so that the state changed signal listener
- // will not be triggered as it causes a save state.
- return state.clear(silent)
- .then(() => commands.execute(CommandIDs.saveState, { immediate }));
- }
- });
- // Conflate all outstanding requests to the save state command that happen
- // within the `WORKSPACE_SAVE_DEBOUNCE_INTERVAL` into a single promise.
- let conflated: PromiseDelegate<void> | null = null;
- commands.addCommand(CommandIDs.saveState, {
- label: () => `Save Workspace (${Private.getWorkspace(router)})`,
- isEnabled: () => !!Private.getWorkspace(router),
- execute: args => {
- const workspace = Private.getWorkspace(router);
- if (!workspace) {
- return;
- }
- const timeout = args.immediate ? 0 : WORKSPACE_SAVE_DEBOUNCE_INTERVAL;
- const id = workspace;
- const metadata = { id };
- // Only instantiate a new conflated promise if one is not outstanding.
- if (!conflated) {
- conflated = new PromiseDelegate<void>();
- }
- if (debouncer) {
- window.clearTimeout(debouncer);
- }
- debouncer = window.setTimeout(() => {
- state.toJSON()
- .then(data => workspaces.save(id, { data, metadata }))
- .then(() => {
- conflated.resolve(undefined);
- conflated = null;
- })
- .catch(reason => {
- conflated.reject(reason);
- conflated = null;
- });
- }, timeout);
- return conflated.promise;
- }
- });
- const listener = (sender: any, change: StateDB.Change) => {
- commands.execute(CommandIDs.saveState);
- };
- commands.addCommand(CommandIDs.loadState, {
- execute: (args: IRouter.ILocation) => {
- const workspace = Private.getWorkspace(router);
- // If there is no workspace, bail.
- if (!workspace) {
- return;
- }
- // Any time the local state database changes, save the workspace.
- state.changed.connect(listener, state);
- // Fetch the workspace and overwrite the state database.
- return workspaces.fetch(workspace).then(session => {
- // If this command is called after a reset, the state database will
- // already be resolved.
- 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.
- if (!resolved) {
- resolved = true;
- transform.resolve({ type: 'cancel', contents: null });
- }
- return commands.execute(CommandIDs.saveState);
- });
- }
- });
- router.register({
- command: CommandIDs.loadState,
- pattern: Patterns.loadState
- });
- commands.addCommand(CommandIDs.reset, {
- label: 'Reset Application State',
- execute: () => {
- commands.execute(CommandIDs.recoverState)
- .then(() => { router.reload(); })
- .catch(() => { router.reload(); });
- }
- });
- commands.addCommand(CommandIDs.resetOnLoad, {
- execute: (args: IRouter.ILocation) => {
- const { hash, path, search } = args;
- const query = URLExt.queryStringToObject(search || '');
- const reset = 'reset' in query;
- if (!reset) {
- return;
- }
- // If the state database has already been resolved, resetting is
- // impossible without reloading.
- if (resolved) {
- return router.reload();
- }
- // Empty the state database.
- resolved = true;
- transform.resolve({ type: 'clear', contents: null });
- // Maintain the query string parameters but remove `reset`.
- delete query['reset'];
- const url = path + URLExt.objectToQueryString(query) + hash;
- const cleared = commands.execute(CommandIDs.recoverState)
- .then(() => router.stop); // Stop routing before new route navigation.
- // After the state has been reset, navigate to the URL.
- cleared.then(() => { router.navigate(url, { silent: true }); });
- return cleared;
- }
- });
- router.register({
- command: CommandIDs.resetOnLoad,
- pattern: Patterns.resetOnLoad,
- rank: 10 // Set reset rank at a higher priority than the default 100.
- });
- const fallthrough = () => {
- // If the state database is still unresolved after the first URL has been
- // routed, leave it intact.
- if (!resolved) {
- resolved = true;
- transform.resolve({ type: 'cancel', contents: null });
- }
- router.routed.disconnect(fallthrough, state);
- };
- router.routed.connect(fallthrough, state);
- return state;
- }
- };
- /**
- * Export the plugins as default.
- */
- const plugins: JupyterLabPlugin<any>[] = [
- palette, paletteRestorer, resolver, settings, state, splash, themes
- ];
- export default plugins;
- /**
- * The namespace for module private data.
- */
- namespace Private {
- /**
- * Returns the workspace name from the URL, if it exists.
- */
- export
- function getWorkspace(router: IRouter): string {
- const match = router.current.path.match(Patterns.loadState);
- return match && decodeURIComponent(match[1]) || '';
- }
- /**
- * 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);
- }).catch(() => { /* no-op */ });
- }
- /**
- * Allows the user to clear state if splash screen takes too long.
- */
- export
- function redirect(router: IRouter, warn = false): Promise<void> {
- const form = createRedirectForm(warn);
- const dialog = new Dialog({
- title: 'We have a problem!',
- body: form,
- focusNodeSelector: 'input',
- buttons: [Dialog.okButton({ label: 'Create Workspace' })]
- });
- return dialog.launch().then(result => {
- dialog.dispose();
- if (result.value) {
- const url = `workspaces/${result.value}`;
- // Navigate to a new workspace URL and abandon this session altogether.
- router.navigate(url, { hard: true, silent: true });
- // This promise will never resolve because the application navigates
- // away to a new location. It only exists to satisfy the return type
- // of the `redirect` function.
- return new Promise<void>(() => { /* no-op */ });
- }
- return redirect(router, true);
- });
- }
- /**
- * The splash element.
- */
- const splash = createSplash();
- /**
- * The splash screen counter.
- */
- let splashCount = 0;
- /**
- * 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>, recovery: () => void): IDisposable {
- splash.classList.remove('splash-fade');
- splashCount++;
- if (debouncer) {
- window.clearTimeout(debouncer);
- }
- debouncer = window.setTimeout(() => {
- recover(recovery);
- }, SPLASH_RECOVER_TIMEOUT);
- document.body.appendChild(splash);
- return new DisposableDelegate(() => {
- ready.then(() => {
- if (--splashCount === 0) {
- if (debouncer) {
- window.clearTimeout(debouncer);
- debouncer = 0;
- }
- if (dialog) {
- dialog.dispose();
- dialog = null;
- }
- splash.classList.add('splash-fade');
- window.setTimeout(() => { document.body.removeChild(splash); }, 500);
- }
- });
- });
- }
- }
|