Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9bc4e44
refactor: rename IntegrationConfig to LegacyIntegrationConfig
jankuca Nov 1, 2025
3781507
replace notebook scanning for integrations with full project integrat…
jankuca Nov 1, 2025
702cb0a
move duckdb integration inclusion from env var provider to integratio…
jankuca Nov 1, 2025
dae0131
fix: scope integrations for env vars by the open project
jankuca Nov 1, 2025
558a6f0
consolidate duckdb and other integration processing
jankuca Nov 1, 2025
594624d
add `@deepnote/database-integrations` package
jankuca Nov 1, 2025
ebbbf01
fix import restriction violations in integration env var logic
jankuca Nov 1, 2025
ff380a1
rename IntegrationType to LegacyIntegrationType
jankuca Nov 1, 2025
f614156
add legacy integration config upgrading logic
jankuca Nov 1, 2025
c98fe32
prefix integration config types with "legacy" to differentiate from new
jankuca Nov 1, 2025
70580c1
migrate integration storage to format of deepnote/database-integrations
jankuca Nov 1, 2025
392f52c
replace integration env var logic with deepnote/database-integrations
jankuca Nov 1, 2025
d1e8f49
prevent saving and loading of duckdb integrations via integration sto…
jankuca Nov 1, 2025
4a7c399
migrate all components to the new database integration config format
jankuca Nov 1, 2025
94ba87e
localize "unsupported integration type" message
jankuca Nov 1, 2025
1a9b7de
test: add tests for updated sql env var provider
jankuca Nov 1, 2025
526d11c
test: add tests for updated integrationStorage
jankuca Nov 1, 2025
481bed9
test: add tests for legacy->new config conversions
jankuca Nov 1, 2025
e75ddeb
fix tests
jankuca Nov 2, 2025
70882c2
fix postgres ssl flag migration from legacy config
jankuca Nov 2, 2025
0cf233a
use node assert in SQL env var tests
jankuca Nov 2, 2025
f4f04ab
update INTEGRATIONS_CREDENTIALS document
jankuca Nov 2, 2025
0db9a09
upgrade deepnote/database-integrations (fix snowflake URL construction)
jankuca Nov 2, 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
343 changes: 280 additions & 63 deletions INTEGRATIONS_CREDENTIALS.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2455,6 +2455,7 @@
"@c4312/evt": "^0.1.1",
"@deepnote/blocks": "^1.3.5",
"@deepnote/convert": "^1.2.0",
"@deepnote/database-integrations": "^1.1.1",
"@enonic/fnv-plus": "^1.3.0",
"@jupyter-widgets/base": "^6.0.8",
"@jupyter-widgets/controls": "^5.0.9",
Expand Down
1 change: 1 addition & 0 deletions src/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export type LocalizedMessages = {
integrationsRequiredField: string;
integrationsOptionalField: string;
integrationsUnnamedIntegration: string;
integrationsUnsupportedIntegrationType: string;
// Select input settings strings
selectInputSettingsTitle: string;
allowMultipleValues: string;
Expand Down
35 changes: 8 additions & 27 deletions src/notebooks/deepnote/integrations/integrationDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ import { inject, injectable } from 'inversify';

import { logger } from '../../../platform/logging';
import { IDeepnoteNotebookManager } from '../../types';
import {
DATAFRAME_SQL_INTEGRATION_ID,
DEEPNOTE_TO_INTEGRATION_TYPE,
IntegrationStatus,
IntegrationWithStatus,
RawIntegrationType
} from '../../../platform/notebooks/deepnote/integrationTypes';
import { IntegrationStatus, IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
import { IIntegrationDetector, IIntegrationStorage } from './types';
import { DatabaseIntegrationType, databaseIntegrationTypes } from '@deepnote/database-integrations';

/**
* Service for detecting integrations used in Deepnote notebooks
Expand Down Expand Up @@ -40,39 +35,25 @@ export class IntegrationDetector implements IIntegrationDetector {
const integrations = new Map<string, IntegrationWithStatus>();

// Use the project's integrations field as the source of truth
const projectIntegrations = project.project.integrations || [];
const projectIntegrations = project.project.integrations?.slice() ?? [];
logger.debug(`IntegrationDetector: Found ${projectIntegrations.length} integrations in project.integrations`);

for (const projectIntegration of projectIntegrations) {
const integrationId = projectIntegration.id;

// Skip the internal DuckDB integration
if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) {
continue;
}

logger.debug(`IntegrationDetector: Found integration: ${integrationId} (${projectIntegration.type})`);

// Map the Deepnote integration type to our IntegrationType
const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType];

// Skip unknown integration types
if (!integrationType) {
logger.warn(
`IntegrationDetector: Unknown integration type '${projectIntegration.type}' for integration ID '${integrationId}'. Skipping.`
);
const integrationType = projectIntegration.type;
if (!(databaseIntegrationTypes as readonly string[]).includes(integrationType)) {
logger.debug(`IntegrationDetector: Skipping unsupported integration type: ${integrationType}`);
continue;
}

// Check if the integration is configured
const config = await this.integrationStorage.getIntegrationConfig(integrationId);

const status: IntegrationWithStatus = {
config: config || null,
config: config ?? null,
status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected,
// Include integration metadata from project for prefilling when config is null
integrationName: projectIntegration.name,
integrationType: integrationType
integrationType: integrationType as DatabaseIntegrationType
};

integrations.set(integrationId, status);
Expand Down
70 changes: 10 additions & 60 deletions src/notebooks/deepnote/integrations/integrationManager.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { inject, injectable } from 'inversify';
import { commands, l10n, NotebookDocument, window, workspace } from 'vscode';
import { commands, l10n, window, workspace } from 'vscode';

import { IExtensionContext } from '../../../platform/common/types';
import { Commands } from '../../../platform/common/constants';
import { logger } from '../../../platform/logging';
import { IIntegrationDetector, IIntegrationManager, IIntegrationStorage, IIntegrationWebviewProvider } from './types';
import {
DEEPNOTE_TO_INTEGRATION_TYPE,
IntegrationStatus,
IntegrationType,
IntegrationWithStatus,
RawIntegrationType
} from '../../../platform/notebooks/deepnote/integrationTypes';
import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils';
import { IntegrationStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
import { IDeepnoteNotebookManager } from '../../types';
import { DatabaseIntegrationType, databaseIntegrationTypes } from '@deepnote/database-integrations';

/**
* Manages integration UI and commands for Deepnote notebooks
Expand Down Expand Up @@ -143,14 +137,6 @@ export class IntegrationManager implements IIntegrationManager {

// First try to detect integrations from the stored project
let integrations = await this.integrationDetector.detectIntegrations(projectId);

// If no integrations found in stored project, scan cells directly
// This handles the case where the notebook was already open when the extension loaded
if (integrations.size === 0) {
logger.debug(`IntegrationManager: No integrations found in stored project, scanning cells directly`);
integrations = await this.detectIntegrationsFromCells(activeNotebook);
}

logger.debug(`IntegrationManager: Found ${integrations.size} integrations`);

// If a specific integration was requested (e.g., from status bar click),
Expand All @@ -164,20 +150,15 @@ export class IntegrationManager implements IIntegrationManager {
const projectIntegration = project?.project.integrations?.find((i) => i.id === selectedIntegrationId);

let integrationName: string | undefined;
let integrationType: IntegrationType | undefined;
let integrationType: DatabaseIntegrationType | undefined;

if (projectIntegration) {
// Validate that projectIntegration.type against supported types
if (
projectIntegration &&
(databaseIntegrationTypes as readonly string[]).includes(projectIntegration.type)
) {
integrationName = projectIntegration.name;

// Validate that projectIntegration.type exists in the mapping before lookup
if (projectIntegration.type in DEEPNOTE_TO_INTEGRATION_TYPE) {
// Map the Deepnote integration type to our IntegrationType
integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType];
} else {
logger.warn(
`IntegrationManager: Unknown integration type '${projectIntegration.type}' for integration ID '${selectedIntegrationId}' in project '${projectId}'. Integration type will be undefined.`
);
}
integrationType = projectIntegration.type as DatabaseIntegrationType;
}

integrations.set(selectedIntegrationId, {
Expand All @@ -196,35 +177,4 @@ export class IntegrationManager implements IIntegrationManager {
// Show the webview with optional selected integration
await this.webviewProvider.show(projectId, integrations, selectedIntegrationId);
}

/**
* Detect integrations by scanning cells directly (fallback method)
* This is used when the project isn't stored in the notebook manager
*/
private async detectIntegrationsFromCells(notebook: NotebookDocument): Promise<Map<string, IntegrationWithStatus>> {
// Collect all cells with SQL integration metadata
const blocksWithIntegrations: BlockWithIntegration[] = [];

for (const cell of notebook.getCells()) {
const metadata = cell.metadata;
logger.trace(`IntegrationManager: Cell ${cell.index} metadata:`, metadata);

// Check cell metadata for sql_integration_id
if (metadata && typeof metadata === 'object') {
const integrationId = (metadata as Record<string, unknown>).sql_integration_id;
if (typeof integrationId === 'string') {
logger.debug(`IntegrationManager: Found integration ${integrationId} in cell ${cell.index}`);
blocksWithIntegrations.push({
id: `cell-${cell.index}`,
sql_integration_id: integrationId
});
}
}
}

logger.debug(`IntegrationManager: Found ${blocksWithIntegrations.length} cells with integrations`);

// Use the shared utility to scan blocks and build the status map
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationManager');
}
}
68 changes: 0 additions & 68 deletions src/notebooks/deepnote/integrations/integrationUtils.ts

This file was deleted.

25 changes: 10 additions & 15 deletions src/notebooks/deepnote/integrations/integrationWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,8 @@ import { logger } from '../../../platform/logging';
import { LocalizedMessages, SharedMessages } from '../../../messageTypes';
import { IDeepnoteNotebookManager, ProjectIntegration } from '../../types';
import { IIntegrationStorage, IIntegrationWebviewProvider } from './types';
import {
INTEGRATION_TYPE_TO_DEEPNOTE,
IntegrationConfig,
IntegrationStatus,
IntegrationWithStatus,
RawIntegrationType
} from '../../../platform/notebooks/deepnote/integrationTypes';
import { IntegrationStatus, IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
import { DatabaseIntegrationConfig } from '@deepnote/database-integrations';

/**
* Manages the webview panel for integration configuration
Expand Down Expand Up @@ -182,7 +177,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
integrationsSnowflakeRolePlaceholder: localize.Integrations.snowflakeRolePlaceholder,
integrationsSnowflakeWarehouseLabel: localize.Integrations.snowflakeWarehouseLabel,
integrationsSnowflakeWarehousePlaceholder: localize.Integrations.snowflakeWarehousePlaceholder,
integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}')
integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}'),
integrationsUnsupportedIntegrationType: localize.Integrations.unsupportedIntegrationType('{0}')
};

await this.currentPanel.webview.postMessage({
Expand Down Expand Up @@ -221,7 +217,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
private async handleMessage(message: {
type: string;
integrationId?: string;
config?: IntegrationConfig;
config?: DatabaseIntegrationConfig;
}): Promise<void> {
switch (message.type) {
case 'configure':
Expand Down Expand Up @@ -263,7 +259,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
/**
* Save the configuration for an integration
*/
private async saveConfiguration(integrationId: string, config: IntegrationConfig): Promise<void> {
private async saveConfiguration(integrationId: string, config: DatabaseIntegrationConfig): Promise<void> {
try {
await this.integrationStorage.save(config);

Expand Down Expand Up @@ -349,17 +345,16 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
return null;
}

// Map to Deepnote integration type
const deepnoteType: RawIntegrationType | undefined = INTEGRATION_TYPE_TO_DEEPNOTE[type];
if (!deepnoteType) {
logger.warn(`IntegrationWebviewProvider: Cannot map type ${type} for integration ${id}, skipping`);
// Skip DuckDB integration (internal, not a real Deepnote integration)
if (type === 'pandas-dataframe') {
logger.trace(`IntegrationWebviewProvider: Skipping internal DuckDB integration ${id}`);
return null;
}

return {
id,
name: integration.config?.name || integration.integrationName || id,
type: deepnoteType
type
};
})
.filter((integration): integration is ProjectIntegration => integration !== null);
Expand Down
Loading