Skip to content

Commit

Permalink
NEOS-337: postgres connection name validation (#603)
Browse files Browse the repository at this point in the history
  • Loading branch information
evisdrenova authored Nov 17, 2023
1 parent 1655dad commit e3eca4f
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 62 deletions.
21 changes: 21 additions & 0 deletions frontend/app/api/connections/is-connection-name-available/route.ts
Original file line number Diff line number Diff line change
@@ -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<NextResponse> {
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);
}
150 changes: 126 additions & 24 deletions frontend/app/connections/[id]/components/PostgresForm.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(),
Expand All @@ -45,7 +86,6 @@ const FORM_SCHEMA = Yup.object({
sslMode: Yup.string().optional(),
}).required(),
});

type FormValues = Yup.InferType<typeof FORM_SCHEMA>;

interface Props {
Expand All @@ -61,16 +101,31 @@ export default function PostgresForm(props: Props) {
resolver: yupResolver(FORM_SCHEMA),
defaultValues: {
connectionName: '',
db: {},
db: {
host: '',
name: '',
user: '',
pass: '',
port: 0,
sslMode: '',
},
},
values: defaultValues,
});
const [checkResp, setCheckResp] = useState<
CheckConnectionConfigResponse | undefined
>();

const [isTesting, setIsTesting] = useState<boolean>(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
Expand All @@ -81,21 +136,37 @@ export default function PostgresForm(props: Props) {
onSaveFailed(err);
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
<Controller
control={form.control}
name="connectionName"
render={({ field }) => (
render={({ field: { onChange, ...field } }) => (
<FormItem>
<FormLabel>Connection Name</FormLabel>
<FormLabel>
<RequiredLabel />
Connection Name
</FormLabel>
<FormDescription>
The unique name of the connection.
The unique name of the connection
</FormDescription>
<FormControl>
<Input disabled placeholder="Connection Name" {...field} />
<Input
placeholder="Connection Name"
{...field}
onChange={async ({ target: { value } }) => {
onChange(value);
await form.trigger('connectionName');
}}
/>
</FormControl>
<FormError
errorMessage={
form.formState.errors.connectionName?.message ?? ''
}
/>
<FormMessage />
</FormItem>
)}
Expand All @@ -106,7 +177,10 @@ export default function PostgresForm(props: Props) {
name="db.host"
render={({ field }) => (
<FormItem>
<FormLabel>Host Name</FormLabel>
<FormLabel>
<RequiredLabel />
Host Name
</FormLabel>
<FormDescription>The host name</FormDescription>
<FormControl>
<Input placeholder="Host" {...field} />
Expand All @@ -121,8 +195,11 @@ export default function PostgresForm(props: Props) {
name="db.port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormDescription>The port of the database</FormDescription>
<FormLabel>
<RequiredLabel />
Database Port
</FormLabel>
<FormDescription>The database port.</FormDescription>
<FormControl>
<Input placeholder="5432" {...field} />
</FormControl>
Expand All @@ -136,8 +213,11 @@ export default function PostgresForm(props: Props) {
name="db.name"
render={({ field }) => (
<FormItem>
<FormLabel>Database Name</FormLabel>
<FormDescription>The name of the database</FormDescription>
<FormLabel>
<RequiredLabel />
Database Name
</FormLabel>
<FormDescription>The database name</FormDescription>
<FormControl>
<Input placeholder="postgres" {...field} />
</FormControl>
Expand All @@ -151,8 +231,11 @@ export default function PostgresForm(props: Props) {
name="db.user"
render={({ field }) => (
<FormItem>
<FormLabel>Database Username</FormLabel>
<FormDescription>The username</FormDescription>
<FormLabel>
<RequiredLabel />
Database Username
</FormLabel>
<FormDescription>The database username</FormDescription>
<FormControl>
<Input placeholder="postgres" {...field} />
</FormControl>
Expand All @@ -166,29 +249,34 @@ export default function PostgresForm(props: Props) {
name="db.pass"
render={({ field }) => (
<FormItem>
<FormLabel>Database Password</FormLabel>
<FormDescription>Password</FormDescription>
<FormLabel>
<RequiredLabel />
Database Password
</FormLabel>
<FormDescription>The database password</FormDescription>
<FormControl>
<Input placeholder="postgres" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="db.sslMode"
render={({ field }) => (
<FormItem>
<FormLabel>SSL Mode</FormLabel>
<FormLabel>
<RequiredLabel />
SSL Mode
</FormLabel>
<FormDescription>
Turn on SSL Mode to use TLS for client/server encryption.
</FormDescription>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Source" />
<SelectValue placeholder={defaultValues.db.sslMode ?? ''} />
</SelectTrigger>
<SelectContent>
{SSL_MODES.map((mode) => (
Expand All @@ -210,10 +298,14 @@ export default function PostgresForm(props: Props) {
<TestConnectionResult resp={checkResp} />
<div className="flex flex-row gap-3 justify-between">
<Button
variant="outline"
disabled={!form.formState.isValid}
onClick={async () => {
setIsTesting(true);
try {
const resp = await checkPostgresConnection(form.getValues().db);
setCheckResp(resp);
setIsTesting(false);
} catch (err) {
setCheckResp(
new CheckConnectionConfigResponse({
Expand All @@ -222,15 +314,25 @@ export default function PostgresForm(props: Props) {
err instanceof Error ? err.message : 'unknown error',
})
);
setIsTesting(false);
}
}}
type="button"
variant="secondary"
>
Test Connection
<ButtonText
leftIcon={
isTesting ? <Spinner className="text-black" /> : <div></div>
}
text="Test Connection"
/>
</Button>

<Button type="submit">Submit</Button>
<Button type="submit" disabled={!form.formState.isValid}>
<ButtonText
leftIcon={form.formState.isSubmitting ? <Spinner /> : <div></div>}
text="submit"
/>
</Button>
</div>
</form>
</Form>
Expand Down
Loading

0 comments on commit e3eca4f

Please sign in to comment.