Skip to content

Commit 90bfefd

Browse files
authored
feat: Creating+deleting of integrations, fix stylesheet load (#163)
* fix integrations management view stylesheet usage * add new integration creating cards to integration management * refactor: replace method with map * replace integration configuration form title to include integration type * replace default integration name placeholder * add integration deletion button * add svg logos of integrations * replace integration delete icon button with labeled button * differentiate reset and delete messages from integration webview * unify integration type labels * format files * remove https from img-src csp in integration webview * fix viewbox alignment of integration type icons * split warehouses and databases * add icons to existing integration items * unify integration type labels with web * add project name to integration management screen * use responsive grid layout * remove dead css code
1 parent 5400045 commit 90bfefd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1215
-156
lines changed

INTEGRATIONS_CREDENTIALS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The extension supports all 18 database integration types from the `@deepnote/dat
4040
- `'pgsql'` - PostgreSQL
4141
- `'mysql'` - MySQL
4242
- `'mariadb'` - MariaDB
43-
- `'alloydb'` - Google Cloud AlloyDB
43+
- `'alloydb'` - Google AlloyDB
4444
- `'clickhouse'` - ClickHouse
4545
- `'materialize'` - Materialize
4646
- `'mindsdb'` - MindsDB
@@ -51,7 +51,7 @@ The extension supports all 18 database integration types from the `@deepnote/dat
5151

5252
- `'big-query'` - Google BigQuery (service account JSON)
5353
- `'snowflake'` - Snowflake (password or key-pair auth)
54-
- `'spanner'` - Google Cloud Spanner (service account JSON)
54+
- `'spanner'` - Google Spanner (service account JSON)
5555

5656
**Cloud Databases (AWS credentials):**
5757

src/messageTypes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,20 @@ export type LocalizedMessages = {
173173
integrationsConfigure: string;
174174
integrationsReconfigure: string;
175175
integrationsReset: string;
176+
integrationsDelete: string;
176177
integrationsConfirmResetTitle: string;
177178
integrationsConfirmResetMessage: string;
178179
integrationsConfirmResetDetails: string;
180+
integrationsConfirmDeleteTitle: string;
181+
integrationsConfirmDeleteMessage: string;
182+
integrationsConfirmDeleteDetails: string;
179183
integrationsConfigureTitle: string;
180184
integrationsCancel: string;
181185
integrationsSave: string;
186+
integrationsAddNewIntegration: string;
187+
integrationsDatabase: string;
188+
integrationsDataWarehousesLakes: string;
189+
integrationsDatabases: string;
182190
// Integration type labels
183191
integrationsPostgresTypeLabel: string;
184192
integrationsBigQueryTypeLabel: string;
@@ -442,6 +450,7 @@ export type LocalizedMessages = {
442450
integrationsRequiredField: string;
443451
integrationsOptionalField: string;
444452
integrationsUnnamedIntegration: string;
453+
integrationsDefaultName: string;
445454
integrationsUnsupportedIntegrationType: string;
446455
// Select input settings strings
447456
selectInputSettingsTitle: string;

src/notebooks/deepnote/integrations/integrationWebview.ts

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,18 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
123123
integrationsConfigure: localize.Integrations.configure,
124124
integrationsReconfigure: localize.Integrations.reconfigure,
125125
integrationsReset: localize.Integrations.reset,
126+
integrationsDelete: localize.Integrations.deleteIntegration,
126127
integrationsConfirmResetTitle: localize.Integrations.confirmResetTitle,
127128
integrationsConfirmResetMessage: localize.Integrations.confirmResetMessage,
128129
integrationsConfirmResetDetails: localize.Integrations.confirmResetDetails,
130+
integrationsConfirmDeleteTitle: localize.Integrations.confirmDeleteTitle,
131+
integrationsConfirmDeleteMessage: localize.Integrations.confirmDeleteMessage,
132+
integrationsConfirmDeleteDetails: localize.Integrations.confirmDeleteDetails,
129133
integrationsConfigureTitle: localize.Integrations.configureTitle,
134+
integrationsAddNewIntegration: localize.Integrations.addNewIntegration,
135+
integrationsDatabase: localize.Integrations.database,
136+
integrationsDataWarehousesLakes: localize.Integrations.dataWarehousesLakes,
137+
integrationsDatabases: localize.Integrations.databases,
130138
integrationsPostgresTypeLabel: localize.Integrations.postgresTypeLabel,
131139
integrationsBigQueryTypeLabel: localize.Integrations.bigQueryTypeLabel,
132140
integrationsSnowflakeTypeLabel: localize.Integrations.snowflakeTypeLabel,
@@ -373,6 +381,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
373381
integrationsCaCertificateText: localize.Integrations.caCertificateText,
374382
integrationsCaCertificateTextPlaceholder: localize.Integrations.caCertificateTextPlaceholder,
375383
integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}'),
384+
integrationsDefaultName: localize.Integrations.defaultName('{0}'),
376385
integrationsUnsupportedIntegrationType: localize.Integrations.unsupportedIntegrationType('{0}')
377386
};
378387

@@ -400,8 +409,16 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
400409
}));
401410
logger.debug(`IntegrationWebviewProvider: Sending ${integrationsData.length} integrations to webview`);
402411

