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 { Button, Modal } from '../../components/ui';
|
||||
import { PageTitle } from '../../layout/AppLayout';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { loadUserSessions, lockUser, terminateUserSession, unlockUser, type UserSession } from './operations-api';
|
||||
import { formatDateTime, LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils';
|
||||
|
||||
@@ -56,11 +57,14 @@ export function UserSessionsPage() {
|
||||
{
|
||||
key: 'user',
|
||||
header: '사용자',
|
||||
className: 'w-[240px] px-5 py-3 font-medium',
|
||||
className: 'w-[280px] px-5 py-3 font-medium',
|
||||
cell: (row) => (
|
||||
<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 className="flex min-w-0 items-center gap-3">
|
||||
<UserAvatar session={row} />
|
||||
<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>
|
||||
)
|
||||
},
|
||||
@@ -174,13 +178,16 @@ export function UserSessionsPage() {
|
||||
{detailSession ? (
|
||||
<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="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 className="flex min-w-0 items-center gap-3">
|
||||
<UserAvatar session={detailSession} size="lg" />
|
||||
<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>
|
||||
<StatusBadge status={detailSession.state} />
|
||||
</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="조직" value={detailSession.primaryAffiliation?.organizationName ?? '-'} />
|
||||
<DetailItem label="조직 유형" value={organizationTypeLabel(detailSession.primaryAffiliation?.organizationType)} />
|
||||
@@ -212,11 +219,36 @@ function organizationTypeLabel(type?: string | null) {
|
||||
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 (
|
||||
<div className="min-w-0 rounded-md border border-border bg-background px-3 py-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||
<p className={`mt-1 truncate text-sm text-foreground ${mono ? 'font-mono text-xs' : 'font-medium'}`}>{value || '-'}</p>
|
||||
</div>
|
||||
<span className={cn(sizeClassName, 'inline-flex shrink-0 items-center justify-center rounded-full border border-border bg-muted font-semibold text-muted-foreground')}>
|
||||
{initial}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
user: string;
|
||||
email?: string;
|
||||
avatarUrl?: string | null;
|
||||
role: string;
|
||||
session: string;
|
||||
expiresAt?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user