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,23 +1,20 @@
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'
export const login = async (req: LoginRequest) => {
const res = await http.post(API_PATH + '/auth/login', LoginRequest.toJson(req))
return LoginResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/auth/login', LoginRequest.toJson(req))
return LoginResponse.fromJson((res.data as BaseResponse).body)
}
export const register = async (req: RegisterRequest) => {
const res = await http.post(API_PATH + '/auth/register', RegisterRequest.toJson(req))
return RegisterResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/auth/register', RegisterRequest.toJson(req))
return RegisterResponse.fromJson((res.data as BaseResponse).body)
}
export const logout = async () => {
const res = await http.get(API_PATH + '/auth/logout')
return CommonResponse.fromJson((res.data as BaseResponse).body)
}
const res = await http.get(API_PATH + '/auth/logout')
return CommonResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -1,30 +1,34 @@
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))
return GetClientResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/client/get', GetClientRequest.toJson(req))
return GetClientResponse.fromJson((res.data as BaseResponse).body)
}
export const listClient = async (req: ListClientsRequest) => {
const res = await http.post(API_PATH + '/client/list', ListClientsRequest.toJson(req))
return ListClientsResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/client/list', ListClientsRequest.toJson(req))
return ListClientsResponse.fromJson((res.data as BaseResponse).body)
}
export const deleteClient = async (req: DeleteClientRequest) => {
const res = await http.post(API_PATH + '/client/delete', DeleteClientRequest.toJson(req))
return DeleteClientResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/client/delete', DeleteClientRequest.toJson(req))
return DeleteClientResponse.fromJson((res.data as BaseResponse).body)
}
export const initClient = async (req: InitClientRequest) => {
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)
}
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,35 +1,25 @@
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))
return UpdateFRPSResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/frps/update', UpdateFRPSRequest.toJson(req))
return UpdateFRPSResponse.fromJson((res.data as BaseResponse).body)
}
export const removeFRPS = async (req: RemoveFRPSRequest) => {
const res = await http.post(API_PATH + '/frps/remove', RemoveFRPSRequest.toJson(req))
return RemoveFRPSResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/frps/remove', RemoveFRPSRequest.toJson(req))
return RemoveFRPSResponse.fromJson((res.data as BaseResponse).body)
}
export const updateFRPC = async (req: UpdateFRPCRequest) => {
const res = await http.post(API_PATH + '/frpc/update', UpdateFRPCRequest.toJson(req))
return UpdateFRPCResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/frpc/update', UpdateFRPCRequest.toJson(req))
return UpdateFRPCResponse.fromJson((res.data as BaseResponse).body)
}
export const removeFRPC = async (req: RemoveFRPCRequest) => {
const res = await http.post(API_PATH + '/frpc/remove', RemoveFRPCRequest.toJson(req))
return RemoveFRPCResponse.fromJson((res.data as BaseResponse).body)
}
const res = await http.post(API_PATH + '/frpc/remove', RemoveFRPCRequest.toJson(req))
return RemoveFRPCResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -1,29 +1,29 @@
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({})
instance.interceptors.request.use((request) => {
let token = 'Bearer ' + localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY)
if (token) {
request.headers.Authorization = token
$token.set(token)
}
request.headers[X_CLIENT_REQUEST_ID] = uuidv4();
return request
let token = 'Bearer ' + localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY)
if (token) {
request.headers.Authorization = token
$token.set(token)
}
request.headers[X_CLIENT_REQUEST_ID] = uuidv4()
return request
})
instance.interceptors.response.use((response) => {
if (response.headers?.[SET_TOKEN_HEADER]) {
localStorage.setItem(LOCAL_STORAGE_TOKEN_KEY, response.headers[SET_TOKEN_HEADER])
$token.set(response.headers[SET_TOKEN_HEADER])
}
if (response.data.code != 200) {
throw response.data.msg
}
return response
if (response.headers?.[SET_TOKEN_HEADER]) {
localStorage.setItem(LOCAL_STORAGE_TOKEN_KEY, response.headers[SET_TOKEN_HEADER])
$token.set(response.headers[SET_TOKEN_HEADER])
}
if (response.data.code != 200) {
throw response.data.msg
}
return response
})
export default instance

View File

@@ -1,20 +1,15 @@
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 () => {
const res = await http.get(API_PATH + '/platform/baseinfo')
return GetPlatformInfoResponse.fromJson((res.data as BaseResponse).body)
const res = await http.get(API_PATH + '/platform/baseinfo')
return GetPlatformInfoResponse.fromJson((res.data as BaseResponse).body)
}
export const getClientsStatus = async (req: GetClientsStatusRequest) => {
const res = await http.post(API_PATH + '/platform/clientsstatus', GetClientsStatusRequest.toJson(req))
return GetClientsStatusResponse.fromJson((res.data as BaseResponse).body)
}
const res = await http.post(API_PATH + '/platform/clientsstatus', GetClientsStatusRequest.toJson(req))
return GetClientsStatusResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -1,31 +1,33 @@
import http from '@/api/http'
import { API_PATH } from "@/lib/consts";
import { API_PATH } from '@/lib/consts'
import {
DeleteServerRequest,
DeleteServerResponse,
GetServerRequest, GetServerResponse,
InitServerRequest,
InitServerResponse,
ListServersRequest, ListServersResponse
} from "@/lib/pb/api_server";
import { BaseResponse } from "@/types/api";
DeleteServerRequest,
DeleteServerResponse,
GetServerRequest,
GetServerResponse,
InitServerRequest,
InitServerResponse,
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))
return GetServerResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/server/get', GetServerRequest.toJson(req))
return GetServerResponse.fromJson((res.data as BaseResponse).body)
}
export const listServer = async (req: ListServersRequest) => {
const res = await http.post(API_PATH + '/server/list', ListServersRequest.toJson(req))
return ListServersResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/server/list', ListServersRequest.toJson(req))
return ListServersResponse.fromJson((res.data as BaseResponse).body)
}
export const deleteServer = async (req: DeleteServerRequest) => {
const res = await http.post(API_PATH + '/server/delete', DeleteServerRequest.toJson(req))
return DeleteServerResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/server/delete', DeleteServerRequest.toJson(req))
return DeleteServerResponse.fromJson((res.data as BaseResponse).body)
}
export const initServer = async (req: InitServerRequest) => {
const res = await http.post(API_PATH + '/server/init', InitServerRequest.toJson(req))
return InitServerResponse.fromJson((res.data as BaseResponse).body)
}
const res = await http.post(API_PATH + '/server/init', InitServerRequest.toJson(req))
return InitServerResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -1,19 +1,21 @@
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'
export const getUserInfo = async (req: GetUserInfoRequest) => {
const res = await http.post(API_PATH + '/user/get', GetUserInfoRequest.toJson(req))
$userInfo.set(GetUserInfoResponse.fromJson((res.data as BaseResponse).body).userInfo)
return GetUserInfoResponse.fromJson((res.data as BaseResponse).body)
const res = await http.post(API_PATH + '/user/get', GetUserInfoRequest.toJson(req))
$userInfo.set(GetUserInfoResponse.fromJson((res.data as BaseResponse).body).userInfo)
return GetUserInfoResponse.fromJson((res.data as BaseResponse).body)
}
export const updateUserInfo = async (req: UpdateUserInfoRequest) => {
const res = await http.post(API_PATH + '/user/update', UpdateUserInfoRequest.toJson(req))
return UpdateUserInfoResponse.fromJson((res.data as BaseResponse).body)
}
const res = await http.post(API_PATH + '/user/update', UpdateUserInfoRequest.toJson(req))
return UpdateUserInfoResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -14,4 +14,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
}
}

View File