412+
// Get the project name from the notebook manager
413+
let projectName: string | undefined;
414+
if (this.projectId) {
415+
const project = this.notebookManager.getOriginalProject(this.projectId);
416+
projectName = project?.project.name;
417+
}
418+
403419
await this.currentPanel.webview.postMessage({
404420
integrations: integrationsData,
421+
projectName,
405422
type: 'update'
406423
});
407424
}
@@ -425,6 +442,11 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
425442
await this.saveConfiguration(message.integrationId, message.config);
426443
}
427444
break;
445+
case 'reset':
446+
if (message.integrationId) {
447+
await this.resetConfiguration(message.integrationId);
448+
}
449+
break;
428450
case 'delete':
429451
if (message.integrationId) {
430452
await this.deleteConfiguration(message.integrationId);
@@ -464,9 +486,20 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
464486
// Update local state
465487
const integration = this.integrations.get(integrationId);
466488
if (integration) {
489+
// Existing integration - update it
467490
integration.config = config;
468491
integration.status = IntegrationStatus.Connected;
492+
integration.integrationName = config.name;
493+
integration.integrationType = config.type;
469494
this.integrations.set(integrationId, integration);
495+
} else {
496+
// New integration - add it to the map
497+
this.integrations.set(integrationId, {
498+
config,
499+
status: IntegrationStatus.Connected,
500+
integrationName: config.name,
501+
integrationType: config.type
502+
});
470503
}
471504

472505
// Update the project's integrations list
@@ -490,9 +523,9 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
490523
}
491524

