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", 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/deepnoteNotebookManager.ts b/src/notebooks/deepnote/deepnoteNotebookManager.ts index ef68f19abf..b6875d5048 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 '../../platform/deepnote/deepnoteTypes'; /** @@ -75,6 +75,35 @@ export class DeepnoteNotebookManager implements IDeepnoteNotebookManager { this.currentNotebookId.set(projectId, notebookId); } + /** + * Updates the integrations list in the project data. + * This modifies the stored project to reflect changes in configured integrations. + * + * @param projectId - Project identifier + * @param integrations - Array of integration metadata to store in the project + * @returns `true` if the project was found and updated successfully, `false` if the project does not exist + */ + updateProjectIntegrations(projectId: string, integrations: ProjectIntegration[]): boolean { + const project = this.originalProjects.get(projectId); + + if (!project) { + return false; + } + + const updatedProject = JSON.parse(JSON.stringify(project)) as DeepnoteProject; + updatedProject.project.integrations = integrations; + + const currentNotebookId = this.currentNotebookId.get(projectId); + + if (currentNotebookId) { + this.storeOriginalProject(projectId, updatedProject, currentNotebookId); + } else { + this.originalProjects.set(projectId, updatedProject); + } + + return true; + } + /** * Checks if the init notebook has already been run for a project. * @param projectId Project identifier diff --git a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts index c300ae0157..f81edcaa07 100644 --- a/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts @@ -183,6 +183,117 @@ suite('DeepnoteNotebookManager', () => { }); }); + suite('updateProjectIntegrations', () => { + test('should update integrations list for existing project and return true', () => { + manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); + + const integrations = [ + { id: 'int-1', name: 'PostgreSQL', type: 'pgsql' }, + { id: 'int-2', name: 'BigQuery', type: 'big-query' } + ]; + + const result = manager.updateProjectIntegrations('project-123', integrations); + + assert.strictEqual(result, true); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, integrations); + }); + + test('should replace existing integrations list and return true', () => { + const projectWithIntegrations: DeepnoteProject = { + ...mockProject, + project: { + ...mockProject.project, + integrations: [{ id: 'old-int', name: 'Old Integration', type: 'pgsql' }] + } + }; + + manager.storeOriginalProject('project-123', projectWithIntegrations, 'notebook-456'); + + const newIntegrations = [ + { id: 'new-int-1', name: 'New Integration 1', type: 'pgsql' }, + { id: 'new-int-2', name: 'New Integration 2', type: 'big-query' } + ]; + + const result = manager.updateProjectIntegrations('project-123', newIntegrations); + + assert.strictEqual(result, true); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, newIntegrations); + }); + + test('should handle empty integrations array and return true', () => { + const projectWithIntegrations: DeepnoteProject = { + ...mockProject, + project: { + ...mockProject.project, + integrations: [{ id: 'int-1', name: 'Integration 1', type: 'pgsql' }] + } + }; + + manager.storeOriginalProject('project-123', projectWithIntegrations, 'notebook-456'); + + const result = manager.updateProjectIntegrations('project-123', []); + + assert.strictEqual(result, true); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, []); + }); + + test('should return false for unknown project', () => { + const result = manager.updateProjectIntegrations('unknown-project', [ + { id: 'int-1', name: 'Integration', type: 'pgsql' } + ]); + + assert.strictEqual(result, false); + + const project = manager.getOriginalProject('unknown-project'); + assert.strictEqual(project, undefined); + }); + + test('should preserve other project properties and return true', () => { + manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); + + const integrations = [{ id: 'int-1', name: 'PostgreSQL', type: 'pgsql' }]; + + const result = manager.updateProjectIntegrations('project-123', integrations); + + assert.strictEqual(result, true); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.strictEqual(updatedProject?.project.id, mockProject.project.id); + assert.strictEqual(updatedProject?.project.name, mockProject.project.name); + assert.strictEqual(updatedProject?.version, mockProject.version); + assert.deepStrictEqual(updatedProject?.metadata, mockProject.metadata); + }); + + test('should update integrations when currentNotebookId is undefined and return true', () => { + // Store project with a notebook ID, then clear it to simulate the edge case + manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); + manager.updateCurrentNotebookId('project-123', undefined as any); + + const integrations = [ + { id: 'int-1', name: 'PostgreSQL', type: 'pgsql' }, + { id: 'int-2', name: 'BigQuery', type: 'big-query' } + ]; + + const result = manager.updateProjectIntegrations('project-123', integrations); + + assert.strictEqual(result, true); + + const updatedProject = manager.getOriginalProject('project-123'); + assert.deepStrictEqual(updatedProject?.project.integrations, integrations); + // Verify other properties remain unchanged + assert.strictEqual(updatedProject?.project.id, mockProject.project.id); + assert.strictEqual(updatedProject?.project.name, mockProject.project.name); + assert.strictEqual(updatedProject?.version, mockProject.version); + assert.deepStrictEqual(updatedProject?.metadata, mockProject.metadata); + }); + }); + suite('integration scenarios', () => { test('should handle complete workflow for multiple projects', () => { manager.storeOriginalProject('project-1', mockProject, 'notebook-1'); diff --git a/src/notebooks/deepnote/integrations/integrationDetector.ts b/src/notebooks/deepnote/integrations/integrationDetector.ts index dac3d044d4..55e627b734 100644 --- a/src/notebooks/deepnote/integrations/integrationDetector.ts +++ b/src/notebooks/deepnote/integrations/integrationDetector.ts @@ -2,9 +2,14 @@ import { inject, injectable } from 'inversify'; import { logger } from '../../../platform/logging'; import { IDeepnoteNotebookManager } from '../../types'; -import { IntegrationStatus, IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes'; +import { + DATAFRAME_SQL_INTEGRATION_ID, + DEEPNOTE_TO_INTEGRATION_TYPE, + IntegrationStatus, + IntegrationWithStatus, + RawIntegrationType +} from '../../../platform/notebooks/deepnote/integrationTypes'; import { IIntegrationDetector, IIntegrationStorage } from './types'; -import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils'; /** * Service for detecting integrations used in Deepnote notebooks @@ -17,7 +22,8 @@ export class IntegrationDetector implements IIntegrationDetector { ) {} /** - * Detect all integrations used in the given project + * Detect all integrations used in the given project. + * Uses the project's integrations field as the source of truth. */ async detectIntegrations(projectId: string): Promise> { // Get the project @@ -29,33 +35,52 @@ export class IntegrationDetector implements IIntegrationDetector { return new Map(); } - logger.debug( - `IntegrationDetector: Scanning project ${projectId} with ${project.project.notebooks.length} notebooks` - ); - - // Collect all blocks with SQL integration metadata from all notebooks - const blocksWithIntegrations: BlockWithIntegration[] = []; - for (const notebook of project.project.notebooks) { - logger.trace(`IntegrationDetector: Scanning notebook ${notebook.id} with ${notebook.blocks.length} blocks`); - - for (const block of notebook.blocks) { - // Check if this is a code block with SQL integration metadata - if (block.type === 'code' && block.metadata?.sql_integration_id) { - blocksWithIntegrations.push({ - id: block.id, - sql_integration_id: block.metadata.sql_integration_id - }); - } else if (block.type === 'code') { - logger.trace( - `IntegrationDetector: Block ${block.id} has no sql_integration_id. Metadata:`, - block.metadata - ); - } + logger.debug(`IntegrationDetector: Scanning project ${projectId} for integrations`); + + const integrations = new Map(); + + // Use the project's integrations field as the source of truth + const projectIntegrations = project.project.integrations || []; + logger.debug(`IntegrationDetector: Found ${projectIntegrations.length} integrations in project.integrations`); + + for (const projectIntegration of projectIntegrations) { + const integrationId = projectIntegration.id; + + // Skip the internal DuckDB integration + if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) { + continue; + } + + logger.debug(`IntegrationDetector: Found integration: ${integrationId} (${projectIntegration.type})`); + + // Map the Deepnote integration type to our IntegrationType + const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType]; + + // Skip unknown integration types + if (!integrationType) { + logger.warn( + `IntegrationDetector: Unknown integration type '${projectIntegration.type}' for integration ID '${integrationId}'. Skipping.` + ); + continue; } + + // Check if the integration is configured + const config = await this.integrationStorage.getIntegrationConfig(integrationId); + + const status: IntegrationWithStatus = { + config: config || null, + status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected, + // Include integration metadata from project for prefilling when config is null + integrationName: projectIntegration.name, + integrationType: integrationType + }; + + integrations.set(integrationId, status); } - // Use the shared utility to scan blocks and build the status map - return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationDetector'); + logger.debug(`IntegrationDetector: Found ${integrations.size} integrations`); + + return integrations; } /** 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..2f5059d787 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -5,11 +5,14 @@ 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, ProjectIntegration } from '../../types'; import { IIntegrationStorage, IIntegrationWebviewProvider } from './types'; import { + INTEGRATION_TYPE_TO_DEEPNOTE, IntegrationConfig, IntegrationStatus, - IntegrationWithStatus + IntegrationWithStatus, + RawIntegrationType } from '../../../platform/notebooks/deepnote/integrationTypes'; /** @@ -23,18 +26,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; @@ -117,6 +129,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, @@ -161,6 +175,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { const integrationsData = Array.from(this.integrations.entries()).map(([id, integration]) => ({ config: integration.config, id, + integrationName: integration.integrationName, + integrationType: integration.integrationType, status: integration.status })); logger.debug(`IntegrationWebviewProvider: Sending ${integrationsData.length} integrations to webview`); @@ -210,6 +226,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { await this.currentPanel?.webview.postMessage({ config: integration.config, integrationId, + integrationName: integration.integrationName, + integrationType: integration.integrationType, type: 'showForm' }); } @@ -229,6 +247,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 +282,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 +302,57 @@ 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: ProjectIntegration[] = Array.from(this.integrations.entries()) + .map(([id, integration]): ProjectIntegration | null => { + // 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; + } + + // Map to Deepnote integration type + const deepnoteType: RawIntegrationType | undefined = INTEGRATION_TYPE_TO_DEEPNOTE[type]; + if (!deepnoteType) { + logger.warn(`IntegrationWebviewProvider: Cannot map type ${type} for integration ${id}, skipping`); + return null; + } + + return { + id, + name: integration.config?.name || integration.integrationName || id, + type: deepnoteType + }; + }) + .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 + 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.') + ); + } + } + /** * 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..5ac805d243 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -22,7 +22,13 @@ 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, + DEEPNOTE_TO_INTEGRATION_TYPE, + IntegrationType, + RawIntegrationType +} from '../../platform/notebooks/deepnote/integrationTypes'; +import { IDeepnoteNotebookManager } from '../types'; /** * QuickPick item with an integration ID @@ -42,7 +48,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 { @@ -195,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 { @@ -285,17 +304,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; + } - // Build quick pick items + // 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 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 +340,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 = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType]; + 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..906ff5d26d 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; @@ -155,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', @@ -168,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); @@ -185,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', { @@ -249,12 +298,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 +353,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 +413,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 +540,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 +574,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 +603,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 +623,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 +644,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 +671,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 +696,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 +720,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); @@ -646,6 +747,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/notebooks/types.ts b/src/notebooks/types.ts index 4392ed7445..a83797fc8a 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,6 +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; + + /** + * 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; } 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/platform/notebooks/deepnote/integrationTypes.ts b/src/platform/notebooks/deepnote/integrationTypes.ts index 048c0ff13a..ac1d782154 100644 --- a/src/platform/notebooks/deepnote/integrationTypes.ts +++ b/src/platform/notebooks/deepnote/integrationTypes.ts @@ -12,6 +12,24 @@ export enum IntegrationType { BigQuery = 'bigquery' } +/** + * Map our IntegrationType enum to Deepnote integration type strings + */ +export const INTEGRATION_TYPE_TO_DEEPNOTE = { + [IntegrationType.Postgres]: 'pgsql', + [IntegrationType.BigQuery]: 'big-query' +} as const satisfies { [type in IntegrationType]: string }; + +export 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 +}; + /** * Base interface for all integration configurations */ @@ -64,4 +82,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) + */ + integrationName?: string; + /** + * Type from the project's integrations list (used for prefilling when config is null) + */ + integrationType?: IntegrationType; } diff --git a/src/webviews/webview-side/integrations/BigQueryForm.tsx b/src/webviews/webview-side/integrations/BigQueryForm.tsx index cad543fce3..ee3b4513f5 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; + integrationName?: 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, + integrationName, + onSave, + onCancel +}) => { + 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 changes + // Update form fields when existingConfig or integrationName 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(integrationName || ''); setProjectId(''); setCredentials(''); setCredentialsError(null); } - }, [existingConfig]); + }, [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 1d7c7c583c..ca6ea2e36d 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; + integrationName?: string; + integrationType?: IntegrationType; onSave: (config: IntegrationConfig) => void; onCancel: () => void; } @@ -14,14 +16,20 @@ export interface IConfigurationFormProps { export const ConfigurationForm: React.FC = ({ integrationId, existingConfig, + integrationName, + integrationType, onSave, onCancel }) => { - // Determine integration type from ID or existing config + // Determine integration type from existing config, integration metadata from project, or ID const getIntegrationType = (): 'postgres' | 'bigquery' => { if (existingConfig) { return existingConfig.type; } + // Use integration type from project if available + if (integrationType) { + return integrationType; + } // Infer from integration ID if (integrationId.includes('postgres')) { return 'postgres'; @@ -33,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}', @@ -51,10 +59,11 @@ export const ConfigurationForm: React.FC = ({
- {integrationType === 'postgres' ? ( + {selectedIntegrationType === 'postgres' ? ( @@ -62,6 +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 6e756c2159..f1472ceb38 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 integration name from project, then ID + const name = integration.config?.name || integration.integrationName || integration.id; + + // 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; return (
diff --git a/src/webviews/webview-side/integrations/IntegrationPanel.tsx b/src/webviews/webview-side/integrations/IntegrationPanel.tsx index d8d396f5bb..9d9cc88e46 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,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 [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); @@ -51,6 +55,8 @@ export const IntegrationPanel: React.FC = ({ baseTheme, case 'showForm': setSelectedIntegrationId(msg.integrationId); setSelectedConfig(msg.config); + setSelectedIntegrationName(msg.integrationName); + setSelectedIntegrationType(msg.integrationType); break; case 'success': @@ -144,6 +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 0a037da8fa..d967baae7b 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; + integrationName?: 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, + integrationName, + onSave, + onCancel +}) => { + 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 || ''); @@ -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 integrationName 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(integrationName || ''); setHost(''); setPort('5432'); setDatabase(''); @@ -37,7 +44,7 @@ export const PostgresForm: React.FC = ({ integrationId, exis setPassword(''); setSsl(false); } - }, [existingConfig]); + }, [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 f1d877f00e..580162ed16 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; + integrationName?: string; + integrationType?: IntegrationType; } export interface IVsCodeMessage { @@ -47,6 +49,8 @@ export interface ShowFormMessage { type: 'showForm'; integrationId: string; config: IntegrationConfig | null; + integrationName?: string; + integrationType?: IntegrationType; } export interface StatusMessage {