mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2025-10-25 16:30:38 +08:00
chore: prettify frontend code
This commit is contained in:
2
www/.prettierignore
Normal file
2
www/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
lib/pb
|
||||
out
|
||||
41
www/.prettierrc.js
Normal file
41
www/.prettierrc.js
Normal 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',
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import http from '@/api/http'
|
||||
import { API_PATH } from '@/lib/consts'
|
||||
import {
|
||||
LoginRequest, LoginResponse,
|
||||
RegisterRequest, RegisterResponse
|
||||
} from '@/lib/pb/api_auth'
|
||||
import { LoginRequest, LoginResponse, RegisterRequest, RegisterResponse } from '@/lib/pb/api_auth'
|
||||
import { CommonResponse } from '@/lib/pb/common'
|
||||
import { BaseResponse } from '@/types/api'
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import http from '@/api/http'
|
||||
import { API_PATH } from "@/lib/consts";
|
||||
import { API_PATH } from '@/lib/consts'
|
||||
import {
|
||||
DeleteClientRequest, DeleteClientResponse,
|
||||
GetClientRequest, GetClientResponse,
|
||||
InitClientRequest, InitClientResponse,
|
||||
ListClientsRequest, ListClientsResponse
|
||||
} from '@/lib/pb/api_client';
|
||||
import { BaseResponse } from "@/types/api";
|
||||
DeleteClientRequest,
|
||||
DeleteClientResponse,
|
||||
GetClientRequest,
|
||||
GetClientResponse,
|
||||
InitClientRequest,
|
||||
InitClientResponse,
|
||||
ListClientsRequest,
|
||||
ListClientsResponse,
|
||||
} from '@/lib/pb/api_client'
|
||||
import { BaseResponse } from '@/types/api'
|
||||
|
||||
export const getClient = async (req: GetClientRequest) => {
|
||||
const res = await http.post(API_PATH + '/client/get', GetClientRequest.toJson(req))
|
||||
@@ -24,7 +28,7 @@ export const deleteClient = async (req: DeleteClientRequest) => {
|
||||
}
|
||||
|
||||
export const initClient = async (req: InitClientRequest) => {
|
||||
console.log("attempting init client:", InitClientRequest.toJsonString(req))
|
||||
console.log('attempting init client:', InitClientRequest.toJsonString(req))
|
||||
const res = await http.post(API_PATH + '/client/init', InitClientRequest.toJson(req))
|
||||
return InitClientResponse.fromJson((res.data as BaseResponse).body)
|
||||
}
|
||||
@@ -1,18 +1,8 @@
|
||||
import http from '@/api/http'
|
||||
import { API_PATH } from "@/lib/consts";
|
||||
import {
|
||||
RemoveFRPCRequest,
|
||||
RemoveFRPCResponse,
|
||||
UpdateFRPCRequest,
|
||||
UpdateFRPCResponse
|
||||
} from '@/lib/pb/api_client';
|
||||
import {
|
||||
RemoveFRPSRequest,
|
||||
RemoveFRPSResponse,
|
||||
UpdateFRPSRequest,
|
||||
UpdateFRPSResponse
|
||||
} from '@/lib/pb/api_server';
|
||||
import { BaseResponse } from "@/types/api";
|
||||
import { API_PATH } from '@/lib/consts'
|
||||
import { RemoveFRPCRequest, RemoveFRPCResponse, UpdateFRPCRequest, UpdateFRPCResponse } from '@/lib/pb/api_client'
|
||||
import { RemoveFRPSRequest, RemoveFRPSResponse, UpdateFRPSRequest, UpdateFRPSResponse } from '@/lib/pb/api_server'
|
||||
import { BaseResponse } from '@/types/api'
|
||||
|
||||
export const updateFRPS = async (req: UpdateFRPSRequest) => {
|
||||
const res = await http.post(API_PATH + '/frps/update', UpdateFRPSRequest.toJson(req))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LOCAL_STORAGE_TOKEN_KEY, SET_TOKEN_HEADER, X_CLIENT_REQUEST_ID } from '@/lib/consts';
|
||||
import { $token } from '@/store/user';
|
||||
import { LOCAL_STORAGE_TOKEN_KEY, SET_TOKEN_HEADER, X_CLIENT_REQUEST_ID } from '@/lib/consts'
|
||||
import { $token } from '@/store/user'
|
||||
import axios from 'axios'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const instance = axios.create({})
|
||||
|
||||
@@ -11,7 +11,7 @@ instance.interceptors.request.use((request) => {
|
||||
request.headers.Authorization = token
|
||||
$token.set(token)
|
||||
}
|
||||
request.headers[X_CLIENT_REQUEST_ID] = uuidv4();
|
||||
request.headers[X_CLIENT_REQUEST_ID] = uuidv4()
|
||||
return request
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import http from '@/api/http'
|
||||
import { API_PATH } from '@/lib/consts'
|
||||
import {
|
||||
GetClientsStatusRequest,
|
||||
GetClientsStatusResponse
|
||||
} from '@/lib/pb/api_master'
|
||||
import {
|
||||
GetPlatformInfoResponse,
|
||||
} from '@/lib/pb/api_user'
|
||||
import { GetClientsStatusRequest, GetClientsStatusResponse } from '@/lib/pb/api_master'
|
||||
import { GetPlatformInfoResponse } from '@/lib/pb/api_user'
|
||||
import { BaseResponse } from '@/types/api'
|
||||
|
||||
export const getPlatformInfo = async () => {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import http from '@/api/http'
|
||||
import { API_PATH } from "@/lib/consts";
|
||||
import { API_PATH } from '@/lib/consts'
|
||||
import {
|
||||
DeleteServerRequest,
|
||||
DeleteServerResponse,
|
||||
GetServerRequest, GetServerResponse,
|
||||
GetServerRequest,
|
||||
GetServerResponse,
|
||||
InitServerRequest,
|
||||
InitServerResponse,
|
||||
ListServersRequest, ListServersResponse
|
||||
} from "@/lib/pb/api_server";
|
||||
import { BaseResponse } from "@/types/api";
|
||||
ListServersRequest,
|
||||
ListServersResponse,
|
||||
} from '@/lib/pb/api_server'
|
||||
import { BaseResponse } from '@/types/api'
|
||||
|
||||
export const getServer = async (req: GetServerRequest) => {
|
||||
const res = await http.post(API_PATH + '/server/get', GetServerRequest.toJson(req))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import http from '@/api/http'
|
||||
import { API_PATH } from '@/lib/consts'
|
||||
import {
|
||||
GetUserInfoRequest, GetUserInfoResponse,
|
||||
UpdateUserInfoRequest, UpdateUserInfoResponse
|
||||
GetUserInfoRequest,
|
||||
GetUserInfoResponse,
|
||||
UpdateUserInfoRequest,
|
||||
UpdateUserInfoResponse,
|
||||
} from '@/lib/pb/api_user'
|
||||
import { $userInfo } from '@/store/user'
|
||||
import { BaseResponse } from '@/types/api'
|
||||
|
||||
@@ -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 >
|
||||
<div className="flex flex-col w-full p-10 lg:w-1/2">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4 my-4">
|
||||
<div>
|
||||
<Label>username</Label>
|
||||
<Input value={username} onChange={e => setUsername(e.target.value)} />
|
||||
<Input value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
</div>
|
||||
<div >
|
||||
<div>
|
||||
<Label>password</Label>
|
||||
<Input value={password} onChange={e => setPassword(e.target.value)} />
|
||||
<Input value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</div>
|
||||
<div >
|
||||
<div>
|
||||
<Label>email</Label>
|
||||
<Input value={email} onChange={e => setEmail(e.target.value)} />
|
||||
<Input value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div >
|
||||
<div>
|
||||
<Label>clientID</Label>
|
||||
<Input value={clientID} onChange={e => setClientID(e.target.value)} />
|
||||
<Input value={clientID} onChange={(e) => setClientID(e.target.value)} />
|
||||
</div>
|
||||
<div >
|
||||
<div>
|
||||
<Label>serverID</Label>
|
||||
<Input value={serverID} onChange={e => setServerID(e.target.value)} />
|
||||
<Input value={serverID} onChange={(e) => setServerID(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting login:", await login({ username: username, password: password }))
|
||||
}}>login</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting register:", await register({ username: username, password: password, email: email }))
|
||||
}}>register</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting update user:", await updateUserInfo({
|
||||
userInfo: { token: "123123" },
|
||||
}))
|
||||
}}>update user</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting get user:", await getUserInfo({}))
|
||||
}}>get user</Button>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting login:', await login({ username: username, password: password }))
|
||||
}}
|
||||
>
|
||||
login
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(
|
||||
'attempting register:',
|
||||
await register({ username: username, password: password, email: email }),
|
||||
)
|
||||
}}
|
||||
>
|
||||
register
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(
|
||||
'attempting update user:',
|
||||
await updateUserInfo({
|
||||
userInfo: { token: '123123' },
|
||||
}),
|
||||
)
|
||||
}}
|
||||
>
|
||||
update user
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting get user:', await getUserInfo({}))
|
||||
}}
|
||||
>
|
||||
get user
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting init server:", await initServer({ serverId: serverID.replace(username + ".", ""), serverIp: "127.0.0.1" }))
|
||||
}}>init server</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting delete server:", await deleteServer({ serverId: serverID }))
|
||||
}}>delete server</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting list servers:", await listServer({ page: 1, pageSize: 10 }))
|
||||
}}>list servers</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting get server:", await getServer({ serverId: serverID }))
|
||||
}}>get server</Button>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(
|
||||
'attempting init server:',
|
||||
await initServer({ serverId: serverID.replace(username + '.', ''), serverIp: '127.0.0.1' }),
|
||||
)
|
||||
}}
|
||||
>
|
||||
init server
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting delete server:', await deleteServer({ serverId: serverID }))
|
||||
}}
|
||||
>
|
||||
delete server
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting list servers:', await listServer({ page: 1, pageSize: 10 }))
|
||||
}}
|
||||
>
|
||||
list servers
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting get server:', await getServer({ serverId: serverID }))
|
||||
}}
|
||||
>
|
||||
get server
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting update frps:", await updateFRPS({
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(
|
||||
'attempting update frps:',
|
||||
await updateFRPS({
|
||||
serverId: serverID,
|
||||
config: Buffer.from(JSON.stringify({
|
||||
config: Buffer.from(
|
||||
JSON.stringify({
|
||||
bindPort: 1122,
|
||||
} as ServerConfig))
|
||||
}))
|
||||
}}>update frps</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting delete frps:", await deleteServer({ serverId: serverID }))
|
||||
}}>delete frps</Button>
|
||||
} as ServerConfig),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}}
|
||||
>
|
||||
update frps
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting delete frps:', await deleteServer({ serverId: serverID }))
|
||||
}}
|
||||
>
|
||||
delete frps
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting init client:", await initClient({ clientId: clientID.replace(username + ".", "") }))
|
||||
}}>init client</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting delete client:", await deleteClient({ clientId: clientID }))
|
||||
}}>delete client</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting list clients:", await listClient({ page: 1, pageSize: 10 }))
|
||||
}}>list clients</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting get client:", await getClient({ clientId: clientID }))
|
||||
}}>get client</Button>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting init client:', await initClient({ clientId: clientID.replace(username + '.', '') }))
|
||||
}}
|
||||
>
|
||||
init client
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting delete client:', await deleteClient({ clientId: clientID }))
|
||||
}}
|
||||
>
|
||||
delete client
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting list clients:', await listClient({ page: 1, pageSize: 10 }))
|
||||
}}
|
||||
>
|
||||
list clients
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting get client:', await getClient({ clientId: clientID }))
|
||||
}}
|
||||
>
|
||||
get client
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className='grid grid-cols-2 sm:grid-cols-4 gap-4 my-4'>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting update frpc:", await updateFRPC({
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 my-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log(
|
||||
'attempting update frpc:',
|
||||
await updateFRPC({
|
||||
clientId: clientID,
|
||||
serverId: serverID,
|
||||
config: Buffer.from(JSON.stringify({
|
||||
proxies: [
|
||||
{ name: "test", type: "tcp", localIP: "127.0.0.1", localPort: 1234, remotePort: 4321 }
|
||||
]
|
||||
} as ClientConfig))
|
||||
}))
|
||||
}}>update frpc</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log("attempting delete frpc:", await deleteClient({ clientId: clientID }))
|
||||
}}>delete frpc</Button>
|
||||
config: Buffer.from(
|
||||
JSON.stringify({
|
||||
proxies: [{ name: 'test', type: 'tcp', localIP: '127.0.0.1', localPort: 1234, remotePort: 4321 }],
|
||||
} as ClientConfig),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}}
|
||||
>
|
||||
update frpc
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('attempting delete frpc:', await deleteClient({ clientId: clientID }))
|
||||
}}
|
||||
>
|
||||
delete frpc
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from "react"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { initClient, listClient } from "@/api/client"
|
||||
import { Label } from "./ui/label"
|
||||
import { Input } from "./ui/input"
|
||||
import { Button } from "./ui/button"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { RespCode } from "@/lib/pb/common"
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { initClient, listClient } from '@/api/client'
|
||||
import { Label } from './ui/label'
|
||||
import { Input } from './ui/input'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
export const CreateClientDialog = () => {
|
||||
const [clientID, setClientID] = useState<string | undefined>()
|
||||
@@ -22,32 +22,34 @@ export const CreateClientDialog = () => {
|
||||
mutationFn: initClient,
|
||||
})
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ["listClient", { pageIndex: 0, pageSize: 10 }],
|
||||
queryKey: ['listClient', { pageIndex: 0, pageSize: 10 }],
|
||||
queryFn: async () => {
|
||||
return await listClient({ page: 1, pageSize: 10 })
|
||||
}
|
||||
},
|
||||
})
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleNewClient = async () => {
|
||||
toast({ title: "已提交创建请求" })
|
||||
toast({ title: '已提交创建请求' })
|
||||
try {
|
||||
let resp = await newClient.mutateAsync({ clientId: clientID })
|
||||
if (resp.status?.code !== RespCode.SUCCESS) {
|
||||
toast({ title: "创建客户端失败" })
|
||||
toast({ title: '创建客户端失败' })
|
||||
return
|
||||
}
|
||||
toast({ title: "创建客户端成功" })
|
||||
toast({ title: '创建客户端成功' })
|
||||
dataQuery.refetch()
|
||||
} catch (error) {
|
||||
toast({ title: "创建客户端失败" })
|
||||
toast({ title: '创建客户端失败' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size={"sm"}>新建</Button>
|
||||
<Button variant="outline" size={'sm'}>
|
||||
新建
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ColumnDef, Table } from "@tanstack/react-table"
|
||||
import { MoreHorizontal } from "lucide-react"
|
||||
import { ColumnDef, Table } from '@tanstack/react-table'
|
||||
import { MoreHorizontal } from 'lucide-react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -19,23 +19,23 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import React, { useState } from "react"
|
||||
import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from "@/lib/consts"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { deleteClient, listClient } from "@/api/client"
|
||||
import { useRouter } from "next/router"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $platformInfo } from "@/store/user"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
||||
import { getClientsStatus } from "@/api/platform"
|
||||
import { ClientType } from "@/lib/pb/common"
|
||||
import { ClientStatus, ClientStatus_Status } from "@/lib/pb/api_master"
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import React, { useState } from 'react'
|
||||
import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from '@/lib/consts'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { deleteClient, listClient } from '@/api/client'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $platformInfo } from '@/store/user'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
|
||||
import { getClientsStatus } from '@/api/platform'
|
||||
import { ClientType } from '@/lib/pb/common'
|
||||
import { ClientStatus, ClientStatus_Status } from '@/lib/pb/api_master'
|
||||
|
||||
export type ClientTableSchema = {
|
||||
id: string,
|
||||
status: "invalid" | "valid"
|
||||
id: string
|
||||
status: 'invalid' | 'valid'
|
||||
secret: string
|
||||
info?: string
|
||||
config?: string
|
||||
@@ -43,58 +43,65 @@ export type ClientTableSchema = {
|
||||
|
||||
export const columns: ColumnDef<ClientTableSchema>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: ({ row }) => {
|
||||
return < ClientID client={row.original} />
|
||||
}
|
||||
return <ClientID client={row.original} />
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "是否配置",
|
||||
accessorKey: 'status',
|
||||
header: '是否配置',
|
||||
cell: ({ row }) => {
|
||||
const client = row.original
|
||||
return <div className={`font-medium ${client.status === "valid" ? "text-green-500" : "text-red-500"} min-w-12`}>{
|
||||
return (
|
||||
<div className={`font-medium ${client.status === 'valid' ? 'text-green-500' : 'text-red-500'} min-w-12`}>
|
||||
{
|
||||
valid: "已配置",
|
||||
invalid: "未配置",
|
||||
{
|
||||
valid: '已配置',
|
||||
invalid: '未配置',
|
||||
}[client.status]
|
||||
}</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "info",
|
||||
header: "运行信息",
|
||||
accessorKey: 'info',
|
||||
header: '运行信息',
|
||||
cell: ({ row }) => {
|
||||
const client = row.original
|
||||
return <ClientInfo client={client} />
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "secret",
|
||||
header: "连接密钥",
|
||||
accessorKey: 'secret',
|
||||
header: '连接密钥',
|
||||
cell: ({ row }) => {
|
||||
const client = row.original
|
||||
return <ClientSecret client={client} />
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
id: 'action',
|
||||
cell: ({ row, table }) => {
|
||||
const client = row.original
|
||||
return (<ClientActions client={client} table={table} />)
|
||||
return <ClientActions client={client} table={table} />
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const ClientID = ({ client }: { client: ClientTableSchema }) => {
|
||||
const platformInfo = useStore($platformInfo)
|
||||
return <Popover >
|
||||
<PopoverTrigger asChild><div className="font-mono">{client.id}</div></PopoverTrigger>
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="font-mono">{client.id}</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit overflow-auto max-w-64">
|
||||
<div>Linux安装到systemd</div>
|
||||
<div className="p-2 border rounded font-mono w-fit">
|
||||
{platformInfo === undefined ? "获取平台信息失败" : LinuxInstallCommand("client", client, platformInfo)}
|
||||
{platformInfo === undefined ? '获取平台信息失败' : LinuxInstallCommand('client', client, platformInfo)}
|
||||
</div>
|
||||
{/* <div>Windows</div>
|
||||
<div className="p-2 border rounded font-mono w-fit">
|
||||
@@ -102,68 +109,78 @@ export const ClientID = ({ client }: { client: ClientTableSchema }) => {
|
||||
</div> */}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClientInfo = ({ client }: { client: ClientTableSchema }) => {
|
||||
const clientsInfo = useQuery({
|
||||
queryKey: ["getClientsStatus", [client.id]],
|
||||
queryKey: ['getClientsStatus', [client.id]],
|
||||
queryFn: async () => {
|
||||
return await getClientsStatus({
|
||||
clientIds: [client.id],
|
||||
clientType: ClientType.FRPC,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const trans = (info: ClientStatus | undefined) => {
|
||||
let statusText: "在线" | "离线" | "错误" | "未知" = "未知";
|
||||
let statusText: '在线' | '离线' | '错误' | '未知' = '未知'
|
||||
if (info === undefined) {
|
||||
return statusText;
|
||||
return statusText
|
||||
}
|
||||
if (info.status === ClientStatus_Status.ONLINE) {
|
||||
statusText = "在线";
|
||||
statusText = '在线'
|
||||
} else if (info.status === ClientStatus_Status.OFFLINE) {
|
||||
statusText = "离线";
|
||||
statusText = '离线'
|
||||
} else if (info.status === ClientStatus_Status.ERROR) {
|
||||
statusText = "错误";
|
||||
} return statusText;
|
||||
statusText = '错误'
|
||||
}
|
||||
return statusText
|
||||
}
|
||||
|
||||
const infoColor = clientsInfo.data?.clients[client.id]?.status === ClientStatus_Status.ONLINE ? "text-green-500" : "text-red-500"
|
||||
const infoColor =
|
||||
clientsInfo.data?.clients[client.id]?.status === ClientStatus_Status.ONLINE ? 'text-green-500' : 'text-red-500'
|
||||
|
||||
return <div className={`p-2 border rounded font-mono w-fit ${infoColor}`}>
|
||||
return (
|
||||
<div className={`p-2 border rounded font-mono w-fit ${infoColor}`}>
|
||||
{`${clientsInfo.data?.clients[client.id].ping}ms, ${trans(clientsInfo.data?.clients[client.id])}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClientSecret = ({ client }: { client: ClientTableSchema }) => {
|
||||
const [showSecrect, setShowSecrect] = useState<boolean>(false)
|
||||
const fakeSecret = Array.from({ length: client.secret.length }).map(() => '*').join('')
|
||||
const fakeSecret = Array.from({ length: client.secret.length })
|
||||
.map(() => '*')
|
||||
.join('')
|
||||
const platformInfo = useStore($platformInfo)
|
||||
const { toast } = useToast()
|
||||
return <Popover >
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
onMouseEnter={() => setShowSecrect(true)}
|
||||
onMouseLeave={() => setShowSecrect(false)}
|
||||
onClick={() => {
|
||||
if (platformInfo) {
|
||||
navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo));
|
||||
toast({ description: "复制成功", });
|
||||
navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo))
|
||||
toast({ description: '复制成功' })
|
||||
} else {
|
||||
toast({ description: "获取平台信息失败", });
|
||||
toast({ description: '获取平台信息失败' })
|
||||
}
|
||||
}}
|
||||
className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap">{
|
||||
showSecrect ? client.secret : fakeSecret
|
||||
}</div>
|
||||
className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap"
|
||||
>
|
||||
{showSecrect ? client.secret : fakeSecret}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit overflow-auto max-w-48">
|
||||
<div className="p-2 border rounded font-mono w-fit">
|
||||
{platformInfo === undefined ? "获取平台信息失败" : ExecCommandStr("client", client, platformInfo)}
|
||||
{platformInfo === undefined ? '获取平台信息失败' : ExecCommandStr('client', client, platformInfo)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ClientItemProps {
|
||||
@@ -173,7 +190,7 @@ export interface ClientItemProps {
|
||||
|
||||
export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
|
||||
const { toast } = useToast()
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
const platformInfo = useStore($platformInfo)
|
||||
const fetchDataOptions = {
|
||||
pageIndex: table.getState().pagination.pageIndex,
|
||||
@@ -181,27 +198,28 @@ export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
|
||||
}
|
||||
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ["listClient", fetchDataOptions],
|
||||
queryKey: ['listClient', fetchDataOptions],
|
||||
queryFn: async () => {
|
||||
return await listClient({
|
||||
page: fetchDataOptions.pageIndex + 1,
|
||||
pageSize: fetchDataOptions.pageSize
|
||||
pageSize: fetchDataOptions.pageSize,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const removeClient = useMutation({
|
||||
mutationFn: deleteClient,
|
||||
onSuccess: () => {
|
||||
toast({ description: "删除成功" })
|
||||
toast({ description: '删除成功' })
|
||||
dataQuery.refetch()
|
||||
},
|
||||
onError: () => {
|
||||
toast({ description: "删除失败" })
|
||||
}
|
||||
toast({ description: '删除失败' })
|
||||
},
|
||||
})
|
||||
|
||||
return <Dialog>
|
||||
return (
|
||||
<Dialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -214,19 +232,23 @@ export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (platformInfo) {
|
||||
navigator.clipboard.writeText(ExecCommandStr("client", client, platformInfo));
|
||||
toast({ description: "复制成功,如果复制不成功,请点击ID字段手动复制", });
|
||||
navigator.clipboard.writeText(ExecCommandStr('client', client, platformInfo))
|
||||
toast({ description: '复制成功,如果复制不成功,请点击ID字段手动复制' })
|
||||
} else {
|
||||
toast({ description: "获取平台信息失败,如果复制不成功,请点击ID字段手动复制", });
|
||||
toast({ description: '获取平台信息失败,如果复制不成功,请点击ID字段手动复制' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制启动命令
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => {
|
||||
router.push({ pathname: "/clientedit", query: { clientID: client.id } })
|
||||
}}>修改</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push({ pathname: '/clientedit', query: { clientID: client.id } })
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</DropdownMenuItem>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem className="text-destructive">删除</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
@@ -236,17 +258,20 @@ export const ClientActions: React.FC<ClientItemProps> = ({ client, table }) => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>确定删除该客户端?</DialogTitle>
|
||||
<DialogDescription>
|
||||
<p className="text-destructive">
|
||||
此操作无法撤消。您确定要永久从我们的服务器中删除该客户端?
|
||||
<p className="text-destructive">此操作无法撤消。您确定要永久从我们的服务器中删除该客户端?</p>
|
||||
<p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2">
|
||||
删除后运行中的客户端将无法通过现有参数再次连接,如果您需要删除客户端对外的连接,可以选择清空配置
|
||||
</p>
|
||||
<p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2">删除后运行中的客户端将无法通过现有参数再次连接,如果您需要删除客户端对外的连接,可以选择清空配置</p>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="submit" onClick={() => removeClient.mutate({ clientId: client.id })}>确定</Button>
|
||||
<Button type="submit" onClick={() => removeClient.mutate({ clientId: client.id })}>
|
||||
确定
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Client } from "@/lib/pb/common";
|
||||
import { ClientTableSchema, columns as clientColumnsDef } from "./client_item";
|
||||
import { DataTable } from "./data_table";
|
||||
import { Client } from '@/lib/pb/common'
|
||||
import { ClientTableSchema, columns as clientColumnsDef } from './client_item'
|
||||
import { DataTable } from './data_table'
|
||||
|
||||
import {
|
||||
getSortedRowModel,
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
PaginationState,
|
||||
} from "@tanstack/react-table"
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
import React from "react"
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { listClient } from "@/api/client";
|
||||
import React from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { listClient } from '@/api/client'
|
||||
|
||||
export interface ClientListProps {
|
||||
Clients: Client[]
|
||||
@@ -23,19 +23,18 @@ export interface ClientListProps {
|
||||
|
||||
export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const data = Clients.map((client) =>
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
const data = Clients.map(
|
||||
(client) =>
|
||||
({
|
||||
id: client.id == undefined ? "" : client.id,
|
||||
status: client.config == undefined || client.config == "" ? "invalid" : "valid",
|
||||
secret: client.secret == undefined ? "" : client.secret,
|
||||
config: client.config
|
||||
} as ClientTableSchema))
|
||||
id: client.id == undefined ? '' : client.id,
|
||||
status: client.config == undefined || client.config == '' ? 'invalid' : 'valid',
|
||||
secret: client.secret == undefined ? '' : client.secret,
|
||||
config: client.config,
|
||||
}) as ClientTableSchema,
|
||||
)
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] =
|
||||
React.useState<PaginationState>({
|
||||
const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
@@ -49,26 +48,29 @@ export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
[pageIndex, pageSize],
|
||||
)
|
||||
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ["listClient", fetchDataOptions],
|
||||
queryKey: ['listClient', fetchDataOptions],
|
||||
queryFn: async () => {
|
||||
return await listClient({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataQuery.data?.clients.map((client) => {
|
||||
data:
|
||||
dataQuery.data?.clients.map((client) => {
|
||||
return {
|
||||
id: client.id == undefined ? "" : client.id,
|
||||
status: client.config == undefined || client.config == "" ? "invalid" : "valid",
|
||||
secret: client.secret == undefined ? "" : client.secret,
|
||||
config: client.config
|
||||
id: client.id == undefined ? '' : client.id,
|
||||
status: client.config == undefined || client.config == '' ? 'invalid' : 'valid',
|
||||
secret: client.secret == undefined ? '' : client.secret,
|
||||
config: client.config,
|
||||
} as ClientTableSchema
|
||||
}) ?? data,
|
||||
pageCount: Math.ceil((dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0),
|
||||
pageCount: Math.ceil(
|
||||
(dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0,
|
||||
),
|
||||
columns: clientColumnsDef,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
@@ -85,4 +87,4 @@ export const ClientList: React.FC<ClientListProps> = ({ Clients }) => {
|
||||
},
|
||||
})
|
||||
return <DataTable table={table} columns={clientColumnsDef} />
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CaretSortIcon,
|
||||
EyeNoneIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
import { Column } from "@tanstack/react-table"
|
||||
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
|
||||
import { Column } from '@tanstack/react-table'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>
|
||||
title: string
|
||||
}
|
||||
@@ -32,18 +26,14 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center space-x-2", className)}>
|
||||
<div className={cn('flex items-center space-x-2', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<Button variant="ghost" size="sm" className="-ml-3 h-8 data-[state=open]:bg-accent">
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
{column.getIsSorted() === 'desc' ? (
|
||||
<ArrowDownIcon className="ml-2 h-4 w-4" />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
) : column.getIsSorted() === 'asc' ? (
|
||||
<ArrowUpIcon className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
@@ -11,20 +11,13 @@ import {
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
Table as TableType,
|
||||
} from "@tanstack/react-table"
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
|
||||
import React from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { DataTablePagination } from "./data_table_pagination"
|
||||
import React from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { DataTablePagination } from './data_table_pagination'
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
@@ -33,24 +26,19 @@ interface DataTableProps<TData, TValue> {
|
||||
table: TableType<TData>
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
filterColumnName,
|
||||
table,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
export function DataTable<TData, TValue>({ columns, filterColumnName, table }: DataTableProps<TData, TValue>) {
|
||||
return (
|
||||
<div>
|
||||
{filterColumnName && <div className="flex flex-1 items-center py-4">
|
||||
{filterColumnName && (
|
||||
<div className="flex flex-1 items-center py-4">
|
||||
<Input
|
||||
placeholder={`根据 ${filterColumnName} 筛选`}
|
||||
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn(filterColumnName)?.setFilterValue(event.target.value)
|
||||
}
|
||||
value={(table.getColumn(filterColumnName)?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) => table.getColumn(filterColumnName)?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -59,12 +47,7 @@ export function DataTable<TData, TValue>({
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
@@ -74,14 +57,9 @@ export function DataTable<TData, TValue>({
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
import { Table } from "@tanstack/react-table"
|
||||
import { ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons'
|
||||
import { Table } from '@tanstack/react-table'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
|
||||
interface DataTablePaginationProps<TData> {
|
||||
table: Table<TData>
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
table,
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
{/* <div className="flex-1 text-sm text-muted-foreground">
|
||||
@@ -50,8 +37,7 @@ export function DataTablePagination<TData>({
|
||||
<p className="text-sm font-medium">行 每页</p>
|
||||
</div>
|
||||
<div className="flex w-[120px] items-center justify-center text-sm font-medium">
|
||||
第 {table.getState().pagination.pageIndex + 1} 页, 共{" "}
|
||||
{table.getPageCount()}页
|
||||
第 {table.getState().pagination.pageIndex + 1} 页, 共 {table.getPageCount()}页
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
import React, { useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Label } from "@radix-ui/react-label"
|
||||
import React, { useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Label } from '@radix-ui/react-label'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { listServer } from "@/api/server"
|
||||
import { getClient, listClient } from "@/api/client"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { FRPCEditor } from "./frpc_editor"
|
||||
import { FRPCForm } from "./frpc_form"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { listServer } from '@/api/server'
|
||||
import { getClient, listClient } from '@/api/client'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Switch } from './ui/switch'
|
||||
import { FRPCEditor } from './frpc_editor'
|
||||
import { FRPCForm } from './frpc_form'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export interface FRPCFormCardProps {
|
||||
clientID?: string
|
||||
serverID?: string
|
||||
}
|
||||
export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultClientID, serverID: defaultServerID }: FRPCFormCardProps) => {
|
||||
export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({
|
||||
clientID: defaultClientID,
|
||||
serverID: defaultServerID,
|
||||
}: FRPCFormCardProps) => {
|
||||
const [advanceMode, setAdvanceMode] = useState<boolean>(false)
|
||||
const [clientID, setClientID] = useState<string | undefined>()
|
||||
const [serverID, setServerID] = useState<string | undefined>()
|
||||
@@ -51,21 +42,24 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
|
||||
}, [defaultClientID, defaultServerID])
|
||||
|
||||
const { data: serverList, refetch: refetchServers } = useQuery({
|
||||
queryKey: ["listServer"], queryFn: () => {
|
||||
queryKey: ['listServer'],
|
||||
queryFn: () => {
|
||||
return listServer({ page: 1, pageSize: 500 })
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
const { data: clientList, refetch: refetchClients } = useQuery({
|
||||
queryKey: ["listClient"], queryFn: () => {
|
||||
queryKey: ['listClient'],
|
||||
queryFn: () => {
|
||||
return listClient({ page: 1, pageSize: 500 })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { data: client, refetch: refetchClient } = useQuery({
|
||||
queryKey: ["getClient", clientID], queryFn: () => {
|
||||
queryKey: ['getClient', clientID],
|
||||
queryFn: () => {
|
||||
return getClient({ clientId: clientID })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -84,19 +78,15 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
|
||||
<CardContent>
|
||||
<div className=" flex items-center space-x-4 rounded-md border p-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
高级模式
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
编辑客户端原始配置文件
|
||||
</p>
|
||||
<p className="text-sm font-medium leading-none">高级模式</p>
|
||||
<p className="text-sm text-muted-foreground">编辑客户端原始配置文件</p>
|
||||
</div>
|
||||
<Switch onCheckedChange={setAdvanceMode} />
|
||||
</div>
|
||||
<div className="flex flex-col w-full pt-2">
|
||||
|
||||
<Label className="text-sm font-medium">服务端</Label>
|
||||
<Select onValueChange={handleServerChange}
|
||||
<Select
|
||||
onValueChange={handleServerChange}
|
||||
value={serverID}
|
||||
onOpenChange={() => {
|
||||
refetchServers()
|
||||
@@ -108,12 +98,18 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serverList?.servers.map(
|
||||
server => server.id && <SelectItem key={server.id} value={server.id}>{server.id}
|
||||
</SelectItem>)}
|
||||
(server) =>
|
||||
server.id && (
|
||||
<SelectItem key={server.id} value={server.id}>
|
||||
{server.id}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="text-sm font-medium">客户端</Label>
|
||||
<Select onValueChange={handleClientChange}
|
||||
<Select
|
||||
onValueChange={handleClientChange}
|
||||
value={clientID}
|
||||
onOpenChange={() => {
|
||||
refetchServers()
|
||||
@@ -125,8 +121,13 @@ export const FRPCFormCard: React.FC<FRPCFormCardProps> = ({ clientID: defaultCli
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clientList?.clients.map(
|
||||
client => client.id && <SelectItem key={client.id} value={client.id}>{client.id}
|
||||
</SelectItem>)}
|
||||
(client) =>
|
||||
client.id && (
|
||||
<SelectItem key={client.id} value={client.id}>
|
||||
{client.id}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -1,70 +1,94 @@
|
||||
import { Label } from "@radix-ui/react-label"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { FRPCFormProps } from "./frpc_form"
|
||||
import { getClient } from "@/api/client";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { updateFRPC } from "@/api/frp";
|
||||
import { useToast } from "./ui/use-toast";
|
||||
import { RespCode } from "@/lib/pb/common";
|
||||
import { Label } from '@radix-ui/react-label'
|
||||
import { Textarea } from './ui/textarea'
|
||||
import { FRPCFormProps } from './frpc_form'
|
||||
import { getClient } from '@/api/client'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from './ui/button'
|
||||
import { updateFRPC } from '@/api/frp'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
|
||||
export const FRPCEditor: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
|
||||
const { toast } = useToast()
|
||||
const { data: client, refetch: refetchClient } = useQuery({
|
||||
queryKey: ["getClient", clientID], queryFn: () => {
|
||||
queryKey: ['getClient', clientID],
|
||||
queryFn: () => {
|
||||
return getClient({ clientId: clientID })
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
const [configContent, setConfigContent] = useState<string>("{}")
|
||||
const updateFrpc = useMutation({ mutationFn: updateFRPC, })
|
||||
const [editorValue, setEditorValue] = useState<string>("")
|
||||
const [configContent, setConfigContent] = useState<string>('{}')
|
||||
const updateFrpc = useMutation({ mutationFn: updateFRPC })
|
||||
const [editorValue, setEditorValue] = useState<string>('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
let res = await updateFrpc.mutateAsync({
|
||||
clientId: clientID, config: Buffer.from(editorValue), serverId: serverID
|
||||
clientId: clientID,
|
||||
config: Buffer.from(editorValue),
|
||||
serverId: serverID,
|
||||
})
|
||||
if (res.status?.code !== RespCode.SUCCESS) {
|
||||
toast({ title: "更新失败" })
|
||||
toast({ title: '更新失败' })
|
||||
return
|
||||
}
|
||||
toast({ title: "更新成功" })
|
||||
toast({ title: '更新成功' })
|
||||
} catch (error) {
|
||||
toast({ title: "更新失败" })
|
||||
toast({ title: '更新失败' })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refetchClient()
|
||||
try {
|
||||
setConfigContent(JSON.stringify(JSON.parse(client?.client?.config == undefined ? "{}" ||
|
||||
client?.client?.config == "" : client?.client?.config), null, 2))
|
||||
setEditorValue(JSON.stringify(JSON.parse(client?.client?.config == undefined ||
|
||||
client?.client?.config == "" ? "{}" : client?.client?.config), null, 2))
|
||||
setConfigContent(
|
||||
JSON.stringify(
|
||||
JSON.parse(
|
||||
client?.client?.config == undefined ? '{}' || client?.client?.config == '' : client?.client?.config,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
setEditorValue(
|
||||
JSON.stringify(
|
||||
JSON.parse(
|
||||
client?.client?.config == undefined || client?.client?.config == '' ? '{}' : client?.client?.config,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
setConfigContent("{}")
|
||||
setEditorValue("{}")
|
||||
setConfigContent('{}')
|
||||
setEditorValue('{}')
|
||||
}
|
||||
}, [client, refetchClient])
|
||||
|
||||
return (<div className="grid w-full gap-1.5">
|
||||
return (
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label className="text-sm font-medium">客户端 {clientID} 配置文件`frpc.json`内容</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
只需要配置proxies和visitors字段,认证信息和服务器连接信息会由系统补全
|
||||
</p>
|
||||
<Textarea key={configContent} placeholder="配置文件内容" id="message"
|
||||
<Textarea
|
||||
key={configContent}
|
||||
placeholder="配置文件内容"
|
||||
id="message"
|
||||
defaultValue={configContent}
|
||||
onChange={(e) => setEditorValue(e.target.value)}
|
||||
className="h-72"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2 mt-1">
|
||||
<Button size="sm" onClick={handleSubmit} >提交</Button>
|
||||
<Button size="sm" onClick={handleSubmit}>
|
||||
提交
|
||||
</Button>
|
||||
{/* <Button variant="outline" size="sm" onClick={async () => {
|
||||
await refetchClient()
|
||||
setConfigContent(client?.client?.config == undefined ? "{}" : client?.client?.config)
|
||||
}}>加载服务端配置</Button> */}
|
||||
</div>
|
||||
</div>)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +1,24 @@
|
||||
import { ProxyType, TypedProxyConfig } from "@/types/proxy"
|
||||
import React, { useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Label } from "@radix-ui/react-label"
|
||||
import { HTTPProxyForm, TCPProxyForm, UDPProxyForm } from "./proxy_form"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getClient } from "@/api/client"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $clientProxyConfigs } from "@/store/proxy"
|
||||
import { Button } from "./ui/button"
|
||||
import { RespCode } from "@/lib/pb/common"
|
||||
import { ClientConfig } from "@/types/client"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
import { Input } from "./ui/input"
|
||||
import { AccordionHeader } from "@radix-ui/react-accordion"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { ProxyType, TypedProxyConfig } from '@/types/proxy'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Label } from '@radix-ui/react-label'
|
||||
import { HTTPProxyForm, TCPProxyForm, UDPProxyForm } from './proxy_form'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getClient } from '@/api/client'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $clientProxyConfigs } from '@/store/proxy'
|
||||
import { Button } from './ui/button'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
import { ClientConfig } from '@/types/client'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { Input } from './ui/input'
|
||||
import { AccordionHeader } from '@radix-ui/react-accordion'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { updateFRPC } from "@/api/frp"
|
||||
import { Card, CardContent } from "./ui/card"
|
||||
|
||||
import { updateFRPC } from '@/api/frp'
|
||||
import { Card, CardContent } from './ui/card'
|
||||
|
||||
export interface FRPCFormProps {
|
||||
clientID: string
|
||||
@@ -50,10 +34,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
|
||||
}
|
||||
|
||||
const { data: client, refetch: refetchClient } = useQuery({
|
||||
queryKey: ["getClient", clientID], queryFn: () => {
|
||||
queryKey: ['getClient', clientID],
|
||||
queryFn: () => {
|
||||
return getClient({ clientId: clientID })
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
const clientProxyConfigs = useStore($clientProxyConfigs)
|
||||
|
||||
@@ -66,11 +51,11 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
|
||||
}, [clientID, serverID, client])
|
||||
|
||||
const handleAddProxy = () => {
|
||||
console.log("add proxy", proxyName, proxyType)
|
||||
console.log('add proxy', proxyName, proxyType)
|
||||
if (!proxyName) return
|
||||
if (!proxyType) return
|
||||
if (clientProxyConfigs.findIndex(proxy => proxy.name === proxyName) !== -1) {
|
||||
toast({ title: "创建隧道状态", description: "名称重复" })
|
||||
if (clientProxyConfigs.findIndex((proxy) => proxy.name === proxyName) !== -1) {
|
||||
toast({ title: '创建隧道状态', description: '名称重复' })
|
||||
return
|
||||
}
|
||||
const newProxy = {
|
||||
@@ -81,23 +66,27 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
|
||||
}
|
||||
|
||||
const handleDeleteProxy = (proxyName: string) => {
|
||||
const newProxies = clientProxyConfigs.filter(proxy => proxy.name !== proxyName)
|
||||
const newProxies = clientProxyConfigs.filter((proxy) => proxy.name !== proxyName)
|
||||
$clientProxyConfigs.set(newProxies)
|
||||
}
|
||||
|
||||
const updateFrpc = useMutation({ mutationFn: updateFRPC, })
|
||||
const updateFrpc = useMutation({ mutationFn: updateFRPC })
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
const res = await updateFrpc.mutateAsync({
|
||||
config: Buffer.from(JSON.stringify({
|
||||
config: Buffer.from(
|
||||
JSON.stringify({
|
||||
proxies: clientProxyConfigs,
|
||||
} as ClientConfig)), serverId: serverID, clientId: clientID
|
||||
} as ClientConfig),
|
||||
),
|
||||
serverId: serverID,
|
||||
clientId: clientID,
|
||||
})
|
||||
toast({ title: "更新隧道状态", description: res.status?.code === RespCode.SUCCESS ? "更新成功" : "更新失败" })
|
||||
toast({ title: '更新隧道状态', description: res.status?.code === RespCode.SUCCESS ? '更新成功' : '更新失败' })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast({ title: "更新隧道状态", description: "更新失败" })
|
||||
toast({ title: '更新隧道状态', description: '更新失败' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +96,13 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="my-2">新增</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent >
|
||||
<PopoverContent>
|
||||
<Label className="text-sm font-medium">名称</Label>
|
||||
<Input onChange={(e) => { setProxyName(e.target.value) }} />
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
setProxyName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Select onValueChange={handleTypeChange} defaultValue={proxyType}>
|
||||
<Label className="text-sm font-medium">协议</Label>
|
||||
<SelectTrigger className="my-2">
|
||||
@@ -121,61 +114,73 @@ export const FRPCForm: React.FC<FRPCFormProps> = ({ clientID, serverID }) => {
|
||||
<SelectItem value="udp">udp</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant={"outline"} onClick={handleAddProxy}>确定</Button>
|
||||
<Button variant={'outline'} onClick={handleAddProxy}>
|
||||
确定
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4" key={clientID + serverID + client}>
|
||||
{
|
||||
clientProxyConfigs.map((item) => {
|
||||
return (<Card key={item.name}>
|
||||
{clientProxyConfigs.map((item) => {
|
||||
return (
|
||||
<Card key={item.name}>
|
||||
<CardContent>
|
||||
<div className="flex flex-col w-full pt-2">
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value={item.name}>
|
||||
<AccordionHeader className="flex flex-row justify-between">
|
||||
<div>隧道名称:{item.name}</div>
|
||||
<Button variant={"outline"} onClick={() => { handleDeleteProxy(item.name) }}>删除</Button>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
onClick={() => {
|
||||
handleDeleteProxy(item.name)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</AccordionHeader>
|
||||
<AccordionTrigger>类型:「{item.type}」</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{
|
||||
item.type === 'tcp' && serverID && clientID &&
|
||||
{item.type === 'tcp' && serverID && clientID && (
|
||||
<TCPProxyForm
|
||||
defaultProxyConfig={item}
|
||||
proxyName={item.name}
|
||||
serverID={serverID}
|
||||
clientID={clientID}
|
||||
/>
|
||||
}
|
||||
{
|
||||
item.type === 'udp' && serverID && clientID &&
|
||||
< UDPProxyForm
|
||||
)}
|
||||
{item.type === 'udp' && serverID && clientID && (
|
||||
<UDPProxyForm
|
||||
defaultProxyConfig={item}
|
||||
proxyName={item.name}
|
||||
serverID={serverID}
|
||||
clientID={clientID}
|
||||
/>
|
||||
}
|
||||
{
|
||||
item.type === 'http' && serverID && clientID &&
|
||||
< HTTPProxyForm
|
||||
)}
|
||||
{item.type === 'http' && serverID && clientID && (
|
||||
<HTTPProxyForm
|
||||
defaultProxyConfig={item}
|
||||
proxyName={item.name}
|
||||
serverID={serverID}
|
||||
clientID={clientID}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div >
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<Button className="mt-2" onClick={() => { handleUpdate() }}>提交</Button>
|
||||
<Button
|
||||
className="mt-2"
|
||||
onClick={() => {
|
||||
handleUpdate()
|
||||
}}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Label } from "./ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
import { getServer, listServer } from "@/api/server"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Switch } from "./ui/switch"
|
||||
import { FRPSEditor } from "./frps_editor"
|
||||
import FRPSForm from "./frps_form"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from './ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
|
||||
import { getServer, listServer } from '@/api/server'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Switch } from './ui/switch'
|
||||
import { FRPSEditor } from './frps_editor'
|
||||
import FRPSForm from './frps_form'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export interface FRPSFormCardProps {
|
||||
serverID?: string
|
||||
@@ -25,15 +18,17 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
|
||||
const searchParams = useSearchParams()
|
||||
const paramServerID = searchParams.get('serverID')
|
||||
const { data: serverList, refetch: refetchServers } = useQuery({
|
||||
queryKey: ["listServer"], queryFn: () => {
|
||||
queryKey: ['listServer'],
|
||||
queryFn: () => {
|
||||
return listServer({ page: 1, pageSize: 100 })
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
const { data: server, refetch: refetchServer } = useQuery({
|
||||
queryKey: ["getServer", serverID], queryFn: () => {
|
||||
queryKey: ['getServer', serverID],
|
||||
queryFn: () => {
|
||||
return getServer({ serverId: serverID })
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultServerID) {
|
||||
@@ -51,7 +46,8 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
|
||||
setServerID(value)
|
||||
}
|
||||
|
||||
return (<Card className="w-full">
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>服务端配置</CardTitle>
|
||||
<CardDescription>选择服务端以管理Frps服务</CardDescription>
|
||||
@@ -59,18 +55,15 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
|
||||
<CardContent>
|
||||
<div className=" flex items-center space-x-4 rounded-md border p-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
高级模式
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
编辑服务端原始配置文件
|
||||
</p>
|
||||
<p className="text-sm font-medium leading-none">高级模式</p>
|
||||
<p className="text-sm text-muted-foreground">编辑服务端原始配置文件</p>
|
||||
</div>
|
||||
<Switch onCheckedChange={setAdvanceMode} />
|
||||
</div>
|
||||
<div className="flex flex-col w-full pt-2">
|
||||
<Label className="text-sm font-medium">服务端</Label>
|
||||
<Select onValueChange={handleServerChange}
|
||||
<Select
|
||||
onValueChange={handleServerChange}
|
||||
value={serverID}
|
||||
onOpenChange={() => {
|
||||
refetchServers()
|
||||
@@ -80,17 +73,24 @@ export const FRPSFormCard: React.FC<FRPSFormCardProps> = ({ serverID: defaultSer
|
||||
<SelectTrigger className="my-2">
|
||||
<SelectValue placeholder="节点名称" />
|
||||
</SelectTrigger>
|
||||
<SelectContent >
|
||||
<SelectContent>
|
||||
{serverList?.servers.map(
|
||||
serverItem => serverItem.id && <SelectItem key={serverItem.id} value={serverItem.id}>{serverItem.id}
|
||||
</SelectItem>)}
|
||||
(serverItem) =>
|
||||
serverItem.id && (
|
||||
<SelectItem key={serverItem.id} value={serverItem.id}>
|
||||
{serverItem.id}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{serverID && server && server.server && !advanceMode && <FRPSForm serverID={serverID} server={server.server} />}
|
||||
{serverID && server && server.server && advanceMode && <FRPSEditor serverID={serverID} server={server.server} />}
|
||||
{serverID && server && server.server && advanceMode && (
|
||||
<FRPSEditor serverID={serverID} server={server.server} />
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
</CardFooter>
|
||||
</Card>)
|
||||
<CardFooter></CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,76 +1,104 @@
|
||||
import { Label } from "@radix-ui/react-label"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { FRPSFormProps } from "./frps_form"
|
||||
import { Button } from "./ui/button"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { getServer } from "@/api/server"
|
||||
import { useEffect, useState } from "react"
|
||||
import { updateFRPS } from "@/api/frp"
|
||||
import { RespCode } from "@/lib/pb/common"
|
||||
import { Label } from '@radix-ui/react-label'
|
||||
import { Textarea } from './ui/textarea'
|
||||
import { FRPSFormProps } from './frps_form'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { getServer } from '@/api/server'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { updateFRPS } from '@/api/frp'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
|
||||
export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => {
|
||||
const { toast } = useToast()
|
||||
const { data: serverResp, refetch: refetchServer } = useQuery({
|
||||
queryKey: ["getServer", serverID], queryFn: () => {
|
||||
queryKey: ['getServer', serverID],
|
||||
queryFn: () => {
|
||||
return getServer({ serverId: serverID })
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
const [configContent, setConfigContent] = useState<string>("{}")
|
||||
const updateFrps = useMutation({ mutationFn: updateFRPS, })
|
||||
const [editorValue, setEditorValue] = useState<string>("")
|
||||
const [serverComment, setServerComment] = useState<string>("")
|
||||
const [configContent, setConfigContent] = useState<string>('{}')
|
||||
const updateFrps = useMutation({ mutationFn: updateFRPS })
|
||||
const [editorValue, setEditorValue] = useState<string>('')
|
||||
const [serverComment, setServerComment] = useState<string>('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
let res = await updateFrps.mutateAsync({
|
||||
serverId: serverID, config: Buffer.from(editorValue),
|
||||
comment: serverComment
|
||||
serverId: serverID,
|
||||
config: Buffer.from(editorValue),
|
||||
comment: serverComment,
|
||||
})
|
||||
if (res.status?.code !== RespCode.SUCCESS) {
|
||||
toast({ title: "更新失败" })
|
||||
toast({ title: '更新失败' })
|
||||
return
|
||||
}
|
||||
toast({ title: "更新成功" })
|
||||
toast({ title: '更新成功' })
|
||||
} catch (error) {
|
||||
toast({ title: "更新失败" })
|
||||
toast({ title: '更新失败' })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refetchServer()
|
||||
try {
|
||||
setConfigContent(JSON.stringify(JSON.parse(serverResp?.server?.config == undefined ||
|
||||
serverResp?.server?.config == "" ? "{}" : serverResp?.server?.config), null, 2))
|
||||
setEditorValue(JSON.stringify(JSON.parse(serverResp?.server?.config == undefined ||
|
||||
serverResp?.server?.config == "" ? "{}" : serverResp?.server?.config), null, 2))
|
||||
setServerComment(serverResp?.server?.comment || "")
|
||||
setConfigContent(
|
||||
JSON.stringify(
|
||||
JSON.parse(
|
||||
serverResp?.server?.config == undefined || serverResp?.server?.config == ''
|
||||
? '{}'
|
||||
: serverResp?.server?.config,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
setEditorValue(
|
||||
JSON.stringify(
|
||||
JSON.parse(
|
||||
serverResp?.server?.config == undefined || serverResp?.server?.config == ''
|
||||
? '{}'
|
||||
: serverResp?.server?.config,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
setServerComment(serverResp?.server?.comment || '')
|
||||
} catch (error) {
|
||||
setConfigContent("{}")
|
||||
setEditorValue("{}")
|
||||
setServerComment("")
|
||||
setConfigContent('{}')
|
||||
setEditorValue('{}')
|
||||
setServerComment('')
|
||||
}
|
||||
}, [serverResp, refetchServer])
|
||||
|
||||
return (<div className="grid w-full gap-1.5">
|
||||
return (
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label className="text-sm font-medium">节点 {serverID} 的备注</Label>
|
||||
<Textarea key={serverResp?.server?.comment}
|
||||
placeholder="备注" id="message" defaultValue={serverResp?.server?.comment}
|
||||
<Textarea
|
||||
key={serverResp?.server?.comment}
|
||||
placeholder="备注"
|
||||
id="message"
|
||||
defaultValue={serverResp?.server?.comment}
|
||||
onChange={(e) => setServerComment(e.target.value)}
|
||||
className="h-12"
|
||||
/>
|
||||
<Label className="text-sm font-medium">节点 {serverID} 配置文件`frps.json`内容</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
只需要配置端口和IP等字段,认证信息会由系统补全
|
||||
</p>
|
||||
<Textarea key={configContent}
|
||||
placeholder="配置文件内容" id="message" defaultValue={configContent}
|
||||
<p className="text-sm text-muted-foreground">只需要配置端口和IP等字段,认证信息会由系统补全</p>
|
||||
<Textarea
|
||||
key={configContent}
|
||||
placeholder="配置文件内容"
|
||||
id="message"
|
||||
defaultValue={configContent}
|
||||
onChange={(e) => setEditorValue(e.target.value)}
|
||||
className="h-72"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2 mt-1">
|
||||
<Button size="sm" onClick={handleSubmit} >提交</Button>
|
||||
<Button size="sm" onClick={handleSubmit}>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +1,27 @@
|
||||
import { ServerConfig } from "@/types/server"
|
||||
import { useEffect, useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from "@/lib/consts"
|
||||
import { RespCode, Server } from "@/lib/pb/common"
|
||||
import { updateFRPS } from "@/api/frp"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { Label } from "@radix-ui/react-label"
|
||||
import { ServerConfig } from '@/types/server'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import * as z from 'zod'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from '@/lib/consts'
|
||||
import { RespCode, Server } from '@/lib/pb/common'
|
||||
import { updateFRPS } from '@/api/frp'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { Label } from '@radix-ui/react-label'
|
||||
|
||||
const ServerConfigSchema = z.object({
|
||||
bindAddr: ZodIPSchema.default("0.0.0.0").optional(),
|
||||
bindAddr: ZodIPSchema.default('0.0.0.0').optional(),
|
||||
bindPort: ZodPortSchema.default(7000),
|
||||
proxyBindAddr: ZodIPSchema.optional(),
|
||||
vhostHTTPPort: ZodPortSchema.optional(),
|
||||
subDomainHost: ZodStringSchema.optional(),
|
||||
});
|
||||
|
||||
export const ServerConfigZodSchema = ServerConfigSchema;
|
||||
})
|
||||
|
||||
export const ServerConfigZodSchema = ServerConfigSchema
|
||||
|
||||
export interface FRPSFormProps {
|
||||
serverID: string
|
||||
@@ -51,24 +43,27 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(JSON.parse(server?.config || "{}") as ServerConfig)
|
||||
form.reset(JSON.parse(server?.config || '{}') as ServerConfig)
|
||||
}, [server])
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof ServerConfigZodSchema>) => {
|
||||
setFrpsConfig({ ...values })
|
||||
try {
|
||||
let resp = await updateFrps.mutateAsync({
|
||||
serverId: serverID, config: Buffer.from(JSON.stringify({
|
||||
...values
|
||||
} as ServerConfig))
|
||||
serverId: serverID,
|
||||
config: Buffer.from(
|
||||
JSON.stringify({
|
||||
...values,
|
||||
} as ServerConfig),
|
||||
),
|
||||
})
|
||||
toast({
|
||||
title: resp.status?.code === RespCode.SUCCESS ? "创建成功" : "创建失败",
|
||||
title: resp.status?.code === RespCode.SUCCESS ? '创建成功' : '创建失败',
|
||||
description: resp.status?.message,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast({ title: "创建服务端状态", description: "创建失败" })
|
||||
toast({ title: '创建服务端状态', description: '创建失败' })
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -76,9 +71,10 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
|
||||
<Label className="text-sm font-medium">节点 {serverID} 的备注</Label>
|
||||
<p className="text-sm text-muted-foreground">可以到高级模式修改备注哦!</p>
|
||||
<p className="text-sm border rounded p-2 my-2">
|
||||
{server?.comment == undefined || server?.comment === "" ? "空空如也" : server?.comment}
|
||||
{server?.comment == undefined || server?.comment === '' ? '空空如也' : server?.comment}
|
||||
</p>
|
||||
{serverID && <Form {...form}>
|
||||
{serverID && (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -149,11 +145,10 @@ const FRPSForm: React.FC<FRPSFormProps> = ({ serverID, server }) => {
|
||||
/>
|
||||
<Button type="submit">提交</Button>
|
||||
</form>
|
||||
</Form>}
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FRPSForm
|
||||
@@ -1,28 +1,24 @@
|
||||
import { TbBuildingTunnel } from "react-icons/tb"
|
||||
import { Button } from "./ui/button"
|
||||
import { TbBuildingTunnel } from 'react-icons/tb'
|
||||
import { Button } from './ui/button'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { useRouter } from "next/router";
|
||||
import { $platformInfo, $userInfo } from "@/store/user";
|
||||
import { getUserInfo } from "@/api/user";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from 'next/router'
|
||||
import { $platformInfo, $userInfo } from '@/store/user'
|
||||
import { getUserInfo } from '@/api/user'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect } from 'react'
|
||||
import Gravatar from 'react-gravatar'
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar"
|
||||
import { LOCAL_STORAGE_TOKEN_KEY } from "@/lib/consts";
|
||||
import { logout } from "@/api/auth";
|
||||
import { getPlatformInfo } from "@/api/platform";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { LOCAL_STORAGE_TOKEN_KEY } from '@/lib/consts'
|
||||
import { logout } from '@/api/auth'
|
||||
import { getPlatformInfo } from '@/api/platform'
|
||||
|
||||
export const Header = () => {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
const userInfo = useStore($userInfo)
|
||||
|
||||
const platformInfo = useQuery({
|
||||
queryKey: ['platformInfo'],
|
||||
queryFn: getPlatformInfo
|
||||
queryFn: getPlatformInfo,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,8 +26,8 @@ export const Header = () => {
|
||||
}, [platformInfo])
|
||||
|
||||
const userInfoQuery = useQuery({
|
||||
queryKey: ["userInfo"],
|
||||
queryFn: getUserInfo
|
||||
queryKey: ['userInfo'],
|
||||
queryFn: getUserInfo,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,27 +35,48 @@ export const Header = () => {
|
||||
}, [userInfoQuery])
|
||||
|
||||
const redirToHome = () => {
|
||||
router.push("/")
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row h-10 items-center px-4 border-b">
|
||||
<TbBuildingTunnel />
|
||||
<p className="ml-2 font-mono" onClick={redirToHome}>frp-panel</p>
|
||||
{!userInfo && <Button variant={"ghost"} className="ml-auto" size={"sm"} onClick={() => router.push("/login")}>登录</Button>}
|
||||
{!userInfo && <Button variant={"ghost"} className="ml-2" size={"sm"} onClick={() => router.push("/register")}>注册</Button>}
|
||||
{userInfo && <Button variant={"ghost"} className="ml-auto" size={"sm"} onClick={async () => {
|
||||
<p className="ml-2 font-mono" onClick={redirToHome}>
|
||||
frp-panel
|
||||
</p>
|
||||
{!userInfo && (
|
||||
<Button variant={'ghost'} className="ml-auto" size={'sm'} onClick={() => router.push('/login')}>
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
{!userInfo && (
|
||||
<Button variant={'ghost'} className="ml-2" size={'sm'} onClick={() => router.push('/register')}>
|
||||
注册
|
||||
</Button>
|
||||
)}
|
||||
{userInfo && (
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="ml-auto"
|
||||
size={'sm'}
|
||||
onClick={async () => {
|
||||
$userInfo.set(undefined)
|
||||
localStorage.removeItem(LOCAL_STORAGE_TOKEN_KEY)
|
||||
await logout()
|
||||
window.location.reload()
|
||||
}}>退出</Button>}
|
||||
{userInfo && <Avatar className="ml-2 w-7 h-7">
|
||||
<AvatarImage alt={"@" + userInfo.userName} asChild>
|
||||
}}
|
||||
>
|
||||
退出
|
||||
</Button>
|
||||
)}
|
||||
{userInfo && (
|
||||
<Avatar className="ml-2 w-7 h-7">
|
||||
<AvatarImage alt={'@' + userInfo.userName} asChild>
|
||||
<Gravatar email={userInfo.email} />
|
||||
</AvatarImage>
|
||||
<AvatarFallback>{userInfo.userName}</AvatarFallback>
|
||||
</Avatar>}
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 }) => {
|
||||
export const RootLayout = ({
|
||||
children,
|
||||
header,
|
||||
sidebar,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
header: React.ReactNode
|
||||
sidebar?: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<main className={`${inter.className}`}>
|
||||
<div><Providers>{header}</Providers></div>
|
||||
<div>
|
||||
<Providers>{header}</Providers>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{sidebar}
|
||||
<div className="my-2 ml-0 mr-2 max-w-full w-full">
|
||||
{children}
|
||||
</div>
|
||||
<div className="my-2 ml-0 mr-2 max-w-full w-full">{children}</div>
|
||||
</div>
|
||||
<Toaster />
|
||||
</main >
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +1,23 @@
|
||||
import { ZodStringSchema } from "@/lib/consts"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "./ui/input"
|
||||
import { login } from "@/api/auth"
|
||||
import { Button } from "./ui/button"
|
||||
import { ZodStringSchema } from '@/lib/consts'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import * as z from 'zod'
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from './ui/input'
|
||||
import { login } from '@/api/auth'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
import { ExclamationTriangleIcon } from "@radix-ui/react-icons"
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert"
|
||||
import { useState } from "react"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { RespCode } from "@/lib/pb/common"
|
||||
import { useRouter } from "next/router"
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { useState } from 'react'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const LoginSchema = z.object({
|
||||
username: ZodStringSchema,
|
||||
password: ZodStringSchema
|
||||
password: ZodStringSchema,
|
||||
})
|
||||
|
||||
export const LoginComponent = () => {
|
||||
@@ -40,22 +30,22 @@ export const LoginComponent = () => {
|
||||
const [loginAlert, setLoginAlert] = useState(false)
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
|
||||
toast({ title: "登录中,请稍候" })
|
||||
toast({ title: '登录中,请稍候' })
|
||||
try {
|
||||
const res = await login({ ...values })
|
||||
if (res.status?.code === RespCode.SUCCESS) {
|
||||
toast({ title: "登录成功,正在跳转到首页" })
|
||||
toast({ title: '登录成功,正在跳转到首页' })
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
}, 3000);
|
||||
router.push('/')
|
||||
}, 3000)
|
||||
setLoginAlert(false)
|
||||
} else {
|
||||
toast({ title: "登录失败" })
|
||||
toast({ title: '登录失败' })
|
||||
setLoginAlert(true)
|
||||
}
|
||||
} catch (e) {
|
||||
toast({ title: "登录失败" })
|
||||
console.log("login error", e)
|
||||
toast({ title: '登录失败' })
|
||||
console.log('login error', e)
|
||||
setLoginAlert(true)
|
||||
}
|
||||
}
|
||||
@@ -63,21 +53,14 @@ export const LoginComponent = () => {
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
{...field}
|
||||
/>
|
||||
<Input type="text" placeholder="用户名" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -89,23 +72,19 @@ export const LoginComponent = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
{...field}
|
||||
/>
|
||||
<Input type="password" placeholder="密码" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{loginAlert && <Alert variant="destructive">
|
||||
{loginAlert && (
|
||||
<Alert variant="destructive">
|
||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>错误</AlertTitle>
|
||||
<AlertDescription>
|
||||
登录失败,请重试
|
||||
</AlertDescription>
|
||||
</Alert>}
|
||||
<AlertDescription>登录失败,请重试</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Button type="submit">登录</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb';
|
||||
import { useEffect } from 'react';
|
||||
import { $platformInfo } from '@/store/user';
|
||||
import { getPlatformInfo } from '@/api/platform';
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { TbDeviceHeartMonitor, TbEngine, TbEngineOff, TbServer2, TbServerBolt, TbServerOff } from 'react-icons/tb'
|
||||
import { useEffect } from 'react'
|
||||
import { $platformInfo } from '@/store/user'
|
||||
import { getPlatformInfo } from '@/api/platform'
|
||||
export const PlatformInfo = () => {
|
||||
const platformInfo = useQuery({
|
||||
queryKey: ['platformInfo'],
|
||||
queryFn: getPlatformInfo
|
||||
queryFn: getPlatformInfo,
|
||||
})
|
||||
useEffect(() => {
|
||||
$platformInfo.set(platformInfo.data)
|
||||
@@ -16,85 +16,73 @@ export const PlatformInfo = () => {
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex justify-between'>
|
||||
<h3 className='tracking-tight text-sm font-medium'>
|
||||
已配置服务端数
|
||||
</h3>
|
||||
<TbServerBolt className='mt-1' />
|
||||
<div className="flex justify-between">
|
||||
<h3 className="tracking-tight text-sm font-medium">已配置服务端数</h3>
|
||||
<TbServerBolt className="mt-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{platformInfo.data?.configuredServerCount} 个</div>
|
||||
<div className="text-2xl font-bold">{platformInfo.data?.configuredServerCount} 个</div>
|
||||
<p className="text-xs text-muted-foreground">请前往左侧🫲菜单修改</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex justify-between'>
|
||||
<h3 className='tracking-tight text-sm font-medium'>
|
||||
已配置客户端数
|
||||
</h3>
|
||||
<TbEngine className='mt-1' />
|
||||
<div className="flex justify-between">
|
||||
<h3 className="tracking-tight text-sm font-medium">已配置客户端数</h3>
|
||||
<TbEngine className="mt-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{platformInfo.data?.configuredClientCount} 个</div>
|
||||
<div className="text-2xl font-bold">{platformInfo.data?.configuredClientCount} 个</div>
|
||||
<p className="text-xs text-muted-foreground">请前往左侧🫲菜单修改</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex justify-between'>
|
||||
<h3 className='tracking-tight text-sm font-medium'>
|
||||
未配置服务端数
|
||||
</h3>
|
||||
<TbServerOff className='mt-1' />
|
||||
<div className="flex justify-between">
|
||||
<h3 className="tracking-tight text-sm font-medium">未配置服务端数</h3>
|
||||
<TbServerOff className="mt-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{platformInfo.data?.unconfiguredServerCount} 个</div>
|
||||
<div className="text-2xl font-bold">{platformInfo.data?.unconfiguredServerCount} 个</div>
|
||||
<p className="text-xs text-muted-foreground">请前往左侧🫲菜单修改</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex justify-between'>
|
||||
<h3 className='tracking-tight text-sm font-medium'>
|
||||
未配置客户端数
|
||||
</h3>
|
||||
<TbEngineOff className='mt-1' />
|
||||
<div className="flex justify-between">
|
||||
<h3 className="tracking-tight text-sm font-medium">未配置客户端数</h3>
|
||||
<TbEngineOff className="mt-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{platformInfo.data?.unconfiguredClientCount} 个</div>
|
||||
<div className="text-2xl font-bold">{platformInfo.data?.unconfiguredClientCount} 个</div>
|
||||
<p className="text-xs text-muted-foreground">请前往左侧🫲菜单修改</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex justify-between'>
|
||||
<h3 className='tracking-tight text-sm font-medium'>
|
||||
服务端总数
|
||||
</h3>
|
||||
<TbServer2 className='mt-1' />
|
||||
<div className="flex justify-between">
|
||||
<h3 className="tracking-tight text-sm font-medium">服务端总数</h3>
|
||||
<TbServer2 className="mt-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{platformInfo.data?.totalServerCount} 个</div>
|
||||
<div className="text-2xl font-bold">{platformInfo.data?.totalServerCount} 个</div>
|
||||
<p className="text-xs text-muted-foreground">请前往左侧🫲菜单修改</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex justify-between'>
|
||||
<h3 className='tracking-tight text-sm font-medium'>
|
||||
客户端总数
|
||||
</h3>
|
||||
<TbDeviceHeartMonitor className='mt-1' />
|
||||
<div className="flex justify-between">
|
||||
<h3 className="tracking-tight text-sm font-medium">客户端总数</h3>
|
||||
<TbDeviceHeartMonitor className="mt-1" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{platformInfo.data?.totalClientCount} 个</div>
|
||||
<div className="text-2xl font-bold">{platformInfo.data?.totalClientCount} 个</div>
|
||||
<p className="text-xs text-muted-foreground">请前往左侧🫲菜单修改</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import React from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import {
|
||||
TooltipProvider,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
import { HTTPProxyConfig, TCPProxyConfig, TypedProxyConfig, UDPProxyConfig } from "@/types/proxy"
|
||||
import * as z from "zod"
|
||||
import React from "react"
|
||||
import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from "@/lib/consts"
|
||||
import { useEffect, useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { $clientProxyConfigs } from "@/store/proxy"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { YesIcon } from "./ui/icon"
|
||||
import { Label } from "./ui/label"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getServer } from "@/api/server"
|
||||
import { ServerConfig } from "@/types/server"
|
||||
import { HTTPProxyConfig, TCPProxyConfig, TypedProxyConfig, UDPProxyConfig } from '@/types/proxy'
|
||||
import * as z from 'zod'
|
||||
import React from 'react'
|
||||
import { ZodIPSchema, ZodPortSchema, ZodStringSchema } from '@/lib/consts'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { $clientProxyConfigs } from '@/store/proxy'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { YesIcon } from './ui/icon'
|
||||
import { Label } from './ui/label'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getServer } from '@/api/server'
|
||||
import { ServerConfig } from '@/types/server'
|
||||
export const TCPConfigSchema = z.object({
|
||||
remotePort: ZodPortSchema,
|
||||
localIP: ZodIPSchema.default("127.0.0.1"),
|
||||
localIP: ZodIPSchema.default('127.0.0.1'),
|
||||
localPort: ZodPortSchema,
|
||||
})
|
||||
|
||||
export const UDPConfigSchema = z.object({
|
||||
remotePort: ZodPortSchema.optional(),
|
||||
localIP: ZodIPSchema.default("127.0.0.1"),
|
||||
localIP: ZodIPSchema.default('127.0.0.1'),
|
||||
localPort: ZodPortSchema,
|
||||
})
|
||||
|
||||
export const HTTPConfigSchema = z.object({
|
||||
localPort: ZodPortSchema,
|
||||
localIP: ZodIPSchema.default("127.0.0.1"),
|
||||
localIP: ZodIPSchema.default('127.0.0.1'),
|
||||
subDomain: ZodStringSchema,
|
||||
})
|
||||
|
||||
@@ -61,29 +54,30 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
const clientProxyConfigs = useStore($clientProxyConfigs)
|
||||
const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => {
|
||||
handleSave()
|
||||
setTCPConfig({ type: "tcp", ...values, name: proxyName })
|
||||
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => {
|
||||
setTCPConfig({ type: 'tcp', ...values, name: proxyName })
|
||||
const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
|
||||
if (proxyCfg.name === proxyName) {
|
||||
return { ...values, type: "tcp", name: proxyName } as TCPProxyConfig
|
||||
return { ...values, type: 'tcp', name: proxyName } as TCPProxyConfig
|
||||
}
|
||||
return proxyCfg
|
||||
})
|
||||
$clientProxyConfigs.set(newProxiyConfigs)
|
||||
}
|
||||
|
||||
const [isSaveDisabled, setSaveDisabled] = useState(false);
|
||||
const [isSaveDisabled, setSaveDisabled] = useState(false)
|
||||
|
||||
const handleSave = () => {
|
||||
setSaveDisabled(true);
|
||||
setSaveDisabled(true)
|
||||
setTimeout(() => {
|
||||
setSaveDisabled(false);
|
||||
}, 3000);
|
||||
};
|
||||
setSaveDisabled(false)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const { data: server } = useQuery({
|
||||
queryKey: ["getServer", serverID], queryFn: () => {
|
||||
queryKey: ['getServer', serverID],
|
||||
queryFn: () => {
|
||||
return getServer({ serverId: serverID })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -91,7 +85,8 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Label className="text-sm font-medium">访问方式</Label>
|
||||
<p className="text-sm border rounded p-2 my-2 font-mono overflow-auto">
|
||||
{`${server?.server?.ip}:${(defaultProxyConfig as TCPProxyConfig).remotePort
|
||||
{`${server?.server?.ip}:${
|
||||
(defaultProxyConfig as TCPProxyConfig).remotePort
|
||||
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
|
||||
</p>
|
||||
<FormField
|
||||
@@ -120,7 +115,7 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP}
|
||||
defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -136,12 +131,13 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
)}
|
||||
defaultValue={defaultProxyConfig === undefined ? 4321 : (defaultProxyConfig as TCPProxyConfig).remotePort}
|
||||
/>
|
||||
<Button type="submit" disabled={isSaveDisabled} variant={"outline"}>
|
||||
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? "" : "hidden"}`}></YesIcon>
|
||||
暂存修改</Button>
|
||||
<Button type="submit" disabled={isSaveDisabled} variant={'outline'}>
|
||||
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
|
||||
暂存修改
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
|
||||
@@ -155,33 +151,33 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
form.reset({})
|
||||
}, [])
|
||||
|
||||
|
||||
const clientProxyConfigs = useStore($clientProxyConfigs)
|
||||
const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => {
|
||||
handleSave()
|
||||
setUDPConfig({ type: "udp", ...values, name: proxyName })
|
||||
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => {
|
||||
setUDPConfig({ type: 'udp', ...values, name: proxyName })
|
||||
const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
|
||||
if (proxyCfg.name === proxyName) {
|
||||
return { ...values, type: "udp", name: proxyName } as UDPProxyConfig
|
||||
return { ...values, type: 'udp', name: proxyName } as UDPProxyConfig
|
||||
}
|
||||
return proxyCfg
|
||||
})
|
||||
$clientProxyConfigs.set(newProxiyConfigs)
|
||||
}
|
||||
|
||||
const [isSaveDisabled, setSaveDisabled] = useState(false);
|
||||
const [isSaveDisabled, setSaveDisabled] = useState(false)
|
||||
|
||||
const handleSave = () => {
|
||||
setSaveDisabled(true);
|
||||
setSaveDisabled(true)
|
||||
setTimeout(() => {
|
||||
setSaveDisabled(false);
|
||||
}, 3000);
|
||||
};
|
||||
setSaveDisabled(false)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const { data: server } = useQuery({
|
||||
queryKey: ["getServer", serverID], queryFn: () => {
|
||||
queryKey: ['getServer', serverID],
|
||||
queryFn: () => {
|
||||
return getServer({ serverId: serverID })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -189,7 +185,8 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Label className="text-sm font-medium">访问方式</Label>
|
||||
<p className="text-sm border rounded p-2 my-2 font-mono overflow-auto">
|
||||
{`${server?.server?.ip}:${(defaultProxyConfig as UDPProxyConfig).remotePort
|
||||
{`${server?.server?.ip}:${
|
||||
(defaultProxyConfig as UDPProxyConfig).remotePort
|
||||
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
|
||||
</p>
|
||||
<FormField
|
||||
@@ -218,7 +215,7 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP}
|
||||
defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -230,15 +227,17 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, def
|
||||
<Input type="number" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>)}
|
||||
</FormItem>
|
||||
)}
|
||||
defaultValue={defaultProxyConfig === undefined ? 4321 : (defaultProxyConfig as UDPProxyConfig).remotePort}
|
||||
/>
|
||||
<Button type="submit" disabled={isSaveDisabled} variant={"outline"}>
|
||||
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? "" : "hidden"}`}></YesIcon>
|
||||
暂存修改</Button>
|
||||
<Button type="submit" disabled={isSaveDisabled} variant={'outline'}>
|
||||
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
|
||||
暂存修改
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, defaultProxyConfig, proxyName }) => {
|
||||
@@ -256,29 +255,30 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
|
||||
const clientProxyConfigs = useStore($clientProxyConfigs)
|
||||
const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => {
|
||||
handleSave()
|
||||
setHTTPConfig({ ...values, type: "http", name: proxyName })
|
||||
const newProxiyConfigs = clientProxyConfigs.map(proxyCfg => {
|
||||
setHTTPConfig({ ...values, type: 'http', name: proxyName })
|
||||
const newProxiyConfigs = clientProxyConfigs.map((proxyCfg) => {
|
||||
if (proxyCfg.name === proxyName) {
|
||||
return { ...values, type: "http", name: proxyName } as HTTPProxyConfig
|
||||
return { ...values, type: 'http', name: proxyName } as HTTPProxyConfig
|
||||
}
|
||||
return proxyCfg
|
||||
})
|
||||
$clientProxyConfigs.set(newProxiyConfigs)
|
||||
}
|
||||
|
||||
const [isSaveDisabled, setSaveDisabled] = useState(false);
|
||||
const [isSaveDisabled, setSaveDisabled] = useState(false)
|
||||
|
||||
const handleSave = () => {
|
||||
setSaveDisabled(true);
|
||||
setSaveDisabled(true)
|
||||
setTimeout(() => {
|
||||
setSaveDisabled(false);
|
||||
}, 3000);
|
||||
};
|
||||
setSaveDisabled(false)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const { data: server } = useQuery({
|
||||
queryKey: ["getServer", serverID], queryFn: () => {
|
||||
queryKey: ['getServer', serverID],
|
||||
queryFn: () => {
|
||||
return getServer({ serverId: serverID })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -292,7 +292,8 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Label className="text-sm font-medium">访问方式</Label>
|
||||
<p className="text-sm border rounded p-2 my-2 font-mono overflow-auto">
|
||||
{`http://${(defaultProxyConfig as HTTPProxyConfig).subdomain}.${serverConfig?.subDomainHost}:${serverConfig?.vhostHTTPPort
|
||||
{`http://${(defaultProxyConfig as HTTPProxyConfig).subdomain}.${serverConfig?.subDomainHost}:${
|
||||
serverConfig?.vhostHTTPPort
|
||||
} -> ${defaultProxyConfig?.localIP}:${defaultProxyConfig?.localPort}`}
|
||||
</p>
|
||||
<FormField
|
||||
@@ -321,7 +322,7 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
defaultValue={defaultProxyConfig === undefined ? "127.0.0.1" : defaultProxyConfig.localIP}
|
||||
defaultValue={defaultProxyConfig === undefined ? '127.0.0.1' : defaultProxyConfig.localIP}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -333,12 +334,14 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({ serverID, clientID, de
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>)}
|
||||
defaultValue={defaultProxyConfig === undefined ? "" : (defaultProxyConfig as HTTPProxyConfig).subdomain}
|
||||
</FormItem>
|
||||
)}
|
||||
defaultValue={defaultProxyConfig === undefined ? '' : (defaultProxyConfig as HTTPProxyConfig).subdomain}
|
||||
/>
|
||||
<Button type="submit" disabled={isSaveDisabled} variant={"outline"}>
|
||||
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? "" : "hidden"}`}></YesIcon>
|
||||
暂存修改</Button>
|
||||
<Button type="submit" disabled={isSaveDisabled} variant={'outline'}>
|
||||
<YesIcon className={`mr-2 h-4 w-4 ${isSaveDisabled ? '' : 'hidden'}`}></YesIcon>
|
||||
暂存修改
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import { ZodEmailSchema, ZodStringSchema } from "@/lib/consts"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "./ui/input"
|
||||
import { register } from "@/api/auth"
|
||||
import { Button } from "./ui/button"
|
||||
import { ZodEmailSchema, ZodStringSchema } from '@/lib/consts'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import * as z from 'zod'
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from './ui/input'
|
||||
import { register } from '@/api/auth'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
import { ExclamationTriangleIcon } from "@radix-ui/react-icons"
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert"
|
||||
import { useState } from "react"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { RespCode } from "@/lib/pb/common"
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { useState } from 'react'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Toast } from "./ui/toast"
|
||||
import { Toast } from './ui/toast'
|
||||
|
||||
export const RegisterSchema = z.object({
|
||||
username: ZodStringSchema,
|
||||
@@ -41,25 +31,25 @@ export const RegisterComponent = () => {
|
||||
|
||||
const [registerAlert, setRegisterAlert] = useState(false)
|
||||
const sleep = async (ms: number): Promise<void> => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
|
||||
toast({ title: "注册中,请稍候" })
|
||||
toast({ title: '注册中,请稍候' })
|
||||
try {
|
||||
const res = await register({ ...values })
|
||||
if (res.status?.code === RespCode.SUCCESS) {
|
||||
toast({ title: "注册成功,正在跳转到登录" })
|
||||
toast({ title: '注册成功,正在跳转到登录' })
|
||||
setRegisterAlert(false)
|
||||
await sleep(3000)
|
||||
router.push("/login")
|
||||
router.push('/login')
|
||||
} else {
|
||||
toast({ title: "注册失败" })
|
||||
toast({ title: '注册失败' })
|
||||
setRegisterAlert(true)
|
||||
}
|
||||
} catch (e) {
|
||||
toast({ title: "注册失败" })
|
||||
console.log("register error", e)
|
||||
toast({ title: '注册失败' })
|
||||
console.log('register error', e)
|
||||
setRegisterAlert(true)
|
||||
}
|
||||
}
|
||||
@@ -67,21 +57,14 @@ export const RegisterComponent = () => {
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
{...field}
|
||||
/>
|
||||
<Input type="text" placeholder="用户名" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -93,11 +76,7 @@ export const RegisterComponent = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="邮箱地址"
|
||||
{...field}
|
||||
/>
|
||||
<Input type="email" placeholder="邮箱地址" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -109,23 +88,19 @@ export const RegisterComponent = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
{...field}
|
||||
/>
|
||||
<Input type="password" placeholder="密码" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{registerAlert && <Alert variant="destructive">
|
||||
{registerAlert && (
|
||||
<Alert variant="destructive">
|
||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>错误</AlertTitle>
|
||||
<AlertDescription>
|
||||
注册失败,请重试
|
||||
</AlertDescription>
|
||||
</Alert>}
|
||||
<AlertDescription>注册失败,请重试</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Button type="submit">注册</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from "react"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { initServer, listServer } from "@/api/server"
|
||||
import { Label } from "./ui/label"
|
||||
import { Input } from "./ui/input"
|
||||
import { Button } from "./ui/button"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import { RespCode } from "@/lib/pb/common"
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { initServer, listServer } from '@/api/server'
|
||||
import { Label } from './ui/label'
|
||||
import { Input } from './ui/input'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { RespCode } from '@/lib/pb/common'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -14,19 +14,19 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
export const CreateServerDialog = () => {
|
||||
const [serverID, setServerID] = useState<string | undefined>()
|
||||
const [serverIP, setServerIP] = useState<string | undefined>()
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ["listServer", { pageIndex: 0, pageSize: 10 }],
|
||||
queryKey: ['listServer', { pageIndex: 0, pageSize: 10 }],
|
||||
queryFn: async () => {
|
||||
return await listServer({
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
pageSize: 10,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
const newServer = useMutation({
|
||||
mutationFn: initServer,
|
||||
@@ -34,25 +34,26 @@ export const CreateServerDialog = () => {
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleNewServer = async () => {
|
||||
toast({ title: "已提交创建请求" })
|
||||
toast({ title: '已提交创建请求' })
|
||||
try {
|
||||
let resp = await newServer.mutateAsync({ serverId: serverID, serverIp: serverIP })
|
||||
if (resp.status?.code !== RespCode.SUCCESS) {
|
||||
toast({ title: "创建服务端失败" })
|
||||
toast({ title: '创建服务端失败' })
|
||||
return
|
||||
}
|
||||
toast({ title: "创建服务端成功" })
|
||||
toast({ title: '创建服务端成功' })
|
||||
dataQuery.refetch()
|
||||
} catch (error) {
|
||||
toast({ title: "创建服务端失败" })
|
||||
toast({ title: '创建服务端失败' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size={"sm"}>新建</Button>
|
||||
<Button variant="outline" size={'sm'}>
|
||||
新建
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ColumnDef, Table } from "@tanstack/react-table"
|
||||
import { MoreHorizontal } from "lucide-react"
|
||||
import { ColumnDef, Table } from '@tanstack/react-table'
|
||||
import { MoreHorizontal } from 'lucide-react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -19,24 +19,24 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import React, { useState } from "react"
|
||||
import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from "@/lib/consts"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { deleteServer, listServer } from "@/api/server"
|
||||
import { useRouter } from "next/router"
|
||||
import { getUserInfo } from "@/api/user"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $platformInfo } from "@/store/user"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
||||
import { getClientsStatus } from "@/api/platform"
|
||||
import { ClientType } from "@/lib/pb/common"
|
||||
import { ClientStatus, ClientStatus_Status } from "@/lib/pb/api_master"
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import React, { useState } from 'react'
|
||||
import { ExecCommandStr, LinuxInstallCommand, WindowsInstallCommand } from '@/lib/consts'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { deleteServer, listServer } from '@/api/server'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getUserInfo } from '@/api/user'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $platformInfo } from '@/store/user'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
|
||||
import { getClientsStatus } from '@/api/platform'
|
||||
import { ClientType } from '@/lib/pb/common'
|
||||
import { ClientStatus, ClientStatus_Status } from '@/lib/pb/api_master'
|
||||
|
||||
export type ServerTableSchema = {
|
||||
id: string,
|
||||
status: "invalid" | "valid"
|
||||
id: string
|
||||
status: 'invalid' | 'valid'
|
||||
secret: string
|
||||
info?: string
|
||||
ip: string
|
||||
@@ -45,66 +45,73 @@ export type ServerTableSchema = {
|
||||
|
||||
export const columns: ColumnDef<ServerTableSchema>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: ({ row }) => {
|
||||
return <ServerID server={row.original} />
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "是否配置",
|
||||
accessorKey: 'status',
|
||||
header: '是否配置',
|
||||
cell: ({ row }) => {
|
||||
const Server = row.original
|
||||
return <div className={`font-mono ${Server.status === "valid" ? "text-green-500" : "text-red-500"} min-w-12`}>{
|
||||
return (
|
||||
<div className={`font-mono ${Server.status === 'valid' ? 'text-green-500' : 'text-red-500'} min-w-12`}>
|
||||
{
|
||||
valid: "已配置",
|
||||
invalid: "未配置",
|
||||
{
|
||||
valid: '已配置',
|
||||
invalid: '未配置',
|
||||
}[Server.status]
|
||||
}</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "info",
|
||||
header: "运行信息",
|
||||
accessorKey: 'info',
|
||||
header: '运行信息',
|
||||
cell: ({ row }) => {
|
||||
const Server = row.original
|
||||
return <ServerInfo server={Server} />
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "ip",
|
||||
header: "IP",
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
cell: ({ row }) => {
|
||||
const Server = row.original
|
||||
return <div className="font-mono">{Server.ip}</div>
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "secret",
|
||||
header: "连接密钥",
|
||||
accessorKey: 'secret',
|
||||
header: '连接密钥',
|
||||
cell: ({ row }) => {
|
||||
const Server = row.original
|
||||
return <ServerSecret server={Server} />
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
id: 'action',
|
||||
cell: ({ row, table }) => {
|
||||
const Server = row.original
|
||||
return (<ServerActions server={Server} table={table} />)
|
||||
return <ServerActions server={Server} table={table} />
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const ServerID = ({ server }: { server: ServerTableSchema }) => {
|
||||
const platformInfo = useStore($platformInfo)
|
||||
return <Popover >
|
||||
<PopoverTrigger asChild><div className="font-mono">{server.id}</div></PopoverTrigger>
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="font-mono">{server.id}</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit overflow-auto max-w-72 max-h-72">
|
||||
<div>Linux安装到systemd</div>
|
||||
<div className="p-2 border rounded font-mono w-fit">
|
||||
{platformInfo === undefined ? "获取平台信息失败" : LinuxInstallCommand("server", server, platformInfo)}
|
||||
{platformInfo === undefined ? '获取平台信息失败' : LinuxInstallCommand('server', server, platformInfo)}
|
||||
</div>
|
||||
{/* <div>Windows</div>
|
||||
<div className="p-2 border rounded font-mono w-fit">
|
||||
@@ -112,69 +119,79 @@ export const ServerID = ({ server }: { server: ServerTableSchema }) => {
|
||||
</div> */}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export const ServerInfo = ({ server }: { server: ServerTableSchema }) => {
|
||||
const clientsInfo = useQuery({
|
||||
queryKey: ["getClientsStatus", [server.id]],
|
||||
queryKey: ['getClientsStatus', [server.id]],
|
||||
queryFn: async () => {
|
||||
return await getClientsStatus({
|
||||
clientIds: [server.id],
|
||||
clientType: ClientType.FRPS,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const trans = (info: ClientStatus | undefined) => {
|
||||
let statusText: "在线" | "离线" | "错误" | "未知" = "未知";
|
||||
let statusText: '在线' | '离线' | '错误' | '未知' = '未知'
|
||||
if (info === undefined) {
|
||||
return statusText;
|
||||
return statusText
|
||||
}
|
||||
if (info.status === ClientStatus_Status.ONLINE) {
|
||||
statusText = "在线";
|
||||
statusText = '在线'
|
||||
} else if (info.status === ClientStatus_Status.OFFLINE) {
|
||||
statusText = "离线";
|
||||
statusText = '离线'
|
||||
} else if (info.status === ClientStatus_Status.ERROR) {
|
||||
statusText = "错误";
|
||||
} return statusText;
|
||||
statusText = '错误'
|
||||
}
|
||||
return statusText
|
||||
}
|
||||
|
||||
const infoColor = clientsInfo.data?.clients[server.id]?.status === ClientStatus_Status.ONLINE ? "text-green-500" : "text-red-500"
|
||||
const infoColor =
|
||||
clientsInfo.data?.clients[server.id]?.status === ClientStatus_Status.ONLINE ? 'text-green-500' : 'text-red-500'
|
||||
|
||||
return <div className={`p-2 border rounded font-mono w-fit ${infoColor}`}>
|
||||
return (
|
||||
<div className={`p-2 border rounded font-mono w-fit ${infoColor}`}>
|
||||
{`${clientsInfo.data?.clients[server.id].ping}ms, ${trans(clientsInfo.data?.clients[server.id])}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ServerSecret = ({ server }: { server: ServerTableSchema }) => {
|
||||
const [showSecrect, setShowSecrect] = useState<boolean>(false)
|
||||
const fakeSecret = Array.from({ length: server.secret.length }).map(() => '*').join('')
|
||||
const fakeSecret = Array.from({ length: server.secret.length })
|
||||
.map(() => '*')
|
||||
.join('')
|
||||
const { toast } = useToast()
|
||||
const platformInfo = useStore($platformInfo)
|
||||
|
||||
return <Popover>
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
onMouseEnter={() => setShowSecrect(true)}
|
||||
onMouseLeave={() => setShowSecrect(false)}
|
||||
onClick={() => {
|
||||
if (platformInfo) {
|
||||
navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo));
|
||||
toast({ description: "复制成功", });
|
||||
navigator.clipboard.writeText(ExecCommandStr('server', server, platformInfo))
|
||||
toast({ description: '复制成功' })
|
||||
} else {
|
||||
toast({ description: "获取平台信息失败", });
|
||||
toast({ description: '获取平台信息失败' })
|
||||
}
|
||||
}}
|
||||
className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap">{
|
||||
showSecrect ? server.secret : fakeSecret
|
||||
}</div>
|
||||
className="font-medium hover:rounded hover:bg-slate-100 p-2 font-mono whitespace-nowrap"
|
||||
>
|
||||
{showSecrect ? server.secret : fakeSecret}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit overflow-auto max-w-48">
|
||||
<div className="p-2 border rounded font-mono w-fit">
|
||||
{platformInfo === undefined ? "获取平台信息失败" : ExecCommandStr("server", server, platformInfo)}
|
||||
{platformInfo === undefined ? '获取平台信息失败' : ExecCommandStr('server', server, platformInfo)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ServerItemProps {
|
||||
@@ -184,7 +201,7 @@ export interface ServerItemProps {
|
||||
|
||||
export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
|
||||
const { toast } = useToast()
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
const platformInfo = useStore($platformInfo)
|
||||
|
||||
const fetchDataOptions = {
|
||||
@@ -193,25 +210,26 @@ export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
|
||||
}
|
||||
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ["listServer", fetchDataOptions],
|
||||
queryKey: ['listServer', fetchDataOptions],
|
||||
queryFn: async () => {
|
||||
return await listServer({
|
||||
page: fetchDataOptions.pageIndex + 1,
|
||||
pageSize: fetchDataOptions.pageSize
|
||||
pageSize: fetchDataOptions.pageSize,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
const removeServer = useMutation({
|
||||
mutationFn: deleteServer,
|
||||
onSuccess: () => {
|
||||
toast({ description: "删除成功" })
|
||||
toast({ description: '删除成功' })
|
||||
dataQuery.refetch()
|
||||
},
|
||||
onError: () => {
|
||||
toast({ description: "删除失败" })
|
||||
}
|
||||
toast({ description: '删除失败' })
|
||||
},
|
||||
})
|
||||
return <Dialog>
|
||||
return (
|
||||
<Dialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -224,26 +242,28 @@ export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (platformInfo) {
|
||||
navigator.clipboard.writeText(ExecCommandStr("server", server, platformInfo));
|
||||
toast({ description: "复制成功,如果复制不成功,请点击ID字段手动复制", });
|
||||
navigator.clipboard.writeText(ExecCommandStr('server', server, platformInfo))
|
||||
toast({ description: '复制成功,如果复制不成功,请点击ID字段手动复制' })
|
||||
} else {
|
||||
toast({ description: "获取平台信息失败,如果复制不成功,请点击ID字段手动复制", });
|
||||
toast({ description: '获取平台信息失败,如果复制不成功,请点击ID字段手动复制' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制启动命令
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={
|
||||
() => {
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push({
|
||||
pathname: "/serveredit",
|
||||
pathname: '/serveredit',
|
||||
query: {
|
||||
serverID: server.id
|
||||
}
|
||||
serverID: server.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}>修改</DropdownMenuItem>
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</DropdownMenuItem>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem className="text-destructive">删除</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
@@ -254,14 +274,19 @@ export const ServerActions: React.FC<ServerItemProps> = ({ server, table }) => {
|
||||
<DialogTitle>确定删除该客户端?</DialogTitle>
|
||||
<DialogDescription>
|
||||
<p className="text-destructive">此操作无法撤消。您确定要永久从我们的服务器中删除该客户端?</p>
|
||||
<p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2">删除后运行中的客户端将无法通过现有参数再次连接,如果您需要删除客户端对外的连接,可以选择清空配置</p>
|
||||
<p className="text-gray-500 border-l-4 border-gray-500 pl-4 py-2">
|
||||
删除后运行中的客户端将无法通过现有参数再次连接,如果您需要删除客户端对外的连接,可以选择清空配置
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="submit" onClick={() => removeServer.mutate({ serverId: server.id })}>确定</Button>
|
||||
<Button type="submit" onClick={() => removeServer.mutate({ serverId: server.id })}>
|
||||
确定
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Server } from "@/lib/pb/common";
|
||||
import { ServerTableSchema, columns as serverColumnsDef } from "./server_item";
|
||||
import { DataTable } from "./data_table";
|
||||
import { Server } from '@/lib/pb/common'
|
||||
import { ServerTableSchema, columns as serverColumnsDef } from './server_item'
|
||||
import { DataTable } from './data_table'
|
||||
|
||||
import {
|
||||
getSortedRowModel,
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
PaginationState,
|
||||
} from "@tanstack/react-table"
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
import React from "react"
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { listServer } from "@/api/server";
|
||||
import React from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { listServer } from '@/api/server'
|
||||
|
||||
export interface ServerListProps {
|
||||
Servers: Server[]
|
||||
@@ -23,19 +23,18 @@ export interface ServerListProps {
|
||||
|
||||
export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const data = Servers.map((server) =>
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
const data = Servers.map(
|
||||
(server) =>
|
||||
({
|
||||
id: server.id == undefined ? "" : server.id,
|
||||
status: server.config == undefined || server.config == "" ? "invalid" : "valid",
|
||||
secret: server.secret == undefined ? "" : server.secret,
|
||||
config: server.config
|
||||
} as ServerTableSchema))
|
||||
id: server.id == undefined ? '' : server.id,
|
||||
status: server.config == undefined || server.config == '' ? 'invalid' : 'valid',
|
||||
secret: server.secret == undefined ? '' : server.secret,
|
||||
config: server.config,
|
||||
}) as ServerTableSchema,
|
||||
)
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] =
|
||||
React.useState<PaginationState>({
|
||||
const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
@@ -49,27 +48,30 @@ export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
[pageIndex, pageSize],
|
||||
)
|
||||
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ["listServer", fetchDataOptions],
|
||||
queryKey: ['listServer', fetchDataOptions],
|
||||
queryFn: async () => {
|
||||
return await listServer({ page: fetchDataOptions.pageIndex + 1, pageSize: fetchDataOptions.pageSize })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataQuery.data?.servers.map((server) => {
|
||||
data:
|
||||
dataQuery.data?.servers.map((server) => {
|
||||
return {
|
||||
id: server.id == undefined ? "" : server.id,
|
||||
status: server.config == undefined || server.config == "" ? "invalid" : "valid",
|
||||
secret: server.secret == undefined ? "" : server.secret,
|
||||
id: server.id == undefined ? '' : server.id,
|
||||
status: server.config == undefined || server.config == '' ? 'invalid' : 'valid',
|
||||
secret: server.secret == undefined ? '' : server.secret,
|
||||
ip: server.ip,
|
||||
config: server.config
|
||||
config: server.config,
|
||||
} as ServerTableSchema
|
||||
}) ?? data,
|
||||
pageCount: Math.ceil((dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0),
|
||||
pageCount: Math.ceil(
|
||||
(dataQuery.data?.total == undefined ? 0 : dataQuery.data?.total) / fetchDataOptions.pageSize ?? 0,
|
||||
),
|
||||
columns: serverColumnsDef,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
@@ -86,4 +88,4 @@ export const ServerList: React.FC<ServerListProps> = ({ Servers }) => {
|
||||
},
|
||||
})
|
||||
return <DataTable table={table} columns={serverColumnsDef} />
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "./ui/button"
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react'
|
||||
import { Button } from './ui/button'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export interface SideBarItem {
|
||||
id: string
|
||||
@@ -13,24 +13,71 @@ export interface SideBarProps {
|
||||
}
|
||||
|
||||
export const SideBar: React.FC<SideBarProps> = ({ items }) => {
|
||||
const router = useRouter();
|
||||
const defaultItems =
|
||||
[
|
||||
{ id: "clients", label: "客户端", eventHandler: () => { router.push("/clients") } },
|
||||
{ id: "servers", label: "服务端", eventHandler: () => { router.push("/servers") } },
|
||||
{ id: "clientedit", label: "编辑隧道", eventHandler: () => { router.push("/clientedit") } },
|
||||
{ id: "serveredit", label: "编辑端点", eventHandler: () => { router.push("/serveredit") } },
|
||||
const router = useRouter()
|
||||
const defaultItems = [
|
||||
{
|
||||
id: 'clients',
|
||||
label: '客户端',
|
||||
eventHandler: () => {
|
||||
router.push('/clients')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'servers',
|
||||
label: '服务端',
|
||||
eventHandler: () => {
|
||||
router.push('/servers')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'clientedit',
|
||||
label: '编辑隧道',
|
||||
eventHandler: () => {
|
||||
router.push('/clientedit')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'serveredit',
|
||||
label: '编辑端点',
|
||||
eventHandler: () => {
|
||||
router.push('/serveredit')
|
||||
},
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="w-48 h-full grid grid-cols-1 mt-1 min-w-24">
|
||||
{items && items.map(item => <Button className={`mx-2 my-1 justify-start ${router.pathname.includes(item.id) ?
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground" : ""}`} variant={"ghost"}
|
||||
size={"sm"} key={item.id}
|
||||
onClick={item.eventHandler}>{item.label}</Button>)}
|
||||
{!items && defaultItems.map(item => <Button className={`mx-2 my-1 justify-start ${router.pathname.includes(item.id) ?
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground" : ""}`} variant={"ghost"}
|
||||
size={"sm"} key={item.id}
|
||||
onClick={item.eventHandler}>{item.label}</Button>)}
|
||||
{items &&
|
||||
items.map((item) => (
|
||||
<Button
|
||||
className={`mx-2 my-1 justify-start ${
|
||||
router.pathname.includes(item.id)
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground'
|
||||
: ''
|
||||
}`}
|
||||
variant={'ghost'}
|
||||
size={'sm'}
|
||||
key={item.id}
|
||||
onClick={item.eventHandler}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
{!items &&
|
||||
defaultItems.map((item) => (
|
||||
<Button
|
||||
className={`mx-2 my-1 justify-start ${
|
||||
router.pathname.includes(item.id)
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground'
|
||||
: ''
|
||||
}`}
|
||||
variant={'ghost'}
|
||||
size={'sm'}
|
||||
key={item.id}
|
||||
onClick={item.eventHandler}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
@@ -47,7 +38,7 @@ const useFormField = () => {
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
@@ -66,23 +57,20 @@ type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
},
|
||||
)
|
||||
FormItem.displayName = 'FormItem'
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
@@ -90,60 +78,40 @@ const FormLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <Label ref={ref} className={cn(error && 'text-destructive', className)} htmlFor={formItemId} {...props} />
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
|
||||
({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
},
|
||||
)
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
<p ref={ref} id={formDescriptionId} className={cn('text-[0.8rem] text-muted-foreground', className)} {...props} />
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
},
|
||||
)
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
@@ -155,22 +123,14 @@ const FormMessage = React.forwardRef<
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
className={cn('text-[0.8rem] font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
},
|
||||
)
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
})
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from 'react'
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
className={cn('shrink-0 bg-border', orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,120 +1,76 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
),
|
||||
)
|
||||
Table.displayName = 'Table'
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />,
|
||||
)
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
),
|
||||
)
|
||||
TableBody.displayName = 'TableBody'
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
|
||||
),
|
||||
)
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
),
|
||||
)
|
||||
TableRow.displayName = 'TableRow'
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
),
|
||||
)
|
||||
TableHead.displayName = 'TableHead'
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
),
|
||||
)
|
||||
TableCell.displayName = 'TableCell'
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
|
||||
),
|
||||
)
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
})
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export { Textarea }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
import * as z from "zod"
|
||||
import { Client, Server } from "./pb/common"
|
||||
import { GetPlatformInfoResponse } from "./pb/api_user"
|
||||
import * as z from 'zod'
|
||||
import { Client, Server } from './pb/common'
|
||||
import { GetPlatformInfoResponse } from './pb/api_user'
|
||||
|
||||
export const API_PATH = '/api/v1'
|
||||
export const SET_TOKEN_HEADER = 'x-set-authorization'
|
||||
export const X_CLIENT_REQUEST_ID = 'x-client-request-id'
|
||||
export const LOCAL_STORAGE_TOKEN_KEY = 'token'
|
||||
export const ZodPortSchema = z.coerce.number().min(1, {
|
||||
message: "端口号不能小于 1",
|
||||
}).max(65535, { message: "端口号不能大于 65535" })
|
||||
export const ZodIPSchema = z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
{ message: "请输入正确的IP地址" })
|
||||
export const ZodStringSchema = z.string().min(1,
|
||||
{ message: "不能为空" })
|
||||
export const ZodEmailSchema = z.string()
|
||||
.min(1, { message: "不能为空" })
|
||||
.email("是不是输错了邮箱地址呢?")
|
||||
export const ZodPortSchema = z.coerce
|
||||
.number()
|
||||
.min(1, {
|
||||
message: '端口号不能小于 1',
|
||||
})
|
||||
.max(65535, { message: '端口号不能大于 65535' })
|
||||
export const ZodIPSchema = z.string().regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, { message: '请输入正确的IP地址' })
|
||||
export const ZodStringSchema = z.string().min(1, { message: '不能为空' })
|
||||
export const ZodEmailSchema = z.string().min(1, { message: '不能为空' }).email('是不是输错了邮箱地址呢?')
|
||||
// .refine((e) => e === "abcd@fg.com", "This email is not in our database")
|
||||
|
||||
export const ExecCommandStr = <T extends Client | Server>(type: string, item: T, info: GetPlatformInfoResponse, fileName?: string) => {
|
||||
return `${fileName || 'frp-panel'} ${type
|
||||
} -s ${item.secret
|
||||
} -i ${item.id
|
||||
} -a ${info.globalSecret
|
||||
} -r ${info.masterRpcHost
|
||||
} -c ${info.masterRpcPort
|
||||
} -p ${info.masterApiPort
|
||||
} -e ${info.masterApiScheme
|
||||
}`
|
||||
export const ExecCommandStr = <T extends Client | Server>(
|
||||
type: string,
|
||||
item: T,
|
||||
info: GetPlatformInfoResponse,
|
||||
fileName?: string,
|
||||
) => {
|
||||
return `${fileName || 'frp-panel'} ${type} -s ${item.secret} -i ${item.id} -a ${info.globalSecret} -r ${
|
||||
info.masterRpcHost
|
||||
} -c ${info.masterRpcPort} -p ${info.masterApiPort} -e ${info.masterApiScheme}`
|
||||
}
|
||||
|
||||
export const WindowsInstallCommand = <T extends Client | Server>(type: string, item: T, info: GetPlatformInfoResponse) => {
|
||||
export const WindowsInstallCommand = <T extends Client | Server>(
|
||||
type: string,
|
||||
item: T,
|
||||
info: GetPlatformInfoResponse,
|
||||
) => {
|
||||
return `Invoke-WebRequest -Uri 'https://github.com/your_repository/frp-panel/releases/latest/download/frp-panel-amd64.exe' -OutFile 'frp-panel.exe'
|
||||
Move-Item .\\frp-panel.exe C:\\Tools\\frp-panel.exe
|
||||
$command = "C:\\Tools\\${ExecCommandStr(type, item, info, 'frp-panel.exe')}"
|
||||
@@ -38,6 +40,10 @@ export const WindowsInstallCommand = <T extends Client | Server>(type: string, i
|
||||
New-Service -Name 'FRPPanel' -BinaryPathName 'C:\\Tools\\frp-panel.exe' -StartupType Automatic | Start-Service`
|
||||
}
|
||||
|
||||
export const LinuxInstallCommand = <T extends Client | Server>(type: string, item: T, info: GetPlatformInfoResponse) => {
|
||||
return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, " ")}`
|
||||
export const LinuxInstallCommand = <T extends Client | Server>(
|
||||
type: string,
|
||||
item: T,
|
||||
info: GetPlatformInfoResponse,
|
||||
) => {
|
||||
return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}`
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Providers } from '@/components/providers';
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { Header } from '@/components/header';
|
||||
import { SideBar } from '@/components/sidebar';
|
||||
import { FRPCFormCard } from '@/components/frpc_card';
|
||||
import { Providers } from '@/components/providers'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
import { Header } from '@/components/header'
|
||||
import { SideBar } from '@/components/sidebar'
|
||||
import { FRPCFormCard } from '@/components/frpc_card'
|
||||
|
||||
export default function ClientEditPage() {
|
||||
return (
|
||||
<RootLayout header={<Header />} sidebar={<SideBar />}>
|
||||
<Providers>
|
||||
<div className='w-full'>
|
||||
<div className='flex-1 flex-col'>
|
||||
<div className="w-full">
|
||||
<div className="flex-1 flex-col">
|
||||
<FRPCFormCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Providers } from '@/components/providers';
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { ClientList } from '@/components/client_list';
|
||||
import { Header } from '@/components/header';
|
||||
import { SideBar } from '@/components/sidebar';
|
||||
import { CreateClientDialog } from '@/components/client_create_dialog';
|
||||
import { Providers } from '@/components/providers'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
import { ClientList } from '@/components/client_list'
|
||||
import { Header } from '@/components/header'
|
||||
import { SideBar } from '@/components/sidebar'
|
||||
import { CreateClientDialog } from '@/components/client_create_dialog'
|
||||
|
||||
export default function ClientListPage() {
|
||||
return (
|
||||
<RootLayout header={<Header />} sidebar={<SideBar />}>
|
||||
<Providers>
|
||||
<div className='w-full'>
|
||||
<div className='flex-1 flex-col'>
|
||||
<div className="w-full">
|
||||
<div className="flex-1 flex-col">
|
||||
<div className="flex-1 flex-row mb-2">
|
||||
<CreateClientDialog />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,53 +1,58 @@
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Providers } from '@/components/providers';
|
||||
import { TbBuildingTunnel } from "react-icons/tb";
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { LoginComponent } from '@/components/login';
|
||||
import { Header } from '@/components/header';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import { Providers } from '@/components/providers'
|
||||
import { TbBuildingTunnel } from 'react-icons/tb'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
import { LoginComponent } from '@/components/login'
|
||||
import { Header } from '@/components/header'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<main
|
||||
className={`${inter.className}`}
|
||||
>
|
||||
<main className={`${inter.className}`}>
|
||||
<Providers>
|
||||
<div className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
|
||||
<div
|
||||
className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
<div className='flex rounded hover:bg-slate-100 p-2'>
|
||||
<div className="flex rounded hover:bg-slate-100 p-2">
|
||||
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
|
||||
FRP Panel
|
||||
</div>
|
||||
</div>
|
||||
<div className='container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0'>
|
||||
<div className='relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r'>
|
||||
<div className='absolute inset-0 bg-zinc-900'></div>
|
||||
<div className="relative flex items-center text-lg font-medium"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
<div className='flex rounded hover:bg-zinc-800 p-2'>
|
||||
<div className="container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-zinc-900"></div>
|
||||
<div className="relative flex items-center text-lg font-medium" onClick={() => router.push('/')}>
|
||||
<div className="flex rounded hover:bg-zinc-800 p-2">
|
||||
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
|
||||
FRP Panel
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">A multi node frp webui and for <a href='https://github.com/fatedier/frp'>[FRP]</a> server and client management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source alternative
|
||||
</p><footer className="text-sm">navigate to: <a href='https://github.com/VaalaCat/frp-panel'>VaalaCat/frp-panel</a></footer></blockquote></div>
|
||||
<p className="text-lg">
|
||||
A multi node frp webui and for <a href="https://github.com/fatedier/frp">[FRP]</a> server and client
|
||||
management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source
|
||||
alternative
|
||||
</p>
|
||||
<footer className="text-sm">
|
||||
navigate to: <a href="https://github.com/VaalaCat/frp-panel">VaalaCat/frp-panel</a>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div className='lg:p-8 justify-center w-[300px]'>
|
||||
<div className='flex flex-col justify-center space-y-6 w-[300px]'>
|
||||
</div>
|
||||
<div className="lg:p-8 justify-center w-[300px]">
|
||||
<div className="flex flex-col justify-center space-y-6 w-[300px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">登录</h1>
|
||||
<p className="text-sm text-muted-foreground">输入您的账号信息</p>
|
||||
</div>
|
||||
<div className='w-full justify-center'>
|
||||
<div className='w-[300px]'>
|
||||
<div className="w-full justify-center">
|
||||
<div className="w-[300px]">
|
||||
<LoginComponent />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +1,56 @@
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Providers } from '@/components/providers';
|
||||
import { TbBuildingTunnel } from "react-icons/tb";
|
||||
import { RegisterComponent } from '@/components/register';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import { Providers } from '@/components/providers'
|
||||
import { TbBuildingTunnel } from 'react-icons/tb'
|
||||
import { RegisterComponent } from '@/components/register'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<main
|
||||
className={`${inter.className}`}
|
||||
>
|
||||
<main className={`${inter.className}`}>
|
||||
<Providers>
|
||||
<div className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
|
||||
<div
|
||||
className="absolute text-lg font-medium left-1/2 transform -translate-x-1/2 mt-3 lg:hidden"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
<div className='flex rounded hover:bg-slate-100 p-2'>
|
||||
<div className="flex rounded hover:bg-slate-100 p-2">
|
||||
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
|
||||
FRP Panel
|
||||
</div>
|
||||
</div>
|
||||
<div className='container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0'>
|
||||
<div className='relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r'>
|
||||
<div className='absolute inset-0 bg-zinc-900'></div>
|
||||
<div className="relative flex items-center text-lg font-medium"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
<div className='flex rounded hover:bg-zinc-800 p-2'>
|
||||
<div className="container h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-zinc-900"></div>
|
||||
<div className="relative flex items-center text-lg font-medium" onClick={() => router.push('/')}>
|
||||
<div className="flex rounded hover:bg-zinc-800 p-2">
|
||||
<TbBuildingTunnel className="mr-2 h-8 w-8 pb-1" />
|
||||
FRP Panel
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">A multi node frp webui and for <a href='https://github.com/fatedier/frp'>[FRP]</a> server and client management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source alternative
|
||||
</p><footer className="text-sm">navigate to: <a href='https://github.com/VaalaCat/frp-panel'>VaalaCat/frp-panel</a></footer></blockquote></div>
|
||||
<p className="text-lg">
|
||||
A multi node frp webui and for <a href="https://github.com/fatedier/frp">[FRP]</a> server and client
|
||||
management, which makes this project a [Cloudflare Tunnel] or [Tailscale Funnel] open source
|
||||
alternative
|
||||
</p>
|
||||
<footer className="text-sm">
|
||||
navigate to: <a href="https://github.com/VaalaCat/frp-panel">VaalaCat/frp-panel</a>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div className='lg:p-8 justify-center w-[300px]'>
|
||||
<div className='flex flex-col justify-center space-y-6 w-[300px]'>
|
||||
</div>
|
||||
<div className="lg:p-8 justify-center w-[300px]">
|
||||
<div className="flex flex-col justify-center space-y-6 w-[300px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">注册</h1>
|
||||
<p className="text-sm text-muted-foreground">输入您的账号信息</p>
|
||||
</div>
|
||||
<div className='w-full justify-center'>
|
||||
<div className='w-[300px]'>
|
||||
<div className="w-full justify-center">
|
||||
<div className="w-[300px]">
|
||||
<RegisterComponent />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Providers } from '@/components/providers';
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { Header } from '@/components/header';
|
||||
import { SideBar } from '@/components/sidebar';
|
||||
import { FRPSFormCard } from '@/components/frps_card';
|
||||
import { Providers } from '@/components/providers'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
import { Header } from '@/components/header'
|
||||
import { SideBar } from '@/components/sidebar'
|
||||
import { FRPSFormCard } from '@/components/frps_card'
|
||||
|
||||
export default function ServerListPage() {
|
||||
return (
|
||||
<RootLayout header={<Header />} sidebar={<SideBar />}>
|
||||
<Providers>
|
||||
<div className='w-full'>
|
||||
<div className='flex-1 flex-col'>
|
||||
<div className="w-full">
|
||||
<div className="flex-1 flex-col">
|
||||
<FRPSFormCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Providers } from '@/components/providers';
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { ServerList } from '@/components/server_list';
|
||||
import { Header } from '@/components/header';
|
||||
import { SideBar } from '@/components/sidebar';
|
||||
import { CreateServerDialog } from '@/components/server_create_dialog';
|
||||
|
||||
import { Providers } from '@/components/providers'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
import { ServerList } from '@/components/server_list'
|
||||
import { Header } from '@/components/header'
|
||||
import { SideBar } from '@/components/sidebar'
|
||||
import { CreateServerDialog } from '@/components/server_create_dialog'
|
||||
|
||||
export default function ServerListPage() {
|
||||
return (
|
||||
<RootLayout header={<Header />} sidebar={<SideBar />}>
|
||||
<Providers>
|
||||
<div className='w-full'>
|
||||
<div className='flex-1 flex-col'>
|
||||
<div className="w-full">
|
||||
<div className="flex-1 flex-col">
|
||||
<div className="flex-1 flex-row mb-2">
|
||||
<CreateServerDialog />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { FRPCFormCard } from '@/components/frpc_card';
|
||||
import { Providers } from '@/components/providers';
|
||||
import { APITest } from '@/components/apitest';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { FRPSFormCard } from '@/components/frps_card';
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { Header } from '@/components/header';
|
||||
import { SideBar } from '@/components/sidebar';
|
||||
|
||||
import { FRPCFormCard } from '@/components/frpc_card'
|
||||
import { Providers } from '@/components/providers'
|
||||
import { APITest } from '@/components/apitest'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { FRPSFormCard } from '@/components/frps_card'
|
||||
import { RootLayout } from '@/components/layout'
|
||||
import { Header } from '@/components/header'
|
||||
import { SideBar } from '@/components/sidebar'
|
||||
|
||||
export default function Test() {
|
||||
return (
|
||||
|
||||
3172
www/pnpm-lock.yaml
generated
3172
www/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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[]>([])
|
||||
@@ -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')],
|
||||
}
|
||||
@@ -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))',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
"@/*": ["./*"],
|
||||
},
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules"],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user