Skip to content

Commit 635883e

Browse files
authored
feat: SQL integration management (#24)
1 parent 92b4461 commit 635883e

27 files changed

+1894
-14
lines changed

build/esbuild/build.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ async function buildAll() {
343343
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'data-explorer', 'index.tsx'),
344344
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'viewers', 'dataExplorer.js'),
345345
{ target: 'web', watch: watchAll }
346+
),
347+
build(
348+
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'integrations', 'index.tsx'),
349+
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'integrations', 'index.js'),
350+
{ target: 'web', watch: watchAll }
346351
)
347352
);
348353

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
"category": "Deepnote",
8686
"icon": "$(reveal)"
8787
},
88+
{
89+
"command": "deepnote.manageIntegrations",
90+
"title": "%deepnote.commands.manageIntegrations.title%",
91+
"category": "Deepnote",
92+
"icon": "$(plug)"
93+
},
8894
{
8995
"command": "dataScience.ClearCache",
9096
"title": "%jupyter.command.dataScience.clearCache.title%",
@@ -707,6 +713,11 @@
707713
}
708714
],
709715
"notebook/toolbar": [
716+
{
717+
"command": "deepnote.manageIntegrations",
718+
"group": "navigation@0",
719+
"when": "notebookType == 'deepnote'"
720+
},
710721
{
711722
"command": "jupyter.restartkernel",
712723
"group": "navigation/execute@5",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@
249249
"deepnote.commands.openNotebook.title": "Open Notebook",
250250
"deepnote.commands.openFile.title": "Open File",
251251
"deepnote.commands.revealInExplorer.title": "Reveal in Explorer",
252+
"deepnote.commands.manageIntegrations.title": "Manage Integrations",
252253
"deepnote.views.explorer.name": "Explorer",
253254
"deepnote.command.selectNotebook.title": "Select Notebook"
254255
}

src/notebooks/deepnote/deepnoteActivationService.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IExtensionContext } from '../../platform/common/types';
55
import { IDeepnoteNotebookManager } from '../types';
66
import { DeepnoteNotebookSerializer } from './deepnoteSerializer';
77
import { DeepnoteExplorerView } from './deepnoteExplorerView';
8+
import { IIntegrationManager } from './integrations/types';
89

