Browse Source

Backport PR #10730 on branch 3.3.x (Add execution progress indicator) (#11941)

* Add execution progress indicator

* Update import path
Duc Trung LE 3 years ago
parent
commit
e0f89a537d
34 changed files with 1321 additions and 78 deletions
  1. 3 3
      galata/src/inpage/index.ts
  2. 1 1
      galata/test/galata/fixture.spec.ts
  3. BIN
      galata/test/galata/notebook.spec.ts-snapshots/code-cell-galata-linux.png
  4. BIN
      galata/test/galata/notebook.spec.ts-snapshots/example-run-galata-linux.png
  5. BIN
      galata/test/galata/notebook.spec.ts-snapshots/markdown-cell-galata-linux.png
  6. BIN
      galata/test/galata/notebook.spec.ts-snapshots/raw-cell-galata-linux.png
  7. BIN
      galata/test/galata/notebook.spec.ts-snapshots/run-cells-galata-linux.png
  8. 2 2
      galata/test/jupyterlab/workspace.test.ts
  9. 1 0
      packages/apputils/src/index.ts
  10. 32 0
      packages/apputils/src/kernelstatuses.ts
  11. 2 17
      packages/apputils/src/toolbar/widget.tsx
  12. 1 1
      packages/debugger-extension/src/index.ts
  13. 2 1
      packages/notebook-extension/schema/panel.json
  14. 19 0
      packages/notebook-extension/schema/tracker.json
  15. 121 3
      packages/notebook-extension/src/index.ts
  16. 1 0
      packages/notebook/package.json
  17. 0 4
      packages/notebook/src/default-toolbar.tsx
  18. 641 0
      packages/notebook/src/executionindicator.tsx
  19. 1 0
      packages/notebook/src/index.ts
  20. 1 0
      packages/notebook/style/base.css
  21. 65 0
      packages/notebook/style/executionindicator.css
  22. 1 2
      packages/notebook/test/default-toolbar.spec.ts
  23. 250 0
      packages/notebook/test/executionindicator.spec.tsx
  24. 0 1
      packages/notebook/test/widgetfactory.spec.ts
  25. 3 0
      packages/notebook/tsconfig.json
  26. 6 0
      packages/notebook/tsconfig.test.json
  27. 1 0
      packages/statusbar/src/components/index.ts
  28. 28 7
      packages/statusbar/src/components/progressBar.tsx
  29. 55 0
      packages/statusbar/src/components/progressCircle.tsx
  30. 7 17
      packages/statusbar/src/defaults/kernelStatus.tsx
  31. 0 19
      packages/statusbar/src/style/progressBar.ts
  32. 73 0
      packages/statusbar/style/base.css
  33. 2 0
      packages/statusbar/style/index.css
  34. 2 0
      packages/statusbar/style/index.js

+ 3 - 3
galata/src/inpage/index.ts

@@ -235,7 +235,7 @@ export class GalataInpage implements IGalataInpage {
     const nbPanel = this._app.shell.currentWidget as NotebookPanel;
     const nb = nbPanel.content;
 
-    this._app.commands.execute('notebook:delete-cell');
+    void this._app.commands.execute('notebook:delete-cell');
 
     nb.update();
   }
@@ -251,7 +251,7 @@ export class GalataInpage implements IGalataInpage {
     const nb = nbPanel.content;
 
     if (nb !== null) {
-      this._app.commands.execute('notebook:insert-cell-below');
+      void this._app.commands.execute('notebook:insert-cell-below');
 
       const numCells = nb.widgets.length;
 
@@ -536,7 +536,7 @@ export class GalataInpage implements IGalataInpage {
       return;
     }
 
-    this._app.commands.execute('notebook:deselect-all');
+    void this._app.commands.execute('notebook:deselect-all');
 
     for (let i = 0; i < numCells; ++i) {
       const cell = notebook.widgets[i];

+ 1 - 1
galata/test/galata/fixture.spec.ts

@@ -122,7 +122,7 @@ test.describe('sessions', () => {
     await page.menu.clickMenuItem('File>New>Console');
     await page.waitForSelector('.jp-Dialog');
     await page.click('.jp-Dialog .jp-mod-accept');
-    await page.waitForSelector('text=Idle');
+    await page.waitForSelector('text= | Idle');
 
     expect(sessions.size).toEqual(2);
   });

BIN
galata/test/galata/notebook.spec.ts-snapshots/code-cell-galata-linux.png


BIN
galata/test/galata/notebook.spec.ts-snapshots/example-run-galata-linux.png


BIN
galata/test/galata/notebook.spec.ts-snapshots/markdown-cell-galata-linux.png


BIN
galata/test/galata/notebook.spec.ts-snapshots/raw-cell-galata-linux.png


BIN
galata/test/galata/notebook.spec.ts-snapshots/run-cells-galata-linux.png


+ 2 - 2
galata/test/jupyterlab/workspace.test.ts

@@ -17,7 +17,7 @@ test.use({
         state: 'detached'
       });
     };
-    use(simpleWait);
+    void use(simpleWait);
   }
 });
 
@@ -147,7 +147,7 @@ test.describe('Workspace', () => {
     ).toBeVisible();
 
     // Wait for the kernel to be ready so it does not unfocus the menu
-    await page.waitForSelector('text=Idle');
+    await page.waitForSelector('text= | Idle');
 
     await expect(page.menu.getMenuItem(`Tabs>${mdFile}`)).toBeDefined();
   });

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

@@ -28,3 +28,4 @@ export * from './toolbar';
 export * from './vdom';
 export * from './widgettracker';
 export * from './windowresolver';
+export * from './kernelstatuses';

+ 32 - 0
packages/apputils/src/kernelstatuses.ts

@@ -0,0 +1,32 @@
+import { ITranslator, nullTranslator } from '@jupyterlab/translation';
+import { ISessionContext } from './sessioncontext';
+
+/**
+ * Helper function to translate kernel statuses mapping by using
+ * input translator.
+ *
+ * @param translator - - Language translator.
+ * @return The translated kernel status mapping.
+ */
+export function translateKernelStatuses(
+  translator?: ITranslator
+): Record<ISessionContext.KernelDisplayStatus, string> {
+  translator = translator || nullTranslator;
+  const trans = translator.load('jupyterlab');
+  const translated: Record<ISessionContext.KernelDisplayStatus, string> = {
+    unknown: trans.__('Unknown'),
+    starting: trans.__('Starting'),
+    idle: trans.__('Idle'),
+    busy: trans.__('Busy'),
+    terminating: trans.__('Terminating'),
+    restarting: trans.__('Restarting'),
+    autorestarting: trans.__('Autorestarting'),
+    dead: trans.__('Dead'),
+    connected: trans.__('Connected'),
+    connecting: trans.__('Connecting'),
+    disconnected: trans.__('Disconnected'),
+    initializing: trans.__('Initializing'),
+    '': ''
+  };
+  return translated;
+}

+ 2 - 17
packages/apputils/src/toolbar/widget.tsx

