diff --git a/packages/application/src/lab.ts b/packages/application/src/lab.ts index 8bbc8cd202fd..6bf610aa9059 100644 --- a/packages/application/src/lab.ts +++ b/packages/application/src/lab.ts @@ -4,6 +4,7 @@ import { PageConfig } from '@jupyterlab/coreutils'; import { Base64ModelFactory } from '@jupyterlab/docregistry'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; +import { ServiceManager } from '@jupyterlab/services'; import { Token } from '@lumino/coreutils'; import { JupyterFrontEnd, JupyterFrontEndPlugin } from './frontend'; import { createRendermimePlugins } from './mimerenderers'; @@ -18,7 +19,17 @@ export class JupyterLab extends JupyterFrontEnd { * Construct a new JupyterLab object. */ constructor(options: JupyterLab.IOptions = { shell: new LabShell() }) { - super({ ...options, shell: options.shell || new LabShell() }); + super({ + ...options, + shell: options.shell || new LabShell(), + serviceManager: + options.serviceManager || + new ServiceManager({ + standby: () => { + return !this._info.isConnected || 'when-hidden'; + } + }) + }); this.restored = this.shell.restored .then(() => undefined) .catch(() => undefined); @@ -157,7 +168,7 @@ export class JupyterLab extends JupyterFrontEnd { }); } - private _info: JupyterLab.IInfo; + private _info: JupyterLab.IInfo = JupyterLab.defaultInfo; private _paths: JupyterFrontEnd.IPaths; } @@ -207,6 +218,18 @@ export namespace JupyterLab { * Whether files are cached on the server. */ readonly filesCached: boolean; + + /** + * Every periodic network polling should be paused while this is set + * to `false`. Extensions should use this value to decide whether to proceed + * with the polling. + * The extensions may also set this value to `false` if there is no need to + * fetch anything from the server backend basing on some conditions + * (e.g. when an error message dialog is displayed). + * At the same time, the extensions are responsible for setting this value + * back to `true`. + */ + isConnected: boolean; } /** @@ -217,7 +240,8 @@ export namespace JupyterLab { deferred: { patterns: [], matches: [] }, disabled: { patterns: [], matches: [] }, mimeExtensions: [], - filesCached: PageConfig.getOption('cacheFiles').toLowerCase() === 'true' + filesCached: PageConfig.getOption('cacheFiles').toLowerCase() === 'true', + isConnected: true }; /** diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts index 4b5a7a617e81..279d5c2c07be 100644 --- a/packages/docmanager-extension/src/index.ts +++ b/packages/docmanager-extension/src/index.ts @@ -9,7 +9,8 @@ import { ILabShell, ILabStatus, JupyterFrontEnd, - JupyterFrontEndPlugin + JupyterFrontEndPlugin, + JupyterLab } from '@jupyterlab/application'; import { Dialog, @@ -89,7 +90,8 @@ const docManagerPlugin: JupyterFrontEndPlugin = { ICommandPalette, ILabShell, ISessionContextDialogs, - IDocumentProviderFactory + IDocumentProviderFactory, + JupyterLab.IInfo ], activate: ( app: JupyterFrontEnd, @@ -99,7 +101,8 @@ const docManagerPlugin: JupyterFrontEndPlugin = { palette: ICommandPalette | null, labShell: ILabShell | null, sessionDialogs: ISessionContextDialogs | null, - docProviderFactory: IDocumentProviderFactory | null + docProviderFactory: IDocumentProviderFactory | null, + info: JupyterLab.IInfo | null ): IDocumentManager => { const trans = translator.load('jupyterlab'); const manager = app.serviceManager; @@ -139,7 +142,13 @@ const docManagerPlugin: JupyterFrontEndPlugin = { sessionDialogs: sessionDialogs || undefined, translator, collaborative: true, - docProviderFactory: docProviderFactory ?? undefined + docProviderFactory: docProviderFactory ?? undefined, + isConnectedCallback: () => { + if (info) { + return info.isConnected; + } + return true; + } }); // Register the file operations commands. diff --git a/packages/docmanager/src/manager.ts b/packages/docmanager/src/manager.ts index 78b8e1dab753..d2089dde586a 100644 --- a/packages/docmanager/src/manager.ts +++ b/packages/docmanager/src/manager.ts @@ -42,6 +42,7 @@ export class DocumentManager implements IDocumentManager { this._collaborative = !!options.collaborative; this._dialogs = options.sessionDialogs || sessionContextDialogs; this._docProviderFactory = options.docProviderFactory; + this._isConnectedCallback = options.isConnectedCallback || (() => true); this._opener = options.opener; this._when = options.when || options.manager.ready; @@ -477,6 +478,7 @@ export class DocumentManager implements IDocumentManager { }); const handler = new SaveHandler({ context, + isConnectedCallback: this._isConnectedCallback, saveInterval: this.autosaveInterval }); Private.saveHandlerProperty.set(context, handler); @@ -602,6 +604,7 @@ export class DocumentManager implements IDocumentManager { private _dialogs: ISessionContext.IDialogs; private _docProviderFactory: IDocumentProviderFactory | undefined; private _collaborative: boolean; + private _isConnectedCallback: () => boolean; } /** @@ -657,6 +660,12 @@ export namespace DocumentManager { * If true, the context will connect through yjs_ws_server to share information if possible. */ collaborative?: boolean; + + /** + * Autosaving should be paused while this callback function returns `false`. + * By default, it always returns `true`. + */ + isConnectedCallback?: () => boolean; } /** diff --git a/packages/docmanager/src/savehandler.ts b/packages/docmanager/src/savehandler.ts index d9aa8d57e185..1ea9b71ff55f 100644 --- a/packages/docmanager/src/savehandler.ts +++ b/packages/docmanager/src/savehandler.ts @@ -17,6 +17,7 @@ export class SaveHandler implements IDisposable { */ constructor(options: SaveHandler.IOptions) { this._context = options.context; + this._isConnectedCallback = options.isConnectedCallback || (() => true); const interval = options.saveInterval || 120; this._minInterval = interval * 1000; this._interval = this._minInterval; @@ -89,7 +90,9 @@ export class SaveHandler implements IDisposable { return; } this._autosaveTimer = window.setTimeout(() => { - this._save(); + if (this._isConnectedCallback()) { + this._save(); + } }, this._interval); } @@ -144,6 +147,7 @@ export class SaveHandler implements IDisposable { private _minInterval = -1; private _interval = -1; private _context: DocumentRegistry.Context; + private _isConnectedCallback: () => boolean; private _isActive = false; private _inDialog = false; private _isDisposed = false; @@ -163,6 +167,12 @@ export namespace SaveHandler { */ context: DocumentRegistry.Context; + /** + * Autosaving should be paused while this callback function returns `false`. + * By default, it always returns `true`. + */ + isConnectedCallback?: () => boolean; + /** * The minimum save interval in seconds (default is two minutes). */ diff --git a/packages/filebrowser-extension/src/index.ts b/packages/filebrowser-extension/src/index.ts index e1003e65a710..cd39f4e90cdc 100644 --- a/packages/filebrowser-extension/src/index.ts +++ b/packages/filebrowser-extension/src/index.ts @@ -11,7 +11,8 @@ import { IRouter, ITreePathUpdater, JupyterFrontEnd, - JupyterFrontEndPlugin + JupyterFrontEndPlugin, + JupyterLab } from '@jupyterlab/application'; import { Clipboard, @@ -246,14 +247,20 @@ const factory: JupyterFrontEndPlugin = { id: '@jupyterlab/filebrowser-extension:factory', provides: IFileBrowserFactory, requires: [IDocumentManager, ITranslator], - optional: [IStateDB, IRouter, JupyterFrontEnd.ITreeResolver], + optional: [ + IStateDB, + IRouter, + JupyterFrontEnd.ITreeResolver, + JupyterLab.IInfo + ], activate: async ( app: JupyterFrontEnd, docManager: IDocumentManager, translator: ITranslator, state: IStateDB | null, router: IRouter | null, - tree: JupyterFrontEnd.ITreeResolver | null + tree: JupyterFrontEnd.ITreeResolver | null, + info: JupyterLab.IInfo | null ): Promise => { const { commands } = app; const tracker = new WidgetTracker({ namespace }); @@ -267,6 +274,12 @@ const factory: JupyterFrontEndPlugin = { manager: docManager, driveName: options.driveName || '', refreshInterval: options.refreshInterval, + refreshStandby: () => { + if (info) { + return !info.isConnected || 'when-hidden'; + } + return 'when-hidden'; + }, state: options.state === null ? undefined diff --git a/packages/filebrowser/src/model.ts b/packages/filebrowser/src/model.ts index 01028f33ee3a..b9617aedb60b 100644 --- a/packages/filebrowser/src/model.ts +++ b/packages/filebrowser/src/model.ts @@ -103,7 +103,7 @@ export class FileBrowserModel implements IDisposable { backoff: true, max: 300 * 1000 }, - standby: 'when-hidden' + standby: options.refreshStandby || 'when-hidden' }); } @@ -695,6 +695,11 @@ export namespace FileBrowserModel { */ refreshInterval?: number; + /** + * When the model stops polling the API. Defaults to `when-hidden`. + */ + refreshStandby?: Poll.Standby | (() => boolean | Poll.Standby); + /** * An optional state database. If provided, the model will restore which * folder was last opened when it is restored. diff --git a/packages/hub-extension/src/index.ts b/packages/hub-extension/src/index.ts index 5000757b60e7..647125a74fa0 100644 --- a/packages/hub-extension/src/index.ts +++ b/packages/hub-extension/src/index.ts @@ -11,7 +11,8 @@ import { ConnectionLost, IConnectionLost, JupyterFrontEnd, - JupyterFrontEndPlugin + JupyterFrontEndPlugin, + JupyterLab } from '@jupyterlab/application'; import { Dialog, ICommandPalette, showDialog } from '@jupyterlab/apputils'; import { URLExt } from '@jupyterlab/coreutils'; @@ -125,10 +126,12 @@ const hubExtensionMenu: JupyterFrontEndPlugin = { const connectionlost: JupyterFrontEndPlugin = { id: '@jupyterlab/apputils-extension:connectionlost', requires: [JupyterFrontEnd.IPaths, ITranslator], + optional: [JupyterLab.IInfo], activate: ( app: JupyterFrontEnd, paths: JupyterFrontEnd.IPaths, - translator: ITranslator + translator: ITranslator, + info: JupyterLab.IInfo | null ): IConnectionLost => { const trans = translator.load('jupyterlab'); const hubPrefix = paths.urls.hubPrefix || ''; @@ -149,7 +152,12 @@ const connectionlost: JupyterFrontEndPlugin = { if (showingError) { return; } + showingError = true; + if (info) { + info.isConnected = false; + } + const result = await showDialog({ title: trans.__('Server unavailable or unreachable'), body: trans.__( @@ -161,7 +169,12 @@ const connectionlost: JupyterFrontEndPlugin = { Dialog.cancelButton({ label: trans.__('Dismiss') }) ] }); + + if (info) { + info.isConnected = true; + } showingError = false; + if (result.button.accept) { await app.commands.execute(CommandIDs.restart); } diff --git a/packages/services/src/kernel/manager.ts b/packages/services/src/kernel/manager.ts index 015d66e772e3..1dbc23ce6827 100644 --- a/packages/services/src/kernel/manager.ts +++ b/packages/services/src/kernel/manager.ts @@ -337,6 +337,6 @@ export namespace KernelManager { /** * When the manager stops polling the API. Defaults to `when-hidden`. */ - standby?: Poll.Standby; + standby?: Poll.Standby | (() => boolean | Poll.Standby); } } diff --git a/packages/services/src/kernelspec/manager.ts b/packages/services/src/kernelspec/manager.ts index 717a771f2bf6..9ce1f92b40b8 100644 --- a/packages/services/src/kernelspec/manager.ts +++ b/packages/services/src/kernelspec/manager.ts @@ -147,6 +147,6 @@ export namespace KernelSpecManager { /** * When the manager stops polling the API. Defaults to `when-hidden`. */ - standby?: Poll.Standby; + standby?: Poll.Standby | (() => boolean | Poll.Standby); } } diff --git a/packages/services/src/manager.ts b/packages/services/src/manager.ts index cd4d93131f34..a328f5730e91 100644 --- a/packages/services/src/manager.ts +++ b/packages/services/src/manager.ts @@ -254,6 +254,6 @@ export namespace ServiceManager { /** * When the manager stops polling the API. Defaults to `when-hidden`. */ - standby?: Poll.Standby; + standby?: Poll.Standby | (() => boolean | Poll.Standby); } } diff --git a/packages/services/src/session/manager.ts b/packages/services/src/session/manager.ts index bd803f6bb321..2c51b65c0918 100644 --- a/packages/services/src/session/manager.ts +++ b/packages/services/src/session/manager.ts @@ -355,7 +355,7 @@ export namespace SessionManager { /** * When the manager stops polling the API. Defaults to `when-hidden`. */ - standby?: Poll.Standby; + standby?: Poll.Standby | (() => boolean | Poll.Standby); /** * Kernel Manager diff --git a/packages/services/src/terminal/manager.ts b/packages/services/src/terminal/manager.ts index 948245aaad59..ef308a535907 100644 --- a/packages/services/src/terminal/manager.ts +++ b/packages/services/src/terminal/manager.ts @@ -289,6 +289,6 @@ export namespace TerminalManager { /** * When the manager stops polling the API. Defaults to `when-hidden`. */ - standby?: Poll.Standby; + standby?: Poll.Standby | (() => boolean | Poll.Standby); } }