diff --git a/web/package-lock.json b/web/package-lock.json index 59a29e94d..97c6ff80d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -79,6 +79,7 @@ "ts-jest": "^29.2.6", "use-context-selector": "^2.0.0", "utility-types": "^3.11.0", + "valibot": "^1.0.0", "virtua": "^0.40.3", "zustand": "^5.0.3" }, @@ -21322,6 +21323,20 @@ "node": ">=10.12.0" } }, + "node_modules/valibot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0.tgz", + "integrity": "sha512-1Hc0ihzWxBar6NGeZv7fPLY0QuxFMyxwYR2sF1Blu7Wq7EnremwY2W02tit2ij2VJT8HcSkHAQqmFfl77f73Yw==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/web/package.json b/web/package.json index 1aeeb5628..d64b9f361 100644 --- a/web/package.json +++ b/web/package.json @@ -87,6 +87,7 @@ "ts-jest": "^29.2.6", "use-context-selector": "^2.0.0", "utility-types": "^3.11.0", + "valibot": "^1.0.0", "virtua": "^0.40.3", "zustand": "^5.0.3" }, diff --git a/web/src/api/buster_rest/data_source/requests.ts b/web/src/api/buster_rest/data_source/requests.ts index 2d43b0995..635c9f0a8 100644 --- a/web/src/api/buster_rest/data_source/requests.ts +++ b/web/src/api/buster_rest/data_source/requests.ts @@ -1,6 +1,19 @@ -import type { DataSource, DataSourceListItem } from '@/api/asset_interfaces/datasources'; +import type { + BigQueryCredentials, + DatabricksCredentials, + DataSource, + DataSourceListItem, + MySQLCredentials, + PostgresCredentials, + SnowflakeCredentials, + SQLServerCredentials +} from '@/api/asset_interfaces/datasources'; import mainApi from '../instances'; -import { DatasourceUpdateParams } from '@/api/request_interfaces/datasources'; +import { + DatasourceUpdateParams, + RedshiftCreateCredentials +} from '@/api/request_interfaces/datasources'; +import { MySQLCreateParams } from './types'; export const listDatasources = async () => { return await mainApi.get('/data_sources').then((res) => res.data); @@ -14,16 +27,7 @@ export const deleteDatasource = async (id: string) => { return await mainApi.delete(`/data_sources/${id}`).then((res) => res.data); }; -export const createPostgresDataSource = async (params: { - name: string; - type: 'postgres' | 'supabase'; - host: string; - port: number; - username: string; - password: string; - default_database: string; //postgres - default_schema: string; //public -}) => { +export const createPostgresDataSource = async (params: PostgresCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; @@ -34,15 +38,7 @@ export const updatePostgresDataSource = async ({ return mainApi.put(`/data_sources/${id}`, params).then((res) => res.data); }; -export const createMySQLDataSource = async (params: { - name: string; - type: 'mysql' | 'mariadb'; - host: string; - port: number; - username: string; - password: string; - default_database: string; -}) => { +export const createMySQLDataSource = async (params: MySQLCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; @@ -53,16 +49,7 @@ export const updateMySQLDataSource = async ({ return mainApi.put(`/data_sources/${id}`, params).then((res) => res.data); }; -export const createRedshiftDataSource = async (params: { - name: string; - type: 'redshift'; - host: string; - port: number; - username: string; - password: string; - default_database: string; - default_schema: string; -}) => { +export const createRedshiftDataSource = async (params: RedshiftCreateCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; @@ -73,13 +60,7 @@ export const updateRedshiftDataSource = async ({ return mainApi.put(`/data_sources/${id}`, params).then((res) => res.data); }; -export const createBigQueryDataSource = async (params: { - name: string; - type: 'bigquery'; - service_role_key: string; - default_project_id: string; - default_dataset_id: string; -}) => { +export const createBigQueryDataSource = async (params: BigQueryCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; @@ -90,17 +71,7 @@ export const updateBigQueryDataSource = async ({ return mainApi.put(`/data_sources/${id}`, params).then((res) => res.data); }; -export const createSnowflakeDataSource = async (params: { - name: string; - type: 'snowflake'; - account_id: string; - warehouse_id: string; - username: string; - password: string; - role: string | null; - default_database: string; - default_schema: string; -}) => { +export const createSnowflakeDataSource = async (params: SnowflakeCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; @@ -111,15 +82,7 @@ export const updateSnowflakeDataSource = async ({ return mainApi.put(`/data_sources/${id}`, params).then((res) => res.data); }; -export const createDatabricksDataSource = async (params: { - name: string; - type: 'databricks'; - host: string; - api_key: string; - warehouse_id: string; - default_catalog: string; - default_schema: string; -}) => { +export const createDatabricksDataSource = async (params: DatabricksCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; @@ -130,16 +93,7 @@ export const updateDatabricksDataSource = async ({ return mainApi.put(`/data_sources/${id}`, params).then((res) => res.data); }; -export const createSQLServerDataSource = async (params: { - name: string; - type: 'sqlserver'; - host: string; - port: number; - username: string; - password: string; - default_database: string; - default_schema: string; -}) => { +export const createSQLServerDataSource = async (params: SQLServerCredentials) => { return mainApi.post('/data_sources', params).then((res) => res.data); }; diff --git a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/DatasourceFormContent.tsx b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/DatasourceFormContent.tsx index accc6d627..36b0f108a 100644 --- a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/DatasourceFormContent.tsx +++ b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/DatasourceFormContent.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { DataSource, DataSourceTypes } from '@/api/asset_interfaces'; +import { DatabaseNames, type DataSource, type DataSourceTypes } from '@/api/asset_interfaces'; import React from 'react'; import { PostgresForm } from './PostgresForm'; import { MySqlForm } from './MySqlForm'; @@ -9,6 +9,8 @@ import { SnowflakeForm } from './SnowflakeForm'; import { RedshiftForm } from './RedshiftForm'; import { DataBricksForm } from './DataBricksForm'; import { SqlServerForm } from './SqlServerForm'; +import { AppDataSourceIcon } from '@/components/ui/icons/AppDataSourceIcons'; +import { Text } from '@/components/ui/typography'; const FormRecord: Record< DataSourceTypes, @@ -32,14 +34,19 @@ const FormRecord: Record< export const DataSourceFormContent: React.FC<{ dataSource?: DataSource; type: DataSourceTypes; -}> = ({ dataSource, type }) => { +}> = React.memo(({ dataSource, type }) => { const SelectedForm = FormRecord[type]; + const DatabaseName = DatabaseNames[type]; return ( -
-
Datasource credentials
+
+
+ {`${DatabaseName} credentials`} +
{SelectedForm && }
); -}; +}); + +DataSourceFormContent.displayName = 'DataSourceFormContent'; diff --git a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/SnowflakeForm.tsx b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/SnowflakeForm.tsx index 75f226f37..046870f49 100644 --- a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/SnowflakeForm.tsx +++ b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/SnowflakeForm.tsx @@ -30,7 +30,7 @@ export const SnowflakeForm: React.FC<{ warehouse_id: credentials?.warehouse_id || '', username: credentials?.username || '', password: credentials?.password || '', - role: credentials?.role || null, + role: credentials?.role || '', default_database: credentials?.default_database || '', default_schema: credentials?.default_schema || '', type: 'snowflake' as const, diff --git a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/helpers.ts b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/[datasourceId]/_forms/helpers.ts new file mode 100644 index 000000000..e69de29bb diff --git a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/add/page.tsx b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/add/page.tsx index c5dfe8edb..a13a1922b 100644 --- a/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/add/page.tsx +++ b/web/src/app/app/(settings_layout)/settings/(restricted-width)/datasources/(admin-restricted-space)/add/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useEffect } from 'react'; import { BusterRoutes, createBusterRoute } from '@/routes'; import { HeaderContainer } from '../../_HeaderContainer'; import { useState } from 'react'; @@ -10,22 +10,44 @@ import { Title, Text } from '@/components/ui/typography'; import { useBusterNotifications } from '@/context/BusterNotifications'; import { cn } from '@/lib/classMerge'; import { AppDataSourceIcon } from '@/components/ui/icons/AppDataSourceIcons'; +import { useRouter } from 'next/navigation'; +import { useMemoizedFn } from '@/hooks'; +import Link from 'next/link'; -export default function Page() { - const [selectedDataSource, setSelectedDataSource] = useState(null); - const { openInfoMessage } = useBusterNotifications(); +export default function Page({ + searchParams: { type } +}: { + searchParams: { type?: DataSourceTypes }; +}) { + const router = useRouter(); + const [selectedDataSource, setSelectedDataSource] = useState( + type || null + ); const linkUrl = selectedDataSource - ? '' + ? createBusterRoute({ + route: BusterRoutes.SETTINGS_DATASOURCES_ADD + }) : createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES }); + const onClearSelectedDataSource = useMemoizedFn(() => { + setSelectedDataSource(null); + router.push(createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES_ADD })); + }); + + useEffect(() => { + if (type) { + setSelectedDataSource(type); + } + }, [type]); + return (
setSelectedDataSource(null)} + onClick={onClearSelectedDataSource} linkUrl={linkUrl} /> @@ -34,15 +56,7 @@ export default function Page() { ) : (
- { - if (SUPPORTED_DATASOURCES.includes(v)) { - setSelectedDataSource(v); - } else { - openInfoMessage('This data source is not currently supported'); - } - }} - /> +
)}
@@ -58,25 +72,27 @@ const ConnectHeader: React.FC<{}> = ({}) => { ); }; -const DataSourceList: React.FC<{ - onSelect: (dataSource: DataSourceTypes) => void; -}> = ({ onSelect }) => { +const DataSourceList: React.FC<{}> = ({}) => { return (
- {Object.values(DataSourceTypes).map((dataSource) => { + {SUPPORTED_DATASOURCES.map((dataSource) => { const name = DatabaseNames[dataSource]; return ( -
onSelect(dataSource)} + {name} -
+ ); })}