Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
65c1f1e
remove localization from integration webview as it is not available
jankuca Oct 16, 2025
88bcd66
feat: add integration link to each SQL block
jankuca Oct 16, 2025
8be88f3
working button on sql block
jankuca Oct 16, 2025
e6a9dff
fix loading of integration list
jankuca Oct 16, 2025
360d58e
localize integrationManager
jankuca Oct 17, 2025
1cceb9c
use cancel token when creating sql cell status bar button
jankuca Oct 17, 2025
3c79842
use imported constant in sql status bar test
jankuca Oct 17, 2025
031cd15
remove logs from integration panel
jankuca Oct 17, 2025
62b5206
use absolute value literal due to unavailable import (?)
jankuca Oct 17, 2025
3f6cd2e
remove useless template literal
jankuca Oct 17, 2025
bc036b8
remove logging private data
jankuca Oct 17, 2025
4b7dc3c
add dispose logic for integration storage
jankuca Oct 17, 2025
082797c
add localization for integration management views
jankuca Oct 17, 2025
ec55052
remove empty localization strings
jankuca Oct 17, 2025
b703449
fix sql status bar emitter disposing
jankuca Oct 17, 2025
23b7186
add interface use
jankuca Oct 17, 2025
65ed522
fix integration emission
jankuca Oct 17, 2025
44afb9c
add form value resetting
jankuca Oct 17, 2025
768aa72
add trimming of config
jankuca Oct 17, 2025
a6dd774
drop extra localization parsing roundtrip
jankuca Oct 17, 2025
1faed39
remove extra if
jankuca Oct 17, 2025
94843c9
show "unknown integration" on block when not connected
jankuca Oct 17, 2025
ed65018
fix scope leak
jankuca Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,45 @@ export type LocalizedMessages = {
dataframePageOf: string;
dataframeCopyTable: string;
dataframeExportTable: string;
// Integration panel strings
integrationsTitle: string;
integrationsNoIntegrationsFound: string;
integrationsConnected: string;
integrationsNotConfigured: string;
integrationsConfigure: string;
integrationsReconfigure: string;
integrationsReset: string;
integrationsConfirmResetTitle: string;
integrationsConfirmResetMessage: string;
integrationsConfirmResetDetails: string;
integrationsConfigureTitle: string;
integrationsCancel: string;
integrationsSave: string;
// PostgreSQL form strings
integrationsPostgresNameLabel: string;
integrationsPostgresNamePlaceholder: string;
integrationsPostgresHostLabel: string;
integrationsPostgresHostPlaceholder: string;
integrationsPostgresPortLabel: string;
integrationsPostgresPortPlaceholder: string;
integrationsPostgresDatabaseLabel: string;
integrationsPostgresDatabasePlaceholder: string;
integrationsPostgresUsernameLabel: string;
integrationsPostgresUsernamePlaceholder: string;
integrationsPostgresPasswordLabel: string;
integrationsPostgresPasswordPlaceholder: string;
integrationsPostgresSslLabel: string;
// BigQuery form strings
integrationsBigQueryNameLabel: string;
integrationsBigQueryNamePlaceholder: string;
integrationsBigQueryProjectIdLabel: string;
integrationsBigQueryProjectIdPlaceholder: string;
integrationsBigQueryCredentialsLabel: string;
integrationsBigQueryCredentialsPlaceholder: string;
integrationsBigQueryCredentialsRequired: string;
// Common form strings
integrationsRequiredField: string;
integrationsOptionalField: string;
};
// Map all messages to specific payloads
export class IInteractiveWindowMapping {
Expand Down
70 changes: 54 additions & 16 deletions src/notebooks/deepnote/integrations/integrationManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { commands, NotebookDocument, window, workspace } from 'vscode';
import { commands, l10n, NotebookDocument, window, workspace } from 'vscode';

import { IExtensionContext } from '../../../platform/common/types';
import { Commands } from '../../../platform/common/constants';
Expand All @@ -26,8 +26,27 @@ export class IntegrationManager implements IIntegrationManager {

public activate(): void {
// Register the manage integrations command
// The command can optionally receive an integration ID to select/configure
// Note: When invoked from a notebook cell status bar, VSCode passes context object first,
// then the actual arguments from the command definition
this.extensionContext.subscriptions.push(
commands.registerCommand(Commands.ManageIntegrations, () => this.showIntegrationsUI())
commands.registerCommand(Commands.ManageIntegrations, (...args: unknown[]) => {
logger.debug(`IntegrationManager: Command invoked with args:`, args);

// Find the integration ID from the arguments
// It could be the first arg (if called directly) or in the args array (if called from UI)
let integrationId: string | undefined;

for (const arg of args) {
if (typeof arg === 'string') {
integrationId = arg;
break;
}
}

logger.debug(`IntegrationManager: Extracted integrationId: ${integrationId}`);
return this.showIntegrationsUI(integrationId);
})
);

// Listen for active notebook changes to update context
Expand Down Expand Up @@ -95,18 +114,19 @@ export class IntegrationManager implements IIntegrationManager {

/**
* Show the integrations management UI
* @param selectedIntegrationId Optional integration ID to select/configure immediately
*/
private async showIntegrationsUI(): Promise<void> {
private async showIntegrationsUI(selectedIntegrationId?: string): Promise<void> {
const activeNotebook = window.activeNotebookEditor?.notebook;

if (!activeNotebook || activeNotebook.notebookType !== 'deepnote') {
void window.showErrorMessage('No active Deepnote notebook');
void window.showErrorMessage(l10n.t('No active Deepnote notebook'));
return;
}

const projectId = activeNotebook.metadata?.deepnoteProjectId;
if (!projectId) {
void window.showErrorMessage('Cannot determine project ID');
void window.showErrorMessage(l10n.t('Cannot determine project ID'));
return;
}

Expand All @@ -125,13 +145,24 @@ export class IntegrationManager implements IIntegrationManager {

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

// If a specific integration was requested (e.g., from status bar click),
// ensure it's in the map even if not detected from the project
if (selectedIntegrationId && !integrations.has(selectedIntegrationId)) {
logger.debug(`IntegrationManager: Adding requested integration ${selectedIntegrationId} to the map`);
const config = await this.integrationStorage.get(selectedIntegrationId);
integrations.set(selectedIntegrationId, {
config: config || null,
status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected
});
}

if (integrations.size === 0) {
void window.showInformationMessage(`No integrations found in this project.`);
void window.showInformationMessage(l10n.t('No integrations found in this project.'));
return;
}

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

/**
Expand All @@ -143,17 +174,24 @@ export class IntegrationManager implements IIntegrationManager {
const blocksWithIntegrations: BlockWithIntegration[] = [];

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

if (deepnoteMetadata?.sql_integration_id) {
blocksWithIntegrations.push({
id: `cell-${cell.index}`,
sql_integration_id: deepnoteMetadata.sql_integration_id
});
const metadata = cell.metadata;
logger.trace(`IntegrationManager: Cell ${cell.index} metadata:`, metadata);

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

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

// Use the shared utility to scan blocks and build the status map
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationManager');
}
Expand Down
42 changes: 40 additions & 2 deletions src/notebooks/deepnote/integrations/integrationStorage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { inject, injectable } from 'inversify';
import { EventEmitter } from 'vscode';

import { IEncryptedStorage } from '../../../platform/common/application/types';
import { IAsyncDisposableRegistry } from '../../../platform/common/types';
import { logger } from '../../../platform/logging';
import { IntegrationConfig, IntegrationType } from './integrationTypes';
import { IIntegrationStorage } from './types';

const INTEGRATION_SERVICE_NAME = 'deepnote-integrations';

Expand All @@ -12,12 +15,22 @@ const INTEGRATION_SERVICE_NAME = 'deepnote-integrations';
* Storage is scoped to the user's machine and shared across all deepnote projects.
*/
@injectable()
export class IntegrationStorage {
export class IntegrationStorage implements IIntegrationStorage {
private readonly cache: Map<string, IntegrationConfig> = new Map();

private cacheLoaded = false;

constructor(@inject(IEncryptedStorage) private readonly encryptedStorage: IEncryptedStorage) {}
private readonly _onDidChangeIntegrations = new EventEmitter<void>();

public readonly onDidChangeIntegrations = this._onDidChangeIntegrations.event;

constructor(
@inject(IEncryptedStorage) private readonly encryptedStorage: IEncryptedStorage,
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry
) {
// Register for disposal when the extension deactivates
asyncRegistry.push(this);
}

/**
* Get all stored integration configurations
Expand All @@ -35,6 +48,15 @@ export class IntegrationStorage {
return this.cache.get(integrationId);
}

/**
* Get integration configuration for a specific project and integration
* Note: Currently integrations are stored globally, not per-project,
* so this method ignores the projectId parameter
*/
async getIntegrationConfig(_projectId: string, integrationId: string): Promise<IntegrationConfig | undefined> {
return this.get(integrationId);
}

/**
* Get all integrations of a specific type
*/
Expand All @@ -58,6 +80,9 @@ export class IntegrationStorage {

// Update the index
await this.updateIndex();

// Fire change event
this._onDidChangeIntegrations.fire();
}

/**
Expand All @@ -74,6 +99,9 @@ export class IntegrationStorage {

// Update the index
await this.updateIndex();

// Fire change event
this._onDidChangeIntegrations.fire();
}

/**
Expand Down Expand Up @@ -101,6 +129,9 @@ export class IntegrationStorage {

// Clear cache
this.cache.clear();

// Notify listeners
this._onDidChangeIntegrations.fire();
}

/**
Expand Down Expand Up @@ -148,4 +179,11 @@ export class IntegrationStorage {
const indexJson = JSON.stringify(integrationIds);
await this.encryptedStorage.store(INTEGRATION_SERVICE_NAME, 'index', indexJson);
}

/**
* Dispose of resources to prevent memory leaks
*/
public dispose(): void {
this._onDidChangeIntegrations.dispose();
}
}
71 changes: 70 additions & 1 deletion src/notebooks/deepnote/integrations/integrationWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { inject, injectable } from 'inversify';
import { Disposable, l10n, Uri, ViewColumn, WebviewPanel, window } from 'vscode';

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 { IIntegrationStorage, IIntegrationWebviewProvider } from './types';
import { IntegrationConfig, IntegrationStatus, IntegrationWithStatus } from './integrationTypes';

Expand All @@ -24,8 +26,10 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {

/**
* Show the integration management webview
* @param integrations Map of integration IDs to their status
* @param selectedIntegrationId Optional integration ID to select/configure immediately
*/
public async show(integrations: Map<string, IntegrationWithStatus>): Promise<void> {
public async show(integrations: Map<string, IntegrationWithStatus>, selectedIntegrationId?: string): Promise<void> {
// Update the stored integrations with the latest data
this.integrations = integrations;

Expand All @@ -35,6 +39,11 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
if (this.currentPanel) {
this.currentPanel.reveal(column);
await this.updateWebview();

// If a specific integration was requested, show its configuration form
if (selectedIntegrationId) {
await this.showConfigurationForm(selectedIntegrationId);
}
return;
}

Expand Down Expand Up @@ -75,14 +84,73 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
this.disposables
);

await this.sendLocStrings();
await this.updateWebview();

// If a specific integration was requested, show its configuration form
if (selectedIntegrationId) {
await this.showConfigurationForm(selectedIntegrationId);
}
}

/**
* Send localization strings to the webview
*/
private async sendLocStrings(): Promise<void> {
if (!this.currentPanel) {
return;
}

const locStrings: Partial<LocalizedMessages> = {
integrationsTitle: localize.Integrations.title,
integrationsNoIntegrationsFound: localize.Integrations.noIntegrationsFound,
integrationsConnected: localize.Integrations.connected,
integrationsNotConfigured: localize.Integrations.notConfigured,
integrationsConfigure: localize.Integrations.configure,
integrationsReconfigure: localize.Integrations.reconfigure,
integrationsReset: localize.Integrations.reset,
integrationsConfirmResetTitle: localize.Integrations.confirmResetTitle,
integrationsConfirmResetMessage: localize.Integrations.confirmResetMessage,
integrationsConfirmResetDetails: localize.Integrations.confirmResetDetails,
integrationsConfigureTitle: localize.Integrations.configureTitle,
integrationsCancel: localize.Integrations.cancel,
integrationsSave: localize.Integrations.save,
integrationsRequiredField: localize.Integrations.requiredField,
integrationsOptionalField: localize.Integrations.optionalField,
integrationsPostgresNameLabel: localize.Integrations.postgresNameLabel,
integrationsPostgresNamePlaceholder: localize.Integrations.postgresNamePlaceholder,
integrationsPostgresHostLabel: localize.Integrations.postgresHostLabel,
integrationsPostgresHostPlaceholder: localize.Integrations.postgresHostPlaceholder,
integrationsPostgresPortLabel: localize.Integrations.postgresPortLabel,
integrationsPostgresPortPlaceholder: localize.Integrations.postgresPortPlaceholder,
integrationsPostgresDatabaseLabel: localize.Integrations.postgresDatabaseLabel,
integrationsPostgresDatabasePlaceholder: localize.Integrations.postgresDatabasePlaceholder,
integrationsPostgresUsernameLabel: localize.Integrations.postgresUsernameLabel,
integrationsPostgresUsernamePlaceholder: localize.Integrations.postgresUsernamePlaceholder,
integrationsPostgresPasswordLabel: localize.Integrations.postgresPasswordLabel,
integrationsPostgresPasswordPlaceholder: localize.Integrations.postgresPasswordPlaceholder,
integrationsPostgresSslLabel: localize.Integrations.postgresSslLabel,
integrationsBigQueryNameLabel: localize.Integrations.bigQueryNameLabel,
integrationsBigQueryNamePlaceholder: localize.Integrations.bigQueryNamePlaceholder,
integrationsBigQueryProjectIdLabel: localize.Integrations.bigQueryProjectIdLabel,
integrationsBigQueryProjectIdPlaceholder: localize.Integrations.bigQueryProjectIdPlaceholder,
integrationsBigQueryCredentialsLabel: localize.Integrations.bigQueryCredentialsLabel,
integrationsBigQueryCredentialsPlaceholder: localize.Integrations.bigQueryCredentialsPlaceholder,
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired
};

await this.currentPanel.webview.postMessage({
type: SharedMessages.LocInit,
locStrings: locStrings
});
}

/**
* Update the webview with current integration data
*/
private async updateWebview(): Promise<void> {
if (!this.currentPanel) {
logger.debug('IntegrationWebviewProvider: No current panel, skipping update');
return;
}

Expand All @@ -91,6 +159,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
id,
status: integration.status
}));
logger.debug(`IntegrationWebviewProvider: Sending ${integrationsData.length} integrations to webview`);

await this.currentPanel.webview.postMessage({
integrations: integrationsData,
Expand Down
Loading