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 { API_PATH } from '@/lib/consts'
import {
LoginRequest, LoginResponse,
RegisterRequest, RegisterResponse
} from '@/lib/pb/api_auth'
import { LoginRequest, LoginResponse, RegisterRequest, RegisterResponse } from '@/lib/pb/api_auth'
import { CommonResponse } from '@/lib/pb/common'
import { BaseResponse } from '@/types/api'

View File

@@ -1,12 +1,16 @@
import http from '@/api/http'
import { API_PATH } from "@/lib/consts";
import { API_PATH } from '@/lib/consts'
import {
DeleteClientRequest, DeleteClientResponse,
GetClientRequest, GetClientResponse,
InitClientRequest, InitClientResponse,
ListClientsRequest, ListClientsResponse
} from '@/lib/pb/api_client';
import { BaseResponse } from "@/types/api";
DeleteClientRequest,
DeleteClientResponse,
GetClientRequest,
GetClientResponse,
InitClientRequest,
InitClientResponse,
ListClientsRequest,
ListClientsResponse,
} from '@/lib/pb/api_client'
import { BaseResponse } from '@/types/api'
export const getClient = async (req: GetClientRequest) => {
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) => {
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))
return InitClientResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -1,18 +1,8 @@
import http from '@/api/http'
import { API_PATH } from "@/lib/consts";
import {
RemoveFRPCRequest,
RemoveFRPCResponse,
UpdateFRPCRequest,
UpdateFRPCResponse
} from '@/lib/pb/api_client';
import {
RemoveFRPSRequest,
RemoveFRPSResponse,
UpdateFRPSRequest,
UpdateFRPSResponse
} from '@/lib/pb/api_server';
import { BaseResponse } from "@/types/api";
import { API_PATH } from '@/lib/consts'
import { RemoveFRPCRequest, RemoveFRPCResponse, 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) => {
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 { $token } from '@/store/user';
import { LOCAL_STORAGE_TOKEN_KEY, SET_TOKEN_HEADER, X_CLIENT_REQUEST_ID } from '@/lib/consts'
import { $token } from '@/store/user'
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4 } from 'uuid'
const instance = axios.create({})
@@ -11,7 +11,7 @@ instance.interceptors.request.use((request) => {
request.headers.Authorization = token
$token.set(token)
}
request.headers[X_CLIENT_REQUEST_ID] = uuidv4();
request.headers[X_CLIENT_REQUEST_ID] = uuidv4()
return request
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,17 @@
import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from "@radix-ui/react-icons"
import { Column } from "@tanstack/react-table"
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
import { Column } from '@tanstack/react-table'
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
@@ -32,18 +26,14 @@ export function DataTableColumnHeader<TData, TValue>({
}
return (
<div className={cn("flex items-center space-x-2", className)}>
<div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<Button variant="ghost" size="sm" className="-ml-3 h-8 data-[state=open]:bg-accent">
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
{column.getIsSorted() === 'desc' ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === "asc" ? (
) : column.getIsSorted() === 'asc' ? (
<ArrowUpIcon 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 {
ColumnDef,
@@ -11,20 +11,13 @@ import {
getPaginationRowModel,
SortingState,
Table as TableType,
} from "@tanstack/react-table"
} from '@tanstack/react-table'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import React from "react"
import { Input } from "@/components/ui/input"
import { DataTablePagination } from "./data_table_pagination"
import React from 'react'
import { Input } from '@/components/ui/input'
import { DataTablePagination } from './data_table_pagination'
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
@@ -33,24 +26,19 @@ interface DataTableProps<TData, TValue> {
table: TableType<TData>
}
export function DataTable<TData, TValue>({
columns,
filterColumnName,
table,
}: DataTableProps<TData, TValue>) {
export function DataTable<TData, TValue>({ columns, filterColumnName, table }: DataTableProps<TData, TValue>) {
return (
<div>
{filterColumnName && <div className="flex flex-1 items-center py-4">
{filterColumnName && (
<div className="flex flex-1 items-center py-4">
<Input
placeholder={`根据 ${filterColumnName} 筛选`}
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn(filterColumnName)?.setFilterValue(event.target.value)
}
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn(filterColumnName)?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
</div>}
</div>
)}
<div className="rounded-md border">
<Table>
<TableHeader>
@@ -59,12 +47,7 @@ export function DataTable<TData, TValue>({
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
@@ -74,14 +57,9 @@ export function DataTable<TData, TValue>({
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))

View File

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

View File

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

View File

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

View File

@@ -1,40 +1,24 @@
import { ProxyType, TypedProxyConfig } from "@/types/proxy"
import React, { useEffect } from "react"
import { useState } from "react"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Label } from "@radix-ui/react-label"
import { HTTPProxyForm, TCPProxyForm, UDPProxyForm } from "./proxy_form"
import { useQuery } from "@tanstack/react-query"
import { getClient } from "@/api/client"
import { useStore } from "@nanostores/react"
import { $clientProxyConfigs } from "@/store/proxy"
import { Button } from "./ui/button"
import { RespCode } from "@/lib/pb/common"
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 { ProxyType, TypedProxyConfig } from '@/types/proxy'
import React, { useEffect } from 'react'
import { useState } from 'react'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Label } from '@radix-ui/react-label'
import { HTTPProxyForm, TCPProxyForm, UDPProxyForm } from './proxy_form'
import { useQuery } from '@tanstack/react-query'
import { getClient } from '@/api/client'
import { useStore } from '@nanostores/react'
import { $clientProxyConfigs } from '@/store/proxy'
import { Button } from './ui/button'
import { RespCode } from '@/lib/pb/common'
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 { updateFRPC } from "@/api/frp"
import { Card, CardContent } from "./ui/card"
import { updateFRPC } from '@/api/frp'
import { Card, CardContent } from './ui/card'
export interface FRPCFormProps {
clientID: string
@@ -50,10 +34,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
}
const { data: client, refetch: refetchClient } = useQuery({
queryKey: ["getClient", clientID], queryFn: () => {
queryKey: ['getClient', clientID],
queryFn: () => {
return getClient({ clientId: clientID })
}
});
},
})
const clientProxyConfigs = useStore($clientProxyConfigs)
@@ -66,11 +51,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
}, [clientID, serverID, client])
const handleAddProxy = () => {
console.log("add proxy", proxyName, proxyType)
console.log('add proxy', proxyName, proxyType)
if (!proxyName) return
if (!proxyType) return
if (clientProxyConfigs.findIndex(proxy => proxy.name === proxyName) !== -1) {
toast({ title: "创建隧道状态", description: "名称重复" })
if (clientProxyConfigs.findIndex((proxy) => proxy.name === proxyName) !== -1) {
toast({ title: '创建隧道状态', description: '名称重复' })
return
}
const newProxy = {
@@ -81,23 +66,27 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
}
const handleDeleteProxy = (proxyName: string) => {
const newProxies = clientProxyConfigs.filter(proxy => proxy.name !== proxyName)
const newProxies = clientProxyConfigs.filter((proxy) => proxy.name !== proxyName)
$clientProxyConfigs.set(newProxies)
}
const updateFrpc = useMutation({ mutationFn: updateFRPC, })
const updateFrpc = useMutation({ mutationFn: updateFRPC })
const handleUpdate = async () => {
try {
const res = await updateFrpc.mutateAsync({
config: Buffer.from(JSON.stringify({
config: Buffer.from(
JSON.stringify({
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) {
console.error(error)
toast({ title: "更新隧道状态", description: "更新失败" })
toast({ title: '更新隧道状态', description: '更新失败' })
}
}
@@ -109,7 +98,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
</PopoverTrigger>
<PopoverContent>
<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}>
<Label className="text-sm font-medium"></Label>
<SelectTrigger className="my-2">
@@ -121,50 +114,56 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
<SelectItem value="udp">udp</SelectItem>
</SelectContent>
</Select>
<Button variant={"outline"} onClick={handleAddProxy}></Button>
<Button variant={'outline'} onClick={handleAddProxy}>
</Button>
</PopoverContent>
</Popover>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4" key={clientID + serverID + client}>
{
clientProxyConfigs.map((item) => {
return (<Card key={item.name}>
{clientProxyConfigs.map((item) => {
return (
<Card key={item.name}>
<CardContent>
<div className="flex flex-col w-full pt-2">
<Accordion type="single" collapsible>
<AccordionItem value={item.name}>
<AccordionHeader className="flex flex-row justify-between">
<div>{item.name}</div>
<Button variant={"outline"} onClick={() => { handleDeleteProxy(item.name) }}></Button>
<Button
variant={'outline'}
onClick={() => {
handleDeleteProxy(item.name)
}}
>
</Button>
</AccordionHeader>
<AccordionTrigger>:{item.type}</AccordionTrigger>
<AccordionContent>
{
item.type === 'tcp' && serverID && clientID &&
{item.type === 'tcp' && serverID && clientID && (
<TCPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
}
{
item.type === 'udp' && serverID && clientID &&
)}
{item.type === 'udp' && serverID && clientID && (
<UDPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
}
{
item.type === 'http' && serverID && clientID &&
)}
{item.type === 'http' && serverID && clientID && (
<HTTPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
}
)}
</AccordionContent>
</AccordionItem>
</Accordion>
@@ -172,10 +171,16 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
</CardContent>
</Card>
)
})
}
})}
</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 {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Label } from "./ui/label"
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"
import { useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from './ui/label'
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 {
serverID?: string
@@ -25,15 +18,17 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
const searchParams = useSearchParams()
const paramServerID = searchParams.get('serverID')
const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ["listServer"], queryFn: () => {
queryKey: ['listServer'],
queryFn: () => {
return listServer({ page: 1, pageSize: 100 })
}
});
},
})
const { data: server, refetch: refetchServer } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => {
queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID })
}
});
},
})
useEffect(() => {
if (defaultServerID) {
@@ -51,7 +46,8 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
setServerID(value)
}
return (<Card className="w-full">
return (
<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>Frps服务</CardDescription>
@@ -59,18 +55,15 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
<CardContent>
<div className=" flex items-center space-x-4 rounded-md border p-4">
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
</p>
<p className="text-sm text-muted-foreground">
</p>
<p className="text-sm font-medium leading-none"></p>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch onCheckedChange={setAdvanceMode} />
</div>
<div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label>
<Select onValueChange={handleServerChange}
<Select
onValueChange={handleServerChange}
value={serverID}
onOpenChange={() => {
refetchServers()
@@ -82,15 +75,22 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
</SelectTrigger>
<SelectContent>
{serverList?.servers.map(
serverItem => serverItem.id && <SelectItem key={serverItem.id} value={serverItem.id}>{serverItem.id}
</SelectItem>)}
(serverItem) =>
serverItem.id && (
<SelectItem key={serverItem.id} value={serverItem.id}>
{serverItem.id}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
{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>
<CardFooter>
</CardFooter>
</Card>)
<CardFooter></CardFooter>
</Card>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb';
import { useEffect } from 'react';
import { $platformInfo } from '@/store/user';
import { getPlatformInfo } from '@/api/platform';
import { useQuery } from '@tanstack/react-query'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb'
import { useEffect } from 'react'
import { $platformInfo } from '@/store/user'
import { getPlatformInfo } from '@/api/platform'
export const PlatformInfo = () => {
const platformInfo = useQuery({
queryKey: ['platformInfo'],
queryFn: getPlatformInfo
queryFn: getPlatformInfo,
})
useEffect(() => {
$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">
<Card>
<CardHeader>
<div className='flex justify-between'>
<h3 className='tracking-tight text-sm font-medium'>
</h3>
<TbServerBolt className='mt-1' />
<div className="flex justify-between">
<h3 className="tracking-tight text-sm font-medium"></h3>
<TbServerBolt className="mt-1" />
</div>
</CardHeader>
<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>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className='flex justify-between'>
<h3 className='tracking-tight text-sm font-medium'>
</h3>
<TbEngine className='mt-1' />
<div className="flex justify-between">
<h3 className="tracking-tight text-sm font-medium"></h3>
<TbEngine className="mt-1" />
</div>
</CardHeader>
<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>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className='flex justify-between'>
<h3 className='tracking-tight text-sm font-medium'>
</h3>
<TbServerOff className='mt-1' />
<div className="flex justify-between">
<h3 className="tracking-tight text-sm font-medium"></h3>
<TbServerOff className="mt-1" />
</div>
</CardHeader>
<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>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className='flex justify-between'>
<h3 className='tracking-tight text-sm font-medium'>
</h3>
<TbEngineOff className='mt-1' />
<div className="flex justify-between">
<h3 className="tracking-tight text-sm font-medium"></h3>
<TbEngineOff className="mt-1" />
</div>
</CardHeader>
<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>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className='flex justify-between'>
<h3 className='tracking-tight text-sm font-medium'>
</h3>
<TbServer2 className='mt-1' />
<div className="flex justify-between">
<h3 className="tracking-tight text-sm font-medium"></h3>
<TbServer2 className="mt-1" />
</div>
</CardHeader>
<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>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className='flex justify-between'>
<h3 className='tracking-tight text-sm font-medium'>
</h3>
<TbDeviceHeartMonitor className='mt-1' />
<div className="flex justify-between">
<h3 className="tracking-tight text-sm font-medium"></h3>
<TbDeviceHeartMonitor className="mt-1" />
</div>
</CardHeader>
<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>
</CardContent>
</Card>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { Button } from "./ui/button"
import { useRouter } from 'next/router';
import { useEffect } from 'react'
import { Button } from './ui/button'
import { useRouter } from 'next/router'
export interface SideBarItem {
id: string
@@ -13,24 +13,71 @@ export interface SideBarProps {
}
export const SideBar: React.FC<SideBarProps> = ({ items }) => {
const router = useRouter();
const defaultItems =
[
{ id: "clients", label: "客户端", eventHandler: () => { router.push("/clients") } },
{ id: "servers", label: "服务端", eventHandler: () => { router.push("/servers") } },
{ id: "clientedit", label: "编辑隧道", eventHandler: () => { router.push("/clientedit") } },
{ id: "serveredit", label: "编辑端点", eventHandler: () => { router.push("/serveredit") } },
const router = useRouter()
const defaultItems = [
{
id: 'clients',
label: '客户端',
eventHandler: () => {
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 (
<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) ?
"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>)}
{!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>)}
{items &&
items.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>
))}
{!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>
)
}

View File

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

View File

@@ -1,59 +1,43 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
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: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
default: 'bg-background text-foreground',
destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
))
Alert.displayName = "Alert"
Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />
),
)
AlertTitle.displayName = 'AlertTitle'
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
),
)
AlertDescription.displayName = 'AlertDescription'
export { Alert, AlertTitle, AlertDescription }

View File

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

View File

@@ -1,37 +1,33 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
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: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
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",
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
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: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
},
}
)
export interface ButtonProps
@@ -42,16 +38,10 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
const Comp = asChild ? Slot : 'button'
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
}
)
Button.displayName = "Button"
Button.displayName = 'Button'
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<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
const Card = React.forwardRef<HTMLDivElement, 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<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
)
CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
),
)
CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
),
)
CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
)
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
)
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { Cross2Icon } from '@radix-ui/react-icons'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root
@@ -19,8 +19,8 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
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",
className
'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,
)}
{...props}
/>
@@ -36,8 +36,8 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
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",
className
'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,
)}
{...props}
>
@@ -51,33 +51,15 @@ const DialogContent = React.forwardRef<
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
const DialogHeader = ({ className, ...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 = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
const DialogFooter = ({ className, ...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<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -85,10 +67,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
))
@@ -98,11 +77,7 @@ const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
<DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName

View File

@@ -1,12 +1,8 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
@@ -29,9 +25,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
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",
inset && "pl-8",
className
'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',
className,
)}
{...props}
>
@@ -39,8 +35,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -49,14 +44,13 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
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",
className
'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,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -67,9 +61,9 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"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",
className
'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',
className,
)}
{...props}
/>
@@ -86,9 +80,9 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
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",
inset && "pl-8",
className
'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',
className,
)}
{...props}
/>
@@ -102,8 +96,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
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",
className
'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,
)}
checked={checked}
{...props}
@@ -116,8 +110,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -126,8 +119,8 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
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",
className
'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,
)}
{...props}
>
@@ -149,11 +142,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...props}
/>
))
@@ -163,26 +152,14 @@ const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
<DropdownMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
const DropdownMenuShortcut = ({ className, ...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 {
DropdownMenu,

View File

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

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import * as React from 'react'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const HoverCard = HoverCardPrimitive.Root
@@ -10,14 +10,14 @@ const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
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",
className
'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,
)}
{...props}
/>

View File

@@ -1,5 +1,14 @@
export const YesIcon = ({ className }: { className?: string }) => {
return <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>
return (
<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>
)
}

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
extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
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",
className
'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,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
})
Input.displayName = 'Input'
export { Input }

