Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dntk",
"dont",
"DONT",
"duckdb",
"ename",
"evalue",
"findstr",
Expand All @@ -42,6 +43,7 @@
"millis",
"nbformat",
"numpy",
"pgsql",
"pids",
"Pids",
"PYTHONHOME",
Expand Down
3 changes: 3 additions & 0 deletions src/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ export type LocalizedMessages = {
integrationsConfigureTitle: string;
integrationsCancel: string;
integrationsSave: string;
// Integration type labels
integrationsPostgresTypeLabel: string;
integrationsBigQueryTypeLabel: string;
// PostgreSQL form strings
integrationsPostgresNameLabel: string;
integrationsPostgresNamePlaceholder: string;
Expand Down
31 changes: 30 additions & 1 deletion src/notebooks/deepnote/deepnoteNotebookManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { injectable } from 'inversify';

import { IDeepnoteNotebookManager } from '../types';
import { IDeepnoteNotebookManager, ProjectIntegration } from '../types';
import type { DeepnoteProject } from '../../platform/deepnote/deepnoteTypes';

/**
Expand Down Expand Up @@ -75,6 +75,35 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager {
this.currentNotebookId.set(projectId, notebookId);
}

/**
* Updates the integrations list in the project data.
* This modifies the stored project to reflect changes in configured integrations.
*
* @param projectId - Project identifier
* @param integrations - Array of integration metadata to store in the project
* @returns `true` if the project was found and updated successfully, `false` if the project does not exist
*/
updateProjectIntegrations(projectId: string, integrations: ProjectIntegration[]): boolean {
const project = this.originalProjects.get(projectId);

if (!project) {
return false;
}

const updatedProject = JSON.parse(JSON.stringify(project)) as DeepnoteProject;
updatedProject.project.integrations = integrations;

const currentNotebookId = this.currentNotebookId.get(projectId);

if (currentNotebookId) {
this.storeOriginalProject(projectId, updatedProject, currentNotebookId);
} else {
this.originalProjects.set(projectId, updatedProject);
}

return true;
}

/**
* Checks if the init notebook has already been run for a project.
* @param projectId Project identifier
Expand Down
111 changes: 111 additions & 0 deletions src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,117 @@ suite('DeepnoteNotebookManager', () => {
});
});

suite('updateProjectIntegrations', () => {
test('should update integrations list for existing project and return true', () => {
manager.storeOriginalProject('project-123', mockProject, 'notebook-456');

const integrations = [
{ id: 'int-1', name: 'PostgreSQL', type: 'pgsql' },
{ id: 'int-2', name: 'BigQuery', type: 'big-query' }
];

const result = manager.updateProjectIntegrations('project-123', integrations);

assert.strictEqual(result, true);

const updatedProject = manager.getOriginalProject('project-123');
assert.deepStrictEqual(updatedProject?.project.integrations, integrations);
});

test('should replace existing integrations list and return true', () => {
const projectWithIntegrations: DeepnoteProject = {
...mockProject,
project: {
...mockProject.project,
integrations: [{ id: 'old-int', name: 'Old Integration', type: 'pgsql' }]
}
};

manager.storeOriginalProject('project-123', projectWithIntegrations, 'notebook-456');

const newIntegrations = [
{ id: 'new-int-1', name: 'New Integration 1', type: 'pgsql' },
{ id: 'new-int-2', name: 'New Integration 2', type: 'big-query' }
];

const result = manager.updateProjectIntegrations('project-123', newIntegrations);

assert.strictEqual(result, true);

const updatedProject = manager.getOriginalProject('project-123');
assert.deepStrictEqual(updatedProject?.project.integrations, newIntegrations);
});

test('should handle empty integrations array and return true', () => {
const projectWithIntegrations: DeepnoteProject = {
...mockProject,
project: {
...mockProject.project,
integrations: [{ id: 'int-1', name: 'Integration 1', type: 'pgsql' }]
}
};

manager.storeOriginalProject('project-123', projectWithIntegrations, 'notebook-456');

const result = manager.updateProjectIntegrations('project-123', []);

assert.strictEqual(result, true);

const updatedProject = manager.getOriginalProject('project-123');
assert.deepStrictEqual(updatedProject?.project.integrations, []);
});

test('should return false for unknown project', () => {
const result = manager.updateProjectIntegrations('unknown-project', [
{ id: 'int-1', name: 'Integration', type: 'pgsql' }
]);

assert.strictEqual(result, false);

const project = manager.getOriginalProject('unknown-project');
assert.strictEqual(project, undefined);
});

test('should preserve other project properties and return true', () => {
manager.storeOriginalProject('project-123', mockProject, 'notebook-456');

const integrations = [{ id: 'int-1', name: 'PostgreSQL', type: 'pgsql' }];

const result = manager.updateProjectIntegrations('project-123', integrations);

assert.strictEqual(result, true);

const updatedProject = manager.getOriginalProject('project-123');
assert.strictEqual(updatedProject?.project.id, mockProject.project.id);
assert.strictEqual(updatedProject?.project.name, mockProject.project.name);
assert.strictEqual(updatedProject?.version, mockProject.version);
assert.deepStrictEqual(updatedProject?.metadata, mockProject.metadata);
});

test('should update integrations when currentNotebookId is undefined and return true', () => {
// Store project with a notebook ID, then clear it to simulate the edge case
manager.storeOriginalProject('project-123', mockProject, 'notebook-456');
manager.updateCurrentNotebookId('project-123', undefined as any);

const integrations = [
{ id: 'int-1', name: 'PostgreSQL', type: 'pgsql' },
{ id: 'int-2', name: 'BigQuery', type: 'big-query' }
];

const result = manager.updateProjectIntegrations('project-123', integrations);

assert.strictEqual(result, true);

const updatedProject = manager.getOriginalProject('project-123');
assert.deepStrictEqual(updatedProject?.project.integrations, integrations);
// Verify other properties remain unchanged
assert.strictEqual(updatedProject?.project.id, mockProject.project.id);
assert.strictEqual(updatedProject?.project.name, mockProject.project.name);
assert.strictEqual(updatedProject?.version, mockProject.version);
assert.deepStrictEqual(updatedProject?.metadata, mockProject.metadata);
});
});

suite('integration scenarios', () => {
test('should handle complete workflow for multiple projects', () => {
manager.storeOriginalProject('project-1', mockProject, 'notebook-1');
Expand Down
79 changes: 52 additions & 27 deletions src/notebooks/deepnote/integrations/integrationDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { inject, injectable } from 'inversify';

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

/**
* Service for detecting integrations used in Deepnote notebooks
Expand All @@ -17,7 +22,8 @@ export class IntegrationDetector implements IIntegrationDetector {
) {}

/**
* Detect all integrations used in the given project
* Detect all integrations used in the given project.
* Uses the project's integrations field as the source of truth.
*/
async detectIntegrations(projectId: string): Promise<Map<string, IntegrationWithStatus>> {
// Get the project
Expand All @@ -29,33 +35,52 @@ export class IntegrationDetector implements IIntegrationDetector {
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
);
}
logger.debug(`IntegrationDetector: Scanning project ${projectId} for integrations`);

const integrations = new Map<string, IntegrationWithStatus>();

// Use the project's integrations field as the source of truth
const projectIntegrations = project.project.integrations || [];
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.`
);
continue;
}

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

const status: IntegrationWithStatus = {
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
};

integrations.set(integrationId, status);
}

// Use the shared utility to scan blocks and build the status map
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationDetector');
logger.debug(`IntegrationDetector: Found ${integrations.size} integrations`);

return integrations;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class IntegrationManager implements IIntegrationManager {
}

// Show the webview with optional selected integration
await this.webviewProvider.show(integrations, selectedIntegrationId);
await this.webviewProvider.show(projectId, integrations, selectedIntegrationId);
}

/**
Expand Down
Loading