@@ -25,6 +25,7 @@ import { AttachedProperty } from '@lumino/properties';
 import { PanelLayout, Widget } from '@lumino/widgets';
 import * as React from 'react';
 import { ISessionContext, sessionContextDialogs } from '../sessioncontext';
+import { translateKernelStatuses } from '../kernelstatuses';
 import { ReactWidget, UseSignal } from '../vdom';
 import { Throttler } from '@lumino/polling';
 
@@ -1261,22 +1262,7 @@ namespace Private {
       this.translator = translator || nullTranslator;
       this._trans = this.translator.load('jupyterlab');
       this.addClass(TOOLBAR_KERNEL_STATUS_CLASS);
-      // TODO-FIXME: this mapping is duplicated in statusbar/kernelStatus.tsx
-      this._statusNames = {
-        unknown: this._trans.__('Unknown'),
-        starting: this._trans.__('Starting'),
-        idle: this._trans.__('Idle'),
-        busy: this._trans.__('Busy'),
-        terminating: this._trans.__('Terminating'),
-        restarting: this._trans.__('Restarting'),
-        autorestarting: this._trans.__('Autorestarting'),
-        dead: this._trans.__('Dead'),
-        connected: this._trans.__('Connected'),
-        connecting: this._trans.__('Connecting'),
-        disconnected: this._trans.__('Disconnected'),
-        initializing: this._trans.__('Initializing'),
-        '': ''
-      };
+      this._statusNames = translateKernelStatuses(this.translator);
       this._onStatusChanged(sessionContext);
       sessionContext.statusChanged.connect(this._onStatusChanged, this);
       sessionContext.connectionStatusChanged.connect(
@@ -1294,7 +1280,6 @@ namespace Private {
       }
 
       const status = sessionContext.kernelDisplayStatus;
-
       const circleIconProps: LabIcon.IProps = {
         container: this.node,
         title: this._trans.__('Kernel %1', this._statusNames[status] || status),

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

@@ -471,7 +471,7 @@ const variables: JupyterFrontEndPlugin<void> = {
         const refreshWidget = () => {
           // Refresh the widget only if the active element is the same.
           if (handler.activeWidget === activeWidget) {
-            widget.refresh();
+            void widget.refresh();
           }
         };
         widget.disposed.connect(disposeWidget);

+ 2 - 1
packages/notebook-extension/schema/panel.json

@@ -19,7 +19,8 @@
       { "name": "cellType", "rank": 40 },
       { "name": "spacer", "type": "spacer", "rank": 100 },
       { "name": "kernelName", "rank": 1000 },
-      { "name": "kernelStatus", "rank": 1001 }
+      { "name": "kernelStatus", "rank": 1001 },
+      { "name": "executionProgress", "rank": 1002 }
     ]
   },
   "jupyter.lab.transform": true,

+ 19 - 0
packages/notebook-extension/schema/tracker.json

@@ -682,6 +682,16 @@
       },
       "additionalProperties": false,
       "type": "object"
+    },
+    "kernelStatusConfig": {
+      "showOnStatusBar": {
+        "type": "boolean",
+        "title": "Show kernel status on toolbar or status bar."
+      },
+      "showProgress": {
+        "type": "boolean",
+        "title": "Show execution progress."
+      }
     }
   },
   "properties": {
@@ -833,6 +843,15 @@
       "type": "number",
       "default": 50
     },
