forked from baron/baron-sso
유저 그룹 계층형 보기
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import {
|
||||
CornerDownRight,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
@@ -19,14 +26,119 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { deleteTenant, fetchTenants } from "../../../lib/adminApi";
|
||||
import {
|
||||
deleteTenant,
|
||||
fetchTenants,
|
||||
type TenantSummary,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
|
||||
function TenantListPage() {
|
||||
type TenantNode = TenantSummary & { children: TenantNode[] };
|
||||
|
||||
function buildTenantTree(tenants: TenantSummary[]): TenantNode[] {
|
||||
const tenantMap = new Map<string, TenantNode>();
|
||||
const rootTenants: TenantNode[] = [];
|
||||
|
||||
for (const tenant of tenants) {
|
||||
tenantMap.set(tenant.id, { ...tenant, children: [] });
|
||||
}
|
||||
|
||||
for (const tenant of tenants) {
|
||||
const node = tenantMap.get(tenant.id)!;
|
||||
if (tenant.parentId) {
|
||||
const parent = tenantMap.get(tenant.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
rootTenants.push(node); // Orphaned
|
||||
}
|
||||
} else {
|
||||
rootTenants.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return rootTenants;
|
||||
}
|
||||
|
||||
const TenantRow: React.FC<{
|
||||
tenant: TenantNode;
|
||||
level: number;
|
||||
onDelete: (id: string, name: string) => void;
|
||||
isDeleting: boolean;
|
||||
}> = ({ tenant, level, onDelete, isDeleting }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<TableRow key={tenant.id}>
|
||||
<TableCell style={{ paddingLeft: `${1 + level * 1.5}rem` }}>
|
||||
<div className="flex items-center gap-2">
|
||||
{level > 0 && <CornerDownRight size={14} className="text-muted-foreground" />}
|
||||
<span className="font-semibold">{tenant.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-[10px] font-mono">
|
||||
{tenant.type || "PERSONAL"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{tenant.slug}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
tenant.status === "active"
|
||||
? "default"
|
||||
: tenant.status === "pending"
|
||||
? "secondary"
|
||||
: "muted"
|
||||
}
|
||||
>
|
||||
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{tenant.updatedAt
|
||||
? new Date(tenant.updatedAt).toLocaleString("ko-KR")
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/tenants/${tenant.id}`)}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
{t("ui.common.edit", "편집")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onDelete(tenant.id, tenant.name)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
{t("ui.common.delete", "삭제")}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{tenant.children.map((child) => (
|
||||
<TenantRow
|
||||
key={child.id}
|
||||
tenant={child}
|
||||
level={level + 1}
|
||||
onDelete={onDelete}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function TenantListPage() {
|
||||
const query = useQuery({
|
||||
queryKey: ["tenants", { limit: 50, offset: 0 }],
|
||||
queryFn: () => fetchTenants(50, 0),
|
||||
queryKey: ["tenants", { limit: 1000, offset: 0 }], // Fetch all to build tree
|
||||
queryFn: () => fetchTenants(1000, 0),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -43,7 +155,7 @@ function TenantListPage() {
|
||||
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
||||
: null;
|
||||
|
||||
const items = query.data?.items ?? [];
|
||||
const tenantTree = query.data?.items ? buildTenantTree(query.data.items) : [];
|
||||
|
||||
const handleDelete = (tenantId: string, tenantName: string) => {
|
||||
if (
|
||||
@@ -148,14 +260,14 @@ function TenantListPage() {
|
||||
<TableBody>
|
||||
{query.isLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>
|
||||
<TableCell colSpan={6}>
|
||||
{t("msg.common.loading", "로딩 중...")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!query.isLoading && items.length === 0 && (
|
||||
{!query.isLoading && tenantTree.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
{t(
|
||||
"msg.admin.tenants.empty",
|
||||
"아직 등록된 테넌트가 없습니다.",
|
||||
@@ -163,60 +275,14 @@ function TenantListPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{items.map((tenant) => (
|
||||
<TableRow key={tenant.id}>
|
||||
<TableCell className="font-semibold">{tenant.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-[10px] font-mono">
|
||||
{tenant.type || "PERSONAL"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{tenant.slug}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
tenant.status === "active"
|
||||
? "default"
|
||||
: tenant.status === "pending"
|
||||
? "secondary"
|
||||
: "muted"
|
||||
}
|
||||
className={
|
||||
tenant.status === "pending"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{tenant.updatedAt
|
||||
? new Date(tenant.updatedAt).toLocaleString("ko-KR")
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/tenants/${tenant.id}`)}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
{t("ui.common.edit", "편집")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(tenant.id, tenant.name)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
{t("ui.common.delete", "삭제")}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{tenantTree.map((tenant) => (
|
||||
<TenantRow
|
||||
key={tenant.id}
|
||||
tenant={tenant}
|
||||
level={0}
|
||||
onDelete={handleDelete}
|
||||
isDeleting={deleteMutation.isPending}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -227,3 +293,4 @@ function TenantListPage() {
|
||||
}
|
||||
|
||||
export default TenantListPage;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user