From 9757a31c224d3c60c9b0c3948dc4d628aea04a7d Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Sun, 10 Mar 2024 21:36:56 +0530 Subject: [PATCH] Bunch of UI improvements (#1456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## UI Usability Changes This PR performs a handful of improvements, fixes and changes across the board for our UI. ### Create Mirror Page - Moves `Snapshot Number of Tables In Parallel` , `CDC Staging Path` ,`Snapshot Staging Path` to **Advanced Settings** - Publication name is now a dropdown field and is mandatory. - Table picker now shows the size of every table Screenshot 2024-03-10 at 6 59 42 PM Screenshot 2024-03-10 at 6 01 11 PM ### Edit Mirror Page - Added a banner regarding adding tables. This banner pops up only when atleast one table has been selected for addition. - Made Edit and Back button float to reduce scrolling hassle Screenshot 2024-03-10 at 6 59 20 PM ### Create Peer Pages - Dropdown now has options for RDS, Azure and GCP - Errors now show in toast bars - Added link to corresponding PeerDB doc. Screenshot 2024-03-10 at 7 46 11 PM Screenshot 2024-03-10 at 7 46 30 PM --- flow/cmd/peer_data.go | 40 ++++++++++++++++- protos/route.proto | 9 ++++ ui/app/api/peers/publications/route.ts | 22 +++++++++ ui/app/api/peers/schemas/route.ts | 5 --- ui/app/dto/MirrorsDTO.ts | 1 + ui/app/dto/PeersDTO.ts | 4 ++ ui/app/mirrors/[mirrorId]/edit/page.tsx | 52 +++++++++++++--------- ui/app/mirrors/create/cdc/cdc.tsx | 29 ++++++++++-- ui/app/mirrors/create/cdc/fields.tsx | 48 ++++++++++++++++++-- ui/app/mirrors/create/cdc/guide.tsx | 22 ++++++--- ui/app/mirrors/create/cdc/schemabox.tsx | 7 +++ ui/app/mirrors/create/handlers.ts | 16 +++++++ ui/app/mirrors/create/helpers/cdc.ts | 38 ++++++++++------ ui/app/mirrors/create/helpers/common.ts | 8 ++-- ui/app/mirrors/create/schema.ts | 13 +++--- ui/app/peers/create/[peerType]/handlers.ts | 26 +++++------ ui/app/peers/create/[peerType]/page.tsx | 44 +++++++++--------- ui/app/utils/titlecase.ts | 12 +++++ ui/components/PeerComponent.tsx | 6 +++ ui/components/PeerForms/PostgresForm.tsx | 4 +- ui/components/SelectSource.tsx | 23 ++++++---- ui/public/svgs/azurepg.svg | 1 + ui/public/svgs/gcp.svg | 1 + ui/public/svgs/rds.svg | 16 +++++++ 24 files changed, 342 insertions(+), 105 deletions(-) create mode 100644 ui/app/api/peers/publications/route.ts create mode 100644 ui/app/utils/titlecase.ts create mode 100644 ui/public/svgs/azurepg.svg create mode 100644 ui/public/svgs/gcp.svg create mode 100644 ui/public/svgs/rds.svg diff --git a/flow/cmd/peer_data.go b/flow/cmd/peer_data.go index 7ec28329e2..6682e1aebb 100644 --- a/flow/cmd/peer_data.go +++ b/flow/cmd/peer_data.go @@ -92,7 +92,8 @@ func (h *FlowRequestHandler) GetTablesInSchema( CASE WHEN con.contype = 'p' OR t.relreplident = 'i' OR t.relreplident = 'f' THEN true ELSE false - END AS can_mirror + END AS can_mirror, + pg_size_pretty(pg_total_relation_size(quote_ident(n.nspname) || '.' || quote_ident(t.relname))) :: text AS table_size FROM pg_class t LEFT JOIN @@ -108,6 +109,7 @@ func (h *FlowRequestHandler) GetTablesInSchema( can_mirror DESC; `, req.SchemaName) if err != nil { + slog.Info("failed to fetch publications", slog.Any("error", err)) return &protos.SchemaTablesResponse{Tables: nil}, err } @@ -116,10 +118,15 @@ func (h *FlowRequestHandler) GetTablesInSchema( for rows.Next() { var table pgtype.Text var hasPkeyOrReplica pgtype.Bool - err := rows.Scan(&table, &hasPkeyOrReplica) + var tableSize pgtype.Text + err := rows.Scan(&table, &hasPkeyOrReplica, &tableSize) if err != nil { return &protos.SchemaTablesResponse{Tables: nil}, err } + var sizeOfTable string + if tableSize.Valid { + sizeOfTable = tableSize.String + } canMirror := false if hasPkeyOrReplica.Valid && hasPkeyOrReplica.Bool { canMirror = true @@ -128,8 +135,14 @@ func (h *FlowRequestHandler) GetTablesInSchema( tables = append(tables, &protos.TableResponse{ TableName: table.String, CanMirror: canMirror, + TableSize: sizeOfTable, }) } + + if err := rows.Err(); err != nil { + slog.Info("failed to fetch publications", slog.Any("error", err)) + return &protos.SchemaTablesResponse{Tables: nil}, err + } return &protos.SchemaTablesResponse{Tables: tables}, nil } @@ -325,3 +338,26 @@ func (h *FlowRequestHandler) GetStatInfo( StatData: statInfoRows, }, nil } + +func (h *FlowRequestHandler) GetPublications( + ctx context.Context, + req *protos.PostgresPeerActivityInfoRequest, +) (*protos.PeerPublicationsResponse, error) { + tunnel, peerConn, err := h.getConnForPGPeer(ctx, req.PeerName) + if err != nil { + return &protos.PeerPublicationsResponse{PublicationNames: nil}, err + } + defer tunnel.Close() + defer peerConn.Close(ctx) + + rows, err := peerConn.Query(ctx, "select pubname from pg_publication;") + if err != nil { + return &protos.PeerPublicationsResponse{PublicationNames: nil}, err + } + + publications, err := pgx.CollectRows[string](rows, pgx.RowTo) + if err != nil { + return &protos.PeerPublicationsResponse{PublicationNames: nil}, err + } + return &protos.PeerPublicationsResponse{PublicationNames: publications}, nil +} diff --git a/protos/route.proto b/protos/route.proto index 316459d78f..4d2c601fad 100644 --- a/protos/route.proto +++ b/protos/route.proto @@ -109,6 +109,10 @@ message PeerSchemasResponse { repeated string schemas = 1; } +message PeerPublicationsResponse { + repeated string publication_names = 1; +} + message SchemaTablesRequest { string peer_name = 1; string schema_name = 2; @@ -121,6 +125,7 @@ message SchemaTablesResponse { message TableResponse { string table_name = 1; bool can_mirror = 2; + string table_size = 3; } message AllTablesResponse { @@ -265,6 +270,10 @@ service FlowService { option (google.api.http) = { get: "/v1/peers/schemas" }; } + rpc GetPublications(PostgresPeerActivityInfoRequest) returns (PeerPublicationsResponse) { + option (google.api.http) = { get: "/v1/peers/publications" }; + } + rpc GetTablesInSchema(SchemaTablesRequest) returns (SchemaTablesResponse) { option (google.api.http) = { get: "/v1/peers/tables" }; } diff --git a/ui/app/api/peers/publications/route.ts b/ui/app/api/peers/publications/route.ts new file mode 100644 index 0000000000..64dcd3dcf6 --- /dev/null +++ b/ui/app/api/peers/publications/route.ts @@ -0,0 +1,22 @@ +import { UPublicationsResponse } from '@/app/dto/PeersDTO'; +import { PeerPublicationsResponse } from '@/grpc_generated/route'; +import { GetFlowHttpAddressFromEnv } from '@/rpc/http'; + +export async function POST(request: Request) { + const body = await request.json(); + const { peerName } = body; + const flowServiceAddr = GetFlowHttpAddressFromEnv(); + try { + const publicationList: PeerPublicationsResponse = await fetch( + `${flowServiceAddr}/v1/peers/publications?peer_name=${peerName}` + ).then((res) => { + return res.json(); + }); + let response: UPublicationsResponse = { + publicationNames: publicationList.publicationNames, + }; + return new Response(JSON.stringify(response)); + } catch (e) { + console.log(e); + } +} diff --git a/ui/app/api/peers/schemas/route.ts b/ui/app/api/peers/schemas/route.ts index 0c701cb713..9a1c10856e 100644 --- a/ui/app/api/peers/schemas/route.ts +++ b/ui/app/api/peers/schemas/route.ts @@ -14,11 +14,6 @@ export async function POST(request: Request) { let response: USchemasResponse = { schemas: schemaList.schemas, }; - if (schemaList.message === 'no rows in result set') { - response = { - schemas: [], - }; - } return new Response(JSON.stringify(response)); } catch (e) { console.log(e); diff --git a/ui/app/dto/MirrorsDTO.ts b/ui/app/dto/MirrorsDTO.ts index 33234a557d..9b3724902a 100644 --- a/ui/app/dto/MirrorsDTO.ts +++ b/ui/app/dto/MirrorsDTO.ts @@ -26,6 +26,7 @@ export type TableMapRow = { exclude: Set; selected: boolean; canMirror: boolean; + tableSize: string; }; export type SyncStatusRow = { diff --git a/ui/app/dto/PeersDTO.ts b/ui/app/dto/PeersDTO.ts index a015439d83..23b0243b4f 100644 --- a/ui/app/dto/PeersDTO.ts +++ b/ui/app/dto/PeersDTO.ts @@ -56,3 +56,7 @@ export type SlotLagPoint = { updatedAt: string; slotSize?: string; }; + +export type UPublicationsResponse = { + publicationNames: string[]; +}; diff --git a/ui/app/mirrors/[mirrorId]/edit/page.tsx b/ui/app/mirrors/[mirrorId]/edit/page.tsx index 4cff1b4538..b4787f1b1c 100644 --- a/ui/app/mirrors/[mirrorId]/edit/page.tsx +++ b/ui/app/mirrors/[mirrorId]/edit/page.tsx @@ -11,6 +11,7 @@ import { Label } from '@/lib/Label'; import { RowWithTextField } from '@/lib/Layout'; import { ProgressCircle } from '@/lib/ProgressCircle'; import { TextField } from '@/lib/TextField'; +import { Callout } from '@tremor/react'; import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { ToastContainer, toast } from 'react-toastify'; @@ -18,6 +19,7 @@ import 'react-toastify/dist/ReactToastify.css'; import TableMapping from '../../create/cdc/tablemapping'; import { reformattedTableMapping } from '../../create/handlers'; import { blankCDCSetting } from '../../create/helpers/common'; +import * as styles from '../../create/styles'; type EditMirrorProps = { params: { mirrorId: string }; }; @@ -120,8 +122,6 @@ const EditMirror = ({ params: { mirrorId } }: EditMirrorProps) => { return (
- - {'Pull Batch Size'} } @@ -174,6 +174,22 @@ const EditMirror = ({ params: { mirrorId } }: EditMirrorProps) => { } /> + + {!isNotPaused && rows.some((row) => row.selected === true) && ( + + CDC will be put on hold until initial load for these added tables have + been completed. +

+ The replication slot will grow during this period. +
+ )} + { omitAdditionalTablesMapping={omitAdditionalTablesMapping} /> - {isNotPaused ? ( - - ) : ( - + {isNotPaused && ( + + Mirror can only be edited while paused. + )} -
+ +
+ -
diff --git a/ui/app/mirrors/create/cdc/cdc.tsx b/ui/app/mirrors/create/cdc/cdc.tsx index 25c15e58f4..cb9ea4a381 100644 --- a/ui/app/mirrors/create/cdc/cdc.tsx +++ b/ui/app/mirrors/create/cdc/cdc.tsx @@ -2,8 +2,9 @@ import { DBType } from '@/grpc_generated/peers'; import { Button } from '@/lib/Button'; import { Icon } from '@/lib/Icon'; -import { Dispatch, SetStateAction, useMemo, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; import { CDCConfig, MirrorSetter, TableMapRow } from '../../../dto/MirrorsDTO'; +import { fetchPublications } from '../handlers'; import { MirrorSetting } from '../helpers/common'; import CDCField from './fields'; import TableMapping from './tablemapping'; @@ -36,6 +37,7 @@ export default function CDCConfigForm({ rows, setRows, }: MirrorConfigProps) { + const [publications, setPublications] = useState(); const [show, setShow] = useState(false); const handleChange = (val: string | boolean, setting: MirrorSetting) => { let stateVal: string | boolean = val; @@ -64,7 +66,26 @@ export default function CDCConfigForm({ return true; }; - if (mirrorConfig.source != undefined && mirrorConfig.destination != undefined) + const optionsForField = (setting: MirrorSetting) => { + switch (setting.label) { + case 'Publication Name': + return publications; + default: + return []; + } + }; + + useEffect(() => { + fetchPublications(mirrorConfig.source?.name || '').then((pubs) => { + setPublications(pubs); + }); + }, [mirrorConfig.source?.name]); + + if ( + mirrorConfig.source != undefined && + mirrorConfig.destination != undefined && + publications != undefined + ) return ( <> {normalSettings.map((setting, id) => { @@ -74,6 +95,7 @@ export default function CDCConfigForm({ key={id} handleChange={handleChange} setting={setting} + options={optionsForField(setting)} /> ) ); @@ -101,9 +123,10 @@ export default function CDCConfigForm({ advancedSettings.map((setting, id) => { return ( ); })} diff --git a/ui/app/mirrors/create/cdc/fields.tsx b/ui/app/mirrors/create/cdc/fields.tsx index acbb47764b..cf9f969d4d 100644 --- a/ui/app/mirrors/create/cdc/fields.tsx +++ b/ui/app/mirrors/create/cdc/fields.tsx @@ -1,21 +1,29 @@ 'use client'; +import SelectTheme from '@/app/styles/select'; import { RequiredIndicator } from '@/components/RequiredIndicator'; import { Label } from '@/lib/Label'; -import { RowWithSwitch, RowWithTextField } from '@/lib/Layout'; +import { RowWithSelect, RowWithSwitch, RowWithTextField } from '@/lib/Layout'; import { Switch } from '@/lib/Switch'; import { TextField } from '@/lib/TextField'; +import ReactSelect from 'react-select'; import { InfoPopover } from '../../../../components/InfoPopover'; import { MirrorSetting } from '../helpers/common'; interface FieldProps { setting: MirrorSetting; handleChange: (val: string | boolean, setting: MirrorSetting) => void; + options?: string[]; } -const CDCField = ({ setting, handleChange }: FieldProps) => { +const CDCField = ({ setting, handleChange, options }: FieldProps) => { return setting.type === 'switch' ? ( {setting.label}} + label={ + + } action={
{
} /> + ) : setting.type === 'select' ? ( + + {setting.label} + {RequiredIndicator(setting.required)} + + } + action={ +
+
+ + val && handleChange(val.option, setting) + } + options={options?.map((option) => ({ option, label: option }))} + getOptionLabel={(option) => option.label} + getOptionValue={(option) => option.option} + theme={SelectTheme} + /> +
+ {setting.tips && ( + + )} +
+ } + /> ) : ( { const linkForDst = () => { - switch (peerType) { + console.log(peerType); + switch (peerType.toUpperCase().replace(/%20/g, ' ')) { case 'SNOWFLAKE': return 'https://docs.peerdb.io/connect/snowflake'; case 'BIGQUERY': return 'https://docs.peerdb.io/connect/bigquery'; + case 'RDS POSTGRESQL': + case 'POSTGRESQL': + return 'https://docs.peerdb.io/connect/rds_postgres'; + case 'AZURE FLEXIBLE POSTGRESQL': + return 'https://docs.peerdb.io/connect/azure_flexible_server_postgres'; + case 'GOOGLE CLOUD POSTGRESQL': + return 'https://docs.peerdb.io/connect/cloudsql_postgres'; default: - return 'https://docs.peerdb.io/'; + return ''; } }; - if (peerType != 'SNOWFLAKE' && peerType != 'BIGQUERY') { + if (linkForDst() == '') { return <>; } return ( diff --git a/ui/app/mirrors/create/cdc/schemabox.tsx b/ui/app/mirrors/create/cdc/schemabox.tsx index 0c1d7e9a19..4ec403f8ee 100644 --- a/ui/app/mirrors/create/cdc/schemabox.tsx +++ b/ui/app/mirrors/create/cdc/schemabox.tsx @@ -237,6 +237,13 @@ const SchemaBox = ({ > {row.source} + } action={ diff --git a/ui/app/mirrors/create/handlers.ts b/ui/app/mirrors/create/handlers.ts index beeb2aaf5d..282fe70a34 100644 --- a/ui/app/mirrors/create/handlers.ts +++ b/ui/app/mirrors/create/handlers.ts @@ -1,6 +1,7 @@ import { UCreateMirrorResponse } from '@/app/dto/MirrorsDTO'; import { UColumnsResponse, + UPublicationsResponse, USchemasResponse, UTablesAllResponse, UTablesResponse, @@ -298,6 +299,7 @@ export const fetchTables = async ( exclude: new Set(), selected: false, canMirror: tableObject.canMirror, + tableSize: tableObject.tableSize, }); } } @@ -367,3 +369,17 @@ export const handleValidateCDC = async ( notify('CDC Mirror is valid', true); setLoading(false); }; + +export const fetchPublications = async (peerName: string) => { + const publicationsRes: UPublicationsResponse = await fetch( + '/api/peers/publications', + { + method: 'POST', + body: JSON.stringify({ + peerName, + }), + cache: 'no-store', + } + ).then((res) => res.json()); + return publicationsRes.publicationNames; +}; diff --git a/ui/app/mirrors/create/helpers/cdc.ts b/ui/app/mirrors/create/helpers/cdc.ts index 10ab07a951..fbf713d609 100644 --- a/ui/app/mirrors/create/helpers/cdc.ts +++ b/ui/app/mirrors/create/helpers/cdc.ts @@ -11,6 +11,7 @@ export const cdcSettings: MirrorSetting[] = [ tips: 'Specify if you want initial load to happen for your tables.', type: 'switch', default: true, + required: true, }, { label: 'Pull Batch Size', @@ -35,6 +36,7 @@ export const cdcSettings: MirrorSetting[] = [ helpfulLink: 'https://docs.peerdb.io/metrics/important_cdc_configs', type: 'number', default: '60', + required: true, }, { label: 'Publication Name', @@ -43,7 +45,11 @@ export const cdcSettings: MirrorSetting[] = [ ...curr, publicationName: (value as string) || '', })), - tips: 'If set, PeerDB will use this publication for the mirror.', + type: 'select', + tips: 'PeerDB requires a publication associated with the tables you wish to sync.', + helpfulLink: + 'https://www.postgresql.org/docs/current/sql-createpublication.html', + required: true, }, { label: 'Replication Slot Name', @@ -59,33 +65,36 @@ export const cdcSettings: MirrorSetting[] = [ stateHandler: (value, setter) => setter((curr: CDCConfig) => ({ ...curr, - snapshotNumRowsPerPartition: parseInt(value as string, 10) || 500000, + snapshotNumRowsPerPartition: parseInt(value as string, 10) || 1000000, })), - tips: 'PeerDB splits up table data into partitions for increased performance. This setting controls the number of rows per partition. The default value is 500000.', - default: '500000', + tips: 'PeerDB splits up table data into partitions for increased performance. This setting controls the number of rows per partition. The default value is 1000000.', + default: '1000000', type: 'number', + advanced: true, }, { - label: 'Snapshot Maximum Parallel Workers', + label: 'Parallelism for Initial Load', stateHandler: (value, setter) => setter((curr: CDCConfig) => ({ ...curr, - snapshotMaxParallelWorkers: parseInt(value as string, 10) || 1, + snapshotMaxParallelWorkers: parseInt(value as string, 10) || 4, })), - tips: 'PeerDB spins up parallel threads for each partition. This setting controls the number of partitions to sync in parallel. The default value is 1.', - default: '1', + tips: 'PeerDB spins up parallel threads for each partition in initial load. This setting controls the number of partitions to sync in parallel. The default value is 4.', + default: '4', type: 'number', + required: true, }, { label: 'Snapshot Number of Tables In Parallel', stateHandler: (value, setter) => setter((curr: CDCConfig) => ({ ...curr, - snapshotNumTablesInParallel: parseInt(value as string, 10) || 4, + snapshotNumTablesInParallel: parseInt(value as string, 10) || 1, })), - tips: 'Specify the number of tables to sync perform initial load for, in parallel. The default value is 4.', - default: '4', + tips: 'Specify the number of tables to sync perform initial load for, in parallel. The default value is 1.', + default: '1', type: 'number', + advanced: true, }, { label: 'Snapshot Staging Path', @@ -95,6 +104,7 @@ export const cdcSettings: MirrorSetting[] = [ snapshotStagingPath: value as string | '', })), tips: 'You can specify staging path for Snapshot sync mode AVRO. For Snowflake as destination peer, this must be either empty or an S3 bucket URL. For BigQuery, this must be either empty or an existing GCS bucket name. In both cases, if empty, the local filesystem will be used.', + advanced: true, }, { label: 'CDC Staging Path', @@ -104,17 +114,19 @@ export const cdcSettings: MirrorSetting[] = [ cdcStagingPath: (value as string) || '', })), tips: 'You can specify staging path for CDC sync mode AVRO. For Snowflake as destination peer, this must be either empty or an S3 bucket URL. For BigQuery, this must be either empty or an existing GCS bucket name. In both cases, if empty, the local filesystem will be used.', + advanced: true, }, { label: 'Soft Delete', stateHandler: (value, setter) => setter((curr: CDCConfig) => ({ ...curr, - softDelete: (value as boolean) || false, + softDelete: (value as boolean) || true, })), tips: 'Allows you to mark some records as deleted without actual erasure from the database', - default: false, + default: true, type: 'switch', + required: true, }, { label: 'Initial Copy Only', diff --git a/ui/app/mirrors/create/helpers/common.ts b/ui/app/mirrors/create/helpers/common.ts index 5d1b8d9a93..0ed217e87b 100644 --- a/ui/app/mirrors/create/helpers/common.ts +++ b/ui/app/mirrors/create/helpers/common.ts @@ -23,12 +23,12 @@ export const blankCDCSetting: FlowConnectionConfigs = { maxBatchSize: 1000000, doInitialSnapshot: true, publicationName: '', - snapshotNumRowsPerPartition: 500000, - snapshotMaxParallelWorkers: 1, - snapshotNumTablesInParallel: 4, + snapshotNumRowsPerPartition: 1000000, + snapshotMaxParallelWorkers: 4, + snapshotNumTablesInParallel: 1, snapshotStagingPath: '', cdcStagingPath: '', - softDelete: false, + softDelete: true, replicationSlotName: '', resync: false, softDeleteColName: '', diff --git a/ui/app/mirrors/create/schema.ts b/ui/app/mirrors/create/schema.ts index 3ca4408315..dda55828a4 100644 --- a/ui/app/mirrors/create/schema.ts +++ b/ui/app/mirrors/create/schema.ts @@ -58,14 +58,15 @@ export const cdcSchema = z.object({ publicationName: z .string({ invalid_type_error: 'Publication name must be a string', + required_error: 'Publication name must be selected.', }) - .max(255, 'Publication name must be less than 255 characters') - .optional(), + .min(1, 'Publication name must not be empty') + .max(255, 'Publication name must be less than 255 characters'), replicationSlotName: z .string({ - invalid_type_error: 'Publication name must be a string', + invalid_type_error: 'Replication slot name must be a string', }) - .max(255, 'Publication name must be less than 255 characters') + .max(255, 'Replication slot name must be less than 255 characters') .optional(), snapshotNumRowsPerPartition: z .number({ @@ -76,10 +77,10 @@ export const cdcSchema = z.object({ .optional(), snapshotMaxParallelWorkers: z .number({ - invalid_type_error: 'Snapshot max workers must be a number', + invalid_type_error: 'Initial load parallelism must be a number', }) .int() - .min(1, 'Snapshot max workers must be a positive integer') + .min(1, 'Initial load parallelism must be a positive integer') .optional(), snapshotNumTablesInParallel: z .number({ diff --git a/ui/app/peers/create/[peerType]/handlers.ts b/ui/app/peers/create/[peerType]/handlers.ts index e43164aa96..0964c80e35 100644 --- a/ui/app/peers/create/[peerType]/handlers.ts +++ b/ui/app/peers/create/[peerType]/handlers.ts @@ -17,20 +17,20 @@ import { const validateFields = ( type: string, config: PeerConfig, - setMessage: Dispatch>, + notify: (msg: string) => void, name?: string ): boolean => { const peerNameValid = peerNameSchema.safeParse(name); if (!peerNameValid.success) { const peerNameErr = peerNameValid.error.issues[0].message; - setMessage({ ok: false, msg: peerNameErr }); + notify(peerNameErr); return false; } if (type === 'S3') { const s3Valid = S3Validation(config as S3Config); if (s3Valid.length > 0) { - setMessage({ ok: false, msg: s3Valid }); + notify(s3Valid); return false; } } @@ -61,9 +61,9 @@ const validateFields = ( validationErr = 'Unsupported peer type ' + type; } if (validationErr) { - setMessage({ ok: false, msg: validationErr }); + notify(validationErr); return false; - } else setMessage({ ok: true, msg: '' }); + } return true; }; @@ -71,11 +71,11 @@ const validateFields = ( export const handleValidate = async ( type: string, config: PeerConfig, - setMessage: Dispatch>, + notify: (msg: string) => void, setLoading: Dispatch>, name?: string ) => { - const isValid = validateFields(type, config, setMessage, name); + const isValid = validateFields(type, config, notify, name); if (!isValid) return; setLoading(true); const valid: UValidatePeerResponse = await fetch('/api/peers/', { @@ -89,11 +89,11 @@ export const handleValidate = async ( cache: 'no-store', }).then((res) => res.json()); if (!valid.valid) { - setMessage({ ok: false, msg: valid.message }); + notify(valid.message); setLoading(false); return; } - setMessage({ ok: true, msg: 'Peer is valid' }); + notify('Peer is valid'); setLoading(false); }; @@ -108,12 +108,12 @@ const S3Validation = (config: S3Config): string => { export const handleCreate = async ( type: string, config: PeerConfig, - setMessage: Dispatch>, + notify: (msg: string) => void, setLoading: Dispatch>, route: RouteCallback, name?: string ) => { - let isValid = validateFields(type, config, setMessage, name); + let isValid = validateFields(type, config, notify, name); if (!isValid) return; setLoading(true); const createdPeer: UCreatePeerResponse = await fetch('/api/peers/', { @@ -127,11 +127,11 @@ export const handleCreate = async ( cache: 'no-store', }).then((res) => res.json()); if (!createdPeer.created) { - setMessage({ ok: false, msg: createdPeer.message }); + notify(createdPeer.message); setLoading(false); return; } - setMessage({ ok: true, msg: 'Peer created successfully' }); + route(); setLoading(false); }; diff --git a/ui/app/peers/create/[peerType]/page.tsx b/ui/app/peers/create/[peerType]/page.tsx index b0693075b8..0f01d59279 100644 --- a/ui/app/peers/create/[peerType]/page.tsx +++ b/ui/app/peers/create/[peerType]/page.tsx @@ -7,6 +7,7 @@ import PostgresForm from '@/components/PeerForms/PostgresForm'; import S3Form from '@/components/PeerForms/S3Form'; import SnowflakeForm from '@/components/PeerForms/SnowflakeForm'; +import TitleCase from '@/app/utils/titlecase'; import { Button } from '@/lib/Button'; import { ButtonGroup } from '@/lib/ButtonGroup'; import { Label } from '@/lib/Label'; @@ -17,6 +18,8 @@ import { Tooltip } from '@/lib/Tooltip'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import { handleCreate, handleValidate } from './handlers'; import { clickhouseSetting } from './helpers/ch'; import { getBlankSetting } from './helpers/common'; @@ -27,6 +30,12 @@ type CreateConfigProps = { params: { peerType: string }; }; +const notifyErr = (errMsg: string) => { + toast.error(errMsg, { + position: 'bottom-center', + }); +}; + export default function CreateConfig({ params: { peerType }, }: CreateConfigProps) { @@ -35,15 +44,19 @@ export default function CreateConfig({ const blankSetting = getBlankSetting(dbType); const [name, setName] = useState(''); const [config, setConfig] = useState(blankSetting); - const [formMessage, setFormMessage] = useState<{ ok: boolean; msg: string }>({ - ok: true, - msg: '', - }); const [loading, setLoading] = useState(false); const configComponentMap = (dbType: string) => { + if (dbType.includes('POSTGRESQL')) { + return ( + + ); + } + switch (dbType) { - case 'POSTGRES': - return ; case 'SNOWFLAKE': return ; case 'BIGQUERY': @@ -76,11 +89,10 @@ export default function CreateConfig({ > - + - handleValidate(dbType, config, setFormMessage, setLoading, name) + handleValidate(dbType, config, notifyErr, setLoading, name) } > Validate @@ -131,7 +143,7 @@ export default function CreateConfig({ handleCreate( dbType, config, - setFormMessage, + notifyErr, setLoading, listPeersRoute, name @@ -151,15 +163,7 @@ export default function CreateConfig({ Validating... )} - {!loading && formMessage.msg.length > 0 && ( - - )} +
diff --git a/ui/app/utils/titlecase.ts b/ui/app/utils/titlecase.ts new file mode 100644 index 0000000000..31f262eed2 --- /dev/null +++ b/ui/app/utils/titlecase.ts @@ -0,0 +1,12 @@ +function TitleCase(input: string): string { + return input + .toLowerCase() + .replace(/(?:^|\s)\S/g, function (char) { + return char.toUpperCase(); + }) + .replace(/Postgresql/g, 'PostgreSQL') + .replace(/Postgresql/g, 'PostgreSQL') + .replace(/Rds/g, 'RDS'); +} + +export default TitleCase; diff --git a/ui/components/PeerComponent.tsx b/ui/components/PeerComponent.tsx index 5b2f717cd2..66851820ed 100644 --- a/ui/components/PeerComponent.tsx +++ b/ui/components/PeerComponent.tsx @@ -8,6 +8,12 @@ import { useRouter } from 'next/navigation'; import { useState } from 'react'; export const DBTypeToImageMapping = (peerType: DBType | string) => { switch (peerType) { + case 'AZURE FLEXIBLE POSTGRESQL': + return '/svgs/azurepg.svg'; + case 'RDS POSTGRESQL': + return '/svgs/rds.svg'; + case 'GOOGLE CLOUD POSTGRESQL': + return '/svgs/gcp.svg'; case DBType.POSTGRES: case 'POSTGRES': return '/svgs/pg.svg'; diff --git a/ui/components/PeerForms/PostgresForm.tsx b/ui/components/PeerForms/PostgresForm.tsx index 0c7f5e808d..98bef1aad2 100644 --- a/ui/components/PeerForms/PostgresForm.tsx +++ b/ui/components/PeerForms/PostgresForm.tsx @@ -18,12 +18,12 @@ import { InfoPopover } from '../InfoPopover'; interface ConfigProps { settings: PeerSetting[]; setter: PeerSetter; + type: string; } -export default function PostgresForm({ settings, setter }: ConfigProps) { +export default function PostgresForm({ settings, setter, type }: ConfigProps) { const [showSSH, setShowSSH] = useState(false); const [sshConfig, setSSHConfig] = useState(blankSSHConfig); - const handleFile = ( file: File, setFile: (value: string, configSetter: sshSetter) => void diff --git a/ui/components/SelectSource.tsx b/ui/components/SelectSource.tsx index 4aefe67082..c445ceb21e 100644 --- a/ui/components/SelectSource.tsx +++ b/ui/components/SelectSource.tsx @@ -1,5 +1,6 @@ 'use client'; import SelectTheme from '@/app/styles/select'; +import TitleCase from '@/app/utils/titlecase'; import { DBType } from '@/grpc_generated/peers'; import Image from 'next/image'; import { Dispatch, SetStateAction } from 'react'; @@ -11,12 +12,12 @@ interface SelectSourceProps { setPeerType: Dispatch>; } -function SourceLabel({ value }: { value: string }) { - const peerLogo = DBTypeToImageMapping(value); +function SourceLabel({ value, label }: { value: string; label: string }) { + const peerLogo = DBTypeToImageMapping(label); return (
peer -
{value}
+
{TitleCase(label)}
); } @@ -25,11 +26,11 @@ export default function SelectSource({ peerType, setPeerType, }: SelectSourceProps) { - const dbTypes = Object.values(DBType) + let dbTypes = Object.values(DBType) .filter( (value): value is string => typeof value === 'string' && - (value === 'POSTGRES' || + (value === 'POSTGRESQL' || value === 'SNOWFLAKE' || value === 'BIGQUERY' || value === 'S3' || @@ -37,13 +38,19 @@ export default function SelectSource({ ) .map((value) => ({ label: value, value })); + dbTypes.push( + { value: 'POSTGRESQL', label: 'POSTGRESQL' }, + { value: 'POSTGRESQL', label: 'RDS POSTGRESQL' }, + { value: 'POSTGRESQL', label: 'GOOGLE CLOUD POSTGRESQL' }, + { value: 'POSTGRESQL', label: 'AZURE FLEXIBLE POSTGRESQL' } + ); return ( opt.value === peerType)} - onChange={(val, _) => val && setPeerType(val.value)} + defaultValue={dbTypes.find((opt) => opt.label === peerType)} + onChange={(val, _) => val && setPeerType(val.label)} formatOptionLabel={SourceLabel} theme={SelectTheme} /> diff --git a/ui/public/svgs/azurepg.svg b/ui/public/svgs/azurepg.svg new file mode 100644 index 0000000000..811a6c7397 --- /dev/null +++ b/ui/public/svgs/azurepg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/svgs/gcp.svg b/ui/public/svgs/gcp.svg new file mode 100644 index 0000000000..81b7d24547 --- /dev/null +++ b/ui/public/svgs/gcp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/svgs/rds.svg b/ui/public/svgs/rds.svg new file mode 100644 index 0000000000..f384cdad89 --- /dev/null +++ b/ui/public/svgs/rds.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file