910
/**
1011
* Service responsible for activating and configuring Deepnote notebook support in VS Code.
@@ -13,12 +14,18 @@ import { DeepnoteExplorerView } from './deepnoteExplorerView';
1314
@injectable()
1415
export class DeepnoteActivationService implements IExtensionSyncActivationService {
1516
private explorerView: DeepnoteExplorerView;
17+
18+
private integrationManager: IIntegrationManager;
19+
1620
private serializer: DeepnoteNotebookSerializer;
1721

1822
constructor(
1923
@inject(IExtensionContext) private extensionContext: IExtensionContext,
20-
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager
21-
) {}
24+
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
25+
@inject(IIntegrationManager) integrationManager: IIntegrationManager
26+
) {
27+
this.integrationManager = integrationManager;
28+
}
2229

2330
/**
2431
* Activates Deepnote support by registering serializers and commands.
@@ -31,5 +38,6 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic
3138
this.extensionContext.subscriptions.push(workspace.registerNotebookSerializer('deepnote', this.serializer));
3239

3340
this.explorerView.activate();
41+
this.integrationManager.activate();
3442
}
3543
}

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@ import { assert } from 'chai';
33
import { DeepnoteActivationService } from './deepnoteActivationService';
44
import { DeepnoteNotebookManager } from './deepnoteNotebookManager';
55
import { IExtensionContext } from '../../platform/common/types';
6+
import { IIntegrationManager } from './integrations/types';
67

78
suite('DeepnoteActivationService', () => {
89
let activationService: DeepnoteActivationService;
910
let mockExtensionContext: IExtensionContext;
1011
let manager: DeepnoteNotebookManager;
12+
let mockIntegrationManager: IIntegrationManager;
1113

1214
setup(() => {
1315
mockExtensionContext = {
1416
subscriptions: []
1517
} as any;
1618

1719
manager = new DeepnoteNotebookManager();
18-
activationService = new DeepnoteActivationService(mockExtensionContext, manager);
20+
mockIntegrationManager = {
21+
activate: () => {
22+
return;
23+
}
24+
};
25+
activationService = new DeepnoteActivationService(mockExtensionContext, manager, mockIntegrationManager);
1926
});
2027

2128
suite('constructor', () => {
@@ -75,8 +82,18 @@ suite('DeepnoteActivationService', () => {
7582

7683
const manager1 = new DeepnoteNotebookManager();
7784
const manager2 = new DeepnoteNotebookManager();
78-
const service1 = new DeepnoteActivationService(context1, manager1);
79-
const service2 = new DeepnoteActivationService(context2, manager2);
85+
const mockIntegrationManager1: IIntegrationManager = {
86+
activate: () => {
87+
return;
88+
}
89+
};
90+
const mockIntegrationManager2: IIntegrationManager = {
91+
activate: () => {
92+
return;
93+
}
94+
};
95+
const service1 = new DeepnoteActivationService(context1, manager1, mockIntegrationManager1);
96+
const service2 = new DeepnoteActivationService(context2, manager2, mockIntegrationManager2);
8097

8198
// Verify each service has its own context
8299
assert.strictEqual((service1 as any).extensionContext, context1);
@@ -101,8 +118,18 @@ suite('DeepnoteActivationService', () => {
101118

102119
const manager1 = new DeepnoteNotebookManager();
103120
const manager2 = new DeepnoteNotebookManager();
104-
new DeepnoteActivationService(context1, manager1);
105-
new DeepnoteActivationService(context2, manager2);
121+
const mockIntegrationManager1: IIntegrationManager = {
122+
activate: () => {
123+
return;
124+
}
125+
};
126+
const mockIntegrationManager2: IIntegrationManager = {
127+
activate: () => {
128+
return;
129+
}
130+
};
131+
new DeepnoteActivationService(context1, manager1, mockIntegrationManager1);
132+
new DeepnoteActivationService(context2, manager2, mockIntegrationManager2);
106133

107134
assert.strictEqual(context1.subscriptions.length, 0);
108135
assert.strictEqual(context2.subscriptions.length, 1);

src/notebooks/deepnote/deepnoteSerializer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { injectable, inject } from 'inversify';
22
import { l10n, type CancellationToken, type NotebookData, type NotebookSerializer, workspace } from 'vscode';
33
import * as yaml from 'js-yaml';
44

5+
import { logger } from '../../platform/logging';
56
import { IDeepnoteNotebookManager } from '../types';
67
import type { DeepnoteProject } from './deepnoteTypes';
78
import { DeepnoteDataConverter } from './deepnoteDataConverter';
@@ -35,7 +36,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
3536
* @returns Promise resolving to notebook data
3637
*/
3738
async deserializeNotebook(content: Uint8Array, token: CancellationToken): Promise<NotebookData> {
38-
console.log('Deserializing Deepnote notebook');
39+
logger.debug('DeepnoteSerializer: Deserializing Deepnote notebook');
3940

4041
if (token?.isCancellationRequested) {
4142
throw new Error('Serialization cancelled');
@@ -52,7 +53,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
5253
const projectId = deepnoteProject.project.id;
5354
const notebookId = this.findCurrentNotebookId(projectId);
5455

55-
console.log(`Selected notebook ID: ${notebookId}.`);
56+
logger.debug(`DeepnoteSerializer: Project ID: ${projectId}, Selected notebook ID: ${notebookId}`);
5657

5758
const selectedNotebook = notebookId
5859
? deepnoteProject.project.notebooks.find((nb) => nb.id === notebookId)
@@ -64,9 +65,10 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
6465

6566
const cells = this.converter.convertBlocksToCells(selectedNotebook.blocks);
6667

67-
console.log(`Converted ${cells.length} cells from notebook blocks.`);
68+
logger.debug(`DeepnoteSerializer: Converted ${cells.length} cells from notebook blocks`);
6869

6970
this.notebookManager.storeOriginalProject(deepnoteProject.project.id, deepnoteProject, selectedNotebook.id);
71+
logger.debug(`DeepnoteSerializer: Stored project ${projectId} in notebook manager`);
7072

7173
return {
7274
cells,
@@ -81,7 +83,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
8183
}
8284
};
8385
} catch (error) {
84-
console.error('Error deserializing Deepnote notebook:', error);
86+
logger.error('DeepnoteSerializer: Error deserializing Deepnote notebook', error);
8587

8688
throw new Error(
8789
`Failed to parse Deepnote file: ${error instanceof Error ? error.message : 'Unknown error'}`
@@ -148,7 +150,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
148150

149151
return new TextEncoder().encode(yamlString);
150152
} catch (error) {
151-
console.error('Error serializing Deepnote notebook:', error);
153+
logger.error('DeepnoteSerializer: Error serializing Deepnote notebook', error);
152154
throw new Error(
153155
`Failed to save Deepnote file: ${error instanceof Error ? error.message : 'Unknown error'}`
154156
);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { inject, injectable } from 'inversify';
2+
3+
import { logger } from '../../../platform/logging';
4+
import { IDeepnoteNotebookManager } from '../../types';
5+
import { IntegrationStatus, IntegrationWithStatus } from './integrationTypes';
6+
import { IIntegrationDetector, IIntegrationStorage } from './types';
7+
import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils';
8+
9+
/**
10+
* Service for detecting integrations used in Deepnote notebooks
11+
*/
12+
@injectable()
13+
export class IntegrationDetector implements IIntegrationDetector {
14+
constructor(
15+
@inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage,
16+
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager
17+
) {}
18+
19+
/**
20+
* Detect all integrations used in the given project
21+
*/
22+
async detectIntegrations(projectId: string): Promise<Map<string, IntegrationWithStatus>> {
23+
// Get the project
24+
const project = this.notebookManager.getOriginalProject(projectId);
25+
if (!project) {
26+
logger.warn(
27+
`IntegrationDetector: No project found for ID: ${projectId}. The project may not have been loaded yet.`
28+
);
29+
return new Map();
30+
}
31+
32+
logger.debug(
33+
`IntegrationDetector: Scanning project ${projectId} with ${project.project.notebooks.length} notebooks`
34+
);
35+
36+
// Collect all blocks with SQL integration metadata from all notebooks
37+
const blocksWithIntegrations: BlockWithIntegration[] = [];
38+
for (const notebook of project.project.notebooks) {
39+
logger.trace(`IntegrationDetector: Scanning notebook ${notebook.id} with ${notebook.blocks.length} blocks`);
40+
41+
for (const block of notebook.blocks) {
42+
// Check if this is a code block with SQL integration metadata
43+
if (block.type === 'code' && block.metadata?.sql_integration_id) {
44+
blocksWithIntegrations.push({
45+
id: block.id,
46+
sql_integration_id: block.metadata.sql_integration_id
47+
});
48+
} else if (block.type === 'code') {
49+
logger.trace(
50+
`IntegrationDetector: Block ${block.id} has no sql_integration_id. Metadata:`,
51+
block.metadata
52+
);
53+
}
54+
}
55+
}
56+
57+
// Use the shared utility to scan blocks and build the status map
58+
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationDetector');
59+
}
60+
61+
/**
62+
* Check if a project has any unconfigured integrations
63+
*/
64+
async hasUnconfiguredIntegrations(projectId: string): Promise<boolean> {
65+
const integrations = await this.detectIntegrations(projectId);
66+
67+
for (const integration of integrations.values()) {
68+
if (integration.status === IntegrationStatus.Disconnected) {
69+
return true;
70+
}
71+
}
72+
73+
return false;
74+
}
75+
}

0 commit comments

Comments
 (0)