Skip to content

Commit c1b5526

Browse files
authored
feat: Add a command for adding chart blocks (#137)
* feat: Add a command for adding chart blocks. * hardcode the variable name * better errrors
1 parent 87e8346 commit c1b5526

File tree

6 files changed

+303
-2
lines changed

6 files changed

+303
-2
lines changed

package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@
129129
"category": "Deepnote",
130130
"icon": "$(graph)"
131131
},
132+
{
133+
"command": "deepnote.addChartBlock",
134+
"title": "%deepnote.commands.addChartBlock.title%",
135+
"category": "Deepnote",
136+
"icon": "$(graph-line)"
137+
},
132138
{
133139
"command": "deepnote.addInputTextBlock",
134140
"title": "%deepnote.commands.addInputTextBlock.title%",
@@ -850,6 +856,51 @@
850856
"group": "navigation@1",
851857
"when": "notebookType == 'deepnote'"
852858
},
859+
{
860+
"command": "deepnote.addChartBlock",
861+
"group": "navigation@2",
862+
"when": "notebookType == 'deepnote'"
863+
},
864+
{
865+
"command": "deepnote.addBigNumberChartBlock",
866+
"group": "navigation@3",
867+
"when": "notebookType == 'deepnote'"
868+
},
869+
{
870+
"command": "deepnote.addInputTextBlock",
871+
"group": "navigation@4",
872+
"when": "notebookType == 'deepnote'"
873+
},
874+
{
875+
"command": "deepnote.addInputTextareaBlock",
876+
"group": "navigation@5",
877+
"when": "notebookType == 'deepnote'"
878+
},
879+
{
880+
"command": "deepnote.addInputSelectBlock",
881+
"group": "navigation@6",
882+
"when": "notebookType == 'deepnote'"
883+
},
884+
{
885+
"command": "deepnote.addInputSliderBlock",
886+
"group": "navigation@7",
887+
"when": "notebookType == 'deepnote'"
888+
},
889+
{
890+
"command": "deepnote.addInputCheckboxBlock",
891+
"group": "navigation@8",
892+
"when": "notebookType == 'deepnote'"
893+
},
894+
{
895+
"command": "deepnote.addInputDateBlock",
896+
"group": "navigation@9",
897+
"when": "notebookType == 'deepnote'"
898+
},
899+
{
900+
"command": "deepnote.addInputDateRangeBlock",
901+
"group": "navigation@10",
902+
"when": "notebookType == 'deepnote'"
903+
},
853904
{
854905
"command": "jupyter.restartkernel",
855906
"group": "navigation/execute@5",

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@
255255
"deepnote.commands.importNotebook.title": "Import Notebook",
256256
"deepnote.commands.importJupyterNotebook.title": "Import Jupyter Notebook",
257257
"deepnote.commands.addSqlBlock.title": "Add SQL Block",
258-
"deepnote.commands.addBigNumberChartBlock.title": "Add Big Number Chart Block",
258+
"deepnote.commands.addBigNumberChartBlock.title": "Add Big Number Block",
259+
"deepnote.commands.addChartBlock.title": "Add Chart Block",
259260
"deepnote.commands.addInputTextBlock.title": "Add Input Text Block",
260261
"deepnote.commands.addInputTextareaBlock.title": "Add Input Textarea Block",
261262
"deepnote.commands.addInputSelectBlock.title": "Add Input Select Block",

src/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export interface ICommandNameArgumentTypeMapping {
188188
[DSCommands.ContinueEditSessionInCodespace]: [];
189189
[DSCommands.AddSqlBlock]: [];
190190
[DSCommands.AddBigNumberChartBlock]: [];
191+
[DSCommands.AddChartBlock]: [];
191192
[DSCommands.AddInputTextBlock]: [];
192193
[DSCommands.AddInputTextareaBlock]: [];
193194
[DSCommands.AddInputSelectBlock]: [];

src/notebooks/deepnote/deepnoteNotebookCommandListener.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { IExtensionSyncActivationService } from '../../platform/activation/types
1717
import { IDisposableRegistry } from '../../platform/common/types';
1818
import { Commands } from '../../platform/common/constants';
1919
import { chainWithPendingUpdates } from '../../kernels/execution/notebookUpdater';
20+
import { WrappedError } from '../../platform/errors/types';
2021
import {
2122
DeepnoteBigNumberMetadataSchema,
2223
DeepnoteTextInputMetadataSchema,
@@ -76,7 +77,10 @@ export function getInputBlockMetadata(blockType: InputBlockType, variableName: s
7677

7778
export function safeParseDeepnoteVariableNameFromContentJson(content: string): string | undefined {
7879
try {
79-
const variableNameResult = z.string().safeParse(JSON.parse(content)['deepnote_variable_name']);
80+
const parsed = JSON.parse(content);
81+
// Chart blocks use 'variable' key, other blocks use 'deepnote_variable_name'
82+
const variableName = parsed['variable'] ?? parsed['deepnote_variable_name'];
83+
const variableNameResult = z.string().safeParse(variableName);
8084
return variableNameResult.success ? variableNameResult.data : undefined;
8185
} catch (error) {
8286
logger.error('Error parsing deepnote variable name from content JSON', error);
@@ -148,6 +152,7 @@ export class DeepnoteNotebookCommandListener implements IExtensionSyncActivation
148152
this.disposableRegistry.push(
149153
commands.registerCommand(Commands.AddBigNumberChartBlock, () => this.addBigNumberChartBlock())
150154
);
155+
this.disposableRegistry.push(commands.registerCommand(Commands.AddChartBlock, () => this.addChartBlock()));
151156
this.disposableRegistry.push(
152157
commands.registerCommand(Commands.AddInputTextBlock, () => this.addInputBlock('input-text'))
153158
);
@@ -262,6 +267,61 @@ export class DeepnoteNotebookCommandListener implements IExtensionSyncActivation
262267
await commands.executeCommand('notebook.cell.edit');
263268
}
264269

270+
public async addChartBlock(): Promise<void> {
271+
const editor = window.activeNotebookEditor;
272+
273+
if (!editor) {
274+
throw new WrappedError(l10n.t('No active notebook editor found'));
275+
}
276+
277+
const document = editor.notebook;
278+
const selection = editor.selection;
279+
const insertIndex = selection ? selection.end : document.cellCount;
280+
281+
const defaultVisualizationSpec = {
282+
mark: 'line',
283+
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
284+
data: { values: [] },
285+
encoding: {
286+
x: { field: 'x', type: 'quantitative' },
287+
y: { field: 'y', type: 'quantitative' }
288+
}
289+
};
290+
291+
const cellContent = {
292+
variable: 'df_1',
293+
spec: defaultVisualizationSpec,
294+
filters: []
295+
};
296+
297+
const metadata = {
298+
__deepnotePocket: {
299+
type: 'visualization'
300+
}
301+
};
302+
303+
const result = await chainWithPendingUpdates(document, (edit) => {
304+
const newCell = new NotebookCellData(NotebookCellKind.Code, JSON.stringify(cellContent, null, 2), 'json');
305+
306+
newCell.metadata = metadata;
307+
308+
const nbEdit = NotebookEdit.insertCells(insertIndex, [newCell]);
309+
310+
edit.set(document.uri, [nbEdit]);
311+
});
312+
313+
if (result !== true) {
314+
throw new WrappedError(l10n.t('Failed to insert chart block'));
315+
}
316+
317+
const notebookRange = new NotebookRange(insertIndex, insertIndex + 1);
318+
319+
editor.revealRange(notebookRange, NotebookEditorRevealType.Default);
320+
editor.selection = notebookRange;
321+
322+
await commands.executeCommand('notebook.cell.edit');
323+
}
324+
265325
public async addInputBlock(blockType: InputBlockType): Promise<void> {
266326
const editor = window.activeNotebookEditor;
267327
if (!editor) {

src/notebooks/deepnote/deepnoteNotebookCommandListener.unit.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { IDisposable } from '../../platform/common/types';
2222
import * as notebookUpdater from '../../kernels/execution/notebookUpdater';
2323
import { createMockedNotebookDocument } from '../../test/datascience/editor-integration/helpers';
24+
import { WrappedError } from '../../platform/errors/types';
2425
import { DATAFRAME_SQL_INTEGRATION_ID } from '../../platform/notebooks/deepnote/integrationTypes';
2526

2627
suite('DeepnoteNotebookCommandListener', () => {
@@ -976,5 +977,191 @@ suite('DeepnoteNotebookCommandListener', () => {
976977
);
977978
});
978979
});
980+
981+
suite('addChartBlock', () => {
982+
test('should add chart block at the end when no selection exists', async () => {
983+
// Setup mocks
984+
const { editor, document } = createMockEditor([], undefined);
985+
const { chainStub, executeCommandStub, getCapturedNotebookEdits } =
986+
mockNotebookUpdateAndExecute(editor);
987+
988+
// Call the method
989+
await commandListener.addChartBlock();
990+
991+
const capturedNotebookEdits = getCapturedNotebookEdits();
992+
993+
// Verify chainWithPendingUpdates was called
994+
assert.isTrue(chainStub.calledOnce, 'chainWithPendingUpdates should be called once');
995+
assert.equal(chainStub.firstCall.args[0], document, 'Should be called with correct document');
996+
997+
// Verify the edits were captured
998+
assert.isNotNull(capturedNotebookEdits, 'Notebook edits should be captured');
999+
assert.isDefined(capturedNotebookEdits, 'Notebook edits should be defined');
1000+
1001+
const editsArray = capturedNotebookEdits!;
1002+
assert.equal(editsArray.length, 1, 'Should have one notebook edit');
1003+
1004+
const notebookEdit = editsArray[0] as any;
1005+
assert.equal(notebookEdit.newCells.length, 1, 'Should insert one cell');
1006+
1007+
const newCell = notebookEdit.newCells[0];
1008+
assert.equal(newCell.kind, NotebookCellKind.Code, 'Should be a code cell');
1009+
assert.equal(newCell.languageId, 'json', 'Should have json language');
1010+
1011+
// Verify cell content is valid JSON with correct structure
1012+
const content = JSON.parse(newCell.value);
1013+
assert.equal(content.variable, 'df_1', 'Should have correct variable name');
1014+
assert.property(content, 'spec', 'Should have spec property');
1015+
assert.property(content, 'filters', 'Should have filters property');
1016+
1017+
// Verify the spec has the correct Vega-Lite structure
1018+
assert.equal(content.spec.mark, 'line', 'Should be a line chart');
1019+
assert.equal(
1020+
content.spec.$schema,
1021+
'https://vega.github.io/schema/vega-lite/v5.json',
1022+
'Should have Vega-Lite schema'
1023+
);
1024+
assert.deepStrictEqual(content.spec.data, { values: [] }, 'Should have empty data array');
1025+
assert.property(content.spec, 'encoding', 'Should have encoding property');
1026+
assert.property(content.spec.encoding, 'x', 'Should have x encoding');
1027+
assert.property(content.spec.encoding, 'y', 'Should have y encoding');
1028+
1029+
// Verify metadata structure
1030+
assert.property(newCell.metadata, '__deepnotePocket', 'Should have __deepnotePocket metadata');
1031+
assert.equal(newCell.metadata.__deepnotePocket.type, 'visualization', 'Should have visualization type');
1032+
1033+
// Verify reveal and selection were set
1034+
assert.isTrue((editor.revealRange as sinon.SinonStub).calledOnce, 'Should reveal the new cell range');
1035+
const revealCall = (editor.revealRange as sinon.SinonStub).firstCall;
1036+
assert.equal(revealCall.args[0].start, 0, 'Should reveal correct range start');
1037+
assert.equal(revealCall.args[0].end, 1, 'Should reveal correct range end');
1038+
assert.equal(revealCall.args[1], 0, 'Should use NotebookEditorRevealType.Default (value 0)');
1039+
1040+
// Verify notebook.cell.edit command was executed
1041+
assert.isTrue(
1042+
executeCommandStub.calledWith('notebook.cell.edit'),
1043+
'Should execute notebook.cell.edit command'
1044+
);
1045+
});
1046+
1047+
test('should add chart block after selection when selection exists', async () => {
1048+
// Setup mocks
1049+
const existingCells = [createMockCell('{}'), createMockCell('{}')];
1050+
const selection = new NotebookRange(0, 1);
1051+
const { editor } = createMockEditor(existingCells, selection);
1052+
const { chainStub, getCapturedNotebookEdits } = mockNotebookUpdateAndExecute(editor);
1053+
1054+
// Call the method
1055+
await commandListener.addChartBlock();
1056+
1057+
const capturedNotebookEdits = getCapturedNotebookEdits();
1058+
1059+
// Verify chainWithPendingUpdates was called
1060+
assert.isTrue(chainStub.calledOnce, 'chainWithPendingUpdates should be called once');
1061+
1062+
// Verify a cell was inserted
1063+
assert.isNotNull(capturedNotebookEdits, 'Notebook edits should be captured');
1064+
const notebookEdit = capturedNotebookEdits![0] as any;
1065+
assert.equal(notebookEdit.newCells.length, 1, 'Should insert one cell');
1066+
assert.equal(notebookEdit.newCells[0].languageId, 'json', 'Should be JSON cell');
1067+
});
1068+
1069+
test('should use hardcoded variable name df_1', async () => {
1070+
// Setup mocks with existing df variables
1071+
const existingCells = [
1072+
createMockCell('{ "deepnote_variable_name": "df_1" }'),
1073+
createMockCell('{ "variable": "df_2" }')
1074+
];
1075+
const { editor } = createMockEditor(existingCells, undefined);
1076+
const { getCapturedNotebookEdits } = mockNotebookUpdateAndExecute(editor);
1077+
1078+
// Call the method
1079+
await commandListener.addChartBlock();
1080+
1081+
const capturedNotebookEdits = getCapturedNotebookEdits();
1082+
const notebookEdit = capturedNotebookEdits![0] as any;
1083+
const newCell = notebookEdit.newCells[0];
1084+
1085+
// Verify variable name is always df_1
1086+
const content = JSON.parse(newCell.value);
1087+
assert.equal(content.variable, 'df_1', 'Should always use df_1');
1088+
});
1089+
1090+
test('should always use df_1 regardless of existing variables', async () => {
1091+
// Setup mocks with various existing variables
1092+
const existingCells = [
1093+
createMockCell('{ "deepnote_variable_name": "input_10" }'),
1094+
createMockCell('{ "deepnote_variable_name": "df_5" }'),
1095+
createMockCell('{ "variable": "df_2" }')
1096+
];
1097+
const { editor } = createMockEditor(existingCells, undefined);
1098+
const { getCapturedNotebookEdits } = mockNotebookUpdateAndExecute(editor);
1099+
1100+
// Call the method
1101+
await commandListener.addChartBlock();
1102+
1103+
const capturedNotebookEdits = getCapturedNotebookEdits();
1104+
const notebookEdit = capturedNotebookEdits![0] as any;
1105+
const newCell = notebookEdit.newCells[0];
1106+
1107+
// Verify variable name is always df_1
1108+
const content = JSON.parse(newCell.value);
1109+
assert.equal(content.variable, 'df_1', 'Should always use df_1');
1110+
});
1111+
1112+
test('should insert at correct position in the middle of notebook', async () => {
1113+
// Setup mocks
1114+
const existingCells = [createMockCell('{}'), createMockCell('{}'), createMockCell('{}')];
1115+
const selection = new NotebookRange(1, 2);
1116+
const { editor } = createMockEditor(existingCells, selection);
1117+
const { chainStub, getCapturedNotebookEdits } = mockNotebookUpdateAndExecute(editor);
1118+
1119+
// Call the method
1120+
await commandListener.addChartBlock();
1121+
1122+
const capturedNotebookEdits = getCapturedNotebookEdits();
1123+
1124+
// Verify chainWithPendingUpdates was called
1125+
assert.isTrue(chainStub.calledOnce, 'chainWithPendingUpdates should be called once');
1126+
1127+
// Verify a cell was inserted
1128+
assert.isNotNull(capturedNotebookEdits, 'Notebook edits should be captured');
1129+
const notebookEdit = capturedNotebookEdits![0] as any;
1130+
assert.equal(notebookEdit.newCells.length, 1, 'Should insert one cell');
1131+
assert.equal(notebookEdit.newCells[0].languageId, 'json', 'Should be JSON cell');
1132+
});
1133+
1134+
test('should throw error when no active editor exists', async () => {
1135+
// Setup: no active editor
1136+
Object.defineProperty(window, 'activeNotebookEditor', {
1137+
value: undefined,
1138+
configurable: true,
1139+
writable: true
1140+
});
1141+
1142+
// Call the method and expect rejection
1143+
await assert.isRejected(
1144+
commandListener.addChartBlock(),
1145+
WrappedError,
1146+
'No active notebook editor found'
1147+
);
1148+
});
1149+
1150+
test('should throw error when chainWithPendingUpdates fails', async () => {
1151+
// Setup mocks
1152+
const { editor } = createMockEditor([], undefined);
1153+
Object.defineProperty(window, 'activeNotebookEditor', {
1154+
value: editor,
1155+
configurable: true,
1156+
writable: true
1157+
});
1158+
1159+
// Mock chainWithPendingUpdates to return false
1160+
sandbox.stub(notebookUpdater, 'chainWithPendingUpdates').resolves(false);
1161+
1162+
// Call the method and expect rejection
1163+
await assert.isRejected(commandListener.addChartBlock(), WrappedError, 'Failed to insert chart block');
1164+
});
1165+
});
9791166
});
9801167
});

src/platform/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export namespace Commands {
227227
export const ManageIntegrations = 'deepnote.manageIntegrations';
228228
export const AddSqlBlock = 'deepnote.addSqlBlock';
229229
export const AddBigNumberChartBlock = 'deepnote.addBigNumberChartBlock';
230+
export const AddChartBlock = 'deepnote.addChartBlock';
230231
export const AddInputTextBlock = 'deepnote.addInputTextBlock';
231232
export const AddInputTextareaBlock = 'deepnote.addInputTextareaBlock';
232233
export const AddInputSelectBlock = 'deepnote.addInputSelectBlock';

0 commit comments

Comments
 (0)