Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI for Create Postgres Peer #456

Merged
merged 12 commits into from
Oct 3, 2023
54 changes: 54 additions & 0 deletions ui/app/api/peers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { PeerConfig } from '@/app/peers/create/configuration/types';
import { DBType, Peer, PostgresConfig } from '@/grpc_generated/peers';
import {
CreatePeerRequest,
CreatePeerResponse,
CreatePeerStatus,
ValidatePeerRequest,
ValidatePeerResponse,
ValidatePeerStatus,
} from '@/grpc_generated/route';
import { GetFlowServiceClientFromEnv } from '@/rpc/rpc';

export const constructPeer = (
name: string,
type: string,
config: PeerConfig
): Peer | undefined => {
switch (type) {
case 'POSTGRES':
return {
name,
type: DBType.POSTGRES,
postgresConfig: config as PostgresConfig,
};
default:
return;
}
};

export async function POST(request: Request) {
const body = await request.json();
const { name, type, config, mode } = body;
const flowServiceClient = GetFlowServiceClientFromEnv();
const peer = constructPeer(name, type, config);
if (mode === 'validate') {
const validateReq: ValidatePeerRequest = { peer };
const validateStatus: ValidatePeerResponse =
await flowServiceClient.validatePeer(validateReq);
if (validateStatus.status === ValidatePeerStatus.INVALID) {
return new Response(validateStatus.message);
} else if (validateStatus.status === ValidatePeerStatus.VALID) {
return new Response('valid');
}
} else if (mode === 'create') {
const req: CreatePeerRequest = { peer };
const createStatus: CreatePeerResponse =
await flowServiceClient.createPeer(req);
if (createStatus.status === CreatePeerStatus.FAILED) {
return new Response(createStatus.message);
} else if (createStatus.status === CreatePeerStatus.CREATED) {
return new Response('created');
} else return new Response('status of peer creation is unknown');
} else return new Response('mode of peer creation is unknown');
}
40 changes: 40 additions & 0 deletions ui/app/peers/create/configuration/configForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';
import { Label } from '@/lib/Label';
import { RowWithTextField } from '@/lib/Layout';
import { TextField } from '@/lib/TextField';
import { PeerSetter } from './types';

interface Setting {
label: string;
stateHandler: (value: string, setter: PeerSetter) => void;
type?: string;
}

interface ConfigProps {
settings: Setting[];
setter: PeerSetter;
}

