Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
515cda2
feat: add integration types for postgres and bigquery
jankuca Oct 7, 2025
8561979
feat: add integration storage service with encrypted credential storage
jankuca Oct 7, 2025
7a6f49b
feat: add integration detector service to scan notebooks for SQL inte…
jankuca Oct 7, 2025
8b2c72e
feat: add integration manager with toolbar button and command registr…
jankuca Oct 7, 2025
b19c6f2
feat: add integration services to web registry and fix type definitions
jankuca Oct 7, 2025
4566fdf
refactor: simplify integration metadata to use sql_integration_id field
jankuca Oct 7, 2025
438e09c
fix: update DeepnoteActivationService unit tests to include Integrati…
jankuca Oct 7, 2025
2e1ddf4
fix: handle promises properly in IntegrationManager
jankuca Oct 7, 2025
ee48635
fix: remove unused properties from IntegrationManager to resolve TS e…
jankuca Oct 7, 2025
1f4e3f4
feat: make integrations button always visible for deepnote notebooks …
jankuca Oct 7, 2025
648ee59
feat: implement configuration forms for postgres and bigquery integra…
jankuca Oct 7, 2025
7449153
feat: wire up integration storage to save postgres and bigquery confi…
jankuca Oct 7, 2025
bbcb5f8
debug: add logging to integration detector to diagnose detection issues
jankuca Oct 7, 2025
3dfba64
debug: add cell scanning fallback and more detailed logging
jankuca Oct 7, 2025
4ea0210
refactor: replace console.log with logger in integration management code
jankuca Oct 7, 2025
37ccc21
feat: exclude deepnote-dataframe-sql from integration management
jankuca Oct 7, 2025
6893ead
debug: add project storage diagnostics to integration manager
jankuca Oct 7, 2025
5ad77bb
refactor: replace console.log with logger in deepnote serializer
jankuca Oct 7, 2025
8fa226e
fix: detect integrations from cells when project not in manager
jankuca Oct 7, 2025
f040eb5
fix: remove unused notebookManager dependency from IntegrationManager
jankuca Oct 7, 2025
49f4f97
feat: replace QuickPick with persistent webview UI for integration ma…
jankuca Oct 7, 2025
dec72df
fix: mark unused integrations parameter with underscore prefix
jankuca Oct 7, 2025
14dfa27
fix: use event delegation for webview buttons to fix onclick handlers
jankuca Oct 7, 2025
fa90447
feat: add display name field and fix status display in integration UI
jankuca Oct 7, 2025
e11cd45
fix lint
jankuca Oct 7, 2025
bd7c238
fix: handle promise rejections in integration manager event handlers
jankuca Oct 7, 2025
b41dfe4
fix: replace console.error with logger in integrationStorage
jankuca Oct 7, 2025
94be660
security: fix XSS vulnerabilities by replacing innerHTML with DOM APIs
jankuca Oct 7, 2025
439181a
refactor: deduplicate EXCLUDED_INTEGRATION_ID constant and simplify t…
jankuca Oct 7, 2025
342d9bf
fix: replace void operator with proper error handling in event handlers
jankuca Oct 7, 2025
0764321
refactor: extract shared integration scanning logic to utility function
jankuca Oct 7, 2025
3c6526a
refactor: use interface injection for IntegrationWebviewProvider
jankuca Oct 7, 2025
a6a72b1
fix lint issue
jankuca Oct 7, 2025
47cac5b
fix: persist integration state in webview provider to prevent stale data
jankuca Oct 7, 2025
65bfb7a
fix: avoid collecting undefined integration ids
jankuca Oct 7, 2025
6034479
delete sample file
jankuca Oct 7, 2025
8970049
refactor: convert integration management rendering to react
jankuca Oct 15, 2025
65cf8f0
fix showing existing config
jankuca Oct 15, 2025
efd59a3
fix integration configuration delete/reset button
jankuca Oct 16, 2025
18a9f57
remove extra copyright headers
jankuca Oct 16, 2025
d7a6608
remove unsafe log
jankuca Oct 16, 2025
b29090e
use localization in integration mangement UI
jankuca Oct 16, 2025
cbf6e6a
improve input props
jankuca Oct 16, 2025
ba7b600
fix timer cleanup
jankuca Oct 16, 2025
acea24b
polish integration html base
jankuca Oct 16, 2025
7c424c3
add IIntegrationManager interface
jankuca Oct 16, 2025
d090618
localize "Manage Integrations" command label
jankuca Oct 16, 2025
4da6b4b
add simple json validation to big query form
jankuca Oct 16, 2025
c02a7a0
remove unsafe flag from integration webview csp
jankuca Oct 16, 2025
f86b6c7
fix integration manager interface
jankuca Oct 16, 2025
964a9c5
lint empty methods in tests
jankuca Oct 16, 2025
ce673f4
fix localization usage
jankuca Oct 16, 2025
a7d0d92
show "unkonwn" for unknown integrations
jankuca Oct 16, 2025
b2219b2
show missing BQ credentials error
jankuca Oct 16, 2025
9b5b7aa
lint imports
jankuca Oct 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build/esbuild/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@ async function buildAll() {
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'data-explorer', 'index.tsx'),
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'viewers', 'dataExplorer.js'),
{ target: 'web', watch: watchAll }
),
build(
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'integrations', 'index.tsx'),
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'integrations', 'index.js'),
{ target: 'web', watch: watchAll }
)
);

