Files
Gerhard Scheikl d317e8c758 feat(admin): live redis kill-switch on tunnel actions, sortable columns + CSV export + bulk actions, Node 24 LTS
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.
2026-05-31 14:46:22 +02:00

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();
}