chore: prettify frontend code

This commit is contained in:
Semesse
2024-01-22 00:44:18 +08:00
parent 0e03c931ea
commit 90414a69b6
82 changed files with 5224 additions and 5586 deletions

2
www/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
lib/pb
out

41
www/.prettierrc.js Normal file
View File

@@ -0,0 +1,41 @@
module.exports = {
// max 120 characters per line
printWidth: 120,
// use 2 spaces for indentation
tabWidth: 2,
// use spaces instead of indentations
useTabs: false,
// semicolon at the end of the line
semi: false,
// use single quotes
singleQuote: true,
// object's key is quoted only when necessary
quoteProps: 'as-needed',
// use double quotes instead of single quotes in jsx
jsxSingleQuote: false,
// no comma at the end
trailingComma: 'all',
// spaces are required at the beginning and end of the braces
bracketSpacing: true,
// end tag of jsx need to wrap
bracketSameLine: false,
// brackets are required for arrow function parameter, even when there is only one parameter
arrowParens: 'always',
// format the entire contents of the file
rangeStart: 0,
rangeEnd: Infinity,
// no need to write the beginning @prettier of the file
requirePragma: false,
// No need to automatically insert @prettier at the beginning of the file
insertPragma: false,
// use default break criteria
proseWrap: 'preserve',
// decide whether to break the html according to the display style
htmlWhitespaceSensitivity: 'css',
// vue files script and style tags indentation
vueIndentScriptAndStyle: false,
// lf for newline
endOfLine: 'lf',
// formats quoted code embedded
embeddedLanguageFormatting: 'auto',
}

View File

@@ -1,9 +1,6 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from '@/lib/consts' import { API_PATH } from '@/lib/consts'
import { import { LoginRequest, LoginResponse, RegisterRequest, RegisterResponse } from '@/lib/pb/api_auth'
LoginRequest, LoginResponse,
RegisterRequest, RegisterResponse
} from '@/lib/pb/api_auth'
import { CommonResponse } from '@/lib/pb/common' import { CommonResponse } from '@/lib/pb/common'
import { BaseResponse } from '@/types/api' import { BaseResponse } from '@/types/api'

View File

@@ -1,12 +1,16 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from "@/lib/consts"; import { API_PATH } from '@/lib/consts'
import { import {
DeleteClientRequest, DeleteClientResponse, DeleteClientRequest,
GetClientRequest, GetClientResponse, DeleteClientResponse,
InitClientRequest, InitClientResponse, GetClientRequest,
ListClientsRequest, ListClientsResponse GetClientResponse,
} from '@/lib/pb/api_client'; InitClientRequest,
import { BaseResponse } from "@/types/api"; InitClientResponse,
ListClientsRequest,
ListClientsResponse,
} from '@/lib/pb/api_client'
import { BaseResponse } from '@/types/api'
export const getClient = async (req: GetClientRequest) => { export const getClient = async (req: GetClientRequest) => {
const res = await http.post(API_PATH + '/client/get', GetClientRequest.toJson(req)) const res = await http.post(API_PATH + '/client/get', GetClientRequest.toJson(req))
@@ -24,7 +28,7 @@ export const deleteClient = async (req: DeleteClientRequest) => {
} }
export const initClient = async (req: InitClientRequest) => { export const initClient = async (req: InitClientRequest) => {
console.log("attempting init client:", InitClientRequest.toJsonString(req)) console.log('attempting init client:', InitClientRequest.toJsonString(req))
const res = await http.post(API_PATH + '/client/init', InitClientRequest.toJson(req)) const res = await http.post(API_PATH + '/client/init', InitClientRequest.toJson(req))
return InitClientResponse.fromJson((res.data as BaseResponse).body) return InitClientResponse.fromJson((res.data as BaseResponse).body)
} }

View File

@@ -1,18 +1,8 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from "@/lib/consts"; import { API_PATH } from '@/lib/consts'
import { import { RemoveFRPCRequest, RemoveFRPCResponse, UpdateFRPCRequest, UpdateFRPCResponse } from '@/lib/pb/api_client'
RemoveFRPCRequest, import { RemoveFRPSRequest, RemoveFRPSResponse, UpdateFRPSRequest, UpdateFRPSResponse } from '@/lib/pb/api_server'
RemoveFRPCResponse, import { BaseResponse } from '@/types/api'
UpdateFRPCRequest,
UpdateFRPCResponse
} from '@/lib/pb/api_client';
import {
RemoveFRPSRequest,
RemoveFRPSResponse,
UpdateFRPSRequest,
UpdateFRPSResponse
} from '@/lib/pb/api_server';
import { BaseResponse } from "@/types/api";
export const updateFRPS = async (req: UpdateFRPSRequest) => { export const updateFRPS = async (req: UpdateFRPSRequest) => {
const res = await http.post(API_PATH + '/frps/update', UpdateFRPSRequest.toJson(req)) const res = await http.post(API_PATH + '/frps/update', UpdateFRPSRequest.toJson(req))

View File

@@ -1,7 +1,7 @@
import { LOCAL_STORAGE_TOKEN_KEY, SET_TOKEN_HEADER, X_CLIENT_REQUEST_ID } from '@/lib/consts'; import { LOCAL_STORAGE_TOKEN_KEY, SET_TOKEN_HEADER, X_CLIENT_REQUEST_ID } from '@/lib/consts'
import { $token } from '@/store/user'; import { $token } from '@/store/user'
import axios from 'axios' import axios from 'axios'
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'
const instance = axios.create({}) const instance = axios.create({})
@@ -11,7 +11,7 @@ instance.interceptors.request.use((request) => {
request.headers.Authorization = token request.headers.Authorization = token
$token.set(token) $token.set(token)
} }
request.headers[X_CLIENT_REQUEST_ID] = uuidv4(); request.headers[X_CLIENT_REQUEST_ID] = uuidv4()
return request return request
}) })

View File

@@ -1,12 +1,7 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from '@/lib/consts' import { API_PATH } from '@/lib/consts'
import { import { GetClientsStatusRequest, GetClientsStatusResponse } from '@/lib/pb/api_master'
GetClientsStatusRequest, import { GetPlatformInfoResponse } from '@/lib/pb/api_user'
GetClientsStatusResponse
} from '@/lib/pb/api_master'
import {
GetPlatformInfoResponse,
} from '@/lib/pb/api_user'
import { BaseResponse } from '@/types/api' import { BaseResponse } from '@/types/api'
export const getPlatformInfo = async () => { export const getPlatformInfo = async () => {

View File

@@ -1,14 +1,16 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from "@/lib/consts"; import { API_PATH } from '@/lib/consts'
import { import {
DeleteServerRequest, DeleteServerRequest,
DeleteServerResponse, DeleteServerResponse,
GetServerRequest, GetServerResponse, GetServerRequest,
GetServerResponse,
InitServerRequest, InitServerRequest,
InitServerResponse, InitServerResponse,
ListServersRequest, ListServersResponse ListServersRequest,
} from "@/lib/pb/api_server"; ListServersResponse,
import { BaseResponse } from "@/types/api"; } from '@/lib/pb/api_server'
import { BaseResponse } from '@/types/api'
export const getServer = async (req: GetServerRequest) => { export const getServer = async (req: GetServerRequest) => {
const res = await http.post(API_PATH + '/server/get', GetServerRequest.toJson(req)) const res = await http.post(API_PATH + '/server/get', GetServerRequest.toJson(req))

View File

@@ -1,8 +1,10 @@
import http from '@/api/http' import http from '@/api/http'
import { API_PATH } from '@/lib/consts' import { API_PATH } from '@/lib/consts'
import { import {
GetUserInfoRequest, GetUserInfoResponse, GetUserInfoRequest,
UpdateUserInfoRequest, UpdateUserInfoResponse GetUserInfoResponse,
UpdateUserInfoRequest,
UpdateUserInfoResponse,
} from '@/lib/pb/api_user' } from '@/lib/pb/api_user'
import { $userInfo } from '@/store/user' import { $userInfo } from '@/store/user'
import { BaseResponse } from '@/types/api' import { BaseResponse } from '@/types/api'

View File

@@ -1,124 +1,205 @@
import { login, register } from "@/api/auth" import { login, register } from '@/api/auth'
import { Button } from "./ui/button" import { Button } from './ui/button'
import { deleteClient, getClient, initClient, listClient } from "@/api/client" import { deleteClient, getClient, initClient, listClient } from '@/api/client'
import { deleteServer, getServer, initServer, listServer } from "@/api/server" import { deleteServer, getServer, initServer, listServer } from '@/api/server'
import { updateFRPC, updateFRPS } from "@/api/frp" import { updateFRPC, updateFRPS } from '@/api/frp'
import { ClientConfig } from "@/types/client" import { ClientConfig } from '@/types/client'
import { ServerConfig } from "@/types/server" import { ServerConfig } from '@/types/server'
import { getUserInfo, updateUserInfo } from "@/api/user" import { getUserInfo, updateUserInfo } from '@/api/user'
import { Separator } from "./ui/separator" import { Separator } from './ui/separator'
import { useState } from "react" import { useState } from 'react'
import { Input } from "./ui/input" import { Input } from './ui/input'
import { Label } from "@radix-ui/react-label" import { Label } from '@radix-ui/react-label'
export const APITest = () => { export const APITest = () => {
const [serverID, setServerID] = useState<string>("admin.server") const [serverID, setServerID] = useState<string>('admin.server')
const [clientID, setClientID] = useState<string>("admin.client") const [clientID, setClientID] = useState<string>('admin.client')
const [username, setUsername] = useState<string>("admin") const [username, setUsername] = useState<string>('admin')
const [password, setPassword] = useState<string>("admin") const [password, setPassword] = useState<string>('admin')
const [email, setEmail] = useState<string>("admin@localhost") const [email, setEmail] = useState<string>('admin@localhost')
return ( return (
<div className='flex flex-col w-full p-10 lg:w-1/2'> <div className="flex flex-col w-full p-10 lg:w-1/2">
<div className='grid grid-cols-2 sm:grid-cols-5 gap-4 my-4'> <div className="grid grid-cols-2 sm:grid-cols-5 gap-4 my-4">
<div > <div>
<Label>username</Label> <Label>username</Label>
<Input value={username} onChange={e => setUsername(e.target.value)} /> <Input value={username} onChange={(e) => setUsername(e.target.value)} />
</div> </div>
<div > <div>
<Label>password</Label> <Label>password</Label>
<Input value={password} onChange={e => setPassword(e.target.value)} /> <Input value={password} onChange={(e) => setPassword(e.target.value)} />
</div> </div>
<div > <div>
<Label>email</Label> <Label>email</Label>
<Input value={email} onChange={e => setEmail(e.target.value)} /> <Input value={email} onChange={(e) => setEmail(e.target.value)} />
</div> </div>
<div > <div>
<Label>clientID</Label> <Label>clientID</Label>
<Input value={clientID} onChange={e => setClientID(e.target.value)} /> <Input value={clientID} onChange={(e) => setClientID(e.target.value)} />
</div> </div>
<div > <div>
<Label>serverID</Label> <Label>serverID</Label>
<Input value={serverID} onChange={e => setServerID(e.target.value)} /> <Input value={serverID} onChange={(e) => setServerID(e.target.value)} />
</div> </div>
</div> </div>
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
<Button onClick={async () => { <Button
console.log("attempting login:", await login({ username: username, password: password })) onClick={async () => {
}}>login</Button> console.log('attempting login:', await login({ username: username, password: password }))
<Button onClick={async () => { }}
console.log("attempting register:", await register({ username: username, password: password, email: email })) >
}}>register</Button> login
<Button onClick={async () => { </Button>
console.log("attempting update user:", await updateUserInfo({ <Button
userInfo: { token: "123123" }, onClick={async () => {
})) console.log(
}}>update user</Button> 'attempting register:',
<Button onClick={async () => { await register({ username: username, password: password, email: email }),
console.log("attempting get user:", await getUserInfo({})) )
}}>get user</Button> }}
>
register
</Button>
<Button
onClick={async () => {
console.log(
'attempting update user:',
await updateUserInfo({
userInfo: { token: '123123' },
}),
)
}}
>
update user
</Button>
<Button
onClick={async () => {
console.log('attempting get user:', await getUserInfo({}))
}}
>
get user
</Button>
</div> </div>
<Separator /> <Separator />
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
<Button onClick={async () => { <Button
console.log("attempting init server:", await initServer({ serverId: serverID.replace(username + ".", ""), serverIp: "127.0.0.1" })) onClick={async () => {
}}>init server</Button> console.log(
<Button onClick={async () => { 'attempting init server:',
console.log("attempting delete server:", await deleteServer({ serverId: serverID })) await initServer({ serverId: serverID.replace(username + '.', ''), serverIp: '127.0.0.1' }),
}}>delete server</Button> )
<Button onClick={async () => { }}
console.log("attempting list servers:", await listServer({ page: 1, pageSize: 10 })) >
}}>list servers</Button> init server
<Button onClick={async () => { </Button>
console.log("attempting get server:", await getServer({ serverId: serverID })) <Button
}}>get server</Button> onClick={async () => {
console.log('attempting delete server:', await deleteServer({ serverId: serverID }))
}}
>
delete server
</Button>
<Button
onClick={async () => {
console.log('attempting list servers:', await listServer({ page: 1, pageSize: 10 }))
}}
>
list servers
</Button>
<Button
onClick={async () => {
console.log('attempting get server:', await getServer({ serverId: serverID }))
}}
>
get server
</Button>
</div> </div>
<Separator /> <Separator />
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
<Button onClick={async () => { <Button
console.log("attempting update frps:", await updateFRPS({ onClick={async () => {
console.log(
'attempting update frps:',
await updateFRPS({
serverId: serverID, serverId: serverID,
config: Buffer.from(JSON.stringify({ config: Buffer.from(
JSON.stringify({
bindPort: 1122, bindPort: 1122,
} as ServerConfig)) } as ServerConfig),
})) ),
}}>update frps</Button> }),
<Button onClick={async () => { )
console.log("attempting delete frps:", await deleteServer({ serverId: serverID })) }}
}}>delete frps</Button> >
update frps
</Button>
<Button
onClick={async () => {
console.log('attempting delete frps:', await deleteServer({ serverId: serverID }))
}}
>
delete frps
</Button>
</div> </div>
<Separator /> <Separator />
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
<Button onClick={async () => { <Button
console.log("attempting init client:", await initClient({ clientId: clientID.replace(username + ".", "") })) onClick={async () => {
}}>init client</Button> console.log('attempting init client:', await initClient({ clientId: clientID.replace(username + '.', '') }))
<Button onClick={async () => { }}
console.log("attempting delete client:", await deleteClient({ clientId: clientID })) >
}}>delete client</Button> init client
<Button onClick={async () => { </Button>
console.log("attempting list clients:", await listClient({ page: 1, pageSize: 10 })) <Button
}}>list clients</Button> onClick={async () => {
<Button onClick={async () => { console.log('attempting delete client:', await deleteClient({ clientId: clientID }))
console.log("attempting get client:", await getClient({ clientId: clientID })) }}
}}>get client</Button> >
delete client
</Button>
<Button
onClick={async () => {
console.log('attempting list clients:', await listClient({ page: 1, pageSize: 10 }))
}}
>
list clients
</Button>
<Button
onClick={async () => {
console.log('attempting get client:', await getClient({ clientId: clientID }))
}}
>
get client
</Button>
</div> </div>
<Separator /> <Separator />
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
<Button onClick={async () => { <Button
console.log("attempting update frpc:", await updateFRPC({ onClick={async () => {
console.log(
'attempting update frpc:',
await updateFRPC({
clientId: clientID, clientId: clientID,
serverId: serverID, serverId: serverID,
config: Buffer.from(JSON.stringify({ config: Buffer.from(
proxies: [ JSON.stringify({
{ name: "test", type: "tcp", localIP: "127.0.0.1", localPort: 1234, remotePort: 4321 } proxies: [{ name: 'test', type: 'tcp', localIP: '127.0.0.1', localPort: 1234, remotePort: 4321 }],
] } as ClientConfig),
} as ClientConfig)) ),
})) }),
}}>update frpc</Button> )
<Button onClick={async () => { }}
console.log("attempting delete frpc:", await deleteClient({ clientId: clientID })) >
}}>delete frpc</Button> update frpc
</Button>
<Button
onClick={async () => {
console.log('attempting delete frpc:', await deleteClient({ clientId: clientID }))
}}
>
delete frpc
</Button>
</div>
</div> </div>
</div >
) )
} }

View File

@@ -1,11 +1,11 @@
import { useState } from "react" import { useState } from 'react'
import { useMutation, useQuery } from "@tanstack/react-query" import { useMutation, useQuery } from '@tanstack/react-query'
import { initClient, listClient } from "@/api/client" import { initClient, listClient } from '@/api/client'
import { Label } from "./ui/label" import { Label } from './ui/label'
import { Input } from "./ui/input" import { Input } from './ui/input'
import { Button } from "./ui/button" import { Button } from './ui/button'
import { useToast } from "./ui/use-toast" import { useToast } from './ui/use-toast'
import { RespCode } from "@/lib/pb/common" import { RespCode } from '@/lib/pb/common'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -14,7 +14,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from '@/components/ui/dialog'
export const CreateClientDialog = () => { export const CreateClientDialog = () => {
const [clientID, setClientID] = useState<string | undefined>() const [clientID, setClientID] = useState<string | undefined>()
@@ -22,32 +22,34 @@ export const CreateClientDialog = () => {
mutationFn: initClient, mutationFn: initClient,
}) })
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ["listClient", { pageIndex: 0, pageSize: 10 }], queryKey: ['listClient', { pageIndex: 0, pageSize: 10 }],
queryFn: async () => { queryFn: async () => {
return await listClient({ page: 1, pageSize: 10 }) return await listClient({ page: 1, pageSize: 10 })
} },
}) })
const { toast } = useToast() const { toast } = useToast()
const handleNewClient = async () => { const handleNewClient = async () => {
toast({ title: "已提交创建请求" }) toast({ title: '已提交创建请求' })
try { try {
let resp = await newClient.mutateAsync({ clientId: clientID }) let resp = await newClient.mutateAsync({ clientId: clientID })
if (resp.status?.code !== RespCode.SUCCESS) { if (resp.status?.code !== RespCode.SUCCESS) {
toast({ title: "创建客户端失败" }) toast({ title: '创建客户端失败' })
return return
} }
toast({ title: "创建客户端成功" }) toast({ title: '创建客户端成功' })
dataQuery.refetch() dataQuery.refetch()
} catch (error) { } catch (error) {
toast({ title: "创建客户端失败" }) toast({ title: '创建客户端失败' })
} }
} }
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" size={"sm"}></Button> <Button variant="outline" size={'sm'}>
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>

View File

