feat: version 4.3.0 -- Sesiones activas y pagina Mi cuenta

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-26 16:42:19 -05:00
parent 544928d04a
commit a6cc41666e
34 changed files with 464 additions and 110 deletions
+1
View File
@@ -57,6 +57,7 @@ export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowC
<tr
key={getRowKey(row)}
onClick={() => onRowClick?.(row)}
onContextMenu={rowActions ? (event) => { event.preventDefault(); event.currentTarget.querySelector('.row-menu-btn')?.click(); } : undefined}
tabIndex={onRowClick ? 0 : undefined}
onKeyDown={(event) => { if (onRowClick && event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); onRowClick(row); } }}
className={dragEnabled && isDragging(getRowKey(row)) ? 'dragging' : ''}
+49
View File
@@ -0,0 +1,49 @@
import { useCallback, useEffect, useState } from 'react';
import { LogOut, RefreshCw } from 'lucide-react';
import { useUi } from '../context/ui-context.jsx';
import { ListView } from './ListView.jsx';
import { Button } from './Button.jsx';
import { Badge } from './Badge.jsx';
export function SessionsList({ load, revoke, revokeAll, onCurrentRevoked, allLabel }) {
const { t, lang, openDialog, pushToast } = useUi();
const [sessions, setSessions] = useState([]);
const [loading, setLoading] = useState(true);
const reload = useCallback(async () => {
setLoading(true);
try { setSessions(await load()); }
catch (error) { pushToast({ tone: 'error', message: error.message }); }
finally { setLoading(false); }
}, [load, pushToast]);
useEffect(() => { reload(); }, [reload]);
function confirm(action, message, current = false) {
openDialog({ tone: 'warning', title: t('sessions_close'), message,
actions: [
{ label: t('common_cancel'), variant: 'standard' },
{ label: t('sessions_close'), variant: 'danger', onClick: async () => {
try {
const result = await action();
if (current || result?.current) onCurrentRevoked?.();
else { await reload(); pushToast({ tone: 'success', message: t('sessions_closed') }); }
} catch (error) { pushToast({ tone: 'error', message: error.message }); }
} },
] });
}
const date = (value) => value ? new Date(value.replace(' ', 'T')).toLocaleString(lang) : '—';
const columns = [
{ key: 'browser', label: t('sessions_device'), render: (row) => <>{row.browser === 'unknown' ? t('sessions_unknown') : row.browser} · {row.os === 'unknown' ? t('sessions_unknown') : row.os} {row.current && <Badge color="accent">{t('sessions_current')}</Badge>}</> },
{ key: 'ip', label: t('sessions_ip'), render: (row) => row.ip || '—' },
{ key: 'created_at', label: t('sessions_started'), render: (row) => date(row.created_at) },
{ key: 'last_seen_at', label: t('sessions_seen'), render: (row) => date(row.last_seen_at) },
{ key: 'close', label: t('sessions_close'), sortable: false, render: (row) => <Button size="sm" variant="danger" icon={LogOut} onClick={(event) => { event.stopPropagation(); confirm(() => revoke(row.id), t('sessions_confirm'), row.current); }}>{t('sessions_close')}</Button> },
];
return <div>
<div className="toolbar"><div className="toolbar-left">
<Button icon={RefreshCw} onClick={reload}>{t('sessions_refresh')}</Button>
{sessions.some((session) => allLabel || !session.current) && <Button variant="danger" icon={LogOut} onClick={() => confirm(revokeAll, t('sessions_confirm_all'))}>{allLabel || t('sessions_close_others')}</Button>}
</div></div>
{loading ? <div className="text-muted">{t('common_loading')}</div> : <ListView columns={columns} rows={sessions} getRowKey={(row) => row.id}
rowActions={(row) => [{ label: t('sessions_close'), icon: LogOut, danger: true, onClick: () => confirm(() => revoke(row.id), t('sessions_confirm'), row.current) }]} />}
</div>;
}