Skip to content

Commit eb82ddf

Browse files
authored
Add cell toolbar buttons (#7)
* Add cell toolbar buttons and refactor code * lint * Simplify the cell toolbar buttons * Add integration tests on cell toolbar buttons * fix test * Use a prebuild notebook for the tests * lint
1 parent a630780 commit eb82ddf

File tree

10 files changed

+330
-132
lines changed

10 files changed

+330
-132
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@
5959
"@json2csv/plainjs": "^7.0.0",
6060
"@jupyterlab/application": "^4.0.0",
6161
"@jupyterlab/apputils": "^4.0.0",
62+
"@jupyterlab/cell-toolbar": "^4.0.0",
6263
"@jupyterlab/cells": "^4.0.0",
6364
"@jupyterlab/coreutils": "^6.0.0",
6465
"@jupyterlab/filebrowser": "^4.0.0",
6566
"@jupyterlab/notebook": "^4.0.0",
6667
"@jupyterlab/services": "^7.0.0",
6768
"@jupyterlab/settingregistry": "^4.0.0",
69+
"@jupyterlab/translation": "^4.0.0",
6870
"@jupyterlab/ui-components": "^4.0.0",
6971
"@lumino/commands": "^2.0.0",
7072
"@lumino/signaling": "^2.0.0",

schema/notebook-toolbar.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"jupyter.lab.toolbars": {
3+
"Notebook": [
4+
{
5+
"name": "SqlWidget",
6+
"rank": 50
7+
}
8+
]
9+
},
10+
"jupyter.lab.shortcuts": [],
11+
"title": "@jupyter/sql-cell",
12+
"description": "@jupyter/sql-cell settings.",
13+
"type": "object",
14+
"properties": {},
15+
"additionalProperties": false
16+
}