@@ -1,5 +1,5 @@
import { ColumnDef, Table } from "@tanstack/react-table" import { ColumnDef, Table } from '@tanstack/react-table'
import { MoreHorizontal } from "lucide-react" import { MoreHorizontal } from 'lucide-react'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,9 +9,9 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from '@/components/ui/dialog'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -19,23 +19,23 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu'
import { useToast } from "./ui/use-toast" import { useToast } from './ui/use-toast'
import React, { useState } from "react" import React, { useState } from 'react'
import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from "@/lib/consts" import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from '@/lib/consts'
import { useMutation, useQuery } from "@tanstack/react-query" import { useMutation, useQuery } from '@tanstack/react-query'
import { deleteClient, listClient } from "@/api/client" import { deleteClient, listClient } from '@/api/client'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { useStore } from "@nanostores/react" import { useStore } from '@nanostores/react'
import { $platformInfo } from "@/store/user" import { $platformInfo } from '@/store/user'
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover" import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
import { getClientsStatus } from "@/api/platform" import { getClientsStatus } from '@/api/platform'
import { ClientType } from "@/lib/pb/common" import { ClientType } from '@/lib/pb/common'
import { ClientStatus, ClientStatus_Status } from "@/lib/pb/api_master" import { ClientStatus, ClientStatus_Status } from '@/lib/pb/api_master'
export type ClientTableSchema = { export type ClientTableSchema = {
id: string, id: string
status: "invalid" | "valid" status: 'invalid' | 'valid'
secret: string secret: string
info?: string info?: string
config?: string config?: string
@@ -43,58 +43,65 @@ export type ClientTableSchema = {
export const columns: ColumnDef<ClientTableSchema>[] = [ export const columns: ColumnDef<ClientTableSchema>[] = [
{ {
accessorKey: "id", accessorKey: 'id',
header: "ID", header: 'ID',
cell: ({ row }) => { cell: ({ row }) => {
return < ClientID client={row.original} /> return <ClientID client={row.original} />
} },
}, },
{ {
accessorKey: "status", accessorKey: 'status',
header: "是否配置", header: '是否配置',
cell: ({ row }) => { cell: ({ row }) => {
const client = row.original const client = row.original
return <div className={`font-medium ${client.status === "valid" ? "text-green-500" : "text-red-500"} min-w-12`}>{ return (
<div className={`font-medium ${client.status === 'valid' ? 'text-green-500' : 'text-red-500'} min-w-12`}>
{ {
valid: "已配置", {
invalid: "未配置", valid: '已配置',
invalid: '未配置',
}[client.status] }[client.status]
}</div>
} }
</div>
)
},
}, },
{ {
accessorKey: "info", accessorKey: 'info',
header: "运行信息", header: '运行信息',
cell: ({ row }) => { cell: ({ row }) => {
const client = row.original const client = row.original
return <ClientInfo client={client} /> return <ClientInfo client={client} />
} },
}, },
{ {
accessorKey: "secret", accessorKey: 'secret',
header: "连接密钥", header: '连接密钥',
cell: ({ row }) => { cell: ({ row }) => {
const client = row.original const client = row.original
return <ClientSecret client={client} /> return <ClientSecret client={client} />
} },
}, },
{ {
id: "action", id: 'action',
cell: ({ row, table }) => { cell: ({ row, table }) => {
const client = row.original const client = row.original
return (<ClientActions client={client} table={table} />) return <ClientActions client={client} table={table} />
}, },
}, },
] ]
export const ClientID = ({ client }: { client: ClientTableSchema }) => { export const ClientID = ({ client }: { client: ClientTableSchema }) => {
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
return <Popover > return (
<PopoverTrigger asChild><div className="font-mono">{client.id}</div></PopoverTrigger> <Popover>
<PopoverTrigger asChild>
<div className="font-mono">{client.id}</div>
</PopoverTrigger>
<PopoverContent className="w-fit overflow-auto max-w-64"> <PopoverContent className="w-fit overflow-auto max-w-64">
<div>Linux安装到systemd</div> <div>Linux安装到systemd</div>
<div className="p-2 border rounded font-mono w-fit"> <div className="p-2 border rounded font-mono w-fit">
{platformInfo === undefined ? "获取平台信息失败" : LinuxInstallCommand("client", client, platformInfo)} {platformInfo === undefined ? '获取平台信息失败' : LinuxInstallCommand('client', client, platformInfo)}
</div> </div>
{/* <div>Windows</div> {/* <div>Windows</div>
<div className="p-2 border rounded font-mono w-fit"> <div className="p-2 border rounded font-mono w-fit">
@@ -102,68 +109,78 @@ export const ClientID = ({ client }: { client: ClientTableSchema }) => {
</div> */} </div> */}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)
} }
export const ClientInfo = ({ client }: { client: ClientTableSchema }) => { export const ClientInfo = ({ client }: { client: ClientTableSchema }) => {
const clientsInfo = useQuery({ const clientsInfo = useQuery({
queryKey: ["getClientsStatus", [client.id]], queryKey: ['getClientsStatus', [client.id]],
queryFn: async () => { queryFn: async () => {
return await getClientsStatus({ return await getClientsStatus({
clientIds: [client.id], clientIds: [client.id],
clientType: ClientType.FRPC, clientType: ClientType.FRPC,
}) })
} },
}) })
const trans = (info: ClientStatus | undefined) => { const trans = (info: ClientStatus | undefined) => {
let statusText: "在线" | "离线" | "错误" | "未知" = "未知"; let statusText: '在线' | '离线' | '错误' | '未知' = '未知'
if (info === undefined) { if (info === undefined) {
return statusText; return statusText
} }
if (info.status === ClientStatus_Status.ONLINE) { if (info.status === ClientStatus_Status.ONLINE) {
statusText = "在线"; statusText = '在线'
} else if (info.status === ClientStatus_Status.OFFLINE) { } else if (info.status === ClientStatus_Status.OFFLINE) {
statusText = "离线"; statusText = '离线'
} else if (info.status === ClientStatus_Status.ERROR) { } else if (info.status === ClientStatus_Status.ERROR) {
statusText = "错误"; statusText = '错误'
} return statusText; }
return statusText
} }
const infoColor = clientsInfo.data?.clients[client.id]?.status === ClientStatus_Status.ONLINE ? "text-green-500" : "text-red-500" const infoColor =
clientsInfo.data?.clients[client.id]?.status === ClientStatus_Status.ONLINE ? 'text-green-500' : 'text-red-500'
return <div className={`p-2 border rounded font-mono w-fit ${infoColor}`}> return (
<div className={`p-2 border rounded font-mono w-fit ${infoColor}`}>
{`${clientsInfo.data?.clients[client.id].ping}ms, ${trans(clientsInfo.data?.clients[client.id])}`} {`${clientsInfo.data?.clients[client.id].ping}ms, ${trans(clientsInfo.data?.clients[client.id])}`}
</div> </div>
)
} }
export const ClientSecret = ({ client }: { client: ClientTableSchema }) => { export const ClientSecret = ({ client }: { client: ClientTableSchema }) => {
const [showSecrect, setShowSecrect] = useState<boolean>(false) const [showSecrect, setShowSecrect] = useState<boolean>(false)
const fakeSecret = Array.from({ length: client.secret.length }).map(() => '*').join('') const fakeSecret = Array.from({ length: client.secret.length })
.map(() => '*')
.join('')
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
const { toast } = useToast() const { toast } = useToast()
return <Popover > return (
<Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div <div
onMouseEnter={() => setShowSecrect(true)} onMouseEnter={() => setShowSecrect(true)}
onMouseLeave={() => setShowSecrect(false)} onMouseLeave={() => setShowSecrect(false)}
onClick={() => { onClick={() => {
if (platformInfo) { if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo)); navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo))
toast({ description: "复制成功", }); toast({ description: '复制成功' })
} else { } else {
toast({ description: "获取平台信息失败", }); toast({ description: '获取平台信息失败' })
} }
}} }}
className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap">{ className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap"
showSecrect ? client.secret : fakeSecret >
}</div> {showSecrect ? client.secret : fakeSecret}
</div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-fit overflow-auto max-w-48"> <PopoverContent className="w-fit overflow-auto max-w-48">
<div className="p-2 border rounded font-mono w-fit"> <div className="p-2 border rounded font-mono w-fit">
{platformInfo === undefined ? "获取平台信息失败" : ExecCommandStr("client", client, platformInfo)} {platformInfo === undefined ? '获取平台信息失败' : ExecCommandStr('client', client, platformInfo)}
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)
} }
export interface ClientItemProps { export interface ClientItemProps {
@@ -173,7 +190,7 @@ export interface ClientItemProps {
export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => { export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
const { toast } = useToast() const { toast } = useToast()
const router = useRouter(); const router = useRouter()
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
const fetchDataOptions = { const fetchDataOptions = {
pageIndex: table.getState().pagination.pageIndex, pageIndex: table.getState().pagination.pageIndex,
@@ -181,27 +198,28 @@ export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
} }
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ["listClient", fetchDataOptions], queryKey: ['listClient', fetchDataOptions],
queryFn: async () => { queryFn: async () => {
return await listClient({ return await listClient({
page: fetchDataOptions.pageIndex + 1, page: fetchDataOptions.pageIndex + 1,
pageSize: fetchDataOptions.pageSize pageSize: fetchDataOptions.pageSize,
}) })
} },
}) })
const removeClient = useMutation({ const removeClient = useMutation({
mutationFn: deleteClient, mutationFn: deleteClient,
onSuccess: () => { onSuccess: () => {
toast({ description: "删除成功" }) toast({ description: '删除成功' })
dataQuery.refetch() dataQuery.refetch()
}, },
onError: () => { onError: () => {
toast({ description: "删除失败" }) toast({ description: '删除失败' })
} },
}) })
return <Dialog> return (
<Dialog>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"> <Button variant="ghost" className="h-8 w-8 p-0">
@@ -214,19 +232,23 @@ export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
if (platformInfo) { if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo)); navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo))
toast({ description: "复制成功如果复制不成功请点击ID字段手动复制", }); toast({ description: '复制成功如果复制不成功请点击ID字段手动复制' })
} else { } else {
toast({ description: "获取平台信息失败如果复制不成功请点击ID字段手动复制", }); toast({ description: '获取平台信息失败如果复制不成功请点击ID字段手动复制' })
} }
}} }}
> >
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => { <DropdownMenuItem
router.push({ pathname: "/clientedit", query: { clientID: client.id } }) onClick={() => {
}}></DropdownMenuItem> router.push({ pathname: '/clientedit', query: { clientID: client.id } })
}}
>
</DropdownMenuItem>
<DialogTrigger asChild> <DialogTrigger asChild>
<DropdownMenuItem className="text-destructive"></DropdownMenuItem> <DropdownMenuItem className="text-destructive"></DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
@@ -236,17 +258,20 @@ export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
<DialogHeader> <DialogHeader>
<DialogTitle>?</DialogTitle> <DialogTitle>?</DialogTitle>
<DialogDescription> <DialogDescription>
<p className="text-destructive"> <p className="text-destructive">?</p>
? <p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2">
</p> </p>
<p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2"></p>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button type="submit" onClick={() => removeClient.mutate({ clientId: client.id })}></Button> <Button type="submit" onClick={() => removeClient.mutate({ clientId: client.id })}>
</Button>
</DialogClose> </DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)
} }

View File

@@ -1,6 +1,6 @@
import { Client } from "@/lib/pb/common"; import { Client } from '@/lib/pb/common'
import { ClientTableSchema, columns as clientColumnsDef } from "./client_item"; import { ClientTableSchema, columns as clientColumnsDef } from './client_item'
import { DataTable } from "./data_table"; import { DataTable } from './data_table'
import { import {
getSortedRowModel, getSortedRowModel,
@@ -11,11 +11,11 @@ import {
getPaginationRowModel, getPaginationRowModel,
SortingState, SortingState,
PaginationState, PaginationState,
} from "@tanstack/react-table" } from '@tanstack/react-table'
import React from "react" import React from 'react'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query'
import { listClient } from "@/api/client"; import { listClient } from '@/api/client'
export interface ClientListProps { export interface ClientListProps {
Clients: Client[] Clients: Client[]
@@ -23,19 +23,18 @@ export interface ClientListProps {
export const ClientList: React.FC<ClientListProps> = ({ Clients }) => { export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
const [sorting, setSorting] = React.useState<SortingState>([]) const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
[] const data = Clients.map(
) (client) =>
const data = Clients.map((client) =>
({ ({
id: client.id == undefined ? "" : client.id, id: client.id == undefined ? '' : client.id,
status: client.config == undefined || client.config == "" ? "invalid" : "valid", status: client.config == undefined || client.config == '' ? 'invalid' : 'valid',
secret: client.secret == undefined ? "" : client.secret, secret: client.secret == undefined ? '' : client.secret,
config: client.config config: client.config,
} as ClientTableSchema)) }) as ClientTableSchema,
)
const [{ pageIndex, pageSize }, setPagination] = const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}) })
@@ -49,26 +48,29 @@ export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
pageIndex, pageIndex,
pageSize, pageSize,
}), }),
[pageIndex, pageSize] [pageIndex, pageSize],
) )
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ["listClient", fetchDataOptions], queryKey: ['listClient', fetchDataOptions],
queryFn: async () => { queryFn: async () => {
return await listClient({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize }) return await listClient({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
} },
}) })
const table = useReactTable({ const table = useReactTable({
data: dataQuery.data?.clients.map((client) => { data:
dataQuery.data?.clients.map((client) => {
return { return {
id: client.id == undefined ? "" : client.id, id: client.id == undefined ? '' : client.id,
status: client.config == undefined || client.config == "" ? "invalid" : "valid", status: client.config == undefined || client.config == '' ? 'invalid' : 'valid',
secret: client.secret == undefined ? "" : client.secret, secret: client.secret == undefined ? '' : client.secret,
config: client.config config: client.config,
} as ClientTableSchema } as ClientTableSchema
}) ?? data, }) ?? data,
pageCount: Math.ceil((dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0), pageCount: Math.ceil(
(dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0,
),
columns: clientColumnsDef, columns: clientColumnsDef,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
@@ -85,4 +87,4 @@ export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
}, },
}) })
return <DataTable table={table} columns={clientColumnsDef} /> return <DataTable table={table} columns={clientColumnsDef} />
}; }

View File

@@ -1,23 +1,17 @@
import { import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
ArrowDownIcon, import { Column } from '@tanstack/react-table'
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from "@radix-ui/react-icons"
import { Column } from "@tanstack/react-table"
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps<TData, TValue> interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue> column: Column<TData, TValue>
title: string title: string
} }
@@ -32,18 +26,14 @@ export function DataTableColumnHeader<TData, TValue>({
} }
return ( return (
<div className={cn("flex items-center space-x-2", className)}> <div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button variant="ghost" size="sm" className="-ml-3 h-8 data-[state=open]:bg-accent">
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span> <span>{title}</span>
{column.getIsSorted() === "desc" ? ( {column.getIsSorted() === 'desc' ? (
<ArrowDownIcon className="ml-2 h-4 w-4" /> <ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === "asc" ? ( ) : column.getIsSorted() === 'asc' ? (
<ArrowUpIcon className="ml-2 h-4 w-4" /> <ArrowUpIcon className="ml-2 h-4 w-4" />
) : ( ) : (
<CaretSortIcon className="ml-2 h-4 w-4" /> <CaretSortIcon className="ml-2 h-4 w-4" />

View File

@@ -1,4 +1,4 @@
"use client" 'use client'
import { import {
ColumnDef, ColumnDef,
@@ -11,20 +11,13 @@ import {
getPaginationRowModel, getPaginationRowModel,
SortingState, SortingState,
Table as TableType, Table as TableType,
} from "@tanstack/react-table" } from '@tanstack/react-table'
import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import React from "react" import React from 'react'
import { Input } from "@/components/ui/input" import { Input } from '@/components/ui/input'
import { DataTablePagination } from "./data_table_pagination" import { DataTablePagination } from './data_table_pagination'
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, TValue>[]
@@ -33,24 +26,19 @@ interface DataTableProps<TData, TValue> {
table: TableType<TData> table: TableType<TData>
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({ columns, filterColumnName, table }: DataTableProps<TData, TValue>) {
columns,
filterColumnName,
table,
}: DataTableProps<TData, TValue>) {
return ( return (
<div> <div>
{filterColumnName && <div className="flex flex-1 items-center py-4"> {filterColumnName && (
<div className="flex flex-1 items-center py-4">
<Input <Input
placeholder={`根据 ${filterColumnName} 筛选`} placeholder={`根据 ${filterColumnName} 筛选`}
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ""} value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ''}
onChange={(event) => onChange={(event) => table.getColumn(filterColumnName)?.setFilterValue(event.target.value)}
table.getColumn(filterColumnName)?.setFilterValue(event.target.value)
}
className="max-w-sm" className="max-w-sm"
/> />
</div>} </div>
)}
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -59,12 +47,7 @@ export function DataTable<TData, TValue>({
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead key={header.id}>
{header.isPlaceholder {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead> </TableHead>
) )
})} })}
@@ -74,14 +57,9 @@ export function DataTable<TData, TValue>({
<TableBody> <TableBody>
{table.getRowModel().rows?.length ? ( {table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))} ))}
</TableRow> </TableRow>
)) ))

View File

@@ -1,27 +1,14 @@
import { import { ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons'
ChevronLeftIcon, import { Table } from '@tanstack/react-table'
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from "@radix-ui/react-icons"
import { Table } from "@tanstack/react-table"
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
interface DataTablePaginationProps<TData> { interface DataTablePaginationProps<TData> {
table: Table<TData> table: Table<TData>
} }
export function DataTablePagination<TData>({ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
table,
}: DataTablePaginationProps<TData>) {
return ( return (
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
{/* <div className="flex-1 text-sm text-muted-foreground"> {/* <div className="flex-1 text-sm text-muted-foreground">
@@ -50,8 +37,7 @@ export function DataTablePagination<TData>({
<p className="text-sm font-medium"> </p> <p className="text-sm font-medium"> </p>
</div> </div>
<div className="flex w-[120px] items-center justify-center text-sm font-medium"> <div className="flex w-[120px] items-center justify-center text-sm font-medium">
{table.getState().pagination.pageIndex + 1} , {" "} {table.getState().pagination.pageIndex + 1} , {table.getPageCount()}
{table.getPageCount()}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button

View File

@@ -1,33 +1,24 @@
import React, { useEffect } from "react" import React, { useEffect } from 'react'
import { useState } from "react" import { useState } from 'react'
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
Select, import { Label } from '@radix-ui/react-label'
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Label } from "@radix-ui/react-label"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { listServer } from "@/api/server" import { listServer } from '@/api/server'
import { getClient, listClient } from "@/api/client" import { getClient, listClient } from '@/api/client'
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
Card, import { Switch } from './ui/switch'
CardContent, import { FRPCEditor } from './frpc_editor'
CardDescription, import { FRPCForm } from './frpc_form'
CardHeader, import { useSearchParams } from 'next/navigation'
CardTitle,
} from "@/components/ui/card"
import { Switch } from "./ui/switch"
import { FRPCEditor } from "./frpc_editor"
import { FRPCForm } from "./frpc_form"
import { useSearchParams } from "next/navigation"
export interface FRPCFormCardProps { export interface FRPCFormCardProps {
clientID?: string clientID?: string
serverID?: string serverID?: string
} }
export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultClientID, serverID: defaultServerID }: FRPCFormCardProps) => { export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({
clientID: defaultClientID,
serverID: defaultServerID,
}: FRPCFormCardProps) => {
const [advanceMode, setAdvanceMode] = useState<boolean>(false) const [advanceMode, setAdvanceMode] = useState<boolean>(false)
const [clientID, setClientID] = useState<string | undefined>() const [clientID, setClientID] = useState<string | undefined>()
const [serverID, setServerID] = useState<string | undefined>() const [serverID, setServerID] = useState<string | undefined>()
@@ -51,21 +42,24 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
}, [defaultClientID, defaultServerID]) }, [defaultClientID, defaultServerID])
const { data: serverList, refetch: refetchServers } = useQuery({ const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ["listServer"], queryFn: () => { queryKey: ['listServer'],
queryFn: () => {
return listServer({ page: 1, pageSize: 500 }) return listServer({ page: 1, pageSize: 500 })
} },
}); })
const { data: clientList, refetch: refetchClients } = useQuery({ const { data: clientList, refetch: refetchClients } = useQuery({
queryKey: ["listClient"], queryFn: () => { queryKey: ['listClient'],
queryFn: () => {
return listClient({ page: 1, pageSize: 500 }) return listClient({ page: 1, pageSize: 500 })
} },
}) })
const { data: client, refetch: refetchClient } = useQuery({ const { data: client, refetch: refetchClient } = useQuery({
queryKey: ["getClient", clientID], queryFn: () => { queryKey: ['getClient', clientID],
queryFn: () => {
return getClient({ clientId: clientID }) return getClient({ clientId: clientID })
} },
}) })
useEffect(() => { useEffect(() => {
@@ -84,19 +78,15 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
<CardContent> <CardContent>
<div className=" flex items-center space-x-4 rounded-md border p-4"> <div className=" flex items-center space-x-4 rounded-md border p-4">
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none"> <p className="text-sm font-medium leading-none"></p>
<p className="text-sm text-muted-foreground"></p>
</p>
<p className="text-sm text-muted-foreground">
</p>
</div> </div>
<Switch onCheckedChange={setAdvanceMode} /> <Switch onCheckedChange={setAdvanceMode} />
</div> </div>
<div className="flex flex-col w-full pt-2"> <div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label> <Label className="text-sm font-medium"></Label>
<Select onValueChange={handleServerChange} <Select
onValueChange={handleServerChange}
value={serverID} value={serverID}
onOpenChange={() => { onOpenChange={() => {
refetchServers() refetchServers()
@@ -108,12 +98,18 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{serverList?.servers.map( {serverList?.servers.map(
server => server.id && <SelectItem key={server.id} value={server.id}>{server.id} (server) =>
</SelectItem>)} server.id && (
<SelectItem key={server.id} value={server.id}>
{server.id}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
<Label className="text-sm font-medium"></Label> <Label className="text-sm font-medium"></Label>
<Select onValueChange={handleClientChange} <Select
onValueChange={handleClientChange}
value={clientID} value={clientID}
onOpenChange={() => { onOpenChange={() => {
refetchServers() refetchServers()
@@ -125,8 +121,13 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{clientList?.clients.map( {clientList?.clients.map(
client => client.id && <SelectItem key={client.id} value={client.id}>{client.id} (client) =>
</SelectItem>)} client.id && (
<SelectItem key={client.id} value={client.id}>
{client.id}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@@ -1,70 +1,94 @@
import { Label } from "@radix-ui/react-label" import { Label } from '@radix-ui/react-label'
import { Textarea } from "./ui/textarea" import { Textarea } from './ui/textarea'
import { FRPCFormProps } from "./frpc_form" import { FRPCFormProps } from './frpc_form'
import { getClient } from "@/api/client"; import { getClient } from '@/api/client'
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from '@tanstack/react-query'
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
import { Button } from "./ui/button"; import { Button } from './ui/button'
import { updateFRPC } from "@/api/frp"; import { updateFRPC } from '@/api/frp'
import { useToast } from "./ui/use-toast"; import { useToast } from './ui/use-toast'
import { RespCode } from "@/lib/pb/common"; import { RespCode } from '@/lib/pb/common'
export const FRPCEditor: React.FC<FRPCFormProps> = ({ clientID, serverID }) => { export const FRPCEditor: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
const { toast } = useToast() const { toast } = useToast()
const { data: client, refetch: refetchClient } = useQuery({ const { data: client, refetch: refetchClient } = useQuery({
queryKey: ["getClient", clientID], queryFn: () => { queryKey: ['getClient', clientID],
queryFn: () => {
return getClient({ clientId: clientID }) return getClient({ clientId: clientID })
} },
}); })
const [configContent, setConfigContent] = useState<string>("{}") const [configContent, setConfigContent] = useState<string>('{}')
const updateFrpc = useMutation({ mutationFn: updateFRPC, }) const updateFrpc = useMutation({ mutationFn: updateFRPC })
const [editorValue, setEditorValue] = useState<string>("") const [editorValue, setEditorValue] = useState<string>('')
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
let res = await updateFrpc.mutateAsync({ let res = await updateFrpc.mutateAsync({
clientId: clientID, config: Buffer.from(editorValue), serverId: serverID clientId: clientID,
config: Buffer.from(editorValue),
serverId: serverID,
}) })
if (res.status?.code !== RespCode.SUCCESS) { if (res.status?.code !== RespCode.SUCCESS) {
toast({ title: "更新失败" }) toast({ title: '更新失败' })
return return
} }
toast({ title: "更新成功" }) toast({ title: '更新成功' })
} catch (error) { } catch (error) {
toast({ title: "更新失败" }) toast({ title: '更新失败' })
} }
} }
useEffect(() => { useEffect(() => {
refetchClient() refetchClient()
try { try {
setConfigContent(JSON.stringify(JSON.parse(client?.client?.config == undefined ? "{}" || setConfigContent(
client?.client?.config == "" : client?.client?.config), null, 2)) JSON.stringify(
setEditorValue(JSON.stringify(JSON.parse(client?.client?.config == undefined || JSON.parse(
client?.client?.config == "" ? "{}" : client?.client?.config), null, 2)) client?.client?.config == undefined ? '{}' || client?.client?.config == '' : client?.client?.config,
),
null,
2,
),
)
setEditorValue(
JSON.stringify(
JSON.parse(
client?.client?.config == undefined || client?.client?.config == '' ? '{}' : client?.client?.config,
),
null,
2,
),
)
} catch (error) { } catch (error) {
setConfigContent("{}") setConfigContent('{}')
setEditorValue("{}") setEditorValue('{}')
} }
}, [client, refetchClient]) }, [client, refetchClient])
return (<div className="grid w-full gap-1.5"> return (
<div className="grid w-full gap-1.5">
<Label className="text-sm font-medium"> {clientID} `frpc.json`</Label> <Label className="text-sm font-medium"> {clientID} `frpc.json`</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
proxies和visitors字段 proxies和visitors字段
</p> </p>
<Textarea key={configContent} placeholder="配置文件内容" id="message" <Textarea
key={configContent}
placeholder="配置文件内容"
id="message"
defaultValue={configContent} defaultValue={configContent}
onChange={(e) => setEditorValue(e.target.value)} onChange={(e) => setEditorValue(e.target.value)}
className="h-72" className="h-72"
/> />
<div className="grid grid-cols-2 gap-2 mt-1"> <div className="grid grid-cols-2 gap-2 mt-1">
<Button size="sm" onClick={handleSubmit} ></Button> <Button size="sm" onClick={handleSubmit}>
</Button>
{/* <Button variant="outline" size="sm" onClick={async () => { {/* <Button variant="outline" size="sm" onClick={async () => {
await refetchClient() await refetchClient()
setConfigContent(client?.client?.config == undefined ? "{}" : client?.client?.config) setConfigContent(client?.client?.config == undefined ? "{}" : client?.client?.config)
}}>加载服务端配置</Button> */} }}>加载服务端配置</Button> */}
</div> </div>
</div>) </div>
)
} }

