d317e8c758
WS1: pin all Docker stages to node:24.16.0-alpine; add engines node>=20. WS2: lib/redis.ts gains TTL-backed redisSet, redisDel, setTunnelActive (writes tunnel:active:<sub>=1/0 EX 30, TUNNEL_ACTIVE_TTL override, no-op without REDIS_URL); wired into tunnel active/delete/reassign routes. WS3: sortable columns, CSV export routes (token excluded), and bulk actions (self-account guard) across users/tunnels/audit admin tables.
65 lines
1.5 KiB
TypeScript
65 lines
1.5 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Small shared client helpers for the admin tables: a clickable sortable
|
|
* column header and a CSV-download trigger. Kept dependency-free and
|
|
* dark-theme consistent with globals.css.
|
|
*/
|
|
|
|
export type SortOrder = 'asc' | 'desc';
|
|
|
|
export function SortHeader({
|
|
label,
|
|
col,
|
|
sort,
|
|
order,
|
|
onSort,
|
|
className,
|
|
}: {
|
|
label: string;
|
|
col: string;
|
|
sort: string;
|
|
order: SortOrder;
|
|
onSort: (col: string) => void;
|
|
className?: string;
|
|
}) {
|
|
const active = sort === col;
|
|
const indicator = active ? (order === 'asc' ? '▲' : '▼') : '↕';
|
|
return (
|
|
<th
|
|
className={`th-sort${active ? ' sorted' : ''}${
|
|
className ? ` ${className}` : ''
|
|
}`}
|
|
onClick={() => onSort(col)}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-sort={active ? (order === 'asc' ? 'ascending' : 'descending') : 'none'}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
onSort(col);
|
|
}
|
|
}}
|
|
>
|
|
{label}
|
|
<span className="sort-ind" aria-hidden="true">
|
|
{indicator}
|
|
</span>
|
|
</th>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Triggers a browser download of a same-origin URL. The server sets
|
|
* Content-Disposition: attachment, so the cookie-authenticated GET streams the
|
|
* CSV straight to a file.
|
|
*/
|
|
export function downloadUrl(url: string): void {
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.rel = 'noopener';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|