forked from baron/baron-sso
92e607aee8 기준 code check 오류 수정
This commit is contained in:
@@ -17,11 +17,7 @@ const Table = React.forwardRef<
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={commonTableWrapperClass}>
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn(commonTableClass, className)}
|
||||
{...props}
|
||||
/>
|
||||
<table ref={ref} className={cn(commonTableClass, className)} {...props} />
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
@@ -30,7 +26,11 @@ const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn(commonTableHeaderClass, className)} {...props} />
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(commonTableHeaderClass, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
@@ -38,11 +38,7 @@ const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn(commonTableBodyClass, className)}
|
||||
{...props}
|
||||
/>
|
||||
<tbody ref={ref} className={cn(commonTableBodyClass, className)} {...props} />
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
@@ -62,11 +58,7 @@ const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(commonTableRowClass, className)}
|
||||
{...props}
|
||||
/>
|
||||
<tr ref={ref} className={cn(commonTableRowClass, className)} {...props} />
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
@@ -74,11 +66,7 @@ const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(commonTableHeadClass, className)}
|
||||
{...props}
|
||||
/>
|
||||
<th ref={ref} className={cn(commonTableHeadClass, className)} {...props} />
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
@@ -86,11 +74,7 @@ const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(commonTableCellClass, className)}
|
||||
{...props}
|
||||
/>
|
||||
<td ref={ref} className={cn(commonTableCellClass, className)} {...props} />
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { 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 {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ArrowUpDown,
|
||||
Building2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -79,7 +83,12 @@ import {
|
||||
importTenantsCSV,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
import { normalizeAdminRole } from "../../../lib/roles";
|
||||
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree";
|
||||
import {
|
||||
filterNonHanmacFamilyTenants,
|
||||
isHanmacFamilyUser,
|
||||
} from "../../users/orgChartPicker";
|
||||
import { isSeedTenant } from "../utils/protectedTenants";
|
||||
import {
|
||||
type TenantImportPreviewRow,
|
||||
@@ -93,8 +102,14 @@ import {
|
||||
|
||||
const tenantCSVTemplate =
|
||||
"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 TenantListRow = TenantSummary & { recursiveMemberCount: number };
|
||||
|
||||
const getTenantIcon = (type?: string) => {
|
||||
switch (type?.toUpperCase()) {
|
||||
@@ -234,12 +249,11 @@ function TenantListPage() {
|
||||
const [viewMode, setViewMode] = React.useState<"list" | "hierarchy">("list");
|
||||
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [sortConfig, setSortConfig] = React.useState<SortConfig<TenantSortKey> | null>(
|
||||
{
|
||||
const [sortConfig, setSortConfig] =
|
||||
React.useState<SortConfig<TenantSortKey> | null>({
|
||||
key: "createdAt",
|
||||
direction: "desc",
|
||||
},
|
||||
);
|
||||
});
|
||||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const [importMessage, setImportMessage] = React.useState("");
|
||||
const [previewRows, setPreviewRows] = React.useState<
|
||||
@@ -255,15 +269,17 @@ function TenantListPage() {
|
||||
Record<number, string>
|
||||
>({});
|
||||
const [previewOpen, setPreviewOpen] = React.useState(false);
|
||||
const tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: fetchMe,
|
||||
});
|
||||
const profileRole = normalizeAdminRole(profile?.role);
|
||||
|
||||
// Redirect tenant_admin ONLY if they have one or fewer manageable tenants in the list
|
||||
React.useEffect(() => {
|
||||
if (profile?.role === "tenant_admin") {
|
||||
if (profile && profileRole === "tenant_admin") {
|
||||
const manageableCount = profile.manageableTenants?.length ?? 0;
|
||||
if (
|
||||
(manageableCount === 1 || manageableCount === 0) &&
|
||||
@@ -272,15 +288,24 @@ function TenantListPage() {
|
||||
navigate(`/tenants/${profile.tenantId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}, [profile, navigate]);
|
||||
}, [profile, profileRole, navigate]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["tenants", { limit: 1000, offset: 0 }],
|
||||
queryFn: () => fetchTenants(1000, 0),
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["tenants", "lazy"],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchTenants(
|
||||
tenantPageSize,
|
||||
0,
|
||||
undefined,
|
||||
pageParam ? pageParam : undefined,
|
||||
),
|
||||
initialPageParam: "",
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.nextCursor || lastPage.next_cursor || undefined,
|
||||
enabled:
|
||||
profile?.role === "super_admin" ||
|
||||
(profile?.role === "tenant_admin" &&
|
||||
(profile.manageableTenants?.length ?? 0) > 1),
|
||||
profileRole === "super_admin" ||
|
||||
(profileRole === "tenant_admin" &&
|
||||
(profile?.manageableTenants?.length ?? 0) > 1),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -345,8 +370,8 @@ function TenantListPage() {
|
||||
|
||||
if (
|
||||
profile &&
|
||||
profile.role !== "super_admin" &&
|
||||
profile.role !== "tenant_admin"
|
||||
profileRole !== "super_admin" &&
|
||||
profileRole !== "tenant_admin"
|
||||
) {
|
||||
return (
|
||||
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||
@@ -361,7 +386,7 @@ function TenantListPage() {
|
||||
}
|
||||
|
||||
if (
|
||||
profile?.role === "tenant_admin" &&
|
||||
profileRole === "tenant_admin" &&
|
||||
(profile.manageableTenants?.length ?? 0) <= 1
|
||||
) {
|
||||
return null;
|
||||
@@ -374,7 +399,28 @@ function TenantListPage() {
|
||||
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
||||
: null;
|
||||
|
||||
const allTenants = query.data?.items ?? [];
|
||||
const tenantPages = query.data?.pages ?? [];
|
||||
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 =
|
||||
buildTenantImportParentOptionGroups(allTenants);
|
||||
const tenantSortResolvers = React.useMemo<
|
||||
@@ -389,15 +435,8 @@ function TenantListPage() {
|
||||
[],
|
||||
);
|
||||
const tenants = React.useMemo(() => {
|
||||
// 1. Calculate recursive counts
|
||||
// buildTenantFullTree returns subTree which represents roots, but it also mutates the mapped nodes internally.
|
||||
// However, to easily map them back to a flat list, we can just run the builder,
|
||||
// and then extract the recursive counts.
|
||||
const treeResult = buildTenantFullTree(allTenants);
|
||||
|
||||
// Flatten the tree or just extract from allTenants map?
|
||||
// buildTenantFullTree does NOT mutate the objects passed in allTenants. It creates new ones.
|
||||
// Let's create a map of id -> recursiveMemberCount
|
||||
const recursiveCounts = new Map<string, number>();
|
||||
const extractCounts = (nodes: TenantNode[]) => {
|
||||
for (const node of nodes) {
|
||||
@@ -424,6 +463,56 @@ function TenantListPage() {
|
||||
return sortItems(enriched, 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) => {
|
||||
setSortConfig((current) => toggleSort(current, key));
|
||||
};
|
||||
@@ -599,6 +688,94 @@ function TenantListPage() {
|
||||
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.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>
|
||||
);
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -727,7 +904,7 @@ function TenantListPage() {
|
||||
"msg.admin.tenants.registry.count",
|
||||
"총 {{count}}개 테넌트",
|
||||
{
|
||||
count: query.data?.total ?? 0,
|
||||
count: tenantTotal,
|
||||
},
|
||||
)}
|
||||
</CardDescription>
|
||||
@@ -770,7 +947,12 @@ function TenantListPage() {
|
||||
className="flex-1 flex flex-col min-h-0 m-0"
|
||||
>
|
||||
<div className={commonTableShellClass}>
|
||||
<div className={commonTableViewportClass}>
|
||||
<div
|
||||
className={commonTableViewportClass}
|
||||
ref={tenantTableScrollRef}
|
||||
onScroll={handleTenantTableScroll}
|
||||
data-testid="tenant-table-scroll"
|
||||
>
|
||||
<Table className="min-w-[1180px]">
|
||||
<TableHeader className={sortableTableHeaderClassName}>
|
||||
<TableRow>
|
||||
@@ -844,7 +1026,18 @@ function TenantListPage() {
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody
|
||||
className={
|
||||
shouldVirtualizeTenants ? "relative block" : undefined
|
||||
}
|
||||
style={
|
||||
shouldVirtualizeTenants
|
||||
? {
|
||||
height: `${tenantRowVirtualizer.getTotalSize()}px`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{query.isLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9}>
|
||||
@@ -865,102 +1058,26 @@ function TenantListPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{tenants.map((tenant) => (
|
||||
<TableRow key={tenant.id}>
|
||||
<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.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>
|
||||
))}
|
||||
{shouldVirtualizeTenants
|
||||
? virtualTenantRows.map((virtualRow) => {
|
||||
const tenant = tenants[virtualRow.index];
|
||||
if (!tenant) {
|
||||
return null;
|
||||
}
|
||||
return renderTenantRow(tenant, {
|
||||
virtualIndex: virtualRow.index,
|
||||
style: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
display: "table",
|
||||
tableLayout: "fixed",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
},
|
||||
});
|
||||
})
|
||||
: tenants.map((tenant) => renderTenantRow(tenant))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -601,6 +601,13 @@ function UserListPage() {
|
||||
sortConfig={sortConfig}
|
||||
sortKey="status"
|
||||
/>
|
||||
<SortableTableHead
|
||||
className="whitespace-nowrap"
|
||||
label={t("ui.admin.users.list.table.role", "ROLE")}
|
||||
onSort={requestSort}
|
||||
sortConfig={sortConfig}
|
||||
sortKey="role"
|
||||
/>
|
||||
<SortableTableHead
|
||||
className="whitespace-nowrap"
|
||||
label={t(
|
||||
@@ -638,7 +645,7 @@ function UserListPage() {
|
||||
{query.isLoading && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6 + userSchema.length}
|
||||
colSpan={7 + userSchema.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t("msg.common.loading", "로딩 중...")}
|
||||
@@ -648,7 +655,7 @@ function UserListPage() {
|
||||
{!query.isLoading && items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6 + userSchema.length}
|
||||
colSpan={7 + userSchema.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t(
|
||||
|
||||
Reference in New Issue
Block a user