update the adminpage ui
This commit is contained in:
131
src/features/settings/components/ActiveSessionsPanel.tsx
Normal file
131
src/features/settings/components/ActiveSessionsPanel.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import {proportional, pixel} from '@astryxdesign/core/Table';
|
||||
import type {TableColumn} from '@astryxdesign/core/Table';
|
||||
import {HStack} from '@astryxdesign/core/Layout';
|
||||
import {Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {Button} from '@astryxdesign/core/Button';
|
||||
import {useToast} from '@astryxdesign/core/Toast';
|
||||
import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable';
|
||||
import {PanelCard} from '@/shared/components/patterns/PanelCard';
|
||||
import {SkeletonRows} from '@/shared/components/patterns/LoadingState';
|
||||
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
|
||||
import {useResource} from '@/shared/hooks/useResource';
|
||||
import {securityRepository} from '@/features/settings/repositories/securityRepository';
|
||||
import type {DeviceSession} from '@/features/settings/services/mapSession';
|
||||
|
||||
/**
|
||||
* Every device signed in as the current person, from GET /api/auth/sessions.
|
||||
*
|
||||
* Shared by the merchant Security settings and the platform console's
|
||||
* Settings: `/api/auth/*` is scoped to whoever is signed in, not to a tenant,
|
||||
* so it answers for a platform admin exactly as it does for a merchant. One
|
||||
* component so the two consoles cannot drift on what "sign out" means.
|
||||
*
|
||||
* Nothing optimistic: after a revoke the list is re-read, because the platform
|
||||
* decides what is still live.
|
||||
*/
|
||||
export function ActiveSessionsPanel() {
|
||||
const toast = useToast();
|
||||
const sessions = useResource(securityRepository.sessions());
|
||||
|
||||
const handleRevokeSession = async (id: string) => {
|
||||
const res = await securityRepository.revoke(id);
|
||||
if (!res.ok) {
|
||||
toast({type: 'error', body: res.message ?? 'Could not sign that device out.'});
|
||||
return;
|
||||
}
|
||||
sessions.refetch();
|
||||
toast({body: 'That device has been signed out.'});
|
||||
};
|
||||
|
||||
const handleRevokeAllOther = async () => {
|
||||
const res = await securityRepository.revokeOthers();
|
||||
if (!res.ok) {
|
||||
toast({type: 'error', body: res.message ?? 'Could not sign the other devices out.'});
|
||||
return;
|
||||
}
|
||||
sessions.refetch();
|
||||
toast({body: 'Every other device has been signed out.'});
|
||||
};
|
||||
|
||||
const columns: TableColumn<DeviceSession & Record<string, unknown>>[] = [
|
||||
{
|
||||
key: 'device',
|
||||
header: 'Device',
|
||||
width: proportional(2),
|
||||
renderCell: (row) => (
|
||||
<HStack gap={2} vAlign="center">
|
||||
<Text size="sm" weight="medium">
|
||||
{row.device}
|
||||
</Text>
|
||||
{row.isCurrent ? <Badge variant="success" label="This browser" /> : null}
|
||||
</HStack>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'lastUsedAt',
|
||||
header: 'Last used',
|
||||
width: proportional(1.2),
|
||||
renderCell: (row) => (
|
||||
<Text size="sm" color="secondary">
|
||||
{new Date(row.lastUsedAt).toLocaleString()}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'Action',
|
||||
align: 'center',
|
||||
width: pixel(120),
|
||||
// The current session CAN be revoked upstream, but doing it from this
|
||||
// table signs you out mid-page with no warning. Signing out is what the
|
||||
// account menu is for.
|
||||
renderCell: (row) =>
|
||||
row.isCurrent ? null : (
|
||||
<HStack hAlign="center">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label="Sign out"
|
||||
onClick={() => void handleRevokeSession(row.id)}
|
||||
/>
|
||||
</HStack>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PanelCard
|
||||
title="Active sessions"
|
||||
subtitle="Every device signed in as you."
|
||||
resource={sessions}
|
||||
loading={<SkeletonRows count={3} />}
|
||||
empty={
|
||||
<EmptyPanel
|
||||
icon="security"
|
||||
title="No other devices"
|
||||
description="Only this browser is signed in."
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
label="Sign out everywhere else"
|
||||
onClick={() => void handleRevokeAllOther()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(rows) => (
|
||||
<ResponsiveTable
|
||||
columns={columns}
|
||||
data={rows as (DeviceSession & Record<string, unknown>)[]}
|
||||
idKey="id"
|
||||
primaryKey="device"
|
||||
/>
|
||||
)}
|
||||
</PanelCard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user