feat(frontend): show intercepted request & response

This commit is contained in:
2025-12-08 17:35:12 +08:00
parent 2a6a3a739c
commit 51a482de7f
11 changed files with 202 additions and 88 deletions

View File

@@ -0,0 +1,141 @@
import { Component, createMemo, createResource, createSignal, For, Show } from "solid-js";
import { logsApi } from "../api/client";
import type { RequestLog as RequestLogType } from "../types";
import { Button } from "./ui/Button";
import { Card } from "./ui/Card";
import { Input } from "./ui/Input";
import { Select } from "./ui/Select";
import RequestDetails from "./RequestDetails";
const RequestLogs: Component = () => {
const [search, setSearch] = createSignal("");
const [inputValue, setInputValue] = createSignal("");
const [method, setMethod] = createSignal("");
const [selectedLog, setSelectedLog] = createSignal<RequestLogType | null>(null);
let debounceTimer: number;
const handleInput = (e: { currentTarget: { value: string } }) => {
const value = e.currentTarget.value;
setInputValue(value);
clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(() => setSearch(value), 300);
};
const [logs, { refetch: refetchLogs }] = createResource(
() => ({ search: search(), method: method() }),
async (filters) => {
return logsApi.list(filters);
},
);
const [allLogs] = createResource(async () => {
return await logsApi.list();
});
const methods = createMemo(() => {
return allLogs.loading ? [] : [...new Set(allLogs()!.map((log) => log.method))];
});
const handleLogClick = (log: RequestLogType) => {
setSelectedLog(log);
};
const handleCloseDetails = () => {
setSelectedLog(null);
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString();
};
return (
<>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-semibold">
Request Logs
<Show when={logs() && !logs.loading && !allLogs.loading}>
<span class="text-gray-500 text-base ml-2">
({logs()?.length} of {allLogs()?.length} logs)
</span>
</Show>
</h2>
<Button onClick={() => refetchLogs()} variant="secondary">
Refresh
</Button>
</div>
<Card>
<div class="p-4 flex flex-col sm:flex-row items-center space-y-4 sm:space-x-4 sm:space-y-0 bg-gray-50 border-b">
<div class="flex-1 w-full">
<Input
type="text"
placeholder="Search in request/response..."
value={inputValue()}
onInput={handleInput}
/>
</div>
<div class="w-full sm:w-1/4">
<Select value={method()} onChange={(e) => setMethod(e.currentTarget.value)} class="w-full">
<option value="">All Methods</option>
<For each={methods()}>{(m) => <option value={m}>{m}</option>}</For>
</Select>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Request Body
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<Show
when={!logs.loading}
fallback={
<tr>
<td colspan="3" class="text-center py-4">
Loading...
</td>
</tr>
}
>
<For
each={logs()}
fallback={
<tr>
<td colspan="3" class="text-center py-8 text-gray-500">
No logs found.
</td>
</tr>
}
>
{(log) => (
<tr class="hover:bg-gray-50 cursor-pointer" onClick={() => handleLogClick(log)}>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{log.method}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(log.created_at)}
</td>
<td class="px-6 py-4 text-sm text-gray-500 font-mono truncate max-w-lg">
{JSON.stringify(log.request_body)}
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</Card>
</div>
<Show when={selectedLog()}>{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}</Show>
</>
);
};
export default RequestLogs;