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