updates ui and fixes a few minor issues

This commit is contained in:
Shahrad Elahi 2023-09-25 15:29:10 +03:30
parent 0b18182103
commit 34050ed173
6 changed files with 248 additions and 50 deletions

View File

@ -4,13 +4,16 @@ import PageRouter from "@ui/pages/PageRouter";
import React from "react"; import React from "react";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import useSWR from "swr"; import useSWR from "swr";
import { APIResponse } from "@lib/typings"; import { APIResponse, WgServer } from "@lib/typings";
import useSWRMutation from "swr/mutation"; import useSWRMutation from "swr/mutation";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MiddleEllipsis } from "@ui/MiddleEllipsis"; import { MiddleEllipsis } from "@ui/MiddleEllipsis";
import StatusBadge from "@ui/StatusBadge"; import StatusBadge from "@ui/StatusBadge";
import { SmartModalRef } from "@ui/Modal/SmartModal"; import { SmartModalRef } from "@ui/Modal/SmartModal";
import CreateClientModal from "@ui/Modal/CreateClientModal"; import CreateClientModal from "@ui/Modal/CreateClientModal";
import { twMerge } from "tailwind-merge";
import QRCodeModal from "@ui/Modal/QRCodeModal";
import { getPeerConf } from "@lib/wireguard-utils";
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
@ -31,7 +34,7 @@ export default function ServerPage(props: PageProps) {
const createClientRef = React.useRef<SmartModalRef | null>(null) const createClientRef = React.useRef<SmartModalRef | null>(null)
const { data, error, isLoading } = useSWR( const { data, error, isLoading, mutate: refresh } = useSWR(
`/api/wireguard/${props.serverId}`, `/api/wireguard/${props.serverId}`,
async (url: string) => { async (url: string) => {
const resp = await fetch(url, { const resp = await fetch(url, {
@ -52,7 +55,9 @@ export default function ServerPage(props: PageProps) {
const { isMutating: isChangingStatus, trigger: changeStatus } = useSWRMutation( const { isMutating: isChangingStatus, trigger: changeStatus } = useSWRMutation(
`/api/wireguard/${props.serverId}`, `/api/wireguard/${props.serverId}`,
async (url: string, { arg }: { arg: string }) => { async (url: string, { arg }: {
arg: string
}) => {
const resp = await fetch(url, { const resp = await fetch(url, {
method: arg === 'remove' ? 'DELETE' : 'PUT', method: arg === 'remove' ? 'DELETE' : 'PUT',
headers: { headers: {
@ -71,10 +76,8 @@ export default function ServerPage(props: PageProps) {
return true return true
}, },
{ {
onSuccess: () => { onSuccess: async () => await refresh(),
}, onError: async () => await refresh(),
onError: () => {
}
} }
) )
@ -82,7 +85,11 @@ export default function ServerPage(props: PageProps) {
return ( return (
<BasePage> <BasePage>
<CreateClientModal ref={createClientRef} /> <CreateClientModal
ref={createClientRef}
serverId={props.serverId}
refreshTrigger={() => refresh()}
/>
<PageRouter <PageRouter
route={[ route={[
{ title: data ? data.name.toString() : 'LOADING...' } { title: data ? data.name.toString() : 'LOADING...' }
@ -158,8 +165,37 @@ export default function ServerPage(props: PageProps) {
<Card <Card
className={'[&>.ant-card-body]:p-0'} className={'[&>.ant-card-body]:p-0'}
title={<span> Clients </span>} title={(
<div className={'flex items-center justify-between'}>
<span> Clients </span>
{data && data.peers.length > 0 && (
<div>
<Button
type={'primary'}
icon={<PlusOutlined />}
onClick={() => createClientRef.current?.open()}
> >
Add a client
</Button>
</div>
)}
</div>
)}
>
{data && data.peers.length > 0 ? (
<List>
{data.peers.map((s) => (
<Client
key={s.id}
{...s}
serverId={props.serverId}
listenPort={data?.listen}
refreshTrigger={() => refresh()}
/>
))}
</List>
) : (
<div className={'flex flex-col items-center justify-center gap-y-4 py-8'}> <div className={'flex flex-col items-center justify-center gap-y-4 py-8'}>
<p className={'text-gray-400 text-md'}> <p className={'text-gray-400 text-md'}>
There are no clients yet! There are no clients yet!
@ -168,6 +204,7 @@ export default function ServerPage(props: PageProps) {
Add a client Add a client
</Button> </Button>
</div> </div>
)}
</Card> </Card>
</div> </div>
)} )}
@ -175,6 +212,114 @@ export default function ServerPage(props: PageProps) {
); );
} }
type Peer = WgServer['peers'][0]
interface ClientProps extends Peer {
serverId: string
listenPort: number
refreshTrigger: () => void
}
function Client(props: ClientProps) {
const qrcodeRef = React.useRef<SmartModalRef | null>(null)
const [ conf, setConf ] = React.useState<string | null>(null)
React.useEffect(() => {
setConf(getPeerConf({
...props,
port: props.listenPort
}))
console.log('conf', conf)
}, [ props ])
const { isMutating: removingClient, trigger: removeClient } = useSWRMutation(
`/api/wireguard/${props.serverId}/${props.id}`,
async (url: string,) => {
const resp = await fetch(url, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
})
const data = await resp.json() as APIResponse<any>
if (!data.ok) throw new Error('Server responded with error status')
return true
},
{
onSuccess: () => props.refreshTrigger(),
onError: () => props.refreshTrigger()
}
)
return (
<List.Item className={'flex items-center justify-between p-4'}>
<QRCodeModal ref={qrcodeRef} content={conf?.trim() || 'null'} />
<div className={'w-full grid grid-cols-12 items-center gap-x-2'}>
<div className={'col-span-1 rounded-full bg-gray-200 aspect-square'} />
<span className={'font-medium col-span-4'}> {props.name} </span>
</div>
<div className={'flex items-center justify-center gap-x-3'}>
{/* QRCode */}
<ClientBaseButton disabled={removingClient} onClick={() => {
qrcodeRef.current?.open()
}}>
<i className={'fal text-neutral-700 group-hover:text-primary fa-qrcode'} />
</ClientBaseButton>
{/* Download */}
<ClientBaseButton disabled={removingClient} onClick={() => {
if (!conf) {
console.error('conf is null')
return
}
console.log('conf', conf)
// create a blob
const blob = new Blob([ conf ], { type: 'text/plain' })
// create a link
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `${props.name}.conf`
// click the link
link.click()
// remove the link
link.remove()
}}>
<i className={'fal text-neutral-700 group-hover:text-primary fa-download'} />
</ClientBaseButton>
{/* Remove */}
<ClientBaseButton loading={removingClient} onClick={() => removeClient()}>
<i className={'fal text-neutral-700 group-hover:text-primary text-lg fa-trash-can'} />
</ClientBaseButton>
</div>
</List.Item>
)
}
function ClientBaseButton(props: {
onClick: () => void
loading?: boolean
disabled?: boolean
children: React.ReactNode
}) {
return (
<div
className={twMerge(
'group flex items-center justify-center w-10 aspect-square rounded-md',
'bg-gray-200/80 hover:bg-gray-100',
'border border-transparent hover:border-primary',
'transition-colors duration-200 ease-in-out',
'cursor-pointer',
props.disabled && 'opacity-50 cursor-not-allowed',
props.loading && 'animate-pulse'
)}
onClick={props.onClick}
>
{props.children}
</div>
)
}
function Row(props: { function Row(props: {
label: string label: string
children: React.ReactNode children: React.ReactNode

View File

@ -13,7 +13,9 @@ import CreateServerModal from "@ui/Modal/CreateServerModal";
import StatusBadge from "@ui/StatusBadge"; import StatusBadge from "@ui/StatusBadge";
export default function Home() { export default function Home() {
const { data, error, isLoading } = useSWR('/api/wireguard/listServers', async (url: string) => { const { data, error, isLoading, mutate } = useSWR(
'/api/wireguard/listServers',
async (url: string) => {
const resp = await fetch(url, { const resp = await fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
@ -23,7 +25,8 @@ export default function Home() {
const data = await resp.json() as APIResponse<any> const data = await resp.json() as APIResponse<any>
if (!data.ok) throw new Error('Server responded with error status') if (!data.ok) throw new Error('Server responded with error status')
return data.result return data.result
}) }
)
const createServerRef = React.useRef<SmartModalRef | null>(null) const createServerRef = React.useRef<SmartModalRef | null>(null)
return ( return (
<BasePage> <BasePage>
@ -35,7 +38,7 @@ export default function Home() {
</Button> </Button>
)} )}
</PageRouter> </PageRouter>
<CreateServerModal ref={createServerRef} /> <CreateServerModal ref={createServerRef} refreshTrigger={() => mutate()} />
<div className={'space-y-4'}> <div className={'space-y-4'}>
{error ? ( {error ? (
<Card className={'flex items-center justify-center p-4'}> <Card className={'flex items-center justify-center p-4'}>
@ -51,7 +54,7 @@ export default function Home() {
title={<span> Servers </span>} title={<span> Servers </span>}
> >
<List> <List>
{data.map((s) => <Server {...s} />)} {data.map((s) => <Server key={s.id} {...s} />)}
</List> </List>
</Card> </Card>
) : ( ) : (

View File

@ -19,8 +19,8 @@ export function MiddleEllipsis(props: MiddleEllipsisProps) {
}, [ maxLength ]) }, [ maxLength ])
const [ left, right ] = React.useMemo(() => { const [ left, right ] = React.useMemo(() => {
if (content.length <= maxLength) return [ content, '' ] if (content?.length <= maxLength) return [ content, '' ]
return [ content.slice(0, leftL), content.slice(content.length - rightL) ] return [ content.slice(0, leftL), content.slice(content?.length - rightL) ]
}, [ content, leftL, rightL ]) }, [ content, leftL, rightL ])
return ( return (

View File

@ -4,13 +4,19 @@ import { Button, Form, Input, notification } from "antd";
import { z } from "zod"; import { z } from "zod";
import { APIResponse } from "@lib/typings"; import { APIResponse } from "@lib/typings";
import useSWRMutation from "swr/mutation"; import useSWRMutation from "swr/mutation";
import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from "@lib/schemas/WireGuard"; import { NameSchema } from "@lib/schemas/WireGuard";
import { zodErrorMessage } from "@lib/zod"; import { zodErrorMessage } from "@lib/zod";
type CreateClientModalProps = {
serverId: string
refreshTrigger: () => void
}
const CreateClientModal = React.forwardRef< const CreateClientModal = React.forwardRef<
SmartModalRef, SmartModalRef,
{} CreateClientModalProps
>((_, ref) => { >((props, ref) => {
const [ notificationApi, contextHolder ] = notification.useNotification() const [ notificationApi, contextHolder ] = notification.useNotification()
@ -25,7 +31,7 @@ const CreateClientModal = React.forwardRef<
}, []) }, [])
const { isMutating, trigger } = useSWRMutation( const { isMutating, trigger } = useSWRMutation(
'/api/wireguard/createClient', `/api/wireguard/${props.serverId}/createClient`,
async (url: string, { arg }: { arg: FormValues }) => { async (url: string, { arg }: { arg: FormValues }) => {
const resp = await fetch(url, { const resp = await fetch(url, {
method: 'POST', method: 'POST',
@ -35,11 +41,12 @@ const CreateClientModal = React.forwardRef<
body: JSON.stringify(arg) body: JSON.stringify(arg)
}) })
const data = await resp.json() as APIResponse<any> const data = await resp.json() as APIResponse<any>
if (!data.ok) throw new Error('Client responded with error status') if (!data.ok) throw new Error('Server responded with error status')
return data.result return true
}, },
{ {
onSuccess: () => { onSuccess: () => {
props.refreshTrigger()
notificationApi.success({ notificationApi.success({
message: 'Success', message: 'Success',
description: ( description: (
@ -52,6 +59,7 @@ const CreateClientModal = React.forwardRef<
form?.resetFields() form?.resetFields()
}, },
onError: () => { onError: () => {
props.refreshTrigger()
notificationApi.error({ notificationApi.error({
message: 'Error', message: 'Error',
description: 'Failed to create Client' description: 'Failed to create Client'
@ -98,7 +106,7 @@ const CreateClientModal = React.forwardRef<
<Input placeholder={'Unicorn 🦄'} /> <Input placeholder={'Unicorn 🦄'} />
</Form.Item> </Form.Item>
<Button type={'primary'} htmlType={'submit'} className={'w-full'}> <Button type={'primary'} htmlType={'submit'} className={'w-full'} loading={isMutating}>
Create Create
</Button> </Button>
@ -110,12 +118,7 @@ const CreateClientModal = React.forwardRef<
export default CreateClientModal export default CreateClientModal
const FormSchema = z.object({ const FormSchema = z.object({
name: NameSchema, name: NameSchema
address: AddressSchema,
port: PortSchema,
type: TypeSchema,
dns: DnsSchema,
mtu: MtuSchema
}) })
type FormValues = z.infer<typeof FormSchema> type FormValues = z.infer<typeof FormSchema>

View File

@ -9,7 +9,9 @@ import { isPrivateIP } from "@lib/utils";
import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from "@lib/schemas/WireGuard"; import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from "@lib/schemas/WireGuard";
import { zodErrorMessage } from "@lib/zod"; import { zodErrorMessage } from "@lib/zod";
export type CreateServerModalProps = {} type CreateServerModalProps = {
refreshTrigger: () => void
}
const CreateServerModal = React.forwardRef< const CreateServerModal = React.forwardRef<
SmartModalRef, SmartModalRef,
@ -42,6 +44,7 @@ const CreateServerModal = React.forwardRef<
}) })
const data = await resp.json() as APIResponse<any> const data = await resp.json() as APIResponse<any>
if (!data.ok) throw new Error('Server responded with error status') if (!data.ok) throw new Error('Server responded with error status')
props.refreshTrigger()
return data.result return data.result
}, },
{ {
@ -146,7 +149,7 @@ const CreateServerModal = React.forwardRef<
defaultValue={type} defaultValue={type}
onChange={(v) => setType(v as any)} onChange={(v) => setType(v as any)}
options={[ options={[
{ label: 'Direct', value: 'default', icon: <i className={'fal fa-arrows-left-right-to-line'} /> }, { label: 'Direct', value: 'direct', icon: <i className={'fal fa-arrows-left-right-to-line'} /> },
{ label: 'Tor', value: 'tor', icon: <TorOnion />, disabled: true } { label: 'Tor', value: 'tor', icon: <TorOnion />, disabled: true }
]} ]}
/> />
@ -156,7 +159,7 @@ const CreateServerModal = React.forwardRef<
{ {
validator: (_, value) => { validator: (_, value) => {
if (!value) return Promise.resolve() if (!value) return Promise.resolve()
const res = NameSchema.safeParse(value) const res = DnsSchema.safeParse(value)
if (res.success) return Promise.resolve() if (res.success) return Promise.resolve()
return Promise.reject(zodErrorMessage(res.error)[0]) return Promise.reject(zodErrorMessage(res.error)[0])
} }
@ -169,7 +172,7 @@ const CreateServerModal = React.forwardRef<
{ {
validator: (_, value) => { validator: (_, value) => {
if (!value) return Promise.resolve() if (!value) return Promise.resolve()
const res = NameSchema.safeParse(value) const res = MtuSchema.safeParse(value)
if (res.success) return Promise.resolve() if (res.success) return Promise.resolve()
return Promise.reject(zodErrorMessage(res.error)[0]) return Promise.reject(zodErrorMessage(res.error)[0])
} }
@ -178,7 +181,12 @@ const CreateServerModal = React.forwardRef<
<Input placeholder={'1420'} /> <Input placeholder={'1420'} />
</Form.Item> </Form.Item>
<Button type={'primary'} htmlType={'submit'} className={'w-full'}> <Button
type={'primary'}
htmlType={'submit'}
className={'w-full'}
loading={isMutating}
>
Create Create
</Button> </Button>

View File

@ -0,0 +1,39 @@
import React from "react";
import SmartModal, { SmartModalRef } from "@ui/Modal/SmartModal";
import { QRCodeCanvas } from "qrcode.react";
import { SHA1 } from "crypto-js";
type QRCodeModalProps = {
content: string
}
const QRCodeModal = React.forwardRef<
SmartModalRef,
QRCodeModalProps
>((props, ref) => {
const innerRef = React.useRef<SmartModalRef | null>(null)
React.useImperativeHandle(ref, () => innerRef.current as SmartModalRef)
return (
<SmartModal
key={SHA1(props.content || '').toString()}
ref={innerRef}
title={null}
footer={null}
className={'flex items-center justify-center'}
>
<div className={'flex items-center justify-center p-5'}>
<QRCodeCanvas
size={256}
level={'M'}
value={props.content || ''}
/>
</div>
</SmartModal>
)
})
export default QRCodeModal