Expand Down
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
"category": "Deepnote",
"icon": "$(reveal)"
},
{
"command": "deepnote.manageIntegrations",
"title": "%deepnote.commands.manageIntegrations.title%",
"category": "Deepnote",
"icon": "$(plug)"
},
{
"command": "dataScience.ClearCache",
"title": "%jupyter.command.dataScience.clearCache.title%",
Expand Down Expand Up @@ -707,6 +713,11 @@
}
],
"notebook/toolbar": [
{
"command": "deepnote.manageIntegrations",
"group": "navigation@0",
"when": "notebookType == 'deepnote'"
},
{
"command": "jupyter.restartkernel",
"group": "navigation/execute@5",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
"deepnote.commands.openNotebook.title": "Open Notebook",
"deepnote.commands.openFile.title": "Open File",
"deepnote.commands.revealInExplorer.title": "Reveal in Explorer",
"deepnote.commands.manageIntegrations.title": "Manage Integrations",
"deepnote.views.explorer.name": "Explorer",
"deepnote.command.selectNotebook.title": "Select Notebook"
}
12 changes: 10 additions & 2 deletions src/notebooks/deepnote/deepnoteActivationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IExtensionContext } from '../../platform/common/types';
import { IDeepnoteNotebookManager } from '../types';
import { DeepnoteNotebookSerializer } from './deepnoteSerializer';
import { DeepnoteExplorerView } from './deepnoteExplorerView';
import { IIntegrationManager } from './integrations/types';

