Skip to content

Commit 8597009

Browse files
authored
feat: SQL block variable input and integration switching (#86)
1 parent f142367 commit 8597009

File tree

2 files changed

+859
-39
lines changed

2 files changed

+859
-39
lines changed

src/notebooks/deepnote/sqlCellStatusBarProvider.ts

Lines changed: 317 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@ import {
44
NotebookCell,
55
NotebookCellStatusBarItem,
66
NotebookCellStatusBarItemProvider,
7-
NotebookDocument,
7+
NotebookDocumentChangeEvent,
8+
NotebookEdit,
89
ProviderResult,
10+
QuickPickItem,
11+
QuickPickItemKind,
12+
WorkspaceEdit,
13+
commands,
914
l10n,
10-
notebooks
15+
notebooks,
16+
window,
17+
workspace
1118
} from 'vscode';
1219
import { inject, injectable } from 'inversify';
1320

1421
import { IExtensionSyncActivationService } from '../../platform/activation/types';
1522
import { IDisposableRegistry } from '../../platform/common/types';
16-
import { Commands } from '../../platform/common/constants';
1723
import { IIntegrationStorage } from './integrations/types';
18-
import { DATAFRAME_SQL_INTEGRATION_ID } from '../../platform/notebooks/deepnote/integrationTypes';
24+
import { Commands } from '../../platform/common/constants';
25+
import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes';
1926

2027
/**
21-
* Provides status bar items for SQL cells showing the integration name
28+
* QuickPick item with an integration ID
29+
*/
30+
interface LocalQuickPickItem extends QuickPickItem {
31+
id: string;
32+
}
33+
34+
/**
35+
* Provides status bar items for SQL cells showing the integration name and variable name
2236
*/
2337
@injectable()
2438
export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvider, IExtensionSyncActivationService {
@@ -42,6 +56,55 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
4256
})
4357
);
4458

59+
// Refresh when any Deepnote notebook changes (e.g., metadata updated externally)
60+
this.disposables.push(
61+
workspace.onDidChangeNotebookDocument((e: NotebookDocumentChangeEvent) => {
62+
if (e.notebook.notebookType === 'deepnote') {
63+
this._onDidChangeCellStatusBarItems.fire();
64+
}
65+
})
66+
);
67+
68+
// Register command to update SQL variable name
69+
this.disposables.push(
70+
commands.registerCommand('deepnote.updateSqlVariableName', async (cell?: NotebookCell) => {
71+
if (!cell) {
72+
// Fall back to the active notebook cell
73+
const activeEditor = window.activeNotebookEditor;
74+
if (activeEditor && activeEditor.selection) {
75+
cell = activeEditor.notebook.cellAt(activeEditor.selection.start);
76+
}
77+
}
78+
79+
if (!cell) {
80+
void window.showErrorMessage(l10n.t('No active notebook cell'));
81+
return;
82+
}
83+
84+
await this.updateVariableName(cell);
85+
})
86+
);
87+
88+
// Register command to switch SQL integration
89+
this.disposables.push(
90+
commands.registerCommand('deepnote.switchSqlIntegration', async (cell?: NotebookCell) => {
91+
if (!cell) {
92+
// Fall back to the active notebook cell
93+
const activeEditor = window.activeNotebookEditor;
94+
if (activeEditor && activeEditor.selection) {
95+
cell = activeEditor.notebook.cellAt(activeEditor.selection.start);
96+
}
97+
}
98+
99+
if (!cell) {
100+
void window.showErrorMessage(l10n.t('No active notebook cell'));
101+
return;
102+
}
103+
104+
await this.switchIntegration(cell);
105+
})
106+
);
107+
45108
// Dispose our emitter with the extension
46109
this.disposables.push(this._onDidChangeCellStatusBarItems);
47110
}
@@ -59,18 +122,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
59122
return undefined;
60123
}
61124

62-
// Get the integration ID from cell metadata
63-
const integrationId = this.getIntegrationId(cell);
64-
if (!integrationId) {
65-
return undefined;
66-
}
67-
68-
// Don't show status bar for the internal DuckDB integration
69-
if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) {
70-
return undefined;
71-
}
72-
73-
return this.createStatusBarItem(cell.notebook, integrationId);
125+
return this.createStatusBarItems(cell);
74126
}
75127

76128
private getIntegrationId(cell: NotebookCell): string | undefined {
@@ -86,11 +138,57 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
86138
return undefined;
87139
}
88140

