This commit is contained in:
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table';
|
import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table';
|
||||||
import { Button, Modal } from '../../components/ui';
|
import { Button, Modal } from '../../components/ui';
|
||||||
import { PageTitle } from '../../layout/AppLayout';
|
import { PageTitle } from '../../layout/AppLayout';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
import { loadUserSessions, lockUser, terminateUserSession, unlockUser, type UserSession } from './operations-api';
|
import { loadUserSessions, lockUser, terminateUserSession, unlockUser, type UserSession } from './operations-api';
|
||||||
import { formatDateTime, LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils';
|
import { formatDateTime, LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils';
|
||||||
|
|
||||||
@@ -56,11 +57,14 @@ export function UserSessionsPage() {
|
|||||||
{
|
{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
header: '사용자',
|
header: '사용자',
|
||||||
className: 'w-[240px] px-5 py-3 font-medium',
|
className: 'w-[280px] px-5 py-3 font-medium',
|
||||||
cell: (row) => (
|
cell: (row) => (
|
||||||
<div className="min-w-0">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<p className="truncate font-semibold">{row.user}</p>
|
<UserAvatar session={row} />
|
||||||
{row.email ? <p className="mt-1 truncate text-xs font-medium text-muted-foreground">{row.email}</p> : null}
|
<div className="min-w-0">
|
||||||
|
<p className="truncate font-semibold">{row.user}</p>
|
||||||
|
{row.email ? <p className="mt-1 truncate text-xs font-medium text-muted-foreground">{row.email}</p> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -174,13 +178,16 @@ export function UserSessionsPage() {
|
|||||||
{detailSession ? (
|
{detailSession ? (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex min-w-0 items-start justify-between gap-4 rounded-md border border-border bg-muted/20 px-4 py-3">
|
<div className="flex min-w-0 items-start justify-between gap-4 rounded-md border border-border bg-muted/20 px-4 py-3">
|
||||||
<div className="min-w-0">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<p className="truncate text-sm font-semibold text-foreground">{detailSession.user}</p>
|
<UserAvatar session={detailSession} size="lg" />
|
||||||
<p className="mt-1 truncate text-xs text-muted-foreground">{detailSession.email ?? '-'}</p>
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-sm font-semibold text-foreground">{detailSession.user}</p>
|
||||||
|
<p className="mt-1 truncate text-xs text-muted-foreground">{detailSession.email ?? '-'}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge status={detailSession.state} />
|
<StatusBadge status={detailSession.state} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 text-sm sm:grid-cols-2">
|
<div className="grid gap-3 text-sm">
|
||||||
<DetailItem label="사용자 ID" value={detailSession.userId} mono />
|
<DetailItem label="사용자 ID" value={detailSession.userId} mono />
|
||||||
<DetailItem label="조직" value={detailSession.primaryAffiliation?.organizationName ?? '-'} />
|
<DetailItem label="조직" value={detailSession.primaryAffiliation?.organizationName ?? '-'} />
|
||||||
<DetailItem label="조직 유형" value={organizationTypeLabel(detailSession.primaryAffiliation?.organizationType)} />
|
<DetailItem label="조직 유형" value={organizationTypeLabel(detailSession.primaryAffiliation?.organizationType)} />
|
||||||
@@ -212,11 +219,36 @@ function organizationTypeLabel(type?: string | null) {
|
|||||||
return type ? labels[type] ?? type : '-';
|
return type ? labels[type] ?? type : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailItem({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
|
function UserAvatar({ session, size = 'sm' }: { session: UserSession; size?: 'sm' | 'lg' }) {
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
const initial = (session.user || session.email || '?').trim().charAt(0).toUpperCase() || '?';
|
||||||
|
const sizeClassName = size === 'lg' ? 'size-14 text-lg' : 'size-9 text-sm';
|
||||||
|
|
||||||
|
if (session.avatarUrl && !failed) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={session.avatarUrl}
|
||||||
|
alt=""
|
||||||
|
className={cn(sizeClassName, 'shrink-0 rounded-full border border-border bg-muted object-cover')}
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-0 rounded-md border border-border bg-background px-3 py-2">
|
<span className={cn(sizeClassName, 'inline-flex shrink-0 items-center justify-center rounded-full border border-border bg-muted font-semibold text-muted-foreground')}>
|
||||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
{initial}
|
||||||
<p className={`mt-1 truncate text-sm text-foreground ${mono ? 'font-mono text-xs' : 'font-medium'}`}>{value || '-'}</p>
|
</span>
|
||||||
</div>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailItem({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
|
||||||
|
return (
|
||||||
|
<label className="block min-w-0">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
||||||
|
<div className={cn('mt-2 min-h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-medium text-foreground', mono && 'break-all font-mono text-xs font-normal')}>
|
||||||
|
{value || '-'}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type UserSession = {
|
|||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
user: string;
|
user: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
avatarUrl?: string | null;
|
||||||
role: string;
|
role: string;
|
||||||
session: string;
|
session: string;
|
||||||
expiresAt?: string | null;
|
expiresAt?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user