@@ -1,124 +1,205 @@
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 >
<Label>username</Label>
<Input value={username} onChange={e => setUsername(e.target.value)} />
</div>
<div >
<Label>password</Label>
<Input value={password} onChange={e => setPassword(e.target.value)} />
</div>
<div >
<Label>email</Label>
<Input value={email} onChange={e => setEmail(e.target.value)} />
</div>
<div >
<Label>clientID</Label>
<Input value={clientID} onChange={e => setClientID(e.target.value)} />
</div>
<div >
<Label>serverID</Label>
<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>
<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>
<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({
serverId: serverID,
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>
</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>
<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({
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>
</div>
</div >
)
}
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>
<Label>username</Label>
<Input value={username} onChange={(e) => setUsername(e.target.value)} />
</div>
<div>
<Label>password</Label>
<Input value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<div>
<Label>email</Label>
<Input value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div>
<Label>clientID</Label>
<Input value={clientID} onChange={(e) => setClientID(e.target.value)} />
</div>
<div>
<Label>serverID</Label>
<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>
<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>
<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({
serverId: serverID,
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>
</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>
<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({
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>
</div>
</div>
)
}

View File

@@ -1,66 +1,68 @@
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,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
export const CreateClientDialog = () => {
const [clientID, setClientID] = useState<string | undefined>()
const newClient = useMutation({
mutationFn: initClient,
})
const dataQuery = useQuery({
queryKey: ["listClient", { pageIndex: 0, pageSize: 10 }],
queryFn: async () => {
return await listClient({ page: 1, pageSize: 10 })
}
})
const { toast } = useToast()
const [clientID, setClientID] = useState<string | undefined>()
const newClient = useMutation({
mutationFn: initClient,
})
const dataQuery = useQuery({
queryKey: ['listClient', { pageIndex: 0, pageSize: 10 }],
queryFn: async () => {
return await listClient({ page: 1, pageSize: 10 })
},
})
const { toast } = useToast()
const handleNewClient = async () => {
toast({ title: "已提交创建请求" })
try {
let resp = await newClient.mutateAsync({ clientId: clientID })
if (resp.status?.code !== RespCode.SUCCESS) {
toast({ title: "创建客户端失败" })
return
}
toast({ title: "创建客户端成功" })
dataQuery.refetch()
} catch (error) {
toast({ title: "创建客户端失败" })
}
}
const handleNewClient = async () => {
toast({ title: '已提交创建请求' })
try {
let resp = await newClient.mutateAsync({ clientId: clientID })
if (resp.status?.code !== RespCode.SUCCESS) {
toast({ title: '创建客户端失败' })
return
}
toast({ title: '创建客户端成功' })
dataQuery.refetch()
} catch (error) {
toast({ title: '创建客户端失败' })
}
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size={"sm"}></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>ID必须唯一</DialogDescription>
</DialogHeader>
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size={'sm'}>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>ID必须唯一</DialogDescription>
</DialogHeader>
<Label>ID</Label>
<Input className="mt-2" value={clientID} onChange={(e) => setClientID(e.target.value)} />
<DialogFooter>
<Button onClick={handleNewClient}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
<Label>ID</Label>
<Input className="mt-2" value={clientID} onChange={(e) => setClientID(e.target.value)} />
<DialogFooter>
<Button onClick={handleNewClient}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,252 +1,277 @@
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,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from "@/components/ui/button"
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
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"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
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'
export type ClientTableSchema = {
id: string,
status: "invalid" | "valid"
secret: string
info?: string
config?: string
id: string
status: 'invalid' | 'valid'
secret: string
info?: string
config?: string
}
export const columns: ColumnDef<ClientTableSchema>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
return < ClientID client={row.original} />
}
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => {
return <ClientID client={row.original} />
},
{
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`}>{
{
valid: "已配置",
invalid: "未配置",
}[client.status]
}</div>
}
},
{
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`}>
{
{
valid: '已配置',
invalid: '未配置',
}[client.status]
}
</div>
)
},
{
accessorKey: "info",
header: "运行信息",
cell: ({ row }) => {
const client = row.original
return <ClientInfo client={client} />
}
},
{
accessorKey: 'info',
header: '运行信息',
cell: ({ row }) => {
const client = row.original
return <ClientInfo client={client} />
},
{
accessorKey: "secret",
header: "连接密钥",
cell: ({ row }) => {
const client = row.original
return <ClientSecret client={client} />
}
},
{
accessorKey: 'secret',
header: '连接密钥',
cell: ({ row }) => {
const client = row.original
return <ClientSecret client={client} />
},
{
id: "action",
cell: ({ row, table }) => {
const client = row.original
return (<ClientActions client={client} table={table} />)
},
},
{
id: 'action',
cell: ({ row, table }) => {
const client = row.original
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>
<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)}
</div>
{/* <div>Windows</div>
const platformInfo = useStore($platformInfo)
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)}
</div>
{/* <div>Windows</div>
<div className="p-2 border rounded font-mono w-fit">
{platformInfo === undefined ? "获取平台信息失败" : WindowsInstallCommand("client", client, platformInfo)}
</div> */}
</PopoverContent>
</PopoverContent>
</Popover>
)
}
export const ClientInfo = ({ client }: { client: ClientTableSchema }) => {
const clientsInfo = useQuery({
queryKey: ["getClientsStatus", [client.id]],
queryFn: async () => {
return await getClientsStatus({
clientIds: [client.id],
clientType: ClientType.FRPC,
})
}
})
const clientsInfo = useQuery({
queryKey: ['getClientsStatus', [client.id]],
queryFn: async () => {
return await getClientsStatus({
clientIds: [client.id],
clientType: ClientType.FRPC,
})
},
})
const trans = (info: ClientStatus | undefined) => {
let statusText: "在线" | "离线" | "错误" | "未知" = "未知";
if (info === undefined) {
return statusText;
}
if (info.status === ClientStatus_Status.ONLINE) {
statusText = "在线";
} else if (info.status === ClientStatus_Status.OFFLINE) {
statusText = "离线";
} else if (info.status === ClientStatus_Status.ERROR) {
statusText = "错误";
} return statusText;
const trans = (info: ClientStatus | undefined) => {
let statusText: '在线' | '离线' | '错误' | '未知' = '未知'
if (info === undefined) {
return statusText
}
if (info.status === ClientStatus_Status.ONLINE) {
statusText = '在线'
} else if (info.status === ClientStatus_Status.OFFLINE) {
statusText = '离线'
} else if (info.status === ClientStatus_Status.ERROR) {
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}`}>
{`${clientsInfo.data?.clients[client.id].ping}ms, ${trans(clientsInfo.data?.clients[client.id])}`}
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 platformInfo = useStore($platformInfo)
const { toast } = useToast()
return <Popover >
<PopoverTrigger asChild>
<div
onMouseEnter={() => setShowSecrect(true)}
onMouseLeave={() => setShowSecrect(false)}
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo));
toast({ description: "复制成功", });
} else {
toast({ description: "获取平台信息失败", });
}
}}
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)}
</div>
</PopoverContent>
const [showSecrect, setShowSecrect] = useState<boolean>(false)
const fakeSecret = Array.from({ length: client.secret.length })
.map(() => '*')
.join('')
const platformInfo = useStore($platformInfo)
const { toast } = useToast()
return (
<Popover>
<PopoverTrigger asChild>
<div
onMouseEnter={() => setShowSecrect(true)}
onMouseLeave={() => setShowSecrect(false)}
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo))
toast({ description: '复制成功' })
} else {
toast({ description: '获取平台信息失败' })
}
}}
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)}
</div>
</PopoverContent>
</Popover>
)
}
export interface ClientItemProps {
client: ClientTableSchema
table: Table<ClientTableSchema>
client: ClientTableSchema
table: Table<ClientTableSchema>
}
export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
const { toast } = useToast()
const router = useRouter();
const platformInfo = useStore($platformInfo)
const fetchDataOptions = {
pageIndex: table.getState().pagination.pageIndex,
pageSize: table.getState().pagination.pageSize,
}
const { toast } = useToast()
const router = useRouter()
const platformInfo = useStore($platformInfo)
const fetchDataOptions = {
pageIndex: table.getState().pagination.pageIndex,
pageSize: table.getState().pagination.pageSize,
}
const dataQuery = useQuery({
queryKey: ["listClient", fetchDataOptions],
queryFn: async () => {
return await listClient({
page: fetchDataOptions.pageIndex + 1,
pageSize: fetchDataOptions.pageSize
})
}
})
const dataQuery = useQuery({
queryKey: ['listClient', fetchDataOptions],
queryFn: async () => {
return await listClient({
page: fetchDataOptions.pageIndex + 1,
pageSize: fetchDataOptions.pageSize,
})
},
})
const removeClient = useMutation({
mutationFn: deleteClient,
onSuccess: () => {
toast({ description: "删除成功" })
dataQuery.refetch()
},
onError: () => {
toast({ description: "删除失败" })
}
})
const removeClient = useMutation({
mutationFn: deleteClient,
onSuccess: () => {
toast({ description: '删除成功' })
dataQuery.refetch()
},
onError: () => {
toast({ description: '删除失败' })
},
})
return <Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only"></span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo));
toast({ description: "复制成功如果复制不成功请点击ID字段手动复制", });
} else {
toast({ description: "获取平台信息失败如果复制不成功请点击ID字段手动复制", });
}
}}
>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => {
router.push({ pathname: "/clientedit", query: { clientID: client.id } })
}}></DropdownMenuItem>
<DialogTrigger asChild>
<DropdownMenuItem className="text-destructive"></DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>?</DialogTitle>
<DialogDescription>
<p className="text-destructive">
?
</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>
</DialogClose>
</DialogFooter>
</DialogContent>
return (
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only"></span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo))
toast({ description: '复制成功如果复制不成功请点击ID字段手动复制' })
} else {
toast({ description: '获取平台信息失败如果复制不成功请点击ID字段手动复制' })
}
}}
>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
router.push({ pathname: '/clientedit', query: { clientID: client.id } })
}}
>
</DropdownMenuItem>
<DialogTrigger asChild>
<DropdownMenuItem className="text-destructive"></DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>?</DialogTitle>
<DialogDescription>
<p className="text-destructive">?</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>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,88 +1,90 @@
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,
getCoreRowModel,
ColumnFiltersState,
useReactTable,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
PaginationState,
} from "@tanstack/react-table"
getSortedRowModel,
getCoreRowModel,
ColumnFiltersState,
useReactTable,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
PaginationState,
} 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[]
Clients: Client[]
}
export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
const [sorting, setSorting] = React.useState<SortingState>([])
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))
const [sorting, setSorting] = React.useState<SortingState>([])
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,
)
const [{ pageIndex, pageSize }, setPagination] =
React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const fetchDataOptions = {
pageIndex,
pageSize,
}
const pagination = React.useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const fetchDataOptions = {
pageIndex,
pageSize,
}
const pagination = React.useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize],
)
const dataQuery = useQuery({
queryKey: ["listClient", fetchDataOptions],
queryFn: async () => {
return await listClient({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
}
})
const dataQuery = useQuery({
queryKey: ['listClient', fetchDataOptions],
queryFn: async () => {
return await listClient({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
},
})
const table = useReactTable({
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
} as ClientTableSchema
}) ?? data,
pageCount: Math.ceil((dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0),
columns: clientColumnsDef,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
state: {
sorting,
pagination,
columnFilters,
},
})
return <DataTable table={table} columns={clientColumnsDef} />
};
const table = useReactTable({
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,
} as ClientTableSchema
}) ?? data,
pageCount: Math.ceil(
(dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0,
),
columns: clientColumnsDef,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
state: {
sorting,
pagination,
columnFilters,
},
})
return <DataTable table={table} columns={clientColumnsDef} />
}

View File

@@ -1,71 +1,61 @@
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"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<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"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === "asc" ? (
<ArrowUpIcon className="ml-2 h-4 w-4" />
) : (
<CaretSortIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
return (
<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">
<span>{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === 'asc' ? (
<ArrowUpIcon className="ml-2 h-4 w-4" />
) : (
<CaretSortIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@@ -1,103 +1,81 @@
"use client"
'use client'
import {
ColumnDef,
flexRender,
getSortedRowModel,
getCoreRowModel,
ColumnFiltersState,
useReactTable,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
Table as TableType,
} from "@tanstack/react-table"
ColumnDef,
flexRender,
getSortedRowModel,
getCoreRowModel,
ColumnFiltersState,
useReactTable,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
Table as TableType,
} 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>[]
data?: TData[]
filterColumnName?: string
table: TableType<TData>
columns: ColumnDef<TData, TValue>[]
data?: TData[]
filterColumnName?: string
table: TableType<TData>
}
export function DataTable<TData, TValue>({
columns,
filterColumnName,
table,
}: DataTableProps<TData, TValue>) {
return (
<div>
{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)
}
className="max-w-sm"
/>
</div>}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<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>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="my-2">
<DataTablePagination table={table} />
</div>
</div>
)
export function DataTable<TData, TValue>({ columns, filterColumnName, table }: DataTableProps<TData, TValue>) {
return (
<div>
{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)}
className="max-w-sm"
/>
</div>
)}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<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>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="my-2">
<DataTablePagination table={table} />
</div>
</div>
)
}

View File

@@ -1,97 +1,83 @@
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>
table: Table<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">
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">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div> */}
<div className="flex items-center justify-between w-full space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
<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()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"></span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"></span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"></span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"></span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)
<div className="flex items-center justify-between w-full space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
<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()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"></span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"></span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"></span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"></span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)
}

View File

@@ -1,138 +1,139 @@
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
clientID?: string
serverID?: string
}
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>()
const searchParams = useSearchParams()
const paramClientID = searchParams.get('clientID')
const handleServerChange = (value: string) => {
setServerID(value)
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>()
const searchParams = useSearchParams()
const paramClientID = searchParams.get('clientID')
const handleServerChange = (value: string) => {
setServerID(value)
}
const handleClientChange = (value: string) => {
setClientID(value)
}
useEffect(() => {
if (defaultClientID) {
setClientID(defaultClientID)
}
const handleClientChange = (value: string) => {
setClientID(value)
if (defaultServerID) {
setServerID(defaultServerID)
}
}, [defaultClientID, defaultServerID])
useEffect(() => {
if (defaultClientID) {
setClientID(defaultClientID)
}
if (defaultServerID) {
setServerID(defaultServerID)
}
}, [defaultClientID, defaultServerID])
const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ['listServer'],
queryFn: () => {
return listServer({ page: 1, pageSize: 500 })
},
})
const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ["listServer"], queryFn: () => {
return listServer({ page: 1, pageSize: 500 })
}
});
const { data: clientList, refetch: refetchClients } = useQuery({
queryKey: ['listClient'],
queryFn: () => {
return listClient({ page: 1, pageSize: 500 })
},
})
const { data: clientList, refetch: refetchClients } = useQuery({
queryKey: ["listClient"], queryFn: () => {
return listClient({ page: 1, pageSize: 500 })
}
})
const { data: client, refetch: refetchClient } = useQuery({
queryKey: ['getClient', clientID],
queryFn: () => {
return getClient({ clientId: clientID })
},
})
const { data: client, refetch: refetchClient } = useQuery({
queryKey: ["getClient", clientID], queryFn: () => {
return getClient({ clientId: clientID })
}
})
useEffect(() => {
if (paramClientID) {
setClientID(paramClientID)
setServerID(clientList?.clients?.find((client) => client.id == paramClientID)?.serverId)
}
}, [paramClientID, clientList])
useEffect(() => {
if (paramClientID) {
setClientID(paramClientID)
setServerID(clientList?.clients?.find((client) => client.id == paramClientID)?.serverId)
}
}, [paramClientID, clientList])
return (
<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<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>
</div>
<Switch onCheckedChange={setAdvanceMode} />
</div>
<div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label>
<Select onValueChange={handleServerChange}
value={serverID}
onOpenChange={() => {
refetchServers()
refetchClients()
}}
>
<SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" />
</SelectTrigger>
<SelectContent>
{serverList?.servers.map(
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}
value={clientID}
onOpenChange={() => {
refetchServers()
refetchClients()
}}
>
<SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" />
</SelectTrigger>
<SelectContent>
{clientList?.clients.map(
client => client.id && <SelectItem key={client.id} value={client.id}>{client.id}
</SelectItem>)}
</SelectContent>
</Select>
</div>
{clientID && serverID && !advanceMode && <FRPCForm clientID={clientID} serverID={serverID} />}
{clientID && serverID && advanceMode && <FRPCEditor clientID={clientID} serverID={serverID} />}
</CardContent>
</Card>
)
}
return (
<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<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>
</div>
<Switch onCheckedChange={setAdvanceMode} />
</div>
<div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label>
<Select
onValueChange={handleServerChange}
value={serverID}
onOpenChange={() => {
refetchServers()
refetchClients()
}}
>
<SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" />
</SelectTrigger>
<SelectContent>
{serverList?.servers.map(
(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}
value={clientID}
onOpenChange={() => {
refetchServers()
refetchClients()
}}
>
<SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" />
</SelectTrigger>
<SelectContent>
{clientList?.clients.map(
(client) =>
client.id && (
<SelectItem key={client.id} value={client.id}>
{client.id}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
{clientID && serverID && !advanceMode && <FRPCForm clientID={clientID} serverID={serverID} />}
{clientID && serverID && advanceMode && <FRPCEditor clientID={clientID} serverID={serverID} />}
</CardContent>
</Card>
)
}

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: () => {
return getClient({ clientId: clientID })
}
});
const { toast } = useToast()
const { data: client, refetch: refetchClient } = useQuery({
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
})
if (res.status?.code !== RespCode.SUCCESS) {
toast({ title: "更新失败" })
return
}
toast({ title: "更新成功" })
} catch (error) {
toast({ title: "更新失败" })
}
}
const handleSubmit = async () => {
try {
let res = await updateFrpc.mutateAsync({
clientId: clientID,
config: Buffer.from(editorValue),
serverId: serverID,
})
if (res.status?.code !== RespCode.SUCCESS) {
toast({ title: '更新失败' })
return
}
toast({ title: '更新成功' })
} catch (error) {
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))
} catch (error) {
setConfigContent("{}")
setEditorValue("{}")
}
}, [client, refetchClient])
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,
),
)
} catch (error) {
setConfigContent('{}')
setEditorValue('{}')
}
}, [client, refetchClient])
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"
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 variant="outline" size="sm" onClick={async () => {
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"
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 variant="outline" size="sm" onClick={async () => {
await refetchClient()
setConfigContent(client?.client?.config == undefined ? "{}" : client?.client?.config)
}}>加载服务端配置</Button> */}
</div>
</div>)
}
</div>
</div>
)
}

View File

@@ -1,181 +1,186 @@
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
serverID: string
clientID: string
serverID: string
}
export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
const [proxyType, setProxyType] = useState<ProxyType>('http')
const [proxyName, setProxyName] = useState<string | undefined>()
const { toast } = useToast()
const handleTypeChange = (value: string) => {
setProxyType(value as ProxyType)
}
const [proxyType, setProxyType] = useState<ProxyType>('http')
const [proxyName, setProxyName] = useState<string | undefined>()
const { toast } = useToast()
const handleTypeChange = (value: string) => {
setProxyType(value as ProxyType)
}
const { data: client, refetch: refetchClient } = useQuery({
queryKey: ["getClient", clientID], queryFn: () => {
return getClient({ clientId: clientID })
}
});
const { data: client, refetch: refetchClient } = useQuery({
queryKey: ['getClient', clientID],
queryFn: () => {
return getClient({ clientId: clientID })
},
})
const clientProxyConfigs = useStore($clientProxyConfigs)
const clientProxyConfigs = useStore($clientProxyConfigs)
useEffect(() => {
if (!client || !client?.client || !client?.client?.config) return
const proxyConfs = (JSON.parse(client?.client?.config) as ClientConfig).proxies
if (proxyConfs) {
$clientProxyConfigs.set(proxyConfs)
}
}, [clientID, serverID, client])
useEffect(() => {
if (!client || !client?.client || !client?.client?.config) return
const proxyConfs = (JSON.parse(client?.client?.config) as ClientConfig).proxies
if (proxyConfs) {
$clientProxyConfigs.set(proxyConfs)
}
}, [clientID, serverID, client])
const handleAddProxy = () => {
console.log("add proxy", proxyName, proxyType)
if (!proxyName) return
if (!proxyType) return
if (clientProxyConfigs.findIndex(proxy => proxy.name === proxyName) !== -1) {
toast({ title: "创建隧道状态", description: "名称重复" })
return
}
const newProxy = {
name: proxyName,
type: proxyType,
} as TypedProxyConfig
$clientProxyConfigs.set([...clientProxyConfigs, newProxy])
}
const handleAddProxy = () => {
console.log('add proxy', proxyName, proxyType)
if (!proxyName) return
if (!proxyType) return
if (clientProxyConfigs.findIndex((proxy) => proxy.name === proxyName) !== -1) {
toast({ title: '创建隧道状态', description: '名称重复' })
return
}
const newProxy = {
name: proxyName,
type: proxyType,
} as TypedProxyConfig
$clientProxyConfigs.set([...clientProxyConfigs, newProxy])
}
const handleDeleteProxy = (proxyName: string) => {
const newProxies = clientProxyConfigs.filter(proxy => proxy.name !== proxyName)
$clientProxyConfigs.set(newProxies)
}
const handleDeleteProxy = (proxyName: string) => {
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({
proxies: clientProxyConfigs,
} as ClientConfig)), serverId: serverID, clientId: clientID
})
toast({ title: "更新隧道状态", description: res.status?.code === RespCode.SUCCESS ? "更新成功" : "更新失败" })
} catch (error) {
console.error(error)
toast({ title: "更新隧道状态", description: "更新失败" })
}
}
const handleUpdate = async () => {
try {
const res = await updateFrpc.mutateAsync({
config: Buffer.from(
JSON.stringify({
proxies: clientProxyConfigs,
} as ClientConfig),
),
serverId: serverID,
clientId: clientID,
})
toast({ title: '更新隧道状态', description: res.status?.code === RespCode.SUCCESS ? '更新成功' : '更新失败' })
} catch (error) {
console.error(error)
toast({ title: '更新隧道状态', description: '更新失败' })
}
}
return (
<>
<Popover>
<PopoverTrigger asChild>
<Button className="my-2"></Button>
</PopoverTrigger>
<PopoverContent >
<Label className="text-sm font-medium"></Label>
<Input onChange={(e) => { setProxyName(e.target.value) }} />
<Select onValueChange={handleTypeChange} defaultValue={proxyType}>
<Label className="text-sm font-medium"></Label>
<SelectTrigger className="my-2">
<SelectValue placeholder="类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="tcp">tcp</SelectItem>
<SelectItem value="udp">udp</SelectItem>
</SelectContent>
</Select>
<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}>
<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>
</AccordionHeader>
<AccordionTrigger>:{item.type}</AccordionTrigger>
<AccordionContent>
{
item.type === 'tcp' && serverID && clientID &&
<TCPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
}
{
item.type === 'udp' && serverID && clientID &&
< UDPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
}
{
item.type === 'http' && serverID && clientID &&
< HTTPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
}
</AccordionContent>
</AccordionItem>
</Accordion>
</div >
</CardContent>
</Card>
)
})
}
</div>
<Button className="mt-2" onClick={() => { handleUpdate() }}></Button>
</>
)
return (
<>
<Popover>
<PopoverTrigger asChild>
<Button className="my-2"></Button>
</PopoverTrigger>
<PopoverContent>
<Label className="text-sm font-medium"></Label>
<Input
onChange={(e) => {
setProxyName(e.target.value)
}}
/>
<Select onValueChange={handleTypeChange} defaultValue={proxyType}>
<Label className="text-sm font-medium"></Label>
<SelectTrigger className="my-2">
<SelectValue placeholder="类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="tcp">tcp</SelectItem>
<SelectItem value="udp">udp</SelectItem>
</SelectContent>
</Select>
<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}>
<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>
</AccordionHeader>
<AccordionTrigger>:{item.type}</AccordionTrigger>
<AccordionContent>
{item.type === 'tcp' && serverID && clientID && (
<TCPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
)}
{item.type === 'udp' && serverID && clientID && (
<UDPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
)}
{item.type === 'http' && serverID && clientID && (
<HTTPProxyForm
defaultProxyConfig={item}
proxyName={item.name}
serverID={serverID}
clientID={clientID}
/>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</CardContent>
</Card>
)
})}
</div>
<Button
className="mt-2"
onClick={() => {
handleUpdate()
}}
>
</Button>
</>
)
}

View File

@@ -1,96 +1,96 @@
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
serverID?: string
}
export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultServerID }: FRPSFormCardProps = {}) => {
const [advanceMode, setAdvanceMode] = useState<boolean>(false)
const [serverID, setServerID] = useState<string | undefined>()
const searchParams = useSearchParams()
const paramServerID = searchParams.get('serverID')
const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ["listServer"], queryFn: () => {
return listServer({ page: 1, pageSize: 100 })
}
});
const { data: server, refetch: refetchServer } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => {
return getServer({ serverId: serverID })
}
});
const [advanceMode, setAdvanceMode] = useState<boolean>(false)
const [serverID, setServerID] = useState<string | undefined>()
const searchParams = useSearchParams()
const paramServerID = searchParams.get('serverID')
const { data: serverList, refetch: refetchServers } = useQuery({
queryKey: ['listServer'],
queryFn: () => {
return listServer({ page: 1, pageSize: 100 })
},
})
const { data: server, refetch: refetchServer } = useQuery({
queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID })
},
})
useEffect(() => {
if (defaultServerID) {
setServerID(defaultServerID)
}
}, [defaultServerID])
useEffect(() => {
if (paramServerID) {
setServerID(paramServerID)
}
}, [paramServerID])
const handleServerChange = (value: string) => {
setServerID(value)
useEffect(() => {
if (defaultServerID) {
setServerID(defaultServerID)
}
}, [defaultServerID])
return (<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>Frps服务</CardDescription>
</CardHeader>
<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>
</div>
<Switch onCheckedChange={setAdvanceMode} />
</div>
<div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label>
<Select onValueChange={handleServerChange}
value={serverID}
onOpenChange={() => {
refetchServers()
refetchServer()
}}
>
<SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" />
</SelectTrigger>
<SelectContent >
{serverList?.servers.map(
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} />}
</CardContent>
<CardFooter>
</CardFooter>
</Card>)
}
useEffect(() => {
if (paramServerID) {
setServerID(paramServerID)
}
}, [paramServerID])
const handleServerChange = (value: string) => {
setServerID(value)
}
return (
<Card className="w-full">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>Frps服务</CardDescription>
</CardHeader>
<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>
</div>
<Switch onCheckedChange={setAdvanceMode} />
</div>
<div className="flex flex-col w-full pt-2">
<Label className="text-sm font-medium"></Label>
<Select
onValueChange={handleServerChange}
value={serverID}
onOpenChange={() => {
refetchServers()
refetchServer()
}}
>
<SelectTrigger className="my-2">
<SelectValue placeholder="节点名称" />
</SelectTrigger>
<SelectContent>
{serverList?.servers.map(
(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} />
)}
</CardContent>
<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: () => {
return getServer({ serverId: serverID })
}
});
const { toast } = useToast()
const { data: serverResp, refetch: refetchServer } = useQuery({
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
})
if (res.status?.code !== RespCode.SUCCESS) {
toast({ title: "更新失败" })
return
}
toast({ title: "更新成功" })
} catch (error) {
toast({ title: "更新失败" })
}
}
const handleSubmit = async () => {
try {
let res = await updateFrps.mutateAsync({
serverId: serverID,
config: Buffer.from(editorValue),
comment: serverComment,
})
if (res.status?.code !== RespCode.SUCCESS) {
toast({ title: '更新失败' })
return
}
toast({ title: '更新成功' })
} catch (error) {
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 || "")
} catch (error) {
setConfigContent("{}")
setEditorValue("{}")
setServerComment("")
}
}, [serverResp, refetchServer])
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 || '')
} catch (error) {
setConfigContent('{}')
setEditorValue('{}')
setServerComment('')
}
}, [serverResp, refetchServer])
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}
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}
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>
</div>
</div>)
}
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}
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}
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>
</div>
</div>
)
}

View File

@@ -1,159 +1,154 @@
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(),
bindPort: ZodPortSchema.default(7000),
proxyBindAddr: ZodIPSchema.optional(),
vhostHTTPPort: ZodPortSchema.optional(),
subDomainHost: ZodStringSchema.optional(),
});
export const ServerConfigZodSchema = ServerConfigSchema;
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 interface FRPSFormProps {
serverID: string
server: Server
serverID: string
server: Server
}
const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
const [_, setFrpsConfig] = useState<ServerConfig | undefined>()
const form = useForm<z.infer<typeof ServerConfigZodSchema>>({
resolver: zodResolver(ServerConfigZodSchema),
})
const { toast } = useToast()
const [_, setFrpsConfig] = useState<ServerConfig | undefined>()
const form = useForm<z.infer<typeof ServerConfigZodSchema>>({
resolver: zodResolver(ServerConfigZodSchema),
})
const { toast } = useToast()
const updateFrps = useMutation({ mutationFn: updateFRPS })
const updateFrps = useMutation({ mutationFn: updateFRPS })
useEffect(() => {
setFrpsConfig(undefined)
form.reset({})
}, [])
useEffect(() => {
setFrpsConfig(undefined)
form.reset({})
}, [])
useEffect(() => {
form.reset(JSON.parse(server?.config || "{}") as ServerConfig)
}, [server])
useEffect(() => {
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))
})
toast({
title: resp.status?.code === RespCode.SUCCESS ? "创建成功" : "创建失败",
description: resp.status?.message,
})
} catch (error) {
console.error(error)
toast({ title: "创建服务端状态", description: "创建失败" })
}
}
return (
<div className="flex flex-col w-full pt-2">
<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}
</p>
{serverID && <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="bindPort"
render={({ field }) => (
<FormItem>
<FormLabel>FRPs </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={7000}
/>
<FormField
control={form.control}
name="bindAddr"
render={({ field }) => (
<FormItem>
<FormLabel>FRPs </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue="0.0.0.0"
/>
<FormField
control={form.control}
name="proxyBindAddr"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="vhostHTTPPort"
render={({ field }) => (
<FormItem>
<FormLabel>HTTP </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subDomainHost"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit"></Button>
</form>
</Form>}
</div >
);
};
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),
),
})
toast({
title: resp.status?.code === RespCode.SUCCESS ? '创建成功' : '创建失败',
description: resp.status?.message,
})
} catch (error) {
console.error(error)
toast({ title: '创建服务端状态', description: '创建失败' })
}
}
return (
<div className="flex flex-col w-full pt-2">
<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}
</p>
{serverID && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="bindPort"
render={({ field }) => (
<FormItem>
<FormLabel>FRPs </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={7000}
/>
<FormField
control={form.control}
name="bindAddr"
render={({ field }) => (
<FormItem>
<FormLabel>FRPs </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue="0.0.0.0"
/>
<FormField
control={form.control}
name="proxyBindAddr"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="vhostHTTPPort"
render={({ field }) => (
<FormItem>
<FormLabel>HTTP </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subDomainHost"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit"></Button>
</form>
</Form>
)}
</div>
)
}
export default FRPSForm
export default FRPSForm

View File

@@ -1,65 +1,82 @@
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 userInfo = useStore($userInfo)
const router = useRouter()
const userInfo = useStore($userInfo)
const platformInfo = useQuery({
queryKey: ['platformInfo'],
queryFn: getPlatformInfo
})
const platformInfo = useQuery({
queryKey: ['platformInfo'],
queryFn: getPlatformInfo,
})
useEffect(() => {
$platformInfo.set(platformInfo.data)
}, [platformInfo])
useEffect(() => {
$platformInfo.set(platformInfo.data)
}, [platformInfo])
const userInfoQuery = useQuery({
queryKey: ["userInfo"],
queryFn: getUserInfo
})
const userInfoQuery = useQuery({
queryKey: ['userInfo'],
queryFn: getUserInfo,
})
useEffect(() => {
$userInfo.set(userInfoQuery.data?.userInfo)
}, [userInfoQuery])
useEffect(() => {
$userInfo.set(userInfoQuery.data?.userInfo)
}, [userInfoQuery])
const redirToHome = () => {
router.push("/")
}
const redirToHome = () => {
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 () => {
$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>
<Gravatar email={userInfo.email} />
</AvatarImage>
<AvatarFallback>{userInfo.userName}</AvatarFallback>
</Avatar>}
</div>
)
}
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 () => {
$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>
<Gravatar email={userInfo.email} />
</AvatarImage>
<AvatarFallback>{userInfo.userName}</AvatarFallback>
</Avatar>
)}
</div>
)
}

View File

@@ -1,21 +1,28 @@
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 }) => {
return (
<main className={`${inter.className}`}>
<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>
<Toaster />
</main >
)
}
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 className="flex">
{sidebar}
<div className="my-2 ml-0 mr-2 max-w-full w-full">{children}</div>
</div>
<Toaster />
</main>
)
}

View File

@@ -1,114 +1,93 @@
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
username: ZodStringSchema,
password: ZodStringSchema,
})
export const LoginComponent = () => {
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
})
const { toast } = useToast()
const router = useRouter()
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
})
const { toast } = useToast()
const router = useRouter()
const [loginAlert, setLoginAlert] = useState(false)
const [loginAlert, setLoginAlert] = useState(false)
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
toast({ title: "登录中,请稍候" })
try {
const res = await login({ ...values })
if (res.status?.code === RespCode.SUCCESS) {
toast({ title: "登录成功,正在跳转到首页" })
setTimeout(() => {
router.push('/');
}, 3000);
setLoginAlert(false)
} else {
toast({ title: "登录失败" })
setLoginAlert(true)
}
} catch (e) {
toast({ title: "登录失败" })
console.log("login error", e)
setLoginAlert(true)
}
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
toast({ title: '登录中,请稍候' })
try {
const res = await login({ ...values })
if (res.status?.code === RespCode.SUCCESS) {
toast({ title: '登录成功,正在跳转到首页' })
setTimeout(() => {
router.push('/')
}, 3000)
setLoginAlert(false)
} else {
toast({ title: '登录失败' })
setLoginAlert(true)
}
} catch (e) {
toast({ title: '登录失败' })
console.log('login error', e)
setLoginAlert(true)
}
}
return (
<div className="w-full flex flex-col gap-6">
<Form {...form}>
<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}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="password"
placeholder="密码"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{loginAlert && <Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
</AlertDescription>
</Alert>}
<Button type="submit"></Button>
</form>
</Form>
</div>
)
}
return (
<div className="w-full flex flex-col gap-6">
<Form {...form}>
<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} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="password" placeholder="密码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{loginAlert && (
<Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription></AlertDescription>
</Alert>
)}
<Button type="submit"></Button>
</form>
</Form>
</div>
)
}

View File

@@ -1,103 +1,91 @@
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
})
useEffect(() => {
$platformInfo.set(platformInfo.data)
}, [platformInfo])
return (
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>{platformInfo.data?.totalClientCount} </div>
<p className="text-xs text-muted-foreground">🫲</p>
</CardContent>
</Card>
</div>
)
}
const platformInfo = useQuery({
queryKey: ['platformInfo'],
queryFn: getPlatformInfo,
})
useEffect(() => {
$platformInfo.set(platformInfo.data)
}, [platformInfo])
return (
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<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>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{platformInfo.data?.totalClientCount} </div>
<p className="text-xs text-muted-foreground">🫲</p>
</CardContent>
</Card>
</div>
)
}

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>
</TooltipProvider>
)
return (
<TooltipProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</TooltipProvider>
)
}

View File

@@ -1,345 +1,348 @@
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"),
localPort: ZodPortSchema,
remotePort: ZodPortSchema,
localIP: ZodIPSchema.default('127.0.0.1'),
localPort: ZodPortSchema,
})
export const UDPConfigSchema = z.object({
remotePort: ZodPortSchema.optional(),
localIP: ZodIPSchema.default("127.0.0.1"),
localPort: ZodPortSchema,
remotePort: ZodPortSchema.optional(),
localIP: ZodIPSchema.default('127.0.0.1'),
localPort: ZodPortSchema,
})
export const HTTPConfigSchema = z.object({
localPort: ZodPortSchema,
localIP: ZodIPSchema.default("127.0.0.1"),
subDomain: ZodStringSchema,
localPort: ZodPortSchema,
localIP: ZodIPSchema.default('127.0.0.1'),
subDomain: ZodStringSchema,
})
export interface ProxyFormProps {
clientID: string
serverID: string
proxyName: string
defaultProxyConfig?: TypedProxyConfig
clientID: string
serverID: string
proxyName: string
defaultProxyConfig?: TypedProxyConfig
}
export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
const [_, setTCPConfig] = useState<TCPProxyConfig | undefined>()
const form = useForm<z.infer<typeof TCPConfigSchema>>({
resolver: zodResolver(TCPConfigSchema),
const [_, setTCPConfig] = useState<TCPProxyConfig | undefined>()
const form = useForm<z.infer<typeof TCPConfigSchema>>({
resolver: zodResolver(TCPConfigSchema),
})
useEffect(() => {
setTCPConfig(undefined)
form.reset({})
}, [])
const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => {
handleSave()
setTCPConfig({ type: 'tcp', ...values, name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
if (proxyCfg.name === proxyName) {
return { ...values, type: 'tcp', name: proxyName } as TCPProxyConfig
}
return proxyCfg
})
$clientProxyConfigs.set(newProxiyConfigs)
}
useEffect(() => {
setTCPConfig(undefined)
form.reset({})
}, [])
const [isSaveDisabled, setSaveDisabled] = useState(false)
const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => {
handleSave()
setTCPConfig({ type: "tcp", ...values, name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => {
if (proxyCfg.name === proxyName) {
return { ...values, type: "tcp", name: proxyName } as TCPProxyConfig
}
return proxyCfg
})
$clientProxyConfigs.set(newProxiyConfigs)
}
const handleSave = () => {
setSaveDisabled(true)
setTimeout(() => {
setSaveDisabled(false)
}, 3000)
}
const [isSaveDisabled, setSaveDisabled] = useState(false);
const { data: server } = useQuery({
queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID })
},
})
const handleSave = () => {
setSaveDisabled(true);
setTimeout(() => {
setSaveDisabled(false);
}, 3000);
};
const { data: server } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => {
return getServer({ serverId: serverID })
}
})
return (
<Form {...form}>
<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
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p>
<FormField
control={form.control}
name="localPort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 1234 : defaultProxyConfig.localPort}
/>
<FormField
control={form.control}
name="localIP"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP}
/>
<FormField
control={form.control}
name="remotePort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
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>
</form>
</Form>
);
return (
<Form {...form}>
<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
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p>
<FormField
control={form.control}
name="localPort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 1234 : defaultProxyConfig.localPort}
/>
<FormField
control={form.control}
name="localIP"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
/>
<FormField
control={form.control}
name="remotePort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
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>
</form>
</Form>
)
}
export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
const [_, setUDPConfig] = useState<UDPProxyConfig | undefined>()
const form = useForm<z.infer<typeof UDPConfigSchema>>({
resolver: zodResolver(UDPConfigSchema),
const [_, setUDPConfig] = useState<UDPProxyConfig | undefined>()
const form = useForm<z.infer<typeof UDPConfigSchema>>({
resolver: zodResolver(UDPConfigSchema),
})
useEffect(() => {
setUDPConfig(undefined)
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) => {
if (proxyCfg.name === proxyName) {
return { ...values, type: 'udp', name: proxyName } as UDPProxyConfig
}
return proxyCfg
})
$clientProxyConfigs.set(newProxiyConfigs)
}
useEffect(() => {
setUDPConfig(undefined)
form.reset({})
}, [])
const [isSaveDisabled, setSaveDisabled] = useState(false)
const handleSave = () => {
setSaveDisabled(true)
setTimeout(() => {
setSaveDisabled(false)
}, 3000)
}
const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => {
handleSave()
setUDPConfig({ type: "udp", ...values, name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => {
if (proxyCfg.name === proxyName) {
return { ...values, type: "udp", name: proxyName } as UDPProxyConfig
}
return proxyCfg
})
$clientProxyConfigs.set(newProxiyConfigs)
}
const { data: server } = useQuery({
queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID })
},
})
const [isSaveDisabled, setSaveDisabled] = useState(false);
const handleSave = () => {
setSaveDisabled(true);
setTimeout(() => {
setSaveDisabled(false);
}, 3000);
};
const { data: server } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => {
return getServer({ serverId: serverID })
}
})
return (
<Form {...form}>
<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
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p>
<FormField
control={form.control}
name="localPort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 1234 : defaultProxyConfig.localPort}
/>
<FormField
control={form.control}
name="localIP"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP}
/>
<FormField
control={form.control}
name="remotePort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</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>
</form>
</Form>
);
return (
<Form {...form}>
<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
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p>
<FormField
control={form.control}
name="localPort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 1234 : defaultProxyConfig.localPort}
/>
<FormField
control={form.control}
name="localIP"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
/>
<FormField
control={form.control}
name="remotePort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</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>
</form>
</Form>
)
}
export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
const [_, setHTTPConfig] = useState<HTTPProxyConfig | undefined>()
const [serverConfig, setServerConfig] = useState<ServerConfig | undefined>()
const form = useForm<z.infer<typeof HTTPConfigSchema>>({
resolver: zodResolver(HTTPConfigSchema),
const [_, setHTTPConfig] = useState<HTTPProxyConfig | undefined>()
const [serverConfig, setServerConfig] = useState<ServerConfig | undefined>()
const form = useForm<z.infer<typeof HTTPConfigSchema>>({
resolver: zodResolver(HTTPConfigSchema),
})
useEffect(() => {
setHTTPConfig(undefined)
form.reset({})
}, [])
const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => {
handleSave()
setHTTPConfig({ ...values, type: 'http', name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
if (proxyCfg.name === proxyName) {
return { ...values, type: 'http', name: proxyName } as HTTPProxyConfig
}
return proxyCfg
})
$clientProxyConfigs.set(newProxiyConfigs)
}
useEffect(() => {
setHTTPConfig(undefined)
form.reset({})
}, [])
const [isSaveDisabled, setSaveDisabled] = useState(false)
const clientProxyConfigs = useStore($clientProxyConfigs)
const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => {
handleSave()
setHTTPConfig({ ...values, type: "http", name: proxyName })
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => {
if (proxyCfg.name === proxyName) {
return { ...values, type: "http", name: proxyName } as HTTPProxyConfig
}
return proxyCfg
})
$clientProxyConfigs.set(newProxiyConfigs)
const handleSave = () => {
setSaveDisabled(true)
setTimeout(() => {
setSaveDisabled(false)
}, 3000)
}
const { data: server } = useQuery({
queryKey: ['getServer', serverID],
queryFn: () => {
return getServer({ serverId: serverID })
},
})
useEffect(() => {
if (server && server.server?.config) {
setServerConfig(JSON.parse(server.server?.config) as ServerConfig)
}
}, [server])
const [isSaveDisabled, setSaveDisabled] = useState(false);
const handleSave = () => {
setSaveDisabled(true);
setTimeout(() => {
setSaveDisabled(false);
}, 3000);
};
const { data: server } = useQuery({
queryKey: ["getServer", serverID], queryFn: () => {
return getServer({ serverId: serverID })
}
})
useEffect(() => {
if (server && server.server?.config) {
setServerConfig(JSON.parse(server.server?.config) as ServerConfig)
}
}, [server])
return (
<Form {...form}>
<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
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p>
<FormField
control={form.control}
name="localPort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 1234 : defaultProxyConfig.localPort}
/>
<FormField
control={form.control}
name="localIP"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP}
/>
<FormField
control={form.control}
name="subDomain"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</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>
</form>
</Form>
)
}
return (
<Form {...form}>
<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
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
</p>
<FormField
control={form.control}
name="localPort"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? 1234 : defaultProxyConfig.localPort}
/>
<FormField
control={form.control}
name="localIP"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
/>
<FormField
control={form.control}
name="subDomain"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</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>
</form>
</Form>
)
}

View File

@@ -1,134 +1,109 @@
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,
password: ZodStringSchema,
email: ZodEmailSchema,
username: ZodStringSchema,
password: ZodStringSchema,
email: ZodEmailSchema,
})
export const RegisterComponent = () => {
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
})
const { toast } = useToast()
const router = useRouter()
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
})
const { toast } = useToast()
const router = useRouter()
const [registerAlert, setRegisterAlert] = useState(false)
const sleep = async (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
const [registerAlert, setRegisterAlert] = useState(false)
const sleep = async (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
toast({ title: '注册中,请稍候' })
try {
const res = await register({ ...values })
if (res.status?.code === RespCode.SUCCESS) {
toast({ title: '注册成功,正在跳转到登录' })
setRegisterAlert(false)
await sleep(3000)
router.push('/login')
} else {
toast({ title: '注册失败' })
setRegisterAlert(true)
}
} catch (e) {
toast({ title: '注册失败' })
console.log('register error', e)
setRegisterAlert(true)
}
}
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
toast({ title: "注册中,请稍候" })
try {
const res = await register({ ...values })
if (res.status?.code === RespCode.SUCCESS) {
toast({ title: "注册成功,正在跳转到登录" })
setRegisterAlert(false)
await sleep(3000)
router.push("/login")
} else {
toast({ title: "注册失败" })
setRegisterAlert(true)
}
} catch (e) {
toast({ title: "注册失败" })
console.log("register error", e)
setRegisterAlert(true)
}
}
return (
<div className="w-full flex flex-col gap-6">
<Form {...form}>
<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}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="email"
placeholder="邮箱地址"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="password"
placeholder="密码"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{registerAlert && <Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
</AlertDescription>
</Alert>}
<Button type="submit"></Button>
</form>
</Form>
</div>
)
}
return (
<div className="w-full flex flex-col gap-6">
<Form {...form}>
<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} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="email" placeholder="邮箱地址" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="password" placeholder="密码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{registerAlert && (
<Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription></AlertDescription>
</Alert>
)}
<Button type="submit"></Button>
</form>
</Form>
</div>
)
}

View File

@@ -1,73 +1,74 @@
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,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} 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 }],
queryFn: async () => {
return await listServer({
page: 1,
pageSize: 10
})
}
})
const newServer = useMutation({
mutationFn: initServer,
})
const { toast } = useToast()
const [serverID, setServerID] = useState<string | undefined>()
const [serverIP, setServerIP] = useState<string | undefined>()
const dataQuery = useQuery({
queryKey: ['listServer', { pageIndex: 0, pageSize: 10 }],
queryFn: async () => {
return await listServer({
page: 1,
pageSize: 10,
})
},
})
const newServer = useMutation({
mutationFn: initServer,
})
const { toast } = useToast()
const handleNewServer = async () => {
toast({ title: "已提交创建请求" })
try {
let resp = await newServer.mutateAsync({ serverId: serverID, serverIp: serverIP })
if (resp.status?.code !== RespCode.SUCCESS) {
toast({ title: "创建服务端失败" })
return
}
toast({ title: "创建服务端成功" })
dataQuery.refetch()
} catch (error) {
toast({ title: "创建服务端失败" })
}
}
const handleNewServer = async () => {
toast({ title: '已提交创建请求' })
try {
let resp = await newServer.mutateAsync({ serverId: serverID, serverIp: serverIP })
if (resp.status?.code !== RespCode.SUCCESS) {
toast({ title: '创建服务端失败' })
return
}
toast({ title: '创建服务端成功' })
dataQuery.refetch()
} catch (error) {
toast({ title: '创建服务端失败' })
}
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size={'sm'}>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>ID必须唯一</DialogDescription>
</DialogHeader>
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size={"sm"}></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>ID必须唯一</DialogDescription>
</DialogHeader>
<Label>ID</Label>
<Input className="mt-2" value={serverID} onChange={(e) => setServerID(e.target.value)} />
<Label>IP地址</Label>
<Input className="mt-2" value={serverIP} onChange={(e) => setServerIP(e.target.value)} />
<DialogFooter>
<Button onClick={handleNewServer}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
<Label>ID</Label>
<Input className="mt-2" value={serverID} onChange={(e) => setServerID(e.target.value)} />
<Label>IP地址</Label>
<Input className="mt-2" value={serverIP} onChange={(e) => setServerIP(e.target.value)} />
<DialogFooter>
<Button onClick={handleNewServer}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,267 +1,292 @@
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,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from "@/components/ui/button"
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
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"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
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'
export type ServerTableSchema = {
id: string,
status: "invalid" | "valid"
secret: string
info?: string
ip: string
config?: string
id: string
status: 'invalid' | 'valid'
secret: string
info?: string
ip: string
config?: string
}
export const columns: ColumnDef<ServerTableSchema>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
return <ServerID server={row.original} />
}
},
{
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`}>{
{
valid: "已配置",
invalid: "未配置",
}[Server.status]
}</div>
}
},
{
accessorKey: "info",
header: "运行信息",
cell: ({ row }) => {
const Server = row.original
return <ServerInfo server={Server} />
}
},
{
accessorKey: "ip",
header: "IP",
cell: ({ row }) => {
const Server = row.original
return <div className="font-mono">{Server.ip}</div>
}
},
{
accessorKey: "secret",
header: "连接密钥",
cell: ({ row }) => {
const Server = row.original
return <ServerSecret server={Server} />
}
},
{
id: "action",
cell: ({ row, table }) => {
const Server = row.original
return (<ServerActions server={Server} table={table} />)
},
},
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => {
return <ServerID server={row.original} />
},
},
{
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`}>
{
{
valid: '已配置',
invalid: '未配置',
}[Server.status]
}
</div>
)
},
},
{
accessorKey: 'info',
header: '运行信息',
cell: ({ row }) => {
const Server = row.original
return <ServerInfo server={Server} />
},
},
{
accessorKey: 'ip',
header: 'IP',
cell: ({ row }) => {
const Server = row.original
return <div className="font-mono">{Server.ip}</div>
},
},
{
accessorKey: 'secret',
header: '连接密钥',
cell: ({ row }) => {
const Server = row.original
return <ServerSecret server={Server} />
},
},
{
id: 'action',
cell: ({ row, table }) => {
const Server = row.original
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>
<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)}
</div>
{/* <div>Windows</div>
const platformInfo = useStore($platformInfo)
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)}
</div>
{/* <div>Windows</div>
<div className="p-2 border rounded font-mono w-fit">
{platformInfo === undefined ? "获取平台信息失败" : WindowsInstallCommand("server", server, platformInfo)}
</div> */}
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
)
}
export const ServerInfo = ({ server }: { server: ServerTableSchema }) => {
const clientsInfo = useQuery({
queryKey: ["getClientsStatus", [server.id]],
queryFn: async () => {
return await getClientsStatus({
clientIds: [server.id],
clientType: ClientType.FRPS,
})
}
})
const clientsInfo = useQuery({
queryKey: ['getClientsStatus', [server.id]],
queryFn: async () => {
return await getClientsStatus({
clientIds: [server.id],
clientType: ClientType.FRPS,
})
},
})
const trans = (info: ClientStatus | undefined) => {
let statusText: "在线" | "离线" | "错误" | "未知" = "未知";
if (info === undefined) {
return statusText;
}
if (info.status === ClientStatus_Status.ONLINE) {
statusText = "在线";
} else if (info.status === ClientStatus_Status.OFFLINE) {
statusText = "离线";
} else if (info.status === ClientStatus_Status.ERROR) {
statusText = "错误";
} return statusText;
}
const trans = (info: ClientStatus | undefined) => {
let statusText: '在线' | '离线' | '错误' | '未知' = '未知'
if (info === undefined) {
return statusText
}
if (info.status === ClientStatus_Status.ONLINE) {
statusText = '在线'
} else if (info.status === ClientStatus_Status.OFFLINE) {
statusText = '离线'
} else if (info.status === ClientStatus_Status.ERROR) {
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}`}>
{`${clientsInfo.data?.clients[server.id].ping}ms, ${trans(clientsInfo.data?.clients[server.id])}`}
</div>
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 { toast } = useToast()
const platformInfo = useStore($platformInfo)
const [showSecrect, setShowSecrect] = useState<boolean>(false)
const fakeSecret = Array.from({ length: server.secret.length })
.map(() => '*')
.join('')
const { toast } = useToast()
const platformInfo = useStore($platformInfo)
return <Popover>
<PopoverTrigger asChild>
<div
onMouseEnter={() => setShowSecrect(true)}
onMouseLeave={() => setShowSecrect(false)}
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo));
toast({ description: "复制成功", });
} else {
toast({ description: "获取平台信息失败", });
}
}}
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)}
</div>
</PopoverContent>
</Popover>
return (
<Popover>
<PopoverTrigger asChild>
<div
onMouseEnter={() => setShowSecrect(true)}
onMouseLeave={() => setShowSecrect(false)}
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr('server', server, platformInfo))
toast({ description: '复制成功' })
} else {
toast({ description: '获取平台信息失败' })
}
}}
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)}
</div>
</PopoverContent>
</Popover>
)
}
export interface ServerItemProps {
server: ServerTableSchema
table: Table<ServerTableSchema>
server: ServerTableSchema
table: Table<ServerTableSchema>
}
export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
const { toast } = useToast()
const router = useRouter();
const platformInfo = useStore($platformInfo)
const { toast } = useToast()
const router = useRouter()
const platformInfo = useStore($platformInfo)
const fetchDataOptions = {
pageIndex: table.getState().pagination.pageIndex,
pageSize: table.getState().pagination.pageSize,
}
const fetchDataOptions = {
pageIndex: table.getState().pagination.pageIndex,
pageSize: table.getState().pagination.pageSize,
}
const dataQuery = useQuery({
queryKey: ["listServer", fetchDataOptions],
queryFn: async () => {
return await listServer({
page: fetchDataOptions.pageIndex + 1,
pageSize: fetchDataOptions.pageSize
})
}
})
const removeServer = useMutation({
mutationFn: deleteServer,
onSuccess: () => {
toast({ description: "删除成功" })
dataQuery.refetch()
},
onError: () => {
toast({ description: "删除失败" })
}
})
return <Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only"></span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo));
toast({ description: "复制成功如果复制不成功请点击ID字段手动复制", });
} else {
toast({ description: "获取平台信息失败如果复制不成功请点击ID字段手动复制", });
}
}}
>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={
() => {
router.push({
pathname: "/serveredit",
query: {
serverID: server.id
}
})
}
}></DropdownMenuItem>
<DialogTrigger asChild>
<DropdownMenuItem className="text-destructive"></DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>?</DialogTitle>
<DialogDescription>
<p className="text-destructive">?</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>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
const dataQuery = useQuery({
queryKey: ['listServer', fetchDataOptions],
queryFn: async () => {
return await listServer({
page: fetchDataOptions.pageIndex + 1,
pageSize: fetchDataOptions.pageSize,
})
},
})
const removeServer = useMutation({
mutationFn: deleteServer,
onSuccess: () => {
toast({ description: '删除成功' })
dataQuery.refetch()
},
onError: () => {
toast({ description: '删除失败' })
},
})
return (
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only"></span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
if (platformInfo) {
navigator.clipboard.writeText(ExecCommandStr('server', server, platformInfo))
toast({ description: '复制成功如果复制不成功请点击ID字段手动复制' })
} else {
toast({ description: '获取平台信息失败如果复制不成功请点击ID字段手动复制' })
}
}}
>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
router.push({
pathname: '/serveredit',
query: {
serverID: server.id,
},
})
}}
>
</DropdownMenuItem>
<DialogTrigger asChild>
<DropdownMenuItem className="text-destructive"></DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>?</DialogTitle>
<DialogDescription>
<p className="text-destructive">?</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>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,89 +1,91 @@
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,
getCoreRowModel,
ColumnFiltersState,
useReactTable,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
PaginationState,
} from "@tanstack/react-table"
getSortedRowModel,
getCoreRowModel,
ColumnFiltersState,
useReactTable,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
PaginationState,
} 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[]
Servers: Server[]
}
export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
const [sorting, setSorting] = React.useState<SortingState>([])
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))
const [sorting, setSorting] = React.useState<SortingState>([])
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,
)
const [{ pageIndex, pageSize }, setPagination] =
React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const fetchDataOptions = {
pageIndex,
pageSize,
}
const pagination = React.useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const fetchDataOptions = {
pageIndex,
pageSize,
}
const pagination = React.useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize],
)
const dataQuery = useQuery({
queryKey: ["listServer", fetchDataOptions],
queryFn: async () => {
return await listServer({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
}
})
const dataQuery = useQuery({
queryKey: ['listServer', fetchDataOptions],
queryFn: async () => {
return await listServer({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
},
})
const table = useReactTable({
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,
ip: server.ip,
config: server.config
} as ServerTableSchema
}) ?? data,
pageCount: Math.ceil((dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0),
columns: serverColumnsDef,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
state: {
sorting,
pagination,
columnFilters,
},
})
return <DataTable table={table} columns={serverColumnsDef} />
};
const table = useReactTable({
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,
ip: server.ip,
config: server.config,
} as ServerTableSchema
}) ?? data,
pageCount: Math.ceil(
(dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0,
),
columns: serverColumnsDef,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
state: {
sorting,
pagination,
columnFilters,
},
})
return <DataTable table={table} columns={serverColumnsDef} />
}