492525
/**
493-
* Delete the configuration for an integration
526+
* Reset the configuration for an integration (clears credentials but keeps the integration entry)
494527
*/
495-
private async deleteConfiguration(integrationId: string): Promise<void> {
528+
private async resetConfiguration(integrationId: string): Promise<void> {
496529
try {
497530
await this.integrationStorage.delete(integrationId);
498531

@@ -509,14 +542,44 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
509542

510543
await this.updateWebview();
511544
await this.currentPanel?.webview.postMessage({
512-
message: l10n.t('Configuration deleted successfully'),
545+
message: l10n.t('Configuration reset successfully'),
546+
type: 'success'
547+
});
548+
} catch (error) {
549+
logger.error('Failed to reset integration configuration', error);
550+
await this.currentPanel?.webview.postMessage({
551+
message: l10n.t(
552+
'Failed to reset configuration: {0}',
553+
error instanceof Error ? error.message : 'Unknown error'
554+
),
555+
type: 'error'
556+
});
557+
}
558+
}
559+
560+
/**
561+
* Delete the integration completely (removes credentials and integration entry)
562+
*/
563+
private async deleteConfiguration(integrationId: string): Promise<void> {
564+
try {
565+
await this.integrationStorage.delete(integrationId);
566+
567+
// Remove from local state
568+
this.integrations.delete(integrationId);
569+
570+
// Update the project's integrations list
571+
await this.updateProjectIntegrationsList();
572+
573+
await this.updateWebview();
574+
await this.currentPanel?.webview.postMessage({
575+
message: l10n.t('Integration deleted successfully'),
513576
type: 'success'
514577
});
515578
} catch (error) {
516-
logger.error('Failed to delete integration configuration', error);
579+
logger.error('Failed to delete integration', error);
517580
await this.currentPanel?.webview.postMessage({
518581
message: l10n.t(
519-
'Failed to delete configuration: {0}',
582+
'Failed to delete integration: {0}',
520583
error instanceof Error ? error.message : 'Unknown error'
521584
),
522585
type: 'error'
@@ -590,16 +653,6 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
590653
'index.js'
591654
)
592655
);
593-
const styleUri = webview.asWebviewUri(
594-
Uri.joinPath(
595-
this.extensionContext.extensionUri,
596-
'dist',
597-
'webviews',
598-
'webview-side',
599-
'integrations',
600-
'integrations.css'
601-
)
602-
);
603656
const codiconUri = webview.asWebviewUri(
604657
Uri.joinPath(
605658
this.extensionContext.extensionUri,
@@ -617,9 +670,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
617670
<head>
618671
<meta charset="UTF-8">
619672
<meta name="viewport" content="width=device-width, initial-scale=1.0">
620-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${webview.cspSource};">
673+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${webview.cspSource};">
621674
<link rel="stylesheet" href="${codiconUri}">
622-
<link rel="stylesheet" href="${styleUri}">
623675
<title>Deepnote Integrations</title>
624676
</head>
625677
<body>

src/notebooks/deepnote/sqlCellStatusBarProvider.ts

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ interface LocalQuickPickItem extends QuickPickItem {
3636
id: string;
3737
}
3838

39+
const integrationTypeLabels: Record<ConfigurableDatabaseIntegrationType, string> = {
40+
alloydb: l10n.t('Google AlloyDB'),
41+
athena: l10n.t('Amazon Athena'),
42+
'big-query': l10n.t('Google BigQuery'),
43+
clickhouse: l10n.t('ClickHouse'),
44+
databricks: l10n.t('Databricks'),
45+
dremio: l10n.t('Dremio'),
46+
mariadb: l10n.t('MariaDB'),
47+
materialize: l10n.t('Materialize'),
48+
mindsdb: l10n.t('MindsDB'),
49+
mongodb: l10n.t('MongoDB'),
50+
mysql: l10n.t('MySQL'),
51+
pgsql: l10n.t('PostgreSQL'),
52+
redshift: l10n.t('Amazon Redshift'),
53+
snowflake: l10n.t('Snowflake'),
54+
spanner: l10n.t('Google Spanner'),
55+
'sql-server': l10n.t('Microsoft SQL Server'),
56+
trino: l10n.t('Trino')
57+
};
58+
3959
/**
4060
* Provides status bar items for SQL cells showing the integration name and variable name
4161
*/
@@ -354,7 +374,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
354374

355375
const typeLabel =
356376
integrationType && (databaseIntegrationTypes as readonly string[]).includes(integrationType)
357-
? this.getIntegrationTypeLabel(integrationType)
377+
? integrationTypeLabels[integrationType] ?? integrationType
358378
: projectIntegration.type;
359379

360380
const item: LocalQuickPickItem = {
@@ -437,45 +457,4 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
437457
// Trigger status bar update
438458
this._onDidChangeCellStatusBarItems.fire();
439459
}
440-
441-
private getIntegrationTypeLabel(type: ConfigurableDatabaseIntegrationType): string {
442-
switch (type) {
443-
case 'alloydb':
444-
return l10n.t('AlloyDB');
445-
case 'athena':
446-
return l10n.t('Amazon Athena');
447-
case 'big-query':
448-
return l10n.t('BigQuery');
449-
case 'clickhouse':
450-
return l10n.t('ClickHouse');
451-
case 'databricks':
452-
return l10n.t('Databricks');
453-
case 'dremio':
454-
return l10n.t('Dremio');
455-
case 'mariadb':
456-
return l10n.t('MariaDB');
457-
case 'materialize':
458-
return l10n.t('Materialize');
459-
case 'mindsdb':
460-
return l10n.t('MindsDB');
461-
case 'mongodb':
462-
return l10n.t('MongoDB');
463-
case 'mysql':
464-
return l10n.t('MySQL');
465-
case 'pgsql':
466-
return l10n.t('PostgreSQL');
467-
case 'redshift':
468-
return l10n.t('Amazon Redshift');
469-
case 'snowflake':
470-
return l10n.t('Snowflake');
471-
case 'spanner':
472-
return l10n.t('Google Cloud Spanner');
473-
case 'sql-server':
474-
return l10n.t('SQL Server');
475-
case 'trino':
476-
return l10n.t('Trino');
477-
default:
478-
return String(type);
479-
}
480-
}
481460
}

src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ suite('SqlCellStatusBarProvider', () => {
996996
assert.strictEqual(duckDbItem.label, 'DataFrame SQL (DuckDB)');
997997
});
998998

999-
test('shows BigQuery type label for BigQuery integrations', async () => {
999+
test('shows BigQuery type label for Google BigQuery integrations', async () => {
10001000
const notebookMetadata = { deepnoteProjectId: 'project-1' };
10011001
const cell = createMockCell('sql', {}, notebookMetadata);
10021002
let quickPickItems: any[] = [];
@@ -1006,7 +1006,7 @@ suite('SqlCellStatusBarProvider', () => {
10061006
integrations: [
10071007
{
10081008
id: 'bigquery-integration',
1009-
name: 'My BigQuery',
1009+
name: 'My Google BigQuery',
10101010
type: 'big-query'
10111011
}
10121012
]
@@ -1021,8 +1021,8 @@ suite('SqlCellStatusBarProvider', () => {
10211021
await switchIntegrationHandler(cell);
10221022

10231023
const bigQueryItem = quickPickItems.find((item) => item.id === 'bigquery-integration');
1024-
assert.isDefined(bigQueryItem, 'BigQuery integration should be in quick pick items');
1025-
assert.strictEqual(bigQueryItem.description, 'BigQuery');
1024+
assert.isDefined(bigQueryItem, 'Google BigQuery integration should be in quick pick items');
1025+
assert.strictEqual(bigQueryItem.description, 'Google BigQuery');
10261026
});
10271027

10281028
test('shows raw type for unknown integration types', async () => {

src/platform/common/utils/localize.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -822,22 +822,33 @@ export namespace Integrations {
822822
export const configure = l10n.t('Configure');
823823
export const reconfigure = l10n.t('Reconfigure');
824824
export const reset = l10n.t('Reset');
825+
export const deleteIntegration = l10n.t('Delete');
825826
export const confirmResetTitle = l10n.t('Confirm Reset');
826827
export const confirmResetMessage = l10n.t('Are you sure you want to reset this integration configuration?');
827828
export const confirmResetDetails = l10n.t('This will remove the stored credentials. You can reconfigure it later.');
829+
export const confirmDeleteTitle = l10n.t('Confirm Delete');
830+
export const confirmDeleteMessage = l10n.t('Are you sure you want to permanently delete this integration?');
831+
export const confirmDeleteDetails = l10n.t(
832+
'This will permanently remove the integration from your project. This action cannot be undone.'
833+
);
828834
export const configureTitle = l10n.t('Configure Integration: {0}');
829835
export const cancel = l10n.t('Cancel');
830836
export const save = l10n.t('Save');
837+
export const addNewIntegration = l10n.t('Add New Integration');
838+
export const database = l10n.t('Database');
839+
export const dataWarehousesLakes = l10n.t('Data Warehouses & Lakes');
840+
export const databases = l10n.t('Databases');
831841
export const requiredField = l10n.t('*');
832842
export const optionalField = l10n.t('(optional)');
833843
export const unnamedIntegration = (id: string) => l10n.t('Unnamed Integration ({0})', id);
844+
export const defaultName = (type: string) => l10n.t('My {0} integration', type);
834845
export const unsupportedIntegrationType = (type: string) => l10n.t('Unsupported integration type: {0}', type);
835846

836847
// Integration type labels
837848
export const postgresTypeLabel = l10n.t('PostgreSQL');
838-
export const bigQueryTypeLabel = l10n.t('BigQuery');
849+
export const bigQueryTypeLabel = l10n.t('Google BigQuery');
839850
export const snowflakeTypeLabel = l10n.t('Snowflake');
840-
export const alloyDBTypeLabel = l10n.t('AlloyDB');
851+
export const alloyDBTypeLabel = l10n.t('Google AlloyDB');
841852
export const athenaTypeLabel = l10n.t('Amazon Athena');
842853
export const clickHouseTypeLabel = l10n.t('ClickHouse');
843854
export const databricksTypeLabel = l10n.t('Databricks');
@@ -849,8 +860,8 @@ export namespace Integrations {
849860
export const mySQLTypeLabel = l10n.t('MySQL');
850861
export const duckDBTypeLabel = l10n.t('DuckDB');
851862
export const redshiftTypeLabel = l10n.t('Amazon Redshift');
852-
export const spannerTypeLabel = l10n.t('Google Cloud Spanner');
853-
export const sqlServerTypeLabel = l10n.t('SQL Server');
863+
export const spannerTypeLabel = l10n.t('Google Spanner');
864+
export const sqlServerTypeLabel = l10n.t('Microsoft SQL Server');
854865
export const trinoTypeLabel = l10n.t('Trino');
855866

856867
// PostgreSQL form strings

src/webviews/webview-side/integrations/AlloyDBForm.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as React from 'react';
2-
import { format, getLocString } from '../react-common/locReactSide';
2+
import { getLocString } from '../react-common/locReactSide';
33
import { DatabaseIntegrationConfig } from '@deepnote/database-integrations';
44
import { SshOptionsFields } from './SshOptionsFields';
55
import { CaCertificateFields } from './CaCertificateFields';
6+
import { getDefaultIntegrationName } from './integrationUtils';
67

78
export interface IAlloyDBFormProps {
89
integrationId: string;
@@ -16,11 +17,9 @@ function createEmptyAlloyDBConfig(params: {
1617
id: string;
1718
name?: string;
1819
}): Extract<DatabaseIntegrationConfig, { type: 'alloydb' }> {
19-
const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})');
20-
2120
return {
2221
id: params.id,
23-
name: (params.name || format(unnamedIntegration, params.id)).trim(),
22+
name: (params.name || getDefaultIntegrationName('alloydb')).trim(),
2423
type: 'alloydb',
2524
metadata: {
2625
host: '',

0 commit comments

Comments
 (0)