feat(frontend): logs pagination

This commit is contained in:
2025-12-12 22:52:03 +08:00
parent 611fdb556c
commit d5089dabd9
5 changed files with 84 additions and 23 deletions

View File

@@ -4,7 +4,7 @@ import type {
CreateRuleRequest, CreateRuleRequest,
UpdateRuleRequest, UpdateRuleRequest,
UpdateCommandRequest, UpdateCommandRequest,
RequestLog, RequestLogs,
} from "../types"; } from "../types";
import { authStore } from "./auth"; import { authStore } from "./auth";
@@ -107,7 +107,7 @@ export const authApi = {
// Logs API // Logs API
export const logsApi = { export const logsApi = {
list: (params?: { method?: string; search?: string }) => { list: (params?: { method?: string; search?: string; page: number; limit: number }) => {
const query = new URLSearchParams(); const query = new URLSearchParams();
if (params?.method) { if (params?.method) {
query.set("method", params.method); query.set("method", params.method);
@@ -116,6 +116,9 @@ export const logsApi = {
query.set("search", params.search); query.set("search", params.search);
} }
const queryString = query.toString(); const queryString = query.toString();
return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ""}`); return request<RequestLogs>(`/logs${queryString ? `?${queryString}` : ""}`);
},
methods: () => {
return request<string[]>("/logs/methods");
}, },
}; };

View File

@@ -58,3 +58,8 @@ export interface RequestLog {
response_interception_action?: InterceptionAction; response_interception_action?: InterceptionAction;
created_at: string; created_at: string;
} }
export interface RequestLogs {
data: RequestLog[];
total: number;
}

View File

@@ -1,17 +1,16 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{ use axum::{
Json,
extract::{Path, Query, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
Json,
}; };
use serde::Deserialize; use serde::{Deserialize, Serialize};
use tracing::error; use tracing::error;
use crate::{ use crate::{
auth, AppContext, auth,
db::{self, models::RequestLog}, db::{self, models::RequestLog},
AppContext,
}; };
use super::models::*; 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(_) => match db::repositories::rules::find_by_id(&state.db, id).await {
Ok(Some(rule)) => Ok(Json(rule.into())), Ok(Some(rule)) => Ok(Json(rule.into())),
_ => Err(( _ => Err((StatusCode::NOT_FOUND, Json(ApiError::new("Rule not found")))),
StatusCode::NOT_FOUND,
Json(ApiError::new("Rule not found")),
)),
}, },
Err(e) => { Err(e) => {
error!("Failed to update rule: {}", e); error!("Failed to update rule: {}", e);
@@ -321,21 +317,29 @@ pub async fn verify_command(
pub struct ListLogsParams { pub struct ListLogsParams {
pub method: Option<String>, pub method: Option<String>,
pub search: Option<String>, pub search: Option<String>,
pub page: Option<usize>,
pub limit: Option<usize>,
} }
pub async fn list_logs( pub async fn list_logs(
State(state): State<Arc<AppContext>>, State(state): State<Arc<AppContext>>,
Query(params): Query<ListLogsParams>, Query(params): Query<ListLogsParams>,
) -> Result<Json<Vec<RequestLog>>, (StatusCode, Json<ApiError>)> { ) -> Result<Json<PaginatedRequestLogs>, (StatusCode, Json<ApiError>)> {
match db::repositories::logs::list( match (
&state.db, db::repositories::logs::list(
params.method.as_deref(), &state.db,
params.search.as_deref(), params.method.as_deref(),
) params.search.as_deref(),
.await params.limit,
{ params
Ok(logs) => Ok(Json(logs)), .limit
Err(e) => { .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); error!("Failed to list logs: {}", e);
Err(( Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@@ -344,3 +348,24 @@ pub async fn list_logs(
} }
} }
} }
#[derive(Serialize)]
pub struct PaginatedRequestLogs {
data: Vec<RequestLog>,
total: usize,
}
pub async fn list_methods(
State(state): State<Arc<AppContext>>,
) -> Result<Json<Vec<String>>, (StatusCode, Json<ApiError>)> {
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")),
)
})
}

View File

@@ -23,6 +23,7 @@ pub fn admin_routes(ctx: Arc<AppContext>) -> Router<Arc<AppContext>> {
.route("/api/commands", get(handlers::list_commands)) .route("/api/commands", get(handlers::list_commands))
.route("/api/commands/{:id}", post(handlers::verify_command)) .route("/api/commands/{:id}", post(handlers::verify_command))
.route("/api/logs", get(handlers::list_logs)) .route("/api/logs", get(handlers::list_logs))
.route("/api/logs/methods", get(handlers::list_methods))
.layer(axum::middleware::from_fn_with_state( .layer(axum::middleware::from_fn_with_state(
ctx, ctx,
auth_middleware::auth_middleware, auth_middleware::auth_middleware,

View File

@@ -1,13 +1,22 @@
use crate::db::models::{InterceptionAction, RequestLog}; use crate::db::models::{InterceptionAction, RequestLog};
use sqlx::{QueryBuilder, SqlitePool}; use sqlx::{QueryBuilder, SqlitePool};
pub async fn total(pool: &SqlitePool) -> anyhow::Result<usize> {
Ok(
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM request_logs;")
.fetch_one(pool)
.await? as usize,
)
}
pub async fn list( pub async fn list(
pool: &SqlitePool, pool: &SqlitePool,
method: Option<&str>, method: Option<&str>,
search: Option<&str>, search: Option<&str>,
limit: Option<usize>,
offset: Option<usize>,
) -> anyhow::Result<Vec<RequestLog>> { ) -> anyhow::Result<Vec<RequestLog>> {
let mut builder: QueryBuilder<sqlx::Sqlite> = let mut builder: QueryBuilder<sqlx::Sqlite> = QueryBuilder::new("SELECT * FROM request_logs");
QueryBuilder::new("SELECT * FROM request_logs");
if let Some(method_val) = method { if let Some(method_val) = method {
builder.push(" WHERE method = "); builder.push(" WHERE method = ");
@@ -27,12 +36,30 @@ pub async fn list(
builder.push(")"); 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"); builder.push(" ORDER BY created_at DESC");
let query = builder.build_query_as(); let query = builder.build_query_as();
Ok(query.fetch_all(pool).await?) Ok(query.fetch_all(pool).await?)
} }
pub async fn list_methods(pool: &SqlitePool) -> anyhow::Result<Vec<String>> {
Ok(
sqlx::query_scalar::<_, String>("SELECT DISTINCT method FROM request_logs ORDER BY method;")
.fetch_all(pool)
.await?
)
}
pub async fn create( pub async fn create(
pool: &SqlitePool, pool: &SqlitePool,
method: String, method: String,