View File

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

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const Popover = PopoverPrimitive.Root
@@ -12,15 +12,15 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<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.Content
ref={ref}
align={align}
sideOffset={sideOffset}
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",
className
'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,
)}
{...props}
/>

View File

@@ -1,13 +1,8 @@
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import * as React from 'react'
import { CaretSortIcon, 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
@@ -22,8 +17,8 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
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",
className
'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,
)}
{...props}
>
@@ -41,10 +36,7 @@ const SelectScrollUpButton = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUpIcon />
@@ -58,30 +50,26 @@ const SelectScrollDownButton = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
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",
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",
className
'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' &&
'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,
)}
position={position}
{...props}
@@ -89,9 +77,9 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
@@ -106,11 +94,7 @@ const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
<SelectPrimitive.Label ref={ref} className={cn('px-2 py-1.5 text-sm font-semibold', className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
@@ -121,8 +105,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
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",
className
'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,
)}
{...props}
>
@@ -140,11 +124,7 @@ const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
<SelectPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -9,15 +9,15 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
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",
className
'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,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
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>

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<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
))
Table.displayName = "Table"
),
)
Table.displayName = 'Table'
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />,
)
TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
),
)
TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
),
)
TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
{...props}
/>
))
TableRow.displayName = "TableRow"
),
)
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
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]",
className
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
),
)
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
),
)
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
),
)
TableCaption.displayName = 'TableCaption'
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
export { 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
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
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",
className
'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,
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
})
Textarea.displayName = 'Textarea'
export { Textarea }

