Skip to content

Commit

Permalink
UI for Snowflake Peer (#474)
Browse files Browse the repository at this point in the history
fixes: #431
  • Loading branch information
Amogh-Bharadwaj authored Oct 4, 2023
1 parent af126b5 commit b181cff
Show file tree
Hide file tree
Showing 14 changed files with 464 additions and 64 deletions.
13 changes: 12 additions & 1 deletion ui/app/api/peers/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { PeerConfig } from '@/app/peers/create/configuration/types';
import { DBType, Peer, PostgresConfig } from '@/grpc_generated/peers';
import {
DBType,
Peer,
PostgresConfig,
SnowflakeConfig,
} from '@/grpc_generated/peers';
import {
CreatePeerRequest,
CreatePeerResponse,
Expand All @@ -22,6 +27,12 @@ const constructPeer = (
type: DBType.POSTGRES,
postgresConfig: config as PostgresConfig,
};
case 'SNOWFLAKE':
return {
name,
type: DBType.SNOWFLAKE,
snowflakeConfig: config as SnowflakeConfig,
};
default:
return;
}
Expand Down
40 changes: 0 additions & 40 deletions ui/app/peers/create/configuration/configForm.tsx

This file was deleted.

6 changes: 5 additions & 1 deletion ui/app/peers/create/configuration/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context';
import { Dispatch, SetStateAction } from 'react';
import { pgSchema } from './schema';
import { pgSchema, sfSchema } from './schema';
import { PeerConfig } from './types';

// Frontend form validation
Expand All @@ -20,6 +20,10 @@ const validateFields = (
const pgConfig = pgSchema.safeParse(config);
if (!pgConfig.success) validationErr = pgConfig.error.issues[0].message;
break;
case 'SNOWFLAKE':
const sfConfig = sfSchema.safeParse(config);
if (!sfConfig.success) validationErr = sfConfig.error.issues[0].message;
break;
default:
validationErr = 'Unsupported peer type ' + type;
}
Expand Down
16 changes: 15 additions & 1 deletion ui/app/peers/create/configuration/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { PeerConfig, PeerSetter } from '../types';
import { blankPostgresSetting } from './pg';
import { blankSnowflakeSetting } from './sf';

export const getBlankSetting = (dbType: string) => {
export interface Setting {
label: string;
stateHandler: (value: string, setter: PeerSetter) => void;
type?: string;
optional?: boolean;
tips?: string;
helpfulLink?: string;
default?: string | number;
}

export const getBlankSetting = (dbType: string): PeerConfig => {
switch (dbType) {
case 'POSTGRES':
return blankPostgresSetting;
case 'SNOWFLAKE':
return blankSnowflakeSetting;
default:
return blankPostgresSetting;
}
Expand Down
31 changes: 22 additions & 9 deletions ui/app/peers/create/configuration/helpers/pg.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,54 @@
import { PeerSetter } from '../types';
import { PostgresConfig } from '@/grpc_generated/peers';
import { Setting } from './common';

export const postgresSetting = [
export const postgresSetting: Setting[] = [
{
label: 'Host',
stateHandler: (value: string, setter: PeerSetter) =>
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, host: value })),
tips: 'Specifies the IP host name or address on which postgres is to listen for TCP/IP connections from client applications. Ensure that this host has us whitelisted so we can connect to it.',
},
{
label: 'Port',
stateHandler: (value: string, setter: PeerSetter) =>
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, port: parseInt(value, 10) })),
type: 'number', // type for textfield
default: 5432,
tips: 'Specifies the TCP/IP port or local Unix domain socket file extension on which postgres is listening for connections from client applications.',
},
{
label: 'User',
stateHandler: (value: string, setter: PeerSetter) =>
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, user: value })),
tips: 'Specify the user that we should use to connect to this host.',
helpfulLink: 'https://www.postgresql.org/docs/8.0/user-manag.html',
},
{
label: 'Password',
stateHandler: (value: string, setter: PeerSetter) =>
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, password: value })),
type: 'password',
tips: 'Password associated with the user you provided.',
helpfulLink: 'https://www.postgresql.org/docs/current/auth-password.html',
},
{
label: 'Database',
stateHandler: (value: string, setter: PeerSetter) =>
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, database: value })),
tips: 'Specify which database to associate with this peer.',
helpfulLink:
'https://www.postgresql.org/docs/current/sql-createdatabase.html',
},
{
label: 'Transaction Snapshot',
stateHandler: (value: string, setter: PeerSetter) =>
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, transactionSnapshot: value })),
optional: true,
tips: 'This is optional and only needed if this peer is part of any query replication mirror.',
},
];

