From 8db584f50e73abe57adf63a6f3f8fd7fcc345615 Mon Sep 17 00:00:00 2001 From: Amogh-Bharadwaj Date: Sun, 10 Mar 2024 19:00:40 +0530 Subject: [PATCH] bunch of UI improvements --- 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/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 | 26 +++++------ 16 files changed, 260 insertions(+), 84 deletions(-) create mode 100644 ui/app/api/peers/publications/route.ts 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 && ( + + )} +
+ } + /> ) : ( {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..b3310f32dd 100644 --- a/ui/app/peers/create/[peerType]/page.tsx +++ b/ui/app/peers/create/[peerType]/page.tsx @@ -17,6 +17,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 +29,12 @@ type CreateConfigProps = { params: { peerType: string }; }; +const notifyErr = (errMsg: string) => { + toast.error(errMsg, { + position: 'bottom-center', + }); +}; + export default function CreateConfig({ params: { peerType }, }: CreateConfigProps) { @@ -35,10 +43,6 @@ 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) => { switch (dbType) { @@ -120,7 +124,7 @@ export default function CreateConfig({