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.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
'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();
|
||||
}
|
||||
Reference in New Issue
Block a user