From d5089dabd9ef9e93799dfc73fe81850584744f46 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Fri, 12 Dec 2025 22:52:03 +0800 Subject: [PATCH] feat(frontend): logs pagination --- frontend/src/api/client.ts | 9 ++++-- frontend/src/types/index.ts | 5 +++ src/admin/handlers.rs | 61 ++++++++++++++++++++++++++----------- src/admin/routes.rs | 1 + src/db/repositories/logs.rs | 31 +++++++++++++++++-- 5 files changed, 84 insertions(+), 23 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 534f9d7..d830e93 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -4,7 +4,7 @@ import type { CreateRuleRequest, UpdateRuleRequest, UpdateCommandRequest, - RequestLog, + RequestLogs, } from "../types"; import { authStore } from "./auth"; @@ -107,7 +107,7 @@ export const authApi = { // Logs API export const logsApi = { - list: (params?: { method?: string; search?: string }) => { + list: (params?: { method?: string; search?: string; page: number; limit: number }) => { const query = new URLSearchParams(); if (params?.method) { query.set("method", params.method); @@ -116,6 +116,9 @@ export const logsApi = { query.set("search", params.search); } const queryString = query.toString(); - return request(`/logs${queryString ? `?${queryString}` : ""}`); + return request(`/logs${queryString ? `?${queryString}` : ""}`); + }, + methods: () => { + return request("/logs/methods"); }, }; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 92ecd78..a583cc9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -58,3 +58,8 @@ export interface RequestLog { response_interception_action?: InterceptionAction; created_at: string; } + +export interface RequestLogs { + data: RequestLog[]; + total: number; +} diff --git a/src/admin/handlers.rs b/src/admin/handlers.rs index 27e0e2f..b95a759 100644 --- a/src/admin/handlers.rs +++ b/src/admin/handlers.rs @@ -1,17 +1,16 @@ use std::sync::Arc; use axum::{ + Json, extract::{Path, Query, State}, http::StatusCode, - Json, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tracing::error; use crate::{ - auth, + AppContext, auth, db::{self, models::RequestLog}, - AppContext, }; use super::models::*; @@ -239,10 +238,7 @@ pub async fn update_rule( { Ok(_) => match db::repositories::rules::find_by_id(&state.db, id).await { Ok(Some(rule)) => Ok(Json(rule.into())), - _ => Err(( - StatusCode::NOT_FOUND, - Json(ApiError::new("Rule not found")), - )), + _ => Err((StatusCode::NOT_FOUND, Json(ApiError::new("Rule not found")))), }, Err(e) => { error!("Failed to update rule: {}", e); @@ -321,21 +317,29 @@ pub async fn verify_command( pub struct ListLogsParams { pub method: Option, pub search: Option, + pub page: Option, + pub limit: Option, } pub async fn list_logs( State(state): State>, Query(params): Query, -) -> Result>, (StatusCode, Json)> { - match db::repositories::logs::list( - &state.db, - params.method.as_deref(), - params.search.as_deref(), - ) - .await - { - Ok(logs) => Ok(Json(logs)), - Err(e) => { +) -> Result, (StatusCode, Json)> { + match ( + db::repositories::logs::list( + &state.db, + params.method.as_deref(), + params.search.as_deref(), + params.limit, + params + .limit + .map(|limit| limit * (params.page.unwrap_or(1) - 1)), + ) + .await, + db::repositories::logs::total(&state.db).await, + ) { + (Ok(logs), Ok(total)) => Ok(Json(PaginatedRequestLogs { total, data: logs })), + (Err(e), _) | (_, Err(e)) => { error!("Failed to list logs: {}", e); Err(( StatusCode::INTERNAL_SERVER_ERROR, @@ -344,3 +348,24 @@ pub async fn list_logs( } } } + +#[derive(Serialize)] +pub struct PaginatedRequestLogs { + data: Vec, + total: usize, +} + +pub async fn list_methods( + State(state): State>, +) -> Result>, (StatusCode, Json)> { + db::repositories::logs::list_methods(&state.db) + .await + .map(Json) + .map_err(|e| { + error!("Failed to list logs' methods: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("Failed to fetch logs' methods")), + ) + }) +} diff --git a/src/admin/routes.rs b/src/admin/routes.rs index bb4f078..65fd478 100644 --- a/src/admin/routes.rs +++ b/src/admin/routes.rs @@ -23,6 +23,7 @@ pub fn admin_routes(ctx: Arc) -> Router> { .route("/api/commands", get(handlers::list_commands)) .route("/api/commands/{:id}", post(handlers::verify_command)) .route("/api/logs", get(handlers::list_logs)) + .route("/api/logs/methods", get(handlers::list_methods)) .layer(axum::middleware::from_fn_with_state( ctx, auth_middleware::auth_middleware, diff --git a/src/db/repositories/logs.rs b/src/db/repositories/logs.rs index 28b2fe9..86ecdd6 100644 --- a/src/db/repositories/logs.rs +++ b/src/db/repositories/logs.rs @@ -1,13 +1,22 @@ use crate::db::models::{InterceptionAction, RequestLog}; use sqlx::{QueryBuilder, SqlitePool}; +pub async fn total(pool: &SqlitePool) -> anyhow::Result { + Ok( + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM request_logs;") + .fetch_one(pool) + .await? as usize, + ) +} + pub async fn list( pool: &SqlitePool, method: Option<&str>, search: Option<&str>, + limit: Option, + offset: Option, ) -> anyhow::Result> { - let mut builder: QueryBuilder = - QueryBuilder::new("SELECT * FROM request_logs"); + let mut builder: QueryBuilder = QueryBuilder::new("SELECT * FROM request_logs"); if let Some(method_val) = method { builder.push(" WHERE method = "); @@ -27,12 +36,30 @@ pub async fn list( builder.push(")"); } + if let Some(limit) = limit { + builder.push("LIMIT "); + builder.push(limit); + } + + if let Some(offset) = offset { + builder.push("OFFSET "); + builder.push(offset); + } + builder.push(" ORDER BY created_at DESC"); let query = builder.build_query_as(); Ok(query.fetch_all(pool).await?) } +pub async fn list_methods(pool: &SqlitePool) -> anyhow::Result> { + Ok( + sqlx::query_scalar::<_, String>("SELECT DISTINCT method FROM request_logs ORDER BY method;") + .fetch_all(pool) + .await? + ) +} + pub async fn create( pool: &SqlitePool, method: String,