Skip to content

Commit 129988b

Browse files
krassowskiagriyakhetarpalgithub-actions[bot]
authored
Implement view-only notebook using custom factory (#75)
* First working draft * Fix CSS specificity rules * Add custom view-only notebook factory * Fix * Rename to view-only * Add share and download dropdown factories to view-only notebook * Add specification for view-only notebook * Update Playwright Snapshots * Remove the writable `false` as we are using a custom factory anyways * Try to point to plugin settings * Fix toolbar, save interaction, block markdown cell unrendering --------- Co-authored-by: Agriya Khetarpal <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9a9cf24 commit 129988b

File tree

8 files changed

+368
-49
lines changed

8 files changed

+368
-49
lines changed

schema/plugin.json

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,65 @@
33
"title": "jupytereverywhere",
44
"description": "jupytereverywhere settings.",
55
"type": "object",
6-
"properties": {},
76
"additionalProperties": false,
87
"jupyter.lab.toolbars": {
9-
"Notebook": [
8+
"ViewOnlyNotebook": [
109
{
1110
"name": "save",
11+
"disabled": true
12+
},
13+
{
14+
"name": "cut",
15+
"command": "notebook:cut-cell",
16+
"disabled": true
17+
},
18+
{
19+
"name": "copy",
20+
"command": "notebook:copy-cell",
21+
"disabled": true
22+
},
23+
{
24+
"name": "paste",
25+
"command": "notebook:paste-cell-below",
26+
"disabled": true
27+
},
28+
{
29+
"name": "run",
30+
"caption": "Run cell",
31+
"disabled": true
32+
},
33+
{
34+
"name": "interrupt",
35+
"caption": "Interrupt notebook",
36+
"disabled": true
37+
},
38+
{
39+
"name": "restart",
40+
"caption": "Restart notebook",
41+
"disabled": true
42+
},
43+
{
44+
"name": "restart-and-run",
45+
"caption": "Restart and run all cells",
46+
"disabled": true
47+
},
48+
{
49+
"name": "cellType",
1250
"command": "",
13-
"disabled": true,
51+
"disabled": true
52+
},
53+
{
54+
"name": "share",
55+
"rank": 35
56+
},
57+
{
58+
"name": "downloadDropdown",
59+
"rank": 36
60+
}
61+
],
62+
"Notebook": [
63+
{
64+
"name": "save",
1465
"rank": 10
1566
},
1667
{
@@ -106,5 +157,13 @@
106157
"disabled": true
107158
}
108159
]
160+
},
161+
"jupyter.lab.transform": true,
162+
"properties": {
163+
"toolbar": {
164+
"title": "View-only notebook panel toolbar items",
165+
"type": "array",
166+
"default": []
167+
}
109168
}
110169
}

src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Commands } from './commands';
1616
import { competitions } from './pages/competitions';
1717
import { notebookPlugin } from './pages/notebook';
1818
import { generateDefaultNotebookName } from './notebook-name';
19+
import { viewOnlyNotebookFactoryPlugin } from './view-only';
1920