+    "kernelStatus": {
+      "title": "Kernel status icon configuration",
+      "description": "Defines the position and components of execution progress indicator.",
+      "$ref": "#/definitions/kernelStatusConfig",
+      "default": {
+        "showOnStatusBar": false,
+        "showProgress": true
+      }
+    },
     "experimentalDisableDocumentWideUndoRedo": {
       "title": "Experimental settings to enable the undo/redo on the notebook document level.",
       "description": "Disable the undo/redo on the notebook document level, so actions independent cells can have their own history. The undo/redo never applies on the outputs, in other words, outputs don't have history. A moved cell completely looses history capability for now.",

+ 121 - 3
packages/notebook-extension/src/index.ts

@@ -6,6 +6,7 @@
  */
 
 import {
+  ILabShell,
   ILayoutRestorer,
   JupyterFrontEnd,
   JupyterFrontEndPlugin
@@ -26,6 +27,7 @@ import {
 import { Cell, CodeCell, ICellModel, MarkdownCell } from '@jupyterlab/cells';
 import { IEditorServices } from '@jupyterlab/codeeditor';
 import { PageConfig } from '@jupyterlab/coreutils';
+
 import { IDocumentManager } from '@jupyterlab/docmanager';
 import { ToolbarItems as DocToolbarItems } from '@jupyterlab/docmanager-extension';
 import { DocumentRegistry } from '@jupyterlab/docregistry';
@@ -43,6 +45,7 @@ import {
 import * as nbformat from '@jupyterlab/nbformat';
 import {
   CommandEditStatus,
+  ExecutionIndicator,
   INotebookTools,
   INotebookTracker,
   INotebookWidgetFactory,
@@ -85,7 +88,7 @@ import {
   ReadonlyPartialJSONObject,
   UUID
 } from '@lumino/coreutils';
-import { DisposableSet } from '@lumino/disposable';
+import { DisposableSet, IDisposable } from '@lumino/disposable';
 import { Message, MessageLoop } from '@lumino/messaging';
 import { Menu, Panel } from '@lumino/widgets';
 import { logNotebookOutput } from './nboutput';
@@ -358,6 +361,113 @@ export const commandEditItem: JupyterFrontEndPlugin<void> = {
   }
 };
 
+/**
+ * A plugin that provides a execution indicator item to the status bar.
+ */
+export const executionIndicator: JupyterFrontEndPlugin<void> = {
+  id: '@jupyterlab/notebook-extension:execution-indicator',
+  autoStart: true,
+  requires: [INotebookTracker, ILabShell, ITranslator],
+  optional: [IStatusBar, ISettingRegistry],
+  activate: (
+    app: JupyterFrontEnd,
+    notebookTracker: INotebookTracker,
+    labShell: ILabShell,
+    translator: ITranslator,
+    statusBar: IStatusBar | null,
+    settingRegistry: ISettingRegistry | null
+  ) => {
+    let statusbarItem: ExecutionIndicator;
+    let labShellCurrentChanged: (
+      _: ILabShell,
+      change: ILabShell.IChangedArgs
+    ) => void;
+
+    let statusBarDisposable: IDisposable;
+
+    const updateSettings = (settings: {
+      showOnToolBar: boolean;
+      showProgress: boolean;
+    }): void => {
+      let { showOnToolBar, showProgress } = settings;
+
+      if (!showOnToolBar) {
+        // Status bar mode, only one `ExecutionIndicator` is needed.
+        if (!statusBar) {
+          // Automatically disable if statusbar missing
+          return;
+        }
+
+        if (!statusbarItem?.model) {
+          statusbarItem = new ExecutionIndicator(translator);
+          labShellCurrentChanged = (
+            _: ILabShell,
+            change: ILabShell.IChangedArgs
+          ) => {
+            const { newValue } = change;
+            if (newValue && notebookTracker.has(newValue)) {
+              const panel = newValue as NotebookPanel;
+              statusbarItem.model!.attachNotebook({
+                content: panel.content,
+                context: panel.sessionContext
+              });
+            }
+          };
+          statusBarDisposable = statusBar.registerStatusItem(
+            '@jupyterlab/notebook-extension:execution-indicator',
+            {
+              item: statusbarItem,
+              align: 'left',
+              rank: 3,
+              isActive: () => {
+                const current = labShell.currentWidget;
+                return !!current && notebookTracker.has(current);
+              }
+            }
+          );
+
+          statusbarItem.model.attachNotebook({
+            content: notebookTracker.currentWidget?.content,
+            context: notebookTracker.currentWidget?.sessionContext
+          });
+
+          labShell.currentChanged.connect(labShellCurrentChanged);
+          statusbarItem.disposed.connect(() => {
+            labShell.currentChanged.disconnect(labShellCurrentChanged);
+          });
+        }
+
+        statusbarItem.model.displayOption = {
+          showOnToolBar,
+          showProgress
+        };
+      } else {
+        //Remove old indicator widget on status bar
+        if (statusBarDisposable) {
+          labShell.currentChanged.disconnect(labShellCurrentChanged);
+          statusBarDisposable.dispose();
+        }
+      }
+    };
+
+    if (settingRegistry) {
+      // Indicator is default in tool bar, user needs to specify its
+      // position in settings in order to have indicator on status bar.
+      const loadSettings = settingRegistry.load(trackerPlugin.id);
+      Promise.all([loadSettings, app.restored])
+        .then(([settings]) => {
+          updateSettings(ExecutionIndicator.getSettingValue(settings));
+          settings.changed.connect(sender =>
+            updateSettings(ExecutionIndicator.getSettingValue(sender))
+          );
+        })
+        .catch((reason: Error) => {
+          console.error(reason.message);
+        });
+    }
+  }
+};
+
 /**
  * A plugin providing export commands in the main menu and command palette
  */
@@ -565,6 +675,7 @@ const copyOutputPlugin: JupyterFrontEndPlugin<void> = {
 const plugins: JupyterFrontEndPlugin<any>[] = [
   factory,
   trackerPlugin,
+  executionIndicator,
   exportPlugin,
   tools,
   commandEditItem,
@@ -724,10 +835,17 @@ function activateWidgetFactory(
       translator
     )
   );
+
   toolbarRegistry.registerFactory<NotebookPanel>(
     FACTORY,
-    'kernelStatus',
-    panel => Toolbar.createKernelStatusItem(panel.sessionContext, translator)
+    'executionProgress',
+    panel => {
+      return ExecutionIndicator.createExecutionIndicatorItem(
+        panel,
+        translator,
+        settingRegistry?.load(trackerPlugin.id)
+      );
+    }
   );
 
   if (settingRegistry) {

+ 1 - 0
packages/notebook/package.json

@@ -50,6 +50,7 @@
     "@jupyterlab/observables": "^4.3.0-alpha.17",
     "@jupyterlab/rendermime": "^3.3.0-alpha.17",
     "@jupyterlab/services": "^6.3.0-alpha.17",
+    "@jupyterlab/settingregistry": "^3.3.0-alpha.17",
     "@jupyterlab/shared-models": "^3.3.0-alpha.17",
     "@jupyterlab/statusbar": "^3.3.0-alpha.17",
     "@jupyterlab/translation": "^3.3.0-alpha.17",

+ 0 - 4
packages/notebook/src/default-toolbar.tsx

@@ -286,10 +286,6 @@ export namespace ToolbarItems {
           sessionDialogs,
           translator
         )
-      },
-      {
-        name: 'kernelStatus',
-        widget: Toolbar.createKernelStatusItem(panel.sessionContext, translator)
       }
     ];
   }

+ 641 - 0
packages/notebook/src/executionindicator.tsx

@@ -0,0 +1,641 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  ISessionContext,
+  translateKernelStatuses,
+  VDomModel,
+  VDomRenderer
+} from '@jupyterlab/apputils';
+
+import { ITranslator, nullTranslator } from '@jupyterlab/translation';
+import React from 'react';
+import { interactiveItem, ProgressCircle } from '@jupyterlab/statusbar';
+
+import {
+  circleIcon,
+  LabIcon,
+  offlineBoltIcon
+} from '@jupyterlab/ui-components';
+
+import { Notebook } from './widget';
+import { KernelMessage } from '@jupyterlab/services';
+import {
+  IAnyMessageArgs,
+  IKernelConnection
+} from '@jupyterlab/services/src/kernel/kernel';
+import { NotebookPanel } from './panel';
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
+import { Widget } from '@lumino/widgets';
+import { JSONObject } from '@lumino/coreutils';
+import { IChangedArgs } from '@jupyterlab/coreutils';
+
+/**
+ * A react functional component for rendering execution indicator.
+ */
+export function ExecutionIndicatorComponent(
+  props: ExecutionIndicatorComponent.IProps
+): React.ReactElement<ExecutionIndicatorComponent.IProps> {
+  const translator = props.translator || nullTranslator;
+  const kernelStatuses = translateKernelStatuses(translator);
+  const trans = translator.load('jupyterlab');
+
+  const state = props.state;
+  const showOnToolBar = props.displayOption.showOnToolBar;
+  const showProgress = props.displayOption.showProgress;
+  const tooltipClass = showOnToolBar ? 'down' : 'up';
+  const emptyDiv = <div></div>;
+
+  if (!state) {
+    return emptyDiv;
+  }
+
+  const kernelStatus = state.kernelStatus;
+  const circleIconProps: LabIcon.IProps = {
+    alignSelf: 'normal',
+    height: '24px'
+  };
+  const time = state.totalTime;
+
+  const scheduledCellNumber = state.scheduledCellNumber || 0;
+  const remainingCellNumber = state.scheduledCell.size || 0;
+  const executedCellNumber = scheduledCellNumber - remainingCellNumber;
+  let percentage = (100 * executedCellNumber) / scheduledCellNumber;
+  let displayClass = showProgress ? '' : 'hidden';
+  if (!showProgress && percentage < 100) {
+    percentage = 0;
+  }
+
+  const progressBar = (percentage: number) => (
+    <ProgressCircle progress={percentage} width={16} height={24} />
+  );
+  const titleFactory = (translatedStatus: string) =>
+    trans.__('Kernel status: %1', translatedStatus);
+
+  const reactElement = (
+    status: ISessionContext.KernelDisplayStatus,
+    circle: JSX.Element,
+    popup: JSX.Element[]
+  ): JSX.Element => (
+    <div
+      className={'jp-Notebook-ExecutionIndicator'}
+      title={showProgress ? '' : titleFactory(kernelStatuses[status])}
+    >
+      {circle}
+      <div
+        className={`jp-Notebook-ExecutionIndicator-tooltip ${tooltipClass} ${displayClass}`}
+      >
+        <span> {titleFactory(kernelStatuses[status])} </span>
+        {popup}
+      </div>
+    </div>
+  );
+
+  if (
+    state.kernelStatus === 'connecting' ||
+    state.kernelStatus === 'disconnected' ||
+    state.kernelStatus === 'unknown'
+  ) {
+    return reactElement(
+      kernelStatus,
+      <offlineBoltIcon.react {...circleIconProps} />,
+      []
+    );
+  }
+  if (
+    state.kernelStatus === 'starting' ||
+    state.kernelStatus === 'terminating' ||
+    state.kernelStatus === 'restarting' ||
+    state.kernelStatus === 'initializing'
+  ) {
+    return reactElement(
+      kernelStatus,
+      <circleIcon.react {...circleIconProps} />,
+      []
+    );
+  }
+
+  if (state.executionStatus === 'busy') {
+    return reactElement('busy', progressBar(percentage), [
+      <span key={0}>
+        {trans.__(
+          `Executed ${executedCellNumber}/${scheduledCellNumber} requests`
+        )}
+      </span>,
+      <span key={1}>
+        {trans._n('Elapsed time: %1 second', 'Elapsed time: %1 seconds', time)}
+      </span>
+    ]);
+  } else {
+    if (time === 0) {
+      return reactElement('idle', progressBar(100), []);
+    } else {
+      return reactElement('idle', progressBar(100), [
+        <span key={0}>
+          {trans._n(
+            'Executed %1 request',
+            'Executed %1 requests',
+            scheduledCellNumber
+          )}
+        </span>,
+        <span key={1}>
+          {trans._n(
+            'Elapsed time: %1 second',
+            'Elapsed time: %1 seconds',
+            time
+          )}
+        </span>
+      ]);
+    }
+  }
+}
+
+/**
+ * A namespace for ExecutionIndicatorComponent statics.
+ */
+namespace ExecutionIndicatorComponent {
+  /**
+   * Props for the execution status component.
+   */
+  export interface IProps {
+    /**
+     * Display option for progress bar and elapsed time.
+     */
+    displayOption: Private.DisplayOption;
+
+    /**
+     * Execution state of selected notebook.
+     */
+    state?: Private.IExecutionState;
+
+    /**
+     * The application language translator.
+     */
+    translator?: ITranslator;
+  }
+}
+
+/**
+ * A VDomRenderer widget for displaying the execution status.
+ */
+export class ExecutionIndicator extends VDomRenderer<ExecutionIndicator.Model> {
+  /**
+   * Construct the kernel status widget.
+   */
+  constructor(translator?: ITranslator, showProgress: boolean = true) {
+    super(new ExecutionIndicator.Model());
+    this.translator = translator || nullTranslator;
+    this.addClass(interactiveItem);
+  }
+
+  /**
+   * Render the execution status item.
+   */
+  render(): JSX.Element | null {
+    if (this.model === null || !this.model.renderFlag) {
+      return <div></div>;
+    } else {
+      const nb = this.model.currentNotebook;
+
+      if (!nb) {
+        return (
+          <ExecutionIndicatorComponent
+            displayOption={this.model.displayOption}
+            state={undefined}
+            translator={this.translator}
+          />
+        );
+      }
+
+      return (
+        <ExecutionIndicatorComponent
+          displayOption={this.model.displayOption}
+          state={this.model.executionState(nb)}
+          translator={this.translator}
+        />
+      );
+    }
+  }
+
+  private translator: ITranslator;
+}
+
+/**
+ * A namespace for ExecutionIndicator statics.
+ */
+export namespace ExecutionIndicator {
+  /**
+   * A VDomModel for the execution status indicator.
+   */
+  export class Model extends VDomModel {
+    constructor() {
+      super();
+      this._displayOption = { showOnToolBar: true, showProgress: true };
+      this._renderFlag = true;
+    }
+
+    /**
+     * Attach a notebook with session context to model in order to keep
+     * track of multiple notebooks. If a session context is already
+     * attached, only set current activated notebook to input.
+     *
+     * @param data - The  notebook and session context to be attached to model
+     */
+    attachNotebook(
+      data: { content?: Notebook; context?: ISessionContext } | null
+    ): void {
+      if (data && data.content && data.context) {
+        const nb = data.content;
+        const context = data.context;
+        this._currentNotebook = nb;
+        if (!this._notebookExecutionProgress.has(nb)) {
+          this._notebookExecutionProgress.set(nb, {
+            executionStatus: 'idle',
+            kernelStatus: 'idle',
+            totalTime: 0,
+            interval: 0,
+            timeout: 0,
+            scheduledCell: new Set<string>(),
+            scheduledCellNumber: 0,
+            needReset: true
+          });
+
+          const state = this._notebookExecutionProgress.get(nb);
+          const contextStatusChanged = (ctx: ISessionContext) => {
+            if (state) {
+              state.kernelStatus = ctx.kernelDisplayStatus;
+            }
+            this.stateChanged.emit(void 0);
+          };
+          context.statusChanged.connect(contextStatusChanged, this);
+
+          const contextConnectionStatusChanged = (ctx: ISessionContext) => {
+            if (state) {
+              state.kernelStatus = ctx.kernelDisplayStatus;
+            }
+            this.stateChanged.emit(void 0);
+          };
+          context.connectionStatusChanged.connect(
+            contextConnectionStatusChanged,
+            this
+          );
+
+          context.disposed.connect(ctx => {
+            ctx.connectionStatusChanged.disconnect(
+              contextConnectionStatusChanged,
+              this
+            );
+            ctx.statusChanged.disconnect(contextStatusChanged, this);
+          });
+          const handleKernelMsg = (
+            sender: IKernelConnection,
+            msg: IAnyMessageArgs
+          ) => {
+            const message = msg.msg;
+            const msgId = message.header.msg_id;
+
+            if (
+              KernelMessage.isCommMsgMsg(message) &&
+              message.content.data['method']
+            ) {
+              // Execution request from Comm message
+              const method = message.content.data['method'];
+              if (method !== 'request_state' && method !== 'update') {
+                this._cellScheduledCallback(nb, msgId);
+                this._startTimer(nb);
+              }
+            } else if (message.header.msg_type === 'execute_request') {
+              // A cell code is scheduled for executing
+              this._cellScheduledCallback(nb, msgId);
+            } else if (
+              KernelMessage.isStatusMsg(message) &&
+              message.content.execution_state === 'idle'
+            ) {
+              // Idle status message case.
+              const parentId = (message.parent_header as KernelMessage.IHeader)
+                .msg_id;
+              this._cellExecutedCallback(nb, parentId);
+            } else if (message.header.msg_type === 'execute_input') {
+              // A cell code starts executing.
+              this._startTimer(nb);
+            }
+          };
+          context.session?.kernel?.anyMessage.connect(handleKernelMsg);
+          context.session?.kernel?.disposed.connect(kernel =>
+            kernel.anyMessage.disconnect(handleKernelMsg)
+          );
+          const kernelChangedSlot = (
+            _: ISessionContext,
+            kernelData: IChangedArgs<
+              IKernelConnection | null,
+              IKernelConnection | null,
+              'kernel'
+            >
+          ) => {
+            if (state) {
+              this._resetTime(state);
+              this.stateChanged.emit(void 0);
+              if (kernelData.newValue) {
+                kernelData.newValue.anyMessage.connect(handleKernelMsg);
+              }
+            }
+          };
+          context.kernelChanged.connect(kernelChangedSlot);
+          context.disposed.connect(ctx =>
+            ctx.kernelChanged.disconnect(kernelChangedSlot)
+          );
+        }
+      }
+    }
+
+    /**
+     * The current activated notebook in model.
+     */
+    get currentNotebook(): Notebook | null {
+      return this._currentNotebook;
+    }
+
+    /**
+     * The display options for progress bar and elapsed time.
+     */
+    get displayOption(): Private.DisplayOption {
+      return this._displayOption;
+    }
+
+    /**
+     * Set the display options for progress bar and elapsed time.
+     *
+     * @param options - Options to be used
+     */
+    set displayOption(options: Private.DisplayOption) {
+      this._displayOption = options;
+    }
+
+    /**
+     * Get the execution state associated with a notebook.
+     *
+     * @param nb - The notebook used to identify execution
+     * state.
+     *
+     * @return - The associated execution state.
+     */
+    public executionState(nb: Notebook): Private.IExecutionState | undefined {
+      return this._notebookExecutionProgress.get(nb);
+    }
+
+    /**
+     * The function is called on kernel's idle status message.
+     * It is used to keep track number of executed
+     * cell or Comm custom messages and the status of kernel.
+     *
+     * @param  nb - The notebook which contains the executed code
+     * cell.
+     * @param  msg_id - The id of message.
+     *
+     * ### Note
+     *
+     * To keep track of cells executed under 1 second,
+     * the execution state is marked as `needReset` 1 second after executing
+     * these cells. This `Timeout` will be cleared if there is any cell
+     * scheduled after that.
+     */
+    private _cellExecutedCallback(nb: Notebook, msg_id: string): void {
+      const state = this._notebookExecutionProgress.get(nb);
+      if (state && state.scheduledCell.has(msg_id)) {
+        state.scheduledCell.delete(msg_id);
+        if (state.scheduledCell.size === 0) {
+          window.setTimeout(() => {
+            state.executionStatus = 'idle';
+            clearInterval(state.interval);
+            this.stateChanged.emit(void 0);
+          }, 150);
+          state.timeout = window.setTimeout(() => {
+            state.needReset = true;
+          }, 1000);
+        }
+      }
+    }
+
+    /**
+     * This function is called on kernel's `execute_input` message to start
+     * the elapsed time counter.
+     *
+     * @param  nb - The notebook which contains the scheduled execution request.
+     */
+    private _startTimer(nb: Notebook) {
+      const state = this._notebookExecutionProgress.get(nb);
+      if (state) {
+        if (state.executionStatus !== 'busy') {
+          state.executionStatus = 'busy';
+          clearTimeout(state.timeout);
+          this.stateChanged.emit(void 0);
+          state.interval = window.setInterval(() => {
+            this._tick(state);
+          }, 1000);
+        }
+      }
+    }
+
+    /**
+     * The function is called on kernel's `execute_request` message or Comm message, it is
+     * used to keep track number of scheduled cell or Comm execution message
+     * and the status of kernel.
+     *
+     * @param  nb - The notebook which contains the scheduled code.
+     * cell
+     * @param  msg_id - The id of message.
+     */
+    private _cellScheduledCallback(nb: Notebook, msg_id: string): void {
+      const state = this._notebookExecutionProgress.get(nb);
+
+      if (state && !state.scheduledCell.has(msg_id)) {
+        if (state.needReset) {
+          this._resetTime(state);
+        }
+        state.scheduledCell.add(msg_id);
+        state.scheduledCellNumber += 1;
+      }
+    }
+
+    /**
+     * Increment the executed time of input execution state
+     * and emit `stateChanged` signal to re-render the indicator.
+     *
+     * @param  data - the state to be updated.
+     */
+    private _tick(data: Private.IExecutionState): void {
+      data.totalTime += 1;
+      this.stateChanged.emit(void 0);
+    }
+
+    /**
+     * Reset the input execution state.
+     *
+     * @param  data - the state to be rested.
+     */
+    private _resetTime(data: Private.IExecutionState): void {
+      data.totalTime = 0;
+      data.scheduledCellNumber = 0;
+      data.executionStatus = 'idle';
+      data.scheduledCell = new Set<string>();
+      clearTimeout(data.timeout);
+      clearInterval(data.interval);
+      data.needReset = false;
+    }
+
+    get renderFlag(): boolean {
+      return this._renderFlag;
+    }
+
+    public updateRenderOption(options: {
+      showOnToolBar: boolean;
+      showProgress: boolean;
+    }): void {
+      if (this.displayOption.showOnToolBar) {
+        if (!options.showOnToolBar) {
+          this._renderFlag = false;
+        } else {
+          this._renderFlag = true;
+        }
+      }
+      this.displayOption.showProgress = options.showProgress;
+      this.stateChanged.emit(void 0);
+    }
+
+    /**
+     * The option to show the indicator on status bar or toolbar.
+     */
+    private _displayOption: Private.DisplayOption;
+
+    /**
+     * Current activated notebook.
+     */
+    private _currentNotebook: Notebook;
+
+    /**
+     * A weak map to hold execution status of multiple notebooks.
+     */
+    private _notebookExecutionProgress = new WeakMap<
+      Notebook,
+      Private.IExecutionState
+    >();
+
+    /**
+     * A flag to show or hide the indicator.
+     */
+    private _renderFlag: boolean;
+  }
+
+  export function createExecutionIndicatorItem(
+    panel: NotebookPanel,
+    translator: ITranslator,
+    loadSettings: Promise<ISettingRegistry.ISettings> | undefined
+  ): Widget {
+    const toolbarItem = new ExecutionIndicator(translator);
+    toolbarItem.model.displayOption = {
+      showOnToolBar: true,
+      showProgress: true
+    };
+    toolbarItem.model.attachNotebook({
+      content: panel.content,
+      context: panel.sessionContext
+    });
+
+    panel.disposed.connect(() => {
+      toolbarItem.dispose();
+    });
+    if (loadSettings) {
+      loadSettings
+        .then(settings => {
+          toolbarItem.model.updateRenderOption(getSettingValue(settings));
+          settings.changed.connect(newSettings => {
+            toolbarItem.model.updateRenderOption(getSettingValue(newSettings));
+          });
+        })
+        .catch((reason: Error) => {
+          console.error(reason.message);
+        });
+    }
+    return toolbarItem;
+  }
+
+  export function getSettingValue(
+    settings: ISettingRegistry.ISettings
+  ): { showOnToolBar: boolean; showProgress: boolean } {
+    let showOnToolBar = true;
+    let showProgress = true;
+    const configValues = settings.get('kernelStatus').composite as JSONObject;
+    if (configValues) {
+      showOnToolBar = !(configValues.showOnStatusBar as boolean);
+      showProgress = configValues.showProgress as boolean;
+    }
+
+    return { showOnToolBar, showProgress };
+  }
+}
+
+/**
+ * A namespace for module-private data.
+ */
+namespace Private {
+  export interface IExecutionState {
+    /**
+     * Execution status of kernel, this status is deducted from the
+     * number of scheduled code cells.
+     */
+    executionStatus: string;
+
+    /**
+     * Current status of kernel.
+     */
+    kernelStatus: ISessionContext.KernelDisplayStatus;
+
+    /**
+     * Total execution time.
+     */
+    totalTime: number;
+
+    /**
+     * Id of `setInterval`, it is used to start / stop the elapsed time
+     * counter.
+     */
+    interval: number;
+
+    /**
+     * Id of `setTimeout`, it is used to create / clear the state
+     * resetting request.
+     */
+    timeout: number;
+
+    /**
+     * Set of messages scheduled for executing, `executionStatus` is set
+     *  to `idle if the length of this set is 0 and to `busy` otherwise.
+     */
+    scheduledCell: Set<string>;
+
+    /**
+     * Total number of cells requested for executing, it is used to compute
+     * the execution progress in progress bar.
+     */
+    scheduledCellNumber: number;
+
+    /**
+     * Flag to reset the execution state when a code cell is scheduled for
+     * executing.
+     */
+    needReset: boolean;
+  }
+
+  export type DisplayOption = {
+    /**
+     * The option to show the indicator on status bar or toolbar.
+     */
+    showOnToolBar: boolean;
+
+    /**
+     * The option to show the execution progress inside kernel
+     * status circle.
+     */
+    showProgress: boolean;
+  };
+}

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

@@ -17,3 +17,4 @@ export * from './tracker';
 export * from './truststatus';
 export * from './widget';
 export * from './widgetfactory';
+export * from './executionindicator';

+ 1 - 0
packages/notebook/style/base.css

@@ -19,6 +19,7 @@
 |----------------------------------------------------------------------------*/
 
 @import './toolbar.css';
+@import './executionindicator.css';
 
 /*-----------------------------------------------------------------------------
 | Notebook

+ 65 - 0
packages/notebook/style/executionindicator.css

@@ -0,0 +1,65 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/*-----------------------------------------------------------------------------
+| Variables
+|----------------------------------------------------------------------------*/
+
+/*-----------------------------------------------------------------------------
+
+/*-----------------------------------------------------------------------------
+| Styles
+|----------------------------------------------------------------------------*/
+
+.jp-Notebook-ExecutionIndicator {
+  position: relative;
+  display: inline-block;
+  height: 100%;
+  z-index: 9997;
+}
+
+.jp-Notebook-ExecutionIndicator-tooltip {
+  visibility: hidden;
+  height: auto;
+  width: fit-content;
+  width: -moz-fit-content;
+  background-color: var(--jp-layout-color2);
+  color: var(--jp-ui-font-color1);
+  text-align: justify;
+  border-radius: 6px;
+  padding: 0 5px;
+  position: fixed;
+  display: table;
+}
+
+.jp-Notebook-ExecutionIndicator-tooltip.up {
+  transform: translateX(-50%) translateY(-100%) translateY(-32px);
+}
+
+.jp-Notebook-ExecutionIndicator-tooltip.down {
+  transform: translateX(calc(-100% + 16px)) translateY(5px);
+}
+
+.jp-Notebook-ExecutionIndicator-tooltip.hidden {
+  display: none;
+}
+
+.jp-Notebook-ExecutionIndicator:hover .jp-Notebook-ExecutionIndicator-tooltip {
+  visibility: visible;
+}
+
+.jp-Notebook-ExecutionIndicator span {
+  font-size: var(--jp-ui-font-size1);
+  font-family: var(--jp-ui-font-family);
+  color: var(--jp-ui-font-color1);
+  line-height: 24px;
+  display: block;
+}
+
+.jp-Notebook-ExecutionIndicator-progress-bar {
+  display: flex;
+  justify-content: center;
+  height: 100%;
+}

+ 1 - 2
packages/notebook/test/default-toolbar.spec.ts

@@ -214,8 +214,7 @@ describe('@jupyterlab/notebook', () => {
             'restart-and-run',
             'cellType',
             'spacer',
-            'kernelName',
-            'kernelStatus'
+            'kernelName'
           ]);
         });
       });

