Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
29 changes: 28 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 './deepnoteTypes';

/**
Expand Down Expand Up @@ -75,6 +75,33 @@ 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);
}

return true;
}

/**
* Checks if the init notebook has already been run for a project.
* @param projectId Project identifier
Expand Down
88 changes: 88 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,94 @@ 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);
});
});

suite('integration scenarios', () => {
test('should handle complete workflow for multiple projects', () => {
manager.storeOriginalProject('project-1', mockProject, 'notebook-1');
Expand Down
70 changes: 43 additions & 27 deletions src/notebooks/deepnote/integrations/integrationDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ 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
} 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 +21,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 +34,44 @@ 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})`);

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

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

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