diff --git a/INTEGRATIONS_CREDENTIALS.md b/INTEGRATIONS_CREDENTIALS.md index 6efa29ee6..4754576a5 100644 --- a/INTEGRATIONS_CREDENTIALS.md +++ b/INTEGRATIONS_CREDENTIALS.md @@ -40,7 +40,7 @@ The extension supports all 18 database integration types from the `@deepnote/dat - `'pgsql'` - PostgreSQL - `'mysql'` - MySQL - `'mariadb'` - MariaDB -- `'alloydb'` - Google Cloud AlloyDB +- `'alloydb'` - Google AlloyDB - `'clickhouse'` - ClickHouse - `'materialize'` - Materialize - `'mindsdb'` - MindsDB @@ -51,7 +51,7 @@ The extension supports all 18 database integration types from the `@deepnote/dat - `'big-query'` - Google BigQuery (service account JSON) - `'snowflake'` - Snowflake (password or key-pair auth) -- `'spanner'` - Google Cloud Spanner (service account JSON) +- `'spanner'` - Google Spanner (service account JSON) **Cloud Databases (AWS credentials):** diff --git a/src/messageTypes.ts b/src/messageTypes.ts index 53b36b44c..9cd884dc5 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -173,12 +173,20 @@ export type LocalizedMessages = { integrationsConfigure: string; integrationsReconfigure: string; integrationsReset: string; + integrationsDelete: string; integrationsConfirmResetTitle: string; integrationsConfirmResetMessage: string; integrationsConfirmResetDetails: string; + integrationsConfirmDeleteTitle: string; + integrationsConfirmDeleteMessage: string; + integrationsConfirmDeleteDetails: string; integrationsConfigureTitle: string; integrationsCancel: string; integrationsSave: string; + integrationsAddNewIntegration: string; + integrationsDatabase: string; + integrationsDataWarehousesLakes: string; + integrationsDatabases: string; // Integration type labels integrationsPostgresTypeLabel: string; integrationsBigQueryTypeLabel: string; @@ -442,6 +450,7 @@ export type LocalizedMessages = { integrationsRequiredField: string; integrationsOptionalField: string; integrationsUnnamedIntegration: string; + integrationsDefaultName: string; integrationsUnsupportedIntegrationType: string; // Select input settings strings selectInputSettingsTitle: string; diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index 99d6b1239..3daf4dc47 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -123,10 +123,18 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { integrationsConfigure: localize.Integrations.configure, integrationsReconfigure: localize.Integrations.reconfigure, integrationsReset: localize.Integrations.reset, + integrationsDelete: localize.Integrations.deleteIntegration, integrationsConfirmResetTitle: localize.Integrations.confirmResetTitle, integrationsConfirmResetMessage: localize.Integrations.confirmResetMessage, integrationsConfirmResetDetails: localize.Integrations.confirmResetDetails, + integrationsConfirmDeleteTitle: localize.Integrations.confirmDeleteTitle, + integrationsConfirmDeleteMessage: localize.Integrations.confirmDeleteMessage, + integrationsConfirmDeleteDetails: localize.Integrations.confirmDeleteDetails, integrationsConfigureTitle: localize.Integrations.configureTitle, + integrationsAddNewIntegration: localize.Integrations.addNewIntegration, + integrationsDatabase: localize.Integrations.database, + integrationsDataWarehousesLakes: localize.Integrations.dataWarehousesLakes, + integrationsDatabases: localize.Integrations.databases, integrationsPostgresTypeLabel: localize.Integrations.postgresTypeLabel, integrationsBigQueryTypeLabel: localize.Integrations.bigQueryTypeLabel, integrationsSnowflakeTypeLabel: localize.Integrations.snowflakeTypeLabel, @@ -373,6 +381,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { integrationsCaCertificateText: localize.Integrations.caCertificateText, integrationsCaCertificateTextPlaceholder: localize.Integrations.caCertificateTextPlaceholder, integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}'), + integrationsDefaultName: localize.Integrations.defaultName('{0}'), integrationsUnsupportedIntegrationType: localize.Integrations.unsupportedIntegrationType('{0}') }; @@ -400,8 +409,16 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { })); logger.debug(`IntegrationWebviewProvider: Sending ${integrationsData.length} integrations to webview`); + // Get the project name from the notebook manager + let projectName: string | undefined; + if (this.projectId) { + const project = this.notebookManager.getOriginalProject(this.projectId); + projectName = project?.project.name; + } + await this.currentPanel.webview.postMessage({ integrations: integrationsData, + projectName, type: 'update' }); } @@ -425,6 +442,11 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { await this.saveConfiguration(message.integrationId, message.config); } break; + case 'reset': + if (message.integrationId) { + await this.resetConfiguration(message.integrationId); + } + break; case 'delete': if (message.integrationId) { await this.deleteConfiguration(message.integrationId); @@ -464,9 +486,20 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { // Update local state const integration = this.integrations.get(integrationId); if (integration) { + // Existing integration - update it integration.config = config; integration.status = IntegrationStatus.Connected; + integration.integrationName = config.name; + integration.integrationType = config.type; this.integrations.set(integrationId, integration); + } else { + // New integration - add it to the map + this.integrations.set(integrationId, { + config, + status: IntegrationStatus.Connected, + integrationName: config.name, + integrationType: config.type + }); } // Update the project's integrations list @@ -490,9 +523,9 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { } /** - * Delete the configuration for an integration + * Reset the configuration for an integration (clears credentials but keeps the integration entry) */ - private async deleteConfiguration(integrationId: string): Promise { + private async resetConfiguration(integrationId: string): Promise { try { await this.integrationStorage.delete(integrationId); @@ -509,14 +542,44 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { await this.updateWebview(); await this.currentPanel?.webview.postMessage({ - message: l10n.t('Configuration deleted successfully'), + message: l10n.t('Configuration reset successfully'), + type: 'success' + }); + } catch (error) { + logger.error('Failed to reset integration configuration', error); + await this.currentPanel?.webview.postMessage({ + message: l10n.t( + 'Failed to reset configuration: {0}', + error instanceof Error ? error.message : 'Unknown error' + ), + type: 'error' + }); + } + } + + /** + * Delete the integration completely (removes credentials and integration entry) + */ + private async deleteConfiguration(integrationId: string): Promise { + try { + await this.integrationStorage.delete(integrationId); + + // Remove from local state + this.integrations.delete(integrationId); + + // Update the project's integrations list + await this.updateProjectIntegrationsList(); + + await this.updateWebview(); + await this.currentPanel?.webview.postMessage({ + message: l10n.t('Integration deleted successfully'), type: 'success' }); } catch (error) { - logger.error('Failed to delete integration configuration', error); + logger.error('Failed to delete integration', error); await this.currentPanel?.webview.postMessage({ message: l10n.t( - 'Failed to delete configuration: {0}', + 'Failed to delete integration: {0}', error instanceof Error ? error.message : 'Unknown error' ), type: 'error' @@ -590,16 +653,6 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { 'index.js' ) ); - const styleUri = webview.asWebviewUri( - Uri.joinPath( - this.extensionContext.extensionUri, - 'dist', - 'webviews', - 'webview-side', - 'integrations', - 'integrations.css' - ) - ); const codiconUri = webview.asWebviewUri( Uri.joinPath( this.extensionContext.extensionUri, @@ -617,9 +670,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { - + - Deepnote Integrations diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index b7fade350..210fcdaa2 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -36,6 +36,26 @@ interface LocalQuickPickItem extends QuickPickItem { id: string; } +const integrationTypeLabels: Record = { + alloydb: l10n.t('Google AlloyDB'), + athena: l10n.t('Amazon Athena'), + 'big-query': l10n.t('Google BigQuery'), + clickhouse: l10n.t('ClickHouse'), + databricks: l10n.t('Databricks'), + dremio: l10n.t('Dremio'), + mariadb: l10n.t('MariaDB'), + materialize: l10n.t('Materialize'), + mindsdb: l10n.t('MindsDB'), + mongodb: l10n.t('MongoDB'), + mysql: l10n.t('MySQL'), + pgsql: l10n.t('PostgreSQL'), + redshift: l10n.t('Amazon Redshift'), + snowflake: l10n.t('Snowflake'), + spanner: l10n.t('Google Spanner'), + 'sql-server': l10n.t('Microsoft SQL Server'), + trino: l10n.t('Trino') +}; + /** * Provides status bar items for SQL cells showing the integration name and variable name */ @@ -354,7 +374,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid const typeLabel = integrationType && (databaseIntegrationTypes as readonly string[]).includes(integrationType) - ? this.getIntegrationTypeLabel(integrationType) + ? integrationTypeLabels[integrationType] ?? integrationType : projectIntegration.type; const item: LocalQuickPickItem = { @@ -437,45 +457,4 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Trigger status bar update this._onDidChangeCellStatusBarItems.fire(); } - - private getIntegrationTypeLabel(type: ConfigurableDatabaseIntegrationType): string { - switch (type) { - case 'alloydb': - return l10n.t('AlloyDB'); - case 'athena': - return l10n.t('Amazon Athena'); - case 'big-query': - return l10n.t('BigQuery'); - case 'clickhouse': - return l10n.t('ClickHouse'); - case 'databricks': - return l10n.t('Databricks'); - case 'dremio': - return l10n.t('Dremio'); - case 'mariadb': - return l10n.t('MariaDB'); - case 'materialize': - return l10n.t('Materialize'); - case 'mindsdb': - return l10n.t('MindsDB'); - case 'mongodb': - return l10n.t('MongoDB'); - case 'mysql': - return l10n.t('MySQL'); - case 'pgsql': - return l10n.t('PostgreSQL'); - case 'redshift': - return l10n.t('Amazon Redshift'); - case 'snowflake': - return l10n.t('Snowflake'); - case 'spanner': - return l10n.t('Google Cloud Spanner'); - case 'sql-server': - return l10n.t('SQL Server'); - case 'trino': - return l10n.t('Trino'); - default: - return String(type); - } - } } diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 40a72119b..d89ac13a4 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -996,7 +996,7 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(duckDbItem.label, 'DataFrame SQL (DuckDB)'); }); - test('shows BigQuery type label for BigQuery integrations', async () => { + test('shows BigQuery type label for Google BigQuery integrations', async () => { const notebookMetadata = { deepnoteProjectId: 'project-1' }; const cell = createMockCell('sql', {}, notebookMetadata); let quickPickItems: any[] = []; @@ -1006,7 +1006,7 @@ suite('SqlCellStatusBarProvider', () => { integrations: [ { id: 'bigquery-integration', - name: 'My BigQuery', + name: 'My Google BigQuery', type: 'big-query' } ] @@ -1021,8 +1021,8 @@ suite('SqlCellStatusBarProvider', () => { await switchIntegrationHandler(cell); const bigQueryItem = quickPickItems.find((item) => item.id === 'bigquery-integration'); - assert.isDefined(bigQueryItem, 'BigQuery integration should be in quick pick items'); - assert.strictEqual(bigQueryItem.description, 'BigQuery'); + assert.isDefined(bigQueryItem, 'Google BigQuery integration should be in quick pick items'); + assert.strictEqual(bigQueryItem.description, 'Google BigQuery'); }); test('shows raw type for unknown integration types', async () => { diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index 6296b4b2e..d127170b5 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -822,22 +822,33 @@ export namespace Integrations { export const configure = l10n.t('Configure'); export const reconfigure = l10n.t('Reconfigure'); export const reset = l10n.t('Reset'); + export const deleteIntegration = l10n.t('Delete'); export const confirmResetTitle = l10n.t('Confirm Reset'); export const confirmResetMessage = l10n.t('Are you sure you want to reset this integration configuration?'); export const confirmResetDetails = l10n.t('This will remove the stored credentials. You can reconfigure it later.'); + export const confirmDeleteTitle = l10n.t('Confirm Delete'); + export const confirmDeleteMessage = l10n.t('Are you sure you want to permanently delete this integration?'); + export const confirmDeleteDetails = l10n.t( + 'This will permanently remove the integration from your project. This action cannot be undone.' + ); export const configureTitle = l10n.t('Configure Integration: {0}'); export const cancel = l10n.t('Cancel'); export const save = l10n.t('Save'); + export const addNewIntegration = l10n.t('Add New Integration'); + export const database = l10n.t('Database'); + export const dataWarehousesLakes = l10n.t('Data Warehouses & Lakes'); + export const databases = l10n.t('Databases'); export const requiredField = l10n.t('*'); export const optionalField = l10n.t('(optional)'); export const unnamedIntegration = (id: string) => l10n.t('Unnamed Integration ({0})', id); + export const defaultName = (type: string) => l10n.t('My {0} integration', type); export const unsupportedIntegrationType = (type: string) => l10n.t('Unsupported integration type: {0}', type); // Integration type labels export const postgresTypeLabel = l10n.t('PostgreSQL'); - export const bigQueryTypeLabel = l10n.t('BigQuery'); + export const bigQueryTypeLabel = l10n.t('Google BigQuery'); export const snowflakeTypeLabel = l10n.t('Snowflake'); - export const alloyDBTypeLabel = l10n.t('AlloyDB'); + export const alloyDBTypeLabel = l10n.t('Google AlloyDB'); export const athenaTypeLabel = l10n.t('Amazon Athena'); export const clickHouseTypeLabel = l10n.t('ClickHouse'); export const databricksTypeLabel = l10n.t('Databricks'); @@ -849,8 +860,8 @@ export namespace Integrations { export const mySQLTypeLabel = l10n.t('MySQL'); export const duckDBTypeLabel = l10n.t('DuckDB'); export const redshiftTypeLabel = l10n.t('Amazon Redshift'); - export const spannerTypeLabel = l10n.t('Google Cloud Spanner'); - export const sqlServerTypeLabel = l10n.t('SQL Server'); + export const spannerTypeLabel = l10n.t('Google Spanner'); + export const sqlServerTypeLabel = l10n.t('Microsoft SQL Server'); export const trinoTypeLabel = l10n.t('Trino'); // PostgreSQL form strings diff --git a/src/webviews/webview-side/integrations/AlloyDBForm.tsx b/src/webviews/webview-side/integrations/AlloyDBForm.tsx index 02e772135..921c7a72b 100644 --- a/src/webviews/webview-side/integrations/AlloyDBForm.tsx +++ b/src/webviews/webview-side/integrations/AlloyDBForm.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; export interface IAlloyDBFormProps { integrationId: string; @@ -16,11 +17,9 @@ function createEmptyAlloyDBConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('alloydb')).trim(), type: 'alloydb', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/AthenaForm.tsx b/src/webviews/webview-side/integrations/AthenaForm.tsx index 13f10297d..9a3100792 100644 --- a/src/webviews/webview-side/integrations/AthenaForm.tsx +++ b/src/webviews/webview-side/integrations/AthenaForm.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyAthenaConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('athena')).trim(), type: 'athena', metadata: { access_key_id: '', diff --git a/src/webviews/webview-side/integrations/BigQueryForm.tsx b/src/webviews/webview-side/integrations/BigQueryForm.tsx index b535e71af..b4ae2b208 100644 --- a/src/webviews/webview-side/integrations/BigQueryForm.tsx +++ b/src/webviews/webview-side/integrations/BigQueryForm.tsx @@ -1,15 +1,14 @@ import * as React from 'react'; import { format, getLocString } from '../react-common/locReactSide'; import { BigQueryAuthMethods, DatabaseIntegrationConfig } from '@deepnote/database-integrations'; +import { getDefaultIntegrationName } from './integrationUtils'; type BigQueryConfig = Extract; function createEmptyBigQueryConfig(params: { id: string; name?: string }): BigQueryConfig { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('big-query')).trim(), type: 'big-query', metadata: { authMethod: BigQueryAuthMethods.ServiceAccount, diff --git a/src/webviews/webview-side/integrations/ClickHouseForm.tsx b/src/webviews/webview-side/integrations/ClickHouseForm.tsx index 587f06ebd..1c47ea5c0 100644 --- a/src/webviews/webview-side/integrations/ClickHouseForm.tsx +++ b/src/webviews/webview-side/integrations/ClickHouseForm.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; export interface IClickHouseFormProps { integrationId: string; @@ -16,11 +17,9 @@ function createEmptyClickHouseConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('clickhouse')).trim(), type: 'clickhouse', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/ConfigurationForm.tsx b/src/webviews/webview-side/integrations/ConfigurationForm.tsx index 62e4fc94b..dc79fd3d7 100644 --- a/src/webviews/webview-side/integrations/ConfigurationForm.tsx +++ b/src/webviews/webview-side/integrations/ConfigurationForm.tsx @@ -18,6 +18,7 @@ import { SpannerForm } from './SpannerForm'; import { SQLServerForm } from './SQLServerForm'; import { TrinoForm } from './TrinoForm'; import { ConfigurableDatabaseIntegrationConfig, ConfigurableDatabaseIntegrationType } from './types'; +import { integrationTypeLabels } from './integrationUtils'; export interface IConfigurationFormProps { integrationId: string; @@ -36,10 +37,8 @@ export const ConfigurationForm: React.FC = ({ onSave, onCancel }) => { - const title = getLocString('integrationsConfigureTitle', 'Configure Integration: {0}').replace( - '{0}', - integrationId - ); + const typeLabel = integrationTypeLabels[integrationType] || integrationType; + const title = getLocString('integrationsConfigureTitle', '{0} integration').replace('{0}', typeLabel); return (
diff --git a/src/webviews/webview-side/integrations/DatabricksForm.tsx b/src/webviews/webview-side/integrations/DatabricksForm.tsx index e590de086..f5e82d91d 100644 --- a/src/webviews/webview-side/integrations/DatabricksForm.tsx +++ b/src/webviews/webview-side/integrations/DatabricksForm.tsx @@ -1,17 +1,16 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyDatabricksConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('databricks')).trim(), type: 'databricks', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/DremioForm.tsx b/src/webviews/webview-side/integrations/DremioForm.tsx index 3483819af..4daa5f41c 100644 --- a/src/webviews/webview-side/integrations/DremioForm.tsx +++ b/src/webviews/webview-side/integrations/DremioForm.tsx @@ -1,17 +1,16 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyDremioConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('dremio')).trim(), type: 'dremio', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/IntegrationItem.tsx b/src/webviews/webview-side/integrations/IntegrationItem.tsx index 20332bddb..38a6a31f7 100644 --- a/src/webviews/webview-side/integrations/IntegrationItem.tsx +++ b/src/webviews/webview-side/integrations/IntegrationItem.tsx @@ -1,21 +1,23 @@ import * as React from 'react'; import { getLocString } from '../react-common/locReactSide'; import { ConfigurableDatabaseIntegrationType, IntegrationWithStatus } from './types'; +import { integrationTypeIcons } from './integrationUtils'; export interface IIntegrationItemProps { integration: IntegrationWithStatus; onConfigure: (integrationId: string) => void; + onReset: (integrationId: string) => void; onDelete: (integrationId: string) => void; } const getIntegrationTypeLabel = (type: ConfigurableDatabaseIntegrationType): string => { switch (type) { case 'alloydb': - return getLocString('integrationsAlloyDBTypeLabel', 'AlloyDB'); + return getLocString('integrationsAlloyDBTypeLabel', 'Google AlloyDB'); case 'athena': return getLocString('integrationsAthenaTypeLabel', 'Amazon Athena'); case 'big-query': - return getLocString('integrationsBigQueryTypeLabel', 'BigQuery'); + return getLocString('integrationsBigQueryTypeLabel', 'Google BigQuery'); case 'clickhouse': return getLocString('integrationsClickHouseTypeLabel', 'ClickHouse'); case 'databricks': @@ -39,9 +41,9 @@ const getIntegrationTypeLabel = (type: ConfigurableDatabaseIntegrationType): str case 'snowflake': return getLocString('integrationsSnowflakeTypeLabel', 'Snowflake'); case 'spanner': - return getLocString('integrationsSpannerTypeLabel', 'Google Cloud Spanner'); + return getLocString('integrationsSpannerTypeLabel', 'Google Spanner'); case 'sql-server': - return getLocString('integrationsSQLServerTypeLabel', 'SQL Server'); + return getLocString('integrationsSQLServerTypeLabel', 'Microsoft SQL Server'); case 'trino': return getLocString('integrationsTrinoTypeLabel', 'Trino'); default: @@ -49,7 +51,7 @@ const getIntegrationTypeLabel = (type: ConfigurableDatabaseIntegrationType): str } }; -export const IntegrationItem: React.FC = ({ integration, onConfigure, onDelete }) => { +export const IntegrationItem: React.FC = ({ integration, onConfigure, onReset, onDelete }) => { const statusClass = integration.status === 'connected' ? 'status-connected' : 'status-disconnected'; const statusText = integration.status === 'connected' @@ -65,24 +67,45 @@ export const IntegrationItem: React.FC = ({ integration, // 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; + // Get the type label and icon + const typeLabel = type ? getIntegrationTypeLabel(type) : undefined; + const typeIcon = type ? integrationTypeIcons[type] : undefined; return (
+ {typeIcon && ( +
+ {typeLabel +
+ )}
-
{displayName}
-
{statusText}
+
{name}
+
+ {typeLabel && {typeLabel}} + {typeLabel && } + {statusText} +
{integration.config && ( - )} + {integration.config && ( + + )}
); diff --git a/src/webviews/webview-side/integrations/IntegrationList.tsx b/src/webviews/webview-side/integrations/IntegrationList.tsx index d003ec78d..51ffcc8a3 100644 --- a/src/webviews/webview-side/integrations/IntegrationList.tsx +++ b/src/webviews/webview-side/integrations/IntegrationList.tsx @@ -6,10 +6,11 @@ import { IntegrationWithStatus } from './types'; export interface IIntegrationListProps { integrations: IntegrationWithStatus[]; onConfigure: (integrationId: string) => void; + onReset: (integrationId: string) => void; onDelete: (integrationId: string) => void; } -export const IntegrationList: React.FC = ({ integrations, onConfigure, onDelete }) => { +export const IntegrationList: React.FC = ({ integrations, onConfigure, onReset, onDelete }) => { if (integrations.length === 0) { return (

@@ -25,6 +26,7 @@ export const IntegrationList: React.FC = ({ integrations, key={integration.id} integration={integration} onConfigure={onConfigure} + onReset={onReset} onDelete={onDelete} /> ))} diff --git a/src/webviews/webview-side/integrations/IntegrationPanel.tsx b/src/webviews/webview-side/integrations/IntegrationPanel.tsx index e493f30ab..bdaf61c33 100644 --- a/src/webviews/webview-side/integrations/IntegrationPanel.tsx +++ b/src/webviews/webview-side/integrations/IntegrationPanel.tsx @@ -1,7 +1,10 @@ import * as React from 'react'; + +import { generateUuid } from '../../../platform/common/uuid'; import { IVsCodeApi } from '../react-common/postOffice'; import { getLocString, storeLocStrings } from '../react-common/locReactSide'; import { IntegrationList } from './IntegrationList'; +import { IntegrationTypeSelector } from './IntegrationTypeSelector'; import { ConfigurationForm } from './ConfigurationForm'; import { ConfigurableDatabaseIntegrationConfig, @@ -17,6 +20,7 @@ export interface IIntegrationPanelProps { export const IntegrationPanel: React.FC = ({ baseTheme, vscodeApi }) => { const [integrations, setIntegrations] = React.useState([]); + const [projectName, setProjectName] = React.useState(undefined); const [selectedIntegrationId, setSelectedIntegrationId] = React.useState(null); const [selectedConfig, setSelectedConfig] = React.useState(null); const [selectedIntegrationDefaultName, setSelectedIntegrationDefaultName] = React.useState( @@ -26,9 +30,11 @@ export const IntegrationPanel: React.FC = ({ baseTheme, ConfigurableDatabaseIntegrationType | undefined >(undefined); const [message, setMessage] = React.useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [confirmReset, setConfirmReset] = React.useState(null); const [confirmDelete, setConfirmDelete] = React.useState(null); const messageTimerRef = React.useRef(null); + const confirmResetTimerRef = React.useRef(null); const confirmDeleteTimerRef = React.useRef(null); // Cleanup timers on unmount @@ -38,6 +44,10 @@ export const IntegrationPanel: React.FC = ({ baseTheme, clearTimeout(messageTimerRef.current); messageTimerRef.current = null; } + if (confirmResetTimerRef.current) { + clearTimeout(confirmResetTimerRef.current); + confirmResetTimerRef.current = null; + } if (confirmDeleteTimerRef.current) { clearTimeout(confirmDeleteTimerRef.current); confirmDeleteTimerRef.current = null; @@ -57,6 +67,7 @@ export const IntegrationPanel: React.FC = ({ baseTheme, case 'update': setIntegrations(msg.integrations); + setProjectName(msg.projectName); break; case 'showForm': @@ -95,6 +106,41 @@ export const IntegrationPanel: React.FC = ({ baseTheme, }); }; + const handleReset = (integrationId: string) => { + // Clear any existing confirmReset timer before creating a new one + if (confirmResetTimerRef.current) { + clearTimeout(confirmResetTimerRef.current); + } + + setConfirmReset(integrationId); + }; + + const handleConfirmReset = () => { + if (confirmReset) { + // Clear the timer when user confirms + if (confirmResetTimerRef.current) { + clearTimeout(confirmResetTimerRef.current); + confirmResetTimerRef.current = null; + } + + vscodeApi.postMessage({ + type: 'reset', + integrationId: confirmReset + }); + setConfirmReset(null); + } + }; + + const handleCancelReset = () => { + // Clear the timer when user cancels + if (confirmResetTimerRef.current) { + clearTimeout(confirmResetTimerRef.current); + confirmResetTimerRef.current = null; + } + + setConfirmReset(null); + }; + const handleDelete = (integrationId: string) => { // Clear any existing confirmDelete timer before creating a new one if (confirmDeleteTimerRef.current) { @@ -143,15 +189,36 @@ export const IntegrationPanel: React.FC = ({ baseTheme, const handleCancel = () => { setSelectedIntegrationId(null); setSelectedConfig(null); + setSelectedIntegrationDefaultName(undefined); + setSelectedIntegrationType(undefined); + }; + + const handleSelectIntegrationType = (type: ConfigurableDatabaseIntegrationType) => { + // Generate a new UUID for the integration + const newId = generateUuid(); + + // Set up the form for creating a new integration + setSelectedIntegrationId(newId); + setSelectedConfig(null); + setSelectedIntegrationDefaultName(undefined); + setSelectedIntegrationType(type); }; return (

{getLocString('integrationsTitle', 'Deepnote Integrations')}

+ {projectName &&

{projectName}

} {message &&
{message.text}
} - + + + {selectedIntegrationId && selectedIntegrationType && ( = ({ baseTheme, /> )} - {confirmDelete && ( + {confirmReset && (
@@ -185,9 +252,41 @@ export const IntegrationPanel: React.FC = ({ baseTheme,

- + +
+
+
+ )} + + {confirmDelete && ( +
+
+
+

{getLocString('integrationsConfirmDeleteTitle', 'Confirm Delete')}

+
+
+

+ {getLocString( + 'integrationsConfirmDeleteMessage', + 'Are you sure you want to permanently delete this integration?' + )} +

+

+ {getLocString( + 'integrationsConfirmDeleteDetails', + 'This will permanently remove the integration from your project. This action cannot be undone.' + )} +

+
+
+ diff --git a/src/webviews/webview-side/integrations/IntegrationTypeSelector.tsx b/src/webviews/webview-side/integrations/IntegrationTypeSelector.tsx new file mode 100644 index 000000000..ed5526491 --- /dev/null +++ b/src/webviews/webview-side/integrations/IntegrationTypeSelector.tsx @@ -0,0 +1,155 @@ +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; +import { ConfigurableDatabaseIntegrationType } from './types'; +import { integrationTypeLabels, integrationTypeIcons } from './integrationUtils'; + +export interface IIntegrationTypeSelectorProps { + onSelectType: (type: ConfigurableDatabaseIntegrationType) => void; +} + +interface IntegrationTypeInfo { + type: ConfigurableDatabaseIntegrationType; + label: string; + icon: string; +} + +// Data Warehouses & Lakes +const WAREHOUSE_INTEGRATION_TYPES: IntegrationTypeInfo[] = [ + { + type: 'clickhouse', + label: integrationTypeLabels['clickhouse'], + icon: integrationTypeIcons['clickhouse'] + }, + { + type: 'redshift', + label: integrationTypeLabels['redshift'], + icon: integrationTypeIcons['redshift'] + }, + { + type: 'athena', + label: integrationTypeLabels['athena'], + icon: integrationTypeIcons['athena'] + }, + { + type: 'big-query', + label: integrationTypeLabels['big-query'], + icon: integrationTypeIcons['big-query'] + }, + { + type: 'snowflake', + label: integrationTypeLabels['snowflake'], + icon: integrationTypeIcons['snowflake'] + }, + { + type: 'databricks', + label: integrationTypeLabels['databricks'], + icon: integrationTypeIcons['databricks'] + }, + { + type: 'dremio', + label: integrationTypeLabels['dremio'], + icon: integrationTypeIcons['dremio'] + }, + { + type: 'trino', + label: integrationTypeLabels['trino'], + icon: integrationTypeIcons['trino'] + } +]; + +// Databases +const DATABASE_INTEGRATION_TYPES: IntegrationTypeInfo[] = [ + { + type: 'mongodb', + label: integrationTypeLabels['mongodb'], + icon: integrationTypeIcons['mongodb'] + }, + { + type: 'pgsql', + label: integrationTypeLabels['pgsql'], + icon: integrationTypeIcons['pgsql'] + }, + { + type: 'mysql', + label: integrationTypeLabels['mysql'], + icon: integrationTypeIcons['mysql'] + }, + { + type: 'mariadb', + label: integrationTypeLabels['mariadb'], + icon: integrationTypeIcons['mariadb'] + }, + { + type: 'sql-server', + label: integrationTypeLabels['sql-server'], + icon: integrationTypeIcons['sql-server'] + }, + { + type: 'alloydb', + label: integrationTypeLabels['alloydb'], + icon: integrationTypeIcons['alloydb'] + }, + { + type: 'spanner', + label: integrationTypeLabels['spanner'], + icon: integrationTypeIcons['spanner'] + }, + { + type: 'materialize', + label: integrationTypeLabels['materialize'], + icon: integrationTypeIcons['materialize'] + }, + { + type: 'mindsdb', + label: integrationTypeLabels['mindsdb'], + icon: integrationTypeIcons['mindsdb'] + } +]; + +export const IntegrationTypeSelector: React.FC = ({ onSelectType }) => { + return ( +
+

{getLocString('integrationsAddNewIntegration', 'Add New Integration')}

+ +
+

+ {getLocString('integrationsDataWarehousesLakes', 'Data Warehouses & Lakes')} +

+
+ {WAREHOUSE_INTEGRATION_TYPES.map((integrationInfo) => ( + + ))} +
+
+ +
+

{getLocString('integrationsDatabases', 'Databases')}

+
+ {DATABASE_INTEGRATION_TYPES.map((integrationInfo) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/webviews/webview-side/integrations/MariaDBForm.tsx b/src/webviews/webview-side/integrations/MariaDBForm.tsx index 0f3ded137..dad2e20e0 100644 --- a/src/webviews/webview-side/integrations/MariaDBForm.tsx +++ b/src/webviews/webview-side/integrations/MariaDBForm.tsx @@ -1,18 +1,17 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyMariaDBConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('mariadb')).trim(), type: 'mariadb', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/MaterializeForm.tsx b/src/webviews/webview-side/integrations/MaterializeForm.tsx index 031ccc5d0..647502fe7 100644 --- a/src/webviews/webview-side/integrations/MaterializeForm.tsx +++ b/src/webviews/webview-side/integrations/MaterializeForm.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; export interface IMaterializeFormProps { integrationId: string; @@ -16,11 +17,9 @@ function createEmptyMaterializeConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('materialize')).trim(), type: 'materialize', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/MindsDBForm.tsx b/src/webviews/webview-side/integrations/MindsDBForm.tsx index 928724305..3cf18259e 100644 --- a/src/webviews/webview-side/integrations/MindsDBForm.tsx +++ b/src/webviews/webview-side/integrations/MindsDBForm.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; export interface IMindsDBFormProps { integrationId: string; @@ -16,11 +17,9 @@ function createEmptyMindsDBConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('mindsdb')).trim(), type: 'mindsdb', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/MongoDBForm.tsx b/src/webviews/webview-side/integrations/MongoDBForm.tsx index e169aaa6d..269015430 100644 --- a/src/webviews/webview-side/integrations/MongoDBForm.tsx +++ b/src/webviews/webview-side/integrations/MongoDBForm.tsx @@ -1,18 +1,17 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyMongoDBConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('mongodb')).trim(), type: 'mongodb', metadata: { connection_string: '' diff --git a/src/webviews/webview-side/integrations/MySQLForm.tsx b/src/webviews/webview-side/integrations/MySQLForm.tsx index 37903bcba..ed22a16b8 100644 --- a/src/webviews/webview-side/integrations/MySQLForm.tsx +++ b/src/webviews/webview-side/integrations/MySQLForm.tsx @@ -1,18 +1,17 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyMySQLConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('mysql')).trim(), type: 'mysql', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/PostgresForm.tsx b/src/webviews/webview-side/integrations/PostgresForm.tsx index b6b12efab..4cc4a6d4a 100644 --- a/src/webviews/webview-side/integrations/PostgresForm.tsx +++ b/src/webviews/webview-side/integrations/PostgresForm.tsx @@ -1,18 +1,17 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptyPostgresConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('pgsql')).trim(), type: 'pgsql', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/RedshiftForm.tsx b/src/webviews/webview-side/integrations/RedshiftForm.tsx index 0cd07d691..6864e2730 100644 --- a/src/webviews/webview-side/integrations/RedshiftForm.tsx +++ b/src/webviews/webview-side/integrations/RedshiftForm.tsx @@ -1,17 +1,16 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; type RedshiftConfig = Extract; function createEmptyRedshiftConfig(params: { id: string; name?: string }): RedshiftConfig { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('redshift')).trim(), type: 'redshift', metadata: { authMethod: 'username-and-password', diff --git a/src/webviews/webview-side/integrations/SQLServerForm.tsx b/src/webviews/webview-side/integrations/SQLServerForm.tsx index ad3647072..a21fdac39 100644 --- a/src/webviews/webview-side/integrations/SQLServerForm.tsx +++ b/src/webviews/webview-side/integrations/SQLServerForm.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; +import { getDefaultIntegrationName } from './integrationUtils'; export interface ISQLServerFormProps { integrationId: string; @@ -15,11 +16,9 @@ function createEmptySQLServerConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('sql-server')).trim(), type: 'sql-server', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/SnowflakeForm.tsx b/src/webviews/webview-side/integrations/SnowflakeForm.tsx index 637cf381e..e13c32bf1 100644 --- a/src/webviews/webview-side/integrations/SnowflakeForm.tsx +++ b/src/webviews/webview-side/integrations/SnowflakeForm.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig, SnowflakeAuthMethods } from '@deepnote/database-integrations'; +import { getDefaultIntegrationName } from './integrationUtils'; type SnowflakeConfig = Extract; type SnowflakeAuthMethod = SnowflakeConfig['metadata']['authMethod']; @@ -12,11 +13,9 @@ function isSupportedSnowflakeAuthMethod(authMethod: SnowflakeAuthMethod): boolea } function createEmptySnowflakeConfig(params: { id: string; name?: string }): SnowflakeConfig { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('snowflake')).trim(), type: 'snowflake', metadata: { authMethod: SnowflakeAuthMethods.Password, diff --git a/src/webviews/webview-side/integrations/SpannerForm.tsx b/src/webviews/webview-side/integrations/SpannerForm.tsx index a130ca97b..1e1b75ddc 100644 --- a/src/webviews/webview-side/integrations/SpannerForm.tsx +++ b/src/webviews/webview-side/integrations/SpannerForm.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; +import { getDefaultIntegrationName } from './integrationUtils'; function createEmptySpannerConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('spanner')).trim(), type: 'spanner', metadata: { instance: '', diff --git a/src/webviews/webview-side/integrations/TrinoForm.tsx b/src/webviews/webview-side/integrations/TrinoForm.tsx index 61ef215e0..c93d100a9 100644 --- a/src/webviews/webview-side/integrations/TrinoForm.tsx +++ b/src/webviews/webview-side/integrations/TrinoForm.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { format, getLocString } from '../react-common/locReactSide'; +import { getLocString } from '../react-common/locReactSide'; import { DatabaseIntegrationConfig } from '@deepnote/database-integrations'; import { SshOptionsFields } from './SshOptionsFields'; import { CaCertificateFields } from './CaCertificateFields'; +import { getDefaultIntegrationName } from './integrationUtils'; export interface ITrinoFormProps { integrationId: string; @@ -16,11 +17,9 @@ function createEmptyTrinoConfig(params: { id: string; name?: string; }): Extract { - const unnamedIntegration = getLocString('integrationsUnnamedIntegration', 'Unnamed Integration ({0})'); - return { id: params.id, - name: (params.name || format(unnamedIntegration, params.id)).trim(), + name: (params.name || getDefaultIntegrationName('trino')).trim(), type: 'trino', metadata: { host: '', diff --git a/src/webviews/webview-side/integrations/icons/alloydb.svg b/src/webviews/webview-side/integrations/icons/alloydb.svg new file mode 100644 index 000000000..798748dbc --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/alloydb.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/athena.svg b/src/webviews/webview-side/integrations/icons/athena.svg new file mode 100644 index 000000000..2944f158d --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/athena.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/bigquery.svg b/src/webviews/webview-side/integrations/icons/bigquery.svg new file mode 100644 index 000000000..2f51ef50a --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/bigquery.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/webviews/webview-side/integrations/icons/clickhouse.svg b/src/webviews/webview-side/integrations/icons/clickhouse.svg new file mode 100644 index 000000000..c4f8f373e --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/clickhouse.svg @@ -0,0 +1,7 @@ + + + + diff --git a/src/webviews/webview-side/integrations/icons/databricks.svg b/src/webviews/webview-side/integrations/icons/databricks.svg new file mode 100644 index 000000000..1c160bcc0 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/databricks.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/webviews/webview-side/integrations/icons/dremio.svg b/src/webviews/webview-side/integrations/icons/dremio.svg new file mode 100644 index 000000000..41a5ae68b --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/dremio.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/mariadb.svg b/src/webviews/webview-side/integrations/icons/mariadb.svg new file mode 100644 index 000000000..5cb7528b7 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/mariadb.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/materialize.svg b/src/webviews/webview-side/integrations/icons/materialize.svg new file mode 100644 index 000000000..7a0c347a6 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/materialize.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/mindsdb.svg b/src/webviews/webview-side/integrations/icons/mindsdb.svg new file mode 100644 index 000000000..c61fa0cce --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/mindsdb.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/webviews/webview-side/integrations/icons/mongodb.svg b/src/webviews/webview-side/integrations/icons/mongodb.svg new file mode 100644 index 000000000..a2bf6cbe6 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/mongodb.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/webviews/webview-side/integrations/icons/mysql.svg b/src/webviews/webview-side/integrations/icons/mysql.svg new file mode 100644 index 000000000..557f87506 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/mysql.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/webviews/webview-side/integrations/icons/postgresql.svg b/src/webviews/webview-side/integrations/icons/postgresql.svg new file mode 100644 index 000000000..8acf80b33 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/postgresql.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/redshift.svg b/src/webviews/webview-side/integrations/icons/redshift.svg new file mode 100644 index 000000000..7b15f14a6 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/redshift.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/snowflake.svg b/src/webviews/webview-side/integrations/icons/snowflake.svg new file mode 100644 index 000000000..70f32b662 --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/snowflake.svg @@ -0,0 +1,7 @@ + + + diff --git a/src/webviews/webview-side/integrations/icons/spanner.svg b/src/webviews/webview-side/integrations/icons/spanner.svg new file mode 100644 index 000000000..ab955453a --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/spanner.svg @@ -0,0 +1,32 @@ + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/sql-server.svg b/src/webviews/webview-side/integrations/icons/sql-server.svg new file mode 100644 index 000000000..ba7b395bb --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/sql-server.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/icons/trino.svg b/src/webviews/webview-side/integrations/icons/trino.svg new file mode 100644 index 000000000..2dba4da6c --- /dev/null +++ b/src/webviews/webview-side/integrations/icons/trino.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/webviews/webview-side/integrations/index.tsx b/src/webviews/webview-side/integrations/index.tsx index 5c6e3166f..9c912116d 100644 --- a/src/webviews/webview-side/integrations/index.tsx +++ b/src/webviews/webview-side/integrations/index.tsx @@ -6,6 +6,8 @@ import { detectBaseTheme } from '../react-common/themeDetector'; import { IntegrationPanel } from './IntegrationPanel'; import '../common/index.css'; +import '../react-common/codicon/codicon.css'; +import './integrations.css'; // This special function talks to vscode from a web panel declare function acquireVsCodeApi(): IVsCodeApi; diff --git a/src/webviews/webview-side/integrations/integrationUtils.ts b/src/webviews/webview-side/integrations/integrationUtils.ts new file mode 100644 index 000000000..4797b41bf --- /dev/null +++ b/src/webviews/webview-side/integrations/integrationUtils.ts @@ -0,0 +1,75 @@ +import { getLocString } from '../react-common/locReactSide'; +import { ConfigurableDatabaseIntegrationType } from './types'; + +// Import integration logos +/* eslint-disable @typescript-eslint/no-require-imports */ +const postgresqlLogo: string = require('./icons/postgresql.svg'); +const mysqlLogo: string = require('./icons/mysql.svg'); +const mariadbLogo: string = require('./icons/mariadb.svg'); +const mongodbLogo: string = require('./icons/mongodb.svg'); +const sqlServerLogo: string = require('./icons/sql-server.svg'); +const bigqueryLogo: string = require('./icons/bigquery.svg'); +const snowflakeLogo: string = require('./icons/snowflake.svg'); +const alloydbLogo: string = require('./icons/alloydb.svg'); +const spannerLogo: string = require('./icons/spanner.svg'); +const materializeLogo: string = require('./icons/materialize.svg'); +const clickhouseLogo: string = require('./icons/clickhouse.svg'); +const athenaLogo: string = require('./icons/athena.svg'); +const redshiftLogo: string = require('./icons/redshift.svg'); +const databricksLogo: string = require('./icons/databricks.svg'); +const dremioLogo: string = require('./icons/dremio.svg'); +const mindsdbLogo: string = require('./icons/mindsdb.svg'); +const trinoLogo: string = require('./icons/trino.svg'); +/* eslint-enable @typescript-eslint/no-require-imports */ + +// Localized labels for integration types (duplicated from sqlCellStatusBarProvider.ts due to import restrictions) +export const integrationTypeLabels: Record = { + alloydb: 'Google AlloyDB', + athena: 'Amazon Athena', + 'big-query': 'Google BigQuery', + clickhouse: 'ClickHouse', + databricks: 'Databricks', + dremio: 'Dremio', + mariadb: 'MariaDB', + materialize: 'Materialize', + mindsdb: 'MindsDB', + mongodb: 'MongoDB', + mysql: 'MySQL', + pgsql: 'PostgreSQL', + redshift: 'Amazon Redshift', + snowflake: 'Snowflake', + spanner: 'Google Spanner', + 'sql-server': 'Microsoft SQL Server', + trino: 'Trino' +}; + +// Icon mapping for integration types +export const integrationTypeIcons: Record = { + alloydb: alloydbLogo, + athena: athenaLogo, + 'big-query': bigqueryLogo, + clickhouse: clickhouseLogo, + databricks: databricksLogo, + dremio: dremioLogo, + mariadb: mariadbLogo, + materialize: materializeLogo, + mindsdb: mindsdbLogo, + mongodb: mongodbLogo, + mysql: mysqlLogo, + pgsql: postgresqlLogo, + redshift: redshiftLogo, + snowflake: snowflakeLogo, + spanner: spannerLogo, + 'sql-server': sqlServerLogo, + trino: trinoLogo +}; + +/** + * Get the default name for a new integration + * @param type The integration type + * @returns The default name in the format "My {type} integration" + */ +export function getDefaultIntegrationName(type: ConfigurableDatabaseIntegrationType): string { + const typeLabel = integrationTypeLabels[type] || type; + return getLocString('integrationsDefaultName', 'My {0} integration').replace('{0}', typeLabel); +} diff --git a/src/webviews/webview-side/integrations/integrations.css b/src/webviews/webview-side/integrations/integrations.css index a550f6054..2ade190f9 100644 --- a/src/webviews/webview-side/integrations/integrations.css +++ b/src/webviews/webview-side/integrations/integrations.css @@ -1,5 +1,7 @@ .integration-panel { padding: 20px; + max-width: 640px; + margin: 0 auto; color: var(--vscode-foreground); font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); @@ -7,11 +9,18 @@ .integration-panel h1 { margin-top: 0; - margin-bottom: 20px; + margin-bottom: 8px; font-size: 1.5em; font-weight: 600; } +.project-name { + margin-top: 0; + margin-bottom: 20px; + color: var(--vscode-descriptionForeground); + font-size: 0.95em; +} + /* Message styles */ .message { padding: 12px 16px; @@ -49,14 +58,31 @@ display: flex; justify-content: space-between; align-items: center; + gap: 16px; padding: 16px; background-color: var(--vscode-editor-background); border: 1px solid var(--vscode-panel-border); border-radius: 4px; } +.integration-item-icon { + width: 32px; + height: 32px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.integration-item-icon img { + width: 100%; + height: 100%; + object-fit: contain; +} + .integration-info { flex: 1; + min-width: 0; } .integration-name { @@ -64,8 +90,23 @@ margin-bottom: 4px; } -.integration-status { +.integration-meta { + display: flex; + align-items: center; font-size: 0.9em; + color: var(--vscode-descriptionForeground); +} + +.integration-type { + color: var(--vscode-foreground); +} + +.integration-meta-separator { + margin: 0 4px; +} + +.integration-status { + font-size: inherit; } .status-connected { @@ -79,6 +120,7 @@ .integration-actions { display: flex; gap: 8px; + flex-shrink: 0; } /* Buttons */ @@ -116,6 +158,22 @@ button.primary { color: var(--vscode-button-foreground); } +button.icon-button { + padding: 6px 8px; + min-width: auto; + background-color: transparent; + border: none; + color: var(--vscode-foreground); +} + +button.icon-button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +button.icon-button .codicon { + font-size: 16px; +} + /* Configuration form overlay */ .configuration-form-overlay { position: fixed; @@ -263,3 +321,84 @@ form { .form-actions button { flex: 1; } + +/* Integration type selector */ +.integration-type-selector { + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid var(--vscode-panel-border); +} + +.integration-type-selector h2 { + margin-top: 0; + margin-bottom: 24px; + font-size: 1.2em; + font-weight: 600; +} + +.integration-type-section { + margin-bottom: 32px; +} + +.integration-type-section:last-child { + margin-bottom: 0; +} + +.integration-type-section-title { + margin-top: 0; + margin-bottom: 12px; + font-size: 0.95em; + font-weight: 600; + color: var(--vscode-descriptionForeground); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.integration-type-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; +} + +.integration-type-card { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + padding: 12px 16px; + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + cursor: pointer; + text-align: left; + transition: + background-color 0.2s, + border-color 0.2s; +} + +.integration-type-card:hover { + background-color: var(--vscode-toolbar-hoverBackground); + border-color: var(--vscode-focusBorder); +} + +.integration-type-icon { + width: 32px; + height: 32px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.integration-type-icon img { + width: 32px; + height: 32px; + object-fit: contain; +} + +.integration-type-label { + font-weight: 400; + font-size: 13px; + flex: 1; + color: var(--vscode-foreground); +} diff --git a/src/webviews/webview-side/integrations/types.ts b/src/webviews/webview-side/integrations/types.ts index 2312282ee..145c04109 100644 --- a/src/webviews/webview-side/integrations/types.ts +++ b/src/webviews/webview-side/integrations/types.ts @@ -23,6 +23,7 @@ export interface IVsCodeMessage { export interface UpdateMessage { type: 'update'; integrations: IntegrationWithStatus[]; + projectName?: string; } export interface ShowFormMessage {