View File

@@ -1,40 +1,24 @@
import { ProxyType, TypedProxyConfig } from "@/types/proxy" import { ProxyType, TypedProxyConfig } from '@/types/proxy'
import React, { useEffect } from "react" import React, { useEffect } from 'react'
import { useState } from "react" import { useState } from 'react'
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
Select, import { Label } from '@radix-ui/react-label'
SelectContent, import { HTTPProxyForm, TCPProxyForm, UDPProxyForm } from './proxy_form'
SelectItem, import { useQuery } from '@tanstack/react-query'
SelectTrigger, import { getClient } from '@/api/client'
SelectValue, import { useStore } from '@nanostores/react'
} from "@/components/ui/select" import { $clientProxyConfigs } from '@/store/proxy'
import { Label } from "@radix-ui/react-label" import { Button } from './ui/button'
import { HTTPProxyForm, TCPProxyForm, UDPProxyForm } from "./proxy_form" import { RespCode } from '@/lib/pb/common'
import { useQuery } from "@tanstack/react-query" import { ClientConfig } from '@/types/client'
import { getClient } from "@/api/client" import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useStore } from "@nanostores/react" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { $clientProxyConfigs } from "@/store/proxy" import { Input } from './ui/input'
import { Button } from "./ui/button" import { AccordionHeader } from '@radix-ui/react-accordion'
import { RespCode } from "@/lib/pb/common" import { useToast } from './ui/use-toast'
import { ClientConfig } from "@/types/client"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Input } from "./ui/input"
import { AccordionHeader } from "@radix-ui/react-accordion"
import { useToast } from "./ui/use-toast"
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { updateFRPC } from "@/api/frp" import { updateFRPC } from '@/api/frp'
import { Card, CardContent } from "./ui/card" import { Card, CardContent } from './ui/card'
export interface FRPCFormProps { export interface FRPCFormProps {
clientID: string clientID: string
@@ -50,10 +34,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
} }
const { data: client, refetch: refetchClient } = useQuery({ const { data: client, refetch: refetchClient } = useQuery({
queryKey: ["getClient", clientID], queryFn: () => { queryKey: ['getClient', clientID],
queryFn: () => {
return getClient({ clientId: clientID }) return getClient({ clientId: clientID })
} },
}); })
const clientProxyConfigs = useStore($clientProxyConfigs) const clientProxyConfigs = useStore($clientProxyConfigs)
@@ -66,11 +51,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
}, [clientID, serverID, client]) }, [clientID, serverID, client])
const handleAddProxy = () => { const handleAddProxy = () => {
console.log("add proxy", proxyName, proxyType) console.log('add proxy', proxyName, proxyType)
if (!proxyName) return if (!proxyName) return
if (!proxyType) return if (!proxyType) return
if (clientProxyConfigs.findIndex(proxy => proxy.name === proxyName) !== -1) { if (clientProxyConfigs.findIndex((proxy) => proxy.name === proxyName) !== -1) {
toast({ title: "创建隧道状态", description: "名称重复" }) toast({ title: '创建隧道状态', description: '名称重复' })
return return
} }
const newProxy = { const newProxy = {
@@ -81,23 +66,27 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
} }
const handleDeleteProxy = (proxyName: string) => { const handleDeleteProxy = (proxyName: string) => {
const newProxies = clientProxyConfigs.filter(proxy => proxy.name !== proxyName) const newProxies = clientProxyConfigs.filter((proxy) => proxy.name !== proxyName)
$clientProxyConfigs.set(newProxies) $clientProxyConfigs.set(newProxies)
} }
const updateFrpc = useMutation({ mutationFn: updateFRPC, }) const updateFrpc = useMutation({ mutationFn: updateFRPC })
const handleUpdate = async () => { const handleUpdate = async () => {
try { try {
const res = await updateFrpc.mutateAsync({ const res = await updateFrpc.mutateAsync({
config: Buffer.from(JSON.stringify({ config: Buffer.from(
JSON.stringify({
proxies: clientProxyConfigs, proxies: clientProxyConfigs,
} as ClientConfig)), serverId: serverID, clientId: clientID } as ClientConfig),
),
serverId: serverID,
clientId: clientID,
}) })
toast({ title: "更新隧道状态", description: res.status?.code === RespCode.SUCCESS ? "更新成功" : "更新失败" }) toast({ title: '更新隧道状态', description: res.status?.code === RespCode.SUCCESS ? '更新成功' : '更新失败' })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast({ title: "更新隧道状态", description: "更新失败" }) toast({ title: '更新隧道状态', description: '更新失败' })
} }
} }
@@ -107,9 +96,13 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button className="my-2"></Button> <Button className="my-2"></Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent > <PopoverContent>
<Label className="text-sm font-medium"></Label> <Label className="text-sm font-medium"></Label>
<Input onChange={(e) => { setProxyName(e.target.value) }} /> <Input
onChange={(e) => {
setProxyName(e.target.value)
}}
/>
<Select onValueChange={handleTypeChange} defaultValue={proxyType}> <Select onValueChange={handleTypeChange} defaultValue={proxyType}>
<Label className="text-sm font-medium"></Label> <Label className="text-sm font-medium"></Label>
<SelectTrigger className="my-2"> <SelectTrigger className="my-2">
@@ -121,61 +114,73 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
<SelectItem value="udp">udp</SelectItem> <SelectItem value="udp">udp</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button variant={"outline"} onClick={handleAddProxy}></Button> <Button variant={'outline'} onClick={handleAddProxy}>
</Button>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4" key={clientID + serverID + client}> <div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4" key={clientID + serverID + client}>
{ {clientProxyConfigs.map((item) => {
clientProxyConfigs.map((item) => { return (
return (<Card key={item.name}> <Card key={item.name}>
<CardContent> <CardContent>
<div className="flex flex-col w-full pt-2"> <div className="flex flex-col w-full pt-2">
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value={item.name}> <AccordionItem value={item.name}>
<AccordionHeader className="flex flex-row justify-between"> <AccordionHeader className="flex flex-row justify-between">
<div>{item.name}</div> <div>{item.name}</div>
<Button variant={"outline"} onClick={() => { handleDeleteProxy(item.name) }}></Button> <Button
variant={'outline'}
onClick={() => {
handleDeleteProxy(item.name)
}}
>
</Button>
</AccordionHeader> </AccordionHeader>
<AccordionTrigger>:{item.type}</AccordionTrigger> <AccordionTrigger>:{item.type}</AccordionTrigger>
<AccordionContent> <AccordionContent>
{ {item.type === 'tcp' && serverID && clientID && (
item.type === 'tcp' && serverID && clientID &&
<TCPProxyForm <TCPProxyForm
defaultProxyConfig={item} defaultProxyConfig={item}
proxyName={item.name} proxyName={item.name}
serverID={serverID} serverID={serverID}
clientID={clientID} clientID={clientID}
/> />
} )}
{ {item.type === 'udp' && serverID && clientID && (
item.type === 'udp' && serverID && clientID && <UDPProxyForm
< UDPProxyForm
defaultProxyConfig={item} defaultProxyConfig={item}
proxyName={item.name} proxyName={item.name}
serverID={serverID} serverID={serverID}
clientID={clientID} clientID={clientID}
/> />
} )}
{ {item.type === 'http' && serverID && clientID && (
item.type === 'http' && serverID && clientID && <HTTPProxyForm
< HTTPProxyForm
defaultProxyConfig={item} defaultProxyConfig={item}
proxyName={item.name} proxyName={item.name}
serverID={serverID} serverID={serverID}
clientID={clientID} clientID={clientID}
/> />
} )}
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div > </div>
</CardContent> </CardContent>
</Card> </Card>
) )
}) })}
}
</div> </div>
<Button className="mt-2" onClick={() => { handleUpdate() }}></Button> <Button
className="mt-2"
onClick={() => {
handleUpdate()
}}
>
</Button>
</> </>
) )
} }

View File

@@ -1,20 +1,13 @@
import { useEffect, useState } from "react" import { useEffect, useState } from 'react'
import { import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
Card, import { Label } from './ui/label'
CardContent, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
CardDescription, import { getServer, listServer } from '@/api/server'
CardFooter, import { useQuery } from '@tanstack/react-query'
CardHeader, import { Switch } from './ui/switch'
CardTitle, import { FRPSEditor } from './frps_editor'
} from "@/components/ui/card" import FRPSForm from './frps_form'
import { Label } from "./ui/label" import { useSearchParams } from 'next/navigation'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { getServer, listServer } from "@/api/server"
import { useQuery } from "@tanstack/react-query"
import { Switch } from "./ui/switch"
import { FRPSEditor } from "./frps_editor"
import FRPSForm from "./frps_form"
import { useSearchParams } from "next/navigation"
export interface FRPSFormCardProps { export interface FRPSFormCardProps {
serverID?: string serverID?: string
@@ -25,15 +18,17 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
const searchParams = useSearchParams() const searchParams = useSearchParams()
const paramServerID = searchParams.get('serverID') const paramServerID = searchParams.get('serverID')
const { data: serverList, refetch: refetchServers } = useQuery({ const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ["listServer"], queryFn: () => { queryKey: ['listServer'],
queryFn: () => {
return listServer({ page: 1, pageSize: 100 }) return listServer({ page: 1, pageSize: 100 })
} },
}); })
const { data: server, refetch: refetchServer } = useQuery({ const { data: server, refetch: refetchServer } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => { queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID }) return getServer({ serverId: serverID })
} },
}); })
useEffect(() => { useEffect(() => {
if (defaultServerID) { if (defaultServerID) {
@@ -51,7 +46,8 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
setServerID(value) setServerID(value)
} }
return (<Card className="w-full"> return (
<Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>Frps服务</CardDescription> <CardDescription>Frps服务</CardDescription>
@@ -59,18 +55,15 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
<CardContent> <CardContent>
<div className=" flex items-center space-x-4 rounded-md border p-4"> <div className=" flex items-center space-x-4 rounded-md border p-4">
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none"> <p className="text-sm font-medium leading-none"></p>
<p className="text-sm text-muted-foreground"></p>
</p>
<p className="text-sm text-muted-foreground">
</p>
</div> </div>
<Switch onCheckedChange={setAdvanceMode} /> <Switch onCheckedChange={setAdvanceMode} />
</div> </div>
<div className="flex flex-col w-full pt-2"> <div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label> <Label className="text-sm font-medium"></Label>
<Select onValueChange={handleServerChange} <Select
onValueChange={handleServerChange}
value={serverID} value={serverID}
onOpenChange={() => { onOpenChange={() => {
refetchServers() refetchServers()
@@ -80,17 +73,24 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
<SelectTrigger className="my-2"> <SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" /> <SelectValue placeholder="节点名称" />
</SelectTrigger> </SelectTrigger>
<SelectContent > <SelectContent>
{serverList?.servers.map( {serverList?.servers.map(
serverItem => serverItem.id && <SelectItem key={serverItem.id} value={serverItem.id}>{serverItem.id} (serverItem) =>
</SelectItem>)} serverItem.id && (
<SelectItem key={serverItem.id} value={serverItem.id}>
{serverItem.id}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{serverID && server && server.server && !advanceMode && <FRPSForm serverID={serverID} server={server.server} />} {serverID && server && server.server && !advanceMode && <FRPSForm serverID={serverID} server={server.server} />}
{serverID && server && server.server && advanceMode && <FRPSEditor serverID={serverID} server={server.server} />} {serverID && server && server.server && advanceMode && (
<FRPSEditor serverID={serverID} server={server.server} />
)}
</CardContent> </CardContent>
<CardFooter> <CardFooter></CardFooter>
</CardFooter> </Card>
</Card>) )
} }

View File

@@ -1,76 +1,104 @@
import { Label } from "@radix-ui/react-label" import { Label } from '@radix-ui/react-label'
import { Textarea } from "./ui/textarea" import { Textarea } from './ui/textarea'
import { FRPSFormProps } from "./frps_form" import { FRPSFormProps } from './frps_form'
import { Button } from "./ui/button" import { Button } from './ui/button'
import { useToast } from "./ui/use-toast" import { useToast } from './ui/use-toast'
import { useMutation, useQuery } from "@tanstack/react-query" import { useMutation, useQuery } from '@tanstack/react-query'
import { getServer } from "@/api/server" import { getServer } from '@/api/server'
import { useEffect, useState } from "react" import { useEffect, useState } from 'react'
import { updateFRPS } from "@/api/frp" import { updateFRPS } from '@/api/frp'
import { RespCode } from "@/lib/pb/common" import { RespCode } from '@/lib/pb/common'
export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => { export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => {
const { toast } = useToast() const { toast } = useToast()
const { data: serverResp, refetch: refetchServer } = useQuery({ const { data: serverResp, refetch: refetchServer } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => { queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID }) return getServer({ serverId: serverID })
} },
}); })
const [configContent, setConfigContent] = useState<string>("{}") const [configContent, setConfigContent] = useState<string>('{}')
const updateFrps = useMutation({ mutationFn: updateFRPS, }) const updateFrps = useMutation({ mutationFn: updateFRPS })
const [editorValue, setEditorValue] = useState<string>("") const [editorValue, setEditorValue] = useState<string>('')
const [serverComment, setServerComment] = useState<string>("") const [serverComment, setServerComment] = useState<string>('')
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
let res = await updateFrps.mutateAsync({ let res = await updateFrps.mutateAsync({
serverId: serverID, config: Buffer.from(editorValue), serverId: serverID,
comment: serverComment config: Buffer.from(editorValue),
comment: serverComment,
}) })
if (res.status?.code !== RespCode.SUCCESS) { if (res.status?.code !== RespCode.SUCCESS) {
toast({ title: "更新失败" }) toast({ title: '更新失败' })
return return
} }
toast({ title: "更新成功" }) toast({ title: '更新成功' })
} catch (error) { } catch (error) {
toast({ title: "更新失败" }) toast({ title: '更新失败' })
} }
} }
useEffect(() => { useEffect(() => {
refetchServer() refetchServer()
try { try {
setConfigContent(JSON.stringify(JSON.parse(serverResp?.server?.config == undefined || setConfigContent(
serverResp?.server?.config == "" ? "{}" : serverResp?.server?.config), null, 2)) JSON.stringify(
setEditorValue(JSON.stringify(JSON.parse(serverResp?.server?.config == undefined || JSON.parse(
serverResp?.server?.config == "" ? "{}" : serverResp?.server?.config), null, 2)) serverResp?.server?.config == undefined || serverResp?.server?.config == ''
setServerComment(serverResp?.server?.comment || "") ? '{}'
: serverResp?.server?.config,
),
null,
2,
),
)
setEditorValue(
JSON.stringify(
JSON.parse(
serverResp?.server?.config == undefined || serverResp?.server?.config == ''
? '{}'
: serverResp?.server?.config,
),
null,
2,
),
)
setServerComment(serverResp?.server?.comment || '')
} catch (error) { } catch (error) {
setConfigContent("{}") setConfigContent('{}')
setEditorValue("{}") setEditorValue('{}')
setServerComment("") setServerComment('')
} }
}, [serverResp, refetchServer]) }, [serverResp, refetchServer])
return (<div className="grid w-full gap-1.5"> return (
<div className="grid w-full gap-1.5">
<Label className="text-sm font-medium"> {serverID} </Label> <Label className="text-sm font-medium"> {serverID} </Label>
<Textarea key={serverResp?.server?.comment} <Textarea
placeholder="备注" id="message" defaultValue={serverResp?.server?.comment} key={serverResp?.server?.comment}
placeholder="备注"
id="message"
defaultValue={serverResp?.server?.comment}
onChange={(e) => setServerComment(e.target.value)} onChange={(e) => setServerComment(e.target.value)}
className="h-12" className="h-12"
/> />
<Label className="text-sm font-medium"> {serverID} `frps.json`</Label> <Label className="text-sm font-medium"> {serverID} `frps.json`</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">IP等字段</p>
IP等字段 <Textarea
</p> key={configContent}
<Textarea key={configContent} placeholder="配置文件内容"
placeholder="配置文件内容" id="message" defaultValue={configContent} id="message"
defaultValue={configContent}
onChange={(e) => setEditorValue(e.target.value)} onChange={(e) => setEditorValue(e.target.value)}
className="h-72" className="h-72"
/> />
<div className="grid grid-cols-2 gap-2 mt-1"> <div className="grid grid-cols-2 gap-2 mt-1">
<Button size="sm" onClick={handleSubmit} ></Button> <Button size="sm" onClick={handleSubmit}>
</Button>
</div> </div>
</div>) </div>
)
} }