/**
* Service responsible for activating and configuring Deepnote notebook support in VS Code.
Expand All @@ -13,12 +14,18 @@ import { DeepnoteExplorerView } from './deepnoteExplorerView';
@injectable()
export class DeepnoteActivationService implements IExtensionSyncActivationService {
private explorerView: DeepnoteExplorerView;

private integrationManager: IIntegrationManager;

private serializer: DeepnoteNotebookSerializer;

constructor(
@inject(IExtensionContext) private extensionContext: IExtensionContext,
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager
) {}
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
@inject(IIntegrationManager) integrationManager: IIntegrationManager
) {
this.integrationManager = integrationManager;
}

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

this.explorerView.activate();
this.integrationManager.activate();
}
}
37 changes: 32 additions & 5 deletions src/notebooks/deepnote/deepnoteActivationService.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@ import { assert } from 'chai';
import { DeepnoteActivationService } from './deepnoteActivationService';
import { DeepnoteNotebookManager } from './deepnoteNotebookManager';
import { IExtensionContext } from '../../platform/common/types';
import { IIntegrationManager } from './integrations/types';

suite('DeepnoteActivationService', () => {
let activationService: DeepnoteActivationService;
let mockExtensionContext: IExtensionContext;
let manager: DeepnoteNotebookManager;
let mockIntegrationManager: IIntegrationManager;

setup(() => {
mockExtensionContext = {
subscriptions: []
} as any;

manager = new DeepnoteNotebookManager();
activationService = new DeepnoteActivationService(mockExtensionContext, manager);
mockIntegrationManager = {
activate: () => {
return;
}
};
activationService = new DeepnoteActivationService(mockExtensionContext, manager, mockIntegrationManager);
});

suite('constructor', () => {
Expand Down Expand Up @@ -75,8 +82,18 @@ suite('DeepnoteActivationService', () => {

const manager1 = new DeepnoteNotebookManager();
const manager2 = new DeepnoteNotebookManager();
const service1 = new DeepnoteActivationService(context1, manager1);
const service2 = new DeepnoteActivationService(context2, manager2);
const mockIntegrationManager1: IIntegrationManager = {
activate: () => {
return;
}
};
const mockIntegrationManager2: IIntegrationManager = {
activate: () => {
return;
}
};
const service1 = new DeepnoteActivationService(context1, manager1, mockIntegrationManager1);
const service2 = new DeepnoteActivationService(context2, manager2, mockIntegrationManager2);

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

const manager1 = new DeepnoteNotebookManager();
const manager2 = new DeepnoteNotebookManager();
new DeepnoteActivationService(context1, manager1);
new DeepnoteActivationService(context2, manager2);
const mockIntegrationManager1: IIntegrationManager = {
activate: () => {
return;
}
};
const mockIntegrationManager2: IIntegrationManager = {
activate: () => {
return;
}
};
new DeepnoteActivationService(context1, manager1, mockIntegrationManager1);
new DeepnoteActivationService(context2, manager2, mockIntegrationManager2);

assert.strictEqual(context1.subscriptions.length, 0);
assert.strictEqual(context2.subscriptions.length, 1);
Expand Down
12 changes: 7 additions & 5 deletions src/notebooks/deepnote/deepnoteSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { injectable, inject } from 'inversify';
import { l10n, type CancellationToken, type NotebookData, type NotebookSerializer, workspace } from 'vscode';
import * as yaml from 'js-yaml';

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

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

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

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

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

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

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

return {
cells,
Expand All @@ -81,7 +83,7 @@ export class DeepnoteNotebookSerializer implements NotebookSerializer {
}
};
} catch (error) {
console.error('Error deserializing Deepnote notebook:', error);
logger.error('DeepnoteSerializer: Error deserializing Deepnote notebook', error);

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

return new TextEncoder().encode(yamlString);
} catch (error) {
console.error('Error serializing Deepnote notebook:', error);
logger.error('DeepnoteSerializer: Error serializing Deepnote notebook', error);
throw new Error(
`Failed to save Deepnote file: ${error instanceof Error ? error.message : 'Unknown error'}`
);
Expand Down
75 changes: 75 additions & 0 deletions src/notebooks/deepnote/integrations/integrationDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { inject, injectable } from 'inversify';

import { logger } from '../../../platform/logging';
import { IDeepnoteNotebookManager } from '../../types';
import { IntegrationStatus, IntegrationWithStatus } from './integrationTypes';
import { IIntegrationDetector, IIntegrationStorage } from './types';
import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils';

/**
* Service for detecting integrations used in Deepnote notebooks
*/
@injectable()
export class IntegrationDetector implements IIntegrationDetector {
constructor(
@inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage,
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager
) {}

/**
* Detect all integrations used in the given project
*/
async detectIntegrations(projectId: string): Promise<Map<string, IntegrationWithStatus>> {
// Get the project
const project = this.notebookManager.getOriginalProject(projectId);
if (!project) {
logger.warn(
`IntegrationDetector: No project found for ID: ${projectId}. The project may not have been loaded yet.`
);
return new Map();
}

logger.debug(
`IntegrationDetector: Scanning project ${projectId} with ${project.project.notebooks.length} notebooks`
);

// Collect all blocks with SQL integration metadata from all notebooks
const blocksWithIntegrations: BlockWithIntegration[] = [];
for (const notebook of project.project.notebooks) {
logger.trace(`IntegrationDetector: Scanning notebook ${notebook.id} with ${notebook.blocks.length} blocks`);

for (const block of notebook.blocks) {
// Check if this is a code block with SQL integration metadata
if (block.type === 'code' && block.metadata?.sql_integration_id) {
blocksWithIntegrations.push({
id: block.id,
sql_integration_id: block.metadata.sql_integration_id
});
} else if (block.type === 'code') {
logger.trace(
`IntegrationDetector: Block ${block.id} has no sql_integration_id. Metadata:`,
block.metadata
);
}
}
}

// Use the shared utility to scan blocks and build the status map
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationDetector');
}

/**
* Check if a project has any unconfigured integrations
*/
async hasUnconfiguredIntegrations(projectId: string): Promise<boolean> {
const integrations = await this.detectIntegrations(projectId);

for (const integration of integrations.values()) {
if (integration.status === IntegrationStatus.Disconnected) {
return true;
}
}

return false;
}
}
Loading