schema/plugin.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
{
22
"jupyter.lab.toolbars": {
3-
"Notebook": [
3+
"Cell": [
44
{
5-
"name": "SqlWidget",
6-
"rank": 50
5+
"name": "SQL switch",
6+
"command": "jupyter-sql-cell:switch"
7+
},
8+
{
9+
"name": "SQL run",
10+
"command": "jupyter-sql-cell:execute"
711
}
812
]
913
},

src/common.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ICellModel } from '@jupyterlab/cells';
2+
3+
export namespace CommandIDs {
4+
export const switchSQL = 'jupyter-sql-cell:switch';
5+
export const run = 'jupyter-sql-cell:execute';
6+
}
7+
8+
export const METADATA_SQL_FORMAT = 'application/sql';
9+
10+
export namespace SqlCell {
11+
export function isRaw(model: ICellModel | undefined) {
12+
if (!model) {
13+
return false;
14+
}
15+
return model.type === 'raw';
16+
}
17+
export function isSqlCell(model: ICellModel | undefined) {
18+
if (!model) {
19+
return false;
20+
}
21+
return (
22+
model.type === 'raw' &&
23+
model.getMetadata('format') === METADATA_SQL_FORMAT
24+
);
25+
}
26+
}

src/index.ts

Lines changed: 120 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,27 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
1111
import { runIcon } from '@jupyterlab/ui-components';
1212

1313
import { requestAPI } from './handler';
14-
import { METADATA_SQL_FORMAT, SqlWidget } from './widget';
14+
import { CommandIDs, METADATA_SQL_FORMAT, SqlCell } from './common';
15+
import { SqlWidget } from './widget';
1516

1617
/**
17-
* Initialization data for the @jupyter/sql-cell extension.
18+
* Load the commands and the cell toolbar buttons (from settings).
1819
*/
1920
const plugin: JupyterFrontEndPlugin<void> = {
2021
id: '@jupyter/sql-cell:plugin',
21-
description: 'A JupyterLab extension to run SQL in notebook dedicated cells',
22+
description: 'Add the commands to the registry.',
2223
autoStart: true,
23-
requires: [INotebookTracker, IToolbarWidgetRegistry],
24-
optional: [ICommandPalette, IDefaultFileBrowser, ISettingRegistry],
24+
requires: [INotebookTracker],
25+
optional: [ICommandPalette, IDefaultFileBrowser],
2526
activate: (
2627
app: JupyterFrontEnd,
2728
tracker: INotebookTracker,
28-
toolbarRegistry: IToolbarWidgetRegistry,
29-
commandPalette: ICommandPalette | null,
30-
fileBrowser: IDefaultFileBrowser | null,
31-
settingRegistry: ISettingRegistry | null
29+
commandPalette: ICommandPalette,
30+
fileBrowser: IDefaultFileBrowser | null
3231
) => {
3332
const { commands } = app;
3433

35-
const commandID = 'jupyter-sql-cell:execute';
36-
37-
commands.addCommand(commandID, {
34+
commands.addCommand(CommandIDs.run, {
3835
label: 'Run SQL',
3936
caption: 'Run SQL',
4037
icon: runIcon,
@@ -53,7 +50,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
5350
body: JSON.stringify({ query: source })
5451
})
5552
.then(data => {
56-
saveData(path, data.data, date, fileBrowser)
53+
Private.saveData(path, data.data, date, fileBrowser)
5754
.then(dataPath => console.log(`Data saved ${dataPath}`))
5855
.catch(undefined);
5956
})
@@ -63,20 +60,65 @@ const plugin: JupyterFrontEndPlugin<void> = {
6360
);
6461
});
6562
},
66-
isEnabled: () => {
63+
isEnabled: () => SqlCell.isSqlCell(tracker.activeCell?.model),
64+
isVisible: () => SqlCell.isRaw(tracker.activeCell?.model)
65+
});
66+
67+
commands.addCommand(CommandIDs.switchSQL, {
68+
label: 'SQL',
69+
caption: () => {
6770
const model = tracker.activeCell?.model;
68-
if (!model) {
69-
return false;
71+
return SqlCell.isRaw(model)
72+
? SqlCell.isSqlCell(model)
73+
? 'Switch to Raw'
74+
: 'Switch to SQL'
75+
: 'Not available';
76+
},
77+
execute: async () => {
78+
const model = tracker.activeCell?.model;
79+
if (!model || model.type !== 'raw') {
80+
return;
7081
}
71-
return (
72-
model.type === 'raw' &&
73-
model.getMetadata('format') === METADATA_SQL_FORMAT
74-
);
75-
}
82+
if (model.getMetadata('format') !== METADATA_SQL_FORMAT) {
83+
model.setMetadata('format', METADATA_SQL_FORMAT);
84+
} else if (model.getMetadata('format') === METADATA_SQL_FORMAT) {
85+
model.deleteMetadata('format');
86+
}
87+
app.commands.notifyCommandChanged(CommandIDs.switchSQL);
88+
app.commands.notifyCommandChanged(CommandIDs.run);
89+
},
90+
isVisible: () => SqlCell.isRaw(tracker.activeCell?.model),
91+
isToggled: () => SqlCell.isSqlCell(tracker.activeCell?.model)
7692
});
7793

94+
if (commandPalette) {
95+
commandPalette.addItem({
96+
command: CommandIDs.run,
97+
category: 'SQL'
98+
});
99+
}
100+
}
101+
};
102+
103+
/**
104+
* The notebook toolbar widget.
105+
*/
106+
const notebookToolbarWidget: JupyterFrontEndPlugin<void> = {
107+
id: '@jupyter/sql-cell:notebook-toolbar',
108+
description: 'A JupyterLab extension to run SQL in notebook dedicated cells',
109+
autoStart: true,
110+
requires: [INotebookTracker, IToolbarWidgetRegistry],
111+
optional: [ISettingRegistry],
112+
activate: (
113+
app: JupyterFrontEnd,
114+
tracker: INotebookTracker,
115+
toolbarRegistry: IToolbarWidgetRegistry,
116+
settingRegistry: ISettingRegistry | null
117+
) => {
118+
const { commands } = app;
119+
78120
const toolbarFactory = (panel: NotebookPanel) => {
79-
return new SqlWidget({ commands, commandID, tracker });
121+
return new SqlWidget({ commands, commandID: CommandIDs.run, tracker });
80122
};
81123

82124
toolbarRegistry.addFactory<NotebookPanel>(
@@ -87,7 +129,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
87129

88130
if (settingRegistry) {
89131
settingRegistry
90-
.load(plugin.id)
132+
.load(notebookToolbarWidget.id)
91133
.then(settings => {
92134
console.log('@jupyter/sql-cell settings loaded:', settings.composite);
93135
})
@@ -98,70 +140,65 @@ const plugin: JupyterFrontEndPlugin<void> = {
98140
);
99141
});
100142
}
101-
102-
if (commandPalette) {
103-
commandPalette.addItem({
104-
command: commandID,
105-
category: 'SQL'
106-
});
107-
}
108-
109-
console.log('JupyterLab extension @jupyter/sql-cell is activated!');
110143
}
111144
};
112145