View File

@@ -1,35 +1,27 @@
import { ServerConfig } from "@/types/server" import { ServerConfig } from '@/types/server'
import { useEffect, useState } from "react" import { useEffect, useState } from 'react'
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from "react-hook-form" import { useForm } from 'react-hook-form'
import * as z from "zod" import * as z from 'zod'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
Form, import { Input } from '@/components/ui/input'
FormControl, import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from '@/lib/consts'
FormField, import { RespCode, Server } from '@/lib/pb/common'
FormItem, import { updateFRPS } from '@/api/frp'
FormLabel, import { useMutation } from '@tanstack/react-query'
FormMessage, import { useToast } from './ui/use-toast'
} from "@/components/ui/form" import { Label } from '@radix-ui/react-label'
import { Input } from "@/components/ui/input"
import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from "@/lib/consts"
import { RespCode, Server } from "@/lib/pb/common"
import { updateFRPS } from "@/api/frp"
import { useMutation } from "@tanstack/react-query"
import { useToast } from "./ui/use-toast"
import { Label } from "@radix-ui/react-label"
const ServerConfigSchema = z.object({ const ServerConfigSchema = z.object({
bindAddr: ZodIPSchema.default("0.0.0.0").optional(), bindAddr: ZodIPSchema.default('0.0.0.0').optional(),
bindPort: ZodPortSchema.default(7000), bindPort: ZodPortSchema.default(7000),
proxyBindAddr: ZodIPSchema.optional(), proxyBindAddr: ZodIPSchema.optional(),
vhostHTTPPort: ZodPortSchema.optional(), vhostHTTPPort: ZodPortSchema.optional(),
subDomainHost: ZodStringSchema.optional(), subDomainHost: ZodStringSchema.optional(),
}); })
export const ServerConfigZodSchema = ServerConfigSchema;
export const ServerConfigZodSchema = ServerConfigSchema
export interface FRPSFormProps { export interface FRPSFormProps {
serverID: string serverID: string
@@ -51,24 +43,27 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
}, []) }, [])
useEffect(() => { useEffect(() => {
form.reset(JSON.parse(server?.config || "{}") as ServerConfig) form.reset(JSON.parse(server?.config || '{}') as ServerConfig)
}, [server]) }, [server])
const onSubmit = async (values: z.infer<typeof ServerConfigZodSchema>) => { const onSubmit = async (values: z.infer<typeof ServerConfigZodSchema>) => {
setFrpsConfig({ ...values }) setFrpsConfig({ ...values })
try { try {
let resp = await updateFrps.mutateAsync({ let resp = await updateFrps.mutateAsync({
serverId: serverID, config: Buffer.from(JSON.stringify({ serverId: serverID,
...values config: Buffer.from(
} as ServerConfig)) JSON.stringify({
...values,
} as ServerConfig),
),
}) })
toast({ toast({
title: resp.status?.code === RespCode.SUCCESS ? "创建成功" : "创建失败", title: resp.status?.code === RespCode.SUCCESS ? '创建成功' : '创建失败',
description: resp.status?.message, description: resp.status?.message,
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast({ title: "创建服务端状态", description: "创建失败" }) toast({ title: '创建服务端状态', description: '创建失败' })
} }
} }
return ( return (
@@ -76,9 +71,10 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
<Label className="text-sm font-medium"> {serverID} </Label> <Label className="text-sm font-medium"> {serverID} </Label>
<p className="text-sm text-muted-foreground"></p> <p className="text-sm text-muted-foreground"></p>
<p className="text-sm border rounded p-2 my-2"> <p className="text-sm border rounded p-2 my-2">
{server?.comment == undefined || server?.comment === "" ? "空空如也" : server?.comment} {server?.comment == undefined || server?.comment === '' ? '空空如也' : server?.comment}
</p> </p>
{serverID && <Form {...form}> {serverID && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -149,11 +145,10 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
/> />
<Button type="submit"></Button> <Button type="submit"></Button>
</form> </form>
</Form>} </Form>
</div > )}
); </div>
}; )
}
export default FRPSForm export default FRPSForm

View File

@@ -1,28 +1,24 @@
import { TbBuildingTunnel } from "react-icons/tb" import { TbBuildingTunnel } from 'react-icons/tb'
import { Button } from "./ui/button" import { Button } from './ui/button'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { useRouter } from "next/router"; import { useRouter } from 'next/router'
import { $platformInfo, $userInfo } from "@/store/user"; import { $platformInfo, $userInfo } from '@/store/user'
import { getUserInfo } from "@/api/user"; import { getUserInfo } from '@/api/user'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query'
import { useEffect } from "react"; import { useEffect } from 'react'
import Gravatar from 'react-gravatar' import Gravatar from 'react-gravatar'
import { import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
Avatar, import { LOCAL_STORAGE_TOKEN_KEY } from '@/lib/consts'
AvatarFallback, import { logout } from '@/api/auth'
AvatarImage, import { getPlatformInfo } from '@/api/platform'
} from "@/components/ui/avatar"
import { LOCAL_STORAGE_TOKEN_KEY } from "@/lib/consts";
import { logout } from "@/api/auth";
import { getPlatformInfo } from "@/api/platform";
export const Header = () => { export const Header = () => {
const router = useRouter(); const router = useRouter()
const userInfo = useStore($userInfo) const userInfo = useStore($userInfo)
const platformInfo = useQuery({ const platformInfo = useQuery({
queryKey: ['platformInfo'], queryKey: ['platformInfo'],
queryFn: getPlatformInfo queryFn: getPlatformInfo,
}) })
useEffect(() => { useEffect(() => {
@@ -30,8 +26,8 @@ export const Header = () => {
}, [platformInfo]) }, [platformInfo])
const userInfoQuery = useQuery({ const userInfoQuery = useQuery({
queryKey: ["userInfo"], queryKey: ['userInfo'],
queryFn: getUserInfo queryFn: getUserInfo,
}) })
useEffect(() => { useEffect(() => {
@@ -39,27 +35,48 @@ export const Header = () => {
}, [userInfoQuery]) }, [userInfoQuery])
const redirToHome = () => { const redirToHome = () => {
router.push("/") router.push('/')
} }
return ( return (
<div className="flex flex-row h-10 items-center px-4 border-b"> <div className="flex flex-row h-10 items-center px-4 border-b">
<TbBuildingTunnel /> <TbBuildingTunnel />
<p className="ml-2 font-mono" onClick={redirToHome}>frp-panel</p> <p className="ml-2 font-mono" onClick={redirToHome}>
{!userInfo && <Button variant={"ghost"} className="ml-auto" size={"sm"} onClick={() => router.push("/login")}></Button>} frp-panel
{!userInfo && <Button variant={"ghost"} className="ml-2" size={"sm"} onClick={() => router.push("/register")}></Button>} </p>
{userInfo && <Button variant={"ghost"} className="ml-auto" size={"sm"} onClick={async () => { {!userInfo && (
<Button variant={'ghost'} className="ml-auto" size={'sm'} onClick={() => router.push('/login')}>
</Button>
)}
{!userInfo && (
<Button variant={'ghost'} className="ml-2" size={'sm'} onClick={() => router.push('/register')}>
</Button>
)}
{userInfo && (
<Button
variant={'ghost'}
className="ml-auto"
size={'sm'}
onClick={async () => {
$userInfo.set(undefined) $userInfo.set(undefined)
localStorage.removeItem(LOCAL_STORAGE_TOKEN_KEY) localStorage.removeItem(LOCAL_STORAGE_TOKEN_KEY)
await logout() await logout()
window.location.reload() window.location.reload()
}}>退</Button>} }}
{userInfo && <Avatar className="ml-2 w-7 h-7"> >
<AvatarImage alt={"@" + userInfo.userName} asChild> 退
</Button>
)}
{userInfo && (
<Avatar className="ml-2 w-7 h-7">
<AvatarImage alt={'@' + userInfo.userName} asChild>
<Gravatar email={userInfo.email} /> <Gravatar email={userInfo.email} />
</AvatarImage> </AvatarImage>
<AvatarFallback>{userInfo.userName}</AvatarFallback> <AvatarFallback>{userInfo.userName}</AvatarFallback>
</Avatar>} </Avatar>
)}
</div> </div>
) )
} }

View File

@@ -1,21 +1,28 @@
import { Providers } from "./providers" import { Providers } from './providers'
import { Toaster } from "./ui/toaster" import { Toaster } from './ui/toaster'
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
export const RootLayout = ({ children, header, sidebar }: export const RootLayout = ({
{ children: React.ReactNode, header: React.ReactNode, sidebar?: React.ReactNode }) => { children,
header,
sidebar,
}: {
children: React.ReactNode
header: React.ReactNode
sidebar?: React.ReactNode
}) => {
return ( return (
<main className={`${inter.className}`}> <main className={`${inter.className}`}>
<div><Providers>{header}</Providers></div> <div>
<Providers>{header}</Providers>
</div>
<div className="flex"> <div className="flex">
{sidebar} {sidebar}
<div className="my-2 ml-0 mr-2 max-w-full w-full"> <div className="my-2 ml-0 mr-2 max-w-full w-full">{children}</div>
{children}
</div>
</div> </div>
<Toaster /> <Toaster />
</main > </main>
) )
} }

View File

@@ -1,33 +1,23 @@
import { ZodStringSchema } from "@/lib/consts" import { ZodStringSchema } from '@/lib/consts'
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from "react-hook-form" import { useForm } from 'react-hook-form'
import * as z from "zod" import * as z from 'zod'
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'
Form, import { Input } from './ui/input'
FormControl, import { login } from '@/api/auth'
FormField, import { Button } from './ui/button'
FormItem,
FormMessage,
} from "@/components/ui/form"
import { Input } from "./ui/input"
import { login } from "@/api/auth"
import { Button } from "./ui/button"
import { ExclamationTriangleIcon } from "@radix-ui/react-icons" import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
import { import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
Alert, import { useState } from 'react'
AlertDescription, import { useToast } from './ui/use-toast'
AlertTitle, import { RespCode } from '@/lib/pb/common'
} from "@/components/ui/alert" import { useRouter } from 'next/router'
import { useState } from "react"
import { useToast } from "./ui/use-toast"
import { RespCode } from "@/lib/pb/common"
import { useRouter } from "next/router"
export const LoginSchema = z.object({ export const LoginSchema = z.object({
username: ZodStringSchema, username: ZodStringSchema,
password: ZodStringSchema password: ZodStringSchema,
}) })
export const LoginComponent = () => { export const LoginComponent = () => {
@@ -40,22 +30,22 @@ export const LoginComponent = () => {
const [loginAlert, setLoginAlert] = useState(false) const [loginAlert, setLoginAlert] = useState(false)
const onSubmit = async (values: z.infer<typeof LoginSchema>) => { const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
toast({ title: "登录中,请稍候" }) toast({ title: '登录中,请稍候' })
try { try {
const res = await login({ ...values }) const res = await login({ ...values })
if (res.status?.code === RespCode.SUCCESS) { if (res.status?.code === RespCode.SUCCESS) {
toast({ title: "登录成功,正在跳转到首页" }) toast({ title: '登录成功,正在跳转到首页' })
setTimeout(() => { setTimeout(() => {
router.push('/'); router.push('/')
}, 3000); }, 3000)
setLoginAlert(false) setLoginAlert(false)
} else { } else {
toast({ title: "登录失败" }) toast({ title: '登录失败' })
setLoginAlert(true) setLoginAlert(true)
} }
} catch (e) { } catch (e) {
toast({ title: "登录失败" }) toast({ title: '登录失败' })
console.log("login error", e) console.log('login error', e)
setLoginAlert(true) setLoginAlert(true)
} }
} }
@@ -63,21 +53,14 @@ export const LoginComponent = () => {
return ( return (
<div className="w-full flex flex-col gap-6"> <div className="w-full flex flex-col gap-6">
<Form {...form}> <Form {...form}>
<form <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<FormField <FormField
control={form.control} control={form.control}
name="username" name="username"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input type="text" placeholder="用户名" {...field} />
type="text"
placeholder="用户名"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -89,23 +72,19 @@ export const LoginComponent = () => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input type="password" placeholder="密码" {...field} />
type="password"
placeholder="密码"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{loginAlert && <Alert variant="destructive"> {loginAlert && (
<Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" /> <ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle></AlertTitle> <AlertTitle></AlertTitle>
<AlertDescription> <AlertDescription></AlertDescription>
</Alert>
</AlertDescription> )}
</Alert>}
<Button type="submit"></Button> <Button type="submit"></Button>
</form> </form>
</Form> </Form>

View File

@@ -1,13 +1,13 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb'; import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb'
import { useEffect } from 'react'; import { useEffect } from 'react'
import { $platformInfo } from '@/store/user'; import { $platformInfo } from '@/store/user'
import { getPlatformInfo } from '@/api/platform'; import { getPlatformInfo } from '@/api/platform'
export const PlatformInfo = () => { export const PlatformInfo = () => {
const platformInfo = useQuery({ const platformInfo = useQuery({
queryKey: ['platformInfo'], queryKey: ['platformInfo'],
queryFn: getPlatformInfo queryFn: getPlatformInfo,
}) })
useEffect(() => { useEffect(() => {
$platformInfo.set(platformInfo.data) $platformInfo.set(platformInfo.data)
@@ -16,85 +16,73 @@ export const PlatformInfo = () => {
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div className='flex justify-between'> <div className="flex justify-between">
<h3 className='tracking-tight text-sm font-medium'> <h3 className="tracking-tight text-sm font-medium"></h3>
<TbServerBolt className="mt-1" />
</h3>
<TbServerBolt className='mt-1' />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.configuredServerCount} </div> <div className="text-2xl font-bold">{platformInfo.data?.configuredServerCount} </div>
<p className="text-xs text-muted-foreground">🫲</p> <p className="text-xs text-muted-foreground">🫲</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<div className='flex justify-between'> <div className="flex justify-between">
<h3 className='tracking-tight text-sm font-medium'> <h3 className="tracking-tight text-sm font-medium"></h3>
<TbEngine className="mt-1" />
</h3>
<TbEngine className='mt-1' />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.configuredClientCount} </div> <div className="text-2xl font-bold">{platformInfo.data?.configuredClientCount} </div>
<p className="text-xs text-muted-foreground">🫲</p> <p className="text-xs text-muted-foreground">🫲</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<div className='flex justify-between'> <div className="flex justify-between">
<h3 className='tracking-tight text-sm font-medium'> <h3 className="tracking-tight text-sm font-medium"></h3>
<TbServerOff className="mt-1" />
</h3>
<TbServerOff className='mt-1' />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.unconfiguredServerCount} </div> <div className="text-2xl font-bold">{platformInfo.data?.unconfiguredServerCount} </div>
<p className="text-xs text-muted-foreground">🫲</p> <p className="text-xs text-muted-foreground">🫲</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<div className='flex justify-between'> <div className="flex justify-between">
<h3 className='tracking-tight text-sm font-medium'> <h3 className="tracking-tight text-sm font-medium"></h3>
<TbEngineOff className="mt-1" />
</h3>
<TbEngineOff className='mt-1' />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.unconfiguredClientCount} </div> <div className="text-2xl font-bold">{platformInfo.data?.unconfiguredClientCount} </div>
<p className="text-xs text-muted-foreground">🫲</p> <p className="text-xs text-muted-foreground">🫲</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<div className='flex justify-between'> <div className="flex justify-between">
<h3 className='tracking-tight text-sm font-medium'> <h3 className="tracking-tight text-sm font-medium"></h3>
<TbServer2 className="mt-1" />
</h3>
<TbServer2 className='mt-1' />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.totalServerCount} </div> <div className="text-2xl font-bold">{platformInfo.data?.totalServerCount} </div>
<p className="text-xs text-muted-foreground">🫲</p> <p className="text-xs text-muted-foreground">🫲</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<div className='flex justify-between'> <div className="flex justify-between">
<h3 className='tracking-tight text-sm font-medium'> <h3 className="tracking-tight text-sm font-medium"></h3>
<TbDeviceHeartMonitor className="mt-1" />
</h3>
<TbDeviceHeartMonitor className='mt-1' />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.totalClientCount} </div> <div className="text-2xl font-bold">{platformInfo.data?.totalClientCount} </div>
<p className="text-xs text-muted-foreground">🫲</p> <p className="text-xs text-muted-foreground">🫲</p>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,17 +1,13 @@
import React from 'react' import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { import { TooltipProvider } from '@/components/ui/tooltip'
TooltipProvider,
} from "@/components/ui/tooltip"
const queryClient = new QueryClient() const queryClient = new QueryClient()
export const Providers = ({ children }: { children: React.ReactNode }) => { export const Providers = ({ children }: { children: React.ReactNode }) => {
return ( return (
<TooltipProvider> <TooltipProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
{children}
</QueryClientProvider>
</TooltipProvider> </TooltipProvider>
) )
} }

View File

@@ -1,42 +1,35 @@
import { HTTPProxyConfig, TCPProxyConfig, TypedProxyConfig, UDPProxyConfig } from "@/types/proxy" import { HTTPProxyConfig, TCPProxyConfig, TypedProxyConfig, UDPProxyConfig } from '@/types/proxy'
import * as z from "zod" import * as z from 'zod'
import React from "react" import React from 'react'
import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from "@/lib/consts" import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from '@/lib/consts'
import { useEffect, useState } from "react" import { useEffect, useState } from 'react'
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from "react-hook-form" import { useForm } from 'react-hook-form'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
Form, import { Input } from '@/components/ui/input'
FormControl, import { $clientProxyConfigs } from '@/store/proxy'
FormField, import { useStore } from '@nanostores/react'
FormItem, import { YesIcon } from './ui/icon'
FormLabel, import { Label } from './ui/label'
FormMessage, import { useQuery } from '@tanstack/react-query'
} from "@/components/ui/form" import { getServer } from '@/api/server'
import { Input } from "@/components/ui/input" import { ServerConfig } from '@/types/server'
import { $clientProxyConfigs } from "@/store/proxy"
import { useStore } from "@nanostores/react"
import { YesIcon } from "./ui/icon"
import { Label } from "./ui/label"
import { useQuery } from "@tanstack/react-query"
import { getServer } from "@/api/server"
import { ServerConfig } from "@/types/server"
export const TCPConfigSchema = z.object({ export const TCPConfigSchema = z.object({
remotePort: ZodPortSchema, remotePort: ZodPortSchema,
localIP: ZodIPSchema.default("127.0.0.1"), localIP: ZodIPSchema.default('127.0.0.1'),
localPort: ZodPortSchema, localPort: ZodPortSchema,
}) })
export const UDPConfigSchema = z.object({ export const UDPConfigSchema = z.object({
remotePort: ZodPortSchema.optional(), remotePort: ZodPortSchema.optional(),
localIP: ZodIPSchema.default("127.0.0.1"), localIP: ZodIPSchema.default('127.0.0.1'),
localPort: ZodPortSchema, localPort: ZodPortSchema,
}) })
export const HTTPConfigSchema = z.object({ export const HTTPConfigSchema = z.object({
localPort: ZodPortSchema, localPort: ZodPortSchema,
localIP: ZodIPSchema.default("127.0.0.1"), localIP: ZodIPSchema.default('127.0.0.1'),
subDomain: ZodStringSchema, subDomain: ZodStringSchema,
}) })
@@ -61,29 +54,30 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
const clientProxyConfigs = useStore($clientProxyConfigs) const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => {
handleSave() handleSave()
setTCPConfig({ type: "tcp", ...values, name: proxyName }) setTCPConfig({ type: 'tcp', ...values, name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => { const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
if (proxyCfg.name === proxyName) { if (proxyCfg.name === proxyName) {
return { ...values, type: "tcp", name: proxyName } as TCPProxyConfig return { ...values, type: 'tcp', name: proxyName } as TCPProxyConfig
} }
return proxyCfg return proxyCfg
}) })
$clientProxyConfigs.set(newProxiyConfigs) $clientProxyConfigs.set(newProxiyConfigs)
} }
const [isSaveDisabled, setSaveDisabled] = useState(false); const [isSaveDisabled, setSaveDisabled] = useState(false)
const handleSave = () => { const handleSave = () => {
setSaveDisabled(true); setSaveDisabled(true)
setTimeout(() => { setTimeout(() => {
setSaveDisabled(false); setSaveDisabled(false)
}, 3000); }, 3000)
}; }
const { data: server } = useQuery({ const { data: server } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => { queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID }) return getServer({ serverId: serverID })
} },
}) })
return ( return (
@@ -91,7 +85,8 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Label className="text-sm font-medium">访</Label> <Label className="text-sm font-medium">访</Label>
<p className="text-sm border rounded p-2 my-2 font-mono overflow-auto"> <p className="text-sm border rounded p-2 my-2 font-mono overflow-auto">
{`${server?.server?.ip}:${(defaultProxyConfig as TCPProxyConfig).remotePort {`${server?.server?.ip}:${
(defaultProxyConfig as TCPProxyConfig).remotePort
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`} } -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p> </p>
<FormField <FormField
@@ -120,7 +115,7 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP} defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
/> />
<FormField <FormField
control={form.control} control={form.control}
@@ -136,12 +131,13 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
)} )}
defaultValue={defaultProxyConfig === undefined ? 4321 : (defaultProxyConfig as TCPProxyConfig).remotePort} defaultValue={defaultProxyConfig === undefined ? 4321 : (defaultProxyConfig as TCPProxyConfig).remotePort}
/> />
<Button type="submit" disabled={isSaveDisabled} variant={"outline"}> <Button type="submit" disabled={isSaveDisabled} variant={'outline'}>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? "" : "hidden"}`}></YesIcon> <YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
</Button>
</Button>
</form> </form>
</Form> </Form>
); )
} }
export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => { export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
@@ -155,33 +151,33 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
form.reset({}) form.reset({})
}, []) }, [])
const clientProxyConfigs = useStore($clientProxyConfigs) const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => {
handleSave() handleSave()
setUDPConfig({ type: "udp", ...values, name: proxyName }) setUDPConfig({ type: 'udp', ...values, name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => { const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
if (proxyCfg.name === proxyName) { if (proxyCfg.name === proxyName) {
return { ...values, type: "udp", name: proxyName } as UDPProxyConfig return { ...values, type: 'udp', name: proxyName } as UDPProxyConfig
} }
return proxyCfg return proxyCfg
}) })
$clientProxyConfigs.set(newProxiyConfigs) $clientProxyConfigs.set(newProxiyConfigs)
} }
const [isSaveDisabled, setSaveDisabled] = useState(false); const [isSaveDisabled, setSaveDisabled] = useState(false)
const handleSave = () => { const handleSave = () => {
setSaveDisabled(true); setSaveDisabled(true)
setTimeout(() => { setTimeout(() => {
setSaveDisabled(false); setSaveDisabled(false)
}, 3000); }, 3000)
}; }
const { data: server } = useQuery({ const { data: server } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => { queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID }) return getServer({ serverId: serverID })
} },
}) })
return ( return (
@@ -189,7 +185,8 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Label className="text-sm font-medium">访</Label> <Label className="text-sm font-medium">访</Label>
<p className="text-sm border rounded p-2 my-2 font-mono overflow-auto"> <p className="text-sm border rounded p-2 my-2 font-mono overflow-auto">
{`${server?.server?.ip}:${(defaultProxyConfig as UDPProxyConfig).remotePort {`${server?.server?.ip}:${
(defaultProxyConfig as UDPProxyConfig).remotePort
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`} } -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p> </p>
<FormField <FormField
@@ -218,7 +215,7 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP} defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
/> />
<FormField <FormField
control={form.control} control={form.control}
@@ -230,15 +227,17 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
<Input type="number" {...field} /> <Input type="number" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem>)} </FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 4321 : (defaultProxyConfig as UDPProxyConfig).remotePort} defaultValue={defaultProxyConfig === undefined ? 4321 : (defaultProxyConfig as UDPProxyConfig).remotePort}
/> />
<Button type="submit" disabled={isSaveDisabled} variant={"outline"}> <Button type="submit" disabled={isSaveDisabled} variant={'outline'}>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? "" : "hidden"}`}></YesIcon> <YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
</Button>
</Button>
</form> </form>
</Form> </Form>
); )
} }
export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => { export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
@@ -256,29 +255,30 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
const clientProxyConfigs = useStore($clientProxyConfigs) const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => {
handleSave() handleSave()
setHTTPConfig({ ...values, type: "http", name: proxyName }) setHTTPConfig({ ...values, type: 'http', name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => { const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
if (proxyCfg.name === proxyName) { if (proxyCfg.name === proxyName) {
return { ...values, type: "http", name: proxyName } as HTTPProxyConfig return { ...values, type: 'http', name: proxyName } as HTTPProxyConfig
} }
return proxyCfg return proxyCfg
}) })
$clientProxyConfigs.set(newProxiyConfigs) $clientProxyConfigs.set(newProxiyConfigs)
} }
const [isSaveDisabled, setSaveDisabled] = useState(false); const [isSaveDisabled, setSaveDisabled] = useState(false)
const handleSave = () => { const handleSave = () => {
setSaveDisabled(true); setSaveDisabled(true)
setTimeout(() => { setTimeout(() => {
setSaveDisabled(false); setSaveDisabled(false)
}, 3000); }, 3000)
}; }
const { data: server } = useQuery({ const { data: server } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => { queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID }) return getServer({ serverId: serverID })
} },
}) })
useEffect(() => { useEffect(() => {
@@ -292,7 +292,8 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Label className="text-sm font-medium">访</Label> <Label className="text-sm font-medium">访</Label>
<p className="text-sm border rounded p-2 my-2 font-mono overflow-auto"> <p className="text-sm border rounded p-2 my-2 font-mono overflow-auto">
{`http://${(defaultProxyConfig as HTTPProxyConfig).subdomain}.${serverConfig?.subDomainHost}:${serverConfig?.vhostHTTPPort {`http://${(defaultProxyConfig as HTTPProxyConfig).subdomain}.${serverConfig?.subDomainHost}:${
serverConfig?.vhostHTTPPort
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`} } -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p> </p>
<FormField <FormField
@@ -321,7 +322,7 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP} defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
/> />
<FormField <FormField
control={form.control} control={form.control}
@@ -333,12 +334,14 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
<Input type="text" {...field} /> <Input type="text" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem>)} </FormItem>
defaultValue={defaultProxyConfig === undefined ? "" : (defaultProxyConfig as HTTPProxyConfig).subdomain} )}
defaultValue={defaultProxyConfig === undefined ? '' : (defaultProxyConfig as HTTPProxyConfig).subdomain}
/> />
<Button type="submit" disabled={isSaveDisabled} variant={"outline"}> <Button type="submit" disabled={isSaveDisabled} variant={'outline'}>
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? "" : "hidden"}`}></YesIcon> <YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
</Button>
</Button>
</form> </form>
</Form> </Form>
) )

View File

@@ -1,30 +1,20 @@
import { ZodEmailSchema, ZodStringSchema } from "@/lib/consts" import { ZodEmailSchema, ZodStringSchema } from '@/lib/consts'
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from "react-hook-form" import { useForm } from 'react-hook-form'
import * as z from "zod" import * as z from 'zod'
import { import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'
Form, import { Input } from './ui/input'
FormControl, import { register } from '@/api/auth'
FormField, import { Button } from './ui/button'
FormItem,
FormMessage,
} from "@/components/ui/form"
import { Input } from "./ui/input"
import { register } from "@/api/auth"
import { Button } from "./ui/button"
import { ExclamationTriangleIcon } from "@radix-ui/react-icons" import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
import { import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
Alert, import { useState } from 'react'
AlertDescription, import { useToast } from './ui/use-toast'
AlertTitle, import { RespCode } from '@/lib/pb/common'
} from "@/components/ui/alert"
import { useState } from "react"
import { useToast } from "./ui/use-toast"
import { RespCode } from "@/lib/pb/common"
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Toast } from "./ui/toast" import { Toast } from './ui/toast'
export const RegisterSchema = z.object({ export const RegisterSchema = z.object({
username: ZodStringSchema, username: ZodStringSchema,
@@ -41,25 +31,25 @@ export const RegisterComponent = () => {
const [registerAlert, setRegisterAlert] = useState(false) const [registerAlert, setRegisterAlert] = useState(false)
const sleep = async (ms: number): Promise<void> => { const sleep = async (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms))
} }
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => { const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
toast({ title: "注册中,请稍候" }) toast({ title: '注册中,请稍候' })
try { try {
const res = await register({ ...values }) const res = await register({ ...values })
if (res.status?.code === RespCode.SUCCESS) { if (res.status?.code === RespCode.SUCCESS) {
toast({ title: "注册成功,正在跳转到登录" }) toast({ title: '注册成功,正在跳转到登录' })
setRegisterAlert(false) setRegisterAlert(false)
await sleep(3000) await sleep(3000)
router.push("/login") router.push('/login')
} else { } else {
toast({ title: "注册失败" }) toast({ title: '注册失败' })
setRegisterAlert(true) setRegisterAlert(true)
} }
} catch (e) { } catch (e) {
toast({ title: "注册失败" }) toast({ title: '注册失败' })
console.log("register error", e) console.log('register error', e)
setRegisterAlert(true) setRegisterAlert(true)
} }
} }
@@ -67,21 +57,14 @@ export const RegisterComponent = () => {
return ( return (
<div className="w-full flex flex-col gap-6"> <div className="w-full flex flex-col gap-6">
<Form {...form}> <Form {...form}>
<form <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<FormField <FormField
control={form.control} control={form.control}
name="username" name="username"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input type="text" placeholder="用户名" {...field} />
type="text"
placeholder="用户名"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -93,11 +76,7 @@ export const RegisterComponent = () => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input type="email" placeholder="邮箱地址" {...field} />
type="email"
placeholder="邮箱地址"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -109,23 +88,19 @@ export const RegisterComponent = () => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input type="password" placeholder="密码" {...field} />
type="password"
placeholder="密码"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{registerAlert && <Alert variant="destructive"> {registerAlert && (
<Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" /> <ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle></AlertTitle> <AlertTitle></AlertTitle>
<AlertDescription> <AlertDescription></AlertDescription>
</Alert>
</AlertDescription> )}
</Alert>}
<Button type="submit"></Button> <Button type="submit"></Button>
</form> </form>
</Form> </Form>

View File

@@ -1,11 +1,11 @@
import { useState } from "react" import { useState } from 'react'
import { useMutation, useQuery } from "@tanstack/react-query" import { useMutation, useQuery } from '@tanstack/react-query'
import { initServer, listServer } from "@/api/server" import { initServer, listServer } from '@/api/server'
import { Label } from "./ui/label" import { Label } from './ui/label'
import { Input } from "./ui/input" import { Input } from './ui/input'
import { Button } from "./ui/button" import { Button } from './ui/button'
import { useToast } from "./ui/use-toast" import { useToast } from './ui/use-toast'
import { RespCode } from "@/lib/pb/common" import { RespCode } from '@/lib/pb/common'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -14,19 +14,19 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from '@/components/ui/dialog'
export const CreateServerDialog = () => { export const CreateServerDialog = () => {
const [serverID, setServerID] = useState<string | undefined>() const [serverID, setServerID] = useState<string | undefined>()
const [serverIP, setServerIP] = useState<string | undefined>() const [serverIP, setServerIP] = useState<string | undefined>()
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ["listServer", { pageIndex: 0, pageSize: 10 }], queryKey: ['listServer', { pageIndex: 0, pageSize: 10 }],
queryFn: async () => { queryFn: async () => {
return await listServer({ return await listServer({
page: 1, page: 1,
pageSize: 10 pageSize: 10,
}) })
} },
}) })
const newServer = useMutation({ const newServer = useMutation({
mutationFn: initServer, mutationFn: initServer,
@@ -34,25 +34,26 @@ export const CreateServerDialog = () => {
const { toast } = useToast() const { toast } = useToast()
const handleNewServer = async () => { const handleNewServer = async () => {
toast({ title: "已提交创建请求" }) toast({ title: '已提交创建请求' })
try { try {
let resp = await newServer.mutateAsync({ serverId: serverID, serverIp: serverIP }) let resp = await newServer.mutateAsync({ serverId: serverID, serverIp: serverIP })
if (resp.status?.code !== RespCode.SUCCESS) { if (resp.status?.code !== RespCode.SUCCESS) {
toast({ title: "创建服务端失败" }) toast({ title: '创建服务端失败' })
return return
} }
toast({ title: "创建服务端成功" }) toast({ title: '创建服务端成功' })
dataQuery.refetch() dataQuery.refetch()
} catch (error) { } catch (error) {
toast({ title: "创建服务端失败" }) toast({ title: '创建服务端失败' })
} }
} }
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" size={"sm"}></Button> <Button variant="outline" size={'sm'}>
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>

View File

@@ -1,5 +1,5 @@
import { ColumnDef, Table } from "@tanstack/react-table" import { ColumnDef, Table } from '@tanstack/react-table'
import { MoreHorizontal } from "lucide-react" import { MoreHorizontal } from 'lucide-react'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,9 +9,9 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from '@/components/ui/dialog'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -19,24 +19,24 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu'
import { useToast } from "./ui/use-toast" import { useToast } from './ui/use-toast'
import React, { useState } from "react" import React, { useState } from 'react'
import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from "@/lib/consts" import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from '@/lib/consts'
import { useMutation, useQuery } from "@tanstack/react-query" import { useMutation, useQuery } from '@tanstack/react-query'
import { deleteServer, listServer } from "@/api/server" import { deleteServer, listServer } from '@/api/server'
import { useRouter } from "next/router" import { useRouter } from 'next/router'
import { getUserInfo } from "@/api/user" import { getUserInfo } from '@/api/user'
import { useStore } from "@nanostores/react" import { useStore } from '@nanostores/react'
import { $platformInfo } from "@/store/user" import { $platformInfo } from '@/store/user'
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover" import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
import { getClientsStatus } from "@/api/platform" import { getClientsStatus } from '@/api/platform'
import { ClientType } from "@/lib/pb/common" import { ClientType } from '@/lib/pb/common'
import { ClientStatus, ClientStatus_Status } from "@/lib/pb/api_master" import { ClientStatus, ClientStatus_Status } from '@/lib/pb/api_master'
export type ServerTableSchema = { export type ServerTableSchema = {
id: string, id: string
status: "invalid" | "valid" status: 'invalid' | 'valid'
secret: string secret: string
info?: string info?: string
ip: string ip: string
@@ -45,66 +45,73 @@ export type ServerTableSchema = {
export const columns: ColumnDef<ServerTableSchema>[] = [ export const columns: ColumnDef<ServerTableSchema>[] = [
{ {
accessorKey: "id", accessorKey: 'id',
header: "ID", header: 'ID',
cell: ({ row }) => { cell: ({ row }) => {
return <ServerID server={row.original} /> return <ServerID server={row.original} />
} },
}, },
{ {
accessorKey: "status", accessorKey: 'status',
header: "是否配置", header: '是否配置',
cell: ({ row }) => { cell: ({ row }) => {
const Server = row.original const Server = row.original
return <div className={`font-mono ${Server.status === "valid" ? "text-green-500" : "text-red-500"} min-w-12`}>{ return (
<div className={`font-mono ${Server.status === 'valid' ? 'text-green-500' : 'text-red-500'} min-w-12`}>
{ {
valid: "已配置", {
invalid: "未配置", valid: '已配置',
invalid: '未配置',
}[Server.status] }[Server.status]
}</div>
} }
</div>
)
},
}, },
{ {
accessorKey: "info", accessorKey: 'info',
header: "运行信息", header: '运行信息',
cell: ({ row }) => { cell: ({ row }) => {
const Server = row.original const Server = row.original
return <ServerInfo server={Server} /> return <ServerInfo server={Server} />
} },
}, },
{ {
accessorKey: "ip", accessorKey: 'ip',
header: "IP", header: 'IP',
cell: ({ row }) => { cell: ({ row }) => {
const Server = row.original const Server = row.original
return <div className="font-mono">{Server.ip}</div> return <div className="font-mono">{Server.ip}</div>
} },
}, },
{ {
accessorKey: "secret", accessorKey: 'secret',
header: "连接密钥", header: '连接密钥',
cell: ({ row }) => { cell: ({ row }) => {
const Server = row.original const Server = row.original
return <ServerSecret server={Server} /> return <ServerSecret server={Server} />
} },
}, },
{ {
id: "action", id: 'action',
cell: ({ row, table }) => { cell: ({ row, table }) => {
const Server = row.original const Server = row.original
return (<ServerActions server={Server} table={table} />) return <ServerActions server={Server} table={table} />
}, },
}, },
] ]
export const ServerID = ({ server }: { server: ServerTableSchema }) => { export const ServerID = ({ server }: { server: ServerTableSchema }) => {
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
return <Popover > return (
<PopoverTrigger asChild><div className="font-mono">{server.id}</div></PopoverTrigger> <Popover>
<PopoverTrigger asChild>
<div className="font-mono">{server.id}</div>
</PopoverTrigger>
<PopoverContent className="w-fit overflow-auto max-w-72 max-h-72"> <PopoverContent className="w-fit overflow-auto max-w-72 max-h-72">
<div>Linux安装到systemd</div> <div>Linux安装到systemd</div>
<div className="p-2 border rounded font-mono w-fit"> <div className="p-2 border rounded font-mono w-fit">
{platformInfo === undefined ? "获取平台信息失败" : LinuxInstallCommand("server", server, platformInfo)} {platformInfo === undefined ? '获取平台信息失败' : LinuxInstallCommand('server', server, platformInfo)}
</div> </div>
{/* <div>Windows</div> {/* <div>Windows</div>
<div className="p-2 border rounded font-mono w-fit"> <div className="p-2 border rounded font-mono w-fit">
@@ -112,69 +119,79 @@ export const ServerID = ({ server }: { server: ServerTableSchema }) => {
</div> */} </div> */}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)
} }
export const ServerInfo = ({ server }: { server: ServerTableSchema }) => { export const ServerInfo = ({ server }: { server: ServerTableSchema }) => {
const clientsInfo = useQuery({ const clientsInfo = useQuery({
queryKey: ["getClientsStatus", [server.id]], queryKey: ['getClientsStatus', [server.id]],
queryFn: async () => { queryFn: async () => {
return await getClientsStatus({ return await getClientsStatus({
clientIds: [server.id], clientIds: [server.id],
clientType: ClientType.FRPS, clientType: ClientType.FRPS,
}) })
} },
}) })
const trans = (info: ClientStatus | undefined) => { const trans = (info: ClientStatus | undefined) => {
let statusText: "在线" | "离线" | "错误" | "未知" = "未知"; let statusText: '在线' | '离线' | '错误' | '未知' = '未知'
if (info === undefined) { if (info === undefined) {
return statusText; return statusText
} }
if (info.status === ClientStatus_Status.ONLINE) { if (info.status === ClientStatus_Status.ONLINE) {
statusText = "在线"; statusText = '在线'
} else if (info.status === ClientStatus_Status.OFFLINE) { } else if (info.status === ClientStatus_Status.OFFLINE) {
statusText = "离线"; statusText = '离线'
} else if (info.status === ClientStatus_Status.ERROR) { } else if (info.status === ClientStatus_Status.ERROR) {
statusText = "错误"; statusText = '错误'
} return statusText; }
return statusText
} }
const infoColor = clientsInfo.data?.clients[server.id]?.status === ClientStatus_Status.ONLINE ? "text-green-500" : "text-red-500" const infoColor =
clientsInfo.data?.clients[server.id]?.status === ClientStatus_Status.ONLINE ? 'text-green-500' : 'text-red-500'
return <div className={`p-2 border rounded font-mono w-fit ${infoColor}`}> return (
<div className={`p-2 border rounded font-mono w-fit ${infoColor}`}>
{`${clientsInfo.data?.clients[server.id].ping}ms, ${trans(clientsInfo.data?.clients[server.id])}`} {`${clientsInfo.data?.clients[server.id].ping}ms, ${trans(clientsInfo.data?.clients[server.id])}`}
</div> </div>
)
} }
export const ServerSecret = ({ server }: { server: ServerTableSchema }) => { export const ServerSecret = ({ server }: { server: ServerTableSchema }) => {
const [showSecrect, setShowSecrect] = useState<boolean>(false) const [showSecrect, setShowSecrect] = useState<boolean>(false)
const fakeSecret = Array.from({ length: server.secret.length }).map(() => '*').join('') const fakeSecret = Array.from({ length: server.secret.length })
.map(() => '*')
.join('')
const { toast } = useToast() const { toast } = useToast()
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
return <Popover> return (
<Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div <div
onMouseEnter={() => setShowSecrect(true)} onMouseEnter={() => setShowSecrect(true)}
onMouseLeave={() => setShowSecrect(false)} onMouseLeave={() => setShowSecrect(false)}
onClick={() => { onClick={() => {
if (platformInfo) { if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo)); navigator.clipboard.writeText(ExecCommandStr('server', server, platformInfo))
toast({ description: "复制成功", }); toast({ description: '复制成功' })
} else { } else {
toast({ description: "获取平台信息失败", }); toast({ description: '获取平台信息失败' })
} }
}} }}
className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap">{ className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap"
showSecrect ? server.secret : fakeSecret >
}</div> {showSecrect ? server.secret : fakeSecret}
</div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-fit overflow-auto max-w-48"> <PopoverContent className="w-fit overflow-auto max-w-48">
<div className="p-2 border rounded font-mono w-fit"> <div className="p-2 border rounded font-mono w-fit">
{platformInfo === undefined ? "获取平台信息失败" : ExecCommandStr("server", server, platformInfo)} {platformInfo === undefined ? '获取平台信息失败' : ExecCommandStr('server', server, platformInfo)}
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)
} }
export interface ServerItemProps { export interface ServerItemProps {
@@ -184,7 +201,7 @@ export interface ServerItemProps {
export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => { export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
const { toast } = useToast() const { toast } = useToast()
const router = useRouter(); const router = useRouter()
const platformInfo = useStore($platformInfo) const platformInfo = useStore($platformInfo)
const fetchDataOptions = { const fetchDataOptions = {
@@ -193,25 +210,26 @@ export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
} }
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ["listServer", fetchDataOptions], queryKey: ['listServer', fetchDataOptions],
queryFn: async () => { queryFn: async () => {
return await listServer({ return await listServer({
page: fetchDataOptions.pageIndex + 1, page: fetchDataOptions.pageIndex + 1,
pageSize: fetchDataOptions.pageSize pageSize: fetchDataOptions.pageSize,
}) })
} },
}) })
const removeServer = useMutation({ const removeServer = useMutation({
mutationFn: deleteServer, mutationFn: deleteServer,
onSuccess: () => { onSuccess: () => {
toast({ description: "删除成功" }) toast({ description: '删除成功' })
dataQuery.refetch() dataQuery.refetch()
}, },
onError: () => { onError: () => {
toast({ description: "删除失败" }) toast({ description: '删除失败' })
} },
}) })
return <Dialog> return (
<Dialog>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"> <Button variant="ghost" className="h-8 w-8 p-0">
@@ -224,26 +242,28 @@ export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
if (platformInfo) { if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo)); navigator.clipboard.writeText(ExecCommandStr('server', server, platformInfo))
toast({ description: "复制成功如果复制不成功请点击ID字段手动复制", }); toast({ description: '复制成功如果复制不成功请点击ID字段手动复制' })
} else { } else {
toast({ description: "获取平台信息失败如果复制不成功请点击ID字段手动复制", }); toast({ description: '获取平台信息失败如果复制不成功请点击ID字段手动复制' })
} }
}} }}
> >
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={ <DropdownMenuItem
() => { onClick={() => {
router.push({ router.push({
pathname: "/serveredit", pathname: '/serveredit',
query: { query: {
serverID: server.id serverID: server.id,
} },
}) })
} }}
}></DropdownMenuItem> >
</DropdownMenuItem>
<DialogTrigger asChild> <DialogTrigger asChild>
<DropdownMenuItem className="text-destructive"></DropdownMenuItem> <DropdownMenuItem className="text-destructive"></DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
@@ -254,14 +274,19 @@ export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
<DialogTitle>?</DialogTitle> <DialogTitle>?</DialogTitle>
<DialogDescription> <DialogDescription>
<p className="text-destructive">?</p> <p className="text-destructive">?</p>
<p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2"></p> <p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2">
</p>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button type="submit" onClick={() => removeServer.mutate({ serverId: server.id })}></Button> <Button type="submit" onClick={() => removeServer.mutate({ serverId: server.id })}>
</Button>
</DialogClose> </DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)
} }