export const blankPostgresSetting = {
export const blankPostgresSetting: PostgresConfig = {
host: '',
port: 5432,
user: '',
Expand Down
95 changes: 95 additions & 0 deletions ui/app/peers/create/configuration/helpers/sf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { SnowflakeConfig } from '@/grpc_generated/peers';
import { Setting } from './common';

export const snowflakeSetting: Setting[] = [
{
label: 'Account ID',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, accountId: value })),
tips: 'This is the unique identifier for your Snowflake account. It has a URL-like format',
helpfulLink:
'https://docs.snowflake.com/en/user-guide/admin-account-identifier',
},
{
label: 'Username',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, username: value })),
tips: 'This is the username you use to login to your Snowflake account.',
helpfulLink:
'https://docs.snowflake.com/en/user-guide/admin-user-management',
},
{
label: 'Private Key',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, privateKey: value })),
type: 'file',
tips: 'This can be of any file extension. If you are using an encrypted key, you must fill the below password field for decryption.',
helpfulLink: 'https://docs.snowflake.com/en/user-guide/key-pair-auth',
},
{
label: 'Warehouse',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, warehouse: value })),
tips: 'Warehouses denote a cluster of snowflake resources.',
helpfulLink: 'https://docs.snowflake.com/en/user-guide/warehouses',
},
{
label: 'Database',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, database: value })),
tips: 'Specify which database to associate with this peer.',
helpfulLink: 'https://docs.snowflake.com/en/sql-reference/snowflake-db',
},
{
label: 'Role',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, role: value })),
tips: 'You could use a default role, or setup a role with the required permissions.',
helpfulLink:
'https://docs.snowflake.com/en/user-guide/security-access-control-overview#roles',
},
{
label: 'Query Timeout',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, queryTimeout: parseInt(value, 10) || 30 })),
optional: true,
tips: 'This is the maximum time in seconds that a query can run before being cancelled. If not specified, the default is 30 seconds',
default: 30,
},
{
label: 'S3 Integration',
stateHandler: (value, setter) =>
setter((curr) => ({ ...curr, s3Integration: value })),
optional: true,
tips: `This is needed only if you plan to run a mirror and you wish to stage AVRO files on S3.`,
helpfulLink:
'https://docs.snowflake.com/en/user-guide/data-load-s3-config-storage-integration',
},
{
label: 'Password',
stateHandler: (value, setter) => {
if (!value.length) {
// remove password key from state if empty
setter((curr) => {
delete curr['password'];
return curr;
});
} else setter((curr) => ({ ...curr, password: value }));
},
type: 'password',
optional: true,
tips: 'This is needed only if the private key you provided is encrypted.',
helpfulLink: 'https://docs.snowflake.com/en/user-guide/key-pair-auth',
},
];

export const blankSnowflakeSetting: SnowflakeConfig = {
accountId: '',
privateKey: '',
username: '',
warehouse: '',
database: '',
role: '',
queryTimeout: 30,
s3Integration: '',
};
41 changes: 32 additions & 9 deletions ui/app/peers/create/configuration/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Label } from '@/lib/Label';
import { LayoutMain, RowWithTextField } from '@/lib/Layout';
import { Panel } from '@/lib/Panel';
import { TextField } from '@/lib/TextField';
import { Tooltip } from '@/lib/Tooltip';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import ConfigForm from './configForm';
import ConfigForm from '../../../../components/ConfigForm';
import { handleCreate, handleValidate } from './handlers';
import { getBlankSetting } from './helpers/common';
import { Setting, getBlankSetting } from './helpers/common';
import { postgresSetting } from './helpers/pg';
import { snowflakeSetting } from './helpers/sf';
import { PeerConfig } from './types';
export default function CreateConfig() {
const searchParams = useSearchParams();
Expand All @@ -26,9 +28,14 @@ export default function CreateConfig() {
});
const [loading, setLoading] = useState<boolean>(false);
const configComponentMap = (dbType: string) => {
const configForm = (settingList: Setting[]) => (
<ConfigForm settings={settingList} setter={setConfig} />
);
switch (dbType) {
case 'POSTGRES':
return <ConfigForm settings={postgresSetting} setter={setConfig} />;
return configForm(postgresSetting);
case 'SNOWFLAKE':
return configForm(snowflakeSetting);
default:
return <></>;
}
Expand All @@ -37,22 +44,38 @@ export default function CreateConfig() {
return (
<LayoutMain alignSelf='center' justifySelf='center' width='xxLarge'>
<Panel>
<Label variant='title3'>New peer</Label>
<Label colorName='lowContrast'>Set up a new peer.</Label>
<Label variant='title3'>
Setup a new{' '}
{dbType.charAt(0).toUpperCase() + dbType.slice(1).toLowerCase()} peer
</Label>
</Panel>
<Panel>
<Label colorName='lowContrast' variant='subheadline'>
Configuration
</Label>
<RowWithTextField
label={<Label as='label'>Name</Label>}
label={
<Label>
Name
{
<Tooltip
style={{ width: '100%' }}
content={'Peer name is a required field.'}
>
<Label colorName='lowContrast' colorSet='destructive'>
*
</Label>
</Tooltip>
}
</Label>
}
action={
<TextField
variant='simple'
onChange={(e) => setName(e.target.value)}
/>
}
/>
<Label colorName='lowContrast' variant='subheadline'>
Configuration
</Label>
{dbType && configComponentMap(dbType)}
</Panel>
<Panel>
Expand Down
Loading

0 comments on commit b181cff

Please sign in to comment.