Skip to content

Commit 6871ebb

Browse files
agriyakhetarpalgithub-actions[bot]peytondmurray
authored
Load notebooks from the CKHub API through their IDs in URL parameters (#63)
* Generate shareable URL using UUID * Handle notebook from URL params * Some debugging + handling of null readable IDs * Linting * Update Playwright Snapshots * Add docstring for `generateShareURL` * Generate shareable link in `showShareDialog` * Remove redundant comments * Simplify a few conditional statements Co-authored-by: Peyton Murray <[email protected]> * Use object destructuring Co-Authored-By: Peyton Murray <[email protected]> --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Peyton Murray <[email protected]>
1 parent d2853a9 commit 6871ebb

File tree

4 files changed

+173
-64
lines changed

4 files changed

+173
-64
lines changed

src/index.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ import { competitions } from './pages/competitions';
1717
import { notebookPlugin } from './pages/notebook';
1818
import { generateDefaultNotebookName } from './notebook-name';
1919

20+
/**
21+
* Generate a shareable URL for the currently active notebook.
22+
* @param notebookID – The ID of the notebook to share (can be readable_id or sharedId).
23+
* @returns A URL string that points to the notebook with the given notebookID.
24+
*/
25+
function generateShareURL(notebookID: string): string {
26+
const currentUrl = new URL(window.location.href);
27+
const baseUrl = `${currentUrl.protocol}//${currentUrl.host}${currentUrl.pathname}`;
28+
return `${baseUrl}?notebook=${notebookID}`;
29+
}
30+
2031
/**
2132
* Get the current notebook panel
2233
*/
@@ -43,8 +54,18 @@ const manuallySharing = new WeakSet<NotebookPanel>();
4354
* @param notebookContent - The content of the notebook to share, from which we extract the ID.
4455
*/
4556
async function showShareDialog(sharingService: SharingService, notebookContent: INotebookContent) {
46-
const id = (notebookContent.metadata.readableId || notebookContent.metadata.sharedId) as string;
47-
const shareableLink = sharingService.makeRetrieveURL(id).toString();
57+
// Grab the readable ID, or fall back to the UUID.
58+
const readableID = notebookContent.metadata?.readableId as string | null;
59+
const sharedID = notebookContent.metadata?.sharedId as string;
60+
61+
const notebookID = readableID ?? sharedID;
62+
63+
if (!notebookID) {
64+
console.error('No notebook ID found for sharing');
65+
return;
66+
}
67+
68+
const shareableLink = generateShareURL(notebookID);
4869

4970
const dialogResult = await showDialog({
5071
title: '',

src/pages/notebook.tsx

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { EverywhereIcons } from '../icons';
55
import { ToolbarButton, IToolbarWidgetRegistry } from '@jupyterlab/apputils';
66
import { DownloadDropdownButton } from '../ui-components/DownloadDropdownButton';
77
import { Commands } from '../commands';
8+
import { SharingService } from '../sharing-service';
9+
import { INotebookContent } from '@jupyterlab/nbformat';
810

911
export const notebookPlugin: JupyterFrontEndPlugin<void> = {
1012
id: 'jupytereverywhere:notebook',
@@ -19,57 +21,104 @@ export const notebookPlugin: JupyterFrontEndPlugin<void> = {
1921
const contents = app.serviceManager.contents;
2022

2123
const params = new URLSearchParams(window.location.search);
22-
const notebookId = params.get('notebook');
24+
let notebookId = params.get('notebook');
2325

24-
if (notebookId) {
25-
// TODO replace with API call (and iteration over cells to set `editable: false`)
26-
const content = {
27-
cells: [
28-
{
29-
cell_type: 'code',
30-
execution_count: null,
31-
id: '55eb9a2d-401d-4abd-b0eb-373ded5b408d',
32-
metadata: {
33-
// This makes cell non-editable
26+
if (notebookId?.endsWith('.ipynb')) {
27+
notebookId = notebookId.slice(0, -6);
28+
}
29+
30+
/**
31+
* Load a shared notebook from the CKHub API
32+
*/
33+
const loadSharedNotebook = async (id: string): Promise<void> => {
34+
try {
35+
console.log(`Loading shared notebook with ID: ${id}`);
36+
37+
const apiUrl = 'http://localhost:8080/api/v1';
38+
const sharingService = new SharingService(apiUrl);
39+
40+
console.log(`API URL: ${apiUrl}`);
41+
console.log('Retrieving notebook from API...');
42+
43+
const notebookResponse = await sharingService.retrieve(id);
44+
console.log('API Response received:', notebookResponse); // debug
45+
46+
const content: INotebookContent = notebookResponse.content;
47+
48+
// We make all cells read-only by setting editable: false
49+
// by iterating over each cell in the notebook content.
50+
if (content.cells) {
51+
content.cells.forEach(cell => {
52+
cell.metadata = {
53+
...cell.metadata,
3454
editable: false
35-
},
36-
outputs: [],
37-
source: [`# This is notebook '${notebookId}'`]
38-
}
39-
],
40-
metadata: {
41-
kernelspec: {
42-
display_name: 'Python 3 (ipykernel)',
43-
language: 'python',
44-
name: 'python3'
45-
},
46-
language_info: {
47-
codemirror_mode: {
48-
name: 'ipython',
49-
version: 3
50-
},
51-
file_extension: '.py',
52-
mimetype: 'text/x-python',
53-
name: 'python',
54-
nbconvert_exporter: 'python',
55-
pygments_lexer: 'ipython3'
56-
}
57-
},
58-
nbformat: 4,
59-
nbformat_minor: 5
60-
};
61-
contents
62-
.save('Untitled.ipynb', {
55+
};
56+
});
57+
}
58+
59+
content.metadata = {
60+
...content.metadata,
61+
isSharedNotebook: true,
62+
sharedId: notebookResponse.id,
63+
readableId: notebookResponse.readable_id,
64+
domainId: notebookResponse.domain_id
65+
};
66+
67+
// Generate a meaningful filename for the shared notebook
68+
const filename = `Shared_${notebookResponse.readable_id || notebookResponse.id}.ipynb`;
69+
70+
await contents.save(filename, {
6371
content,
6472
format: 'json',
6573
type: 'notebook',
6674
writable: false
67-
})
68-
.then(() => commands.execute('docmanager:open', { path: 'Untitled.ipynb' }));
75+
});
76+
77+
await commands.execute('docmanager:open', {
78+
path: filename,
79+
factory: 'Notebook'
80+
});
81+
82+
console.log(`Successfully loaded shared notebook: ${filename}`);
83+
} catch (error) {
84+
console.error('Failed to load shared notebook:', error);
85+
86+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
87+
const errorStack = error instanceof Error ? error.stack : undefined;
88+
89+
console.error('Error details:', {
90+
message: errorMessage,
91+
stack: errorStack,
92+
notebookId: id,
93+
errorType: typeof error,
94+
errorConstructor: error?.constructor?.name
95+
});
96+
97+
alert(`Failed to load shared notebook "${id}": ${errorMessage}`);
98+
await createNewNotebook();
99+
}
100+
};
101+
102+
/**
103+
* Create a new blank notebook
104+
*/
105+
const createNewNotebook = async (): Promise<void> => {
106+
try {
107+
const result = await commands.execute('docmanager:new-untitled', { type: 'notebook' });
108+
if (result) {
109+
await commands.execute('docmanager:open', { path: 'Untitled.ipynb' });
110+
}
111+
} catch (error) {
112+
console.error('Failed to create new notebook:', error);
113+
}
114+
};
115+
116+
// If a notebook ID is provided in the URL, load it; otherwise,
117+
// create a new notebook
118+
if (notebookId) {
119+
void loadSharedNotebook(notebookId);
69120
} else {
70-
commands.execute('docmanager:new-untitled', { type: 'notebook' }).then(() => {
71-
commands.execute('docmanager:open', { path: 'Untitled.ipynb' });
72-
});
121+
void createNewNotebook();
73122
}
74123

75124
const sidebarItem = new SidebarIcon({

src/sharing-service.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface IShareResponse {
1919
message: string;
2020
notebook: {
2121
id: UUID;
22-
readable_id: string;
22+
readable_id: string | null;
2323
};
2424
}
2525

@@ -29,7 +29,7 @@ export interface IShareResponse {
2929
export interface INotebookResponse {
3030
id: UUID;
3131
domain_id: string;
32-
readable_id: string;
32+
readable_id: string | null;
3333
content: INotebookContent;
3434
}
3535

@@ -85,15 +85,35 @@ export function validateNotebookContent(data: unknown): data is INotebookContent
8585
* @returns A boolean indicating whether the data conforms to the IShareResponse interface.
8686
*/
8787
function validateShareResponse(data: unknown): data is IShareResponse {
88-
return (
89-
hasRequiredKeys<IShareResponse, keyof IShareResponse>(data, ['message', 'notebook']) &&
90-
typeof (data as IShareResponse).message === 'string' &&
91-
hasRequiredKeys<IShareResponse['notebook'], keyof IShareResponse['notebook']>(
92-
(data as IShareResponse).notebook,
88+
if (!hasRequiredKeys<IShareResponse, keyof IShareResponse>(data, ['message', 'notebook'])) {
89+
return false;
90+
}
91+
92+
const response = data as IShareResponse;
93+
94+
if (typeof response.message !== 'string') {
95+
return false;
96+
}
97+
98+
if (
99+
!hasRequiredKeys<IShareResponse['notebook'], keyof IShareResponse['notebook']>(
100+
response.notebook,
93101
['id', 'readable_id']
94-
) &&
95-
validateUUID((data as IShareResponse).notebook.id)
96-
);
102+
)
103+
) {
104+
return false;
105+
}
106+
107+
if (!validateUUID(response.notebook.id)) {
108+
return false;
109+
}
110+
111+
// readable_id can be null or string
112+
if (response.notebook.readable_id !== null && typeof response.notebook.readable_id !== 'string') {
113+
return false;
114+
}
115+
116+
return true;
97117
}
98118

99119
/**
@@ -103,18 +123,37 @@ function validateShareResponse(data: unknown): data is IShareResponse {
103123
* @returns A boolean indicating whether the data is a valid INotebookResponse.
104124
*/
105125
function validateNotebookResponse(data: unknown): data is INotebookResponse {
106-
return (
107-
hasRequiredKeys<INotebookResponse, keyof INotebookResponse>(data, [
126+
if (
127+
!hasRequiredKeys<INotebookResponse, keyof INotebookResponse>(data, [
108128
'id',
109129
'domain_id',
110130
'readable_id',
111131
'content'
112-
]) &&
113-
validateUUID((data as INotebookResponse).id) &&
114-
typeof (data as INotebookResponse).domain_id === 'string' &&
115-
typeof (data as INotebookResponse).readable_id === 'string' &&
116-
validateNotebookContent((data as INotebookResponse).content)
117-
);
132+
])
133+
) {
134+
return false;
135+
}
136+
137+
const response = data as INotebookResponse;
138+
139+
if (!validateUUID(response.id)) {
140+
return false;
141+
}
142+
143+
if (typeof response.domain_id !== 'string') {
144+
return false;
145+
}
146+
147+
// readable_id can be null or string
148+
if (response.readable_id !== null && typeof response.readable_id !== 'string') {
149+
return false;
150+
}
151+
152+
if (!validateNotebookContent(response.content)) {
153+
return false;
154+
}
155+
156+
return true;
118157
}
119158

120159
/**
-3.37 KB
Loading

0 commit comments

Comments
 (0)