Add admin dashboard filtering and sorting
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled

Provide stable server-side list queries and focused chart controls so operators can inspect large datasets without misleading partial-page ordering.
This commit is contained in:
Rocky
2026-08-20 18:05:49 +08:00
parent 034a3e8745
commit 74c3fcd45f
39 changed files with 2990 additions and 431 deletions
+41 -19
View File
@@ -4,16 +4,21 @@ import type {
AdminOperatorProvisioning,
AdminSecuritySummary,
AdminLoginResponse,
AuditQuery,
AuditLogEntry,
CreditGrantRequest,
CreditGrantResponse,
LedgerQuery,
LedgerEntry,
OperatorsQuery,
Overview,
PageResult,
ProductAnalyticsOverview,
ReferralsQuery,
ReferralOverview,
SessionResponse,
UserDetail,
UsersQuery,
UserSummary,
} from "./types";
@@ -140,15 +145,21 @@ async function request<T>(
}
}
function query(params: Record<string, string | undefined>): string {
export function encodeQuery<T extends object>(params: T): string {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value) search.set(key, value);
if (value !== undefined && value !== "") search.set(key, String(value));
});
const result = search.toString();
return result ? `?${result}` : "";
}
function cursorQuery<T extends CursorQuery>(value?: string | T): CursorQuery | T {
return typeof value === "string" ? { cursor: value } : (value ?? {});
}
type CursorQuery = { cursor?: string };
export const adminApi = {
session: () => request<SessionResponse>("/auth/session"),
@@ -161,28 +172,35 @@ export const adminApi = {
logout: () => request<void>("/auth/logout", { method: "POST" }),
overview: (range: string) =>
request<Overview>(`/overview${query({ range })}`),
request<Overview>(`/overview${encodeQuery({ range })}`),
referrals: (range: string) =>
request<ReferralOverview>(`/referrals${query({ range })}`),
referrals: (value: string | ReferralsQuery) => {
const params = typeof value === "string" ? { range: value } : value;
return request<ReferralOverview>(`/referrals${encodeQuery(params)}`);
},
productAnalytics: (range: string) =>
request<ProductAnalyticsOverview>(`/analytics${query({ range })}`),
request<ProductAnalyticsOverview>(`/analytics${encodeQuery({ range })}`),
users: (search = "", cursor?: string) =>
request<PageResult<UserSummary>>(
`/users${query({ q: search.trim(), cursor })}`,
),
users: (value: string | UsersQuery = "", legacyCursor?: string) => {
const params =
typeof value === "string"
? { q: value.trim(), cursor: legacyCursor }
: { ...value, q: value.q?.trim() };
return request<PageResult<UserSummary>>(`/users${encodeQuery(params)}`);
},
user: (userId: string) =>
request<UserDetail>(`/users/${encodeURIComponent(userId)}`),
latestLedger: (cursor?: string) =>
request<PageResult<LedgerEntry>>(`/credits/ledger${query({ cursor })}`),
ledger: (userId: string, cursor?: string) =>
latestLedger: (value?: string | LedgerQuery) =>
request<PageResult<LedgerEntry>>(
`/users/${encodeURIComponent(userId)}/ledger${query({ cursor })}`,
`/credits/ledger${encodeQuery(cursorQuery(value))}`,
),
ledger: (userId: string, value?: string | LedgerQuery) =>
request<PageResult<LedgerEntry>>(
`/users/${encodeURIComponent(userId)}/ledger${encodeQuery(cursorQuery(value))}`,
),
grantCredits: (payload: CreditGrantRequest) =>
@@ -196,11 +214,15 @@ export const adminApi = {
headers: { "Idempotency-Key": payload.idempotencyKey },
}),
auditLogs: (cursor?: string) =>
request<PageResult<AuditLogEntry>>(`/audit${query({ cursor })}`),
auditLogs: (value?: string | AuditQuery) =>
request<PageResult<AuditLogEntry>>(
`/audit${encodeQuery(cursorQuery(value))}`,
),
operators: (cursor?: string) =>
request<PageResult<AdminOperator>>(`/operators${query({ cursor })}`),
operators: (value?: string | OperatorsQuery) =>
request<PageResult<AdminOperator>>(
`/operators${encodeQuery(cursorQuery(value))}`,
),
operatorSummary: () =>
request<AdminSecuritySummary>("/operators/summary"),