forked from Mxmilu666/frp
feat: paginate dashboard clients and proxies via API v2 (#5354)
Move the frps dashboard Clients and Proxies views to the paginated /api/v2/clients and /api/v2/proxies endpoints instead of fetching all data at once, and extend server-side proxy search so the search box keeps working under pagination. Frontend: - Add V2Envelope/V2Page types and getV2 HTTP helper to api/http.ts - Add v2 paginated fetch functions to api/client.ts and api/proxy.ts - Add ClientV2Info and ProxyV2Info types for v2 API responses - Rewrite Clients.vue with server-side pagination, status/user search filtering, and ElPagination component - Rewrite Proxies.vue with server-side pagination, type tabs, client dropdown filter, and a search box that passes q to the API - Default page size 10, selectable sizes [10, 20, 50, 100] Backend: - Extend /api/v2/proxies q matching to also cover online proxy spec fields: TCP/UDP remotePort and HTTP/HTTPS/TCPMux customDomains and subdomain, so dashboard search no longer needs to scan every page - Add controller_v2 tests for the new spec-field matching
This commit is contained in:
@@ -11,6 +11,21 @@ class HTTPError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export interface V2Envelope<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface V2Page<T> {
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
items: T[]
|
||||
}
|
||||
|
||||
type QueryParamValue = string | number | boolean | null | undefined
|
||||
|
||||
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const defaultOptions: RequestInit = {
|
||||
credentials: 'include',
|
||||
@@ -34,9 +49,55 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function requestV2<T>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const defaultOptions: RequestInit = {
|
||||
credentials: 'include',
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...defaultOptions, ...options })
|
||||
const envelope = (await response.json().catch(() => null)) as
|
||||
| V2Envelope<T>
|
||||
| null
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HTTPError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
envelope?.msg || `HTTP ${response.status}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!envelope || typeof envelope.code !== 'number') {
|
||||
throw new Error('Invalid API v2 response')
|
||||
}
|
||||
|
||||
if (envelope.code >= 400) {
|
||||
throw new HTTPError(envelope.code, envelope.msg, envelope.msg)
|
||||
}
|
||||
|
||||
return envelope.data
|
||||
}
|
||||
|
||||
export const buildQueryString = (
|
||||
params: Record<string, QueryParamValue>,
|
||||
): string => {
|
||||
const query = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === null || value === undefined) continue
|
||||
query.append(key, String(value))
|
||||
}
|
||||
const text = query.toString()
|
||||
return text ? `?${text}` : ''
|
||||
}
|
||||
|
||||
export const http = {
|
||||
get: <T>(url: string, options?: RequestInit) =>
|
||||
request<T>(url, { ...options, method: 'GET' }),
|
||||
getV2: <T>(url: string, options?: RequestInit) =>
|
||||
requestV2<T>(url, { ...options, method: 'GET' }),
|
||||
post: <T>(url: string, body?: any, options?: RequestInit) =>
|
||||
request<T>(url, {
|
||||
...options,
|
||||
|
||||
Reference in New Issue
Block a user