forked from baron/baron-sso
테넌트 레지스트리 테이블 UI 복원
This commit is contained in:
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
type UseMutationResult,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
@@ -41,7 +36,6 @@ import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
@@ -69,7 +63,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../components/ui/select";
|
||||
import { Switch } from "../../../components/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -89,7 +82,6 @@ import {
|
||||
type TenantImportDetail,
|
||||
type TenantImportResult,
|
||||
type TenantSummary,
|
||||
type UserProfileResponse,
|
||||
updateTenant,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
@@ -416,27 +408,6 @@ function TenantListPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const statusMutation = useMutation({
|
||||
mutationFn: ({
|
||||
tenantId,
|
||||
status,
|
||||
}: {
|
||||
tenantId: string;
|
||||
status: string;
|
||||
}) => updateTenant(tenantId, { status }),
|
||||
onSuccess: () => {
|
||||
query.refetch();
|
||||
toast.success(
|
||||
t("msg.admin.tenants.bulk.update_success", "선택한 테넌트들의 상태가 수정되었습니다."),
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
t("msg.admin.tenants.bulk.update_error", "테넌트 일괄 상태 변경에 실패했습니다."),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const handleApplyBulkStatus = () => {
|
||||
if (selectedIds.length === 0 || !selectedBulkStatus) return;
|
||||
bulkUpdateStatusMutation.mutate({
|
||||
@@ -504,7 +475,6 @@ function TenantListPage() {
|
||||
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
||||
: null;
|
||||
|
||||
const tenantTotal = query.data?.pages[0]?.total ?? 0;
|
||||
const hanmacFamilyTenantId = React.useMemo(() => {
|
||||
const envTenantId = import.meta.env.VITE_HANMAC_FAMILY_TENANT_ID;
|
||||
if (typeof envTenantId === "string" && envTenantId.trim()) {
|
||||
@@ -962,15 +932,6 @@ function TenantListPage() {
|
||||
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
||||
{t("ui.admin.tenants.registry.title", "Tenant Registry")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"msg.admin.tenants.registry.count",
|
||||
"총 {{count}}개의 테넌트가 등록되어 있습니다.",
|
||||
{
|
||||
count: scopeTenantId ? scopedTenants.length : tenantTotal,
|
||||
},
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -988,8 +949,6 @@ function TenantListPage() {
|
||||
onSelectAll={handleSelectAll}
|
||||
search={search}
|
||||
deletableTenants={deletableTenants}
|
||||
statusMutation={statusMutation}
|
||||
profile={profile}
|
||||
sortConfig={sortConfig}
|
||||
requestSort={requestSort}
|
||||
getSortIcon={getSortIcon}
|
||||
@@ -1566,13 +1525,6 @@ const TenantHierarchyView: React.FC<{
|
||||
onSelectAll: (checked: boolean) => void;
|
||||
search: string;
|
||||
deletableTenants: TenantSummary[];
|
||||
statusMutation: UseMutationResult<
|
||||
TenantSummary,
|
||||
Error,
|
||||
{ tenantId: string; status: string },
|
||||
unknown
|
||||
>;
|
||||
profile: UserProfileResponse | undefined;
|
||||
sortConfig: SortConfig<TenantSortKey> | null;
|
||||
requestSort: (key: TenantSortKey) => void;
|
||||
getSortIcon: (key: TenantSortKey) => React.ReactNode;
|
||||
@@ -1589,8 +1541,6 @@ const TenantHierarchyView: React.FC<{
|
||||
onSelectAll,
|
||||
search,
|
||||
deletableTenants,
|
||||
statusMutation,
|
||||
profile,
|
||||
sortConfig,
|
||||
requestSort,
|
||||
getSortIcon,
|
||||
@@ -1829,69 +1779,99 @@ const TenantHierarchyView: React.FC<{
|
||||
className="mr-2 flex-shrink-0 text-muted-foreground"
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<Link
|
||||
to={`/tenants/${node.id}`}
|
||||
className="cursor-pointer truncate text-primary hover:underline"
|
||||
>
|
||||
{node.name}
|
||||
</Link>
|
||||
{isSeedTenant(node) && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex-shrink-0 text-[10px]"
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<Link
|
||||
to={`/tenants/${node.id}`}
|
||||
className="block max-w-full truncate text-foreground transition-colors hover:text-primary hover:underline"
|
||||
>
|
||||
{t("ui.admin.tenants.seed_badge", "초기 설정")}
|
||||
</Badge>
|
||||
)}
|
||||
{node.name}
|
||||
</Link>
|
||||
{isSeedTenant(node) && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex-shrink-0 text-[10px]"
|
||||
>
|
||||
{t("ui.admin.tenants.seed_badge", "초기 설정")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{(() => {
|
||||
const parentPath = tenantParentPathMap.get(node.id) ?? [];
|
||||
return (
|
||||
<p className="mt-0.5 truncate text-xs font-normal text-muted-foreground">
|
||||
{parentPath.length > 0
|
||||
? parentPath.join(" / ")
|
||||
: t("ui.admin.tenants.path.root", "최상위")}
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
|
||||
className="whitespace-nowrap"
|
||||
data-testid={`tenant-internal-id-${node.id}`}
|
||||
>
|
||||
{node.id}
|
||||
<code className="inline-block rounded-md bg-secondary/60 px-2 py-1 font-mono text-xs text-muted-foreground">
|
||||
{node.id}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<Badge variant="outline" className="font-mono text-[10px]">
|
||||
{node.type}
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium uppercase tracking-[0.04em]",
|
||||
getTenantTypeTextClass(node.type),
|
||||
)}
|
||||
>
|
||||
{getTenantTypeLabel(node.type)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<code className="inline-flex max-w-full items-center rounded-md bg-secondary/60 px-2 py-1 font-mono text-xs text-muted-foreground">
|
||||
{node.slug}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<Badge
|
||||
variant={node.status === "active" ? "default" : "muted"}
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs uppercase",
|
||||
node.status === "active"
|
||||
? "border-transparent bg-blue-500 text-white hover:bg-blue-500/90 hover:text-white"
|
||||
: "border-border bg-secondary/60 text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{node.status === "active"
|
||||
? t("ui.common.status.active", "활성")
|
||||
: t("ui.common.status.inactive", "비활성")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{node.slug}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={node.status === "active"}
|
||||
onCheckedChange={(checked) =>
|
||||
statusMutation.mutate({
|
||||
tenantId: node.id,
|
||||
status: checked ? "active" : "inactive",
|
||||
})
|
||||
}
|
||||
disabled={
|
||||
statusMutation.isPending ||
|
||||
node.id === profile?.tenantId ||
|
||||
isSeedTenant(node)
|
||||
}
|
||||
aria-label={t(
|
||||
"ui.admin.tenants.toggle_status",
|
||||
"{{name}} 활성 상태",
|
||||
{ name: node.name },
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t(`ui.common.status.${node.status}`, node.status)}
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="font-medium">
|
||||
{t("ui.admin.tenants.table.members_count", "{{count}}명", {
|
||||
count: node.recursiveMemberCount,
|
||||
})}
|
||||
</span>
|
||||
<span className="mt-0.5 text-xs text-muted-foreground">
|
||||
{t("ui.admin.tenants.table.members_recursive", "하위 포함")}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{node.recursiveMemberCount}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap text-xs">
|
||||
{node.updatedAt
|
||||
? new Date(node.updatedAt).toLocaleString("ko-KR")
|
||||
: "-"}
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{node.updatedAt ? (
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="text-xs">
|
||||
{new Date(node.updatedAt).toLocaleDateString("ko-KR")}
|
||||
</span>
|
||||
<span className="mt-0.5 text-xs text-muted-foreground">
|
||||
{new Date(node.updatedAt).toLocaleTimeString("ko-KR")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -1280,7 +1280,9 @@ pick = "Select parent scope"
|
||||
[ui.admin.tenants.table]
|
||||
actions = "ACTIONS"
|
||||
id = "ID"
|
||||
members_count = "{{count}} members"
|
||||
members = "Members"
|
||||
members_recursive = "Includes descendants"
|
||||
name = "NAME"
|
||||
slug = "SLUG"
|
||||
status = "STATUS"
|
||||
|
||||
@@ -1283,7 +1283,9 @@ pick = "상위 범위 선택"
|
||||
[ui.admin.tenants.table]
|
||||
actions = "ACTIONS"
|
||||
id = "ID"
|
||||
members_count = "{{count}}명"
|
||||
members = "멤버수"
|
||||
members_recursive = "하위 포함"
|
||||
name = "이름"
|
||||
slug = "슬러그"
|
||||
status = "상태"
|
||||
|
||||
@@ -1291,6 +1291,8 @@ slug = ""
|
||||
status = ""
|
||||
|
||||
[ui.admin.tenants.table]
|
||||
members_count = ""
|
||||
members_recursive = ""
|
||||
actions = ""
|
||||
id = ""
|
||||
members = ""
|
||||
|
||||
Reference in New Issue
Block a user