View File

@@ -1,36 +1,83 @@
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
label: string
eventHandler: () => void
id: string
label: string
eventHandler: () => void
}
export interface SideBarProps {
items?: SideBarItem[]
items?: SideBarItem[]
}
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") } },
]
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>)}
</div>
)
}
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>
))}
</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 id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
)
},
)
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} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -90,87 +78,59 @@ 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 { error, formItemId, formDescriptionId, formMessageId } = useFormField()
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-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
)
},
)
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
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}
/>
)
})
FormDescription.displayName = "FormDescription"
return (
<p ref={ref} id={formDescriptionId} className={cn('text-[0.8rem] text-muted-foreground', className)} {...props} />
)
},
)
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
{body}
</p>
)
},
)
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) => {
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
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
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,
)}
ref={ref}
{...props}
/>
)
})
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
) => (
<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
)}
{...props}
/>
)
)
>(({ 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)}
{...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) => (
<div className="relative w-full overflow-auto">
<table
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} />
</div>
),
)
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 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 TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
),
)
TableRow.displayName = 'TableRow'
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 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,
)}
{...props}
/>
),
)
TableHead.displayName = 'TableHead'
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 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)}
{...props}
/>
),
)
TableCell.displayName = 'TableCell'
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 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 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
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
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
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
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
)}
{...props}
/>
))
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"
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) => {
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
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
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,
)}
ref={ref}
{...props}
/>
)
})
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,43 +1,49 @@
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) => {
return `Invoke-WebRequest -Uri 'https://github.com/your_repository/frp-panel/releases/latest/download/frp-panel-amd64.exe' -OutFile 'frp-panel.exe'
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')}"
Set-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\FRPPanel' -Name 'ImagePath' -Value "\`"$command\`""
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

@@ -61,4 +61,4 @@
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
}

View File

@@ -1,19 +1,19 @@
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'>
<FRPCFormCard />
</div>
</div>
</Providers>
</RootLayout>
)
return (
<RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers>
<div className="w-full">
<div className="flex-1 flex-col">
<FRPCFormCard />
</div>
</div>
</Providers>
</RootLayout>
)
}

View File

@@ -1,23 +1,23 @@
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="flex-1 flex-row mb-2">
<CreateClientDialog />
</div>
<ClientList Clients={[]} />
</div>
</div>
</Providers>
</RootLayout>
)
return (
<RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers>
<div className="w-full">
<div className="flex-1 flex-col">
<div className="flex-1 flex-row mb-2">
<CreateClientDialog />
</div>
<ClientList Clients={[]} />
</div>
</div>
</Providers>
</RootLayout>
)
}

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>
<div className='lg:p-8 justify-center w-[300px]'>
<div className='flex flex-col justify-center space-y-6 w-[300px]'>
<div className="lg:p-8 justify-center w-[300px]">
<div className="flex flex-col justify-center space-y-6 w-[300px]">
<div className="flex flex-col space-y-2 text-center">
<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,59 +1,64 @@
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}`}
const router = useRouter()
return (
<main className={`${inter.className}`}>
<Providers>
<div
className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
onClick={() => router.push('/')}
>
<Providers>
<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'>
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
FRP Panel
</div>
<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">
<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>
</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]">
<RegisterComponent />
</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'>
<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>
</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]'>
<RegisterComponent />
</div>
</div>
</div>
</div>
</div>
<Toaster />
</Providers>
</main>
)
</div>
</div>
</div>
</div>
<Toaster />
</Providers>
</main>
)
}

