chore: deep cleanup of test scripts and implement glassmorphism UI enhancements

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-07 00:35:54 +07:00
1 parent 7777b62305
commit 58040d6e6c
81 files changed
+2247 -6734

No files matched your search

+22 -19
View File
@@ -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>
);
})}
+47 -5
View File
@@ -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>
);
+12 -4
View File
@@ -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);
}, []);