-
Notifications
You must be signed in to change notification settings - Fork 97
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
Changes from 10 commits
5ba7047
834f9b7
87162c5
317f11b
d2d08f7
a203ff4
e256f21
e153f18
92d72db
d4aed5f
5580b73
947992e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'); | ||
} |
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 ( | ||
<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) | ||
} | ||
/> | ||
} | ||
/> | ||
); | ||
})} | ||
</> | ||
); | ||
} |
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 ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same issue for validate button There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}; |
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; | ||
} | ||
}; |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: '', | ||
}; |
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> | ||
); | ||
} |
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' } }; | ||
} | ||
}; |
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>>; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.