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 { PlusOutlined } from "@ant-design/icons";
import useSWR from "swr";
import { APIResponse } from "@lib/typings";
import { APIResponse, WgServer } from "@lib/typings";
import useSWRMutation from "swr/mutation";
import { useRouter } from "next/router";
import { MiddleEllipsis } from "@ui/MiddleEllipsis";
import StatusBadge from "@ui/StatusBadge";
import { SmartModalRef } from "@ui/Modal/SmartModal";
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) {
@ -31,7 +34,7 @@ export default function ServerPage(props: PageProps) {
const createClientRef = React.useRef<SmartModalRef | null>(null)
const { data, error, isLoading } = useSWR(
const { data, error, isLoading, mutate: refresh } = useSWR(
`/api/wireguard/${props.serverId}`,
async (url: string) => {
const resp = await fetch(url, {
@ -52,7 +55,9 @@ export default function ServerPage(props: PageProps) {
const { isMutating: isChangingStatus, trigger: changeStatus } = useSWRMutation(
`/api/wireguard/${props.serverId}`,
async (url: string, { arg }: { arg: string }) => {
async (url: string, { arg }: {
arg: string
}) => {
const resp = await fetch(url, {
method: arg === 'remove' ? 'DELETE' : 'PUT',
headers: {
@ -71,10 +76,8 @@ export default function ServerPage(props: PageProps) {
return true
},
{
onSuccess: () => {
},
onError: () => {
}
onSuccess: async () => await refresh(),
onError: async () => await refresh(),
}
)
@ -82,7 +85,11 @@ export default function ServerPage(props: PageProps) {
return (
<BasePage>
<CreateClientModal ref={createClientRef} />
<CreateClientModal
ref={createClientRef}
serverId={props.serverId}
refreshTrigger={() => refresh()}
/>
<PageRouter
route={[
{ title: data ? data.name.toString() : 'LOADING...' }
@ -158,16 +165,46 @@ export default function ServerPage(props: PageProps) {
<Card
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>
)}
>
<div className={'flex flex-col items-center justify-center gap-y-4 py-8'}>
<p className={'text-gray-400 text-md'}>
There are no clients yet!
</p>
<Button type={'primary'} icon={<PlusOutlined />} onClick={() => createClientRef.current?.open()}>
Add a client
</Button>
</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'}>
<p className={'text-gray-400 text-md'}>
There are no clients yet!
</p>
<Button type={'primary'} icon={<PlusOutlined />} onClick={() => createClientRef.current?.open()}>
Add a client
</Button>
</div>
)}
</Card>
</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: {
label: string
children: React.ReactNode

View File

@ -13,17 +13,20 @@ import CreateServerModal from "@ui/Modal/CreateServerModal";
import StatusBadge from "@ui/StatusBadge";
export default function Home() {
const { data, error, isLoading } = useSWR('/api/wireguard/listServers', async (url: string) => {
const resp = await fetch(url, {
method: 'GET',
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 data.result
})
const { data, error, isLoading, mutate } = useSWR(
'/api/wireguard/listServers',
async (url: string) => {
const resp = await fetch(url, {
method: 'GET',
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 data.result
}
)
const createServerRef = React.useRef<SmartModalRef | null>(null)
return (
<BasePage>
@ -35,7 +38,7 @@ export default function Home() {
</Button>
)}
</PageRouter>
<CreateServerModal ref={createServerRef} />
<CreateServerModal ref={createServerRef} refreshTrigger={() => mutate()} />
<div className={'space-y-4'}>
{error ? (
<Card className={'flex items-center justify-center p-4'}>
@ -51,7 +54,7 @@ export default function Home() {
title={<span> Servers </span>}
>
<List>
{data.map((s) => <Server {...s} />)}
{data.map((s) => <Server key={s.id} {...s} />)}
</List>
</Card>
) : (

View File

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

View File

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