forked from baron/baron-sso
adminfront 사용자/테넌트 테이블 쉘 공통화
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
@@ -25,10 +24,14 @@ import {
|
|||||||
sortableTableHeaderClassName,
|
sortableTableHeaderClassName,
|
||||||
} from "../../../../../common/core/components/sort";
|
} from "../../../../../common/core/components/sort";
|
||||||
import {
|
import {
|
||||||
type SortConfig,
|
commonTableShellClass,
|
||||||
type SortResolverMap,
|
commonTableViewportClass,
|
||||||
|
} from "../../../../../common/ui/table";
|
||||||
|
import {
|
||||||
sortItems,
|
sortItems,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
|
type SortConfig,
|
||||||
|
type SortResolverMap,
|
||||||
} from "../../../../../common/core/utils";
|
} from "../../../../../common/core/utils";
|
||||||
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
||||||
import { Badge } from "../../../components/ui/badge";
|
import { Badge } from "../../../components/ui/badge";
|
||||||
@@ -76,12 +79,7 @@ import {
|
|||||||
importTenantsCSV,
|
importTenantsCSV,
|
||||||
} from "../../../lib/adminApi";
|
} from "../../../lib/adminApi";
|
||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
import { normalizeAdminRole } from "../../../lib/roles";
|
|
||||||
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
|
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
|
||||||
import {
|
|
||||||
filterNonHanmacFamilyTenants,
|
|
||||||
isHanmacFamilyUser,
|
|
||||||
} from "../../users/orgChartPicker";
|
|
||||||
import { isSeedTenant } from "../utils/protectedTenants";
|
import { isSeedTenant } from "../utils/protectedTenants";
|
||||||
import {
|
import {
|
||||||
type TenantImportPreviewRow,
|
type TenantImportPreviewRow,
|
||||||
@@ -95,14 +93,8 @@ import {
|
|||||||
|
|
||||||
const tenantCSVTemplate =
|
const tenantCSVTemplate =
|
||||||
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n";
|
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n";
|
||||||
const tenantPageSize = 500;
|
|
||||||
const tenantVirtualizationThreshold = 250;
|
|
||||||
const tenantEstimatedRowHeight = 73;
|
|
||||||
const tenantLoadAheadPx = 360;
|
|
||||||
const tenantLoadAheadRows = 30;
|
|
||||||
|
|
||||||
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
|
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
|
||||||
type TenantListRow = TenantSummary & { recursiveMemberCount: number };
|
|
||||||
|
|
||||||
const getTenantIcon = (type?: string) => {
|
const getTenantIcon = (type?: string) => {
|
||||||
switch (type?.toUpperCase()) {
|
switch (type?.toUpperCase()) {
|
||||||
@@ -263,17 +255,15 @@ function TenantListPage() {
|
|||||||
Record<number, string>
|
Record<number, string>
|
||||||
>({});
|
>({});
|
||||||
const [previewOpen, setPreviewOpen] = React.useState(false);
|
const [previewOpen, setPreviewOpen] = React.useState(false);
|
||||||
const tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const { data: profile } = useQuery({
|
const { data: profile } = useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
queryFn: fetchMe,
|
queryFn: fetchMe,
|
||||||
});
|
});
|
||||||
const profileRole = normalizeAdminRole(profile?.role);
|
|
||||||
|
|
||||||
// Redirect tenant_admin ONLY if they have one or fewer manageable tenants in the list
|
// Redirect tenant_admin ONLY if they have one or fewer manageable tenants in the list
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (profile && profileRole === "tenant_admin") {
|
if (profile?.role === "tenant_admin") {
|
||||||
const manageableCount = profile.manageableTenants?.length ?? 0;
|
const manageableCount = profile.manageableTenants?.length ?? 0;
|
||||||
if (
|
if (
|
||||||
(manageableCount === 1 || manageableCount === 0) &&
|
(manageableCount === 1 || manageableCount === 0) &&
|
||||||
@@ -282,24 +272,15 @@ function TenantListPage() {
|
|||||||
navigate(`/tenants/${profile.tenantId}`, { replace: true });
|
navigate(`/tenants/${profile.tenantId}`, { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [profile, profileRole, navigate]);
|
}, [profile, navigate]);
|
||||||
|
|
||||||
const query = useInfiniteQuery({
|
const query = useQuery({
|
||||||
queryKey: ["tenants", "lazy"],
|
queryKey: ["tenants", { limit: 1000, offset: 0 }],
|
||||||
queryFn: ({ pageParam }) =>
|
queryFn: () => fetchTenants(1000, 0),
|
||||||
fetchTenants(
|
|
||||||
tenantPageSize,
|
|
||||||
0,
|
|
||||||
undefined,
|
|
||||||
pageParam ? pageParam : undefined,
|
|
||||||
),
|
|
||||||
initialPageParam: "",
|
|
||||||
getNextPageParam: (lastPage) =>
|
|
||||||
lastPage.nextCursor || lastPage.next_cursor || undefined,
|
|
||||||
enabled:
|
enabled:
|
||||||
profileRole === "super_admin" ||
|
profile?.role === "super_admin" ||
|
||||||
(profileRole === "tenant_admin" &&
|
(profile?.role === "tenant_admin" &&
|
||||||
(profile?.manageableTenants?.length ?? 0) > 1),
|
(profile.manageableTenants?.length ?? 0) > 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
@@ -364,8 +345,8 @@ function TenantListPage() {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
profile &&
|
profile &&
|
||||||
profileRole !== "super_admin" &&
|
profile.role !== "super_admin" &&
|
||||||
profileRole !== "tenant_admin"
|
profile.role !== "tenant_admin"
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||||
@@ -380,8 +361,7 @@ function TenantListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
profile &&
|
profile?.role === "tenant_admin" &&
|
||||||
profileRole === "tenant_admin" &&
|
|
||||||
(profile.manageableTenants?.length ?? 0) <= 1
|
(profile.manageableTenants?.length ?? 0) <= 1
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -394,28 +374,7 @@ function TenantListPage() {
|
|||||||
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const tenantPages = query.data?.pages ?? [];
|
const allTenants = query.data?.items ?? [];
|
||||||
const rawTenants = tenantPages.flatMap((page) => page.items);
|
|
||||||
const tenantTotal = tenantPages[0]?.total ?? 0;
|
|
||||||
const hanmacFamilyTenantId = React.useMemo(() => {
|
|
||||||
const envTenantId = import.meta.env.VITE_HANMAC_FAMILY_TENANT_ID;
|
|
||||||
if (typeof envTenantId === "string" && envTenantId.trim()) {
|
|
||||||
return envTenantId.trim();
|
|
||||||
}
|
|
||||||
return rawTenants.find((tenant) => tenant.slug === "hanmac-family")?.id;
|
|
||||||
}, [rawTenants]);
|
|
||||||
const allTenants = React.useMemo(() => {
|
|
||||||
if (profileRole === "super_admin") {
|
|
||||||
return rawTenants;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
profile &&
|
|
||||||
isHanmacFamilyUser(profile, rawTenants, hanmacFamilyTenantId)
|
|
||||||
) {
|
|
||||||
return rawTenants;
|
|
||||||
}
|
|
||||||
return filterNonHanmacFamilyTenants(rawTenants, hanmacFamilyTenantId);
|
|
||||||
}, [hanmacFamilyTenantId, profile, profileRole, rawTenants]);
|
|
||||||
const importParentOptionGroups =
|
const importParentOptionGroups =
|
||||||
buildTenantImportParentOptionGroups(allTenants);
|
buildTenantImportParentOptionGroups(allTenants);
|
||||||
const tenantSortResolvers = React.useMemo<
|
const tenantSortResolvers = React.useMemo<
|
||||||
@@ -465,56 +424,6 @@ function TenantListPage() {
|
|||||||
return sortItems(enriched, sortConfig, tenantSortResolvers);
|
return sortItems(enriched, sortConfig, tenantSortResolvers);
|
||||||
}, [allTenants, search, sortConfig, tenantSortResolvers]);
|
}, [allTenants, search, sortConfig, tenantSortResolvers]);
|
||||||
|
|
||||||
const shouldVirtualizeTenants =
|
|
||||||
tenants.length >= tenantVirtualizationThreshold;
|
|
||||||
const tenantRowVirtualizer = useVirtualizer({
|
|
||||||
count: tenants.length,
|
|
||||||
getScrollElement: () => tenantTableScrollRef.current,
|
|
||||||
estimateSize: () => tenantEstimatedRowHeight,
|
|
||||||
overscan: 12,
|
|
||||||
enabled: shouldVirtualizeTenants,
|
|
||||||
});
|
|
||||||
const virtualTenantRows = shouldVirtualizeTenants
|
|
||||||
? tenantRowVirtualizer.getVirtualItems()
|
|
||||||
: [];
|
|
||||||
const lastVirtualTenantIndex =
|
|
||||||
virtualTenantRows[virtualTenantRows.length - 1]?.index ?? -1;
|
|
||||||
|
|
||||||
const fetchNextTenantPage = React.useCallback(() => {
|
|
||||||
if (query.hasNextPage && !query.isFetchingNextPage) {
|
|
||||||
void query.fetchNextPage();
|
|
||||||
}
|
|
||||||
}, [query.fetchNextPage, query.hasNextPage, query.isFetchingNextPage]);
|
|
||||||
|
|
||||||
const handleTenantTableScroll = React.useCallback(
|
|
||||||
(event: React.UIEvent<HTMLDivElement>) => {
|
|
||||||
const scrollElement = event.currentTarget;
|
|
||||||
const distanceToEnd =
|
|
||||||
scrollElement.scrollHeight -
|
|
||||||
scrollElement.scrollTop -
|
|
||||||
scrollElement.clientHeight;
|
|
||||||
if (distanceToEnd <= tenantLoadAheadPx) {
|
|
||||||
fetchNextTenantPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fetchNextTenantPage],
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (
|
|
||||||
!shouldVirtualizeTenants ||
|
|
||||||
lastVirtualTenantIndex < tenants.length - tenantLoadAheadRows
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchNextTenantPage();
|
|
||||||
}, [
|
|
||||||
fetchNextTenantPage,
|
|
||||||
lastVirtualTenantIndex,
|
|
||||||
shouldVirtualizeTenants,
|
|
||||||
tenants.length,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const requestSort = (key: TenantSortKey) => {
|
const requestSort = (key: TenantSortKey) => {
|
||||||
setSortConfig((current) => toggleSort(current, key));
|
setSortConfig((current) => toggleSort(current, key));
|
||||||
};
|
};
|
||||||
@@ -690,96 +599,6 @@ function TenantListPage() {
|
|||||||
deleteMutation.mutate(tenantId);
|
deleteMutation.mutate(tenantId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTenantRow = (
|
|
||||||
tenant: TenantListRow,
|
|
||||||
options?: {
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
virtualIndex?: number;
|
|
||||||
},
|
|
||||||
) => (
|
|
||||||
<TableRow
|
|
||||||
key={tenant.id}
|
|
||||||
data-index={options?.virtualIndex}
|
|
||||||
ref={
|
|
||||||
options?.virtualIndex === undefined
|
|
||||||
? undefined
|
|
||||||
: tenantRowVirtualizer.measureElement
|
|
||||||
}
|
|
||||||
style={options?.style}
|
|
||||||
>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
{isSeedTenant(tenant) ? (
|
|
||||||
<span className="inline-block h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedIds.includes(tenant.id)}
|
|
||||||
onCheckedChange={(checked) => handleSelect(tenant, !!checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
|
|
||||||
data-testid={`tenant-internal-id-${tenant.id}`}
|
|
||||||
>
|
|
||||||
{tenant.id}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-semibold">
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<Link
|
|
||||||
to={`/tenants/${tenant.id}`}
|
|
||||||
className="hover:underline text-primary cursor-pointer"
|
|
||||||
>
|
|
||||||
{tenant.name}
|
|
||||||
</Link>
|
|
||||||
{isSeedTenant(tenant) && (
|
|
||||||
<Badge variant="secondary" className="text-[10px]">
|
|
||||||
{t("ui.admin.tenants.seed_badge", "초기 설정")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="whitespace-nowrap">
|
|
||||||
<Badge variant="outline" className="text-[10px] font-mono">
|
|
||||||
{tenant.type}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">{tenant.slug}</TableCell>
|
|
||||||
<TableCell className="whitespace-nowrap">
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
tenant.status === "active"
|
|
||||||
? "default"
|
|
||||||
: tenant.status === "pending"
|
|
||||||
? "secondary"
|
|
||||||
: "muted"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{tenant.recursiveMemberCount}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="whitespace-nowrap text-xs">
|
|
||||||
{tenant.updatedAt
|
|
||||||
? new Date(tenant.updatedAt).toLocaleString("ko-KR")
|
|
||||||
: "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
disabled={isSeedTenant(tenant) || deleteMutation.isPending}
|
|
||||||
onClick={() => handleDelete(tenant.id, tenant.name)}
|
|
||||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
>
|
|
||||||
<Trash2 size={14} className="mr-2" />
|
|
||||||
{t("ui.common.delete", "삭제")}
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
|
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
|
||||||
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4">
|
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4">
|
||||||
@@ -908,7 +727,7 @@ function TenantListPage() {
|
|||||||
"msg.admin.tenants.registry.count",
|
"msg.admin.tenants.registry.count",
|
||||||
"총 {{count}}개 테넌트",
|
"총 {{count}}개 테넌트",
|
||||||
{
|
{
|
||||||
count: tenantTotal,
|
count: query.data?.total ?? 0,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -950,13 +769,8 @@ function TenantListPage() {
|
|||||||
value="list"
|
value="list"
|
||||||
className="flex-1 flex flex-col min-h-0 m-0"
|
className="flex-1 flex flex-col min-h-0 m-0"
|
||||||
>
|
>
|
||||||
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
|
<div className={commonTableShellClass}>
|
||||||
<div
|
<div className={commonTableViewportClass}>
|
||||||
ref={tenantTableScrollRef}
|
|
||||||
className="flex-1 overflow-auto relative custom-scrollbar"
|
|
||||||
data-testid="tenant-table-scroll"
|
|
||||||
onScroll={handleTenantTableScroll}
|
|
||||||
>
|
|
||||||
<Table className="min-w-[1180px]">
|
<Table className="min-w-[1180px]">
|
||||||
<TableHeader className={sortableTableHeaderClassName}>
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -1030,18 +844,7 @@ function TenantListPage() {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody
|
<TableBody>
|
||||||
className={
|
|
||||||
shouldVirtualizeTenants ? "relative block" : undefined
|
|
||||||
}
|
|
||||||
style={
|
|
||||||
shouldVirtualizeTenants
|
|
||||||
? {
|
|
||||||
height: `${tenantRowVirtualizer.getTotalSize()}px`,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{query.isLoading && (
|
{query.isLoading && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={9}>
|
<TableCell colSpan={9}>
|
||||||
@@ -1062,26 +865,102 @@ function TenantListPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{shouldVirtualizeTenants
|
{tenants.map((tenant) => (
|
||||||
? virtualTenantRows.map((virtualRow) => {
|
<TableRow key={tenant.id}>
|
||||||
const tenant = tenants[virtualRow.index];
|
<TableCell className="text-center">
|
||||||
if (!tenant) {
|
{isSeedTenant(tenant) ? (
|
||||||
return null;
|
<span className="inline-block h-4 w-4" />
|
||||||
}
|
) : (
|
||||||
return renderTenantRow(tenant, {
|
<Checkbox
|
||||||
virtualIndex: virtualRow.index,
|
checked={selectedIds.includes(tenant.id)}
|
||||||
style: {
|
onCheckedChange={(checked) =>
|
||||||
position: "absolute",
|
handleSelect(tenant, !!checked)
|
||||||
top: 0,
|
}
|
||||||
left: 0,
|
/>
|
||||||
width: "100%",
|
)}
|
||||||
display: "table",
|
</TableCell>
|
||||||
tableLayout: "fixed",
|
<TableCell
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
|
||||||
},
|
data-testid={`tenant-internal-id-${tenant.id}`}
|
||||||
});
|
>
|
||||||
})
|
{tenant.id}
|
||||||
: tenants.map((tenant) => renderTenantRow(tenant))}
|
</TableCell>
|
||||||
|
<TableCell className="font-semibold">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Link
|
||||||
|
to={`/tenants/${tenant.id}`}
|
||||||
|
className="hover:underline text-primary cursor-pointer"
|
||||||
|
>
|
||||||
|
{tenant.name}
|
||||||
|
</Link>
|
||||||
|
{isSeedTenant(tenant) && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px]"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.seed_badge",
|
||||||
|
"초기 설정",
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] font-mono"
|
||||||
|
>
|
||||||
|
{tenant.type}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
{tenant.slug}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
tenant.status === "active"
|
||||||
|
? "default"
|
||||||
|
: tenant.status === "pending"
|
||||||
|
? "secondary"
|
||||||
|
: "muted"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
`ui.common.status.${tenant.status}`,
|
||||||
|
tenant.status,
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{tenant.recursiveMemberCount}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap text-xs">
|
||||||
|
{tenant.createdAt
|
||||||
|
? new Date(tenant.createdAt).toLocaleString(
|
||||||
|
"ko-KR",
|
||||||
|
)
|
||||||
|
: "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={
|
||||||
|
isSeedTenant(tenant) || deleteMutation.isPending
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
handleDelete(tenant.id, tenant.name)
|
||||||
|
}
|
||||||
|
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} className="mr-2" />
|
||||||
|
{t("ui.common.delete", "삭제")}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import {
|
|||||||
sortableTableHeadBaseClassName,
|
sortableTableHeadBaseClassName,
|
||||||
sortableTableHeaderClassName,
|
sortableTableHeaderClassName,
|
||||||
} from "../../../../common/core/components/sort";
|
} from "../../../../common/core/components/sort";
|
||||||
|
import {
|
||||||
|
commonTableShellClass,
|
||||||
|
commonTableViewportClass,
|
||||||
|
} from "../../../../common/ui/table";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -555,8 +559,8 @@ function UserListPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
|
<div className={commonTableShellClass}>
|
||||||
<div className="flex-1 overflow-auto relative custom-scrollbar">
|
<div className={commonTableViewportClass}>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className={sortableTableHeaderClassName}>
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user