From 59b90a90c05b5f71494116eceea8fcf2c41ba6b9 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 12:14:14 +0200 Subject: [PATCH 01/15] feat: replace integration listing from blocks with integration list from project --- .../deepnote/deepnoteNotebookManager.ts | 26 ++++ .../integrations/integrationDetector.ts | 70 +++++---- .../integrations/integrationManager.ts | 2 +- .../integrations/integrationWebview.ts | 71 ++++++++- src/notebooks/deepnote/integrations/types.ts | 7 +- .../deepnote/sqlCellStatusBarProvider.ts | 53 +++++-- .../sqlCellStatusBarProvider.unit.test.ts | 145 ++++++++++++------ src/notebooks/types.ts | 1 + .../notebooks/deepnote/integrationTypes.ts | 34 ++++ .../integrations/BigQueryForm.tsx | 17 +- .../integrations/ConfigurationForm.tsx | 14 +- .../integrations/IntegrationPanel.tsx | 8 +- .../integrations/PostgresForm.tsx | 17 +- .../webview-side/integrations/types.ts | 4 + 14 files changed, 364 insertions(+), 105 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.ts b/src/notebooks/deepnote/deepnoteNotebookManager.ts index 3c324fb663..797cc83b4e 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.ts @@ -75,6 +75,32 @@ 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 + */ + updateProjectIntegrations( + projectId: string, + integrations: Array<{ id: string; name: string; type: string }> + ): void { + const project = this.originalProjects.get(projectId); + + if (!project) { + return; + } + + 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); + } + } + /** * Checks if the init notebook has already been run for a project. * @param projectId Project identifier diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index dac3d044d4..35e5d064e7 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -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, + IntegrationStatus, + IntegrationWithStatus, + mapDeepnoteIntegrationType +} from '../../../platform/notebooks/deepnote/integrationTypes'; import { IIntegrationDetector, IIntegrationStorage } from './types'; -import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils'; /** * Service for detecting integrations used in Deepnote notebooks @@ -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> { // Get the project @@ -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(); + + // 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 = mapDeepnoteIntegrationType(projectIntegration.type); + + const status: IntegrationWithStatus = { + config: config || null, + status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected, + // Include project metadata for prefilling when config is null + projectName: projectIntegration.name, + projectType: 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; } /** diff --git a/src/notebooks/deepnote/integrations/integrationManager.ts b/src/notebooks/deepnote/integrations/integrationManager.ts index c138cc42b6..3c4b864dac 100644 --- a/src/notebooks/deepnote/integrations/integrationManager.ts +++ b/src/notebooks/deepnote/integrations/integrationManager.ts @@ -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); } /** diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index bbcbd27660..0faa2a4597 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -5,11 +5,13 @@ import { IExtensionContext } from '../../../platform/common/types'; import * as localize from '../../../platform/common/utils/localize'; import { logger } from '../../../platform/logging'; import { LocalizedMessages, SharedMessages } from '../../../messageTypes'; +import { IDeepnoteNotebookManager } from '../../types'; import { IIntegrationStorage, IIntegrationWebviewProvider } from './types'; import { IntegrationConfig, IntegrationStatus, - IntegrationWithStatus + IntegrationWithStatus, + mapToDeepnoteIntegrationType } from '../../../platform/notebooks/deepnote/integrationTypes'; /** @@ -23,18 +25,27 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { private integrations: Map = new Map(); + private projectId: string | undefined; + constructor( @inject(IExtensionContext) private readonly extensionContext: IExtensionContext, - @inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage + @inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage, + @inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager ) {} /** * Show the integration management webview + * @param projectId The Deepnote project ID * @param integrations Map of integration IDs to their status * @param selectedIntegrationId Optional integration ID to select/configure immediately */ - public async show(integrations: Map, selectedIntegrationId?: string): Promise { - // Update the stored integrations with the latest data + public async show( + projectId: string, + integrations: Map, + selectedIntegrationId?: string + ): Promise { + // Update the stored integrations and project ID with the latest data + this.projectId = projectId; this.integrations = integrations; const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : ViewColumn.One; @@ -161,6 +172,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { const integrationsData = Array.from(this.integrations.entries()).map(([id, integration]) => ({ config: integration.config, id, + projectName: integration.projectName, + projectType: integration.projectType, status: integration.status })); logger.debug(`IntegrationWebviewProvider: Sending ${integrationsData.length} integrations to webview`); @@ -210,6 +223,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { await this.currentPanel?.webview.postMessage({ config: integration.config, integrationId, + projectName: integration.projectName, + projectType: integration.projectType, type: 'showForm' }); } @@ -229,6 +244,9 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { this.integrations.set(integrationId, integration); } + // Update the project's integrations list + await this.updateProjectIntegrationsList(); + await this.updateWebview(); await this.currentPanel?.webview.postMessage({ message: l10n.t('Configuration saved successfully'), @@ -261,6 +279,9 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { this.integrations.set(integrationId, integration); } + // Update the project's integrations list + await this.updateProjectIntegrationsList(); + await this.updateWebview(); await this.currentPanel?.webview.postMessage({ message: l10n.t('Configuration deleted successfully'), @@ -278,6 +299,48 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { } } + /** + * Update the project's integrations list based on current integrations + */ + private async updateProjectIntegrationsList(): Promise { + if (!this.projectId) { + logger.warn('IntegrationWebviewProvider: No project ID available, skipping project update'); + return; + } + + // Build the integrations list from current integrations + const projectIntegrations = Array.from(this.integrations.entries()) + .map(([id, integration]) => { + // Get the integration type from config or project metadata + const type = integration.config?.type || integration.projectType; + if (!type) { + logger.warn(`IntegrationWebviewProvider: No type found for integration ${id}, skipping`); + return null; + } + + // Map to Deepnote integration type + const deepnoteType = mapToDeepnoteIntegrationType(type); + if (!deepnoteType) { + logger.warn(`IntegrationWebviewProvider: Cannot map type ${type} for integration ${id}, skipping`); + return null; + } + + return { + id, + name: integration.config?.name || integration.projectName || id, + type: deepnoteType + }; + }) + .filter((integration): integration is { id: string; name: string; type: string } => integration !== null); + + logger.debug( + `IntegrationWebviewProvider: Updating project ${this.projectId} with ${projectIntegrations.length} integrations` + ); + + // Update the project in the notebook manager + this.notebookManager.updateProjectIntegrations(this.projectId, projectIntegrations); + } + /** * Get the HTML content for the webview (React-based) */ diff --git a/src/notebooks/deepnote/integrations/types.ts b/src/notebooks/deepnote/integrations/types.ts index 254f3c8912..d38a3cd2ed 100644 --- a/src/notebooks/deepnote/integrations/types.ts +++ b/src/notebooks/deepnote/integrations/types.ts @@ -20,10 +20,15 @@ export const IIntegrationWebviewProvider = Symbol('IIntegrationWebviewProvider') export interface IIntegrationWebviewProvider { /** * Show the integration management webview + * @param projectId The Deepnote project ID * @param integrations Map of integration IDs to their status * @param selectedIntegrationId Optional integration ID to select/configure immediately */ - show(integrations: Map, selectedIntegrationId?: string): Promise; + show( + projectId: string, + integrations: Map, + selectedIntegrationId?: string + ): Promise; } export const IIntegrationManager = Symbol('IIntegrationManager'); diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 4637b18cbb..91505c05a3 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -22,7 +22,12 @@ import { IExtensionSyncActivationService } from '../../platform/activation/types import { IDisposableRegistry } from '../../platform/common/types'; import { IIntegrationStorage } from './integrations/types'; import { Commands } from '../../platform/common/constants'; -import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes'; +import { + DATAFRAME_SQL_INTEGRATION_ID, + IntegrationType, + mapDeepnoteIntegrationType +} from '../../platform/notebooks/deepnote/integrationTypes'; +import { IDeepnoteNotebookManager } from '../types'; /** * QuickPick item with an integration ID @@ -42,7 +47,8 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid constructor( @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - @inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage + @inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage, + @inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager ) {} public activate(): void { @@ -285,17 +291,30 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid private async switchIntegration(cell: NotebookCell): Promise { const currentIntegrationId = this.getIntegrationId(cell); - // Get all available integrations - const allIntegrations = await this.integrationStorage.getAll(); + // Get the project ID from the notebook metadata + const projectId = cell.notebook.metadata?.deepnoteProjectId; + if (!projectId) { + void window.showErrorMessage(l10n.t('Cannot determine project ID')); + return; + } + + // Get the project to access its integrations list + const project = this.notebookManager.getOriginalProject(projectId); + if (!project) { + void window.showErrorMessage(l10n.t('Project not found')); + return; + } - // Build quick pick items + // Build quick pick items from project integrations const items: (QuickPickItem | LocalQuickPickItem)[] = []; - // Check if current integration is unknown (not in the list) + const projectIntegrations = project.project.integrations || []; + + // Check if current integration is unknown (not in the project's list) const isCurrentIntegrationUnknown = currentIntegrationId && currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID && - !allIntegrations.some((i) => i.id === currentIntegrationId); + !projectIntegrations.some((i) => i.id === currentIntegrationId); // Add current unknown integration first if it exists if (isCurrentIntegrationUnknown && currentIntegrationId) { @@ -308,15 +327,21 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid items.push(item); } - // Add all configured integrations - for (const integration of allIntegrations) { - const typeLabel = this.getIntegrationTypeLabel(integration.type); + // Add all project integrations + for (const projectIntegration of projectIntegrations) { + // Skip the internal DuckDB integration + if (projectIntegration.id === DATAFRAME_SQL_INTEGRATION_ID) { + continue; + } + + const integrationType = mapDeepnoteIntegrationType(projectIntegration.type); + const typeLabel = integrationType ? this.getIntegrationTypeLabel(integrationType) : projectIntegration.type; + const item: LocalQuickPickItem = { - label: integration.name || integration.id, + label: projectIntegration.name || projectIntegration.id, description: typeLabel, - detail: integration.id === currentIntegrationId ? l10n.t('Currently selected') : undefined, - // Store the integration ID in a custom property - id: integration.id + detail: projectIntegration.id === currentIntegrationId ? l10n.t('Currently selected') : undefined, + id: projectIntegration.id }; items.push(item); } diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 209ecb3c36..7ef62e8aa4 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -18,17 +18,20 @@ import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationType } from '../../platform/no import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; import { createEventHandler } from '../../test/common'; import { Commands } from '../../platform/common/constants'; +import { IDeepnoteNotebookManager } from '../types'; suite('SqlCellStatusBarProvider', () => { let provider: SqlCellStatusBarProvider; let disposables: IDisposableRegistry; let integrationStorage: IIntegrationStorage; + let notebookManager: IDeepnoteNotebookManager; let cancellationToken: CancellationToken; setup(() => { disposables = []; integrationStorage = mock(); - provider = new SqlCellStatusBarProvider(disposables, instance(integrationStorage)); + notebookManager = mock(); + provider = new SqlCellStatusBarProvider(disposables, instance(integrationStorage), instance(notebookManager)); const tokenSource = new CancellationTokenSource(); cancellationToken = tokenSource.token; @@ -249,12 +252,18 @@ suite('SqlCellStatusBarProvider', () => { let activateDisposables: IDisposableRegistry; let activateProvider: SqlCellStatusBarProvider; let activateIntegrationStorage: IIntegrationStorage; + let activateNotebookManager: IDeepnoteNotebookManager; setup(() => { resetVSCodeMocks(); activateDisposables = []; activateIntegrationStorage = mock(); - activateProvider = new SqlCellStatusBarProvider(activateDisposables, instance(activateIntegrationStorage)); + activateNotebookManager = mock(); + activateProvider = new SqlCellStatusBarProvider( + activateDisposables, + instance(activateIntegrationStorage), + instance(activateNotebookManager) + ); }); teardown(() => { @@ -298,11 +307,17 @@ suite('SqlCellStatusBarProvider', () => { let eventDisposables: IDisposableRegistry; let eventProvider: SqlCellStatusBarProvider; let eventIntegrationStorage: IIntegrationStorage; + let eventNotebookManager: IDeepnoteNotebookManager; setup(() => { eventDisposables = []; eventIntegrationStorage = mock(); - eventProvider = new SqlCellStatusBarProvider(eventDisposables, instance(eventIntegrationStorage)); + eventNotebookManager = mock(); + eventProvider = new SqlCellStatusBarProvider( + eventDisposables, + instance(eventIntegrationStorage), + instance(eventNotebookManager) + ); }); test('fires onDidChangeCellStatusBarItems when integration storage changes', () => { @@ -352,13 +367,19 @@ suite('SqlCellStatusBarProvider', () => { let commandDisposables: IDisposableRegistry; let commandProvider: SqlCellStatusBarProvider; let commandIntegrationStorage: IIntegrationStorage; + let commandNotebookManager: IDeepnoteNotebookManager; let updateVariableNameHandler: Function; setup(() => { resetVSCodeMocks(); commandDisposables = []; commandIntegrationStorage = mock(); - commandProvider = new SqlCellStatusBarProvider(commandDisposables, instance(commandIntegrationStorage)); + commandNotebookManager = mock(); + commandProvider = new SqlCellStatusBarProvider( + commandDisposables, + instance(commandIntegrationStorage), + instance(commandNotebookManager) + ); // Capture the command handler when( @@ -473,13 +494,19 @@ suite('SqlCellStatusBarProvider', () => { let commandDisposables: IDisposableRegistry; let commandProvider: SqlCellStatusBarProvider; let commandIntegrationStorage: IIntegrationStorage; + let commandNotebookManager: IDeepnoteNotebookManager; let switchIntegrationHandler: Function; setup(() => { resetVSCodeMocks(); commandDisposables = []; commandIntegrationStorage = mock(); - commandProvider = new SqlCellStatusBarProvider(commandDisposables, instance(commandIntegrationStorage)); + commandNotebookManager = mock(); + commandProvider = new SqlCellStatusBarProvider( + commandDisposables, + instance(commandIntegrationStorage), + instance(commandNotebookManager) + ); // Capture the command handler when(mockedVSCodeNamespaces.commands.registerCommand('deepnote.switchSqlIntegration', anything())).thenCall( @@ -501,24 +528,23 @@ suite('SqlCellStatusBarProvider', () => { }); test('updates cell metadata with selected integration', async () => { - const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }, notebookMetadata); const newIntegrationId = 'new-integration'; - when(commandIntegrationStorage.getAll()).thenReturn( - Promise.resolve([ - { - id: newIntegrationId, - name: 'New Integration', - type: IntegrationType.Postgres, - host: 'localhost', - port: 5432, - database: 'test', - username: 'user', - password: 'pass' - } - ]) - ); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [ + { + id: newIntegrationId, + name: 'New Integration', + type: 'pgsql' + } + ] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) ); @@ -531,9 +557,15 @@ suite('SqlCellStatusBarProvider', () => { }); test('does not update if user cancels quick pick', async () => { - const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }, notebookMetadata); - when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( Promise.resolve(undefined) ); @@ -545,10 +577,16 @@ suite('SqlCellStatusBarProvider', () => { }); test('shows error message if workspace edit fails', async () => { - const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }, notebookMetadata); const newIntegrationId = 'new-integration'; - when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) ); @@ -560,10 +598,16 @@ suite('SqlCellStatusBarProvider', () => { }); test('fires onDidChangeCellStatusBarItems after successful update', async () => { - const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }, notebookMetadata); const newIntegrationId = 'new-integration'; - when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) ); @@ -581,9 +625,15 @@ suite('SqlCellStatusBarProvider', () => { }); test('executes manage integrations command when configure option is selected', async () => { - const cell = createMockCell('sql', { sql_integration_id: 'current-integration' }); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: 'current-integration' }, notebookMetadata); - when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( Promise.resolve({ id: '__configure__', label: 'Configure current integration' } as any) ); @@ -600,10 +650,16 @@ suite('SqlCellStatusBarProvider', () => { }); test('includes DuckDB integration in quick pick items', async () => { - const cell = createMockCell('sql', {}); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', {}, notebookMetadata); let quickPickItems: any[] = []; - when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenCall((items) => { quickPickItems = items; return Promise.resolve(undefined); @@ -618,23 +674,22 @@ suite('SqlCellStatusBarProvider', () => { test('marks current integration as selected in quick pick', async () => { const currentIntegrationId = 'current-integration'; - const cell = createMockCell('sql', { sql_integration_id: currentIntegrationId }); + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: currentIntegrationId }, notebookMetadata); let quickPickItems: any[] = []; - when(commandIntegrationStorage.getAll()).thenReturn( - Promise.resolve([ - { - id: currentIntegrationId, - name: 'Current Integration', - type: IntegrationType.Postgres, - host: 'localhost', - port: 5432, - database: 'test', - username: 'user', - password: 'pass' - } - ]) - ); + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [ + { + id: currentIntegrationId, + name: 'Current Integration', + type: 'pgsql' + } + ] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenCall((items) => { quickPickItems = items; return Promise.resolve(undefined); diff --git a/src/notebooks/types.ts b/src/notebooks/types.ts index 21f5c92a34..040e422170 100644 --- a/src/notebooks/types.ts +++ b/src/notebooks/types.ts @@ -33,6 +33,7 @@ export interface IDeepnoteNotebookManager { selectNotebookForProject(projectId: string, notebookId: string): void; storeOriginalProject(projectId: string, project: DeepnoteProject, notebookId: string): void; updateCurrentNotebookId(projectId: string, notebookId: string): void; + updateProjectIntegrations(projectId: string, integrations: Array<{ id: string; name: string; type: string }>): void; hasInitNotebookBeenRun(projectId: string): boolean; markInitNotebookAsRun(projectId: string): void; } diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index 048c0ff13a..ee1a8f8f0d 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -12,6 +12,32 @@ export enum IntegrationType { BigQuery = 'bigquery' } +/** + * Map Deepnote integration type strings to our IntegrationType enum + */ +export function mapDeepnoteIntegrationType(deepnoteType: string): IntegrationType | undefined { + switch (deepnoteType) { + case 'pgsql': + return IntegrationType.Postgres; + case 'big-query': + return IntegrationType.BigQuery; + default: + return undefined; + } +} + +/** + * Map our IntegrationType enum to Deepnote integration type strings + */ +export function mapToDeepnoteIntegrationType(type: IntegrationType): string { + switch (type) { + case IntegrationType.Postgres: + return 'pgsql'; + case IntegrationType.BigQuery: + return 'big-query'; + } +} + /** * Base interface for all integration configurations */ @@ -64,4 +90,12 @@ export interface IntegrationWithStatus { config: IntegrationConfig | null; status: IntegrationStatus; error?: string; + /** + * Name from the project's integrations list (used for prefilling when config is null) + */ + projectName?: string; + /** + * Type from the project's integrations list (used for prefilling when config is null) + */ + projectType?: IntegrationType; } diff --git a/src/webviews/webview-side/integrations/BigQueryForm.tsx b/src/webviews/webview-side/integrations/BigQueryForm.tsx index cad543fce3..b80d1aea55 100644 --- a/src/webviews/webview-side/integrations/BigQueryForm.tsx +++ b/src/webviews/webview-side/integrations/BigQueryForm.tsx @@ -5,17 +5,24 @@ import { BigQueryIntegrationConfig } from './types'; export interface IBigQueryFormProps { integrationId: string; existingConfig: BigQueryIntegrationConfig | null; + projectName?: string; onSave: (config: BigQueryIntegrationConfig) => void; onCancel: () => void; } -export const BigQueryForm: React.FC = ({ integrationId, existingConfig, onSave, onCancel }) => { - const [name, setName] = React.useState(existingConfig?.name || ''); +export const BigQueryForm: React.FC = ({ + integrationId, + existingConfig, + projectName, + onSave, + onCancel +}) => { + const [name, setName] = React.useState(existingConfig?.name || projectName || ''); const [projectId, setProjectId] = React.useState(existingConfig?.projectId || ''); const [credentials, setCredentials] = React.useState(existingConfig?.credentials || ''); const [credentialsError, setCredentialsError] = React.useState(null); - // Update form fields when existingConfig changes + // Update form fields when existingConfig or projectName changes React.useEffect(() => { if (existingConfig) { setName(existingConfig.name || ''); @@ -23,12 +30,12 @@ export const BigQueryForm: React.FC = ({ integrationId, exis setCredentials(existingConfig.credentials || ''); setCredentialsError(null); } else { - setName(''); + setName(projectName || ''); setProjectId(''); setCredentials(''); setCredentialsError(null); } - }, [existingConfig]); + }, [existingConfig, projectName]); const validateCredentials = (value: string): boolean => { if (!value.trim()) { diff --git a/src/webviews/webview-side/integrations/ConfigurationForm.tsx b/src/webviews/webview-side/integrations/ConfigurationForm.tsx index 1d7c7c583c..834dec1818 100644 --- a/src/webviews/webview-side/integrations/ConfigurationForm.tsx +++ b/src/webviews/webview-side/integrations/ConfigurationForm.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import { getLocString } from '../react-common/locReactSide'; import { PostgresForm } from './PostgresForm'; import { BigQueryForm } from './BigQueryForm'; -import { IntegrationConfig } from './types'; +import { IntegrationConfig, IntegrationType } from './types'; export interface IConfigurationFormProps { integrationId: string; existingConfig: IntegrationConfig | null; + projectName?: string; + projectType?: IntegrationType; onSave: (config: IntegrationConfig) => void; onCancel: () => void; } @@ -14,14 +16,20 @@ export interface IConfigurationFormProps { export const ConfigurationForm: React.FC = ({ integrationId, existingConfig, + projectName, + projectType, onSave, onCancel }) => { - // Determine integration type from ID or existing config + // Determine integration type from existing config, project metadata, or ID const getIntegrationType = (): 'postgres' | 'bigquery' => { if (existingConfig) { return existingConfig.type; } + // Use project type if available + if (projectType) { + return projectType; + } // Infer from integration ID if (integrationId.includes('postgres')) { return 'postgres'; @@ -55,6 +63,7 @@ export const ConfigurationForm: React.FC = ({ @@ -62,6 +71,7 @@ export const ConfigurationForm: React.FC = ({ diff --git a/src/webviews/webview-side/integrations/IntegrationPanel.tsx b/src/webviews/webview-side/integrations/IntegrationPanel.tsx index d8d396f5bb..6372fa9342 100644 --- a/src/webviews/webview-side/integrations/IntegrationPanel.tsx +++ b/src/webviews/webview-side/integrations/IntegrationPanel.tsx @@ -3,7 +3,7 @@ import { IVsCodeApi } from '../react-common/postOffice'; import { getLocString, storeLocStrings } from '../react-common/locReactSide'; import { IntegrationList } from './IntegrationList'; import { ConfigurationForm } from './ConfigurationForm'; -import { IntegrationWithStatus, WebviewMessage, IntegrationConfig } from './types'; +import { IntegrationWithStatus, WebviewMessage, IntegrationConfig, IntegrationType } from './types'; export interface IIntegrationPanelProps { baseTheme: string; @@ -14,6 +14,8 @@ export const IntegrationPanel: React.FC = ({ baseTheme, const [integrations, setIntegrations] = React.useState([]); const [selectedIntegrationId, setSelectedIntegrationId] = React.useState(null); const [selectedConfig, setSelectedConfig] = React.useState(null); + const [selectedProjectName, setSelectedProjectName] = React.useState(undefined); + const [selectedProjectType, setSelectedProjectType] = React.useState(undefined); const [message, setMessage] = React.useState<{ type: 'success' | 'error'; text: string } | null>(null); const [confirmDelete, setConfirmDelete] = React.useState(null); @@ -51,6 +53,8 @@ export const IntegrationPanel: React.FC = ({ baseTheme, case 'showForm': setSelectedIntegrationId(msg.integrationId); setSelectedConfig(msg.config); + setSelectedProjectName(msg.projectName); + setSelectedProjectType(msg.projectType); break; case 'success': @@ -144,6 +148,8 @@ export const IntegrationPanel: React.FC = ({ baseTheme, diff --git a/src/webviews/webview-side/integrations/PostgresForm.tsx b/src/webviews/webview-side/integrations/PostgresForm.tsx index 0a037da8fa..ea24536fec 100644 --- a/src/webviews/webview-side/integrations/PostgresForm.tsx +++ b/src/webviews/webview-side/integrations/PostgresForm.tsx @@ -5,12 +5,19 @@ import { PostgresIntegrationConfig } from './types'; export interface IPostgresFormProps { integrationId: string; existingConfig: PostgresIntegrationConfig | null; + projectName?: string; onSave: (config: PostgresIntegrationConfig) => void; onCancel: () => void; } -export const PostgresForm: React.FC = ({ integrationId, existingConfig, onSave, onCancel }) => { - const [name, setName] = React.useState(existingConfig?.name || ''); +export const PostgresForm: React.FC = ({ + integrationId, + existingConfig, + projectName, + onSave, + onCancel +}) => { + const [name, setName] = React.useState(existingConfig?.name || projectName || ''); const [host, setHost] = React.useState(existingConfig?.host || ''); const [port, setPort] = React.useState(existingConfig?.port?.toString() || '5432'); const [database, setDatabase] = React.useState(existingConfig?.database || ''); @@ -18,7 +25,7 @@ export const PostgresForm: React.FC = ({ integrationId, exis const [password, setPassword] = React.useState(existingConfig?.password || ''); const [ssl, setSsl] = React.useState(existingConfig?.ssl || false); - // Update form fields when existingConfig changes + // Update form fields when existingConfig or projectName changes React.useEffect(() => { if (existingConfig) { setName(existingConfig.name || ''); @@ -29,7 +36,7 @@ export const PostgresForm: React.FC = ({ integrationId, exis setPassword(existingConfig.password || ''); setSsl(existingConfig.ssl || false); } else { - setName(''); + setName(projectName || ''); setHost(''); setPort('5432'); setDatabase(''); @@ -37,7 +44,7 @@ export const PostgresForm: React.FC = ({ integrationId, exis setPassword(''); setSsl(false); } - }, [existingConfig]); + }, [existingConfig, projectName]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); diff --git a/src/webviews/webview-side/integrations/types.ts b/src/webviews/webview-side/integrations/types.ts index f1d877f00e..7ceec3d2c7 100644 --- a/src/webviews/webview-side/integrations/types.ts +++ b/src/webviews/webview-side/integrations/types.ts @@ -30,6 +30,8 @@ export interface IntegrationWithStatus { id: string; config: IntegrationConfig | null; status: IntegrationStatus; + projectName?: string; + projectType?: IntegrationType; } export interface IVsCodeMessage { @@ -47,6 +49,8 @@ export interface ShowFormMessage { type: 'showForm'; integrationId: string; config: IntegrationConfig | null; + projectName?: string; + projectType?: IntegrationType; } export interface StatusMessage { From 01697cc48fdcb272f41751dadf25fa5718b2b53a Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 12:14:19 +0200 Subject: [PATCH 02/15] fix: show integration name/type from project when not configured --- src/messageTypes.ts | 3 +++ .../integrations/integrationWebview.ts | 2 ++ src/platform/common/utils/localize.ts | 4 ++++ .../integrations/IntegrationItem.tsx | 23 +++++++++++++++++-- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/messageTypes.ts b/src/messageTypes.ts index 4398a4d655..57d628f170 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -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; diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index 0faa2a4597..3041045f78 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -128,6 +128,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { integrationsConfirmResetMessage: localize.Integrations.confirmResetMessage, integrationsConfirmResetDetails: localize.Integrations.confirmResetDetails, integrationsConfigureTitle: localize.Integrations.configureTitle, + integrationsPostgresTypeLabel: localize.Integrations.postgresTypeLabel, + integrationsBigQueryTypeLabel: localize.Integrations.bigQueryTypeLabel, integrationsCancel: localize.Integrations.cancel, integrationsSave: localize.Integrations.save, integrationsRequiredField: localize.Integrations.requiredField, diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index ae961c9442..3d34f5f221 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -831,6 +831,10 @@ export namespace Integrations { export const requiredField = l10n.t('*'); export const optionalField = l10n.t('(optional)'); + // Integration type labels + export const postgresTypeLabel = l10n.t('PostgreSQL'); + export const bigQueryTypeLabel = l10n.t('BigQuery'); + // PostgreSQL form strings export const postgresNameLabel = l10n.t('Name (optional)'); export const postgresNamePlaceholder = l10n.t('My PostgreSQL Database'); diff --git a/src/webviews/webview-side/integrations/IntegrationItem.tsx b/src/webviews/webview-side/integrations/IntegrationItem.tsx index 6e756c2159..1b1c6ee0d7 100644 --- a/src/webviews/webview-side/integrations/IntegrationItem.tsx +++ b/src/webviews/webview-side/integrations/IntegrationItem.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { getLocString } from '../react-common/locReactSide'; -import { IntegrationWithStatus } from './types'; +import { IntegrationWithStatus, IntegrationType } from './types'; export interface IIntegrationItemProps { integration: IntegrationWithStatus; @@ -8,6 +8,17 @@ export interface IIntegrationItemProps { onDelete: (integrationId: string) => void; } +const getIntegrationTypeLabel = (type: IntegrationType): string => { + switch (type) { + case 'postgres': + return getLocString('integrationsPostgresTypeLabel', 'PostgreSQL'); + case 'bigquery': + return getLocString('integrationsBigQueryTypeLabel', 'BigQuery'); + default: + return type; + } +}; + export const IntegrationItem: React.FC = ({ integration, onConfigure, onDelete }) => { const statusClass = integration.status === 'connected' ? 'status-connected' : 'status-disconnected'; const statusText = @@ -17,7 +28,15 @@ export const IntegrationItem: React.FC = ({ integration, const configureText = integration.config ? getLocString('integrationsReconfigure', 'Reconfigure') : getLocString('integrationsConfigure', 'Configure'); - const displayName = integration.config?.name || integration.id; + + // Get the name: prefer config name, then project name, then ID + const name = integration.config?.name || integration.projectName || integration.id; + + // Get the type: prefer config type, then project type + const type = integration.config?.type || integration.projectType; + + // Build display name with type + const displayName = type ? `${name} (${getIntegrationTypeLabel(type)})` : name; return (
From a7b7fd575b69cb26a11ccb50d8820f8b087ea373 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 12:20:20 +0200 Subject: [PATCH 03/15] rename fields for clarity --- .../integrations/integrationDetector.ts | 6 ++--- .../integrations/integrationWebview.ts | 14 +++++------ .../notebooks/deepnote/integrationTypes.ts | 4 ++-- .../integrations/BigQueryForm.tsx | 12 +++++----- .../integrations/ConfigurationForm.tsx | 24 +++++++++---------- .../integrations/IntegrationItem.tsx | 8 +++---- .../integrations/IntegrationPanel.tsx | 14 ++++++----- .../integrations/PostgresForm.tsx | 12 +++++----- .../webview-side/integrations/types.ts | 8 +++---- 9 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index 35e5d064e7..ecd7848c17 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -61,9 +61,9 @@ export class IntegrationDetector implements IIntegrationDetector { const status: IntegrationWithStatus = { config: config || null, status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected, - // Include project metadata for prefilling when config is null - projectName: projectIntegration.name, - projectType: integrationType + // Include integration metadata from project for prefilling when config is null + integrationName: projectIntegration.name, + integrationType: integrationType }; integrations.set(integrationId, status); diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index 3041045f78..2b0add870c 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -174,8 +174,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { const integrationsData = Array.from(this.integrations.entries()).map(([id, integration]) => ({ config: integration.config, id, - projectName: integration.projectName, - projectType: integration.projectType, + integrationName: integration.integrationName, + integrationType: integration.integrationType, status: integration.status })); logger.debug(`IntegrationWebviewProvider: Sending ${integrationsData.length} integrations to webview`); @@ -225,8 +225,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { await this.currentPanel?.webview.postMessage({ config: integration.config, integrationId, - projectName: integration.projectName, - projectType: integration.projectType, + integrationName: integration.integrationName, + integrationType: integration.integrationType, type: 'showForm' }); } @@ -313,8 +313,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { // Build the integrations list from current integrations const projectIntegrations = Array.from(this.integrations.entries()) .map(([id, integration]) => { - // Get the integration type from config or project metadata - const type = integration.config?.type || integration.projectType; + // Get the integration type from config or integration metadata + const type = integration.config?.type || integration.integrationType; if (!type) { logger.warn(`IntegrationWebviewProvider: No type found for integration ${id}, skipping`); return null; @@ -329,7 +329,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { return { id, - name: integration.config?.name || integration.projectName || id, + name: integration.config?.name || integration.integrationName || id, type: deepnoteType }; }) diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index ee1a8f8f0d..91397ce01a 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -93,9 +93,9 @@ export interface IntegrationWithStatus { /** * Name from the project's integrations list (used for prefilling when config is null) */ - projectName?: string; + integrationName?: string; /** * Type from the project's integrations list (used for prefilling when config is null) */ - projectType?: IntegrationType; + integrationType?: IntegrationType; } diff --git a/src/webviews/webview-side/integrations/BigQueryForm.tsx b/src/webviews/webview-side/integrations/BigQueryForm.tsx index b80d1aea55..ee3b4513f5 100644 --- a/src/webviews/webview-side/integrations/BigQueryForm.tsx +++ b/src/webviews/webview-side/integrations/BigQueryForm.tsx @@ -5,7 +5,7 @@ import { BigQueryIntegrationConfig } from './types'; export interface IBigQueryFormProps { integrationId: string; existingConfig: BigQueryIntegrationConfig | null; - projectName?: string; + integrationName?: string; onSave: (config: BigQueryIntegrationConfig) => void; onCancel: () => void; } @@ -13,16 +13,16 @@ export interface IBigQueryFormProps { export const BigQueryForm: React.FC = ({ integrationId, existingConfig, - projectName, + integrationName, onSave, onCancel }) => { - const [name, setName] = React.useState(existingConfig?.name || projectName || ''); + const [name, setName] = React.useState(existingConfig?.name || integrationName || ''); const [projectId, setProjectId] = React.useState(existingConfig?.projectId || ''); const [credentials, setCredentials] = React.useState(existingConfig?.credentials || ''); const [credentialsError, setCredentialsError] = React.useState(null); - // Update form fields when existingConfig or projectName changes + // Update form fields when existingConfig or integrationName changes React.useEffect(() => { if (existingConfig) { setName(existingConfig.name || ''); @@ -30,12 +30,12 @@ export const BigQueryForm: React.FC = ({ setCredentials(existingConfig.credentials || ''); setCredentialsError(null); } else { - setName(projectName || ''); + setName(integrationName || ''); setProjectId(''); setCredentials(''); setCredentialsError(null); } - }, [existingConfig, projectName]); + }, [existingConfig, integrationName]); const validateCredentials = (value: string): boolean => { if (!value.trim()) { diff --git a/src/webviews/webview-side/integrations/ConfigurationForm.tsx b/src/webviews/webview-side/integrations/ConfigurationForm.tsx index 834dec1818..ca6ea2e36d 100644 --- a/src/webviews/webview-side/integrations/ConfigurationForm.tsx +++ b/src/webviews/webview-side/integrations/ConfigurationForm.tsx @@ -7,8 +7,8 @@ import { IntegrationConfig, IntegrationType } from './types'; export interface IConfigurationFormProps { integrationId: string; existingConfig: IntegrationConfig | null; - projectName?: string; - projectType?: IntegrationType; + integrationName?: string; + integrationType?: IntegrationType; onSave: (config: IntegrationConfig) => void; onCancel: () => void; } @@ -16,19 +16,19 @@ export interface IConfigurationFormProps { export const ConfigurationForm: React.FC = ({ integrationId, existingConfig, - projectName, - projectType, + integrationName, + integrationType, onSave, onCancel }) => { - // Determine integration type from existing config, project metadata, or ID + // Determine integration type from existing config, integration metadata from project, or ID const getIntegrationType = (): 'postgres' | 'bigquery' => { if (existingConfig) { return existingConfig.type; } - // Use project type if available - if (projectType) { - return projectType; + // Use integration type from project if available + if (integrationType) { + return integrationType; } // Infer from integration ID if (integrationId.includes('postgres')) { @@ -41,7 +41,7 @@ export const ConfigurationForm: React.FC = ({ return 'postgres'; }; - const integrationType = getIntegrationType(); + const selectedIntegrationType = getIntegrationType(); const title = getLocString('integrationsConfigureTitle', 'Configure Integration: {0}').replace( '{0}', @@ -59,11 +59,11 @@ export const ConfigurationForm: React.FC = ({
- {integrationType === 'postgres' ? ( + {selectedIntegrationType === 'postgres' ? ( @@ -71,7 +71,7 @@ export const ConfigurationForm: React.FC = ({ diff --git a/src/webviews/webview-side/integrations/IntegrationItem.tsx b/src/webviews/webview-side/integrations/IntegrationItem.tsx index 1b1c6ee0d7..f1472ceb38 100644 --- a/src/webviews/webview-side/integrations/IntegrationItem.tsx +++ b/src/webviews/webview-side/integrations/IntegrationItem.tsx @@ -29,11 +29,11 @@ export const IntegrationItem: React.FC = ({ integration, ? getLocString('integrationsReconfigure', 'Reconfigure') : getLocString('integrationsConfigure', 'Configure'); - // Get the name: prefer config name, then project name, then ID - const name = integration.config?.name || integration.projectName || integration.id; + // Get the name: prefer config name, then integration name from project, then ID + const name = integration.config?.name || integration.integrationName || integration.id; - // Get the type: prefer config type, then project type - const type = integration.config?.type || integration.projectType; + // Get the type: prefer config type, then integration type from project + const type = integration.config?.type || integration.integrationType; // Build display name with type const displayName = type ? `${name} (${getIntegrationTypeLabel(type)})` : name; diff --git a/src/webviews/webview-side/integrations/IntegrationPanel.tsx b/src/webviews/webview-side/integrations/IntegrationPanel.tsx index 6372fa9342..9d9cc88e46 100644 --- a/src/webviews/webview-side/integrations/IntegrationPanel.tsx +++ b/src/webviews/webview-side/integrations/IntegrationPanel.tsx @@ -14,8 +14,10 @@ export const IntegrationPanel: React.FC = ({ baseTheme, const [integrations, setIntegrations] = React.useState([]); const [selectedIntegrationId, setSelectedIntegrationId] = React.useState(null); const [selectedConfig, setSelectedConfig] = React.useState(null); - const [selectedProjectName, setSelectedProjectName] = React.useState(undefined); - const [selectedProjectType, setSelectedProjectType] = React.useState(undefined); + const [selectedIntegrationName, setSelectedIntegrationName] = React.useState(undefined); + const [selectedIntegrationType, setSelectedIntegrationType] = React.useState( + undefined + ); const [message, setMessage] = React.useState<{ type: 'success' | 'error'; text: string } | null>(null); const [confirmDelete, setConfirmDelete] = React.useState(null); @@ -53,8 +55,8 @@ export const IntegrationPanel: React.FC = ({ baseTheme, case 'showForm': setSelectedIntegrationId(msg.integrationId); setSelectedConfig(msg.config); - setSelectedProjectName(msg.projectName); - setSelectedProjectType(msg.projectType); + setSelectedIntegrationName(msg.integrationName); + setSelectedIntegrationType(msg.integrationType); break; case 'success': @@ -148,8 +150,8 @@ export const IntegrationPanel: React.FC = ({ baseTheme, diff --git a/src/webviews/webview-side/integrations/PostgresForm.tsx b/src/webviews/webview-side/integrations/PostgresForm.tsx index ea24536fec..d967baae7b 100644 --- a/src/webviews/webview-side/integrations/PostgresForm.tsx +++ b/src/webviews/webview-side/integrations/PostgresForm.tsx @@ -5,7 +5,7 @@ import { PostgresIntegrationConfig } from './types'; export interface IPostgresFormProps { integrationId: string; existingConfig: PostgresIntegrationConfig | null; - projectName?: string; + integrationName?: string; onSave: (config: PostgresIntegrationConfig) => void; onCancel: () => void; } @@ -13,11 +13,11 @@ export interface IPostgresFormProps { export const PostgresForm: React.FC = ({ integrationId, existingConfig, - projectName, + integrationName, onSave, onCancel }) => { - const [name, setName] = React.useState(existingConfig?.name || projectName || ''); + const [name, setName] = React.useState(existingConfig?.name || integrationName || ''); const [host, setHost] = React.useState(existingConfig?.host || ''); const [port, setPort] = React.useState(existingConfig?.port?.toString() || '5432'); const [database, setDatabase] = React.useState(existingConfig?.database || ''); @@ -25,7 +25,7 @@ export const PostgresForm: React.FC = ({ const [password, setPassword] = React.useState(existingConfig?.password || ''); const [ssl, setSsl] = React.useState(existingConfig?.ssl || false); - // Update form fields when existingConfig or projectName changes + // Update form fields when existingConfig or integrationName changes React.useEffect(() => { if (existingConfig) { setName(existingConfig.name || ''); @@ -36,7 +36,7 @@ export const PostgresForm: React.FC = ({ setPassword(existingConfig.password || ''); setSsl(existingConfig.ssl || false); } else { - setName(projectName || ''); + setName(integrationName || ''); setHost(''); setPort('5432'); setDatabase(''); @@ -44,7 +44,7 @@ export const PostgresForm: React.FC = ({ setPassword(''); setSsl(false); } - }, [existingConfig, projectName]); + }, [existingConfig, integrationName]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); diff --git a/src/webviews/webview-side/integrations/types.ts b/src/webviews/webview-side/integrations/types.ts index 7ceec3d2c7..580162ed16 100644 --- a/src/webviews/webview-side/integrations/types.ts +++ b/src/webviews/webview-side/integrations/types.ts @@ -30,8 +30,8 @@ export interface IntegrationWithStatus { id: string; config: IntegrationConfig | null; status: IntegrationStatus; - projectName?: string; - projectType?: IntegrationType; + integrationName?: string; + integrationType?: IntegrationType; } export interface IVsCodeMessage { @@ -49,8 +49,8 @@ export interface ShowFormMessage { type: 'showForm'; integrationId: string; config: IntegrationConfig | null; - projectName?: string; - projectType?: IntegrationType; + integrationName?: string; + integrationType?: IntegrationType; } export interface StatusMessage { From 9e86485a34e017bd683cf26582df7cddd9b5aa9f Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 12:56:04 +0200 Subject: [PATCH 04/15] test: cover new logic --- .../deepnoteNotebookManager.unit.test.ts | 77 +++++++++++++++++++ .../sqlCellStatusBarProvider.unit.test.ts | 66 ++++++++++++++++ .../notebooks/deepnote/integrationTypes.ts | 26 +++---- 3 files changed, 155 insertions(+), 14 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts index 302558c290..562c88955d 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts @@ -183,6 +183,83 @@ suite('DeepnoteNotebookManager', () => { }); }); + suite('updateProjectIntegrations', () => { + test('should update integrations list for existing project', () => { + manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); + + const integrations = [ + { id: 'int-1', name: 'PostgreSQL', type: 'pgsql' }, + { id: 'int-2', name: 'BigQuery', type: 'big-query' } + ]; + + manager.updateProjectIntegrations('project-123', integrations); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, integrations); + }); + + test('should replace existing integrations list', () => { + 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' } + ]; + + manager.updateProjectIntegrations('project-123', newIntegrations); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, newIntegrations); + }); + + test('should handle empty integrations array', () => { + const projectWithIntegrations: DeepnoteProject = { + ...mockProject, + project: { + ...mockProject.project, + integrations: [{ id: 'int-1', name: 'Integration 1', type: 'pgsql' }] + } + }; + + manager.storeOriginalProject('project-123', projectWithIntegrations, 'notebook-456'); + + manager.updateProjectIntegrations('project-123', []); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, []); + }); + + test('should do nothing for unknown project', () => { + // Should not throw an error + manager.updateProjectIntegrations('unknown-project', [{ id: 'int-1', name: 'Integration', type: 'pgsql' }]); + + const project = manager.getOriginalProject('unknown-project'); + assert.strictEqual(project, undefined); + }); + + test('should preserve other project properties', () => { + manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); + + const integrations = [{ id: 'int-1', name: 'PostgreSQL', type: 'pgsql' }]; + + manager.updateProjectIntegrations('project-123', integrations); + + 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'); diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 7ef62e8aa4..6f485a6aab 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -701,6 +701,72 @@ suite('SqlCellStatusBarProvider', () => { assert.isDefined(currentItem, 'Current integration should be in quick pick items'); assert.strictEqual(currentItem.detail, 'Currently selected'); }); + + test('shows error message when project ID is missing', async () => { + const cell = createMockCell('sql', {}, {}); // No notebook metadata + + await switchIntegrationHandler(cell); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).never(); + }); + + test('shows error message when project is not found', async () => { + const notebookMetadata = { deepnoteProjectId: 'missing-project' }; + const cell = createMockCell('sql', {}, notebookMetadata); + + when(commandNotebookManager.getOriginalProject('missing-project')).thenReturn(undefined); + + await switchIntegrationHandler(cell); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).never(); + }); + + test('skips DATAFRAME_SQL_INTEGRATION_ID from project integrations list', async () => { + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', {}, notebookMetadata); + let quickPickItems: any[] = []; + + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [ + { + id: DATAFRAME_SQL_INTEGRATION_ID, + name: 'Should be skipped', + type: 'duckdb' + }, + { + id: 'postgres-integration', + name: 'PostgreSQL', + type: 'pgsql' + } + ] + } + } as any); + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenCall((items) => { + quickPickItems = items; + return Promise.resolve(undefined); + }); + + await switchIntegrationHandler(cell); + + // Should have 2 items: postgres-integration and DuckDB (added separately) + const projectIntegrationItems = quickPickItems.filter( + (item) => item.id && item.id !== DATAFRAME_SQL_INTEGRATION_ID + ); + assert.strictEqual( + projectIntegrationItems.length, + 1, + 'Should have only 1 project integration (DATAFRAME_SQL_INTEGRATION_ID should be skipped)' + ); + assert.strictEqual(projectIntegrationItems[0].id, 'postgres-integration'); + + // DuckDB should still be in the list (added separately) + const duckDbItem = quickPickItems.find((item) => item.id === DATAFRAME_SQL_INTEGRATION_ID); + assert.isDefined(duckDbItem, 'DuckDB should still be in the list'); + }); }); function createMockCell( diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index 91397ce01a..196a6d1c99 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -15,27 +15,25 @@ export enum IntegrationType { /** * Map Deepnote integration type strings to our IntegrationType enum */ +const DEEPNOTE_TO_INTEGRATION_TYPE: Record = { + pgsql: IntegrationType.Postgres, + 'big-query': IntegrationType.BigQuery +}; + export function mapDeepnoteIntegrationType(deepnoteType: string): IntegrationType | undefined { - switch (deepnoteType) { - case 'pgsql': - return IntegrationType.Postgres; - case 'big-query': - return IntegrationType.BigQuery; - default: - return undefined; - } + return DEEPNOTE_TO_INTEGRATION_TYPE[deepnoteType]; } /** * Map our IntegrationType enum to Deepnote integration type strings */ +const INTEGRATION_TYPE_TO_DEEPNOTE: Record = { + [IntegrationType.Postgres]: 'pgsql', + [IntegrationType.BigQuery]: 'big-query' +}; + export function mapToDeepnoteIntegrationType(type: IntegrationType): string { - switch (type) { - case IntegrationType.Postgres: - return 'pgsql'; - case IntegrationType.BigQuery: - return 'big-query'; - } + return INTEGRATION_TYPE_TO_DEEPNOTE[type]; } /** From 944ecfbe59b84efe6f66c86469eb94e69b9bda98 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:06:30 +0200 Subject: [PATCH 05/15] add pgsql and duckdb to cspell dict --- cspell.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cspell.json b/cspell.json index f779a5caba..33845480f2 100644 --- a/cspell.json +++ b/cspell.json @@ -29,6 +29,7 @@ "dntk", "dont", "DONT", + "duckdb", "ename", "evalue", "findstr", @@ -42,6 +43,7 @@ "millis", "nbformat", "numpy", + "pgsql", "pids", "Pids", "PYTHONHOME", From 9892e5ff87426fa147e13f28282fc34fe1b0db56 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:08:30 +0200 Subject: [PATCH 06/15] refactor: drop utils and use records only --- .../deepnote/integrations/integrationDetector.ts | 6 +++--- .../deepnote/integrations/integrationWebview.ts | 6 +++--- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 6 +++--- src/platform/notebooks/deepnote/integrationTypes.ts | 12 ++---------- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index ecd7848c17..ee281f90ed 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -4,9 +4,9 @@ import { logger } from '../../../platform/logging'; import { IDeepnoteNotebookManager } from '../../types'; import { DATAFRAME_SQL_INTEGRATION_ID, + DEEPNOTE_TO_INTEGRATION_TYPE, IntegrationStatus, - IntegrationWithStatus, - mapDeepnoteIntegrationType + IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes'; import { IIntegrationDetector, IIntegrationStorage } from './types'; @@ -56,7 +56,7 @@ export class IntegrationDetector implements IIntegrationDetector { const config = await this.integrationStorage.getIntegrationConfig(integrationId); // Map the Deepnote integration type to our IntegrationType - const integrationType = mapDeepnoteIntegrationType(projectIntegration.type); + const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type]; const status: IntegrationWithStatus = { config: config || null, diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index 2b0add870c..96ededf7b9 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -8,10 +8,10 @@ import { LocalizedMessages, SharedMessages } from '../../../messageTypes'; import { IDeepnoteNotebookManager } from '../../types'; import { IIntegrationStorage, IIntegrationWebviewProvider } from './types'; import { + INTEGRATION_TYPE_TO_DEEPNOTE, IntegrationConfig, IntegrationStatus, - IntegrationWithStatus, - mapToDeepnoteIntegrationType + IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes'; /** @@ -321,7 +321,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { } // Map to Deepnote integration type - const deepnoteType = mapToDeepnoteIntegrationType(type); + const deepnoteType = INTEGRATION_TYPE_TO_DEEPNOTE[type]; if (!deepnoteType) { logger.warn(`IntegrationWebviewProvider: Cannot map type ${type} for integration ${id}, skipping`); return null; diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 91505c05a3..1311979456 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -24,8 +24,8 @@ import { IIntegrationStorage } from './integrations/types'; import { Commands } from '../../platform/common/constants'; import { DATAFRAME_SQL_INTEGRATION_ID, - IntegrationType, - mapDeepnoteIntegrationType + DEEPNOTE_TO_INTEGRATION_TYPE, + IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes'; import { IDeepnoteNotebookManager } from '../types'; @@ -334,7 +334,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid continue; } - const integrationType = mapDeepnoteIntegrationType(projectIntegration.type); + const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type]; const typeLabel = integrationType ? this.getIntegrationTypeLabel(integrationType) : projectIntegration.type; const item: LocalQuickPickItem = { diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index 196a6d1c99..87d5171368 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -15,27 +15,19 @@ export enum IntegrationType { /** * Map Deepnote integration type strings to our IntegrationType enum */ -const DEEPNOTE_TO_INTEGRATION_TYPE: Record = { +export const DEEPNOTE_TO_INTEGRATION_TYPE: Record = { pgsql: IntegrationType.Postgres, 'big-query': IntegrationType.BigQuery }; -export function mapDeepnoteIntegrationType(deepnoteType: string): IntegrationType | undefined { - return DEEPNOTE_TO_INTEGRATION_TYPE[deepnoteType]; -} - /** * Map our IntegrationType enum to Deepnote integration type strings */ -const INTEGRATION_TYPE_TO_DEEPNOTE: Record = { +export const INTEGRATION_TYPE_TO_DEEPNOTE: Record = { [IntegrationType.Postgres]: 'pgsql', [IntegrationType.BigQuery]: 'big-query' }; -export function mapToDeepnoteIntegrationType(type: IntegrationType): string { - return INTEGRATION_TYPE_TO_DEEPNOTE[type]; -} - /** * Base interface for all integration configurations */ From a1df9f1ec48c996cb20b837b48c4132140bbf3da Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:09:25 +0200 Subject: [PATCH 07/15] refactor: add exhausiveness check --- src/platform/notebooks/deepnote/integrationTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index 87d5171368..b82a15a0f0 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -15,7 +15,7 @@ export enum IntegrationType { /** * Map Deepnote integration type strings to our IntegrationType enum */ -export const DEEPNOTE_TO_INTEGRATION_TYPE: Record = { +export const DEEPNOTE_TO_INTEGRATION_TYPE: { [type: string]: IntegrationType } = { pgsql: IntegrationType.Postgres, 'big-query': IntegrationType.BigQuery }; @@ -23,7 +23,7 @@ export const DEEPNOTE_TO_INTEGRATION_TYPE: Record = { /** * Map our IntegrationType enum to Deepnote integration type strings */ -export const INTEGRATION_TYPE_TO_DEEPNOTE: Record = { +export const INTEGRATION_TYPE_TO_DEEPNOTE: { [type in IntegrationType]: string } = { [IntegrationType.Postgres]: 'pgsql', [IntegrationType.BigQuery]: 'big-query' }; From addaa7d17cb6adc03f1a9973c372c66a9a8d840b Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:14:04 +0200 Subject: [PATCH 08/15] feat: expose error message from failed project saving --- .../deepnote/deepnoteNotebookManager.ts | 17 +++++----- .../deepnoteNotebookManager.unit.test.ts | 33 ++++++++++++------- .../integrations/integrationWebview.ts | 17 +++++++--- src/notebooks/types.ts | 21 +++++++++++- 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.ts b/src/notebooks/deepnote/deepnoteNotebookManager.ts index 797cc83b4e..511795e0cf 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.ts @@ -1,6 +1,6 @@ import { injectable } from 'inversify'; -import { IDeepnoteNotebookManager } from '../types'; +import { IDeepnoteNotebookManager, ProjectIntegration } from '../types'; import type { DeepnoteProject } from './deepnoteTypes'; /** @@ -78,17 +78,16 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager { /** * 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 + * + * @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: Array<{ id: string; name: string; type: string }> - ): void { + updateProjectIntegrations(projectId: string, integrations: ProjectIntegration[]): boolean { const project = this.originalProjects.get(projectId); if (!project) { - return; + return false; } const updatedProject = JSON.parse(JSON.stringify(project)) as DeepnoteProject; @@ -99,6 +98,8 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager { if (currentNotebookId) { this.storeOriginalProject(projectId, updatedProject, currentNotebookId); } + + return true; } /** diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts index 562c88955d..6022cb0fd1 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts @@ -184,7 +184,7 @@ suite('DeepnoteNotebookManager', () => { }); suite('updateProjectIntegrations', () => { - test('should update integrations list for existing project', () => { + test('should update integrations list for existing project and return true', () => { manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); const integrations = [ @@ -192,13 +192,15 @@ suite('DeepnoteNotebookManager', () => { { id: 'int-2', name: 'BigQuery', type: 'big-query' } ]; - manager.updateProjectIntegrations('project-123', integrations); + 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', () => { + test('should replace existing integrations list and return true', () => { const projectWithIntegrations: DeepnoteProject = { ...mockProject, project: { @@ -214,13 +216,15 @@ suite('DeepnoteNotebookManager', () => { { id: 'new-int-2', name: 'New Integration 2', type: 'big-query' } ]; - manager.updateProjectIntegrations('project-123', newIntegrations); + 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', () => { + test('should handle empty integrations array and return true', () => { const projectWithIntegrations: DeepnoteProject = { ...mockProject, project: { @@ -231,26 +235,33 @@ suite('DeepnoteNotebookManager', () => { manager.storeOriginalProject('project-123', projectWithIntegrations, 'notebook-456'); - manager.updateProjectIntegrations('project-123', []); + const result = manager.updateProjectIntegrations('project-123', []); + + assert.strictEqual(result, true); const updatedProject = manager.getOriginalProject('project-123'); assert.deepStrictEqual(updatedProject?.project.integrations, []); }); - test('should do nothing for unknown project', () => { - // Should not throw an error - manager.updateProjectIntegrations('unknown-project', [{ id: 'int-1', name: 'Integration', type: 'pgsql' }]); + 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', () => { + 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' }]; - manager.updateProjectIntegrations('project-123', integrations); + 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); diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index 96ededf7b9..e5969fc988 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -5,7 +5,7 @@ import { IExtensionContext } from '../../../platform/common/types'; import * as localize from '../../../platform/common/utils/localize'; import { logger } from '../../../platform/logging'; import { LocalizedMessages, SharedMessages } from '../../../messageTypes'; -import { IDeepnoteNotebookManager } from '../../types'; +import { IDeepnoteNotebookManager, ProjectIntegration } from '../../types'; import { IIntegrationStorage, IIntegrationWebviewProvider } from './types'; import { INTEGRATION_TYPE_TO_DEEPNOTE, @@ -311,7 +311,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { } // Build the integrations list from current integrations - const projectIntegrations = Array.from(this.integrations.entries()) + const projectIntegrations: ProjectIntegration[] = Array.from(this.integrations.entries()) .map(([id, integration]) => { // Get the integration type from config or integration metadata const type = integration.config?.type || integration.integrationType; @@ -333,14 +333,23 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { type: deepnoteType }; }) - .filter((integration): integration is { id: string; name: string; type: string } => integration !== null); + .filter((integration): integration is ProjectIntegration => integration !== null); logger.debug( `IntegrationWebviewProvider: Updating project ${this.projectId} with ${projectIntegrations.length} integrations` ); // Update the project in the notebook manager - this.notebookManager.updateProjectIntegrations(this.projectId, projectIntegrations); + const success = this.notebookManager.updateProjectIntegrations(this.projectId, projectIntegrations); + + if (!success) { + logger.error( + `IntegrationWebviewProvider: Failed to update integrations for project ${this.projectId} - project not found` + ); + void window.showErrorMessage( + l10n.t('Failed to update integrations: project not found. Please reopen the notebook and try again.') + ); + } } /** diff --git a/src/notebooks/types.ts b/src/notebooks/types.ts index 040e422170..42ed8fc6dc 100644 --- a/src/notebooks/types.ts +++ b/src/notebooks/types.ts @@ -25,6 +25,15 @@ export interface INotebookPythonEnvironmentService { getPythonEnvironment(uri: Uri): EnvironmentPath | undefined; } +/** + * Represents a Deepnote project integration with basic metadata. + */ +export interface ProjectIntegration { + id: string; + name: string; + type: string; +} + export const IDeepnoteNotebookManager = Symbol('IDeepnoteNotebookManager'); export interface IDeepnoteNotebookManager { getCurrentNotebookId(projectId: string): string | undefined; @@ -33,7 +42,17 @@ export interface IDeepnoteNotebookManager { selectNotebookForProject(projectId: string, notebookId: string): void; storeOriginalProject(projectId: string, project: DeepnoteProject, notebookId: string): void; updateCurrentNotebookId(projectId: string, notebookId: string): void; - updateProjectIntegrations(projectId: string, integrations: Array<{ id: string; name: string; type: string }>): void; + + /** + * 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; + hasInitNotebookBeenRun(projectId: string): boolean; markInitNotebookAsRun(projectId: string): void; } From 3533e262fbaa0081c2bb599027cbaebe092d71a0 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:25:37 +0200 Subject: [PATCH 09/15] feat: add warnings about unknown integrations --- .../deepnote/integrations/integrationDetector.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index ee281f90ed..31108d0746 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -52,12 +52,20 @@ export class IntegrationDetector implements IIntegrationDetector { 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]; + // 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, From ccb2196896e4d16f6036f5fe2e638c6b3f79676d Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:28:27 +0200 Subject: [PATCH 10/15] refactor: righten integration type mappings --- .../integrations/integrationDetector.ts | 3 ++- .../deepnote/sqlCellStatusBarProvider.ts | 3 ++- .../notebooks/deepnote/integrationTypes.ts | 20 ++++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index 31108d0746..bcc3e7fabe 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -53,7 +53,8 @@ export class IntegrationDetector implements IIntegrationDetector { logger.debug(`IntegrationDetector: Found integration: ${integrationId} (${projectIntegration.type})`); // Map the Deepnote integration type to our IntegrationType - const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type]; + const integrationType = + DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as keyof typeof DEEPNOTE_TO_INTEGRATION_TYPE]; // Skip unknown integration types if (!integrationType) { diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 1311979456..47091ccbf3 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -334,7 +334,8 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid continue; } - const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type]; + const integrationType = + DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as keyof typeof DEEPNOTE_TO_INTEGRATION_TYPE]; const typeLabel = integrationType ? this.getIntegrationTypeLabel(integrationType) : projectIntegration.type; const item: LocalQuickPickItem = { diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index b82a15a0f0..885b9b9b61 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -12,20 +12,22 @@ export enum IntegrationType { BigQuery = 'bigquery' } -/** - * Map Deepnote integration type strings to our IntegrationType enum - */ -export const DEEPNOTE_TO_INTEGRATION_TYPE: { [type: string]: IntegrationType } = { - pgsql: IntegrationType.Postgres, - 'big-query': IntegrationType.BigQuery -}; - /** * Map our IntegrationType enum to Deepnote integration type strings */ -export const INTEGRATION_TYPE_TO_DEEPNOTE: { [type in IntegrationType]: string } = { +export const INTEGRATION_TYPE_TO_DEEPNOTE = { [IntegrationType.Postgres]: 'pgsql', [IntegrationType.BigQuery]: 'big-query' +} as const satisfies { [type in IntegrationType]: string }; + +type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typeof INTEGRATION_TYPE_TO_DEEPNOTE]; + +/** + * Map Deepnote integration type strings to our IntegrationType enum + */ +export const DEEPNOTE_TO_INTEGRATION_TYPE: Record = { + pgsql: IntegrationType.Postgres, + 'big-query': IntegrationType.BigQuery }; /** From e08437347a2790d334bbf5ceae294d295d2a488e Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:35:33 +0200 Subject: [PATCH 11/15] refactor: simplify int type typing --- src/notebooks/deepnote/integrations/integrationDetector.ts | 6 +++--- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 6 +++--- src/platform/notebooks/deepnote/integrationTypes.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index bcc3e7fabe..55e627b734 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -6,7 +6,8 @@ import { DATAFRAME_SQL_INTEGRATION_ID, DEEPNOTE_TO_INTEGRATION_TYPE, IntegrationStatus, - IntegrationWithStatus + IntegrationWithStatus, + RawIntegrationType } from '../../../platform/notebooks/deepnote/integrationTypes'; import { IIntegrationDetector, IIntegrationStorage } from './types'; @@ -53,8 +54,7 @@ export class IntegrationDetector implements IIntegrationDetector { 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 keyof typeof DEEPNOTE_TO_INTEGRATION_TYPE]; + const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType]; // Skip unknown integration types if (!integrationType) { diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 47091ccbf3..6802db90f5 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -25,7 +25,8 @@ import { Commands } from '../../platform/common/constants'; import { DATAFRAME_SQL_INTEGRATION_ID, DEEPNOTE_TO_INTEGRATION_TYPE, - IntegrationType + IntegrationType, + RawIntegrationType } from '../../platform/notebooks/deepnote/integrationTypes'; import { IDeepnoteNotebookManager } from '../types'; @@ -334,8 +335,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid continue; } - const integrationType = - DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as keyof typeof DEEPNOTE_TO_INTEGRATION_TYPE]; + const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType]; const typeLabel = integrationType ? this.getIntegrationTypeLabel(integrationType) : projectIntegration.type; const item: LocalQuickPickItem = { diff --git a/src/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index 885b9b9b61..ac1d782154 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -20,7 +20,7 @@ export const INTEGRATION_TYPE_TO_DEEPNOTE = { [IntegrationType.BigQuery]: 'big-query' } as const satisfies { [type in IntegrationType]: string }; -type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typeof INTEGRATION_TYPE_TO_DEEPNOTE]; +export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typeof INTEGRATION_TYPE_TO_DEEPNOTE]; /** * Map Deepnote integration type strings to our IntegrationType enum From 61bae483c1c22e8aee126e65ea89b9a4c08a53d6 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:37:37 +0200 Subject: [PATCH 12/15] fix typecheck --- src/notebooks/deepnote/integrations/integrationWebview.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index e5969fc988..2f5059d787 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -11,7 +11,8 @@ import { INTEGRATION_TYPE_TO_DEEPNOTE, IntegrationConfig, IntegrationStatus, - IntegrationWithStatus + IntegrationWithStatus, + RawIntegrationType } from '../../../platform/notebooks/deepnote/integrationTypes'; /** @@ -312,7 +313,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { // Build the integrations list from current integrations const projectIntegrations: ProjectIntegration[] = Array.from(this.integrations.entries()) - .map(([id, integration]) => { + .map(([id, integration]): ProjectIntegration | null => { // Get the integration type from config or integration metadata const type = integration.config?.type || integration.integrationType; if (!type) { @@ -321,7 +322,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { } // Map to Deepnote integration type - const deepnoteType = INTEGRATION_TYPE_TO_DEEPNOTE[type]; + const deepnoteType: RawIntegrationType | undefined = INTEGRATION_TYPE_TO_DEEPNOTE[type]; if (!deepnoteType) { logger.warn(`IntegrationWebviewProvider: Cannot map type ${type} for integration ${id}, skipping`); return null; From 545b531e1ab8e904cbdeda8c37909eaf40b8b1fb Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:41:47 +0200 Subject: [PATCH 13/15] fix: avoid dropping project update --- src/notebooks/deepnote/deepnoteNotebookManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.ts b/src/notebooks/deepnote/deepnoteNotebookManager.ts index 27022bc9fa..b6875d5048 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.ts @@ -97,6 +97,8 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager { if (currentNotebookId) { this.storeOriginalProject(projectId, updatedProject, currentNotebookId); + } else { + this.originalProjects.set(projectId, updatedProject); } return true; From c3eb62abeca8aaff566ca83f0a5852b674ab025a Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 13:42:21 +0200 Subject: [PATCH 14/15] test: add test for storing project --- .../deepnoteNotebookManager.unit.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts index 645fa23cb6..f81edcaa07 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts @@ -269,6 +269,29 @@ suite('DeepnoteNotebookManager', () => { 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', () => { From 4126aaf19e926940263895c9d64a8186396411cb Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 14:00:06 +0200 Subject: [PATCH 15/15] feat: show integration name when not configured on block --- .../deepnote/sqlCellStatusBarProvider.ts | 14 +++++- .../sqlCellStatusBarProvider.unit.test.ts | 48 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 6802db90f5..5ac805d243 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -202,7 +202,19 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Get integration configuration to display the name const config = await this.integrationStorage.getProjectIntegrationConfig(projectId, integrationId); - const displayName = config?.name || l10n.t('Unknown integration (configure)'); + + // Determine the display name + let displayName: string; + if (config?.name) { + // Integration is configured, use the config name + displayName = config.name; + } else { + // Integration is not configured, try to get the name from the project's integration list + const project = this.notebookManager.getOriginalProject(projectId); + const projectIntegration = project?.project.integrations?.find((i) => i.id === integrationId); + const baseName = projectIntegration?.name || l10n.t('Unknown integration'); + displayName = l10n.t('{0} (configure)', baseName); + } // Create a status bar item that opens the integration picker return { diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 6f485a6aab..906ff5d26d 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -158,7 +158,7 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(variableItem.priority, 90); }); - test('shows "Unknown integration (configure)" when config not found', async () => { + test('shows "Unknown integration (configure)" when config not found and not in project list', async () => { const integrationId = 'postgres-123'; const cell = createMockCell( 'sql', @@ -171,6 +171,11 @@ suite('SqlCellStatusBarProvider', () => { ); when(integrationStorage.getProjectIntegrationConfig(anything(), anything())).thenResolve(undefined); + when(notebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [] + } + } as any); const result = await provider.provideCellStatusBarItems(cell, cancellationToken); @@ -188,6 +193,47 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(items[1].priority, 90); }); + test('shows integration name from project list with (configure) suffix when config not found but integration is in project', async () => { + const integrationId = 'postgres-123'; + const cell = createMockCell( + 'sql', + { + sql_integration_id: integrationId + }, + { + deepnoteProjectId: 'project-1' + } + ); + + when(integrationStorage.getProjectIntegrationConfig(anything(), anything())).thenResolve(undefined); + when(notebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [ + { + id: integrationId, + name: 'Production Database', + type: 'pgsql' + } + ] + } + } as any); + + const result = await provider.provideCellStatusBarItems(cell, cancellationToken); + + assert.isDefined(result); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 2); + assert.strictEqual(items[0].text, '$(database) Production Database (configure)'); + assert.strictEqual(items[0].alignment, 1); + assert.strictEqual(items[0].command.command, 'deepnote.switchSqlIntegration'); + assert.deepStrictEqual(items[0].command.arguments, [cell]); + assert.strictEqual(items[1].text, 'Variable: df'); + assert.strictEqual(items[1].alignment, 1); + assert.strictEqual(items[1].command.command, 'deepnote.updateSqlVariableName'); + assert.strictEqual(items[1].priority, 90); + }); + test('returns only variable item when notebook has no project ID', async () => { const integrationId = 'postgres-123'; const cell = createMockCell('sql', {