UNPKG

19.4 kBTypeScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { ISessionContext, translateKernelStatuses } from '@jupyterlab/apputils';
5
6import { ITranslator, nullTranslator } from '@jupyterlab/translation';
7import React from 'react';
8import { ProgressCircle } from '@jupyterlab/statusbar';
9
10import {
11 circleIcon,
12 LabIcon,
13 offlineBoltIcon,
14 VDomModel,
15 VDomRenderer
16} from '@jupyterlab/ui-components';
17
18import { Notebook } from './widget';
19import { KernelMessage } from '@jupyterlab/services';
20import {
21 IAnyMessageArgs,
22 IKernelConnection
23} from '@jupyterlab/services/src/kernel/kernel';
24import { NotebookPanel } from './panel';
25import { ISettingRegistry } from '@jupyterlab/settingregistry';
26import { Widget } from '@lumino/widgets';
27import { JSONObject } from '@lumino/coreutils';
28import { IChangedArgs } from '@jupyterlab/coreutils';
29
30/**
31 * A react functional component for rendering execution indicator.
32 */
33export function ExecutionIndicatorComponent(
34 props: ExecutionIndicatorComponent.IProps
35): React.ReactElement<ExecutionIndicatorComponent.IProps> {
36 const translator = props.translator || nullTranslator;
37 const kernelStatuses = translateKernelStatuses(translator);
38 const trans = translator.load('jupyterlab');
39
40 const state = props.state;
41 const showOnToolBar = props.displayOption.showOnToolBar;
42 const showProgress = props.displayOption.showProgress;
43 const tooltipClass = showOnToolBar ? 'down' : 'up';
44 const emptyDiv = <div></div>;
45
46 if (!state) {
47 return emptyDiv;
48 }
49
50 const kernelStatus = state.kernelStatus;
51 const circleIconProps: LabIcon.IProps = {
52 alignSelf: 'normal',
53 height: '24px'
54 };
55 const time = state.totalTime;
56
57 const scheduledCellNumber = state.scheduledCellNumber || 0;
58 const remainingCellNumber = state.scheduledCell.size || 0;
59 const executedCellNumber = scheduledCellNumber - remainingCellNumber;
60 let percentage = (100 * executedCellNumber) / scheduledCellNumber;
61 let displayClass = showProgress ? '' : 'hidden';
62 if (!showProgress && percentage < 100) {
63 percentage = 0;
64 }
65
66 const progressBar = (percentage: number) => (
67 <ProgressCircle
68 progress={percentage}
69 width={16}
70 height={24}
71 label={trans.__('Kernel status')}
72 />
73 );
74 const titleFactory = (translatedStatus: string) =>
75 trans.__('Kernel status: %1', translatedStatus);
76
77 const reactElement = (
78 status: ISessionContext.KernelDisplayStatus,
79 circle: JSX.Element,
80 popup: JSX.Element[]
81 ): JSX.Element => (
82 <div
83 className={'jp-Notebook-ExecutionIndicator'}
84 title={showProgress ? '' : titleFactory(kernelStatuses[status])}
85 data-status={status}
86 >
87 {circle}
88 <div
89 className={`jp-Notebook-ExecutionIndicator-tooltip ${tooltipClass} ${displayClass}`}
90 >
91 <span> {titleFactory(kernelStatuses[status])} </span>
92 {popup}
93 </div>
94 </div>
95 );
96
97 if (
98 state.kernelStatus === 'connecting' ||
99 state.kernelStatus === 'disconnected' ||
100 state.kernelStatus === 'unknown'
101 ) {
102 return reactElement(
103 kernelStatus,
104 <offlineBoltIcon.react {...circleIconProps} />,
105 []
106 );
107 }
108 if (
109 state.kernelStatus === 'starting' ||
110 state.kernelStatus === 'terminating' ||
111 state.kernelStatus === 'restarting' ||
112 state.kernelStatus === 'initializing'
113 ) {
114 return reactElement(
115 kernelStatus,
116 <circleIcon.react {...circleIconProps} />,
117 []
118 );
119 }
120
121 if (state.executionStatus === 'busy') {
122 return reactElement('busy', progressBar(percentage), [
123 <span key={0}>
124 {trans.__(
125 `Executed ${executedCellNumber}/${scheduledCellNumber} cells`
126 )}
127 </span>,
128 <span key={1}>
129 {trans._n('Elapsed time: %1 second', 'Elapsed time: %1 seconds', time)}
130 </span>
131 ]);
132 } else {
133 // No cell is scheduled, fall back to the status of kernel
134 const progress = state.kernelStatus === 'busy' ? 0 : 100;
135 const popup =
136 state.kernelStatus === 'busy' || time === 0
137 ? []
138 : [
139 <span key={0}>
140 {trans._n(
141 'Executed %1 cell',
142 'Executed %1 cells',
143 scheduledCellNumber
144 )}
145 </span>,
146 <span key={1}>
147 {trans._n(
148 'Elapsed time: %1 second',
149 'Elapsed time: %1 seconds',
150 time
151 )}
152 </span>
153 ];
154
155 return reactElement(state.kernelStatus, progressBar(progress), popup);
156 }
157}
158
159/**
160 * A namespace for ExecutionIndicatorComponent statics.
161 */
162namespace ExecutionIndicatorComponent {
163 /**
164 * Props for the execution status component.
165 */
166 export interface IProps {
167 /**
168 * Display option for progress bar and elapsed time.
169 */
170 displayOption: Private.DisplayOption;
171
172 /**
173 * Execution state of selected notebook.
174 */
175 state?: ExecutionIndicator.IExecutionState;
176
177 /**
178 * The application language translator.
179 */
180 translator?: ITranslator;
181 }
182}
183
184/**
185 * A VDomRenderer widget for displaying the execution status.
186 */
187export class ExecutionIndicator extends VDomRenderer<ExecutionIndicator.Model> {
188 /**
189 * Construct the kernel status widget.
190 */
191 constructor(translator?: ITranslator, showProgress: boolean = true) {
192 super(new ExecutionIndicator.Model());
193 this.translator = translator || nullTranslator;
194 this.addClass('jp-mod-highlighted');
195 }
196
197 /**
198 * Render the execution status item.
199 */
200 render(): JSX.Element | null {
201 if (this.model === null || !this.model.renderFlag) {
202 return <div></div>;
203 } else {
204 const nb = this.model.currentNotebook;
205
206 if (!nb) {
207 return (
208 <ExecutionIndicatorComponent
209 displayOption={this.model.displayOption}
210 state={undefined}
211 translator={this.translator}
212 />
213 );
214 }
215
216 return (
217 <ExecutionIndicatorComponent
218 displayOption={this.model.displayOption}
219 state={this.model.executionState(nb)}
220 translator={this.translator}
221 />
222 );
223 }
224 }
225
226 private translator: ITranslator;
227}
228
229/**
230 * A namespace for ExecutionIndicator statics.
231 */
232export namespace ExecutionIndicator {
233 /**
234 * Execution state of a notebook.
235 */
236 export interface IExecutionState {
237 /**
238 * Execution status of kernel, this status is deducted from the
239 * number of scheduled code cells.
240 */
241 executionStatus: string;
242
243 /**
244 * Current status of kernel.
245 */
246 kernelStatus: ISessionContext.KernelDisplayStatus;
247
248 /**
249 * Total execution time.
250 */
251 totalTime: number;
252
253 /**
254 * Id of `setInterval`, it is used to start / stop the elapsed time
255 * counter.
256 */
257 interval: number;
258
259 /**
260 * Id of `setTimeout`, it is used to create / clear the state
261 * resetting request.
262 */
263 timeout: number;
264
265 /**
266 * Set of messages scheduled for executing, `executionStatus` is set
267 * to `idle if the length of this set is 0 and to `busy` otherwise.
268 */
269 scheduledCell: Set<string>;
270
271 /**
272 * Total number of cells requested for executing, it is used to compute
273 * the execution progress in progress bar.
274 */
275 scheduledCellNumber: number;
276
277 /**
278 * Flag to reset the execution state when a code cell is scheduled for
279 * executing.
280 */
281 needReset: boolean;
282 }
283
284 /**
285 * A VDomModel for the execution status indicator.
286 */
287 export class Model extends VDomModel {
288 constructor() {
289 super();
290 this._displayOption = { showOnToolBar: true, showProgress: true };
291 this._renderFlag = true;
292 }
293
294 /**
295 * Attach a notebook with session context to model in order to keep
296 * track of multiple notebooks. If a session context is already
297 * attached, only set current activated notebook to input.
298 *
299 * @param data - The notebook and session context to be attached to model
300 */
301 attachNotebook(
302 data: { content?: Notebook; context?: ISessionContext } | null
303 ): void {
304 if (data && data.content && data.context) {
305 const nb = data.content;
306 const context = data.context;
307 this._currentNotebook = nb;
308 if (!this._notebookExecutionProgress.has(nb)) {
309 this._notebookExecutionProgress.set(nb, {
310 executionStatus: 'idle',
311 kernelStatus: 'idle',
312 totalTime: 0,
313 interval: 0,
314 timeout: 0,
315 scheduledCell: new Set<string>(),
316 scheduledCellNumber: 0,
317 needReset: true
318 });
319
320 const state = this._notebookExecutionProgress.get(nb);
321 const contextStatusChanged = (ctx: ISessionContext) => {
322 if (state) {
323 state.kernelStatus = ctx.kernelDisplayStatus;
324 }
325 this.stateChanged.emit(void 0);
326 };
327 context.statusChanged.connect(contextStatusChanged, this);
328
329 const contextConnectionStatusChanged = (ctx: ISessionContext) => {
330 if (state) {
331 state.kernelStatus = ctx.kernelDisplayStatus;
332 }
333 this.stateChanged.emit(void 0);
334 };
335 context.connectionStatusChanged.connect(
336 contextConnectionStatusChanged,
337 this
338 );
339
340 context.disposed.connect(ctx => {
341 ctx.connectionStatusChanged.disconnect(
342 contextConnectionStatusChanged,
343 this
344 );
345 ctx.statusChanged.disconnect(contextStatusChanged, this);
346 });
347 const handleKernelMsg = (
348 sender: IKernelConnection,
349 msg: IAnyMessageArgs
350 ) => {
351 const message = msg.msg;
352 const msgId = message.header.msg_id;
353
354 if (message.header.msg_type === 'execute_request') {
355 // A cell code is scheduled for executing
356 this._cellScheduledCallback(nb, msgId);
357 } else if (
358 KernelMessage.isStatusMsg(message) &&
359 message.content.execution_state === 'idle'
360 ) {
361 // Idle status message case.
362 const parentId = (message.parent_header as KernelMessage.IHeader)
363 .msg_id;
364 this._cellExecutedCallback(nb, parentId);
365 } else if (
366 KernelMessage.isStatusMsg(message) &&
367 message.content.execution_state === 'restarting'
368 ) {
369 this._restartHandler(nb);
370 } else if (message.header.msg_type === 'execute_input') {
371 // A cell code starts executing.
372 this._startTimer(nb);
373 }
374 };
375 context.session?.kernel?.anyMessage.connect(handleKernelMsg);
376 context.session?.kernel?.disposed.connect(kernel =>
377 kernel.anyMessage.disconnect(handleKernelMsg)
378 );
379 const kernelChangedSlot = (
380 _: ISessionContext,
381 kernelData: IChangedArgs<
382 IKernelConnection | null,
383 IKernelConnection | null,
384 'kernel'
385 >
386 ) => {
387 if (state) {
388 this._resetTime(state);
389 this.stateChanged.emit(void 0);
390 if (kernelData.newValue) {
391 kernelData.newValue.anyMessage.connect(handleKernelMsg);
392 }
393 }
394 };
395 context.kernelChanged.connect(kernelChangedSlot);
396 context.disposed.connect(ctx =>
397 ctx.kernelChanged.disconnect(kernelChangedSlot)
398 );
399 }
400 }
401 }
402
403 /**
404 * The current activated notebook in model.
405 */
406 get currentNotebook(): Notebook | null {
407 return this._currentNotebook;
408 }
409
410 /**
411 * The display options for progress bar and elapsed time.
412 */
413 get displayOption(): Private.DisplayOption {
414 return this._displayOption;
415 }
416
417 /**
418 * Set the display options for progress bar and elapsed time.
419 *
420 * @param options - Options to be used
421 */
422 set displayOption(options: Private.DisplayOption) {
423 this._displayOption = options;
424 }
425
426 /**
427 * Get the execution state associated with a notebook.
428 *
429 * @param nb - The notebook used to identify execution
430 * state.
431 *
432 * @returns - The associated execution state.
433 */
434 executionState(nb: Notebook): IExecutionState | undefined {
435 return this._notebookExecutionProgress.get(nb);
436 }
437
438 /**
439 * Schedule switch to idle status and clearing of the timer.
440 *
441 * ### Note
442 *
443 * To keep track of cells executed under 1 second,
444 * the execution state is marked as `needReset` 1 second after executing
445 * these cells. This `Timeout` will be cleared if there is any cell
446 * scheduled after that.
447 */
448 private _scheduleSwitchToIdle(state: IExecutionState) {
449 window.setTimeout(() => {
450 state.executionStatus = 'idle';
451 clearInterval(state.interval);
452 this.stateChanged.emit(void 0);
453 }, 150);
454 state.timeout = window.setTimeout(() => {
455 state.needReset = true;
456 }, 1000);
457 }
458
459 /**
460 * The function is called on kernel's idle status message.
461 * It is used to keep track of number of executed
462 * cells or Comm custom messages and the status of kernel.
463 *
464 * @param nb - The notebook which contains the executed code cell.
465 * @param msg_id - The id of message.
466 */
467 private _cellExecutedCallback(nb: Notebook, msg_id: string): void {
468 const state = this._notebookExecutionProgress.get(nb);
469 if (state && state.scheduledCell.has(msg_id)) {
470 state.scheduledCell.delete(msg_id);
471 if (state.scheduledCell.size === 0) {
472 this._scheduleSwitchToIdle(state);
473 }
474 }
475 }
476
477 /**
478 * The function is called on kernel's restarting status message.
479 * It is used to clear the state tracking the number of executed
480 * cells.
481 *
482 * @param nb - The notebook which contains the executed code cell.
483 */
484 private _restartHandler(nb: Notebook): void {
485 const state = this._notebookExecutionProgress.get(nb);
486 if (state) {
487 state.scheduledCell.clear();
488 this._scheduleSwitchToIdle(state);
489 }
490 }
491
492 /**
493 * This function is called on kernel's `execute_input` message to start
494 * the elapsed time counter.
495 *
496 * @param nb - The notebook which contains the scheduled execution request.
497 */
498 private _startTimer(nb: Notebook) {
499 const state = this._notebookExecutionProgress.get(nb);
500 if (!state) {
501 return;
502 }
503 if (state.scheduledCell.size > 0) {
504 if (state.executionStatus !== 'busy') {
505 state.executionStatus = 'busy';
506 clearTimeout(state.timeout);
507 this.stateChanged.emit(void 0);
508 state.interval = window.setInterval(() => {
509 this._tick(state);
510 }, 1000);
511 }
512 } else {
513 this._resetTime(state);
514 }
515 }
516
517 /**
518 * The function is called on kernel's `execute_request` message or Comm message, it is
519 * used to keep track number of scheduled cell or Comm execution message
520 * and the status of kernel.
521 *
522 * @param nb - The notebook which contains the scheduled code.
523 * cell
524 * @param msg_id - The id of message.
525 */
526 private _cellScheduledCallback(nb: Notebook, msg_id: string): void {
527 const state = this._notebookExecutionProgress.get(nb);
528
529 if (state && !state.scheduledCell.has(msg_id)) {
530 if (state.needReset) {
531 this._resetTime(state);
532 }
533 state.scheduledCell.add(msg_id);
534 state.scheduledCellNumber += 1;
535 }
536 }
537
538 /**
539 * Increment the executed time of input execution state
540 * and emit `stateChanged` signal to re-render the indicator.
541 *
542 * @param data - the state to be updated.
543 */
544 private _tick(data: IExecutionState): void {
545 data.totalTime += 1;
546 this.stateChanged.emit(void 0);
547 }
548
549 /**
550 * Reset the input execution state.
551 *
552 * @param data - the state to be rested.
553 */
554 private _resetTime(data: IExecutionState): void {
555 data.totalTime = 0;
556 data.scheduledCellNumber = 0;
557 data.executionStatus = 'idle';
558 data.scheduledCell = new Set<string>();
559 clearTimeout(data.timeout);
560 clearInterval(data.interval);
561 data.needReset = false;
562 }
563
564 get renderFlag(): boolean {
565 return this._renderFlag;
566 }
567
568 updateRenderOption(options: {
569 showOnToolBar: boolean;
570 showProgress: boolean;
571 }): void {
572 if (this.displayOption.showOnToolBar) {
573 if (!options.showOnToolBar) {
574 this._renderFlag = false;
575 } else {
576 this._renderFlag = true;
577 }
578 }
579 this.displayOption.showProgress = options.showProgress;
580 this.stateChanged.emit(void 0);
581 }
582
583 /**
584 * The option to show the indicator on status bar or toolbar.
585 */
586 private _displayOption: Private.DisplayOption;
587
588 /**
589 * Current activated notebook.
590 */
591 private _currentNotebook: Notebook;
592
593 /**
594 * A weak map to hold execution status of multiple notebooks.
595 */
596 private _notebookExecutionProgress = new WeakMap<
597 Notebook,
598 IExecutionState
599 >();
600
601 /**
602 * A flag to show or hide the indicator.
603 */
604 private _renderFlag: boolean;
605 }
606
607 export function createExecutionIndicatorItem(
608 panel: NotebookPanel,
609 translator?: ITranslator,
610 loadSettings?: Promise<ISettingRegistry.ISettings>
611 ): Widget {
612 const toolbarItem = new ExecutionIndicator(translator);
613 toolbarItem.model.displayOption = {
614 showOnToolBar: true,
615 showProgress: true
616 };
617 toolbarItem.model.attachNotebook({
618 content: panel.content,
619 context: panel.sessionContext
620 });
621
622 if (loadSettings) {
623 loadSettings
624 .then(settings => {
625 const updateSettings = (newSettings: ISettingRegistry.ISettings) => {
626 toolbarItem.model.updateRenderOption(getSettingValue(newSettings));
627 };
628 settings.changed.connect(updateSettings);
629 updateSettings(settings);
630 toolbarItem.disposed.connect(() => {
631 settings.changed.disconnect(updateSettings);
632 });
633 })
634 .catch((reason: Error) => {
635 console.error(reason.message);
636 });
637 }
638 return toolbarItem;
639 }
640
641 export function getSettingValue(settings: ISettingRegistry.ISettings): {
642 showOnToolBar: boolean;
643 showProgress: boolean;
644 } {
645 let showOnToolBar = true;
646 let showProgress = true;
647 const configValues = settings.get('kernelStatus').composite as JSONObject;
648 if (configValues) {
649 showOnToolBar = !(configValues.showOnStatusBar as boolean);
650 showProgress = configValues.showProgress as boolean;
651 }
652
653 return { showOnToolBar, showProgress };
654 }
655}
656
657/**
658 * A namespace for module-private data.
659 */
660namespace Private {
661 export type DisplayOption = {
662 /**
663 * The option to show the indicator on status bar or toolbar.
664 */
665 showOnToolBar: boolean;
666
667 /**
668 * The option to show the execution progress inside kernel
669 * status circle.
670 */
671 showProgress: boolean;
672 };
673}