@jupyterlab/apputils-extension 4.0.0-alpha.9 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,687 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+ /**
6
+ * @packageDocumentation
7
+ * @module apputils-extension
8
+ */
9
+
10
+ import {
11
+ ILayoutRestorer,
12
+ IRouter,
13
+ JupyterFrontEnd,
14
+ JupyterFrontEndPlugin
15
+ } from '@jupyterlab/application';
16
+ import {
17
+ Dialog,
18
+ ICommandPalette,
19
+ ISanitizer,
20
+ ISessionContextDialogs,
21
+ ISplashScreen,
22
+ IWindowResolver,
23
+ MainAreaWidget,
24
+ Printing,
25
+ Sanitizer,
26
+ SessionContextDialogs,
27
+ WindowResolver
28
+ } from '@jupyterlab/apputils';
29
+ import { PageConfig, PathExt, URLExt } from '@jupyterlab/coreutils';
30
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
31
+ import { IStateDB, StateDB } from '@jupyterlab/statedb';
32
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
33
+ import { jupyterFaviconIcon } from '@jupyterlab/ui-components';
34
+ import { PromiseDelegate } from '@lumino/coreutils';
35
+ import { DisposableDelegate } from '@lumino/disposable';
36
+ import { Debouncer, Throttler } from '@lumino/polling';
37
+ import { announcements } from './announcements';
38
+ import { notificationPlugin } from './notificationplugin';
39
+ import { Palette } from './palette';
40
+ import { settingsPlugin } from './settingsplugin';
41
+ import { kernelStatus, runningSessionsStatus } from './statusbarplugin';
42
+ import { themesPaletteMenuPlugin, themesPlugin } from './themesplugins';
43
+ import { toolbarRegistry } from './toolbarregistryplugin';
44
+ import { workspacesPlugin } from './workspacesplugin';
45
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
46
+
47
+ /**
48
+ * The interval in milliseconds before recover options appear during splash.
49
+ */
50
+ const SPLASH_RECOVER_TIMEOUT = 12000;
51
+
52
+ /**
53
+ * The command IDs used by the apputils plugin.
54
+ */
55
+ namespace CommandIDs {
56
+ export const loadState = 'apputils:load-statedb';
57
+
58
+ export const print = 'apputils:print';
59
+
60
+ export const reset = 'apputils:reset';
61
+
62
+ export const resetOnLoad = 'apputils:reset-on-load';
63
+
64
+ export const runFirstEnabled = 'apputils:run-first-enabled';
65
+
66
+ export const runAllEnabled = 'apputils:run-all-enabled';
67
+
68
+ export const toggleHeader = 'apputils:toggle-header';
69
+ }
70
+
71
+ /**
72
+ * The default command palette extension.
73
+ */
74
+ const palette: JupyterFrontEndPlugin<ICommandPalette> = {
75
+ id: '@jupyterlab/apputils-extension:palette',
76
+ autoStart: true,
77
+ requires: [ITranslator],
78
+ provides: ICommandPalette,
79
+ optional: [ISettingRegistry],
80
+ activate: (
81
+ app: JupyterFrontEnd,
82
+ translator: ITranslator,
83
+ settingRegistry: ISettingRegistry | null
84
+ ) => {
85
+ return Palette.activate(app, translator, settingRegistry);
86
+ }
87
+ };
88
+
89
+ /**
90
+ * The default command palette's restoration extension.
91
+ *
92
+ * #### Notes
93
+ * The command palette's restoration logic is handled separately from the
94
+ * command palette provider extension because the layout restorer dependency
95
+ * causes the command palette to be unavailable to other extensions earlier
96
+ * in the application load cycle.
97
+ */
98
+ const paletteRestorer: JupyterFrontEndPlugin<void> = {
99
+ id: '@jupyterlab/apputils-extension:palette-restorer',
100
+ autoStart: true,
101
+ requires: [ILayoutRestorer, ITranslator],
102
+ activate: (
103
+ app: JupyterFrontEnd,
104
+ restorer: ILayoutRestorer,
105
+ translator: ITranslator
106
+ ) => {
107
+ Palette.restore(app, restorer, translator);
108
+ }
109
+ };
110
+
111
+ /**
112
+ * The default window name resolver provider.
113
+ */
114
+ const resolver: JupyterFrontEndPlugin<IWindowResolver> = {
115
+ id: '@jupyterlab/apputils-extension:resolver',
116
+ autoStart: true,
117
+ provides: IWindowResolver,
118
+ requires: [JupyterFrontEnd.IPaths, IRouter],
119
+ activate: async (
120
+ app: JupyterFrontEnd,
121
+ paths: JupyterFrontEnd.IPaths,
122
+ router: IRouter
123
+ ) => {
124
+ const { hash, search } = router.current;
125
+ const query = URLExt.queryStringToObject(search || '');
126
+ const solver = new WindowResolver();
127
+ const workspace = PageConfig.getOption('workspace');
128
+ const treePath = PageConfig.getOption('treePath');
129
+ const mode =
130
+ PageConfig.getOption('mode') === 'multiple-document' ? 'lab' : 'doc';
131
+ // This is used as a key in local storage to refer to workspaces, either the name
132
+ // of the workspace or the string PageConfig.defaultWorkspace. Both lab and doc modes share the same workspace.
133
+ const candidate = workspace ? workspace : PageConfig.defaultWorkspace;
134
+ const rest = treePath ? URLExt.join('tree', treePath) : '';
135
+ try {
136
+ await solver.resolve(candidate);
137
+ return solver;
138
+ } catch (error) {
139
+ // Window resolution has failed so the URL must change. Return a promise
140
+ // that never resolves to prevent the application from loading plugins
141
+ // that rely on `IWindowResolver`.
142
+ return new Promise<IWindowResolver>(() => {
143
+ const { base } = paths.urls;
144
+ const pool =
145
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
146
+ const random = pool[Math.floor(Math.random() * pool.length)];
147
+ let path = URLExt.join(base, mode, 'workspaces', `auto-${random}`);
148
+ path = rest ? URLExt.join(path, URLExt.encodeParts(rest)) : path;
149
+
150
+ // Reset the workspace on load.
151
+ query['reset'] = '';
152
+
153
+ const url = path + URLExt.objectToQueryString(query) + (hash || '');
154
+ router.navigate(url, { hard: true });
155
+ });
156
+ }
157
+ }
158
+ };
159
+
160
+ /**
161
+ * The default splash screen provider.
162
+ */
163
+ const splash: JupyterFrontEndPlugin<ISplashScreen> = {
164
+ id: '@jupyterlab/apputils-extension:splash',
165
+ autoStart: true,
166
+ requires: [ITranslator],
167
+ provides: ISplashScreen,
168
+ activate: (app: JupyterFrontEnd, translator: ITranslator) => {
169
+ const trans = translator.load('jupyterlab');
170
+ const { commands, restored } = app;
171
+
172
+ // Create splash element and populate it.
173
+ const splash = document.createElement('div');
174
+ const galaxy = document.createElement('div');
175
+ const logo = document.createElement('div');
176
+
177
+ splash.id = 'jupyterlab-splash';
178
+ galaxy.id = 'galaxy';
179
+ logo.id = 'main-logo';
180
+
181
+ jupyterFaviconIcon.element({
182
+ container: logo,
183
+
184
+ stylesheet: 'splash'
185
+ });
186
+
187
+ galaxy.appendChild(logo);
188
+ ['1', '2', '3'].forEach(id => {
189
+ const moon = document.createElement('div');
190
+ const planet = document.createElement('div');
191
+
192
+ moon.id = `moon${id}`;
193
+ moon.className = 'moon orbit';
194
+ planet.id = `planet${id}`;
195
+ planet.className = 'planet';
196
+
197
+ moon.appendChild(planet);
198
+ galaxy.appendChild(moon);
199
+ });
200
+
201
+ splash.appendChild(galaxy);
202
+
203
+ // Create debounced recovery dialog function.
204
+ let dialog: Dialog<unknown> | null;
205
+ const recovery = new Throttler(
206
+ async () => {
207
+ if (dialog) {
208
+ return;
209
+ }
210
+
211
+ dialog = new Dialog({
212
+ title: trans.__('Loading…'),
213
+ body: trans.__(`The loading screen is taking a long time.
214
+ Would you like to clear the workspace or keep waiting?`),
215
+ buttons: [
216
+ Dialog.cancelButton({ label: trans.__('Keep Waiting') }),
217
+ Dialog.warnButton({ label: trans.__('Clear Workspace') })
218
+ ]
219
+ });
220
+
221
+ try {
222
+ const result = await dialog.launch();
223
+ dialog.dispose();
224
+ dialog = null;
225
+ if (result.button.accept && commands.hasCommand(CommandIDs.reset)) {
226
+ return commands.execute(CommandIDs.reset);
227
+ }
228
+
229
+ // Re-invoke the recovery timer in the next frame.
230
+ requestAnimationFrame(() => {
231
+ // Because recovery can be stopped, handle invocation rejection.
232
+ void recovery.invoke().catch(_ => undefined);
233
+ });
234
+ } catch (error) {
235
+ /* no-op */
236
+ }
237
+ },
238
+ { limit: SPLASH_RECOVER_TIMEOUT, edge: 'trailing' }
239
+ );
240
+
241
+ // Return ISplashScreen.
242
+ let splashCount = 0;
243
+ return {
244
+ show: (light = true) => {
245
+ splash.classList.remove('splash-fade');
246
+ splash.classList.toggle('light', light);
247
+ splash.classList.toggle('dark', !light);
248
+ splashCount++;
249
+ document.body.appendChild(splash);
250
+
251
+ // Because recovery can be stopped, handle invocation rejection.
252
+ void recovery.invoke().catch(_ => undefined);
253
+
254
+ return new DisposableDelegate(async () => {
255
+ await restored;
256
+ if (--splashCount === 0) {
257
+ void recovery.stop();
258
+
259
+ if (dialog) {
260
+ dialog.dispose();
261
+ dialog = null;
262
+ }
263
+
264
+ splash.classList.add('splash-fade');
265
+ window.setTimeout(() => {
266
+ document.body.removeChild(splash);
267
+ }, 200);
268
+ }
269
+ });
270
+ }
271
+ };
272
+ }
273
+ };
274
+
275
+ const print: JupyterFrontEndPlugin<void> = {
276
+ id: '@jupyterlab/apputils-extension:print',
277
+ autoStart: true,
278
+ requires: [ITranslator],
279
+ activate: (app: JupyterFrontEnd, translator: ITranslator) => {
280
+ const trans = translator.load('jupyterlab');
281
+ app.commands.addCommand(CommandIDs.print, {
282
+ label: trans.__('Print…'),
283
+ isEnabled: () => {
284
+ const widget = app.shell.currentWidget;
285
+ return Printing.getPrintFunction(widget) !== null;
286
+ },
287
+ execute: async () => {
288
+ const widget = app.shell.currentWidget;
289
+ const printFunction = Printing.getPrintFunction(widget);
290
+ if (printFunction) {
291
+ await printFunction();
292
+ }
293
+ }
294
+ });
295
+ }
296
+ };
297
+
298
+ export const toggleHeader: JupyterFrontEndPlugin<void> = {
299
+ id: '@jupyterlab/apputils-extension:toggle-header',
300
+ autoStart: true,
301
+ requires: [ITranslator],
302
+ optional: [ICommandPalette],
303
+ activate: (
304
+ app: JupyterFrontEnd,
305
+ translator: ITranslator,
306
+ palette: ICommandPalette | null
307
+ ) => {
308
+ const trans = translator.load('jupyterlab');
309
+
310
+ const category: string = trans.__('Main Area');
311
+ app.commands.addCommand(CommandIDs.toggleHeader, {
312
+ label: trans.__('Show Header Above Content'),
313
+ isEnabled: () =>
314
+ app.shell.currentWidget instanceof MainAreaWidget &&
315
+ !app.shell.currentWidget.contentHeader.isDisposed &&
316
+ app.shell.currentWidget.contentHeader.widgets.length > 0,
317
+ isToggled: () => {
318
+ const widget = app.shell.currentWidget;
319
+ return widget instanceof MainAreaWidget
320
+ ? !widget.contentHeader.isHidden
321
+ : false;
322
+ },
323
+ execute: async () => {
324
+ const widget = app.shell.currentWidget;
325
+ if (widget instanceof MainAreaWidget) {
326
+ widget.contentHeader.setHidden(!widget.contentHeader.isHidden);
327
+ }
328
+ }
329
+ });
330
+ if (palette) {
331
+ palette.addItem({ command: CommandIDs.toggleHeader, category });
332
+ }
333
+ }
334
+ };
335
+
336
+ /**
337
+ * Update the browser title based on the workspace and the current
338
+ * active item.
339
+ */
340
+ async function updateTabTitle(workspace: string, db: IStateDB, name: string) {
341
+ const data: any = await db.toJSON();
342
+ let current: string = data['layout-restorer:data']?.main?.current;
343
+ if (current === undefined) {
344
+ document.title = `${PageConfig.getOption('appName') || 'JupyterLab'}${
345
+ workspace.startsWith('auto-') ? ` (${workspace})` : ``
346
+ }`;
347
+ } else {
348
+ // File name from current path
349
+ let currentFile: string = PathExt.basename(
350
+ decodeURIComponent(window.location.href)
351
+ );
352
+ // Truncate to first 12 characters of current document name + ... if length > 15
353
+ currentFile =
354
+ currentFile.length > 15
355
+ ? currentFile.slice(0, 12).concat(`…`)
356
+ : currentFile;
357
+ // Number of restorable items that are either notebooks or editors
358
+ const count: number = Object.keys(data).filter(
359
+ item => item.startsWith('notebook') || item.startsWith('editor')
360
+ ).length;
361
+
362
+ if (workspace.startsWith('auto-')) {
363
+ document.title = `${currentFile} (${workspace}${
364
+ count > 1 ? ` : ${count}` : ``
365
+ }) - ${name}`;
366
+ } else {
367
+ document.title = `${currentFile}${
368
+ count > 1 ? ` (${count})` : ``
369
+ } - ${name}`;
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * The default state database for storing application state.
376
+ *
377
+ * #### Notes
378
+ * If this extension is loaded with a window resolver, it will automatically add
379
+ * state management commands, URL support for `clone` and `reset`, and workspace
380
+ * auto-saving. Otherwise, it will return a simple in-memory state database.
381
+ */
382
+ const state: JupyterFrontEndPlugin<IStateDB> = {
383
+ id: '@jupyterlab/apputils-extension:state',
384
+ autoStart: true,
385
+ provides: IStateDB,
386
+ requires: [JupyterFrontEnd.IPaths, IRouter, ITranslator],
387
+ optional: [IWindowResolver],
388
+ activate: (
389
+ app: JupyterFrontEnd,
390
+ paths: JupyterFrontEnd.IPaths,
391
+ router: IRouter,
392
+ translator: ITranslator,
393
+ resolver: IWindowResolver | null
394
+ ) => {
395
+ const trans = translator.load('jupyterlab');
396
+
397
+ if (resolver === null) {
398
+ return new StateDB();
399
+ }
400
+
401
+ let resolved = false;
402
+ const { commands, name, serviceManager } = app;
403
+ const { workspaces } = serviceManager;
404
+ const workspace = resolver.name;
405
+ const transform = new PromiseDelegate<StateDB.DataTransform>();
406
+ const db = new StateDB({ transform: transform.promise });
407
+ const save = new Debouncer(async () => {
408
+ const id = workspace;
409
+ const metadata = { id };
410
+ const data = await db.toJSON();
411
+ await workspaces.save(id, { data, metadata });
412
+ });
413
+
414
+ // Any time the local state database changes, save the workspace.
415
+ db.changed.connect(() => void save.invoke(), db);
416
+ db.changed.connect(() => updateTabTitle(workspace, db, name));
417
+
418
+ commands.addCommand(CommandIDs.loadState, {
419
+ label: trans.__('Load state for the current workspace.'),
420
+ execute: async (args: IRouter.ILocation) => {
421
+ // Since the command can be executed an arbitrary number of times, make
422
+ // sure it is safe to call multiple times.
423
+ if (resolved) {
424
+ return;
425
+ }
426
+
427
+ const { hash, path, search } = args;
428
+ const query = URLExt.queryStringToObject(search || '');
429
+ const clone =
430
+ typeof query['clone'] === 'string'
431
+ ? query['clone'] === ''
432
+ ? PageConfig.defaultWorkspace
433
+ : query['clone']
434
+ : null;
435
+ const source = clone || workspace || null;
436
+
437
+ if (source === null) {
438
+ console.error(`${CommandIDs.loadState} cannot load null workspace.`);
439
+ return;
440
+ }
441
+
442
+ try {
443
+ const saved = await workspaces.fetch(source);
444
+
445
+ // If this command is called after a reset, the state database
446
+ // will already be resolved.
447
+ if (!resolved) {
448
+ resolved = true;
449
+ transform.resolve({ type: 'overwrite', contents: saved.data });
450
+ }
451
+ } catch ({ message }) {
452
+ console.warn(`Fetching workspace "${workspace}" failed.`, message);
453
+
454
+ // If the workspace does not exist, cancel the data transformation
455
+ // and save a workspace with the current user state data.
456
+ if (!resolved) {
457
+ resolved = true;
458
+ transform.resolve({ type: 'cancel', contents: null });
459
+ }
460
+ }
461
+
462
+ if (source === clone) {
463
+ // Maintain the query string parameters but remove `clone`.
464
+ delete query['clone'];
465
+
466
+ const url = path + URLExt.objectToQueryString(query) + hash;
467
+ const cloned = save.invoke().then(() => router.stop);
468
+
469
+ // After the state has been cloned, navigate to the URL.
470
+ void cloned.then(() => {
471
+ router.navigate(url);
472
+ });
473
+
474
+ return cloned;
475
+ }
476
+
477
+ // After the state database has finished loading, save it.
478
+ await save.invoke();
479
+ }
480
+ });
481
+
482
+ commands.addCommand(CommandIDs.reset, {
483
+ label: trans.__('Reset Application State'),
484
+ execute: async ({ reload }: { reload: boolean }) => {
485
+ await db.clear();
486
+ await save.invoke();
487
+ if (reload) {
488
+ router.reload();
489
+ }
490
+ }
491
+ });
492
+
493
+ commands.addCommand(CommandIDs.resetOnLoad, {
494
+ label: trans.__('Reset state when loading for the workspace.'),
495
+ execute: (args: IRouter.ILocation) => {
496
+ const { hash, path, search } = args;
497
+ const query = URLExt.queryStringToObject(search || '');
498
+ const reset = 'reset' in query;
499
+ const clone = 'clone' in query;
500
+
501
+ if (!reset) {
502
+ return;
503
+ }
504
+
505
+ // If the state database has already been resolved, resetting is
506
+ // impossible without reloading.
507
+ if (resolved) {
508
+ return router.reload();
509
+ }
510
+
511
+ // Empty the state database.
512
+ resolved = true;
513
+ transform.resolve({ type: 'clear', contents: null });
514
+
515
+ // Maintain the query string parameters but remove `reset`.
516
+ delete query['reset'];
517
+
518
+ const url = path + URLExt.objectToQueryString(query) + hash;
519
+ const cleared = db.clear().then(() => save.invoke());
520
+
521
+ // After the state has been reset, navigate to the URL.
522
+ if (clone) {
523
+ void cleared.then(() => {
524
+ router.navigate(url, { hard: true });
525
+ });
526
+ } else {
527
+ void cleared.then(() => {
528
+ router.navigate(url);
529
+ });
530
+ }
531
+
532
+ return cleared;
533
+ }
534
+ });
535
+
536
+ router.register({
537
+ command: CommandIDs.loadState,
538
+ pattern: /.?/,
539
+ rank: 30 // High priority: 30:100.
540
+ });
541
+
542
+ router.register({
543
+ command: CommandIDs.resetOnLoad,
544
+ pattern: /(\?reset|\&reset)($|&)/,
545
+ rank: 20 // High priority: 20:100.
546
+ });
547
+
548
+ return db;
549
+ }
550
+ };
551
+
552
+ /**
553
+ * The default session context dialogs extension.
554
+ */
555
+ const sessionDialogs: JupyterFrontEndPlugin<ISessionContextDialogs> = {
556
+ id: '@jupyterlab/apputils-extension:sessionDialogs',
557
+ provides: ISessionContextDialogs,
558
+ optional: [ITranslator],
559
+ autoStart: true,
560
+ activate: async (app: JupyterFrontEnd, translator: ITranslator | null) => {
561
+ return new SessionContextDialogs({
562
+ translator: translator ?? nullTranslator
563
+ });
564
+ }
565
+ };
566
+
567
+ /**
568
+ * Utility commands
569
+ */
570
+ const utilityCommands: JupyterFrontEndPlugin<void> = {
571
+ id: '@jupyterlab/apputils-extension:utilityCommands',
572
+ requires: [ITranslator],
573
+ autoStart: true,
574
+ activate: (app: JupyterFrontEnd, translator: ITranslator) => {
575
+ const trans = translator.load('jupyterlab');
576
+ const { commands } = app;
577
+ commands.addCommand(CommandIDs.runFirstEnabled, {
578
+ label: trans.__('Run First Enabled Command'),
579
+ execute: args => {
580
+ const commands: string[] = args.commands as string[];
581
+ const commandArgs: any = args.args;
582
+ const argList = Array.isArray(args);
583
+ for (let i = 0; i < commands.length; i++) {
584
+ const cmd = commands[i];
585
+ const arg = argList ? commandArgs[i] : commandArgs;
586
+ if (app.commands.isEnabled(cmd, arg)) {
587
+ return app.commands.execute(cmd, arg);
588
+ }
589
+ }
590
+ }
591
+ });
592
+
593
+ // Add a command for taking lists of commands and command arguments
594
+ // and running all the enabled commands.
595
+ commands.addCommand(CommandIDs.runAllEnabled, {
596
+ label: trans.__('Run All Enabled Commands Passed as Args'),
597
+ execute: async args => {
598
+ const commands: string[] = args.commands as string[];
599
+ const commandArgs: any = args.args;
600
+ const argList = Array.isArray(args);
601
+ const errorIfNotEnabled: boolean = args.errorIfNotEnabled as boolean;
602
+ for (let i = 0; i < commands.length; i++) {
603
+ const cmd = commands[i];
604
+ const arg = argList ? commandArgs[i] : commandArgs;
605
+ if (app.commands.isEnabled(cmd, arg)) {
606
+ await app.commands.execute(cmd, arg);
607
+ } else {
608
+ if (errorIfNotEnabled) {
609
+ console.error(`${cmd} is not enabled.`);
610
+ }
611
+ }
612
+ }
613
+ }
614
+ });
615
+ }
616
+ };
617
+
618
+ /**
619
+ * The default HTML sanitizer.
620
+ */
621
+ const sanitizer: JupyterFrontEndPlugin<IRenderMime.ISanitizer> = {
622
+ id: '@jupyterlab/apputils-extension:sanitizer',
623
+ autoStart: true,
624
+ provides: ISanitizer,
625
+ requires: [ISettingRegistry],
626
+ activate: (
627
+ app: JupyterFrontEnd,
628
+ settings: ISettingRegistry
629
+ ): IRenderMime.ISanitizer => {
630
+ const sanitizer = new Sanitizer();
631
+ const loadSetting = (setting: ISettingRegistry.ISettings): void => {
632
+ const allowedSchemes = setting.get('allowedSchemes')
633
+ .composite as Array<string>;
634
+
635
+ const autolink = setting.get('autolink').composite as boolean;
636
+
637
+ if (allowedSchemes) {
638
+ sanitizer.setAllowedSchemes(allowedSchemes);
639
+ }
640
+
641
+ sanitizer.setAutolink(autolink);
642
+ };
643
+
644
+ // Wait for the application to be restored and
645
+ // for the settings for this plugin to be loaded
646
+ settings
647
+ .load('@jupyterlab/apputils-extension:sanitizer')
648
+ .then(setting => {
649
+ // Read the settings
650
+ loadSetting(setting);
651
+
652
+ // Listen for your plugin setting changes using Signal
653
+ setting.changed.connect(loadSetting);
654
+ })
655
+ .catch(reason => {
656
+ console.error(`Failed to load sanitizer settings:`, reason);
657
+ });
658
+
659
+ return sanitizer;
660
+ }
661
+ };
662
+
663
+ /**
664
+ * Export the plugins as default.
665
+ */
666
+ const plugins: JupyterFrontEndPlugin<any>[] = [
667
+ announcements,
668
+ kernelStatus,
669
+ notificationPlugin,
670
+ palette,
671
+ paletteRestorer,
672
+ print,
673
+ resolver,
674
+ runningSessionsStatus,
675
+ sanitizer,
676
+ settingsPlugin,
677
+ state,
678
+ splash,
679
+ sessionDialogs,
680
+ themesPlugin,
681
+ themesPaletteMenuPlugin,
682
+ toggleHeader,
683
+ toolbarRegistry,
684
+ utilityCommands,
685
+ workspacesPlugin
686
+ ];
687
+ export default plugins;