mirror of
https://github.com/wireadmin/wireadmin
synced 2025-02-26 05:48:44 +00:00
updates ui and fixes a few minor issues
This commit is contained in:
parent
0b18182103
commit
34050ed173
@ -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
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
39
src/ui/Modal/QRCodeModal.tsx
Normal file
39
src/ui/Modal/QRCodeModal.tsx
Normal 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
|
||||
|
Loading…
Reference in New Issue
Block a user