View File

@@ -1,6 +1,6 @@
import { Server } from "@/lib/pb/common"; import { Server } from '@/lib/pb/common'
import { ServerTableSchema, columns as serverColumnsDef } from "./server_item"; import { ServerTableSchema, columns as serverColumnsDef } from './server_item'
import { DataTable } from "./data_table"; import { DataTable } from './data_table'
import { import {
getSortedRowModel, getSortedRowModel,
@@ -11,11 +11,11 @@ import {
getPaginationRowModel, getPaginationRowModel,
SortingState, SortingState,
PaginationState, PaginationState,
} from "@tanstack/react-table" } from '@tanstack/react-table'
import React from "react" import React from 'react'
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query'
import { listServer } from "@/api/server"; import { listServer } from '@/api/server'
export interface ServerListProps { export interface ServerListProps {
Servers: Server[] Servers: Server[]
@@ -23,19 +23,18 @@ export interface ServerListProps {
export const ServerList: React.FC<ServerListProps> = ({ Servers }) => { export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
const [sorting, setSorting] = React.useState<SortingState>([]) const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
[] const data = Servers.map(
) (server) =>
const data = Servers.map((server) =>
({ ({
id: server.id == undefined ? "" : server.id, id: server.id == undefined ? '' : server.id,
status: server.config == undefined || server.config == "" ? "invalid" : "valid", status: server.config == undefined || server.config == '' ? 'invalid' : 'valid',
secret: server.secret == undefined ? "" : server.secret, secret: server.secret == undefined ? '' : server.secret,
config: server.config config: server.config,
} as ServerTableSchema)) }) as ServerTableSchema,
)
const [{ pageIndex, pageSize }, setPagination] = const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}) })
@@ -49,27 +48,30 @@ export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
pageIndex, pageIndex,
pageSize, pageSize,
}), }),
[pageIndex, pageSize] [pageIndex, pageSize],
) )
const dataQuery = useQuery({ const dataQuery = useQuery({
queryKey: ["listServer", fetchDataOptions], queryKey: ['listServer', fetchDataOptions],
queryFn: async () => { queryFn: async () => {
return await listServer({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize }) return await listServer({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
} },
}) })
const table = useReactTable({ const table = useReactTable({
data: dataQuery.data?.servers.map((server) => { data:
dataQuery.data?.servers.map((server) => {
return { return {
id: server.id == undefined ? "" : server.id, id: server.id == undefined ? '' : server.id,
status: server.config == undefined || server.config == "" ? "invalid" : "valid", status: server.config == undefined || server.config == '' ? 'invalid' : 'valid',
secret: server.secret == undefined ? "" : server.secret, secret: server.secret == undefined ? '' : server.secret,
ip: server.ip, ip: server.ip,
config: server.config config: server.config,
} as ServerTableSchema } as ServerTableSchema
}) ?? data, }) ?? data,
pageCount: Math.ceil((dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0), pageCount: Math.ceil(
(dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0,
),
columns: serverColumnsDef, columns: serverColumnsDef,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
@@ -86,4 +88,4 @@ export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
}, },
}) })
return <DataTable table={table} columns={serverColumnsDef} /> return <DataTable table={table} columns={serverColumnsDef} />
}; }