2021
/**
2122
* Generate a shareable URL for the currently active notebook.
@@ -236,4 +237,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
236237
}
237238
};
238239

239-
export default [plugin, notebookPlugin, files, competitions, customSidebar];
240+
export default [
241+
viewOnlyNotebookFactoryPlugin,
242+
plugin,
243+
notebookPlugin,
244+
files,
245+
competitions,
246+
customSidebar
247+
];

src/pages/notebook.tsx

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';
22
import { INotebookTracker } from '@jupyterlab/notebook';
3+
import { INotebookContent } from '@jupyterlab/nbformat';
34
import { SidebarIcon } from '../ui-components/SidebarIcon';
45
import { EverywhereIcons } from '../icons';
56
import { ToolbarButton, IToolbarWidgetRegistry } from '@jupyterlab/apputils';
67
import { DownloadDropdownButton } from '../ui-components/DownloadDropdownButton';
78
import { Commands } from '../commands';
89
import { SharingService } from '../sharing-service';
9-
import { INotebookContent } from '@jupyterlab/nbformat';
10+
import { VIEW_ONLY_NOTEBOOK_FACTORY, IViewOnlyNotebookTracker } from '../view-only';
1011

1112
export const notebookPlugin: JupyterFrontEndPlugin<void> = {
1213
id: 'jupytereverywhere:notebook',
1314
autoStart: true,
14-
requires: [INotebookTracker, IToolbarWidgetRegistry],
15+
requires: [INotebookTracker, IViewOnlyNotebookTracker, IToolbarWidgetRegistry],
1516
activate: (
1617
app: JupyterFrontEnd,
1718
tracker: INotebookTracker,
19+
readonlyTracker: IViewOnlyNotebookTracker,
1820
toolbarRegistry: IToolbarWidgetRegistry
1921
) => {
20-
const { commands, shell } = app;
21-
const contents = app.serviceManager.contents;
22+
const { commands, shell, serviceManager } = app;
23+
const { contents } = serviceManager;
2224

2325
const params = new URLSearchParams(window.location.search);
2426
let notebookId = params.get('notebook');
@@ -41,12 +43,15 @@ export const notebookPlugin: JupyterFrontEndPlugin<void> = {
4143
console.log('Retrieving notebook from API...');
4244

4345
const notebookResponse = await sharingService.retrieve(id);
44-
console.log('API Response received:', notebookResponse); // debug
46+
console.log('API Response received:', notebookResponse);
4547

46-
const content: INotebookContent = notebookResponse.content;
48+
const { content }: { content: INotebookContent } = notebookResponse;
4749

48-
// We make all cells read-only by setting editable: false
49-
// by iterating over each cell in the notebook content.
50+
// We make all cells read-only by setting editable: false.
51+
// This is still required with a custom widget factory as
52+
// it is not trivial to coerce the cells to respect the `readOnly`
53+
// property otherwise (Mike tried swapping `Notebook.ContentFactory`
54+
// and it does not work without further hacks).
5055
if (content.cells) {
5156
content.cells.forEach(cell => {
5257
cell.metadata = {
@@ -56,27 +61,31 @@ export const notebookPlugin: JupyterFrontEndPlugin<void> = {
5661
});
5762
}
5863

64+
const { id: responseId, readable_id, domain_id } = notebookResponse;
5965
content.metadata = {
6066
...content.metadata,
6167
isSharedNotebook: true,
62-
sharedId: notebookResponse.id,
63-
readableId: notebookResponse.readable_id,
64-
domainId: notebookResponse.domain_id
68+
sharedId: responseId,
69+
readableId: readable_id,
70+
domainId: domain_id
6571
};
6672

67-
// Generate a meaningful filename for the shared notebook
68-
const filename = `Shared_${notebookResponse.readable_id || notebookResponse.id}.ipynb`;
73+
const filename = `Shared_${readable_id || responseId}.ipynb`;
6974

7075
await contents.save(filename, {
7176
content,
7277
format: 'json',
7378
type: 'notebook',
79+
// Even though we have a custom view-only factory, we still
80+
// want to indicate that notebook is read-only to avoid
81+
// error on Ctrl + S and instead get a nice notification that
82+
// the notebook cannot be saved unless using save-as.
7483
writable: false
7584
});
7685

7786
await commands.execute('docmanager:open', {
7887
path: filename,
79-
factory: 'Notebook'
88+
factory: VIEW_ONLY_NOTEBOOK_FACTORY
8089
});
8190

8291
console.log(`Successfully loaded shared notebook: ${filename}`);
@@ -125,8 +134,11 @@ export const notebookPlugin: JupyterFrontEndPlugin<void> = {
125134
label: 'Notebook',
126135
icon: EverywhereIcons.notebook,
127136
execute: () => {
137+
if (readonlyTracker.currentWidget) {
138+
return shell.activateById(readonlyTracker.currentWidget.id);
139+
}
128140
if (tracker.currentWidget) {
129-
shell.activateById(tracker.currentWidget.id);
141+
return shell.activateById(tracker.currentWidget.id);
130142
}
131143
}
132144
});
@@ -135,24 +147,26 @@ export const notebookPlugin: JupyterFrontEndPlugin<void> = {
135147
app.shell.activateById(sidebarItem.id);
136148
app.restored.then(() => app.shell.activateById(sidebarItem.id));
137149

138-
toolbarRegistry.addFactory(
139-
'Notebook',
140-
'downloadDropdown',
141-
() => new DownloadDropdownButton(commands)
142-
);
143-
144-
toolbarRegistry.addFactory(
145-
'Notebook',
146-
'share',
147-
() =>
148-
new ToolbarButton({
149-
label: 'Share',
150-
icon: EverywhereIcons.link,
151-
tooltip: 'Share this notebook',
152-
onClick: () => {
153-
void commands.execute(Commands.shareNotebookCommand);
154-
}
155-
})
156-
);
150+
for (const toolbarName of ['Notebook', 'ViewOnlyNotebook']) {
151+
toolbarRegistry.addFactory(
152+
toolbarName,
153+
'downloadDropdown',
154+
() => new DownloadDropdownButton(commands)
155+
);
156+
157+
toolbarRegistry.addFactory(
158+
toolbarName,
159+
'share',
160+
() =>
161+
new ToolbarButton({
162+
label: 'Share',
163+
icon: EverywhereIcons.link,
164+
tooltip: 'Share this notebook',
165+
onClick: () => {
166+
void commands.execute(Commands.shareNotebookCommand);
167+
}
168+
})
169+
);
170+
}
157171
}
158172
};

src/sidebar.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export const customSidebar: JupyterFrontEndPlugin<void> = {
2424
const newWidget = args.currentTitle
2525
? leftHandler._findWidgetByTitle(args.currentTitle)
2626
: null;
27-
console.log(newWidget);
2827
if (newWidget && newWidget instanceof SidebarIcon) {
2928
const cancel = newWidget.execute();
3029
if (cancel) {

0 commit comments

Comments
 (0)