mirror of
https://github.com/wireadmin/wireadmin
synced 2025-06-26 18:28:06 +00:00
Adds an ability to update Server
or Client
name
This commit is contained in:
parent
de5f35eec5
commit
379d715ecd
18
src/lib/form-rules.ts
Normal file
18
src/lib/form-rules.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NameSchema } from "@lib/schemas/WireGuard";
|
||||||
|
import { zodErrorMessage } from "@lib/zod";
|
||||||
|
import type { Rule } from "rc-field-form/lib/interface";
|
||||||
|
|
||||||
|
export const RLS_NAME_INPUT: Rule[] = [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Name is required'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (_, value) => {
|
||||||
|
if (!value) return Promise.resolve()
|
||||||
|
const res = NameSchema.safeParse(value)
|
||||||
|
if (res.success) return Promise.resolve()
|
||||||
|
return Promise.reject(zodErrorMessage(res.error)[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
42
src/lib/swr-fetch.ts
Normal file
42
src/lib/swr-fetch.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { APIResponse, Peer, PeerSchema, WgServer, WgServerSchema } from "@lib/typings";
|
||||||
|
import { zodErrorMessage } from "@lib/zod";
|
||||||
|
|
||||||
|
export const UPDATE_SERVER = async (url: string, { arg }: { arg: Partial<WgServer> }) => {
|
||||||
|
const parsed = WgServerSchema.partial().safeParse(arg)
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error('invalid server schema', zodErrorMessage(parsed.error))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(arg)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await resp.json() as APIResponse<any>
|
||||||
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UPDATE_CLIENT = async (url: string, { arg }: { arg: Partial<Peer> }) => {
|
||||||
|
const parsed = PeerSchema
|
||||||
|
.partial()
|
||||||
|
.safeParse(arg)
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error('invalid peer schema', zodErrorMessage(parsed.error))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(arg)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await resp.json() as APIResponse<any>
|
||||||
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -30,6 +30,17 @@ const WgPeerSchema = z.object({
|
|||||||
|
|
||||||
export type WgPeer = z.infer<typeof WgPeerSchema>
|
export type WgPeer = z.infer<typeof WgPeerSchema>
|
||||||
|
|
||||||
|
export const PeerSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: NameSchema,
|
||||||
|
preSharedKey: z.string().nullable(),
|
||||||
|
allowedIps: z.string().regex(IPV4_REGEX),
|
||||||
|
persistentKeepalive: z.number().nullable(),
|
||||||
|
})
|
||||||
|
.merge(WgKeySchema)
|
||||||
|
|
||||||
|
export type Peer = z.infer<typeof PeerSchema>
|
||||||
|
|
||||||
export const WgServerSchema = z.object({
|
export const WgServerSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
confId: z.number(),
|
confId: z.number(),
|
||||||
@ -42,16 +53,7 @@ export const WgServerSchema = z.object({
|
|||||||
preDown: z.string().nullable(),
|
preDown: z.string().nullable(),
|
||||||
postDown: z.string().nullable(),
|
postDown: z.string().nullable(),
|
||||||
dns: z.string().regex(IPV4_REGEX).nullable(),
|
dns: z.string().regex(IPV4_REGEX).nullable(),
|
||||||
peers: z.array(
|
peers: z.array(PeerSchema),
|
||||||
z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
name: NameSchema,
|
|
||||||
preSharedKey: z.string().nullable(),
|
|
||||||
allowedIps: z.string().regex(IPV4_REGEX),
|
|
||||||
persistentKeepalive: z.number().nullable(),
|
|
||||||
})
|
|
||||||
.merge(WgKeySchema)
|
|
||||||
),
|
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
status: z.enum([ 'up', 'down' ]),
|
status: z.enum([ 'up', 'down' ]),
|
||||||
|
@ -2,7 +2,7 @@ import { promises as fs } from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { WG_PATH } from "@lib/constants";
|
import { WG_PATH } from "@lib/constants";
|
||||||
import Shell from "@lib/shell";
|
import Shell from "@lib/shell";
|
||||||
import { WgKey, WgServer } from "@lib/typings";
|
import { Peer, WgKey, WgServer } from "@lib/typings";
|
||||||
import { client, WG_SEVER_PATH } from "@lib/redis";
|
import { client, WG_SEVER_PATH } from "@lib/redis";
|
||||||
import { dynaJoin, isJson } from "@lib/utils";
|
import { dynaJoin, isJson } from "@lib/utils";
|
||||||
import deepmerge from "deepmerge";
|
import deepmerge from "deepmerge";
|
||||||
@ -124,15 +124,15 @@ export class WGServer {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removePeer(id: string, publicKey: string): Promise<boolean> {
|
static async removePeer(serverId: string, publicKey: string): Promise<boolean> {
|
||||||
const server = await findServer(id)
|
const server = await findServer(serverId)
|
||||||
if (!server) {
|
if (!server) {
|
||||||
console.error('server could not be updated (reason: not exists)')
|
console.error('server could not be updated (reason: not exists)')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const peers = await wgPeersStr(server.confId)
|
const peers = await wgPeersStr(server.confId)
|
||||||
|
|
||||||
const index = await findServerIndex(id)
|
const index = await findServerIndex(serverId)
|
||||||
if (typeof index !== 'number') {
|
if (typeof index !== 'number') {
|
||||||
console.warn('findServerIndex: index not found')
|
console.warn('findServerIndex: index not found')
|
||||||
return true
|
return true
|
||||||
@ -162,6 +162,60 @@ export class WGServer {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updatePeer(serverId: string, publicKey: string, update: Partial<Peer>): Promise<boolean> {
|
||||||
|
const server = await findServer(serverId)
|
||||||
|
if (!server) {
|
||||||
|
console.error('WGServer:UpdatePeer: server could not be updated (Reason: not exists)')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = await findServerIndex(serverId)
|
||||||
|
if (typeof index !== 'number') {
|
||||||
|
console.warn('findServerIndex: index not found')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPeers = server.peers.map((p) => {
|
||||||
|
if (p.publicKey !== publicKey) return p
|
||||||
|
return deepmerge(p, update)
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.lset(WG_SEVER_PATH, index, JSON.stringify({ ...server, peers: updatedPeers }))
|
||||||
|
await this.storePeers({ id: server.id, confId: server.confId }, publicKey, updatedPeers)
|
||||||
|
|
||||||
|
await WGServer.stop(serverId)
|
||||||
|
await WGServer.start(serverId)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getPeerIndex(id: string, publicKey: string): Promise<number | undefined> {
|
||||||
|
const server = await findServer(id)
|
||||||
|
if (!server) {
|
||||||
|
console.error('server could not be updated (reason: not exists)')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return server.peers.findIndex((p) => p.publicKey === publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async storePeers(sd: Pick<WgServer, 'id' | 'confId'>, publicKey: string, peers: Peer[]): Promise<void> {
|
||||||
|
|
||||||
|
const peerIndex = await this.getPeerIndex(sd.id, publicKey)
|
||||||
|
if (peerIndex === -1) {
|
||||||
|
console.warn('WGServer:StorePeers: no peer found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const confPath = path.join(WG_PATH, `wg${sd.confId}.conf`)
|
||||||
|
const conf = await fs.readFile(confPath, 'utf-8')
|
||||||
|
const serverConfStr = conf.includes('[Peer]') ?
|
||||||
|
conf.split('[Peer]')[0] :
|
||||||
|
conf
|
||||||
|
|
||||||
|
const peersStr = peers.filter((_, i) => i !== peerIndex).join('\n')
|
||||||
|
await fs.writeFile(confPath, `${serverConfStr}\n${peersStr}`)
|
||||||
|
}
|
||||||
|
|
||||||
static async getFreePeerIp(id: string): Promise<string | undefined> {
|
static async getFreePeerIp(id: string): Promise<string | undefined> {
|
||||||
const server = await findServer(id)
|
const server = await findServer(id)
|
||||||
if (!server) {
|
if (!server) {
|
||||||
@ -388,7 +442,7 @@ export async function generateWgServer(config: {
|
|||||||
throw new Error(`Address ${config.address} is already reserved!`)
|
throw new Error(`Address ${config.address} is already reserved!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(addresses) && ports.includes(config.port)) {
|
if (Array.isArray(ports) && ports.includes(config.port)) {
|
||||||
throw new Error(`Port ${config.port} is already reserved!`)
|
throw new Error(`Port ${config.port} is already reserved!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ 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, WgServer } from "@lib/typings";
|
import { APIResponse, Peer, 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";
|
||||||
@ -14,6 +14,9 @@ import CreateClientModal from "@ui/Modal/CreateClientModal";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import QRCodeModal from "@ui/Modal/QRCodeModal";
|
import QRCodeModal from "@ui/Modal/QRCodeModal";
|
||||||
import { getPeerConf } from "@lib/wireguard-utils";
|
import { getPeerConf } from "@lib/wireguard-utils";
|
||||||
|
import EditableText from "@ui/EditableText";
|
||||||
|
import { RLS_NAME_INPUT } from "@lib/form-rules";
|
||||||
|
import { UPDATE_CLIENT } from "@lib/swr-fetch";
|
||||||
|
|
||||||
|
|
||||||
export async function getServerSideProps(context: any) {
|
export async function getServerSideProps(context: any) {
|
||||||
@ -214,8 +217,6 @@ export default function ServerPage(props: PageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type Peer = WgServer['peers'][0]
|
|
||||||
|
|
||||||
interface ClientProps extends Peer, Pick<WgServer, 'dns'> {
|
interface ClientProps extends Peer, Pick<WgServer, 'dns'> {
|
||||||
serverId: string
|
serverId: string
|
||||||
serverPublicKey: string
|
serverPublicKey: string
|
||||||
@ -237,10 +238,13 @@ function Client(props: ClientProps) {
|
|||||||
dns: props.dns,
|
dns: props.dns,
|
||||||
})
|
})
|
||||||
.then((s) => setConf(s))
|
.then((s) => setConf(s))
|
||||||
|
|
||||||
console.log('conf', conf)
|
|
||||||
}, [ props ])
|
}, [ props ])
|
||||||
|
|
||||||
|
const RefreshOptions = {
|
||||||
|
onSuccess: () => props.refreshTrigger(),
|
||||||
|
onError: () => props.refreshTrigger()
|
||||||
|
}
|
||||||
|
|
||||||
const { isMutating: removingClient, trigger: removeClient } = useSWRMutation(
|
const { isMutating: removingClient, trigger: removeClient } = useSWRMutation(
|
||||||
`/api/wireguard/${props.serverId}/${props.id}`,
|
`/api/wireguard/${props.serverId}/${props.id}`,
|
||||||
async (url: string,) => {
|
async (url: string,) => {
|
||||||
@ -252,10 +256,13 @@ function Client(props: ClientProps) {
|
|||||||
if (!data.ok) throw new Error('Server responded with error status')
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
{
|
RefreshOptions
|
||||||
onSuccess: () => props.refreshTrigger(),
|
)
|
||||||
onError: () => props.refreshTrigger()
|
|
||||||
}
|
const { isMutating, trigger } = useSWRMutation(
|
||||||
|
`/api/wireguard/${props.serverId}/${props.id}`,
|
||||||
|
UPDATE_CLIENT,
|
||||||
|
RefreshOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -268,7 +275,14 @@ function Client(props: ClientProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={'col-span-12 md:col-span-4'}>
|
<div className={'col-span-12 md:col-span-4'}>
|
||||||
<div className={'col-span-12 md:col-span-4'}>
|
<div className={'col-span-12 md:col-span-4'}>
|
||||||
<span className={'inline-block font-medium'}> {props.name} </span>
|
<EditableText
|
||||||
|
disabled={isMutating}
|
||||||
|
rules={RLS_NAME_INPUT}
|
||||||
|
rootClassName={'font-medium col-span-4'}
|
||||||
|
inputClassName={'w-20'}
|
||||||
|
content={props.name}
|
||||||
|
onChange={(v) => trigger({ name: v })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'col-span-12 md:col-span-4'}>
|
<div className={'col-span-12 md:col-span-4'}>
|
||||||
<span className={'font-mono text-gray-400 text-xs'}> {props.allowedIps} </span>
|
<span className={'font-mono text-gray-400 text-xs'}> {props.allowedIps} </span>
|
||||||
|
@ -68,7 +68,7 @@ async function update(server: WgServer, peer: Peer, req: NextApiRequest, res: Ne
|
|||||||
const { name } = req.body as z.infer<typeof PutRequestSchema>
|
const { name } = req.body as z.infer<typeof PutRequestSchema>
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
await WGServer.update(server.id, { name })
|
await WGServer.updatePeer(server.id, peer.publicKey, { name })
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
@ -56,7 +56,7 @@ async function update(server: WgServer, req: NextApiRequest, res: NextApiRespons
|
|||||||
return zodErrorToResponse(res, parsed.error)
|
return zodErrorToResponse(res, parsed.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status } = req.body as z.infer<typeof PutRequestSchema>
|
const { status, name } = req.body as z.infer<typeof PutRequestSchema>
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'start':
|
case 'start':
|
||||||
@ -71,6 +71,10 @@ async function update(server: WgServer, req: NextApiRequest, res: NextApiRespons
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
await WGServer.update(server.id, { name })
|
||||||
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
.status(200)
|
.status(200)
|
||||||
.json({ ok: true })
|
.json({ ok: true })
|
||||||
|
@ -11,6 +11,10 @@ import { SmartModalRef } from "@ui/Modal/SmartModal";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import CreateServerModal from "@ui/Modal/CreateServerModal";
|
import CreateServerModal from "@ui/Modal/CreateServerModal";
|
||||||
import StatusBadge from "@ui/StatusBadge";
|
import StatusBadge from "@ui/StatusBadge";
|
||||||
|
import EditableText from "@ui/EditableText";
|
||||||
|
import useSWRMutation from "swr/mutation";
|
||||||
|
import { UPDATE_SERVER } from "@lib/swr-fetch";
|
||||||
|
import { RLS_NAME_INPUT } from "@lib/form-rules";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
@ -18,11 +22,9 @@ export default function Home() {
|
|||||||
async (url: string) => {
|
async (url: string) => {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' }
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const data = await resp.json() as APIResponse<any>
|
const data = await resp.json() as APIResponse<WgServer[]>
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -48,13 +50,19 @@ export default function Home() {
|
|||||||
<Card className={'flex items-center justify-center p-4'}>
|
<Card className={'flex items-center justify-center p-4'}>
|
||||||
Loading...
|
Loading...
|
||||||
</Card>
|
</Card>
|
||||||
) : data.length > 0 ? (
|
) : Array.isArray(data) && data.length > 0 ? (
|
||||||
<Card
|
<Card
|
||||||
className={'[&>.ant-card-body]:p-0'}
|
className={'[&>.ant-card-body]:p-0'}
|
||||||
title={<span> Servers </span>}
|
title={<span> Servers </span>}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
{data.map((s) => <Server key={s.id} {...s} />)}
|
{data.map((s) => (
|
||||||
|
<ServerListItem
|
||||||
|
{...s}
|
||||||
|
key={s.id}
|
||||||
|
refreshTrigger={() => mutate()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
@ -74,17 +82,38 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Server(s: WgServer) {
|
interface ServerListItemProps extends WgServer {
|
||||||
|
refreshTrigger: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerListItem(props: ServerListItemProps) {
|
||||||
|
|
||||||
|
const { isMutating, trigger } = useSWRMutation(
|
||||||
|
`/api/wireguard/${props.id}`,
|
||||||
|
UPDATE_SERVER,
|
||||||
|
{
|
||||||
|
onSuccess: () => props.refreshTrigger(),
|
||||||
|
onError: () => props.refreshTrigger(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List.Item className={'flex items-center justify-between p-4'}>
|
<List.Item className={'flex items-center justify-between p-4'}>
|
||||||
<div className={'w-full grid grid-cols-12 items-center gap-x-2'}>
|
<div className={'w-full grid grid-cols-12 items-center gap-x-2'}>
|
||||||
<ServerIcon type={s.type} className={'col-span-1'} />
|
<ServerIcon type={props.type} className={'col-span-1'} />
|
||||||
<span className={'font-medium col-span-4'}> {s.name} </span>
|
<EditableText
|
||||||
|
disabled={isMutating}
|
||||||
|
rules={RLS_NAME_INPUT}
|
||||||
|
rootClassName={'font-medium col-span-4'}
|
||||||
|
inputClassName={'w-20'}
|
||||||
|
content={props.name}
|
||||||
|
onChange={(v) => trigger({ name: v })}
|
||||||
|
/>
|
||||||
<div className={'col-span-4 justify-end'}>
|
<div className={'col-span-4 justify-end'}>
|
||||||
<StatusBadge status={s.status} />
|
<StatusBadge status={props.status} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/${s.id}`}>
|
<Link href={`/${props.id}`}>
|
||||||
<Button type={'primary'}>
|
<Button type={'primary'}>
|
||||||
Manage
|
Manage
|
||||||
</Button>
|
</Button>
|
||||||
|
86
src/ui/EditableText.tsx
Normal file
86
src/ui/EditableText.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { ReactHTMLProps } from "@lib/typings";
|
||||||
|
import { Form, Input, InputRef } from "antd";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import type { SizeType } from "antd/lib/config-provider/SizeContext";
|
||||||
|
import type { Rule } from "rc-field-form/lib/interface";
|
||||||
|
|
||||||
|
export interface EditableTextProps extends Omit<ReactHTMLProps<HTMLSpanElement>, 'onChange' | 'children'> {
|
||||||
|
content?: string
|
||||||
|
rootClassName?: string
|
||||||
|
inputClassName?: string
|
||||||
|
inputWidth?: string | number | undefined
|
||||||
|
inputSize?: SizeType
|
||||||
|
rules?: Rule[]
|
||||||
|
onChange?: (val: string) => void
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditableText(props: EditableTextProps) {
|
||||||
|
const {
|
||||||
|
rootClassName,
|
||||||
|
disabled,
|
||||||
|
inputClassName,
|
||||||
|
rules,
|
||||||
|
inputSize,
|
||||||
|
inputWidth,
|
||||||
|
onChange,
|
||||||
|
content,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
const [ editMode, setEditMode ] = React.useState(false)
|
||||||
|
const inputRef = React.useRef<InputRef | null>(null)
|
||||||
|
const [ val, setVal ] = React.useState(content)
|
||||||
|
React.useEffect(() => {
|
||||||
|
const { input } = inputRef.current || {}
|
||||||
|
if (input) {
|
||||||
|
input.value = val || ''
|
||||||
|
}
|
||||||
|
}, [ val ])
|
||||||
|
const [ form ] = Form.useForm()
|
||||||
|
return (
|
||||||
|
<div className={twMerge('group', rootClassName)}>
|
||||||
|
<span {...rest} className={twMerge(
|
||||||
|
editMode ? 'hidden' : 'flex items-center gap-x-2',
|
||||||
|
'leading-none'
|
||||||
|
)}>
|
||||||
|
{val}
|
||||||
|
<i
|
||||||
|
className={'fal fa-pen-to-square text-sm opacity-0 group-hover:opacity-100 text-neutral-400 hover:text-primary cursor-pointer'}
|
||||||
|
onClick={() => setEditMode(true)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<Form
|
||||||
|
rootClassName={twMerge(editMode ? 'block' : 'hidden')}
|
||||||
|
form={form}
|
||||||
|
onFinish={() => {
|
||||||
|
setEditMode(false)
|
||||||
|
const newVal = inputRef.current?.input?.value || ''
|
||||||
|
onChange && onChange(newVal)
|
||||||
|
setVal(newVal)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name={'input'}
|
||||||
|
rules={rules}
|
||||||
|
rootClassName={'m-0'}
|
||||||
|
className={'[&>.ant-row>.ant-col>div>.ant-form-item-explain]:hidden'}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
disabled={disabled}
|
||||||
|
size={inputSize || 'small'}
|
||||||
|
ref={inputRef}
|
||||||
|
style={{ width: inputWidth }}
|
||||||
|
defaultValue={val}
|
||||||
|
className={inputClassName}
|
||||||
|
onKeyDown={async (evt) => {
|
||||||
|
if (evt.key === 'Enter') {
|
||||||
|
form.submit()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { APIResponse } from "@lib/typings";
|
|||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
import { NameSchema } from "@lib/schemas/WireGuard";
|
import { NameSchema } from "@lib/schemas/WireGuard";
|
||||||
import { zodErrorMessage } from "@lib/zod";
|
import { zodErrorMessage } from "@lib/zod";
|
||||||
|
import { RLS_NAME_INPUT } from "@lib/form-rules";
|
||||||
|
|
||||||
|
|
||||||
type CreateClientModalProps = {
|
type CreateClientModalProps = {
|
||||||
@ -89,20 +90,7 @@ const CreateClientModal = React.forwardRef<
|
|||||||
<h4 className={'mb-6'}> Create Client </h4>
|
<h4 className={'mb-6'}> Create Client </h4>
|
||||||
<Form form={form} onFinish={onFinish}>
|
<Form form={form} onFinish={onFinish}>
|
||||||
|
|
||||||
<Form.Item name={'name'} label={'Name'} rules={[
|
<Form.Item name={'name'} label={'Name'} rules={RLS_NAME_INPUT}>
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Name is required'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
validator: (_, value) => {
|
|
||||||
if (!value) return Promise.resolve()
|
|
||||||
const res = NameSchema.safeParse(value)
|
|
||||||
if (res.success) return Promise.resolve()
|
|
||||||
return Promise.reject(zodErrorMessage(res.error)[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]}>
|
|
||||||
<Input placeholder={'Unicorn 🦄'} />
|
<Input placeholder={'Unicorn 🦄'} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user