View File

@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from 'react'
import { Button } from "./ui/button" import { Button } from './ui/button'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router'
export interface SideBarItem { export interface SideBarItem {
id: string id: string
@@ -13,24 +13,71 @@ export interface SideBarProps {
} }
export const SideBar: React.FC<SideBarProps> = ({ items }) => { export const SideBar: React.FC<SideBarProps> = ({ items }) => {
const router = useRouter(); const router = useRouter()
const defaultItems = const defaultItems = [
[ {
{ id: "clients", label: "客户端", eventHandler: () => { router.push("/clients") } }, id: 'clients',
{ id: "servers", label: "服务端", eventHandler: () => { router.push("/servers") } }, label: '客户端',
{ id: "clientedit", label: "编辑隧道", eventHandler: () => { router.push("/clientedit") } }, eventHandler: () => {
{ id: "serveredit", label: "编辑端点", eventHandler: () => { router.push("/serveredit") } }, router.push('/clients')
},
},
{
id: 'servers',
label: '服务端',
eventHandler: () => {
router.push('/servers')
},
},
{
id: 'clientedit',
label: '编辑隧道',
eventHandler: () => {
router.push('/clientedit')
},
},
{
id: 'serveredit',
label: '编辑端点',
eventHandler: () => {
router.push('/serveredit')
},
},
] ]
return ( return (
<div className="w-48 h-full grid grid-cols-1 mt-1 min-w-24"> <div className="w-48 h-full grid grid-cols-1 mt-1 min-w-24">
{items && items.map(item => <Button className={`mx-2 my-1 justify-start ${router.pathname.includes(item.id) ? {items &&
"bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground" : ""}`} variant={"ghost"} items.map((item) => (
size={"sm"} key={item.id} <Button
onClick={item.eventHandler}>{item.label}</Button>)} className={`mx-2 my-1 justify-start ${
{!items && defaultItems.map(item => <Button className={`mx-2 my-1 justify-start ${router.pathname.includes(item.id) ? router.pathname.includes(item.id)
"bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground" : ""}`} variant={"ghost"} ? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground'
size={"sm"} key={item.id} : ''
onClick={item.eventHandler}>{item.label}</Button>)} }`}
variant={'ghost'}
size={'sm'}
key={item.id}
onClick={item.eventHandler}
>
{item.label}
</Button>
))}
{!items &&
defaultItems.map((item) => (
<Button
className={`mx-2 my-1 justify-start ${
router.pathname.includes(item.id)
? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground'
: ''
}`}
variant={'ghost'}
size={'sm'}
key={item.id}
onClick={item.eventHandler}
>
{item.label}
</Button>
))}
</div> </div>
) )
} }

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from 'react'
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDownIcon } from "@radix-ui/react-icons" import { ChevronDownIcon } from '@radix-ui/react-icons'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Accordion = AccordionPrimitive.Root const Accordion = AccordionPrimitive.Root
@@ -10,13 +10,9 @@ const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>, React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AccordionPrimitive.Item <AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
ref={ref}
className={cn("border-b", className)}
{...props}
/>
)) ))
AccordionItem.displayName = "AccordionItem" AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = React.forwardRef< const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>, React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -26,8 +22,8 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", 'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className className,
)} )}
{...props} {...props}
> >
@@ -47,7 +43,7 @@ const AccordionContent = React.forwardRef<
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props} {...props}
> >
<div className={cn("pb-4 pt-0", className)}>{children}</div> <div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
)) ))
AccordionContent.displayName = AccordionPrimitive.Content.displayName AccordionContent.displayName = AccordionPrimitive.Content.displayName

View File

@@ -1,59 +1,43 @@
import * as React from "react" import * as React from 'react'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-background text-foreground", default: 'bg-background text-foreground',
destructive: destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
},
}, },
}
) )
const Alert = React.forwardRef< const Alert = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => ( >(({ className, variant, ...props }, ref) => (
<div <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)) ))
Alert.displayName = "Alert" Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef< const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLHeadingElement> <h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<h5 )
ref={ref} AlertTitle.displayName = 'AlertTitle'
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef< const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement> <div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} AlertDescription.displayName = 'AlertDescription'
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription }

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from 'react'
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -9,10 +9,7 @@ const Avatar = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props} {...props}
/> />
)) ))
@@ -22,11 +19,7 @@ const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} {...props} />
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
)) ))
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName
@@ -36,10 +29,7 @@ const AvatarFallback = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn( className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props} {...props}
/> />
)) ))

View File

@@ -1,37 +1,33 @@
import * as React from "react" import * as React from 'react'
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {
default: default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
"bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
destructive: outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
outline: ghost: 'hover:bg-accent hover:text-accent-foreground',
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", link: 'text-primary underline-offset-4 hover:underline',
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2", default: 'h-9 px-4 py-2',
sm: "h-8 rounded-md px-3 text-xs", sm: 'h-8 rounded-md px-3 text-xs',
lg: "h-10 rounded-md px-8", lg: 'h-10 rounded-md px-8',
icon: "h-9 w-9", icon: 'h-9 w-9',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
},
}, },
}
) )
export interface ButtonProps export interface ButtonProps
@@ -42,16 +38,10 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button'
return ( return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
<Comp },
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
) )
Button.displayName = "Button" Button.displayName = 'Button'
export { Button, buttonVariants } export { Button, buttonVariants }

View File

@@ -1,76 +1,43 @@
import * as React from "react" import * as React from 'react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Card = React.forwardRef< const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
HTMLDivElement, <div ref={ref} className={cn('rounded-xl border bg-card text-card-foreground shadow', className)} {...props} />
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
)) ))
Card.displayName = "Card" Card.displayName = 'Card'
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} CardHeader.displayName = 'CardHeader'
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLHeadingElement> <h3 ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<h3 )
ref={ref} CardTitle.displayName = 'CardTitle'
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement> <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<p )
ref={ref} CardDescription.displayName = 'CardDescription'
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef< const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
React.HTMLAttributes<HTMLDivElement> )
>(({ className, ...props }, ref) => ( CardContent.displayName = 'CardContent'
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} CardFooter.displayName = 'CardFooter'
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from 'react'
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from '@radix-ui/react-dialog'
import { Cross2Icon } from "@radix-ui/react-icons" import { Cross2Icon } from '@radix-ui/react-icons'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root
@@ -19,8 +19,8 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className className,
)} )}
{...props} {...props}
/> />
@@ -36,8 +36,8 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className className,
)} )}
{...props} {...props}
> >
@@ -51,33 +51,15 @@ const DialogContent = React.forwardRef<
)) ))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
) )
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@@ -85,10 +67,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn('text-lg font-semibold leading-none tracking-tight', className)}
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props} {...props}
/> />
)) ))
@@ -98,11 +77,7 @@ const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)) ))
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName

View File

@@ -1,12 +1,8 @@
import * as React from "react" import * as React from 'react'
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root
@@ -29,9 +25,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && "pl-8", inset && 'pl-8',
className className,
)} )}
{...props} {...props}
> >
@@ -39,8 +35,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<ChevronRightIcon className="ml-auto h-4 w-4" /> <ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ))
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -49,14 +44,13 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />
)) ))
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -67,9 +61,9 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />
@@ -86,9 +80,9 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && "pl-8", inset && 'pl-8',
className className,
)} )}
{...props} {...props}
/> />
@@ -102,8 +96,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@@ -116,8 +110,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ))
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -126,8 +119,8 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className,
)} )}
{...props} {...props}
> >
@@ -149,11 +142,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props} {...props}
/> />
)) ))
@@ -163,26 +152,14 @@ const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
)) ))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
className, return <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
} }
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export { export {
DropdownMenu, DropdownMenu,

View File

@@ -1,34 +1,25 @@
import * as React from "react" import * as React from 'react'
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot'
import { import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form'
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
import { Label } from "@/components/ui/label" import { Label } from '@/components/ui/label'
const Form = FormProvider const Form = FormProvider
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName
} }
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
{} as FormFieldContextValue
)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
@@ -47,7 +38,7 @@ const useFormField = () => {
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error('useFormField should be used within <FormField>')
} }
const { id } = itemContext const { id } = itemContext
@@ -66,23 +57,20 @@ type FormItemContextValue = {
id: string id: string
} }
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
{} as FormItemContextValue
)
const FormItem = React.forwardRef< const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => {
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId()
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) )
}) },
FormItem.displayName = "FormItem" )
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
@@ -90,60 +78,40 @@ const FormLabel = React.forwardRef<
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField()
return ( return <Label ref={ref} className={cn(error && 'text-destructive', className)} htmlFor={formItemId} {...props} />
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}) })
FormLabel.displayName = "FormLabel" FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef< const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
React.ElementRef<typeof Slot>, ({ ...props }, ref) => {
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
<Slot <Slot
ref={ref} ref={ref}
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) )
}) },
FormControl.displayName = "FormControl" )
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => {
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField()
return ( return (
<p <p ref={ref} id={formDescriptionId} className={cn('text-[0.8rem] text-muted-foreground', className)} {...props} />
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
) )
}) },
FormDescription.displayName = "FormDescription" )
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, children, ...props }, ref) => {
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children
@@ -155,22 +123,14 @@ const FormMessage = React.forwardRef<
<p <p
ref={ref} ref={ref}
id={formMessageId} id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)} className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) )
}) },
FormMessage.displayName = "FormMessage" )
FormMessage.displayName = 'FormMessage'
export { export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from 'react'
import * as HoverCardPrimitive from "@radix-ui/react-hover-card" import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const HoverCard = HoverCardPrimitive.Root const HoverCard = HoverCardPrimitive.Root
@@ -10,14 +10,14 @@ const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef< const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>, React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content <HoverCardPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -1,5 +1,14 @@
export const YesIcon = ({ className }: { className?: string }) => { export const YesIcon = ({ className }: { className?: string }) => {
return <div className={className}> return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg> <div className={className}>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
</div> </div>
)
} }

View File

@@ -1,25 +1,22 @@
import * as React from "react" import * as React from 'react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
export interface InputProps export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) )
} })
) Input.displayName = 'Input'
Input.displayName = "Input"
export { Input } export { Input }

View File

@@ -1,23 +1,16 @@
import * as React from "react" import * as React from 'react'
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const labelVariants = cva( const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70')
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
)) ))
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from 'react'
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root
@@ -12,15 +12,15 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -1,13 +1,8 @@
import * as React from "react" import * as React from 'react'
import { import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
CaretSortIcon, import * as SelectPrimitive from '@radix-ui/react-select'
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root
@@ -22,8 +17,8 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", 'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className className,
)} )}
{...props} {...props}
> >
@@ -41,10 +36,7 @@ const SelectScrollUpButton = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
className={cn( className={cn('flex cursor-default items-center justify-center py-1', className)}
"flex cursor-default items-center justify-center py-1",
className
)}
{...props} {...props}
> >
<ChevronUpIcon /> <ChevronUpIcon />
@@ -58,30 +50,26 @@ const SelectScrollDownButton = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
className={cn( className={cn('flex cursor-default items-center justify-center py-1', className)}
"flex cursor-default items-center justify-center py-1",
className
)}
{...props} {...props}
> >
<ChevronDownIcon /> <ChevronDownIcon />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)) ))
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === "popper" && position === 'popper' &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className className,
)} )}
position={position} position={position}
{...props} {...props}
@@ -89,9 +77,9 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", 'p-1',
position === "popper" && position === 'popper' &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)} )}
> >
{children} {children}
@@ -106,11 +94,7 @@ const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label ref={ref} className={cn('px-2 py-1.5 text-sm font-semibold', className)} {...props} />
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
)) ))
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
@@ -121,8 +105,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className,
)} )}
{...props} {...props}
> >
@@ -140,11 +124,7 @@ const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
)) ))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName SelectSeparator.displayName = SelectPrimitive.Separator.displayName

View File

@@ -1,29 +1,20 @@
import * as React from "react" import * as React from 'react'
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from '@radix-ui/react-separator'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>( >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn('shrink-0 bg-border', orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]', className)}
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props} {...props}
/> />
) ))
)
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator } export { Separator }

View File

@@ -1,24 +1,22 @@
import { useTheme } from "next-themes" import { useTheme } from 'next-themes'
import { Toaster as Sonner } from "sonner" import { Toaster as Sonner } from 'sonner'
type ToasterProps = React.ComponentProps<typeof Sonner> type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = 'system' } = useTheme()
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps['theme']}
className="toaster group" className="toaster group"
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: "group-[.toast]:text-muted-foreground", description: 'group-[.toast]:text-muted-foreground',
actionButton: actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
}, },
}} }}
{...props} {...props}

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from 'react'
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -9,15 +9,15 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" 'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>

View File

@@ -1,120 +1,76 @@
import * as React from "react" import * as React from 'react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const Table = React.forwardRef< const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
HTMLTableElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div> </div>
)) ),
Table.displayName = "Table" )
Table.displayName = 'Table'
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />,
React.HTMLAttributes<HTMLTableSectionElement> )
>(({ className, ...props }, ref) => ( TableHeader.displayName = 'TableHeader'
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef< const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<tbody )
ref={ref} TableBody.displayName = 'TableBody'
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<tfoot )
ref={ref} TableFooter.displayName = 'TableFooter'
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef< const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
HTMLTableRowElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props} {...props}
/> />
)) ),
TableRow.displayName = "TableRow" )
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef< const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className,
)} )}
{...props} {...props}
/> />
)) ),
TableHead.displayName = "TableHead" )
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef< const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn( className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props} {...props}
/> />
)) ),
TableCell.displayName = "TableCell" )
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef< const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
HTMLTableCaptionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableCaptionElement> <caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<caption )
ref={ref} TableCaption.displayName = 'TableCaption'
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export { export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -1,24 +1,21 @@
import * as React from "react" import * as React from 'react'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
export interface TextareaProps export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
className={cn( className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", 'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) )
} })
) Textarea.displayName = 'Textarea'
Textarea.displayName = "Textarea"
export { Textarea } export { Textarea }

View File