export default function PgConfig(props: ConfigProps) {
return (
<>
{props.settings.map((setting, id) => {
return (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think making the form config-driven is a good idea. But we maybe need to revisit things like how default values can be set. For Example port default value is 5432 in config state variable, but we are not using it anywhere. perhaps a crude way to do it could be as shown below by adding a default value


export default function PgConfig(props: ConfigProps) {
  return (
    <>
      {props.settings.map((setting, id) => {
        let _defaultValue = setting.type === "number" && setting.label === "Port" ? "5432" : "";
        return (
          <RowWithTextField
            key={id}
            label={<Label as='label'>{setting.label}</Label>}
            action={
              <TextField
                variant='simple'
                type={setting.type}
                defaultValue={_defaultValue}
                onChange={(e) =>
                  setting.stateHandler(e.target.value, props.setter)
                }
              />
            }
          />
        );
      })}
    </>
  );

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default values are not really meant to be used in UI, but rather because protobuf-grpc doesn't seem to allow undefined values so we need dummies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also thanks for the catch: the config value for the state here should be made generic to peer. fixed.

<RowWithTextField
key={id}
label={<Label as='label'>{setting.label}</Label>}
action={
<TextField
variant='simple'
type={setting.type}
onChange={(e) =>
setting.stateHandler(e.target.value, props.setter)
}
/>
}
/>
);
})}
</>
);
}
85 changes: 85 additions & 0 deletions ui/app/peers/create/configuration/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context';
import { Dispatch, SetStateAction } from 'react';
import { checkFormFields } from './schema';
import { PeerConfig } from './types';

// Frontend form validation
const validateFields = (
type: string,
config: PeerConfig,
setMessage: Dispatch<SetStateAction<{ ok: boolean; msg: string }>>,
name?: string
): boolean => {
if (!name) {
setMessage({ ok: false, msg: 'Peer name is required' });
return false;
}
const validity = checkFormFields(type, config);
if (validity.success === false) {
setMessage({ ok: false, msg: validity.error.message });
return false;
} else setMessage({ ok: true, msg: '' });
return true;
};

// API call to validate peer
export const handleValidate = async (
type: string,
config: PeerConfig,
setMessage: Dispatch<SetStateAction<{ ok: boolean; msg: string }>>,
setLoading: Dispatch<SetStateAction<boolean>>,
name?: string
) => {
const isValid = validateFields(type, config, setMessage, name);
if (!isValid) return;
setLoading(true);
const statusMessage = await fetch('/api/peers/', {
method: 'POST',
body: JSON.stringify({
name,
type,
config,
mode: 'validate',
}),
}).then((res) => res.text());
if (statusMessage !== 'valid') {
setMessage({ ok: false, msg: statusMessage });
setLoading(false);
return;
} else {
setMessage({ ok: true, msg: 'Peer is valid' });
}
setLoading(false);
};

// API call to create peer
export const handleCreate = async (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets discuss the need for this

type: string,
config: PeerConfig,
setMessage: Dispatch<SetStateAction<{ ok: boolean; msg: string }>>,
setLoading: Dispatch<SetStateAction<boolean>>,
router: AppRouterInstance,
name?: string
) => {
let isValid = validateFields(type, config, setMessage, name);
if (!isValid) return;
setLoading(true);
const statusMessage = await fetch('/api/peers/', {
method: 'POST',
body: JSON.stringify({
name,
type,
config,
mode: 'create',
}),
}).then((res) => res.text());
if (statusMessage !== 'created') {
setMessage({ ok: false, msg: statusMessage });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to show an error message on UI? Coz even for 500 code I am not seeing any error message?

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same issue for validate button

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Joban. Couldn't reproduce this. Maybe we could investigate separately if this persists

setLoading(false);
return;
} else {
setMessage({ ok: true, msg: 'Peer created successfully' });
router.push('/peers');
}
setLoading(false);
};
10 changes: 10 additions & 0 deletions ui/app/peers/create/configuration/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { blankPostgresSetting } from './pg';

export const getBlankSetting = (dbType: string) => {
switch (dbType) {
case 'POSTGRES':
return blankPostgresSetting;
default:
return blankPostgresSetting;
}
};
45 changes: 45 additions & 0 deletions ui/app/peers/create/configuration/helpers/pg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PeerSetter } from '../types';

export const postgresSetting = [
{
label: 'Host',
stateHandler: (value: string, setter: PeerSetter) =>
setter((curr) => ({ ...curr, host: value })),
},
{
label: 'Port',
stateHandler: (value: string, setter: PeerSetter) =>
setter((curr) => ({ ...curr, port: parseInt(value, 10) })),
type: 'number', // type for textfield
Copy link

@Jobandeep2417 Jobandeep2417 Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Port should have a min max range, it should not allow negative values. Although we are validating it in validation helper, thought of adding for better UI experience

image

The way PgConfig and Config form components are setup there is no way to send this info from the settings props.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could prevent typing of non-numbers but right now I personally think it's fine. Happy to discuss though

},
{
label: 'User',
stateHandler: (value: string, setter: PeerSetter) =>
setter((curr) => ({ ...curr, user: value })),
},
{
label: 'Password',
stateHandler: (value: string, setter: PeerSetter) =>
setter((curr) => ({ ...curr, password: value })),
type: 'password',
},
{
label: 'Database',
stateHandler: (value: string, setter: PeerSetter) =>
setter((curr) => ({ ...curr, database: value })),
},
{
label: 'Transaction Snapshot',
stateHandler: (value: string, setter: PeerSetter) =>
setter((curr) => ({ ...curr, transactionSnapshot: value })),
},
];