View File

@@ -1,19 +1,19 @@
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'>
<FRPSFormCard />
</div>
</div>
</Providers>
</RootLayout>
)
return (
<RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers>
<div className="w-full">
<div className="flex-1 flex-col">
<FRPSFormCard />
</div>
</div>
</Providers>
</RootLayout>
)
}

View File

@@ -1,24 +1,23 @@
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="flex-1 flex-row mb-2">
<CreateServerDialog />
</div>
<ServerList Servers={[]} />
</div>
</div>
</Providers>
</RootLayout>
)
return (
<RootLayout header={<Header />} sidebar={<SideBar />}>
<Providers>
<div className="w-full">
<div className="flex-1 flex-col">
<div className="flex-1 flex-row mb-2">
<CreateServerDialog />
</div>
<ServerList Servers={[]} />
</div>
</div>
</Providers>
</RootLayout>
)
}

View File

@@ -1,25 +1,24 @@
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 (
<></>
// <RootLayout header={<Header />} sidebar={<SideBar />}>
// <Providers>
// <div className='grid grid-cols-1 md:grid-cols-2 gap-8'>
// <FRPCFormCard></FRPCFormCard>
// <FRPSFormCard></FRPSFormCard>
// </div>
// <Separator className='my-2' />
// <APITest />
// </Providers>
// </RootLayout>
)
return (
<></>
// <RootLayout header={<Header />} sidebar={<SideBar />}>
// <Providers>
// <div className='grid grid-cols-1 md:grid-cols-2 gap-8'>
// <FRPCFormCard></FRPCFormCard>
// <FRPSFormCard></FRPSFormCard>
// </div>
// <Separator className='my-2' />
// <APITest />
// </Providers>
// </RootLayout>
)
}

3172
www/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -5,4 +5,4 @@ import { atom } from 'nanostores'
export const $userInfo = atom<User | undefined>()
export const $statusOnline = atom<boolean>(false)
export const $token = atom<string | undefined>()
export const $platformInfo = atom<GetPlatformInfoResponse | undefined>()
export const $platformInfo = atom<GetPlatformInfoResponse | undefined>()

View File

@@ -1,7 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
@@ -9,63 +9,63 @@
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
@@ -73,4 +73,4 @@
body {
@apply bg-background text-foreground;
}
}
}

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 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
}