@@ -1,9 +1,9 @@
import * as React from "react" import * as React from 'react'
import { Cross2Icon } from "@radix-ui/react-icons" import { Cross2Icon } from '@radix-ui/react-icons'
import * as ToastPrimitives from "@radix-ui/react-toast" import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider const ToastProvider = ToastPrimitives.Provider
@@ -14,8 +14,8 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport <ToastPrimitives.Viewport
ref={ref} ref={ref}
className={cn( className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", 'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className className,
)} )}
{...props} {...props}
/> />
@@ -23,33 +23,25 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{ {
variants: { variants: {
variant: { variant: {
default: "border bg-background text-foreground", default: 'border bg-background text-foreground',
destructive: destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground',
"destructive group border-destructive bg-destructive text-destructive-foreground",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
},
}, },
}
) )
const Toast = React.forwardRef< const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>, React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => { >(({ className, variant, ...props }, ref) => {
return ( return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
}) })
Toast.displayName = ToastPrimitives.Root.displayName Toast.displayName = ToastPrimitives.Root.displayName
@@ -60,8 +52,8 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action <ToastPrimitives.Action
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", 'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className className,
)} )}
{...props} {...props}
/> />
@@ -75,8 +67,8 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={ref}
className={cn( className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", 'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className className,
)} )}
toast-close="" toast-close=""
{...props} {...props}
@@ -90,11 +82,7 @@ const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>, React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Title <ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold [&+div]:text-xs', className)} {...props} />
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
)) ))
ToastTitle.displayName = ToastPrimitives.Title.displayName ToastTitle.displayName = ToastPrimitives.Title.displayName
@@ -102,11 +90,7 @@ const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>, React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Description <ToastPrimitives.Description ref={ref} className={cn('text-sm opacity-90', className)} {...props} />
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
)) ))
ToastDescription.displayName = ToastPrimitives.Description.displayName ToastDescription.displayName = ToastPrimitives.Description.displayName

View File