+ 250 - 0
packages/notebook/test/executionindicator.spec.tsx

@@ -0,0 +1,250 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { ISessionContext, SessionContext } from '@jupyterlab/apputils';
+import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
+import { createSessionContext } from '@jupyterlab/testutils';
+import { JupyterServer } from '@jupyterlab/testutils/lib/start_jupyter_server';
+import {
+  ExecutionIndicator,
+  ExecutionIndicatorComponent,
+  Notebook,
+  NotebookActions,
+  NotebookModel
+} from '..';
+import * as utils from './utils';
+import React from 'react';
+import ReactDOMServer from 'react-dom/server';
+
+const fastCellModel = {
+  cell_type: 'code',
+  execution_count: 1,
+  metadata: { tags: [] },
+  outputs: [],
+  source: ['print("hello")\n']
+};
+
+const slowCellModel = {
+  cell_type: 'code',
+  execution_count: 1,
+  metadata: { tags: [] },
+  outputs: [],
+  source: ['import time\n', 'time.sleep(3)\n']
+};
+
+const server = new JupyterServer();
+
+beforeAll(async () => {
+  jest.setTimeout(20000);
+  await server.start();
+});
+
+afterAll(async () => {
+  await server.shutdown();
+});
+
+describe('@jupyterlab/notebook', () => {
+  let rendermime: IRenderMimeRegistry;
+
+  describe('ExecutionIndicator', () => {
+    let widget: Notebook;
+    let sessionContext: ISessionContext;
+    let ipySessionContext: ISessionContext;
+    let indicator: ExecutionIndicator;
+    beforeAll(async function () {
+      jest.setTimeout(20000);
+      rendermime = utils.defaultRenderMime();
+
+      async function createContext(options?: Partial<SessionContext.IOptions>) {
+        const context = await createSessionContext(options);
+        await context.initialize();
+        await context.session?.kernel?.info;
+        return context;
+      }
+      [sessionContext, ipySessionContext] = await Promise.all([
+        createContext(),
+        createContext({ kernelPreference: { name: 'ipython' } })
+      ]);
+    });
+
+    beforeEach(async () => {
+      widget = new Notebook({
+        rendermime,
+        contentFactory: utils.createNotebookFactory(),
+        mimeTypeService: utils.mimeTypeService
+      });
+      const model = new NotebookModel();
+      const modelJson = {
+        ...utils.DEFAULT_CONTENT,
+        cells: [fastCellModel, slowCellModel, slowCellModel, fastCellModel]
+      };
+
+      model.fromJSON(modelJson);
+
+      widget.model = model;
+      model.sharedModel.clearUndoHistory();
+
+      widget.activeCellIndex = 0;
+      for (let idx = 0; idx < widget.widgets.length; idx++) {
+        widget.select(widget.widgets[idx]);
+      }
+      indicator = new ExecutionIndicator();
+      indicator.model.attachNotebook({
+        content: widget,
+        context: ipySessionContext
+      });
+      await ipySessionContext.restartKernel();
+    });
+
+    afterEach(() => {
+      widget.dispose();
+      utils.clipboard.clear();
+      indicator.dispose();
+    });
+
+    afterAll(async () => {
+      await Promise.all([
+        sessionContext.shutdown(),
+        ipySessionContext.shutdown()
+      ]);
+    });
+
+    describe('executedAllCell', () => {
+      it('should count correctly number of scheduled cell', async () => {
+        let scheduledCell: number | undefined = 0;
+
+        indicator.model.stateChanged.connect(state => {
+          scheduledCell = state.executionState(widget)!.scheduledCellNumber;
+        });
+
+        await NotebookActions.run(widget, ipySessionContext);
+        expect(scheduledCell).toBe(4);
+      });
+
+      it('should count correctly elapsed time', async () => {
+        let elapsedTime: number | undefined = 0;
+
+        indicator.model.stateChanged.connect(state => {
+          elapsedTime = state.executionState(widget)!.totalTime;
+        });
+
+        await NotebookActions.run(widget, ipySessionContext);
+        expect(elapsedTime).toBeGreaterThanOrEqual(6);
+      });
+
+      it('should tick every second', async () => {
+        let tick: Array<number> = [];
+
+        indicator.model.stateChanged.connect(state => {
+          tick.push(state.executionState(widget)!.totalTime);
+        });
+
+        await NotebookActions.run(widget, ipySessionContext);
+        expect(tick).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6, 6]));
+      });
+
+      it('should count correctly number of executed requests', async () => {
+        let executed: Array<number> = [];
+
+        indicator.model.stateChanged.connect(state => {
+          executed.push(state.executionState(widget)!.scheduledCell.size);
+        });
+
+        await NotebookActions.run(widget, ipySessionContext);
+        expect(executed).toEqual(expect.arrayContaining([3, 3, 3, 2, 2, 2, 0]));
+      });
+    });
+  });
+  describe('testProgressCircle', () => {
+    let displayOption: { showOnToolBar: boolean; showProgress: boolean };
+    let defaultState: {
+      interval: number;
+      kernelStatus: ISessionContext.KernelDisplayStatus;
+      executionStatus: string;
+      needReset: boolean;
+      scheduledCell: Set<string>;
+      scheduledCellNumber: number;
+      timeout: number;
+      totalTime: number;
+    };
+
+    const EMPTY_CIRCLE = 'M 0 0 v -104 A 104 104 1 0 0 -0.0000 -104.0000 z';
+    const HALF_FILLED_CIRCLE = 'M 0 0 v -104 A 104 104 1 0 0 0.0000 104.0000 z';
+    const FILLED_CIRCLE = 'M 0 0 v -104 A 104 104 1 1 0 0.1815 -103.9998 z';
+
+    beforeEach(() => {
+      displayOption = { showOnToolBar: false, showProgress: true };
+      defaultState = {
+        interval: 0,
+        kernelStatus: 'idle',
+        executionStatus: 'idle',
+        needReset: false,
+        scheduledCell: new Set<string>(),
+        scheduledCellNumber: 0,
+        timeout: 0,
+        totalTime: 0
+      };
+    });
+    it('Should render an empty div with undefined state', () => {
+      const element = (
+        <ExecutionIndicatorComponent
+          displayOption={displayOption}
+          state={undefined}
+          translator={undefined}
+        />
+      );
+      const htmlElement = ReactDOMServer.renderToString(element);
+      expect(htmlElement).toContain('<div data-reactroot=""></div>');
+    });
+    it('Should render a filled circle with 0/2 cell executed message', () => {
+      defaultState.scheduledCellNumber = 2;
+      defaultState.scheduledCell.add('foo');
+      defaultState.scheduledCell.add('bar');
+      defaultState.executionStatus = 'busy';
+      defaultState.totalTime = 1;
+      const element = (
+        <ExecutionIndicatorComponent
+          displayOption={displayOption}
+          state={defaultState}
+          translator={undefined}
+        />
+      );
+      const htmlElement = ReactDOMServer.renderToString(element);
+      expect(htmlElement).toContain(FILLED_CIRCLE);
+      expect(htmlElement).toContain(`Executed 0/2 requests`);
+    });
+
+    it('Should render a half filled circle with 1/2 cell executed message', () => {
+      defaultState.scheduledCellNumber = 2;
+      defaultState.scheduledCell.add('foo');
+      defaultState.executionStatus = 'busy';
+      defaultState.totalTime = 1;
+      const element = (
+        <ExecutionIndicatorComponent
+          displayOption={displayOption}
+          state={defaultState}
+          translator={undefined}
+        />
+      );
+      const htmlElement = ReactDOMServer.renderToString(element);
+      expect(htmlElement).toContain(HALF_FILLED_CIRCLE);
+      expect(htmlElement).toContain(`Executed 1/2 requests`);
+    });
+
+    it('Should render an empty circle with 2 requests executed message', () => {
+      defaultState.scheduledCellNumber = 2;
+      defaultState.executionStatus = 'idle';
+      defaultState.totalTime = 1;
+      const element = (
+        <ExecutionIndicatorComponent
+          displayOption={displayOption}
+          state={defaultState}
+          translator={undefined}
+        />
+      );
+      const htmlElement = ReactDOMServer.renderToString(element);
+      expect(htmlElement).toContain(EMPTY_CIRCLE);
+      expect(htmlElement).toContain(`Executed 2 requests`);
+    });
+  });
+});

