UNPKG

9.27 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import {
5 Dialog,
6 ISessionContext,
7 Printing,
8 showDialog
9} from '@jupyterlab/apputils';
10import { isMarkdownCellModel } from '@jupyterlab/cells';
11import { PageConfig } from '@jupyterlab/coreutils';
12import { DocumentRegistry, DocumentWidget } from '@jupyterlab/docregistry';
13import { Kernel, KernelMessage, Session } from '@jupyterlab/services';
14import { ITranslator } from '@jupyterlab/translation';
15import { Token } from '@lumino/coreutils';
16import { INotebookModel } from './model';
17import { Notebook, StaticNotebook } from './widget';
18import { Message } from '@lumino/messaging';
19
20/**
21 * The class name added to notebook panels.
22 */
23const NOTEBOOK_PANEL_CLASS = 'jp-NotebookPanel';
24
25const NOTEBOOK_PANEL_TOOLBAR_CLASS = 'jp-NotebookPanel-toolbar';
26
27const NOTEBOOK_PANEL_NOTEBOOK_CLASS = 'jp-NotebookPanel-notebook';
28
29/**
30 * A widget that hosts a notebook toolbar and content area.
31 *
32 * #### Notes
33 * The widget keeps the document metadata in sync with the current
34 * kernel on the context.
35 */
36export class NotebookPanel extends DocumentWidget<Notebook, INotebookModel> {
37 /**
38 * Construct a new notebook panel.
39 */
40 constructor(options: DocumentWidget.IOptions<Notebook, INotebookModel>) {
41 super(options);
42
43 // Set up CSS classes
44 this.addClass(NOTEBOOK_PANEL_CLASS);
45 this.toolbar.addClass(NOTEBOOK_PANEL_TOOLBAR_CLASS);
46 this.content.addClass(NOTEBOOK_PANEL_NOTEBOOK_CLASS);
47
48 // Set up things related to the context
49 this.content.model = this.context.model;
50 this.context.sessionContext.kernelChanged.connect(
51 this._onKernelChanged,
52 this
53 );
54 this.context.sessionContext.statusChanged.connect(
55 this._onSessionStatusChanged,
56 this
57 );
58 // this.content.fullyRendered.connect(this._onFullyRendered, this);
59 this.context.saveState.connect(this._onSave, this);
60 void this.revealed.then(() => {
61 if (this.isDisposed) {
62 // this widget has already been disposed, bail
63 return;
64 }
65
66 // Set the document edit mode on initial open if it looks like a new document.
67 if (this.content.widgets.length === 1) {
68 const cellModel = this.content.widgets[0].model;
69 if (
70 cellModel.type === 'code' &&
71 cellModel.sharedModel.getSource() === ''
72 ) {
73 this.content.mode = 'edit';
74 }
75 }
76 });
77 }
78
79 /**
80 * Handle a change to the document registry save state.
81 *
82 * @param sender The document registry context
83 * @param state The document registry save state
84 */
85 private _onSave(
86 sender: DocumentRegistry.Context,
87 state: DocumentRegistry.SaveState
88 ): void {
89 if (state === 'started' && this.model) {
90 // Find markdown cells
91 for (const cell of this.model.cells) {
92 if (isMarkdownCellModel(cell)) {
93 for (const key of cell.attachments.keys) {
94 if (!cell.sharedModel.getSource().includes(key)) {
95 cell.attachments.remove(key);
96 }
97 }
98 }
99 }
100 }
101 }
102
103 /**
104 * The session context used by the panel.
105 */
106 get sessionContext(): ISessionContext {
107 return this.context.sessionContext;
108 }
109
110 /**
111 * The model for the widget.
112 */
113 get model(): INotebookModel | null {
114 return this.content.model;
115 }
116
117 /**
118 * Update the options for the current notebook panel.
119 *
120 * @param config new options to set
121 */
122 setConfig(config: NotebookPanel.IConfig): void {
123 this.content.editorConfig = config.editorConfig;
124 this.content.notebookConfig = config.notebookConfig;
125 // Update kernel shutdown behavior
126 const kernelPreference = this.context.sessionContext.kernelPreference;
127 this.context.sessionContext.kernelPreference = {
128 ...kernelPreference,
129 shutdownOnDispose: config.kernelShutdown,
130 autoStartDefault: config.autoStartDefault
131 };
132 }
133
134 /**
135 * Set URI fragment identifier.
136 */
137 setFragment(fragment: string): void {
138 void this.context.ready.then(() => {
139 void this.content.setFragment(fragment);
140 });
141 }
142
143 /**
144 * Dispose of the resources used by the widget.
145 */
146 dispose(): void {
147 this.content.dispose();
148 super.dispose();
149 }
150
151 /**
152 * Prints the notebook by converting to HTML with nbconvert.
153 */
154 [Printing.symbol]() {
155 return async (): Promise<void> => {
156 // Save before generating HTML
157 if (this.context.model.dirty && !this.context.model.readOnly) {
158 await this.context.save();
159 }
160
161 await Printing.printURL(
162 PageConfig.getNBConvertURL({
163 format: 'html',
164 download: false,
165 path: this.context.path
166 })
167 );
168 };
169 }
170
171 /**
172 * A message handler invoked on a 'before-hide' message.
173 */
174 protected onBeforeHide(msg: Message): void {
175 super.onBeforeHide(msg);
176 // Inform the windowed list that the notebook is gonna be hidden
177 this.content.isParentHidden = true;
178 }
179
180 /**
181 * A message handler invoked on a 'before-show' message.
182 */
183 protected onBeforeShow(msg: Message): void {
184 // Inform the windowed list that the notebook is gonna be shown
185 // Use onBeforeShow instead of onAfterShow to take into account
186 // resizing (like sidebars got expanded before switching to the notebook tab)
187 this.content.isParentHidden = false;
188 super.onBeforeShow(msg);
189 }
190
191 /**
192 * Handle a change in the kernel by updating the document metadata.
193 */
194 private _onKernelChanged(
195 sender: any,
196 args: Session.ISessionConnection.IKernelChangedArgs
197 ): void {
198 if (!this.model || !args.newValue) {
199 return;
200 }
201 const { newValue } = args;
202 void newValue.info.then(info => {
203 if (
204 this.model &&
205 this.context.sessionContext.session?.kernel === newValue
206 ) {
207 this._updateLanguage(info.language_info);
208 }
209 });
210 void this._updateSpec(newValue);
211 }
212
213 private _onSessionStatusChanged(
214 sender: ISessionContext,
215 status: Kernel.Status
216 ) {
217 // If the status is autorestarting, and we aren't already in a series of
218 // autorestarts, show the dialog.
219 if (status === 'autorestarting' && !this._autorestarting) {
220 // The kernel died and the server is restarting it. We notify the user so
221 // they know why their kernel state is gone.
222 void showDialog({
223 title: this._trans.__('Kernel Restarting'),
224 body: this._trans.__(
225 'The kernel for %1 appears to have died. It will restart automatically.',
226 this.sessionContext.session?.path
227 ),
228 buttons: [Dialog.okButton({ label: this._trans.__('Ok') })]
229 });
230 this._autorestarting = true;
231 } else if (status === 'restarting') {
232 // Another autorestart attempt will first change the status to
233 // restarting, then to autorestarting again, so we don't reset the
234 // autorestarting status if the status is 'restarting'.
235 /* no-op */
236 } else {
237 this._autorestarting = false;
238 }
239 }
240
241 /**
242 * Update the kernel language.
243 */
244 private _updateLanguage(language: KernelMessage.ILanguageInfo): void {
245 this.model!.setMetadata('language_info', language);
246 }
247
248 /**
249 * Update the kernel spec.
250 */
251 private async _updateSpec(kernel: Kernel.IKernelConnection): Promise<void> {
252 const spec = await kernel.spec;
253 if (this.isDisposed) {
254 return;
255 }
256 this.model!.setMetadata('kernelspec', {
257 name: kernel.name,
258 display_name: spec?.display_name,
259 language: spec?.language
260 });
261 }
262
263 /**
264 * Whether we are currently in a series of autorestarts we have already
265 * notified the user about.
266 */
267 private _autorestarting = false;
268 translator: ITranslator;
269}
270
271/**
272 * A namespace for `NotebookPanel` statics.
273 */
274export namespace NotebookPanel {
275 /**
276 * Notebook config interface for NotebookPanel
277 */
278 export interface IConfig {
279 /**
280 * Whether to automatically start the preferred kernel
281 */
282 autoStartDefault: boolean;
283 /**
284 * A config object for cell editors
285 */
286 editorConfig: StaticNotebook.IEditorConfig;
287 /**
288 * A config object for notebook widget
289 */
290 notebookConfig: StaticNotebook.INotebookConfig;
291 /**
292 * Whether to shut down the kernel when closing the panel or not
293 */
294 kernelShutdown: boolean;
295 }
296
297 /**
298 * A content factory interface for NotebookPanel.
299 */
300 export interface IContentFactory extends Notebook.IContentFactory {
301 /**
302 * Create a new content area for the panel.
303 */
304 createNotebook(options: Notebook.IOptions): Notebook;
305 }
306
307 /**
308 * The default implementation of an `IContentFactory`.
309 */
310 export class ContentFactory
311 extends Notebook.ContentFactory
312 implements IContentFactory
313 {
314 /**
315 * Create a new content area for the panel.
316 */
317 createNotebook(options: Notebook.IOptions): Notebook {
318 return new Notebook(options);
319 }
320 }
321
322 /**
323 * The notebook renderer token.
324 */
325 export const IContentFactory = new Token<IContentFactory>(
326 '@jupyterlab/notebook:IContentFactory',
327 `A factory object that creates new notebooks.
328 Use this if you want to create and host notebooks in your own UI elements.`
329 );
330}