chore: deep cleanup of test scripts and implement glassmorphism UI enhancements
This commit is contained in:
1 parent
7777b62305
commit
58040d6e6c
81 files changed
+2247
-6734
No files matched your search
@@ -133,7 +133,7 @@ export function AppDetailModal({ appLabel, favicon, category, onClose }: Props)
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-300 mb-3 flex items-center gap-2">
|
||||
<Monitor className="w-4 h-4 text-cyan-400" />
|
||||
{data.has_domain_breakdown ? "Top Devices (via Domain Match)" : "Top Devices / IPs"}
|
||||
Top 5 Flow Records (Realtime)
|
||||
{data.top_ips.filter(ip => ip.blacklisted).length > 0 && (
|
||||
<span className="ml-auto text-xs text-red-400 bg-red-500/10 px-2 py-0.5 rounded-full ring-1 ring-red-500/30">
|
||||
🚫 {data.top_ips.filter(ip => ip.blacklisted).length} Blacklisted
|
||||
@@ -146,12 +146,14 @@ export function AppDetailModal({ appLabel, favicon, category, onClose }: Props)
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left border-b border-border">
|
||||
<th className="pb-2 text-xs font-medium text-muted-foreground">#</th>
|
||||
<th className="pb-2 text-xs font-medium text-muted-foreground">IP Address</th>
|
||||
<th className="pb-2 text-xs font-medium text-muted-foreground text-right">Download</th>
|
||||
<th className="pb-2 text-xs font-medium text-muted-foreground text-right">Upload</th>
|
||||
<th className="pb-2 text-xs font-medium text-muted-foreground">Usage</th>
|
||||
<tr className="text-left border-b border-white/10">
|
||||
<th className="pb-3 pr-4 text-xs font-medium text-muted-foreground w-8">#</th>
|
||||
<th className="pb-3 pr-4 text-xs font-medium text-muted-foreground">Source IP</th>
|
||||
<th className="pb-3 pr-4 text-xs font-medium text-muted-foreground">Dest IP</th>
|
||||
<th className="pb-3 pr-4 text-xs font-medium text-muted-foreground">Label / Domain</th>
|
||||
<th className="pb-3 pr-4 text-xs font-medium text-muted-foreground">Time</th>
|
||||
<th className="pb-3 pr-4 text-xs font-medium text-muted-foreground text-right">Download</th>
|
||||
<th className="pb-3 text-xs font-medium text-muted-foreground text-right">Upload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
@@ -159,9 +161,9 @@ export function AppDetailModal({ appLabel, favicon, category, onClose }: Props)
|
||||
const maxDl = Math.max(...data.top_ips.map(x => x.download), 1);
|
||||
const pct = (ip.download / maxDl) * 100;
|
||||
return (
|
||||
<tr key={i} className="hover:bg-white/[0.03] transition-colors">
|
||||
<td className="py-2 pr-4 text-xs text-muted-foreground">{i + 1}</td>
|
||||
<td className="py-2 pr-4 font-mono text-xs">
|
||||
<tr key={i} className="hover:bg-white/[0.04] transition-colors group">
|
||||
<td className="py-3 pr-4 text-xs text-muted-foreground group-hover:text-white transition-colors">{i + 1}</td>
|
||||
<td className="py-3 pr-4 font-mono text-xs">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className={ip.blacklisted ? "text-red-400" : "text-blue-400"}>{ip.ip_address || "-"}</span>
|
||||
{ip.blacklisted && (
|
||||
@@ -172,16 +174,17 @@ export function AppDetailModal({ appLabel, favicon, category, onClose }: Props)
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-right text-xs text-cyan-400">{fmtBytes(ip.download)}</td>
|
||||
<td className="py-2 pr-4 text-right text-xs text-green-400">{fmtBytes(ip.upload ?? 0)}</td>
|
||||
<td className="py-2 w-32">
|
||||
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${ip.blacklisted ? "bg-red-500" : "bg-gradient-to-r from-cyan-500 to-green-500"}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<td className="py-3 pr-4 font-mono text-xs text-muted-foreground truncate max-w-[120px]">
|
||||
{ip.dst_ip || "-"}
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-xs text-slate-300 truncate max-w-[150px]">
|
||||
{ip.domain || data.app_label || "-"}
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-xs text-muted-foreground whitespace-nowrap">
|
||||
{ip.last_seen ? new Date(ip.last_seen).toLocaleString('id-ID', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : "-"}
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-right text-xs font-medium text-cyan-400">{fmtBytes(ip.download)}</td>
|
||||
<td className="py-3 text-right text-xs font-medium text-green-400">{fmtBytes(ip.upload ?? 0)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface DataTableProps<T> {
|
||||
searchFilter?: (row: T, query: string) => boolean;
|
||||
isLoading?: boolean;
|
||||
getRowClassName?: (row: T) => string;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export function DataTable<T>({
|
||||
@@ -24,14 +25,20 @@ export function DataTable<T>({
|
||||
searchPlaceholder = "Search...",
|
||||
searchFilter,
|
||||
isLoading,
|
||||
getRowClassName
|
||||
getRowClassName,
|
||||
pageSize = 50
|
||||
}: DataTableProps<T>) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const filteredData = searchFilter
|
||||
? data.filter(row => searchFilter(row, query))
|
||||
? data.filter(row => searchFilter(row, query.trim()))
|
||||
: data;
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize));
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const paginatedData = filteredData.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{searchFilter && (
|
||||
@@ -41,7 +48,10 @@ export function DataTable<T>({
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="w-full max-w-sm rounded-lg border border-border bg-card/30 backdrop-blur-sm pl-9 pr-4 py-2.5 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:bg-card/50 transition-all shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
@@ -74,7 +84,7 @@ export function DataTable<T>({
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredData.length === 0 ? (
|
||||
) : paginatedData.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center text-muted-foreground">
|
||||
@@ -85,7 +95,7 @@ export function DataTable<T>({
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
paginatedData.map((row, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={`hover:bg-primary/5 transition-colors duration-200 group ${getRowClassName ? getRowClassName(row) : ""}`}
|
||||
@@ -101,6 +111,38 @@ export function DataTable<T>({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{filteredData.length > pageSize && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-border/50 bg-card/30">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing <span className="font-medium text-white">{startIndex + 1}</span> to{" "}
|
||||
<span className="font-medium text-white">
|
||||
{Math.min(startIndex + pageSize, filteredData.length)}
|
||||
</span>{" "}
|
||||
of <span className="font-medium text-white">{filteredData.length}</span> results
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-md bg-secondary/80 text-white hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-border/50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<div className="text-sm text-muted-foreground font-medium px-2">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-md bg-secondary/80 text-white hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-border/50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,10 +78,18 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
|
||||
fetch('https://unpkg.com/globe.gl/example/datasets/ne_110m_admin_0_countries.geojson')
|
||||
.then(res => res.json())
|
||||
.then(setCountries)
|
||||
.catch(err => console.error('Failed to load countries', err));
|
||||
const loadCountries = async () => {
|
||||
try {
|
||||
const res = await fetch('/ne_110m_admin_0_countries.geojson');
|
||||
if (!res.ok) throw new Error('Network response was not ok');
|
||||
const data = await res.json();
|
||||
setCountries(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load countries', err);
|
||||
}
|
||||
};
|
||||
|
||||
loadCountries();
|
||||
|
||||
return () => window.removeEventListener('resize', updateDimensions);
|
||||
}, []);
|
||||
|
||||
Reference in new issue
Block a user