Skip to content

Commit 2a2b9d1

Browse files
committed
Add Snowflake SQL integration support
- Add Snowflake integration type with username+password and key-pair auth methods - Create SnowflakeForm UI component with auth method switcher - Add connection string generation for both auth methods - Add all localization strings for Snowflake form fields - Update integration type mappings and labels
1 parent e2fa9b5 commit 2a2b9d1

File tree

10 files changed

+539
-10
lines changed

10 files changed

+539
-10
lines changed

src/messageTypes.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export type LocalizedMessages = {
182182
// Integration type labels
183183
integrationsPostgresTypeLabel: string;
184184
integrationsBigQueryTypeLabel: string;
185+
integrationsSnowflakeTypeLabel: string;
185186
// PostgreSQL form strings
186187
integrationsPostgresNameLabel: string;
187188
integrationsPostgresNamePlaceholder: string;
@@ -204,6 +205,34 @@ export type LocalizedMessages = {
204205
integrationsBigQueryCredentialsLabel: string;
205206
integrationsBigQueryCredentialsPlaceholder: string;
206207
integrationsBigQueryCredentialsRequired: string;
208+
// Snowflake form strings
209+
integrationsSnowflakeNameLabel: string;
210+
integrationsSnowflakeNamePlaceholder: string;
211+
integrationsSnowflakeAccountLabel: string;
212+
integrationsSnowflakeAccountPlaceholder: string;
213+
integrationsSnowflakeAuthMethodLabel: string;
214+
integrationsSnowflakeAuthMethodSubLabel: string;
215+
integrationsSnowflakeAuthMethodUsernamePassword: string;
216+
integrationsSnowflakeAuthMethodKeyPair: string;
217+
integrationsSnowflakeUsernameLabel: string;
218+
integrationsSnowflakeUsernamePlaceholder: string;
219+
integrationsSnowflakePasswordLabel: string;
220+
integrationsSnowflakePasswordPlaceholder: string;
221+
integrationsSnowflakeServiceAccountUsernameLabel: string;
222+
integrationsSnowflakeServiceAccountUsernameHelp: string;
223+
integrationsSnowflakeServiceAccountUsernamePlaceholder: string;
224+
integrationsSnowflakePrivateKeyLabel: string;
225+
integrationsSnowflakePrivateKeyHelp: string;
226+
integrationsSnowflakePrivateKeyPlaceholder: string;
227+
integrationsSnowflakePrivateKeyPassphraseLabel: string;
228+
integrationsSnowflakePrivateKeyPassphraseHelp: string;
229+
integrationsSnowflakePrivateKeyPassphrasePlaceholder: string;
230+
integrationsSnowflakeDatabaseLabel: string;
231+
integrationsSnowflakeDatabasePlaceholder: string;
232+
integrationsSnowflakeRoleLabel: string;
233+
integrationsSnowflakeRolePlaceholder: string;
234+
integrationsSnowflakeWarehouseLabel: string;
235+
integrationsSnowflakeWarehousePlaceholder: string;
207236
// Common form strings
208237
integrationsRequiredField: string;
209238
integrationsOptionalField: string;

src/notebooks/deepnote/integrations/integrationWebview.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
131131
integrationsConfigureTitle: localize.Integrations.configureTitle,
132132
integrationsPostgresTypeLabel: localize.Integrations.postgresTypeLabel,
133133
integrationsBigQueryTypeLabel: localize.Integrations.bigQueryTypeLabel,
134+
integrationsSnowflakeTypeLabel: localize.Integrations.snowflakeTypeLabel,
134135
integrationsCancel: localize.Integrations.cancel,
135136
integrationsSave: localize.Integrations.save,
136137
integrationsRequiredField: localize.Integrations.requiredField,
@@ -154,7 +155,37 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
154155
integrationsBigQueryProjectIdPlaceholder: localize.Integrations.bigQueryProjectIdPlaceholder,
155156
integrationsBigQueryCredentialsLabel: localize.Integrations.bigQueryCredentialsLabel,
156157
integrationsBigQueryCredentialsPlaceholder: localize.Integrations.bigQueryCredentialsPlaceholder,
157-
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired
158+
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired,
159+
integrationsSnowflakeNameLabel: localize.Integrations.snowflakeNameLabel,
160+
integrationsSnowflakeNamePlaceholder: localize.Integrations.snowflakeNamePlaceholder,
161+
integrationsSnowflakeAccountLabel: localize.Integrations.snowflakeAccountLabel,
162+
integrationsSnowflakeAccountPlaceholder: localize.Integrations.snowflakeAccountPlaceholder,
163+
integrationsSnowflakeAuthMethodLabel: localize.Integrations.snowflakeAuthMethodLabel,
164+
integrationsSnowflakeAuthMethodSubLabel: localize.Integrations.snowflakeAuthMethodSubLabel,
165+
integrationsSnowflakeAuthMethodUsernamePassword: localize.Integrations.snowflakeAuthMethodUsernamePassword,
166+
integrationsSnowflakeAuthMethodKeyPair: localize.Integrations.snowflakeAuthMethodKeyPair,
167+
integrationsSnowflakeUsernameLabel: localize.Integrations.snowflakeUsernameLabel,
168+
integrationsSnowflakeUsernamePlaceholder: localize.Integrations.snowflakeUsernamePlaceholder,
169+
integrationsSnowflakePasswordLabel: localize.Integrations.snowflakePasswordLabel,
170+
integrationsSnowflakePasswordPlaceholder: localize.Integrations.snowflakePasswordPlaceholder,
171+
integrationsSnowflakeServiceAccountUsernameLabel:
172+
localize.Integrations.snowflakeServiceAccountUsernameLabel,
173+
integrationsSnowflakeServiceAccountUsernameHelp: localize.Integrations.snowflakeServiceAccountUsernameHelp,
174+
integrationsSnowflakeServiceAccountUsernamePlaceholder:
175+
localize.Integrations.snowflakeServiceAccountUsernamePlaceholder,
176+
integrationsSnowflakePrivateKeyLabel: localize.Integrations.snowflakePrivateKeyLabel,
177+
integrationsSnowflakePrivateKeyHelp: localize.Integrations.snowflakePrivateKeyHelp,
178+
integrationsSnowflakePrivateKeyPlaceholder: localize.Integrations.snowflakePrivateKeyPlaceholder,
179+
integrationsSnowflakePrivateKeyPassphraseLabel: localize.Integrations.snowflakePrivateKeyPassphraseLabel,
180+
integrationsSnowflakePrivateKeyPassphraseHelp: localize.Integrations.snowflakePrivateKeyPassphraseHelp,
181+
integrationsSnowflakePrivateKeyPassphrasePlaceholder:
182+
localize.Integrations.snowflakePrivateKeyPassphrasePlaceholder,
183+
integrationsSnowflakeDatabaseLabel: localize.Integrations.snowflakeDatabaseLabel,
184+
integrationsSnowflakeDatabasePlaceholder: localize.Integrations.snowflakeDatabasePlaceholder,
185+
integrationsSnowflakeRoleLabel: localize.Integrations.snowflakeRoleLabel,
186+
integrationsSnowflakeRolePlaceholder: localize.Integrations.snowflakeRolePlaceholder,
187+
integrationsSnowflakeWarehouseLabel: localize.Integrations.snowflakeWarehouseLabel,
188+
integrationsSnowflakeWarehousePlaceholder: localize.Integrations.snowflakeWarehousePlaceholder
158189
};
159190

160191
await this.currentPanel.webview.postMessage({

src/notebooks/deepnote/sqlCellStatusBarProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
437437
return l10n.t('PostgreSQL');
438438
case IntegrationType.BigQuery:
439439
return l10n.t('BigQuery');
440+
case IntegrationType.Snowflake:
441+
return l10n.t('Snowflake');
440442
default:
441443
return String(type);
442444
}

src/platform/common/utils/localize.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,7 @@ export namespace Integrations {
834834
// Integration type labels
835835
export const postgresTypeLabel = l10n.t('PostgreSQL');
836836
export const bigQueryTypeLabel = l10n.t('BigQuery');
837+
export const snowflakeTypeLabel = l10n.t('Snowflake');
837838

838839
// PostgreSQL form strings
839840
export const postgresNameLabel = l10n.t('Name (optional)');
@@ -861,6 +862,42 @@ export namespace Integrations {
861862
export const bigQueryCredentialsRequired = l10n.t('Credentials are required');
862863
export const bigQueryInvalidJson = (message: string) => l10n.t('Invalid JSON: {0}', message);
863864
export const bigQueryUnnamedIntegration = (id: string) => l10n.t('Unnamed BigQuery Integration ({0})', id);
865+
866+
// Snowflake form strings
867+
export const snowflakeNameLabel = l10n.t('Integration name');
868+
export const snowflakeNamePlaceholder = l10n.t('[Demo] Snowflake');
869+
export const snowflakeAccountLabel = l10n.t('Account name');
870+
export const snowflakeAccountPlaceholder = l10n.t('ptb34938.us-east-1');
871+
export const snowflakeAuthMethodLabel = l10n.t('Authentication');
872+
export const snowflakeAuthMethodSubLabel = l10n.t('Method');
873+
export const snowflakeAuthMethodUsernamePassword = l10n.t('Username & password');
874+
export const snowflakeAuthMethodKeyPair = l10n.t('Key-pair (service account)');
875+
export const snowflakeUsernameLabel = l10n.t('Username');
876+
export const snowflakeUsernamePlaceholder = l10n.t('WEBSITE_ANALYTICS_USER');
877+
export const snowflakePasswordLabel = l10n.t('Password');
878+
export const snowflakePasswordPlaceholder = l10n.t('••••••••••••••');
879+
export const snowflakeServiceAccountUsernameLabel = l10n.t('Service Account Username');
880+
export const snowflakeServiceAccountUsernameHelp = l10n.t(
881+
'The username of the service account that will be used to connect to Snowflake'
882+
);
883+
export const snowflakeServiceAccountUsernamePlaceholder = l10n.t('WEBSITE_ANALYTICS_USER');
884+
export const snowflakePrivateKeyLabel = l10n.t('Private Key');
885+
export const snowflakePrivateKeyHelp = l10n.t(
886+
'The private key in PEM format. Make sure to include the entire key, including BEGIN and END markers.'
887+
);
888+
export const snowflakePrivateKeyPlaceholder = l10n.t("Begins with '-----BEGIN PRIVATE KEY-----'");
889+
export const snowflakePrivateKeyPassphraseLabel = l10n.t('Private Key Passphrase (optional)');
890+
export const snowflakePrivateKeyPassphraseHelp = l10n.t(
891+
'If the private key is encrypted, provide the passphrase to decrypt it'
892+
);
893+
export const snowflakePrivateKeyPassphrasePlaceholder = l10n.t('Private key passphrase (optional)');
894+
export const snowflakeDatabaseLabel = l10n.t('Database (optional)');
895+
export const snowflakeDatabasePlaceholder = l10n.t('DEEPNOTE');
896+
export const snowflakeRoleLabel = l10n.t('Role (optional)');
897+
export const snowflakeRolePlaceholder = l10n.t('');
898+
export const snowflakeWarehouseLabel = l10n.t('Warehouse (optional)');
899+
export const snowflakeWarehousePlaceholder = l10n.t('');
900+
export const snowflakeUnnamedIntegration = (id: string) => l10n.t('Unnamed Snowflake Integration ({0})', id);
864901
}
865902

866903
export namespace Deprecated {

src/platform/notebooks/deepnote/integrationTypes.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ export const DATAFRAME_SQL_INTEGRATION_ID = 'deepnote-dataframe-sql';
99
*/
1010
export enum IntegrationType {
1111
Postgres = 'postgres',
12-
BigQuery = 'bigquery'
12+
BigQuery = 'bigquery',
13+
Snowflake = 'snowflake'
1314
}
1415

1516
/**
1617
* Map our IntegrationType enum to Deepnote integration type strings
1718
*/
1819
export const INTEGRATION_TYPE_TO_DEEPNOTE = {
1920
[IntegrationType.Postgres]: 'pgsql',
20-
[IntegrationType.BigQuery]: 'big-query'
21+
[IntegrationType.BigQuery]: 'big-query',
22+
[IntegrationType.Snowflake]: 'snowflake'
2123
} as const satisfies { [type in IntegrationType]: string };
2224

2325
export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typeof INTEGRATION_TYPE_TO_DEEPNOTE];
@@ -27,7 +29,8 @@ export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typ
2729
*/
2830
export const DEEPNOTE_TO_INTEGRATION_TYPE: Record<RawIntegrationType, IntegrationType> = {
2931
pgsql: IntegrationType.Postgres,
30-
'big-query': IntegrationType.BigQuery
32+
'big-query': IntegrationType.BigQuery,
33+
snowflake: IntegrationType.Snowflake
3134
};
3235

3336
/**
@@ -61,10 +64,37 @@ export interface BigQueryIntegrationConfig extends BaseIntegrationConfig {
6164
credentials: string; // JSON string of service account credentials
6265
}
6366

67+
/**
68+
* Snowflake authentication method
69+
*/
70+
export enum SnowflakeAuthMethod {
71+
UsernamePassword = 'username_password',
72+
KeyPair = 'key_pair'
73+
}
74+
75+
/**
76+
* Snowflake integration configuration
77+
*/
78+
export interface SnowflakeIntegrationConfig extends BaseIntegrationConfig {
79+
type: IntegrationType.Snowflake;
80+
account: string;
81+
authMethod: SnowflakeAuthMethod;
82+
username: string;
83+
// For username+password auth
84+
password?: string;
85+
// For key-pair auth
86+
privateKey?: string;
87+
privateKeyPassphrase?: string;
88+
// Optional fields
89+
database?: string;
90+
warehouse?: string;
91+
role?: string;
92+
}
93+
6494
/**
6595
* Union type of all integration configurations
6696
*/
67-
export type IntegrationConfig = PostgresIntegrationConfig | BigQueryIntegrationConfig;
97+
export type IntegrationConfig = PostgresIntegrationConfig | BigQueryIntegrationConfig | SnowflakeIntegrationConfig;
6898

6999
/**
70100
* Integration connection status

src/platform/notebooks/deepnote/sqlIntegrationEnvironmentVariablesProvider.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { EnvironmentVariables } from '../../common/variables/types';
66
import { BaseError } from '../../errors/types';
77
import { logger } from '../../logging';
88
import { IIntegrationStorage, ISqlIntegrationEnvVarsProvider } from './types';
9-
import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationConfig, IntegrationType } from './integrationTypes';
9+
import {
10+
DATAFRAME_SQL_INTEGRATION_ID,
11+
IntegrationConfig,
12+
IntegrationType,
13+
SnowflakeAuthMethod
14+
} from './integrationTypes';
1015

1116
/**
1217
* Error thrown when an unsupported integration type is encountered.
@@ -73,6 +78,68 @@ function convertIntegrationConfigToJson(config: IntegrationConfig): string {
7378
});
7479
}
7580

81+
case IntegrationType.Snowflake: {
82+
// Build Snowflake connection URL
83+
// Format depends on auth method:
84+
// Username+password: snowflake://{username}:{password}@{account}/{database}?warehouse={warehouse}&role={role}&application=YourApp
85+
// Key-pair: snowflake://{username}@{account}/{database}?warehouse={warehouse}&role={role}&authenticator=snowflake_jwt&application=YourApp
86+
const encodedUsername = encodeURIComponent(config.username);
87+
const encodedAccount = encodeURIComponent(config.account);
88+
89+
let url: string;
90+
const params: Record<string, unknown> = {};
91+
92+
if (config.authMethod === SnowflakeAuthMethod.UsernamePassword) {
93+
// Username+password authentication
94+
const encodedPassword = encodeURIComponent(config.password || '');
95+
const database = config.database ? `/${encodeURIComponent(config.database)}` : '';
96+
url = `snowflake://${encodedUsername}:${encodedPassword}@${encodedAccount}${database}`;
97+
98+
const queryParams: string[] = [];
99+
if (config.warehouse) {
100+
queryParams.push(`warehouse=${encodeURIComponent(config.warehouse)}`);
101+
}
102+
if (config.role) {
103+
queryParams.push(`role=${encodeURIComponent(config.role)}`);
104+
}
105+
queryParams.push('application=Deepnote');
106+
107+
if (queryParams.length > 0) {
108+
url += `?${queryParams.join('&')}`;
109+
}
110+
} else {
111+
// Key-pair authentication
112+
const database = config.database ? `/${encodeURIComponent(config.database)}` : '';
113+
url = `snowflake://${encodedUsername}@${encodedAccount}${database}`;
114+
115+
const queryParams: string[] = [];
116+
if (config.warehouse) {
117+
queryParams.push(`warehouse=${encodeURIComponent(config.warehouse)}`);
118+
}
119+
if (config.role) {
120+
queryParams.push(`role=${encodeURIComponent(config.role)}`);
121+
}
122+
queryParams.push('authenticator=snowflake_jwt');
123+
queryParams.push('application=Deepnote');
124+
125+
if (queryParams.length > 0) {
126+
url += `?${queryParams.join('&')}`;
127+
}
128+
129+
// For key-pair auth, pass the private key and passphrase as params
130+
params.private_key = config.privateKey || '';
131+
if (config.privateKeyPassphrase) {
132+
params.private_key_passphrase = config.privateKeyPassphrase;
133+
}
134+
}
135+
136+
return JSON.stringify({
137+
url: url,
138+
params: params,
139+
param_style: 'format'
140+
});
141+
}
142+
76143
default:
77144
throw new UnsupportedIntegrationError((config as IntegrationConfig).type);
78145
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { getLocString } from '../react-common/locReactSide';
33
import { PostgresForm } from './PostgresForm';
44
import { BigQueryForm } from './BigQueryForm';
5+
import { SnowflakeForm } from './SnowflakeForm';
56
import { IntegrationConfig, IntegrationType } from './types';
67

78
export interface IConfigurationFormProps {
@@ -22,7 +23,7 @@ export const ConfigurationForm: React.FC<IConfigurationFormProps> = ({
2223
onCancel
2324
}) => {
2425
// Determine integration type from existing config, integration metadata from project, or ID
25-
const getIntegrationType = (): 'postgres' | 'bigquery' => {
26+
const getIntegrationType = (): 'postgres' | 'bigquery' | 'snowflake' => {
2627
if (existingConfig) {
2728
return existingConfig.type;
2829
}
@@ -37,6 +38,9 @@ export const ConfigurationForm: React.FC<IConfigurationFormProps> = ({
3738
if (integrationId.includes('bigquery')) {
3839
return 'bigquery';
3940
}
41+
if (integrationId.includes('snowflake')) {
42+
return 'snowflake';
43+
}
4044
// Default to postgres
4145
return 'postgres';
4246
};
@@ -67,14 +71,22 @@ export const ConfigurationForm: React.FC<IConfigurationFormProps> = ({
6771
onSave={onSave}
6872
onCancel={onCancel}
6973
/>
70-
) : (
74+
) : selectedIntegrationType === 'bigquery' ? (
7175
<BigQueryForm
7276
integrationId={integrationId}
7377
existingConfig={existingConfig?.type === 'bigquery' ? existingConfig : null}
7478
integrationName={integrationName}
7579
onSave={onSave}
7680
onCancel={onCancel}
7781
/>
82+
) : (
83+
<SnowflakeForm
84+
integrationId={integrationId}
85+
existingConfig={existingConfig?.type === 'snowflake' ? existingConfig : null}
86+
integrationName={integrationName}
87+
onSave={onSave}
88+
onCancel={onCancel}
89+
/>
7890
)}
7991
</div>
8092
</div>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const getIntegrationTypeLabel = (type: IntegrationType): string => {
1414
return getLocString('integrationsPostgresTypeLabel', 'PostgreSQL');
1515
case 'bigquery':
1616
return getLocString('integrationsBigQueryTypeLabel', 'BigQuery');
17+
case 'snowflake':
18+
return getLocString('integrationsSnowflakeTypeLabel', 'Snowflake');
1719
default:
1820
return type;
1921
}

0 commit comments

Comments
 (0)