@@ -1,12 +1,5 @@
import { import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '@/components/ui/toast'
Toast, import { useToast } from '@/components/ui/use-toast'
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() { export function Toaster() {
const { toasts } = useToast() const { toasts } = useToast()
@@ -18,9 +11,7 @@ export function Toaster() {
<Toast key={id} {...props}> <Toast key={id} {...props}>
<div className="grid gap-1"> <div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>} {title && <ToastTitle>{title}</ToastTitle>}
{description && ( {description && <ToastDescription>{description}</ToastDescription>}
<ToastDescription>{description}</ToastDescription>
)}
</div> </div>
{action} {action}
<ToastClose /> <ToastClose />

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from 'react'
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider
@@ -17,8 +17,8 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -1,10 +1,7 @@
// Inspired by react-hot-toast library // Inspired by react-hot-toast library
import * as React from "react" import * as React from 'react'
import type { import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1 const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000 const TOAST_REMOVE_DELAY = 1000000
@@ -17,10 +14,10 @@ type ToasterToast = ToastProps & {
} }
const actionTypes = { const actionTypes = {
ADD_TOAST: "ADD_TOAST", ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: "UPDATE_TOAST", UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: "DISMISS_TOAST", DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: "REMOVE_TOAST", REMOVE_TOAST: 'REMOVE_TOAST',
} as const } as const
let count = 0 let count = 0
@@ -34,20 +31,20 @@ type ActionType = typeof actionTypes
type Action = type Action =
| { | {
type: ActionType["ADD_TOAST"] type: ActionType['ADD_TOAST']
toast: ToasterToast toast: ToasterToast
} }
| { | {
type: ActionType["UPDATE_TOAST"] type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast> toast: Partial<ToasterToast>
} }
| { | {
type: ActionType["DISMISS_TOAST"] type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast["id"] toastId?: ToasterToast['id']
} }
| { | {
type: ActionType["REMOVE_TOAST"] type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast["id"] toastId?: ToasterToast['id']
} }
interface State { interface State {
@@ -64,7 +61,7 @@ const addToRemoveQueue = (toastId: string) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
toastTimeouts.delete(toastId) toastTimeouts.delete(toastId)
dispatch({ dispatch({
type: "REMOVE_TOAST", type: 'REMOVE_TOAST',
toastId: toastId, toastId: toastId,
}) })
}, TOAST_REMOVE_DELAY) }, TOAST_REMOVE_DELAY)
@@ -74,21 +71,19 @@ const addToRemoveQueue = (toastId: string) => {
export const reducer = (state: State, action: Action): State => { export const reducer = (state: State, action: Action): State => {
switch (action.type) { switch (action.type) {
case "ADD_TOAST": case 'ADD_TOAST':
return { return {
...state, ...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
} }
case "UPDATE_TOAST": case 'UPDATE_TOAST':
return { return {
...state, ...state,
toasts: state.toasts.map((t) => toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
} }
case "DISMISS_TOAST": { case 'DISMISS_TOAST': {
const { toastId } = action const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action, // ! Side effects ! - This could be extracted into a dismissToast() action,
@@ -109,11 +104,11 @@ export const reducer = (state: State, action: Action): State => {
...t, ...t,
open: false, open: false,
} }
: t : t,
), ),
} }
} }
case "REMOVE_TOAST": case 'REMOVE_TOAST':
if (action.toastId === undefined) { if (action.toastId === undefined) {
return { return {
...state, ...state,
@@ -138,20 +133,20 @@ function dispatch(action: Action) {
}) })
} }
type Toast = Omit<ToasterToast, "id"> type Toast = Omit<ToasterToast, 'id'>
function toast({ ...props }: Toast) { function toast({ ...props }: Toast) {
const id = genId() const id = genId()
const update = (props: ToasterToast) => const update = (props: ToasterToast) =>
dispatch({ dispatch({
type: "UPDATE_TOAST", type: 'UPDATE_TOAST',
toast: { ...props, id }, toast: { ...props, id },
}) })
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
dispatch({ dispatch({
type: "ADD_TOAST", type: 'ADD_TOAST',
toast: { toast: {
...props, ...props,
id, id,
@@ -185,7 +180,7 @@ function useToast() {
return { return {
...state, ...state,
toast, toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
} }
} }

View File

@@ -1,36 +1,38 @@
import * as z from "zod" import * as z from 'zod'
import { Client, Server } from "./pb/common" import { Client, Server } from './pb/common'
import { GetPlatformInfoResponse } from "./pb/api_user" import { GetPlatformInfoResponse } from './pb/api_user'
export const API_PATH = '/api/v1' export const API_PATH = '/api/v1'
export const SET_TOKEN_HEADER = 'x-set-authorization' export const SET_TOKEN_HEADER = 'x-set-authorization'
export const X_CLIENT_REQUEST_ID = 'x-client-request-id' export const X_CLIENT_REQUEST_ID = 'x-client-request-id'
export const LOCAL_STORAGE_TOKEN_KEY = 'token' export const LOCAL_STORAGE_TOKEN_KEY = 'token'
export const ZodPortSchema = z.coerce.number().min(1, { export const ZodPortSchema = z.coerce
message: "端口号不能小于 1", .number()
}).max(65535, { message: "端口号不能大于 65535" }) .min(1, {
export const ZodIPSchema = z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, message: '端口号不能小于 1',
{ message: "请输入正确的IP地址" }) })
export const ZodStringSchema = z.string().min(1, .max(65535, { message: '端口号不能大于 65535' })
{ message: "不能为空" }) export const ZodIPSchema = z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, { message: '请输入正确的IP地址' })
export const ZodEmailSchema = z.string() export const ZodStringSchema = z.string().min(1, { message: '不能为空' })
.min(1, { message: "不能为空" }) export const ZodEmailSchema = z.string().min(1, { message: '不能为空' }).email('是不是输错了邮箱地址呢?')
.email("是不是输错了邮箱地址呢?")
// .refine((e) => e === "abcd@fg.com", "This email is not in our database") // .refine((e) => e === "abcd@fg.com", "This email is not in our database")
export const ExecCommandStr = <T extends Client | Server>(type: string, item: T, info: GetPlatformInfoResponse, fileName?: string) => { export const ExecCommandStr = <T extends Client | Server>(
return `${fileName || 'frp-panel'} ${type type: string,
} -s ${item.secret item: T,
} -i ${item.id info: GetPlatformInfoResponse,
} -a ${info.globalSecret fileName?: string,
} -r ${info.masterRpcHost ) => {
} -c ${info.masterRpcPort return `${fileName || 'frp-panel'} ${type} -s ${item.secret} -i ${item.id} -a ${info.globalSecret} -r ${
} -p ${info.masterApiPort info.masterRpcHost
} -e ${info.masterApiScheme } -c ${info.masterRpcPort} -p ${info.masterApiPort} -e ${info.masterApiScheme}`
}`
} }
export const WindowsInstallCommand = <T extends Client | Server>(type: string, item: T, info: GetPlatformInfoResponse) => { export const WindowsInstallCommand = <T extends Client | Server>(
type: string,
item: T,
info: GetPlatformInfoResponse,
) => {
return `Invoke-WebRequest -Uri 'https://github.com/your_repository/frp-panel/releases/latest/download/frp-panel-amd64.exe' -OutFile 'frp-panel.exe' return `Invoke-WebRequest -Uri 'https://github.com/your_repository/frp-panel/releases/latest/download/frp-panel-amd64.exe' -OutFile 'frp-panel.exe'
Move-Item .\\frp-panel.exe C:\\Tools\\frp-panel.exe Move-Item .\\frp-panel.exe C:\\Tools\\frp-panel.exe
$command = "C:\\Tools\\${ExecCommandStr(type, item, info, 'frp-panel.exe')}" $command = "C:\\Tools\\${ExecCommandStr(type, item, info, 'frp-panel.exe')}"
@@ -38,6 +40,10 @@ export const WindowsInstallCommand = <T extends Client | Server>(type: string, i
New-Service -Name 'FRPPanel' -BinaryPathName 'C:\\Tools\\frp-panel.exe' -StartupType Automatic | Start-Service` New-Service -Name 'FRPPanel' -BinaryPathName 'C:\\Tools\\frp-panel.exe' -StartupType Automatic | Start-Service`
} }
export const LinuxInstallCommand = <T extends Client | Server>(type: string, item: T, info: GetPlatformInfoResponse) => { export const LinuxInstallCommand = <T extends Client | Server>(
return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, " ")}` type: string,
item: T,
info: GetPlatformInfoResponse,
) => {
return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}`
} }

View File

@@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from 'clsx'
import { twMerge } from "tailwind-merge" import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))

View File

@@ -1,15 +1,15 @@
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { SideBar } from '@/components/sidebar'; import { SideBar } from '@/components/sidebar'
import { FRPCFormCard } from '@/components/frpc_card'; import { FRPCFormCard } from '@/components/frpc_card'
export default function ClientEditPage() { export default function ClientEditPage() {
return ( return (
<RootLayout header={<Header />} sidebar={<SideBar />}> <RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers> <Providers>
<div className='w-full'> <div className="w-full">
<div className='flex-1 flex-col'> <div className="flex-1 flex-col">
<FRPCFormCard /> <FRPCFormCard />
</div> </div>
</div> </div>

View File

@@ -1,16 +1,16 @@
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { ClientList } from '@/components/client_list'; import { ClientList } from '@/components/client_list'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { SideBar } from '@/components/sidebar'; import { SideBar } from '@/components/sidebar'
import { CreateClientDialog } from '@/components/client_create_dialog'; import { CreateClientDialog } from '@/components/client_create_dialog'
export default function ClientListPage() { export default function ClientListPage() {
return ( return (
<RootLayout header={<Header />} sidebar={<SideBar />}> <RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers> <Providers>
<div className='w-full'> <div className="w-full">
<div className='flex-1 flex-col'> <div className="flex-1 flex-col">
<div className="flex-1 flex-row mb-2"> <div className="flex-1 flex-row mb-2">
<CreateClientDialog /> <CreateClientDialog />
</div> </div>

View File

@@ -1,13 +1,11 @@
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { ServerList } from '@/components/server_list'; import { ServerList } from '@/components/server_list'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { SideBar } from '@/components/sidebar'; import { SideBar } from '@/components/sidebar'
import { PlatformInfo } from '@/components/platforminfo'; import { PlatformInfo } from '@/components/platforminfo'
export default function Home() { export default function Home() {
return ( return (
<RootLayout header={<Header />} sidebar={<SideBar />}> <RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers> <Providers>

View File

@@ -1,53 +1,58 @@
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { TbBuildingTunnel } from "react-icons/tb"; import { TbBuildingTunnel } from 'react-icons/tb'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { LoginComponent } from '@/components/login'; import { LoginComponent } from '@/components/login'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router'
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster'
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
export default function Login() { export default function Login() {
const router = useRouter() const router = useRouter()
return ( return (
<main <main className={`${inter.className}`}>
className={`${inter.className}`}
>
<Providers> <Providers>
<div className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden" <div
className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
onClick={() => router.push('/')} onClick={() => router.push('/')}
> >
<div className='flex rounded hover:bg-slate-100 p-2'> <div className="flex rounded hover:bg-slate-100 p-2">
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" /> <TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel FRP Panel
</div> </div>
</div> </div>
<div className='container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0'> <div className="container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<div className='relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r'> <div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
<div className='absolute inset-0 bg-zinc-900'></div> <div className="absolute inset-0 bg-zinc-900"></div>
<div className="relative flex items-center text-lg font-medium" <div className="relative flex items-center text-lg font-medium" onClick={() => router.push('/')}>
onClick={() => router.push('/')} <div className="flex rounded hover:bg-zinc-800 p-2">
>
<div className='flex rounded hover:bg-zinc-800 p-2'>
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" /> <TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel FRP Panel
</div> </div>
</div> </div>
<div className="relative z-20 mt-auto"> <div className="relative z-20 mt-auto">
<blockquote className="space-y-2"> <blockquote className="space-y-2">
<p className="text-lg">A multi node frp webui and for <a href='https://github.com/fatedier/frp'>[FRP]</a> server and client management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source alternative <p className="text-lg">
</p><footer className="text-sm">navigate to: <a href='https://github.com/VaalaCat/frp-panel'>VaalaCat/frp-panel</a></footer></blockquote></div> A multi node frp webui and for <a href="https://github.com/fatedier/frp">[FRP]</a> server and client
management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source
alternative
</p>
<footer className="text-sm">
navigate to: <a href="https://github.com/VaalaCat/frp-panel">VaalaCat/frp-panel</a>
</footer>
</blockquote>
</div> </div>
<div className='lg:p-8 justify-center w-[300px]'> </div>
<div className='flex flex-col justify-center space-y-6 w-[300px]'> <div className="lg:p-8 justify-center w-[300px]">
<div className="flex flex-col justify-center space-y-6 w-[300px]">
<div className="flex flex-col space-y-2 text-center"> <div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight"></h1> <h1 className="text-2xl font-semibold tracking-tight"></h1>
<p className="text-sm text-muted-foreground"></p> <p className="text-sm text-muted-foreground"></p>
</div> </div>
<div className='w-full justify-center'> <div className="w-full justify-center">
<div className='w-[300px]'> <div className="w-[300px]">
<LoginComponent /> <LoginComponent />
</div> </div>
</div> </div>

View File

@@ -1,51 +1,56 @@
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { TbBuildingTunnel } from "react-icons/tb"; import { TbBuildingTunnel } from 'react-icons/tb'
import { RegisterComponent } from '@/components/register'; import { RegisterComponent } from '@/components/register'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router'
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster'
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
export default function Login() { export default function Login() {
const router = useRouter() const router = useRouter()
return ( return (
<main <main className={`${inter.className}`}>
className={`${inter.className}`}
>
<Providers> <Providers>
<div className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden" <div
className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
onClick={() => router.push('/')} onClick={() => router.push('/')}
> >
<div className='flex rounded hover:bg-slate-100 p-2'> <div className="flex rounded hover:bg-slate-100 p-2">
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" /> <TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel FRP Panel
</div> </div>
</div> </div>
<div className='container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0'> <div className="container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<div className='relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r'> <div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
<div className='absolute inset-0 bg-zinc-900'></div> <div className="absolute inset-0 bg-zinc-900"></div>
<div className="relative flex items-center text-lg font-medium" <div className="relative flex items-center text-lg font-medium" onClick={() => router.push('/')}>
onClick={() => router.push('/')} <div className="flex rounded hover:bg-zinc-800 p-2">
>
<div className='flex rounded hover:bg-zinc-800 p-2'>
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" /> <TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel FRP Panel
</div> </div>
</div> </div>
<div className="relative z-20 mt-auto"> <div className="relative z-20 mt-auto">
<blockquote className="space-y-2"> <blockquote className="space-y-2">
<p className="text-lg">A multi node frp webui and for <a href='https://github.com/fatedier/frp'>[FRP]</a> server and client management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source alternative <p className="text-lg">
</p><footer className="text-sm">navigate to: <a href='https://github.com/VaalaCat/frp-panel'>VaalaCat/frp-panel</a></footer></blockquote></div> A multi node frp webui and for <a href="https://github.com/fatedier/frp">[FRP]</a> server and client
management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source
alternative
</p>
<footer className="text-sm">
navigate to: <a href="https://github.com/VaalaCat/frp-panel">VaalaCat/frp-panel</a>
</footer>
</blockquote>
</div> </div>
<div className='lg:p-8 justify-center w-[300px]'> </div>
<div className='flex flex-col justify-center space-y-6 w-[300px]'> <div className="lg:p-8 justify-center w-[300px]">
<div className="flex flex-col justify-center space-y-6 w-[300px]">
<div className="flex flex-col space-y-2 text-center"> <div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight"></h1> <h1 className="text-2xl font-semibold tracking-tight"></h1>
<p className="text-sm text-muted-foreground"></p> <p className="text-sm text-muted-foreground"></p>
</div> </div>
<div className='w-full justify-center'> <div className="w-full justify-center">
<div className='w-[300px]'> <div className="w-[300px]">
<RegisterComponent /> <RegisterComponent />
</div> </div>
</div> </div>

View File

@@ -1,15 +1,15 @@
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { SideBar } from '@/components/sidebar'; import { SideBar } from '@/components/sidebar'
import { FRPSFormCard } from '@/components/frps_card'; import { FRPSFormCard } from '@/components/frps_card'
export default function ServerListPage() { export default function ServerListPage() {
return ( return (
<RootLayout header={<Header />} sidebar={<SideBar />}> <RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers> <Providers>
<div className='w-full'> <div className="w-full">
<div className='flex-1 flex-col'> <div className="flex-1 flex-col">
<FRPSFormCard /> <FRPSFormCard />
</div> </div>
</div> </div>

View File

@@ -1,17 +1,16 @@
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { ServerList } from '@/components/server_list'; import { ServerList } from '@/components/server_list'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { SideBar } from '@/components/sidebar'; import { SideBar } from '@/components/sidebar'
import { CreateServerDialog } from '@/components/server_create_dialog'; import { CreateServerDialog } from '@/components/server_create_dialog'
export default function ServerListPage() { export default function ServerListPage() {
return ( return (
<RootLayout header={<Header />} sidebar={<SideBar />}> <RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers> <Providers>
<div className='w-full'> <div className="w-full">
<div className='flex-1 flex-col'> <div className="flex-1 flex-col">
<div className="flex-1 flex-row mb-2"> <div className="flex-1 flex-row mb-2">
<CreateServerDialog /> <CreateServerDialog />
</div> </div>

View File

@@ -1,12 +1,11 @@
import { FRPCFormCard } from '@/components/frpc_card'; import { FRPCFormCard } from '@/components/frpc_card'
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers'
import { APITest } from '@/components/apitest'; import { APITest } from '@/components/apitest'
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator'
import { FRPSFormCard } from '@/components/frps_card'; import { FRPSFormCard } from '@/components/frps_card'
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout'
import { Header } from '@/components/header'; import { Header } from '@/components/header'
import { SideBar } from '@/components/sidebar'; import { SideBar } from '@/components/sidebar'
export default function Test() { export default function Test() {
return ( return (

3172
www/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { TypedProxyConfig } from "@/types/proxy" import { TypedProxyConfig } from '@/types/proxy'
import { atom } from "nanostores" import { atom } from 'nanostores'
export const $clientProxyConfigs = atom<TypedProxyConfig[]>([]) export const $clientProxyConfigs = atom<TypedProxyConfig[]>([])

View File

@@ -1,77 +1,72 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], darkMode: ['class'],
content: [ content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'],
'./pages/**/*.{ts,tsx}', prefix: '',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px',
}, },
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))',
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))',
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))',
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))',
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))',
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))',
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))',
}, },
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)", sm: 'calc(var(--radius) - 4px)',
}, },
keyframes: { keyframes: {
"accordion-down": { 'accordion-down': {
from: { height: "0" }, from: { height: '0' },
to: { height: "var(--radix-accordion-content-height)" }, to: { height: 'var(--radix-accordion-content-height)' },
}, },
"accordion-up": { 'accordion-up': {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: 'var(--radix-accordion-content-height)' },
to: { height: "0" }, to: { height: '0' },
}, },
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", 'accordion-down': 'accordion-down 0.2s ease-out',
"accordion-up": "accordion-up 0.2s ease-out", 'accordion-up': 'accordion-up 0.2s ease-out',
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require('tailwindcss-animate')],
} }

View File

@@ -10,8 +10,7 @@ const config: Config = {
extend: { extend: {
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
}, },
}, },

View File

@@ -14,9 +14,9 @@
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"],
} },
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"] "exclude": ["node_modules"],
} }

View File

@@ -1,9 +1,9 @@
export interface APIMetadata { export interface APIMetadata {
version: string; version: string
} }
export interface BaseResponse { export interface BaseResponse {
code: number; code: number
msg: string; msg: string
body?: any; body?: any
} }

View File

@@ -1,84 +1,84 @@
import { AuthMethod, AuthScope, LogConfig, QUICOptions, TLSConfig, WebServerConfig } from "./common"; import { AuthMethod, AuthScope, LogConfig, QUICOptions, TLSConfig, WebServerConfig } from './common'
import { TypedProxyConfig } from "./proxy"; import { TypedProxyConfig } from './proxy'
import { TypedVisitorConfig } from "./visitor"; import { TypedVisitorConfig } from './visitor'
export interface AuthOIDCClientConfig { export interface AuthOIDCClientConfig {
clientID?: string; clientID?: string
clientSecret?: string; clientSecret?: string
audience?: string; audience?: string
scope?: string; scope?: string
tokenEndpointURL?: string; tokenEndpointURL?: string
additionalEndpointParams?: { [key: string]: string }; additionalEndpointParams?: { [key: string]: string }
} }
export interface AuthClientConfig { export interface AuthClientConfig {
method?: AuthMethod; method?: AuthMethod
additionalScopes?: AuthScope[]; additionalScopes?: AuthScope[]
token?: string; token?: string
oidc?: AuthOIDCClientConfig; oidc?: AuthOIDCClientConfig
} }
export interface ClientTransportConfig { export interface ClientTransportConfig {
protocol?: string; protocol?: string
dialServerTimeout?: number; dialServerTimeout?: number
dialServerKeepAlive?: number; dialServerKeepAlive?: number
connectServerLocalIP?: string; connectServerLocalIP?: string
proxyURL?: string; proxyURL?: string
poolCount?: number; poolCount?: number
tcpMux?: boolean; tcpMux?: boolean
tcpMuxKeepaliveInterval?: number; tcpMuxKeepaliveInterval?: number
quic?: QUICOptions; quic?: QUICOptions
heartbeatInterval?: number; heartbeatInterval?: number
heartbeatTimeout?: number; heartbeatTimeout?: number
tls?: TLSClientConfig; tls?: TLSClientConfig
} }
export interface TLSClientConfig { export interface TLSClientConfig {
enable?: boolean; enable?: boolean
disableCustomTLSFirstByte?: boolean; disableCustomTLSFirstByte?: boolean
tls?: TLSConfig; tls?: TLSConfig
} }
export interface CompleteTLSClientConfig extends TLSClientConfig { export interface CompleteTLSClientConfig extends TLSClientConfig {
enable: boolean; enable: boolean
disableCustomTLSFirstByte: boolean; disableCustomTLSFirstByte: boolean
} }
export interface AuthClientConfig { export interface AuthClientConfig {
auth?: AuthClientConfig; auth?: AuthClientConfig
user?: string; user?: string
serverAddr?: string; serverAddr?: string
serverPort?: number; serverPort?: number
natHoleStunServer?: string; natHoleStunServer?: string
dnsServer?: string; dnsServer?: string
loginFailExit?: boolean; loginFailExit?: boolean
start?: string[]; start?: string[]
log?: LogConfig; log?: LogConfig
webServer?: WebServerConfig; webServer?: WebServerConfig
transport?: ClientTransportConfig; transport?: ClientTransportConfig
udpPacketSize?: number; udpPacketSize?: number
metadatas?: { [key: string]: string }; metadatas?: { [key: string]: string }
includes?: string[]; includes?: string[]
} }
export interface ClientConfig extends ClientCommonConfig { export interface ClientConfig extends ClientCommonConfig {
proxies?: TypedProxyConfig[]; proxies?: TypedProxyConfig[]
visitors?: TypedVisitorConfig[]; visitors?: TypedVisitorConfig[]
} }
export interface ClientCommonConfig extends AuthClientConfig { export interface ClientCommonConfig extends AuthClientConfig {
auth?: AuthClientConfig; auth?: AuthClientConfig
user?: string; user?: string
serverAddr: string; serverAddr: string
serverPort: number; serverPort: number
natHoleStunServer?: string; natHoleStunServer?: string
dnsServer?: string; dnsServer?: string
loginFailExit?: boolean; loginFailExit?: boolean
start?: string[]; start?: string[]
log?: LogConfig; log?: LogConfig
webServer?: WebServerConfig; webServer?: WebServerConfig
transport?: ClientTransportConfig; transport?: ClientTransportConfig
udpPacketSize?: number; udpPacketSize?: number
metadatas?: { [key: string]: string }; metadatas?: { [key: string]: string }
includes?: string[]; includes?: string[]
} }

View File

@@ -1,64 +1,64 @@
export interface QUICOptions { export interface QUICOptions {
keepalivePeriod?: number; keepalivePeriod?: number
maxIdleTimeout?: number; maxIdleTimeout?: number
maxIncomingStreams?: number; maxIncomingStreams?: number
} }
export interface WebServerConfig { export interface WebServerConfig {
addr?: string; addr?: string
port?: number; port?: number
user?: string; user?: string
password?: string; password?: string
assetsDir?: string; assetsDir?: string
pprofEnable?: boolean; pprofEnable?: boolean
tls?: TLSConfig; tls?: TLSConfig
} }
export interface TLSConfig { export interface TLSConfig {
certFile?: string; certFile?: string
keyFile?: string; keyFile?: string
trustedCaFile?: string; trustedCaFile?: string
serverName?: string; serverName?: string
} }
export interface LogConfig { export interface LogConfig {
to?: string; to?: string
level?: string; level?: string
maxDays: number; maxDays: number
disablePrintColor?: boolean; disablePrintColor?: boolean
} }
export interface HTTPPluginOptions { export interface HTTPPluginOptions {
name: string; name: string
addr: string; addr: string
path: string; path: string
ops: string[]; ops: string[]
tlsVerify?: boolean; tlsVerify?: boolean
} }
export interface HeaderOperations { export interface HeaderOperations {
set?: { [key: string]: string }; set?: { [key: string]: string }
} }
export type AuthMethod = "token" | "oidc"; export type AuthMethod = 'token' | 'oidc'
export const AuthMethodToken: AuthMethod = "token"; export const AuthMethodToken: AuthMethod = 'token'
export const AuthMethodOIDC: AuthMethod = "oidc"; export const AuthMethodOIDC: AuthMethod = 'oidc'
export type AuthScope = "HeartBeats" | "NewWorkConns"; export type AuthScope = 'HeartBeats' | 'NewWorkConns'
export const AuthScopeHeartBeats: AuthScope = "HeartBeats"; export const AuthScopeHeartBeats: AuthScope = 'HeartBeats'
export const AuthScopeNewWorkConns: AuthScope = "NewWorkConns"; export const AuthScopeNewWorkConns: AuthScope = 'NewWorkConns'
export interface PortsRange { export interface PortsRange {
start?: number; start?: number
end?: number; end?: number
single?: number; single?: number
} }
export type BandwidthUnit = "MB" | "KB"; export type BandwidthUnit = 'MB' | 'KB'
export interface BandwidthQuantity { export interface BandwidthQuantity {
s: BandwidthUnit; // MB or KB s: BandwidthUnit // MB or KB
i: number; // bytes i: number // bytes
} }

View File

@@ -1,58 +1,58 @@
import { HeaderOperations } from "./common"; import { HeaderOperations } from './common'
export interface ClientPluginOptions { } export interface ClientPluginOptions {}
export interface TypedClientPluginOptions { export interface TypedClientPluginOptions {
type: string; type: string
clientPluginOptions?: ClientPluginOptions; clientPluginOptions?: ClientPluginOptions
} }
export interface HTTP2HTTPSPluginOptions { export interface HTTP2HTTPSPluginOptions {
type?: string; type?: string
localAddr?: string; localAddr?: string
hostHeaderRewrite?: string; hostHeaderRewrite?: string
requestHeaders?: HeaderOperations; requestHeaders?: HeaderOperations
} }
export interface HTTPProxyPluginOptions { export interface HTTPProxyPluginOptions {
type?: string; type?: string
httpUser?: string; httpUser?: string
httpPassword?: string; httpPassword?: string
} }
export interface HTTPS2HTTPPluginOptions { export interface HTTPS2HTTPPluginOptions {
type?: string; type?: string
localAddr?: string; localAddr?: string
hostHeaderRewrite?: string; hostHeaderRewrite?: string
requestHeaders?: HeaderOperations; requestHeaders?: HeaderOperations
crtPath?: string; crtPath?: string
keyPath?: string; keyPath?: string
} }
export interface HTTPS2HTTPSPluginOptions { export interface HTTPS2HTTPSPluginOptions {
type?: string; type?: string
localAddr?: string; localAddr?: string
hostHeaderRewrite?: string; hostHeaderRewrite?: string
requestHeaders?: HeaderOperations; requestHeaders?: HeaderOperations
crtPath?: string; crtPath?: string
keyPath?: string; keyPath?: string
} }
export interface Socks5PluginOptions { export interface Socks5PluginOptions {
type?: string; type?: string
username?: string; username?: string
password?: string; password?: string
} }
export interface StaticFilePluginOptions { export interface StaticFilePluginOptions {
type?: string; type?: string
localPath?: string; localPath?: string
stripPrefix?: string; stripPrefix?: string
httpUser?: string; httpUser?: string
httpPassword?: string; httpPassword?: string
} }
export interface UnixDomainSocketPluginOptions { export interface UnixDomainSocketPluginOptions {
type?: string; type?: string
unixPath?: string; unixPath?: string
} }

View File

@@ -1,106 +1,110 @@
import { BandwidthQuantity, HeaderOperations } from "./common"; import { BandwidthQuantity, HeaderOperations } from './common'
import { TypedClientPluginOptions } from "./plugin"; import { TypedClientPluginOptions } from './plugin'
export interface ProxyTransport { export interface ProxyTransport {
useEncryption?: boolean; useEncryption?: boolean
useCompression?: boolean; useCompression?: boolean
bandwidthLimit?: BandwidthQuantity; bandwidthLimit?: BandwidthQuantity
bandwidthLimitMode?: string; bandwidthLimitMode?: string
proxyProtocolVersion?: string; proxyProtocolVersion?: string
} }
export interface LoadBalancerConfig { export interface LoadBalancerConfig {
group: string; group: string
groupKey?: string; groupKey?: string
} }
export interface ProxyBackend { export interface ProxyBackend {
localIP?: string; localIP?: string
localPort?: number; localPort?: number
plugin?: TypedClientPluginOptions; plugin?: TypedClientPluginOptions
} }
export interface HealthCheckConfig { export interface HealthCheckConfig {
type: string; type: string
timeoutSeconds?: number; timeoutSeconds?: number
maxFailed?: number; maxFailed?: number
intervalSeconds: number; intervalSeconds: number
path?: string; path?: string
} }
export interface DomainConfig { export interface DomainConfig {
customDomains?: string[]; customDomains?: string[]
subdomain?: string; subdomain?: string
} }
export interface ProxyBaseConfig { export interface ProxyBaseConfig {
name: string; name: string
type: string; type: string
transport?: ProxyTransport; transport?: ProxyTransport
metadatas?: { [key: string]: string }; metadatas?: { [key: string]: string }
loadBalancer?: LoadBalancerConfig; loadBalancer?: LoadBalancerConfig
healthCheck?: HealthCheckConfig; healthCheck?: HealthCheckConfig
localIP?: string; localIP?: string
localPort?: number; localPort?: number
plugin?: TypedClientPluginOptions; plugin?: TypedClientPluginOptions
} }
export type TypedProxyConfig = TCPProxyConfig | export type TypedProxyConfig =
UDPProxyConfig | HTTPProxyConfig | | TCPProxyConfig
HTTPSProxyConfig | TCPMuxProxyConfig | | UDPProxyConfig
STCPProxyConfig | XTCPProxyConfig | | HTTPProxyConfig
SUDPProxyConfig; | HTTPSProxyConfig
| TCPMuxProxyConfig
| STCPProxyConfig
| XTCPProxyConfig
| SUDPProxyConfig
export type ProxyType = "tcp" | "udp" | "tcpmux" | "http" | "https" | "stcp" | "xtcp" | "sudp"; export type ProxyType = 'tcp' | 'udp' | 'tcpmux' | 'http' | 'https' | 'stcp' | 'xtcp' | 'sudp'
export interface TCPProxyConfig extends ProxyBaseConfig { export interface TCPProxyConfig extends ProxyBaseConfig {
type: "tcp" type: 'tcp'
remotePort?: number; remotePort?: number
} }
export interface UDPProxyConfig extends ProxyBaseConfig { export interface UDPProxyConfig extends ProxyBaseConfig {
type: "udp" type: 'udp'
remotePort?: number; remotePort?: number
} }
export interface HTTPProxyConfig extends ProxyBaseConfig, DomainConfig { export interface HTTPProxyConfig extends ProxyBaseConfig, DomainConfig {
type: "http" type: 'http'
locations?: string[]; locations?: string[]
httpUser?: string; httpUser?: string
httpPassword?: string; httpPassword?: string
hostHeaderRewrite?: string; hostHeaderRewrite?: string
requestHeaders?: HeaderOperations; requestHeaders?: HeaderOperations
routeByHTTPUser?: string; routeByHTTPUser?: string
} }
export interface HTTPSProxyConfig extends ProxyBaseConfig, DomainConfig { export interface HTTPSProxyConfig extends ProxyBaseConfig, DomainConfig {
type: "https" type: 'https'
} }
export type TCPMultiplexerType = "httpconnect"; export type TCPMultiplexerType = 'httpconnect'
export interface TCPMuxProxyConfig extends ProxyBaseConfig, DomainConfig { export interface TCPMuxProxyConfig extends ProxyBaseConfig, DomainConfig {
type: "tcpmux" type: 'tcpmux'
httpUser?: string; httpUser?: string
httpPassword?: string; httpPassword?: string
routeByHTTPUser?: string; routeByHTTPUser?: string
multiplexer?: string; multiplexer?: string
} }
export interface STCPProxyConfig extends ProxyBaseConfig { export interface STCPProxyConfig extends ProxyBaseConfig {
type: "stcp" type: 'stcp'
secretKey?: string; secretKey?: string
allowUsers?: string[]; allowUsers?: string[]
} }
export interface XTCPProxyConfig extends ProxyBaseConfig { export interface XTCPProxyConfig extends ProxyBaseConfig {
type: "xtcp" type: 'xtcp'
secretKey?: string; secretKey?: string
allowUsers?: string[]; allowUsers?: string[]
} }
export interface SUDPProxyConfig extends ProxyBaseConfig { export interface SUDPProxyConfig extends ProxyBaseConfig {
type: "sudp" type: 'sudp'
secretKey?: string; secretKey?: string
allowUsers?: string[]; allowUsers?: string[]
} }

View File

@@ -1,66 +1,64 @@
import { AuthMethod, AuthScope, HTTPPluginOptions, LogConfig, PortsRange, QUICOptions, WebServerConfig } from "./common"; import { AuthMethod, AuthScope, HTTPPluginOptions, LogConfig, PortsRange, QUICOptions, WebServerConfig } from './common'
export interface ServerConfig { export interface ServerConfig {
auth?: AuthServerConfig; auth?: AuthServerConfig
bindAddr?: string; bindAddr?: string
bindPort?: number; bindPort?: number
kcpBindPort?: number; kcpBindPort?: number
quicBindPort?: number; quicBindPort?: number
proxyBindAddr?: string; proxyBindAddr?: string
vhostHTTPPort?: number; vhostHTTPPort?: number
vhostHTTPTimeout?: number; vhostHTTPTimeout?: number
vhostHTTPSPort?: number; vhostHTTPSPort?: number
tcpmuxHTTPConnectPort?: number; tcpmuxHTTPConnectPort?: number
tcpmuxPassthrough?: boolean; tcpmuxPassthrough?: boolean
subDomainHost?: string; subDomainHost?: string
custom404Page?: string; custom404Page?: string
sshTunnelGateway?: SSHTunnelGateway; sshTunnelGateway?: SSHTunnelGateway
webServer?: WebServerConfig; webServer?: WebServerConfig
enablePrometheus?: boolean; enablePrometheus?: boolean
log?: LogConfig; log?: LogConfig
transport?: ServerTransportConfig; transport?: ServerTransportConfig
detailedErrorsToClient?: boolean; detailedErrorsToClient?: boolean
maxPortsPerClient?: number; maxPortsPerClient?: number
userConnTimeout?: number; userConnTimeout?: number
udpPacketSize?: number; udpPacketSize?: number
natholeAnalysisDataReserveHours?: number; natholeAnalysisDataReserveHours?: number
allowPorts?: PortsRange[]; allowPorts?: PortsRange[]
httpPlugins?: HTTPPluginOptions[]; httpPlugins?: HTTPPluginOptions[]
} }
export interface AuthServerConfig { export interface AuthServerConfig {
method?: AuthMethod; method?: AuthMethod
additionalScopes?: AuthScope[]; additionalScopes?: AuthScope[]
token?: string; token?: string
oidc?: AuthOIDCServerConfig; oidc?: AuthOIDCServerConfig
} }
export interface AuthOIDCServerConfig { export interface AuthOIDCServerConfig {
issuer?: string; issuer?: string
audience?: string; audience?: string
skipExpiryCheck?: boolean; skipExpiryCheck?: boolean
skipIssuerCheck?: boolean; skipIssuerCheck?: boolean
} }
export interface ServerTransportConfig { export interface ServerTransportConfig {
tcpMux?: boolean; tcpMux?: boolean
tcpMuxKeepaliveInterval?: number; tcpMuxKeepaliveInterval?: number
tcpKeepalive?: number; tcpKeepalive?: number
maxPoolCount?: number; maxPoolCount?: number
heartbeatTimeout?: number; heartbeatTimeout?: number
quic?: QUICOptions; quic?: QUICOptions
tls?: TLSServerConfig; tls?: TLSServerConfig
} }
export interface TLSServerConfig { export interface TLSServerConfig {
force?: boolean; force?: boolean
} }
export interface SSHTunnelGateway { export interface SSHTunnelGateway {
bindPort?: number; bindPort?: number
privateKeyFile?: string; privateKeyFile?: string
autoGenPrivateKeyPath?: string; autoGenPrivateKeyPath?: string
authorizedKeysFile?: string; authorizedKeysFile?: string
} }

View File

@@ -1,41 +1,41 @@
export interface VisitorTransport { export interface VisitorTransport {
useEncryption?: boolean; useEncryption?: boolean
useCompression?: boolean; useCompression?: boolean
} }
export interface VisitorBaseConfig { export interface VisitorBaseConfig {
name: string; name: string
type: string; type: string
transport?: VisitorTransport; transport?: VisitorTransport
secretKey?: string; secretKey?: string
serverUser?: string; serverUser?: string
serverName?: string; serverName?: string
bindAddr?: string; bindAddr?: string
bindPort?: number; bindPort?: number
} }
export type VisitorType = "stcp" | "xtcp" | "sudp"; export type VisitorType = 'stcp' | 'xtcp' | 'sudp'
export type TypedVisitorConfig = STCPVisitorConfig | SUDPVisitorConfig | XTCPVisitorConfig; export type TypedVisitorConfig = STCPVisitorConfig | SUDPVisitorConfig | XTCPVisitorConfig
export interface STCPVisitorConfig extends VisitorBaseConfig { export interface STCPVisitorConfig extends VisitorBaseConfig {
type: "stcp" type: 'stcp'
} }
export interface SUDPVisitorConfig extends VisitorBaseConfig { export interface SUDPVisitorConfig extends VisitorBaseConfig {
type: "sudp"; type: 'sudp'
} }
export interface XTCPVisitorConfig extends VisitorBaseConfig { export interface XTCPVisitorConfig extends VisitorBaseConfig {
type: "xtcp"; type: 'xtcp'
protocol?: string; protocol?: string
keepTunnelOpen?: boolean; keepTunnelOpen?: boolean
maxRetriesAnHour?: number; maxRetriesAnHour?: number
minRetryInterval?: number; minRetryInterval?: number
fallbackTo?: string; fallbackTo?: string
fallbackTimeoutMs?: number; fallbackTimeoutMs?: number
} }
export interface ClientCommonConfig { export interface ClientCommonConfig {
user: string; user: string
} }