export const blankPostgresSetting = {
host: '',
port: 5432,
user: '',
password: '',
database: '',
transactionSnapshot: '',
};
110 changes: 110 additions & 0 deletions ui/app/peers/create/configuration/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client';
import { Button } from '@/lib/Button';
import { ButtonGroup } from '@/lib/ButtonGroup';
import { Label } from '@/lib/Label';
import { LayoutMain, RowWithTextField } from '@/lib/Layout';
import { Panel } from '@/lib/Panel';
import { TextField } from '@/lib/TextField';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import PgConfig from './configForm';
import { handleCreate, handleValidate } from './handlers';
import { getBlankSetting } from './helpers/common';
import { postgresSetting } from './helpers/pg';
import { PeerConfig } from './types';
export default function CreateConfig() {
const searchParams = useSearchParams();
const router = useRouter();
const dbType = searchParams.get('dbtype') || '';
const blankSetting = getBlankSetting(dbType);
const [name, setName] = useState<string>('');
const [config, setConfig] = useState<PeerConfig>(blankSetting);
const [formMessage, setFormMessage] = useState<{ ok: boolean; msg: string }>({
ok: true,
msg: '',
});
const [loading, setLoading] = useState<boolean>(false);
const configComponentMap = (dbType: string) => {
switch (dbType) {
case 'POSTGRES':
return <PgConfig settings={postgresSetting} setter={setConfig} />;
default:
return <></>;
}
};

return (
<LayoutMain alignSelf='center' justifySelf='center' width='xxLarge'>
<Panel>
<Label variant='title3'>New peer</Label>
<Label colorName='lowContrast'>Set up a new peer.</Label>
</Panel>
<Panel>
<Label colorName='lowContrast' variant='subheadline'>
Configuration
</Label>
<RowWithTextField
label={<Label as='label'>Name</Label>}
action={
<TextField
variant='simple'
onChange={(e) => setName(e.target.value)}
/>
}
/>
{dbType && configComponentMap(dbType)}
</Panel>
<Panel>
<ButtonGroup>
<Button as={Link} href='/peers/create'>
Back
</Button>
<Button
style={{ backgroundColor: 'gold' }}
onClick={() =>
handleValidate(dbType, config, setFormMessage, setLoading, name)
}
>
Validate
</Button>
<Button
variant='normalSolid'
onClick={() =>
handleCreate(
dbType,
config,
setFormMessage,
setLoading,
router,
name
)
}
>
Create
</Button>
</ButtonGroup>
<Panel>
{loading && (
<Label
colorName='lowContrast'
colorSet='base'
variant='subheadline'
>
Validating...
</Label>
)}
{!loading && formMessage.msg.length > 0 && (
<Label
colorName='lowContrast'
colorSet={formMessage.ok ? 'positive' : 'destructive'}
variant='subheadline'
>
{formMessage.msg}
</Label>
)}
</Panel>
</Panel>
</LayoutMain>
);
}
20 changes: 20 additions & 0 deletions ui/app/peers/create/configuration/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as z from 'zod';
import { PeerConfig } from './types';

const pgSchema = z.object({
host: z.string().nonempty().max(255),
port: z.number().int().min(1).max(65535),
database: z.string().min(1).max(100),
user: z.string().min(1).max(64),
password: z.string().min(1).max(100),
transactionSnapshot: z.string().max(100).optional(),
});

export const checkFormFields = (peerType: string, config: PeerConfig) => {
switch (peerType) {
case 'POSTGRES':
return pgSchema.safeParse(config);
default:
return { success: false, error: { message: 'Peer type not recognized' } };
}
};
5 changes: 5 additions & 0 deletions ui/app/peers/create/configuration/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PostgresConfig } from '@/grpc_generated/peers';
import { Dispatch, SetStateAction } from 'react';

export type PeerConfig = PostgresConfig;
export type PeerSetter = Dispatch<SetStateAction<PeerConfig>>;
Loading
Loading