+ 0 - 1
packages/notebook/test/widgetfactory.spec.ts

@@ -104,7 +104,6 @@ describe('@jupyterlab/notebook', () => {
         const items = toArray(panel.toolbar.names());
         expect(items).toEqual(expect.arrayContaining(['save']));
         expect(items).toEqual(expect.arrayContaining(['restart']));
-        expect(items).toEqual(expect.arrayContaining(['kernelStatus']));
       });
 
       it('should populate the customized toolbar items', () => {

+ 3 - 0
packages/notebook/tsconfig.json

@@ -33,6 +33,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../shared-models"
     },

+ 6 - 0
packages/notebook/tsconfig.test.json

@@ -29,6 +29,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../shared-models"
     },
@@ -74,6 +77,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../shared-models"
     },

+ 1 - 0
packages/statusbar/src/components/index.ts

@@ -5,3 +5,4 @@ export * from './group';
 export * from './hover';
 export * from './progressBar';
 export * from './text';
+export * from './progressCircle';

+ 28 - 7
packages/statusbar/src/components/progressBar.tsx

@@ -2,7 +2,6 @@
 // Distributed under the terms of the Modified BSD License.
 
 import * as React from 'react';
-import { fillerItem, progressBarItem } from '../style/progressBar';
 
 /**
  * A namespace for ProgressBar statics.
@@ -16,16 +15,27 @@ export namespace ProgressBar {
      * The current progress percentage, from 0 to 100
      */
     percentage: number;
