Skip to content

Commit 953a32a

Browse files
authored
feat: Buttons to open SQL block integration config + fix localization (#73)
1 parent 146cda4 commit 953a32a

19 files changed

+652
-76
lines changed

src/messageTypes.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,45 @@ export type LocalizedMessages = {
165165
dataframePageOf: string;
166166
dataframeCopyTable: string;
167167
dataframeExportTable: string;
168+
// Integration panel strings
169+
integrationsTitle: string;
170+
integrationsNoIntegrationsFound: string;
171+
integrationsConnected: string;
172+
integrationsNotConfigured: string;
173+
integrationsConfigure: string;
174+
integrationsReconfigure: string;
175+
integrationsReset: string;
176+
integrationsConfirmResetTitle: string;
177+
integrationsConfirmResetMessage: string;
178+
integrationsConfirmResetDetails: string;
179+
integrationsConfigureTitle: string;
180+
integrationsCancel: string;
181+
integrationsSave: string;
182+
// PostgreSQL form strings
183+
integrationsPostgresNameLabel: string;
184+
integrationsPostgresNamePlaceholder: string;
185+
integrationsPostgresHostLabel: string;
186+
integrationsPostgresHostPlaceholder: string;
187+
integrationsPostgresPortLabel: string;
188+
integrationsPostgresPortPlaceholder: string;
189+
integrationsPostgresDatabaseLabel: string;
190+
integrationsPostgresDatabasePlaceholder: string;
191+
integrationsPostgresUsernameLabel: string;
192+
integrationsPostgresUsernamePlaceholder: string;
193+
integrationsPostgresPasswordLabel: string;
194+
integrationsPostgresPasswordPlaceholder: string;
195+
integrationsPostgresSslLabel: string;
196+
// BigQuery form strings
197+
integrationsBigQueryNameLabel: string;
198+
integrationsBigQueryNamePlaceholder: string;
199+
integrationsBigQueryProjectIdLabel: string;
200+
integrationsBigQueryProjectIdPlaceholder: string;
201+
integrationsBigQueryCredentialsLabel: string;
202+
integrationsBigQueryCredentialsPlaceholder: string;
203+
integrationsBigQueryCredentialsRequired: string;
204+
// Common form strings
205+
integrationsRequiredField: string;
206+
integrationsOptionalField: string;
168207
};
169208
// Map all messages to specific payloads
170209
export class IInteractiveWindowMapping {

src/notebooks/deepnote/integrations/integrationManager.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { inject, injectable } from 'inversify';
2-
import { commands, NotebookDocument, window, workspace } from 'vscode';
2+
import { commands, l10n, NotebookDocument, window, workspace } from 'vscode';
33

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

2727
public activate(): void {
2828
// Register the manage integrations command
29+
// The command can optionally receive an integration ID to select/configure
30+
// Note: When invoked from a notebook cell status bar, VSCode passes context object first,
31+
// then the actual arguments from the command definition
2932
this.extensionContext.subscriptions.push(
30-
commands.registerCommand(Commands.ManageIntegrations, () => this.showIntegrationsUI())
33+
commands.registerCommand(Commands.ManageIntegrations, (...args: unknown[]) => {
34+
logger.debug(`IntegrationManager: Command invoked with args:`, args);
35+
36+
// Find the integration ID from the arguments
37+
// It could be the first arg (if called directly) or in the args array (if called from UI)
38+
let integrationId: string | undefined;
39+
40+
for (const arg of args) {
41+
if (typeof arg === 'string') {
42+
integrationId = arg;
43+
break;
44+
}
45+
}
46+
47+
logger.debug(`IntegrationManager: Extracted integrationId: ${integrationId}`);
48+
return this.showIntegrationsUI(integrationId);
49+
})
3150
);
3251

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

96115
/**
97116
* Show the integrations management UI
117+
* @param selectedIntegrationId Optional integration ID to select/configure immediately
98118
*/
99-
private async showIntegrationsUI(): Promise<void> {
119+
private async showIntegrationsUI(selectedIntegrationId?: string): Promise<void> {
100120
const activeNotebook = window.activeNotebookEditor?.notebook;
101121

102122
if (!activeNotebook || activeNotebook.notebookType !== 'deepnote') {
103-
void window.showErrorMessage('No active Deepnote notebook');
123+
void window.showErrorMessage(l10n.t('No active Deepnote notebook'));
104124
return;
105125
}
106126

107127
const projectId = activeNotebook.metadata?.deepnoteProjectId;
108128
if (!projectId) {
109-
void window.showErrorMessage('Cannot determine project ID');
129+
void window.showErrorMessage(l10n.t('Cannot determine project ID'));
110130
return;
111131
}
112132

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

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

148+
// If a specific integration was requested (e.g., from status bar click),
149+
// ensure it's in the map even if not detected from the project
150+
if (selectedIntegrationId && !integrations.has(selectedIntegrationId)) {
151+
logger.debug(`IntegrationManager: Adding requested integration ${selectedIntegrationId} to the map`);
152+
const config = await this.integrationStorage.get(selectedIntegrationId);
153+
integrations.set(selectedIntegrationId, {
154+
config: config || null,
155+
status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected
156+
});
157+
}
158+
128159
if (integrations.size === 0) {
129-
void window.showInformationMessage(`No integrations found in this project.`);
160+
void window.showInformationMessage(l10n.t('No integrations found in this project.'));
130161
return;
131162
}
132163

133-
// Show the webview
134-
await this.webviewProvider.show(integrations);
164+
// Show the webview with optional selected integration
165+
await this.webviewProvider.show(integrations, selectedIntegrationId);
135166
}
136167

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

145176
for (const cell of notebook.getCells()) {
146-
const deepnoteMetadata = cell.metadata?.deepnoteMetadata;
147-
logger.trace(`IntegrationManager: Cell ${cell.index} metadata:`, deepnoteMetadata);
148-
149-
if (deepnoteMetadata?.sql_integration_id) {
150-
blocksWithIntegrations.push({
151-
id: `cell-${cell.index}`,
152-
sql_integration_id: deepnoteMetadata.sql_integration_id
153-
});
177+
const metadata = cell.metadata;
178+
logger.trace(`IntegrationManager: Cell ${cell.index} metadata:`, metadata);
179+
180+
// Check cell metadata for sql_integration_id
181+
if (metadata && typeof metadata === 'object') {
182+
const integrationId = (metadata as Record<string, unknown>).sql_integration_id;
183+
if (typeof integrationId === 'string') {
184+
logger.debug(`IntegrationManager: Found integration ${integrationId} in cell ${cell.index}`);
185+
blocksWithIntegrations.push({
186+
id: `cell-${cell.index}`,
187+
sql_integration_id: integrationId
188+
});
189+
}
154190
}
155191
}
156192

193+
logger.debug(`IntegrationManager: Found ${blocksWithIntegrations.length} cells with integrations`);
194+
157195
// Use the shared utility to scan blocks and build the status map
158196
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationManager');
159197
}

src/notebooks/deepnote/integrations/integrationStorage.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { inject, injectable } from 'inversify';
2+
import { EventEmitter } from 'vscode';
23

34
import { IEncryptedStorage } from '../../../platform/common/application/types';
5+
import { IAsyncDisposableRegistry } from '../../../platform/common/types';
46
import { logger } from '../../../platform/logging';
57
import { IntegrationConfig, IntegrationType } from './integrationTypes';
8+
import { IIntegrationStorage } from './types';
69

710
const INTEGRATION_SERVICE_NAME = 'deepnote-integrations';
811

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

1821
private cacheLoaded = false;
1922

20-
constructor(@inject(IEncryptedStorage) private readonly encryptedStorage: IEncryptedStorage) {}
23+
private readonly _onDidChangeIntegrations = new EventEmitter<void>();
24+
25+
public readonly onDidChangeIntegrations = this._onDidChangeIntegrations.event;
26+
27+
constructor(
28+
@inject(IEncryptedStorage) private readonly encryptedStorage: IEncryptedStorage,
29+
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry
30+
) {
31+
// Register for disposal when the extension deactivates
32+
asyncRegistry.push(this);
33+
}
2134

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

51+
/**
52+
* Get integration configuration for a specific project and integration
53+
* Note: Currently integrations are stored globally, not per-project,
54+
* so this method ignores the projectId parameter
55+
*/
56+
async getIntegrationConfig(_projectId: string, integrationId: string): Promise<IntegrationConfig | undefined> {
57+
return this.get(integrationId);
58+
}
59+
3860
/**
3961
* Get all integrations of a specific type
4062
*/
@@ -58,6 +80,9 @@ export class IntegrationStorage {
5880

5981
// Update the index
6082
await this.updateIndex();
83+
84+
// Fire change event
85+
this._onDidChangeIntegrations.fire();
6186
}
6287

6388
/**
@@ -74,6 +99,9 @@ export class IntegrationStorage {
7499

75100
// Update the index
76101
await this.updateIndex();
102+
103+
// Fire change event
104+
this._onDidChangeIntegrations.fire();
77105
}
78106

79107
/**
@@ -101,6 +129,9 @@ export class IntegrationStorage {
101129

102130
// Clear cache
103131
this.cache.clear();
132+
133+
// Notify listeners
134+
this._onDidChangeIntegrations.fire();
104135
}
105136

106137
/**
@@ -148,4 +179,11 @@ export class IntegrationStorage {
148179
const indexJson = JSON.stringify(integrationIds);
149180
await this.encryptedStorage.store(INTEGRATION_SERVICE_NAME, 'index', indexJson);
150181
}
182+
183+
/**
184+
* Dispose of resources to prevent memory leaks
185+
*/
186+
public dispose(): void {
187+
this._onDidChangeIntegrations.dispose();
188+
}
151189
}

src/notebooks/deepnote/integrations/integrationWebview.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { inject, injectable } from 'inversify';
22
import { Disposable, l10n, Uri, ViewColumn, WebviewPanel, window } from 'vscode';
33

44
import { IExtensionContext } from '../../../platform/common/types';
5+
import * as localize from '../../../platform/common/utils/localize';
56
import { logger } from '../../../platform/logging';
7+
import { LocalizedMessages, SharedMessages } from '../../../messageTypes';
68
import { IIntegrationStorage, IIntegrationWebviewProvider } from './types';
79
import { IntegrationConfig, IntegrationStatus, IntegrationWithStatus } from './integrationTypes';
810

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

2527
/**
2628
* Show the integration management webview
29+
* @param integrations Map of integration IDs to their status
30+
* @param selectedIntegrationId Optional integration ID to select/configure immediately
2731
*/
28-
public async show(integrations: Map<string, IntegrationWithStatus>): Promise<void> {
32+
public async show(integrations: Map<string, IntegrationWithStatus>, selectedIntegrationId?: string): Promise<void> {
2933
// Update the stored integrations with the latest data
3034
this.integrations = integrations;
3135

@@ -35,6 +39,11 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
3539
if (this.currentPanel) {
3640
this.currentPanel.reveal(column);
3741
await this.updateWebview();
42+
43+
// If a specific integration was requested, show its configuration form
44+
if (selectedIntegrationId) {
45+
await this.showConfigurationForm(selectedIntegrationId);
46+
}
3847
return;
3948
}
4049

@@ -75,14 +84,73 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
7584
this.disposables
7685
);
7786

87+
await this.sendLocStrings();
7888
await this.updateWebview();
89+
90+
// If a specific integration was requested, show its configuration form
91+
if (selectedIntegrationId) {
92+
await this.showConfigurationForm(selectedIntegrationId);
93+
}
94+
}
95+
96+
/**
97+
* Send localization strings to the webview
98+
*/
99+
private async sendLocStrings(): Promise<void> {
100+
if (!this.currentPanel) {
101+
return;
102+
}
103+
104+
const locStrings: Partial<LocalizedMessages> = {
105+
integrationsTitle: localize.Integrations.title,
106+
integrationsNoIntegrationsFound: localize.Integrations.noIntegrationsFound,
107+
integrationsConnected: localize.Integrations.connected,
108+
integrationsNotConfigured: localize.Integrations.notConfigured,
109+
integrationsConfigure: localize.Integrations.configure,
110+
integrationsReconfigure: localize.Integrations.reconfigure,
111+
integrationsReset: localize.Integrations.reset,
112+
integrationsConfirmResetTitle: localize.Integrations.confirmResetTitle,
113+
integrationsConfirmResetMessage: localize.Integrations.confirmResetMessage,
114+
integrationsConfirmResetDetails: localize.Integrations.confirmResetDetails,
115+
integrationsConfigureTitle: localize.Integrations.configureTitle,
116+
integrationsCancel: localize.Integrations.cancel,
117+
integrationsSave: localize.Integrations.save,
118+
integrationsRequiredField: localize.Integrations.requiredField,
119+
integrationsOptionalField: localize.Integrations.optionalField,
120+
integrationsPostgresNameLabel: localize.Integrations.postgresNameLabel,
121+
integrationsPostgresNamePlaceholder: localize.Integrations.postgresNamePlaceholder,
122+
integrationsPostgresHostLabel: localize.Integrations.postgresHostLabel,
123+
integrationsPostgresHostPlaceholder: localize.Integrations.postgresHostPlaceholder,
124+
integrationsPostgresPortLabel: localize.Integrations.postgresPortLabel,
125+
integrationsPostgresPortPlaceholder: localize.Integrations.postgresPortPlaceholder,
126+
integrationsPostgresDatabaseLabel: localize.Integrations.postgresDatabaseLabel,
127+
integrationsPostgresDatabasePlaceholder: localize.Integrations.postgresDatabasePlaceholder,
128+
integrationsPostgresUsernameLabel: localize.Integrations.postgresUsernameLabel,
129+
integrationsPostgresUsernamePlaceholder: localize.Integrations.postgresUsernamePlaceholder,
130+
integrationsPostgresPasswordLabel: localize.Integrations.postgresPasswordLabel,
131+
integrationsPostgresPasswordPlaceholder: localize.Integrations.postgresPasswordPlaceholder,
132+
integrationsPostgresSslLabel: localize.Integrations.postgresSslLabel,
133+
integrationsBigQueryNameLabel: localize.Integrations.bigQueryNameLabel,
134+
integrationsBigQueryNamePlaceholder: localize.Integrations.bigQueryNamePlaceholder,
135+
integrationsBigQueryProjectIdLabel: localize.Integrations.bigQueryProjectIdLabel,
136+
integrationsBigQueryProjectIdPlaceholder: localize.Integrations.bigQueryProjectIdPlaceholder,
137+
integrationsBigQueryCredentialsLabel: localize.Integrations.bigQueryCredentialsLabel,
138+
integrationsBigQueryCredentialsPlaceholder: localize.Integrations.bigQueryCredentialsPlaceholder,
139+
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired
140+
};
141+
142+
await this.currentPanel.webview.postMessage({
143+
type: SharedMessages.LocInit,
144+
locStrings: locStrings
145+
});
79146
}
80147

81148
/**
82149
* Update the webview with current integration data
83150
*/
84151
private async updateWebview(): Promise<void> {
85152
if (!this.currentPanel) {
153+
logger.debug('IntegrationWebviewProvider: No current panel, skipping update');
86154
return;
87155
}
88156

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

95164
await this.currentPanel.webview.postMessage({
96165
integrations: integrationsData,

0 commit comments

Comments
 (0)