-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- UI for Create Peer step where user enters configuration details - Wired validate peer API to the UI - Wired create peer API to the UI - Form validation, loading text - Written in a way where adding new peers is easy - Tested the create peer flow (including psql-ing the peer) for postgres APIs for create peer and validate peer are present in `app/api/peers` Makes progress towards #431
- Loading branch information
1 parent
bb9deb7
commit 8a434aa
Showing
17 changed files
with
481 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
|
||
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'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ConfigForm(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) | ||
} | ||
/> | ||
} | ||
/> | ||
); | ||
})} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context'; | ||
import { Dispatch, SetStateAction } from 'react'; | ||
import { pgSchema } 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; | ||
} | ||
let validationErr: string | undefined; | ||
switch (type) { | ||
case 'POSTGRES': | ||
const pgConfig = pgSchema.safeParse(config); | ||
if (!pgConfig.success) validationErr = pgConfig.error.issues[0].message; | ||
break; | ||
default: | ||
validationErr = 'Unsupported peer type ' + type; | ||
} | ||
if (validationErr) { | ||
setMessage({ ok: false, msg: validationErr }); | ||
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 ( | ||
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 }); | ||
setLoading(false); | ||
return; | ||
} else { | ||
setMessage({ ok: true, msg: 'Peer created successfully' }); | ||
router.push('/peers'); | ||
} | ||
setLoading(false); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, | ||
{ | ||
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: '', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ConfigForm 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 <ConfigForm 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> | ||
); | ||
} |
Oops, something went wrong.