89-
private async createStatusBarItem(
90-
notebook: NotebookDocument,
141+
private async createStatusBarItems(cell: NotebookCell): Promise<NotebookCellStatusBarItem[]> {
142+
const items: NotebookCellStatusBarItem[] = [];
143+
144+
// Add integration status bar item
145+
const integrationId = this.getIntegrationId(cell);
146+
if (integrationId) {
147+
const integrationItem = await this.createIntegrationStatusBarItem(cell, integrationId);
148+
if (integrationItem) {
149+
items.push(integrationItem);
150+
}
151+
} else {
152+
// Show "No integration connected" when no integration is selected
153+
items.push({
154+
text: `$(database) ${l10n.t('No integration connected')}`,
155+
alignment: 1, // NotebookCellStatusBarAlignment.Left
156+
priority: 100,
157+
tooltip: l10n.t('No SQL integration connected\nClick to select an integration'),
158+
command: {
159+
title: l10n.t('Switch Integration'),
160+
command: 'deepnote.switchSqlIntegration',
161+
arguments: [cell]
162+
}
163+
});
164+
}
165+
166+
// Always add variable status bar item for SQL cells
167+
items.push(this.createVariableStatusBarItem(cell));
168+
169+
return items;
170+
}
171+
172+
private async createIntegrationStatusBarItem(
173+
cell: NotebookCell,
91174
integrationId: string
92175
): Promise<NotebookCellStatusBarItem | undefined> {
93-
const projectId = notebook.metadata?.deepnoteProjectId;
176+
// Handle internal DuckDB integration specially
177+
if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) {
178+
return {
179+
text: `$(database) ${l10n.t('DataFrame SQL (DuckDB)')}`,
180+
alignment: 1, // NotebookCellStatusBarAlignment.Left
181+
priority: 100,
182+
tooltip: l10n.t('Internal DuckDB integration for querying DataFrames\nClick to switch'),
183+
command: {
184+
title: l10n.t('Switch Integration'),
185+
command: 'deepnote.switchSqlIntegration',
186+
arguments: [cell]
187+
}
188+
};
189+
}
190+
191+
const projectId = cell.notebook.metadata?.deepnoteProjectId;
94192
if (!projectId) {
95193
return undefined;
96194
}
@@ -99,16 +197,210 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
99197
const config = await this.integrationStorage.getProjectIntegrationConfig(projectId, integrationId);
100198
const displayName = config?.name || l10n.t('Unknown integration (configure)');
101199

102-
// Create a status bar item that opens the integration management UI
200+
// Create a status bar item that opens the integration picker
103201
return {
104202
text: `$(database) ${displayName}`,
105203
alignment: 1, // NotebookCellStatusBarAlignment.Left
106-
tooltip: l10n.t('SQL Integration: {0}\nClick to configure', displayName),
204+
priority: 100,
205+
tooltip: l10n.t('SQL Integration: {0}\nClick to switch or configure', displayName),
206+
command: {
207+
title: l10n.t('Switch Integration'),
208+
command: 'deepnote.switchSqlIntegration',
209+
arguments: [cell]
210+
}
211+
};
212+
}
213+
214+
private createVariableStatusBarItem(cell: NotebookCell): NotebookCellStatusBarItem {
215+
const variableName = this.getVariableName(cell);
216+
217+
return {
218+
text: l10n.t('Variable: {0}', variableName),
219+
alignment: 1, // NotebookCellStatusBarAlignment.Left
220+
priority: 90,
221+
tooltip: l10n.t('Variable name for SQL query result\nClick to change'),
107222
command: {
108-
title: l10n.t('Configure Integration'),
109-
command: Commands.ManageIntegrations,
110-
arguments: [integrationId]
223+
title: l10n.t('Change Variable Name'),
224+
command: 'deepnote.updateSqlVariableName',
225+
arguments: [cell]
226+
}
227+
};
228+
}
229+
230+
private getVariableName(cell: NotebookCell): string {
231+
const metadata = cell.metadata;
232+
if (metadata && typeof metadata === 'object') {
233+
const variableName = (metadata as Record<string, unknown>).deepnote_variable_name;
234+
if (typeof variableName === 'string' && variableName) {
235+
return variableName;
111236
}
237+
}
238+
239+
return 'df';
240+
}
241+
242+
private async updateVariableName(cell: NotebookCell): Promise<void> {
243+
const currentVariableName = this.getVariableName(cell);
244+
245+
const newVariableNameInput = await window.showInputBox({
246+
prompt: l10n.t('Enter variable name for SQL query result'),
247+
value: currentVariableName,
248+
ignoreFocusOut: true,
249+
validateInput: (value) => {
250+
const trimmed = value.trim();
251+
if (!trimmed) {
252+
return l10n.t('Variable name cannot be empty');
253+
}
254+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
255+
return l10n.t('Variable name must be a valid Python identifier');
256+
}
257+
return undefined;
258+
}
259+
});
260+
261+
const newVariableName = newVariableNameInput?.trim();
262+
if (newVariableName === undefined || newVariableName === currentVariableName) {
263+
return;
264+
}
265+
266+
// Update cell metadata
267+
const edit = new WorkspaceEdit();
268+
const updatedMetadata = {
269+
...cell.metadata,
270+
deepnote_variable_name: newVariableName
112271
};
272+
273+
edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]);
274+
275+
const success = await workspace.applyEdit(edit);
276+
if (!success) {
277+
void window.showErrorMessage(l10n.t('Failed to update variable name'));
278+
return;
279+
}
280+
281+
// Trigger status bar update
282+
this._onDidChangeCellStatusBarItems.fire();
283+
}
284+
285+
private async switchIntegration(cell: NotebookCell): Promise<void> {
286+
const currentIntegrationId = this.getIntegrationId(cell);
287+
288+
// Get all available integrations
289+
const allIntegrations = await this.integrationStorage.getAll();
290+
291+
// Build quick pick items
292+
const items: (QuickPickItem | LocalQuickPickItem)[] = [];
293+
294+
// Check if current integration is unknown (not in the list)
295+
const isCurrentIntegrationUnknown =
296+
currentIntegrationId &&
297+
currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID &&
298+
!allIntegrations.some((i) => i.id === currentIntegrationId);
299+
300+
// Add current unknown integration first if it exists
301+
if (isCurrentIntegrationUnknown && currentIntegrationId) {
302+
const item: LocalQuickPickItem = {
303+
label: l10n.t('Unknown integration (configure)'),
304+
description: currentIntegrationId,
305+
detail: l10n.t('Currently selected'),
306+
id: currentIntegrationId
307+
};
308+
items.push(item);
309+
}
310+
311+
// Add all configured integrations
312+
for (const integration of allIntegrations) {
313+
const typeLabel = this.getIntegrationTypeLabel(integration.type);
314+
const item: LocalQuickPickItem = {
315+
label: integration.name || integration.id,
316+
description: typeLabel,
317+
detail: integration.id === currentIntegrationId ? l10n.t('Currently selected') : undefined,
318+
// Store the integration ID in a custom property
319+
id: integration.id
320+
};
321+
items.push(item);
322+
}
323+
324+
// Add DuckDB integration
325+
const duckDbItem: LocalQuickPickItem = {
326+
label: l10n.t('DataFrame SQL (DuckDB)'),
327+
description: l10n.t('DuckDB'),
328+
detail: currentIntegrationId === DATAFRAME_SQL_INTEGRATION_ID ? l10n.t('Currently selected') : undefined,
329+
id: DATAFRAME_SQL_INTEGRATION_ID
330+
};
331+
items.push(duckDbItem);
332+
333+
// Add "Configure current integration" option (with separator)
334+
if (currentIntegrationId && currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID) {
335+
// Add separator
336+
const separator: QuickPickItem = {
337+
label: '',
338+
kind: QuickPickItemKind.Separator
339+
};
340+
items.push(separator);
341+
342+
const configureItem: LocalQuickPickItem = {
343+
label: l10n.t('Configure current integration'),
344+
id: '__configure__'
345+
};
346+
items.push(configureItem);
347+
}
348+
349+
const selected = await window.showQuickPick(items, {
350+
placeHolder: l10n.t('Select SQL integration'),
351+
matchOnDescription: true
352+
});
353+
354+
if (!selected) {
355+
return;
356+
}
357+
358+
// Type guard to check if selected item has an id property
359+
if (!('id' in selected)) {
360+
return;
361+
}
362+
363+
const selectedItem = selected as LocalQuickPickItem;
364+
const selectedId = selectedItem.id;
365+
366+
// Handle "Configure current integration" option
367+
if (selectedId === '__configure__' && currentIntegrationId) {
368+
await commands.executeCommand(Commands.ManageIntegrations, currentIntegrationId);
369+
return;
370+
}
371+
372+
// No change
373+
if (selectedId === currentIntegrationId) {
374+
return;
375+
}
376+
377+
// Update cell metadata with new integration ID
378+
const edit = new WorkspaceEdit();
379+
const updatedMetadata = {
380+
...cell.metadata,
381+
sql_integration_id: selectedId
382+
};
383+
384+
edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]);
385+
386+
const success = await workspace.applyEdit(edit);
387+
if (!success) {
388+
void window.showErrorMessage(l10n.t('Failed to select integration'));
389+
return;
390+
}
391+
392+
// Trigger status bar update
393+
this._onDidChangeCellStatusBarItems.fire();
394+
}
395+
396+
private getIntegrationTypeLabel(type: IntegrationType): string {
397+
switch (type) {
398+
case IntegrationType.Postgres:
399+
return l10n.t('PostgreSQL');
400+
case IntegrationType.BigQuery:
401+
return l10n.t('BigQuery');
402+
default:
403+
return String(type);
404+
}
113405
}
114406
}

0 commit comments

Comments
 (0)