feat(frontend): logs pagination
This commit is contained in:
@@ -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");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
db::repositories::logs::list(
|
||||||
&state.db,
|
&state.db,
|
||||||
params.method.as_deref(),
|
params.method.as_deref(),
|
||||||
params.search.as_deref(),
|
params.search.as_deref(),
|
||||||
|
params.limit,
|
||||||
|
params
|
||||||
|
.limit
|
||||||
|
.map(|limit| limit * (params.page.unwrap_or(1) - 1)),
|
||||||
)
|
)
|
||||||
.await
|
.await,
|
||||||
{
|
db::repositories::logs::total(&state.db).await,
|
||||||
Ok(logs) => Ok(Json(logs)),
|
) {
|
||||||
Err(e) => {
|
(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")),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user