From 8be7af6815ddbdbbe4ebbbe42a9ef811383706a9 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Sat, 6 Dec 2025 21:49:51 +0800 Subject: [PATCH] feat: refine request log --- frontend/src/api/client.ts | 14 ++- frontend/src/components/ChangePassword.tsx | 2 +- frontend/src/components/RequestDetails.tsx | 110 +++++++++++++----- frontend/src/components/RequestLog.tsx | 126 +++++++++++++++------ frontend/src/components/ui/Modal.tsx | 16 +-- src/admin/handlers.rs | 29 ++++- src/db/repositories/logs.rs | 38 +++++-- 7 files changed, 248 insertions(+), 87 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 24463ce..3369b36 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -106,7 +106,15 @@ export const authApi = { // Logs API export const logsApi = { - list: () => request('/logs'), + list: (params?: { method?: string; search?: string }) => { + const query = new URLSearchParams(); + if (params?.method) { + query.set('method', params.method); + } + if (params?.search) { + query.set('search', params.search); + } + const queryString = query.toString(); + return request(`/logs${queryString ? `?${queryString}` : ''}`); + }, }; - -export const fetchLogs = logsApi.list; diff --git a/frontend/src/components/ChangePassword.tsx b/frontend/src/components/ChangePassword.tsx index 445997f..cb8ed7e 100644 --- a/frontend/src/components/ChangePassword.tsx +++ b/frontend/src/components/ChangePassword.tsx @@ -64,7 +64,7 @@ const ChangePassword: Component = (props) => { return ( - +

Change Password

diff --git a/frontend/src/components/RequestDetails.tsx b/frontend/src/components/RequestDetails.tsx index 540b5ac..b09e505 100644 --- a/frontend/src/components/RequestDetails.tsx +++ b/frontend/src/components/RequestDetails.tsx @@ -1,47 +1,103 @@ -import { Component, createSignal, For } from 'solid-js'; +import { Component, createSignal, For, Show } from 'solid-js'; import type { RequestLog } from '../types'; -import { Card } from './ui/Card'; +import { Button } from './ui/Button'; +import { CardContent, CardFooter, CardHeader } from './ui/Card'; +import { Modal, ModalContent } from './ui/Modal'; -const TreeView: Component<{ data: any; name: string }> = (props) => { - const [isOpen, setIsOpen] = createSignal(true); +interface TreeViewProps { + data: any; + name: string; + isRoot?: boolean; +} + +const TreeView: Component = (props) => { + const [isOpen, setIsOpen] = createSignal(props.isRoot ?? false); const isObject = typeof props.data === 'object' && props.data !== null; + const renderValue = (value: any) => { + switch (typeof value) { + case 'string': + return "{value}"; + case 'number': + return {value}; + case 'boolean': + return {String(value)}; + case 'object': + if (value === null) return null; + // This case is handled by recursive TreeView + default: + return {String(value)}; + } + }; + return ( -
-
- setIsOpen(!isOpen())}> +
+
setIsOpen(!isOpen())} + > + {isObject ? (isOpen() ? '▼' : '►') : ''} - {props.name} + {props.name}: + + {renderValue(props.data)} +
- {isOpen() && isObject && ( -
+ +
- {([key, value]) => { - if (typeof value === 'object' && value !== null && 'modified' in value && 'value' in value) { - return - } - return - }} + {([key, value]) => }
- )} - {isOpen() && !isObject &&
{JSON.stringify(props.data)}
} +
); }; -const RequestDetails: Component<{ log: RequestLog }> = (props) => { +interface RequestDetailsProps { + log: RequestLog; + onClose: () => void; +} + +const RequestDetails: Component = (props) => { return ( - -

Request Details

- {/* TODO: interception method */} -
- - -
-
+ + + +

Request Details

+

{props.log.method} at {new Date(props.log.created_at).toLocaleString()}

+
+ + +
+
+

Request

+
+ +
+
+
+

Response

+
+ +
+
+
+
+ + + + +
+
); }; diff --git a/frontend/src/components/RequestLog.tsx b/frontend/src/components/RequestLog.tsx index 32f047d..8b368a1 100644 --- a/frontend/src/components/RequestLog.tsx +++ b/frontend/src/components/RequestLog.tsx @@ -1,57 +1,111 @@ -import { Component, createSignal, onMount, For, Show } from 'solid-js'; -import { fetchLogs } from '../api/client'; +import { Component, createMemo, createResource, createSignal, For, Show } from 'solid-js'; +import { logsApi } from '../api/client'; import type { RequestLog as RequestLogType } from '../types'; -import RequestDetails from './RequestDetails'; import { Card } from './ui/Card'; +import { Input } from './ui/Input'; +import { Select } from './ui/Select'; +import RequestDetails from './RequestDetails'; const RequestLog: Component = () => { - const [logs, setLogs] = createSignal([]); + const [search, setSearch] = createSignal(''); + const [method, setMethod] = createSignal(''); const [selectedLog, setSelectedLog] = createSignal(null); - onMount(async () => { - try { - const fetchedLogs = await fetchLogs(); - setLogs(fetchedLogs); - } catch (error) { - console.error('Error fetching logs:', error); + const [logs] = createResource( + () => ({ search: search(), method: method() }), + async (filters) => { + // Solid's createResource refetches when the source accessor changes. + // We can add a debounce here if we want to avoid too many requests. + return logsApi.list(filters); } + ); + + const methods = createMemo(() => { + if (!logs()) return []; + const allMethods = logs()!.map(log => log.method); + return [...new Set(allMethods)]; }); const handleLogClick = (log: RequestLogType) => { setSelectedLog(log); }; + const handleCloseDetails = () => { + setSelectedLog(null); + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleString(); + }; + + return ( -
-
+ <> +

Request Log

-
- -
    - - {(log) => ( -
  • handleLogClick(log)} - > -
    - {log.method} - {new Date(log.created_at).toLocaleTimeString()} -
    -
  • - )} -
    -
-
-
+ + +
+
+ setSearch(e.currentTarget.value)} + /> +
+
+ +
+
+ +
+ + + + + + + + + + }> + + + + }> + {(log) => ( + handleLogClick(log)}> + + + + + )} + + + +
MethodTimeRequest Body
Loading...
+ No logs found. +
{log.method}{formatDate(log.created_at)} + {JSON.stringify(log.request_body)} +
+
+
- {(log) => -
- -
} + {(log) => }
-
+ ); }; diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx index f9343a3..4c9dae6 100644 --- a/frontend/src/components/ui/Modal.tsx +++ b/frontend/src/components/ui/Modal.tsx @@ -4,20 +4,20 @@ const Modal: Component> = (props) => { const [local, others] = splitProps(props, ['class']); return (
); }; const ModalContent: Component> = (props) => { - const [local, others] = splitProps(props, ['class']); - return ( -
- ); + const [local, others] = splitProps(props, ['class']); + return ( +
+ ); }; export { Modal, ModalContent }; diff --git a/src/admin/handlers.rs b/src/admin/handlers.rs index cb4790a..27e0e2f 100644 --- a/src/admin/handlers.rs +++ b/src/admin/handlers.rs @@ -1,15 +1,17 @@ use std::sync::Arc; use axum::{ - Json, - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, + Json, }; +use serde::Deserialize; use tracing::error; use crate::{ - AppContext, auth, + auth, db::{self, models::RequestLog}, + AppContext, }; use super::models::*; @@ -237,7 +239,10 @@ 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); @@ -310,11 +315,25 @@ pub async fn verify_command( } } } + // Log handlers +#[derive(Deserialize)] +pub struct ListLogsParams { + pub method: Option, + pub search: Option, +} + pub async fn list_logs( State(state): State>, + Query(params): Query, ) -> Result>, (StatusCode, Json)> { - match db::repositories::logs::list_all(&state.db).await { + match db::repositories::logs::list( + &state.db, + params.method.as_deref(), + params.search.as_deref(), + ) + .await + { Ok(logs) => Ok(Json(logs)), Err(e) => { error!("Failed to list logs: {}", e); diff --git a/src/db/repositories/logs.rs b/src/db/repositories/logs.rs index a2ed1b8..f30075b 100644 --- a/src/db/repositories/logs.rs +++ b/src/db/repositories/logs.rs @@ -1,13 +1,37 @@ use crate::db::models::RequestLog; use serde_json::Value; -use sqlx::SqlitePool; +use sqlx::{QueryBuilder, SqlitePool}; -pub async fn list_all(pool: &SqlitePool) -> anyhow::Result> { - Ok( - sqlx::query_as::<_, RequestLog>("SELECT * FROM request_logs ORDER BY created_at") - .fetch_all(pool) - .await?, - ) +pub async fn list( + pool: &SqlitePool, + method: Option<&str>, + search: Option<&str>, +) -> anyhow::Result> { + let mut builder: QueryBuilder = + QueryBuilder::new("SELECT * FROM request_logs"); + + if let Some(method_val) = method { + builder.push(" WHERE method = "); + builder.push_bind(method_val); + } + + if let Some(search_val) = search { + let pattern = format!("%{}%", search_val); + if method.is_some() { + builder.push(" AND (request_body LIKE "); + } else { + builder.push(" WHERE (request_body LIKE "); + } + builder.push_bind(pattern.clone()); + builder.push(" OR response_body LIKE "); + builder.push_bind(pattern); + builder.push(")"); + } + + builder.push(" ORDER BY created_at DESC"); + + let query = builder.build_query_as(); + Ok(query.fetch_all(pool).await?) } pub async fn create(