+
+    /**
+     * Width of progress bar in pixel.
+     */
+    width?: number;
+
+    /**
+     * Text to show inside progress bar.
+     */
+    content?: string;
   }
 }
 
 /**
  * A functional tsx component for a progress bar.
  */
-export function ProgressBar(props: ProgressBar.IProps) {
+export function ProgressBar(props: ProgressBar.IProps): JSX.Element {
+  const { width, ...rest } = props;
   return (
-    <div className={progressBarItem}>
-      <Filler percentage={props.percentage} />
+    <div className={'jp-Statusbar-ProgressBar-progress-bar'}>
+      <Filler {...rest} contentWidth={width} />
     </div>
   );
 }
@@ -42,6 +52,16 @@ namespace Filler {
      * The current percentage filled, from 0 to 100
      */
     percentage: number;
+
+    /**
+     * Width of content inside filler.
+     */
+    contentWidth?: number;
+
+    /**
+     * Text to show inside filler.
+     */
+    content?: string;
   }
 }
 
@@ -51,10 +71,11 @@ namespace Filler {
 function Filler(props: Filler.IProps) {
   return (
     <div
-      className={fillerItem}
       style={{
-        width: `${props.percentage}px`
+        width: `${props.percentage}%`
       }}
-    />
+    >
+      <p>{props.content}</p>
+    </div>
   );
 }

+ 55 - 0
packages/statusbar/src/components/progressCircle.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+export namespace ProgressCircle {
+  /**
+   * Props for the ProgressBar.
+   */
+  export interface IProps {
+    /**
+     * The current progress percentage, from 0 to 100
+     */
+    progress: number;
+
+    width?: number;
+
+    height?: number;
+  }
+}
+
+export function ProgressCircle(props: ProgressCircle.IProps): JSX.Element {
+  const radius = 104;
+  const d = (progress: number): string => {
+    const angle = Math.max(progress * 3.6, 0.1);
+    const rad = (angle * Math.PI) / 180,
+      x = Math.sin(rad) * radius,
+      y = Math.cos(rad) * -radius,
+      mid = angle < 180 ? 1 : 0,
+      shape =
+        `M 0 0 v -${radius} A ${radius} ${radius} 1 ` +
+        mid +
+        ' 0 ' +
+        x.toFixed(4) +
+        ' ' +
+        y.toFixed(4) +
+        ' z';
+    return shape;
+  };
+  return (
+    <div className={'jp-Statusbar-ProgressCircle'}>
+      <svg viewBox="0 0 250 250">
+        <circle
+          cx="125"
+          cy="125"
+          r={`${radius}`}
+          stroke="var(--jp-inverse-layout-color3)"
+          strokeWidth="20"
+          fill="none"
+        />
+        <path
+          transform="translate(125,125) scale(.9)"
+          d={d(props.progress)}
+          fill={'var(--jp-inverse-layout-color3)'}
+        />
+      </svg>
+    </div>
+  );
+}

+ 7 - 17
packages/statusbar/src/defaults/kernelStatus.tsx

@@ -1,7 +1,12 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { ISessionContext, VDomModel, VDomRenderer } from '@jupyterlab/apputils';
+import {
+  ISessionContext,
+  translateKernelStatuses,
+  VDomModel,
+  VDomRenderer
+} from '@jupyterlab/apputils';
 import { Session } from '@jupyterlab/services';
 import {
   ITranslator,
@@ -119,22 +124,7 @@ export namespace KernelStatus {
       translator = translator || nullTranslator;
       this._trans = translator.load('jupyterlab');
       this._kernelName = this._trans.__('No Kernel!');
-      // TODO-FIXME: this mapping is duplicated in apputils/toolbar.tsx
-      this._statusNames = {
-        unknown: this._trans.__('Unknown'),
-        starting: this._trans.__('Starting'),
-        idle: this._trans.__('Idle'),
-        busy: this._trans.__('Busy'),
-        terminating: this._trans.__('Terminating'),
-        restarting: this._trans.__('Restarting'),
-        autorestarting: this._trans.__('Autorestarting'),
-        dead: this._trans.__('Dead'),
-        connected: this._trans.__('Connected'),
-        connecting: this._trans.__('Connecting'),
-        disconnected: this._trans.__('Disconnected'),
-        initializing: this._trans.__('Initializing'),
-        '': ''
-      };
+      this._statusNames = translateKernelStatuses(translator);
     }
 
     /**

+ 0 - 19
packages/statusbar/src/style/progressBar.ts

@@ -1,19 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import { style } from 'typestyle/lib';
-
-export const progressBarItem = style({
-  background: 'black',
-  height: '10px',
-  width: '100px',
-  border: '1px solid black',
-  borderRadius: '3px',
-  marginLeft: '4px',
-  overflow: 'hidden'
-});
-
-export const fillerItem = style({
-  background: 'var(--jp-brand-color2)',
-  height: '10px'
-});

+ 73 - 0
packages/statusbar/style/base.css

@@ -0,0 +1,73 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/*-----------------------------------------------------------------------------
+| Variables
+|----------------------------------------------------------------------------*/
+
+/*-----------------------------------------------------------------------------
+
+/*-----------------------------------------------------------------------------
+| Styles
+|----------------------------------------------------------------------------*/
+
+.jp-Statusbar-ProgressCircle svg {
+  display: block;
+  margin: 0 auto;
+  width: 16px;
+  height: 24px;
+  align-self: normal;
+}
+.jp-Statusbar-ProgressCircle path {
+  fill: var(--jp-inverse-layout-color3);
+}
+
+.jp-Statusbar-ProgressBar-progress-bar {
+  height: 10px;
+  width: 100px;
+  border: solid 0.25px var(--jp-brand-color2);
+  border-radius: 3px;
+  overflow: hidden;
+  align-self: center;
+}
+.jp-Statusbar-ProgressBar-progress-bar > div {
+  background-color: var(--jp-brand-color2);
+  background-image: linear-gradient(
+    -45deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-size: 40px 40px;
+  float: left;
+  width: 0%;
+  height: 100%;
+  font-size: 12px;
+  line-height: 14px;
+  color: #ffffff;
+  text-align: center;
+  animation: jp-Statusbar-ExecutionTime-progress-bar 2s linear infinite;
+}
+
+.jp-Statusbar-ProgressBar-progress-bar p {
+  color: var(--jp-ui-font-color1);
+  font-family: var(--jp-ui-font-family);
+  font-size: var(--jp-ui-font-size1);
+  line-height: 10px;
+  width: 100px;
+}
+
+@keyframes jp-Statusbar-ExecutionTime-progress-bar {
+  0% {
+    background-position: 0 0;
+  }
+  100% {
+    background-position: 40px 40px;
+  }
+}

+ 2 - 0
packages/statusbar/style/index.css

@@ -8,3 +8,5 @@
 @import url('~@jupyterlab/ui-components/style/index.css');
 @import url('~@jupyterlab/apputils/style/index.css');
 @import url('~@jupyterlab/codeeditor/style/index.css');
+
+@import url('./base.css');

+ 2 - 0
packages/statusbar/style/index.js

@@ -8,3 +8,5 @@ import '@lumino/widgets/style/index.js';
 import '@jupyterlab/ui-components/style/index.js';
 import '@jupyterlab/apputils/style/index.js';
 import '@jupyterlab/codeeditor/style/index.js';
+
+import './base.css';