224 lines
8.2 KiB
TypeScript
224 lines
8.2 KiB
TypeScript
import * as React from 'react';
|
|
import { StatsCard } from './StatsCard';
|
|
import { SortableTable, TableData } from './SortableTable';
|
|
|
|
interface StatsPageProps {
|
|
timeWindow: number; // in seconds
|
|
lastWindowCount: number;
|
|
lastWindowBytes: number;
|
|
requestsPerSecond: string;
|
|
totalBytesFormatted: string;
|
|
bytesPerSecondFormatted: string;
|
|
timeWindowFormatted: string;
|
|
contentTypeCounts: Array<{
|
|
content_type: string;
|
|
count: number;
|
|
bytes: number;
|
|
countFormatted: string;
|
|
bytesFormatted: string;
|
|
}>;
|
|
byDomainCounts: Array<{
|
|
domain: string;
|
|
count: number;
|
|
bytes: number;
|
|
countFormatted: string;
|
|
bytesFormatted: string;
|
|
}>;
|
|
availableTimeWindows: Array<{
|
|
seconds: number;
|
|
label: string;
|
|
active: boolean;
|
|
path: string;
|
|
}>;
|
|
}
|
|
|
|
export const StatsPage: React.FC<StatsPageProps> = ({
|
|
lastWindowCount,
|
|
requestsPerSecond,
|
|
totalBytesFormatted,
|
|
bytesPerSecondFormatted,
|
|
timeWindowFormatted,
|
|
contentTypeCounts,
|
|
byDomainCounts,
|
|
availableTimeWindows,
|
|
}) => {
|
|
const contentTypeData: TableData[] = contentTypeCounts.map((item) => ({
|
|
id: item.content_type,
|
|
cells: [
|
|
{ value: item.content_type, sortKey: item.content_type },
|
|
{ value: item.countFormatted, sortKey: item.count },
|
|
{ value: item.bytesFormatted, sortKey: item.bytes },
|
|
],
|
|
}));
|
|
|
|
const domainData: TableData[] = byDomainCounts.map((item) => ({
|
|
id: item.domain,
|
|
cells: [
|
|
{ value: item.domain, sortKey: item.domain },
|
|
{ value: item.countFormatted, sortKey: item.count },
|
|
{ value: item.bytesFormatted, sortKey: item.bytes },
|
|
],
|
|
}));
|
|
|
|
return (
|
|
<div className="mx-auto mt-8 max-w-7xl px-4">
|
|
{/* Header Section */}
|
|
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
|
{/* Top Bar */}
|
|
<div className="border-b border-slate-200 bg-slate-50 px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold text-slate-900">
|
|
HTTP Request Analytics
|
|
</h1>
|
|
<a
|
|
href="/log_entries"
|
|
className="inline-flex items-center gap-2 rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-white hover:text-slate-900 hover:shadow-sm"
|
|
>
|
|
<i className="fas fa-arrow-left" />
|
|
Back to Log Entries
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Summary */}
|
|
<div className="border-t border-slate-200 bg-gradient-to-br from-blue-50 to-indigo-50 px-6 py-6">
|
|
<div className="mb-4 text-center">
|
|
<h3 className="mb-1 text-lg font-semibold text-slate-900">
|
|
Summary for {timeWindowFormatted}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
{/* Total Requests */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
|
Total Requests
|
|
</p>
|
|
<p className="mt-1 text-2xl font-bold text-slate-900">
|
|
{lastWindowCount.toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
|
<i className="fas fa-chart-bar text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Requests per Second */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
|
Requests/sec
|
|
</p>
|
|
<p className="mt-1 text-2xl font-bold text-slate-900">
|
|
{requestsPerSecond}
|
|
</p>
|
|
</div>
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100">
|
|
<i className="fas fa-bolt text-green-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Total Data */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
|
Total Data
|
|
</p>
|
|
<p className="mt-1 text-2xl font-bold text-slate-900">
|
|
{totalBytesFormatted}
|
|
</p>
|
|
</div>
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100">
|
|
<i className="fas fa-database text-purple-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Data per Second */}
|
|
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
|
Data/sec
|
|
</p>
|
|
<p className="mt-1 text-2xl font-bold text-slate-900">
|
|
{bytesPerSecondFormatted}
|
|
</p>
|
|
</div>
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100">
|
|
<i className="fas fa-download text-orange-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time Window Selector */}
|
|
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 px-4 py-4">
|
|
<div className="text-center">
|
|
<div className="inline-flex flex-wrap justify-center gap-1 rounded-lg border border-white/50 bg-white/70 p-1 shadow-md backdrop-blur-sm">
|
|
{availableTimeWindows.map((timeWindowOption, index) => (
|
|
<React.Fragment key={timeWindowOption.seconds}>
|
|
{timeWindowOption.active ? (
|
|
<span className="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm">
|
|
{timeWindowOption.label}
|
|
</span>
|
|
) : (
|
|
<a
|
|
href={timeWindowOption.path}
|
|
className="rounded-md border border-transparent px-4 py-2 text-sm font-medium text-slate-600 transition-colors hover:border-slate-200 hover:bg-white hover:text-slate-900 hover:shadow-sm"
|
|
>
|
|
{timeWindowOption.label}
|
|
</a>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tables Grid - 2 columns */}
|
|
<div className="my-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
|
|
<div>
|
|
<h2 className="mb-3 text-xl font-bold text-slate-900">
|
|
By Content Type
|
|
</h2>
|
|
<SortableTable
|
|
headers={[
|
|
{ label: 'Content Type', key: 'content_type', align: 'left' },
|
|
{ label: 'Requests', key: 'count', align: 'right' },
|
|
{ label: 'Transferred', key: 'bytes', align: 'right' },
|
|
]}
|
|
data={contentTypeData}
|
|
defaultSortKey="count"
|
|
defaultSortOrder="desc"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<h2 className="mb-3 text-xl font-bold text-slate-900">By Domain</h2>
|
|
<SortableTable
|
|
headers={[
|
|
{ label: 'Domain', key: 'domain', align: 'left' },
|
|
{ label: 'Requests', key: 'count', align: 'right' },
|
|
{ label: 'Transferred', key: 'bytes', align: 'right' },
|
|
]}
|
|
data={domainData}
|
|
defaultSortKey="bytes"
|
|
defaultSortOrder="desc"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StatsPage;
|