View File

@@ -1,9 +1,9 @@
import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { Cross2Icon } from '@radix-ui/react-icons'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
@@ -14,8 +14,8 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
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]",
className
'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,
)}
{...props}
/>
@@ -23,33 +23,25 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
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: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
default: 'border bg-background text-foreground',
destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
})
Toast.displayName = ToastPrimitives.Root.displayName
@@ -60,8 +52,8 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
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",
className
'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,
)}
{...props}
/>
@@ -75,8 +67,8 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
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",
className
'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,
)}
toast-close=""
{...props}
@@ -90,11 +82,7 @@ const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
<ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold [&+div]:text-xs', className)} {...props} />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
@@ -102,11 +90,7 @@ const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
<ToastPrimitives.Description ref={ref} className={cn('text-sm opacity-90', className)} {...props} />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName

View File

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

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
@@ -17,8 +17,8 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
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",
className
'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,
)}
{...props}
/>

View File

@@ -1,10 +1,7 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from 'react'
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
@@ -17,10 +14,10 @@ type ToasterToast = ToastProps & {
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const
let count = 0
@@ -34,20 +31,20 @@ type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
@@ -64,7 +61,7 @@ const addToRemoveQueue = (toastId: string) => {
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
type: 'REMOVE_TOAST',
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
@@ -74,21 +71,19 @@ const addToRemoveQueue = (toastId: string) => {
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
case 'ADD_TOAST':
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
case 'UPDATE_TOAST':
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
}
case "DISMISS_TOAST": {
case 'DISMISS_TOAST': {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
@@ -109,11 +104,11 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
: t
: t,
),
}
}
case "REMOVE_TOAST":
case 'REMOVE_TOAST':
if (action.toastId === undefined) {
return {
...state,
@@ -138,20 +133,20 @@ function dispatch(action: Action) {
})
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, 'id'>
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
type: 'UPDATE_TOAST',
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
dispatch({
type: "ADD_TOAST",
type: 'ADD_TOAST',
toast: {
...props,
id,
@@ -185,7 +180,7 @@ function useToast() {
return {
...state,
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 { Client, Server } from "./pb/common"
import { GetPlatformInfoResponse } from "./pb/api_user"
import * as z from 'zod'
import { Client, Server } from './pb/common'
import { GetPlatformInfoResponse } from './pb/api_user'
export const API_PATH = '/api/v1'
export const SET_TOKEN_HEADER = 'x-set-authorization'
export const X_CLIENT_REQUEST_ID = 'x-client-request-id'
export const LOCAL_STORAGE_TOKEN_KEY = 'token'
export const ZodPortSchema = z.coerce.number().min(1, {
message: "端口号不能小于 1",
}).max(65535, { message: "端口号不能大于 65535" })
export const ZodIPSchema = z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
{ message: "请输入正确的IP地址" })
export const ZodStringSchema = z.string().min(1,
{ message: "不能为空" })
export const ZodEmailSchema = z.string()
.min(1, { message: "不能为空" })
.email("是不是输错了邮箱地址呢?")
export const ZodPortSchema = z.coerce
.number()
.min(1, {
message: '端口号不能小于 1',
})
.max(65535, { message: '端口号不能大于 65535' })
export const ZodIPSchema = z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, { message: '请输入正确的IP地址' })
export const ZodStringSchema = z.string().min(1, { message: '不能为空' })
export const ZodEmailSchema = z.string().min(1, { message: '不能为空' }).email('是不是输错了邮箱地址呢?')
// .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) => {
return `${fileName || 'frp-panel'} ${type
} -s ${item.secret
} -i ${item.id
} -a ${info.globalSecret
} -r ${info.masterRpcHost
} -c ${info.masterRpcPort
} -p ${info.masterApiPort
} -e ${info.masterApiScheme
}`
export const ExecCommandStr = <T extends Client | Server>(
type: string,
item: T,
info: GetPlatformInfoResponse,
fileName?: string,
) => {
return `${fileName || 'frp-panel'} ${type} -s ${item.secret} -i ${item.id} -a ${info.globalSecret} -r ${
info.masterRpcHost
} -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'
Move-Item .\\frp-panel.exe C:\\Tools\\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`
}
export const LinuxInstallCommand = <T extends Client | Server>(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, " ")}`
export const LinuxInstallCommand = <T extends Client | Server>(
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 { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))

View File

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

View File

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

View File

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

View File

@@ -1,53 +1,58 @@
import { Inter } from 'next/font/google'
import { Providers } from '@/components/providers';
import { TbBuildingTunnel } from "react-icons/tb";
import { RootLayout } from '@/components/layout';
import { LoginComponent } from '@/components/login';
import { Header } from '@/components/header';
import { useRouter } from 'next/router';
import { Toaster } from '@/components/ui/toaster';
import { Providers } from '@/components/providers'
import { TbBuildingTunnel } from 'react-icons/tb'
import { RootLayout } from '@/components/layout'
import { LoginComponent } from '@/components/login'
import { Header } from '@/components/header'
import { useRouter } from 'next/router'
import { Toaster } from '@/components/ui/toaster'
const inter = Inter({ subsets: ['latin'] })
export default function Login() {
const router = useRouter()
return (
<main
className={`${inter.className}`}
>
<main className={`${inter.className}`}>
<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('/')}
>
<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" />
FRP Panel
</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='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="relative flex items-center text-lg font-medium"
onClick={() => router.push('/')}
>
<div className='flex rounded hover:bg-zinc-800 p-2'>
<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="absolute inset-0 bg-zinc-900"></div>
<div className="relative flex items-center text-lg font-medium" onClick={() => router.push('/')}>
<div className="flex rounded hover:bg-zinc-800 p-2">
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel
</div>
</div>
<div className="relative z-20 mt-auto">
<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><footer className="text-sm">navigate to: <a href='https://github.com/VaalaCat/frp-panel'>VaalaCat/frp-panel</a></footer></blockquote></div>
<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>
<footer className="text-sm">
navigate to: <a href="https://github.com/VaalaCat/frp-panel">VaalaCat/frp-panel</a>
</footer>
</blockquote>
</div>
<div className='lg:p-8 justify-center w-[300px]'>
<div className='flex flex-col justify-center space-y-6 w-[300px]'>
</div>
<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">
<h1 className="text-2xl font-semibold tracking-tight"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className='w-full justify-center'>
<div className='w-[300px]'>
<div className="w-full justify-center">
<div className="w-[300px]">
<LoginComponent />
</div>
</div>

View File

@@ -1,51 +1,56 @@
import { Inter } from 'next/font/google'
import { Providers } from '@/components/providers';
import { TbBuildingTunnel } from "react-icons/tb";
import { RegisterComponent } from '@/components/register';
import { useRouter } from 'next/router';
import { Toaster } from '@/components/ui/toaster';
import { Providers } from '@/components/providers'
import { TbBuildingTunnel } from 'react-icons/tb'
import { RegisterComponent } from '@/components/register'
import { useRouter } from 'next/router'
import { Toaster } from '@/components/ui/toaster'
const inter = Inter({ subsets: ['latin'] })
export default function Login() {
const router = useRouter()
return (
<main
className={`${inter.className}`}
>
<main className={`${inter.className}`}>
<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('/')}
>
<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" />
FRP Panel
</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='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="relative flex items-center text-lg font-medium"
onClick={() => router.push('/')}
>
<div className='flex rounded hover:bg-zinc-800 p-2'>
<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="absolute inset-0 bg-zinc-900"></div>
<div className="relative flex items-center text-lg font-medium" onClick={() => router.push('/')}>
<div className="flex rounded hover:bg-zinc-800 p-2">
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel
</div>
</div>
<div className="relative z-20 mt-auto">
<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><footer className="text-sm">navigate to: <a href='https://github.com/VaalaCat/frp-panel'>VaalaCat/frp-panel</a></footer></blockquote></div>
<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>
<footer className="text-sm">
navigate to: <a href="https://github.com/VaalaCat/frp-panel">VaalaCat/frp-panel</a>
</footer>
</blockquote>
</div>
<div className='lg:p-8 justify-center w-[300px]'>
<div className='flex flex-col justify-center space-y-6 w-[300px]'>
</div>
<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">
<h1 className="text-2xl font-semibold tracking-tight"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className='w-full justify-center'>
<div className='w-[300px]'>
<div className="w-full justify-center">
<div className="w-[300px]">
<RegisterComponent />
</div>
</div>

View File

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

View File

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

View File

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

2656
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 { atom } from "nanostores"
import { TypedProxyConfig } from '@/types/proxy'
import { atom } from 'nanostores'
export const $clientProxyConfigs = atom<TypedProxyConfig[]>([])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,106 +1,110 @@
import { BandwidthQuantity, HeaderOperations } from "./common";
import { TypedClientPluginOptions } from "./plugin";
import { BandwidthQuantity, HeaderOperations } from './common'
import { TypedClientPluginOptions } from './plugin'
export interface ProxyTransport {
useEncryption?: boolean;
useCompression?: boolean;
bandwidthLimit?: BandwidthQuantity;
bandwidthLimitMode?: string;
proxyProtocolVersion?: string;
useEncryption?: boolean
useCompression?: boolean
bandwidthLimit?: BandwidthQuantity
bandwidthLimitMode?: string
proxyProtocolVersion?: string
}
export interface LoadBalancerConfig {
group: string;
groupKey?: string;
group: string
groupKey?: string
}
export interface ProxyBackend {
localIP?: string;
localPort?: number;
plugin?: TypedClientPluginOptions;
localIP?: string
localPort?: number
plugin?: TypedClientPluginOptions
}
export interface HealthCheckConfig {
type: string;
timeoutSeconds?: number;
maxFailed?: number;
intervalSeconds: number;
path?: string;
type: string
timeoutSeconds?: number
maxFailed?: number
intervalSeconds: number
path?: string
}
export interface DomainConfig {
customDomains?: string[];
subdomain?: string;
customDomains?: string[]
subdomain?: string
}
export interface ProxyBaseConfig {
name: string;
type: string;
transport?: ProxyTransport;
metadatas?: { [key: string]: string };
loadBalancer?: LoadBalancerConfig;
healthCheck?: HealthCheckConfig;
localIP?: string;
localPort?: number;
plugin?: TypedClientPluginOptions;
name: string
type: string
transport?: ProxyTransport
metadatas?: { [key: string]: string }
loadBalancer?: LoadBalancerConfig
healthCheck?: HealthCheckConfig
localIP?: string
localPort?: number
plugin?: TypedClientPluginOptions
}
export type TypedProxyConfig = TCPProxyConfig |
UDPProxyConfig | HTTPProxyConfig |
HTTPSProxyConfig | TCPMuxProxyConfig |
STCPProxyConfig | XTCPProxyConfig |
SUDPProxyConfig;
export type TypedProxyConfig =
| TCPProxyConfig
| UDPProxyConfig
| HTTPProxyConfig
| 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 {
type: "tcp"
remotePort?: number;
type: 'tcp'
remotePort?: number
}
export interface UDPProxyConfig extends ProxyBaseConfig {
type: "udp"
remotePort?: number;
type: 'udp'
remotePort?: number
}
export interface HTTPProxyConfig extends ProxyBaseConfig, DomainConfig {
type: "http"
locations?: string[];
httpUser?: string;
httpPassword?: string;
hostHeaderRewrite?: string;
requestHeaders?: HeaderOperations;
routeByHTTPUser?: string;
type: 'http'
locations?: string[]
httpUser?: string
httpPassword?: string
hostHeaderRewrite?: string
requestHeaders?: HeaderOperations
routeByHTTPUser?: string
}
export interface HTTPSProxyConfig extends ProxyBaseConfig, DomainConfig {
type: "https"
type: 'https'
}
export type TCPMultiplexerType = "httpconnect";
export type TCPMultiplexerType = 'httpconnect'
export interface TCPMuxProxyConfig extends ProxyBaseConfig, DomainConfig {
type: "tcpmux"
httpUser?: string;
httpPassword?: string;
routeByHTTPUser?: string;
multiplexer?: string;
type: 'tcpmux'
httpUser?: string
httpPassword?: string
routeByHTTPUser?: string
multiplexer?: string
}
export interface STCPProxyConfig extends ProxyBaseConfig {
type: "stcp"
secretKey?: string;
allowUsers?: string[];
type: 'stcp'
secretKey?: string
allowUsers?: string[]
}
export interface XTCPProxyConfig extends ProxyBaseConfig {
type: "xtcp"
secretKey?: string;
allowUsers?: string[];
type: 'xtcp'
secretKey?: string
allowUsers?: string[]
}
export interface SUDPProxyConfig extends ProxyBaseConfig {
type: "sudp"
secretKey?: string;
allowUsers?: string[];
type: 'sudp'
secretKey?: 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 {
auth?: AuthServerConfig;
bindAddr?: string;
bindPort?: number;
kcpBindPort?: number;
quicBindPort?: number;
proxyBindAddr?: string;
vhostHTTPPort?: number;
vhostHTTPTimeout?: number;
vhostHTTPSPort?: number;
tcpmuxHTTPConnectPort?: number;
tcpmuxPassthrough?: boolean;
subDomainHost?: string;
custom404Page?: string;
sshTunnelGateway?: SSHTunnelGateway;
webServer?: WebServerConfig;
enablePrometheus?: boolean;
log?: LogConfig;
transport?: ServerTransportConfig;
detailedErrorsToClient?: boolean;
maxPortsPerClient?: number;
userConnTimeout?: number;
udpPacketSize?: number;
natholeAnalysisDataReserveHours?: number;
allowPorts?: PortsRange[];
httpPlugins?: HTTPPluginOptions[];
auth?: AuthServerConfig
bindAddr?: string
bindPort?: number
kcpBindPort?: number
quicBindPort?: number
proxyBindAddr?: string
vhostHTTPPort?: number
vhostHTTPTimeout?: number
vhostHTTPSPort?: number
tcpmuxHTTPConnectPort?: number
tcpmuxPassthrough?: boolean
subDomainHost?: string
custom404Page?: string
sshTunnelGateway?: SSHTunnelGateway
webServer?: WebServerConfig
enablePrometheus?: boolean
log?: LogConfig
transport?: ServerTransportConfig
detailedErrorsToClient?: boolean
maxPortsPerClient?: number
userConnTimeout?: number
udpPacketSize?: number
natholeAnalysisDataReserveHours?: number
allowPorts?: PortsRange[]
httpPlugins?: HTTPPluginOptions[]
}
export interface AuthServerConfig {
method?: AuthMethod;
additionalScopes?: AuthScope[];
token?: string;
oidc?: AuthOIDCServerConfig;
method?: AuthMethod
additionalScopes?: AuthScope[]
token?: string
oidc?: AuthOIDCServerConfig
}
export interface AuthOIDCServerConfig {
issuer?: string;
audience?: string;
skipExpiryCheck?: boolean;
skipIssuerCheck?: boolean;
issuer?: string
audience?: string
skipExpiryCheck?: boolean
skipIssuerCheck?: boolean
}
export interface ServerTransportConfig {
tcpMux?: boolean;
tcpMuxKeepaliveInterval?: number;
tcpKeepalive?: number;
maxPoolCount?: number;
heartbeatTimeout?: number;
quic?: QUICOptions;
tls?: TLSServerConfig;
tcpMux?: boolean
tcpMuxKeepaliveInterval?: number
tcpKeepalive?: number
maxPoolCount?: number
heartbeatTimeout?: number
quic?: QUICOptions
tls?: TLSServerConfig
}
export interface TLSServerConfig {
force?: boolean;
force?: boolean
}
export interface SSHTunnelGateway {
bindPort?: number;
privateKeyFile?: string;
autoGenPrivateKeyPath?: string;
authorizedKeysFile?: string;
bindPort?: number
privateKeyFile?: string
autoGenPrivateKeyPath?: string
authorizedKeysFile?: string
}

View File

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