113-
export default plugin;
146+
export default [notebookToolbarWidget, plugin];
147+
148+
namespace Private {
149+
/**
150+
* Save data in a CSV file.
151+
*
152+
* @param path - the path to the directory where to save data.
153+
* @param data - the data to parse as CSV.
154+
* @param date - the query date.
155+
*/
156+
export async function saveData(
157+
path: string,
158+
data: any,
159+
date: Date,
160+
fileBrowser: IDefaultFileBrowser | null
161+
): Promise<string | undefined> {
162+
const contentsManager = new ContentsManager();
163+
const parser = new Parser();
164+
const csv = parser.parse(data);
165+
166+
const dateText = date
167+
.toLocaleString()
168+
.replace(/[/:]/g, '-')
169+
.replace(/\s/g, '')
170+
.replace(',', '_');
171+
172+
let currentPath = '';
173+
if (!path.startsWith('/')) {
174+
currentPath = `${fileBrowser?.model.path}/` || '';
175+
}
114176

115-
/**
116-
* Save data in a CSV file.
117-
*
118-
* @param path - the path to the directory where to save data.
119-
* @param data - the data to parse as CSV.
120-
* @param date - the query date.
121-
*/
122-
async function saveData(
123-
path: string,
124-
data: any,
125-
date: Date,
126-
fileBrowser: IDefaultFileBrowser | null
127-
): Promise<string | undefined> {
128-
const contentsManager = new ContentsManager();
129-
const parser = new Parser();
130-
const csv = parser.parse(data);
131-
132-
const dateText = date
133-
.toLocaleString()
134-
.replace(/[/:]/g, '-')
135-
.replace(/\s/g, '')
136-
.replace(',', '_');
137-
138-
let currentPath = '';
139-
if (!path.startsWith('/')) {
140-
currentPath = `${fileBrowser?.model.path}/` || '';
141-
}
177+
for (const directory of path.split('/')) {
178+
currentPath = `${currentPath}${directory}/`;
179+
await contentsManager
180+
.get(currentPath, { content: false })
181+
.catch(error =>
182+
contentsManager.save(currentPath, { type: 'directory' })
183+
);
184+
}
142185

143-
for (const directory of path.split('/')) {
144-
currentPath = `${currentPath}${directory}/`;
145-
await contentsManager
146-
.get(currentPath, { content: false })
147-
.catch(() => contentsManager.save(currentPath, { type: 'directory' }));
148-
}
186+
const filename = `${dateText}.csv`;
187+
const fileModel = {
188+
name: filename,
189+
path: `${currentPath}/${filename}`,
190+
format: 'text' as Contents.FileFormat,
191+
content: csv
192+
};
149193

150-
const filename = `${dateText}.csv`;
151-
const fileModel = {
152-
name: filename,
153-
path: `${currentPath}/${filename}`,
154-
format: 'text' as Contents.FileFormat,
155-
content: csv
156-
};
157-
158-
return contentsManager
159-
.save(fileModel.path, fileModel)
160-
.then(() => {
161-
return fileModel.path;
162-
})
163-
.catch(e => {
164-
console.error(e);
165-
return undefined;
166-
});
194+
return contentsManager
195+
.save(fileModel.path, fileModel)
196+
.then(() => {
197+
return fileModel.path;
198+
})
199+
.catch(e => {
200+
console.error(e);
201+
return undefined;
202+
});
203+
}
167204
}

0 commit comments

Comments
 (0)