From e3eca4f26da87c6c74203d84dd1b6e5a7feb7041 Mon Sep 17 00:00:00 2001 From: Evis Drenova <80707987+evisdrenova@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:55:12 -0800 Subject: [PATCH] NEOS-337: postgres connection name validation (#603) --- .../is-connection-name-available/route.ts | 21 ++ .../[id]/components/PostgresForm.tsx | 150 +++++++++++--- .../new/connection/postgres/PostgresForm.tsx | 184 +++++++++++++++--- frontend/app/new/connection/postgres/page.tsx | 4 +- frontend/components/FormError.tsx | 9 + frontend/components/ui/alert.tsx | 3 +- frontend/yup-validations/connections.ts | 10 - 7 files changed, 319 insertions(+), 62 deletions(-) create mode 100644 frontend/app/api/connections/is-connection-name-available/route.ts create mode 100644 frontend/components/FormError.tsx diff --git a/frontend/app/api/connections/is-connection-name-available/route.ts b/frontend/app/api/connections/is-connection-name-available/route.ts new file mode 100644 index 0000000000..b385682ddf --- /dev/null +++ b/frontend/app/api/connections/is-connection-name-available/route.ts @@ -0,0 +1,21 @@ +import { withNeosyncContext } from '@/api-only/neosync-context'; +import { IsConnectionNameAvailableRequest } from '@/neosync-api-client/mgmt/v1alpha1/connection_pb'; +import { RequestContext } from '@/shared'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET( + req: NextRequest, + _: RequestContext +): Promise { + const { searchParams } = new URL(req.url); + const name = searchParams.get('connectionName') ?? ''; + const accountId = searchParams.get('accountId') ?? ''; + return withNeosyncContext(async (ctx) => { + return ctx.connectionClient.isConnectionNameAvailable( + new IsConnectionNameAvailableRequest({ + connectionName: name, + accountId: accountId, + }) + ); + })(req); +} diff --git a/frontend/app/connections/[id]/components/PostgresForm.tsx b/frontend/app/connections/[id]/components/PostgresForm.tsx index 3c2de823ad..c96a1ca1cc 100644 --- a/frontend/app/connections/[id]/components/PostgresForm.tsx +++ b/frontend/app/connections/[id]/components/PostgresForm.tsx @@ -1,4 +1,10 @@ 'use client'; +import { isConnectionNameAvailable } from '@/app/new/connection/postgres/PostgresForm'; +import ButtonText from '@/components/ButtonText'; +import FormError from '@/components/FormError'; +import Spinner from '@/components/Spinner'; +import RequiredLabel from '@/components/labels/RequiredLabel'; +import { getAccount } from '@/components/providers/account-provider'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { @@ -30,12 +36,47 @@ import { SSL_MODES } from '@/yup-validations/connections'; import { yupResolver } from '@hookform/resolvers/yup'; import { ExclamationTriangleIcon, RocketIcon } from '@radix-ui/react-icons'; import { ReactElement, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import * as Yup from 'yup'; const FORM_SCHEMA = Yup.object({ - connectionName: Yup.string().required(), + connectionName: Yup.string() + .required('Connection Name is a required field') + .test( + 'validConnectionName', + 'Connection Name must be at least 3 characters long and can only include lowercase letters, numbers, and hyphens.', + async (value, context) => { + if (!value || value.length < 3) { + return false; + } + const regex = /^[a-z0-9-]+$/; + if (!regex.test(value)) { + return context.createError({ + message: + 'Connection Name can only include lowercase letters, numbers, and hyphens.', + }); + } + const account = getAccount(); + if (!account) { + return false; + } + + try { + const res = await isConnectionNameAvailable(value, account.id); + if (!res.isAvailable) { + return context.createError({ + message: 'This Connection Name is already taken.', + }); + } + return true; + } catch (error) { + return context.createError({ + message: 'Error validating name availability.', + }); + } + } + ), db: Yup.object({ host: Yup.string().required(), name: Yup.string().required(), @@ -45,7 +86,6 @@ const FORM_SCHEMA = Yup.object({ sslMode: Yup.string().optional(), }).required(), }); - type FormValues = Yup.InferType; interface Props { @@ -61,7 +101,14 @@ export default function PostgresForm(props: Props) { resolver: yupResolver(FORM_SCHEMA), defaultValues: { connectionName: '', - db: {}, + db: { + host: '', + name: '', + user: '', + pass: '', + port: 0, + sslMode: '', + }, }, values: defaultValues, }); @@ -69,8 +116,16 @@ export default function PostgresForm(props: Props) { CheckConnectionConfigResponse | undefined >(); + const [isTesting, setIsTesting] = useState(false); + async function onSubmit(values: FormValues) { try { + const checkResp = await checkPostgresConnection(values.db); + setCheckResp(checkResp); + + if (!checkResp.isConnected) { + return; + } const connectionResp = await updatePostgresConnection( connectionId, values.db @@ -81,21 +136,37 @@ export default function PostgresForm(props: Props) { onSaveFailed(err); } } + return (
- ( + render={({ field: { onChange, ...field } }) => ( - Connection Name + + + Connection Name + - The unique name of the connection. + The unique name of the connection - + { + onChange(value); + await form.trigger('connectionName'); + }} + /> + )} @@ -106,7 +177,10 @@ export default function PostgresForm(props: Props) { name="db.host" render={({ field }) => ( - Host Name + + + Host Name + The host name @@ -121,8 +195,11 @@ export default function PostgresForm(props: Props) { name="db.port" render={({ field }) => ( - Port - The port of the database + + + Database Port + + The database port. @@ -136,8 +213,11 @@ export default function PostgresForm(props: Props) { name="db.name" render={({ field }) => ( - Database Name - The name of the database + + + Database Name + + The database name @@ -151,8 +231,11 @@ export default function PostgresForm(props: Props) { name="db.user" render={({ field }) => ( - Database Username - The username + + + Database Username + + The database username @@ -166,8 +249,11 @@ export default function PostgresForm(props: Props) { name="db.pass" render={({ field }) => ( - Database Password - Password + + + Database Password + + The database password @@ -175,20 +261,22 @@ export default function PostgresForm(props: Props) { )} /> - ( - SSL Mode + + + SSL Mode + Turn on SSL Mode to use TLS for client/server encryption. + { + onChange(value); + await form.trigger('connectionName'); + }} + /> + )} @@ -121,7 +193,10 @@ export default function PostgresForm() { name="db.host" render={({ field }) => ( - Host Name + + + Host Name + The host name @@ -136,8 +211,11 @@ export default function PostgresForm() { name="db.port" render={({ field }) => ( - Port - The port of the database + + + Database Port + + The database port. @@ -151,8 +229,12 @@ export default function PostgresForm() { name="db.name" render={({ field }) => ( - Database Name - The name of the database + + {' '} + + Database Name + + The database name @@ -166,8 +248,12 @@ export default function PostgresForm() { name="db.user" render={({ field }) => ( - Database Username - The username + + {' '} + + Database Username + + The database username @@ -181,8 +267,12 @@ export default function PostgresForm() { name="db.pass" render={({ field }) => ( - Database Password - Password + + {' '} + + Database Password + + The database password @@ -196,14 +286,18 @@ export default function PostgresForm() { name="db.sslMode" render={({ field }) => ( - SSL Mode + + {' '} + + SSL Mode + Turn on SSL Mode to use TLS for client/server encryption.