forked from baron/baron-sso
92e607aee8 기준 code check 오류 수정
This commit is contained in:
@@ -17,11 +17,7 @@ const Table = React.forwardRef<
|
|||||||
React.HTMLAttributes<HTMLTableElement>
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className={commonTableWrapperClass}>
|
<div className={commonTableWrapperClass}>
|
||||||
<table
|
<table ref={ref} className={cn(commonTableClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
Table.displayName = "Table";
|
Table.displayName = "Table";
|
||||||
@@ -30,7 +26,11 @@ const TableHeader = React.forwardRef<
|
|||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn(commonTableHeaderClass, className)} {...props} />
|
<thead
|
||||||
|
ref={ref}
|
||||||
|
className={cn(commonTableHeaderClass, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
TableHeader.displayName = "TableHeader";
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
@@ -38,11 +38,7 @@ const TableBody = React.forwardRef<
|
|||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody
|
<tbody ref={ref} className={cn(commonTableBodyClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableBodyClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableBody.displayName = "TableBody";
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
@@ -62,11 +58,7 @@ const TableRow = React.forwardRef<
|
|||||||
HTMLTableRowElement,
|
HTMLTableRowElement,
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr ref={ref} className={cn(commonTableRowClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableRowClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableRow.displayName = "TableRow";
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
@@ -74,11 +66,7 @@ const TableHead = React.forwardRef<
|
|||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th ref={ref} className={cn(commonTableHeadClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableHeadClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableHead.displayName = "TableHead";
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
@@ -86,11 +74,7 @@ const TableCell = React.forwardRef<
|
|||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td ref={ref} className={cn(commonTableCellClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableCellClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableCell.displayName = "TableCell";
|
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 type { AxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowUpDown,
|
||||||
Building2,
|
Building2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -79,7 +83,12 @@ 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,
|
||||||
@@ -93,8 +102,14 @@ 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()) {
|
||||||
@@ -234,12 +249,11 @@ function TenantListPage() {
|
|||||||
const [viewMode, setViewMode] = React.useState<"list" | "hierarchy">("list");
|
const [viewMode, setViewMode] = React.useState<"list" | "hierarchy">("list");
|
||||||
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
const [sortConfig, setSortConfig] = React.useState<SortConfig<TenantSortKey> | null>(
|
const [sortConfig, setSortConfig] =
|
||||||
{
|
React.useState<SortConfig<TenantSortKey> | null>({
|
||||||
key: "createdAt",
|
key: "createdAt",
|
||||||
direction: "desc",
|
direction: "desc",
|
||||||
},
|
});
|
||||||
);
|
|
||||||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
const [importMessage, setImportMessage] = React.useState("");
|
const [importMessage, setImportMessage] = React.useState("");
|
||||||
const [previewRows, setPreviewRows] = React.useState<
|
const [previewRows, setPreviewRows] = React.useState<
|
||||||
@@ -255,15 +269,17 @@ 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?.role === "tenant_admin") {
|
if (profile && profileRole === "tenant_admin") {
|
||||||
const manageableCount = profile.manageableTenants?.length ?? 0;
|
const manageableCount = profile.manageableTenants?.length ?? 0;
|
||||||
if (
|
if (
|
||||||
(manageableCount === 1 || manageableCount === 0) &&
|
(manageableCount === 1 || manageableCount === 0) &&
|
||||||
@@ -272,15 +288,24 @@ function TenantListPage() {
|
|||||||
navigate(`/tenants/${profile.tenantId}`, { replace: true });
|
navigate(`/tenants/${profile.tenantId}`, { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [profile, navigate]);
|
}, [profile, profileRole, navigate]);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useInfiniteQuery({
|
||||||
queryKey: ["tenants", { limit: 1000, offset: 0 }],
|
queryKey: ["tenants", "lazy"],
|
||||||
queryFn: () => fetchTenants(1000, 0),
|
queryFn: ({ pageParam }) =>
|
||||||
|
fetchTenants(
|
||||||
|
tenantPageSize,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
pageParam ? pageParam : undefined,
|
||||||
|
),
|
||||||
|
initialPageParam: "",
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.nextCursor || lastPage.next_cursor || undefined,
|
||||||
enabled:
|
enabled:
|
||||||
profile?.role === "super_admin" ||
|
profileRole === "super_admin" ||
|
||||||
(profile?.role === "tenant_admin" &&
|
(profileRole === "tenant_admin" &&
|
||||||
(profile.manageableTenants?.length ?? 0) > 1),
|
(profile?.manageableTenants?.length ?? 0) > 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
@@ -345,8 +370,8 @@ function TenantListPage() {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
profile &&
|
profile &&
|
||||||
profile.role !== "super_admin" &&
|
profileRole !== "super_admin" &&
|
||||||
profile.role !== "tenant_admin"
|
profileRole !== "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">
|
||||||
@@ -361,7 +386,7 @@ function TenantListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
profile?.role === "tenant_admin" &&
|
profileRole === "tenant_admin" &&
|
||||||
(profile.manageableTenants?.length ?? 0) <= 1
|
(profile.manageableTenants?.length ?? 0) <= 1
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -374,7 +399,28 @@ function TenantListPage() {
|
|||||||
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
|
||||||
: null;
|
: 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 =
|
const importParentOptionGroups =
|
||||||
buildTenantImportParentOptionGroups(allTenants);
|
buildTenantImportParentOptionGroups(allTenants);
|
||||||
const tenantSortResolvers = React.useMemo<
|
const tenantSortResolvers = React.useMemo<
|
||||||
@@ -389,15 +435,8 @@ function TenantListPage() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const tenants = React.useMemo(() => {
|
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);
|
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 recursiveCounts = new Map<string, number>();
|
||||||
const extractCounts = (nodes: TenantNode[]) => {
|
const extractCounts = (nodes: TenantNode[]) => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -424,6 +463,56 @@ 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));
|
||||||
};
|
};
|
||||||
@@ -599,6 +688,94 @@ 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.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 (
|
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">
|
||||||
@@ -727,7 +904,7 @@ function TenantListPage() {
|
|||||||
"msg.admin.tenants.registry.count",
|
"msg.admin.tenants.registry.count",
|
||||||
"총 {{count}}개 테넌트",
|
"총 {{count}}개 테넌트",
|
||||||
{
|
{
|
||||||
count: query.data?.total ?? 0,
|
count: tenantTotal,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -770,7 +947,12 @@ function TenantListPage() {
|
|||||||
className="flex-1 flex flex-col min-h-0 m-0"
|
className="flex-1 flex flex-col min-h-0 m-0"
|
||||||
>
|
>
|
||||||
<div className={commonTableShellClass}>
|
<div className={commonTableShellClass}>
|
||||||
<div className={commonTableViewportClass}>
|
<div
|
||||||
|
className={commonTableViewportClass}
|
||||||
|
ref={tenantTableScrollRef}
|
||||||
|
onScroll={handleTenantTableScroll}
|
||||||
|
data-testid="tenant-table-scroll"
|
||||||
|
>
|
||||||
<Table className="min-w-[1180px]">
|
<Table className="min-w-[1180px]">
|
||||||
<TableHeader className={sortableTableHeaderClassName}>
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -844,7 +1026,18 @@ 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}>
|
||||||
@@ -865,102 +1058,26 @@ function TenantListPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{tenants.map((tenant) => (
|
{shouldVirtualizeTenants
|
||||||
<TableRow key={tenant.id}>
|
? virtualTenantRows.map((virtualRow) => {
|
||||||
<TableCell className="text-center">
|
const tenant = tenants[virtualRow.index];
|
||||||
{isSeedTenant(tenant) ? (
|
if (!tenant) {
|
||||||
<span className="inline-block h-4 w-4" />
|
return null;
|
||||||
) : (
|
}
|
||||||
<Checkbox
|
return renderTenantRow(tenant, {
|
||||||
checked={selectedIds.includes(tenant.id)}
|
virtualIndex: virtualRow.index,
|
||||||
onCheckedChange={(checked) =>
|
style: {
|
||||||
handleSelect(tenant, !!checked)
|
position: "absolute",
|
||||||
}
|
top: 0,
|
||||||
/>
|
left: 0,
|
||||||
)}
|
width: "100%",
|
||||||
</TableCell>
|
display: "table",
|
||||||
<TableCell
|
tableLayout: "fixed",
|
||||||
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
data-testid={`tenant-internal-id-${tenant.id}`}
|
},
|
||||||
>
|
});
|
||||||
{tenant.id}
|
})
|
||||||
</TableCell>
|
: tenants.map((tenant) => renderTenantRow(tenant))}
|
||||||
<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>
|
||||||
|
|||||||
@@ -601,6 +601,13 @@ function UserListPage() {
|
|||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="status"
|
sortKey="status"
|
||||||
/>
|
/>
|
||||||
|
<SortableTableHead
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
label={t("ui.admin.users.list.table.role", "ROLE")}
|
||||||
|
onSort={requestSort}
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
sortKey="role"
|
||||||
|
/>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
label={t(
|
label={t(
|
||||||
@@ -638,7 +645,7 @@ function UserListPage() {
|
|||||||
{query.isLoading && (
|
{query.isLoading && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={6 + userSchema.length}
|
colSpan={7 + userSchema.length}
|
||||||
className="h-24 text-center"
|
className="h-24 text-center"
|
||||||
>
|
>
|
||||||
{t("msg.common.loading", "로딩 중...")}
|
{t("msg.common.loading", "로딩 중...")}
|
||||||
@@ -648,7 +655,7 @@ function UserListPage() {
|
|||||||
{!query.isLoading && items.length === 0 && (
|
{!query.isLoading && items.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={6 + userSchema.length}
|
colSpan={7 + userSchema.length}
|
||||||
className="h-24 text-center"
|
className="h-24 text-center"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ unknown_error = "unknown error"
|
|||||||
actions = "Actions"
|
actions = "Actions"
|
||||||
add = "Add"
|
add = "Add"
|
||||||
all = "All"
|
all = "All"
|
||||||
|
apply = "Apply"
|
||||||
admin_only = "Admin Only"
|
admin_only = "Admin Only"
|
||||||
apply = "Apply"
|
apply = "Apply"
|
||||||
approve = "Approve"
|
approve = "Approve"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ unknown_error = "알 수 없는 오류"
|
|||||||
actions = "액션"
|
actions = "액션"
|
||||||
add = "추가"
|
add = "추가"
|
||||||
all = "전체"
|
all = "전체"
|
||||||
|
apply = "적용"
|
||||||
admin_only = "관리자 전용"
|
admin_only = "관리자 전용"
|
||||||
apply = "적용"
|
apply = "적용"
|
||||||
approve = "승인"
|
approve = "승인"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ unknown_error = ""
|
|||||||
actions = ""
|
actions = ""
|
||||||
add = ""
|
add = ""
|
||||||
all = ""
|
all = ""
|
||||||
|
apply = ""
|
||||||
admin_only = ""
|
admin_only = ""
|
||||||
apply = ""
|
apply = ""
|
||||||
approve = ""
|
approve = ""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const commonTableWrapperClass = "relative w-full overflow-auto";
|
export const commonTableWrapperClass = "relative w-full";
|
||||||
export const commonTableClass = "w-full caption-bottom text-sm";
|
export const commonTableClass = "w-full caption-bottom text-sm";
|
||||||
export const commonTableHeaderClass = "[&_tr]:border-b";
|
export const commonTableHeaderClass = "[&_tr]:border-b";
|
||||||
export const commonTableBodyClass = "[&_tr:last-child]:border-0";
|
export const commonTableBodyClass = "[&_tr:last-child]:border-0";
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ const Table = React.forwardRef<
|
|||||||
React.HTMLAttributes<HTMLTableElement>
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className={commonTableWrapperClass}>
|
<div className={commonTableWrapperClass}>
|
||||||
<table
|
<table ref={ref} className={cn(commonTableClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
Table.displayName = "Table";
|
Table.displayName = "Table";
|
||||||
@@ -30,7 +26,11 @@ const TableHeader = React.forwardRef<
|
|||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn(commonTableHeaderClass, className)} {...props} />
|
<thead
|
||||||
|
ref={ref}
|
||||||
|
className={cn(commonTableHeaderClass, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
TableHeader.displayName = "TableHeader";
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
@@ -38,11 +38,7 @@ const TableBody = React.forwardRef<
|
|||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody
|
<tbody ref={ref} className={cn(commonTableBodyClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableBodyClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableBody.displayName = "TableBody";
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
@@ -62,11 +58,7 @@ const TableRow = React.forwardRef<
|
|||||||
HTMLTableRowElement,
|
HTMLTableRowElement,
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr ref={ref} className={cn(commonTableRowClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableRowClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableRow.displayName = "TableRow";
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
@@ -74,11 +66,7 @@ const TableHead = React.forwardRef<
|
|||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th ref={ref} className={cn(commonTableHeadClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableHeadClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableHead.displayName = "TableHead";
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
@@ -86,11 +74,7 @@ const TableCell = React.forwardRef<
|
|||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td ref={ref} className={cn(commonTableCellClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableCellClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableCell.displayName = "TableCell";
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
|||||||
@@ -288,154 +288,164 @@ function AuditLogsPage() {
|
|||||||
<div className={commonTableViewportClass}>
|
<div className={commonTableViewportClass}>
|
||||||
<Table className="table-fixed">
|
<Table className="table-fixed">
|
||||||
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
|
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[190px]">
|
<TableHead className="w-[190px]">
|
||||||
{t("ui.dev.audit.table.time", "Time")}
|
{t("ui.dev.audit.table.time", "Time")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[180px]">
|
<TableHead className="w-[180px]">
|
||||||
{t("ui.dev.audit.table.actor", "Actor")}
|
{t("ui.dev.audit.table.actor", "Actor")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[180px]">
|
<TableHead className="w-[180px]">
|
||||||
{t("ui.dev.audit.table.action", "Action")}
|
{t("ui.dev.audit.table.action", "Action")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[260px]">
|
<TableHead className="w-[260px]">
|
||||||
{t("ui.dev.audit.table.target", "Target")}
|
{t("ui.dev.audit.table.target", "Target")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[120px]">
|
<TableHead className="w-[120px]">
|
||||||
{t("ui.dev.audit.table.status", "Status")}
|
{t("ui.dev.audit.table.status", "Status")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[80px]" />
|
<TableHead className="w-[80px]" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{query.isLoading && logs.length === 0 ? (
|
{query.isLoading && logs.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={6}
|
colSpan={6}
|
||||||
className="py-8 text-center text-muted-foreground"
|
className="py-8 text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t("msg.dev.audit.loading", "Loading audit logs...")}
|
{t("msg.dev.audit.loading", "Loading audit logs...")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : logs.length === 0 ? (
|
) : logs.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={6}
|
colSpan={6}
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t("msg.dev.audit.empty", "No audit logs found.")}
|
{t("msg.dev.audit.empty", "No audit logs found.")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
logs.map((row, index) => {
|
logs.map((row, index) => {
|
||||||
const details = parseDetails(row.details);
|
const details = parseDetails(row.details);
|
||||||
const actionLabel = details.action || row.event_type;
|
const actionLabel = details.action || row.event_type;
|
||||||
const targetValue = details.target_id || "-";
|
const targetValue = details.target_id || "-";
|
||||||
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
|
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
|
||||||
const expanded = Boolean(expandedRows[rowKey]);
|
const expanded = Boolean(expandedRows[rowKey]);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={rowKey}>
|
<React.Fragment key={rowKey}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="text-xs text-muted-foreground">
|
<TableCell className="text-xs text-muted-foreground">
|
||||||
{formatDateTime(row.timestamp)}
|
{formatDateTime(row.timestamp)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">
|
<TableCell className="font-mono text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{row.user_id || "-"}</span>
|
<span>{row.user_id || "-"}</span>
|
||||||
{row.user_id ? (
|
{row.user_id ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-muted-foreground"
|
||||||
|
onClick={() => handleCopy(row.user_id)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs">
|
||||||
|
{actionLabel}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="break-all">
|
||||||
|
{targetValue}
|
||||||
|
</span>
|
||||||
|
{targetValue !== "-" ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-muted-foreground"
|
||||||
|
onClick={() => handleCopy(targetValue)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
row.status === "success"
|
||||||
|
? "success"
|
||||||
|
: "warning"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
className="h-7 w-7 text-muted-foreground"
|
onClick={() =>
|
||||||
onClick={() => handleCopy(row.user_id)}
|
setExpandedRows((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[rowKey]: !expanded,
|
||||||
|
}))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Copy className="h-3 w-3" />
|
{expanded ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
</TableCell>
|
||||||
</div>
|
</TableRow>
|
||||||
</TableCell>
|
{expanded ? (
|
||||||
<TableCell className="text-xs">
|
<TableRow className="bg-card/20">
|
||||||
{actionLabel}
|
<TableCell
|
||||||
</TableCell>
|
colSpan={6}
|
||||||
<TableCell className="font-mono text-xs">
|
className="text-xs text-muted-foreground"
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="break-all">{targetValue}</span>
|
|
||||||
{targetValue !== "-" ? (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 text-muted-foreground"
|
|
||||||
onClick={() => handleCopy(targetValue)}
|
|
||||||
>
|
>
|
||||||
<Copy className="h-3 w-3" />
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
</Button>
|
<div className="space-y-1">
|
||||||
) : null}
|
<div>
|
||||||
</div>
|
Request ID:{" "}
|
||||||
</TableCell>
|
{formatValue(details.request_id)}
|
||||||
<TableCell>
|
</div>
|
||||||
<Badge
|
<div>
|
||||||
variant={
|
Method: {formatValue(details.method)}
|
||||||
row.status === "success" ? "success" : "warning"
|
</div>
|
||||||
}
|
<div>
|
||||||
>
|
Path: {formatValue(details.path)}
|
||||||
{row.status}
|
</div>
|
||||||
</Badge>
|
<div>
|
||||||
</TableCell>
|
Tenant: {formatValue(details.tenant_id)}
|
||||||
<TableCell className="text-right">
|
</div>
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
<div className="space-y-1 break-all">
|
||||||
size="sm"
|
<div>
|
||||||
onClick={() =>
|
Before: {formatValue(details.before)}
|
||||||
setExpandedRows((prev) => ({
|
</div>
|
||||||
...prev,
|
<div>
|
||||||
[rowKey]: !expanded,
|
After: {formatValue(details.after)}
|
||||||
}))
|
</div>
|
||||||
}
|
<div>
|
||||||
>
|
Error: {formatValue(details.error)}
|
||||||
{expanded ? (
|
</div>
|
||||||
<ChevronUp className="h-4 w-4" />
|
</div>
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{expanded ? (
|
|
||||||
<TableRow className="bg-card/20">
|
|
||||||
<TableCell
|
|
||||||
colSpan={6}
|
|
||||||
className="text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div>
|
|
||||||
Request ID:{" "}
|
|
||||||
{formatValue(details.request_id)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</TableCell>
|
||||||
Method: {formatValue(details.method)}
|
</TableRow>
|
||||||
</div>
|
) : null}
|
||||||
<div>Path: {formatValue(details.path)}</div>
|
</React.Fragment>
|
||||||
<div>
|
);
|
||||||
Tenant: {formatValue(details.tenant_id)}
|
})
|
||||||
</div>
|
)}
|
||||||
</div>
|
</TableBody>
|
||||||
<div className="space-y-1 break-all">
|
|
||||||
<div>
|
|
||||||
Before: {formatValue(details.before)}
|
|
||||||
</div>
|
|
||||||
<div>After: {formatValue(details.after)}</div>
|
|
||||||
<div>Error: {formatValue(details.error)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : null}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -449,7 +449,10 @@ function ClientConsentsPage() {
|
|||||||
{t("ui.dev.clients.consents.table.status", "Status")}
|
{t("ui.dev.clients.consents.table.status", "Status")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.dev.clients.consents.table.scopes", "Granted Scopes")}
|
{t(
|
||||||
|
"ui.dev.clients.consents.table.scopes",
|
||||||
|
"Granted Scopes",
|
||||||
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{t(
|
{t(
|
||||||
@@ -553,7 +556,10 @@ function ClientConsentsPage() {
|
|||||||
<TableCell className="text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{row.status === "revoked" && row.deletedAt ? (
|
{row.status === "revoked" && row.deletedAt ? (
|
||||||
<span className="text-destructive font-medium">
|
<span className="text-destructive font-medium">
|
||||||
{t("ui.dev.clients.consents.revoked_at", "Revoked: ")}
|
{t(
|
||||||
|
"ui.dev.clients.consents.revoked_at",
|
||||||
|
"Revoked: ",
|
||||||
|
)}
|
||||||
{new Date(row.deletedAt).toLocaleString()}
|
{new Date(row.deletedAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
) : row.authenticatedAt ? (
|
) : row.authenticatedAt ? (
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import {
|
import { BookOpenText, Filter, Plus, Search, X } from "lucide-react";
|
||||||
BookOpenText,
|
|
||||||
Filter,
|
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
@@ -128,12 +122,11 @@ function ClientsPage() {
|
|||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false);
|
const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false);
|
||||||
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig<ClientSortKey> | null>(
|
const [sortConfig, setSortConfig] =
|
||||||
{
|
useState<SortConfig<ClientSortKey> | null>({
|
||||||
key: "createdAt",
|
key: "createdAt",
|
||||||
direction: "desc",
|
direction: "desc",
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const clients = data?.items || [];
|
const clients = data?.items || [];
|
||||||
const clientSortResolvers = useMemo<
|
const clientSortResolvers = useMemo<
|
||||||
@@ -460,7 +453,10 @@ function ClientsPage() {
|
|||||||
<TableHeader className={sortableTableHeaderClassName}>
|
<TableHeader className={sortableTableHeaderClassName}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<SortableTableHead
|
<SortableTableHead
|
||||||
label={t("ui.dev.clients.table.application", "애플리케이션")}
|
label={t(
|
||||||
|
"ui.dev.clients.table.application",
|
||||||
|
"애플리케이션",
|
||||||
|
)}
|
||||||
onSort={requestSort}
|
onSort={requestSort}
|
||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
sortKey="application"
|
sortKey="application"
|
||||||
@@ -587,7 +583,10 @@ function ClientsPage() {
|
|||||||
t("ui.dev.clients.untitled", "Untitled")}
|
t("ui.dev.clients.untitled", "Untitled")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("ui.dev.clients.tenant_scoped", "Tenant-scoped")}
|
{t(
|
||||||
|
"ui.dev.clients.tenant_scoped",
|
||||||
|
"Tenant-scoped",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -625,7 +624,9 @@ function ClientsPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={client.status === "active" ? "info" : "muted"}
|
variant={
|
||||||
|
client.status === "active" ? "info" : "muted"
|
||||||
|
}
|
||||||
className="px-3 py-1 text-xs uppercase"
|
className="px-3 py-1 text-xs uppercase"
|
||||||
>
|
>
|
||||||
{client.status === "active"
|
{client.status === "active"
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ notice_emphasis = "Store it in a secure location."
|
|||||||
notice_suffix = "Rotate the key immediately if you think it has been exposed."
|
notice_suffix = "Rotate the key immediately if you think it has been exposed."
|
||||||
|
|
||||||
[msg.admin.api_keys.list]
|
[msg.admin.api_keys.list]
|
||||||
|
edit_scopes_desc = "Keep the CLIENT_ID unchanged and modify scopes only."
|
||||||
|
rotate_confirm = "API key \"{{name}}\"'s secret will be rotated. The existing secret will no longer work."
|
||||||
|
rotate_secret_notice = "The new secret is shown only once. The CLIENT_ID has not changed."
|
||||||
delete_confirm = "Are you sure you want to delete this API key?"
|
delete_confirm = "Are you sure you want to delete this API key?"
|
||||||
empty = "No API keys have been issued yet."
|
empty = "No API keys have been issued yet."
|
||||||
fetch_error = "Failed to load the API key list."
|
fetch_error = "Failed to load the API key list."
|
||||||
@@ -188,6 +191,7 @@ description = "Jump to the most frequently used administrative workflows."
|
|||||||
audit_events_24h = "24h Audit Events"
|
audit_events_24h = "24h Audit Events"
|
||||||
oidc_clients = "OIDC Clients"
|
oidc_clients = "OIDC Clients"
|
||||||
policy_gate = "Policy Gate Status"
|
policy_gate = "Policy Gate Status"
|
||||||
|
total_users = "Total Users"
|
||||||
total_tenants = "Total Tenants"
|
total_tenants = "Total Tenants"
|
||||||
|
|
||||||
[msg.admin.tenants]
|
[msg.admin.tenants]
|
||||||
@@ -197,6 +201,7 @@ delete_confirm = "Delete Tenant \\\\\\\"{{name}}\\\\\\\"?"
|
|||||||
delete_success = "Tenant deleted."
|
delete_success = "Tenant deleted."
|
||||||
empty = "No tenants have been registered yet."
|
empty = "No tenants have been registered yet."
|
||||||
fetch_error = "Failed to load the tenant list."
|
fetch_error = "Failed to load the tenant list."
|
||||||
|
export_error = "Failed to export tenants."
|
||||||
import_empty = "There are no tenant rows to import."
|
import_empty = "There are no tenant rows to import."
|
||||||
import_error = "Failed to import tenants."
|
import_error = "Failed to import tenants."
|
||||||
import_result = "Created {{created}}, updated {{updated}}, failed {{failed}}"
|
import_result = "Created {{created}}, updated {{updated}}, failed {{failed}}"
|
||||||
@@ -283,6 +288,8 @@ move_success = "{{count}} users moved successfully."
|
|||||||
parsed_count = "Parsed {{count}} rows."
|
parsed_count = "Parsed {{count}} rows."
|
||||||
schema_incompatible = "Fields not in target schema may be lost:"
|
schema_incompatible = "Fields not in target schema may be lost:"
|
||||||
schema_missing = "Missing required fields for target tenant:"
|
schema_missing = "Missing required fields for target tenant:"
|
||||||
|
status_placeholder = "Select status"
|
||||||
|
permission_placeholder = "Select permission"
|
||||||
update_success = "User info updated successfully."
|
update_success = "User info updated successfully."
|
||||||
|
|
||||||
[msg.admin.users.create]
|
[msg.admin.users.create]
|
||||||
@@ -970,6 +977,10 @@ title = "API Key Created"
|
|||||||
|
|
||||||
[ui.admin.api_keys.list]
|
[ui.admin.api_keys.list]
|
||||||
add = "Add"
|
add = "Add"
|
||||||
|
edit_scopes = "Edit Scopes"
|
||||||
|
rotate_secret = "Rotate Secret"
|
||||||
|
rotate_secret_done = "Secret Rotated"
|
||||||
|
save_scopes = "Save Scopes"
|
||||||
title = "API Key Management"
|
title = "API Key Management"
|
||||||
|
|
||||||
[ui.admin.api_keys.list.breadcrumb]
|
[ui.admin.api_keys.list.breadcrumb]
|
||||||
@@ -1116,6 +1127,7 @@ view_audit_logs = "View Audit Logs"
|
|||||||
audit_events_24h = "24h Events"
|
audit_events_24h = "24h Events"
|
||||||
oidc_clients = "OIDC Clients"
|
oidc_clients = "OIDC Clients"
|
||||||
policy_gate = "Policy Gate"
|
policy_gate = "Policy Gate"
|
||||||
|
total_users = "Total Users"
|
||||||
total_tenants = "Total Tenants"
|
total_tenants = "Total Tenants"
|
||||||
|
|
||||||
[ui.admin.profile]
|
[ui.admin.profile]
|
||||||
@@ -1378,6 +1390,7 @@ add = "Add"
|
|||||||
add_dialog_desc = "Select a tenant to add as a sub-tenant."
|
add_dialog_desc = "Select a tenant to add as a sub-tenant."
|
||||||
add_dialog_title = "Add Sub-tenant"
|
add_dialog_title = "Add Sub-tenant"
|
||||||
add_existing = "Add Existing Tenant"
|
add_existing = "Add Existing Tenant"
|
||||||
|
export = "Subtree CSV"
|
||||||
manage = "Manage"
|
manage = "Manage"
|
||||||
no_candidates = "No available tenants to add."
|
no_candidates = "No available tenants to add."
|
||||||
search_placeholder = "Search..."
|
search_placeholder = "Search..."
|
||||||
@@ -1399,6 +1412,7 @@ slug = "SLUG"
|
|||||||
status = "STATUS"
|
status = "STATUS"
|
||||||
type = "TYPE"
|
type = "TYPE"
|
||||||
updated = "UPDATED"
|
updated = "UPDATED"
|
||||||
|
created = "CREATED"
|
||||||
|
|
||||||
[ui.admin.users]
|
[ui.admin.users]
|
||||||
|
|
||||||
@@ -1416,6 +1430,8 @@ selected_count = "{{count}} users selected"
|
|||||||
start_upload = "Start Upload"
|
start_upload = "Start Upload"
|
||||||
tenant_resolution = "Tenant mapping"
|
tenant_resolution = "Tenant mapping"
|
||||||
title = "Bulk Actions"
|
title = "Bulk Actions"
|
||||||
|
status_placeholder = "Select status"
|
||||||
|
permission_placeholder = "Select permission"
|
||||||
|
|
||||||
[ui.admin.users.create]
|
[ui.admin.users.create]
|
||||||
back = "Back"
|
back = "Back"
|
||||||
@@ -2332,6 +2348,7 @@ title = "User Info"
|
|||||||
[ui.dev.profile.org]
|
[ui.dev.profile.org]
|
||||||
company_code = "Company Code"
|
company_code = "Company Code"
|
||||||
tenant = "Tenant"
|
tenant = "Tenant"
|
||||||
|
tenant_slug = "Tenant Slug"
|
||||||
title = "Organization Info"
|
title = "Organization Info"
|
||||||
|
|
||||||
[ui.dev.profile.role]
|
[ui.dev.profile.role]
|
||||||
@@ -2514,7 +2531,7 @@ department = "Department"
|
|||||||
email = "Email"
|
email = "Email"
|
||||||
name = "Name"
|
name = "Name"
|
||||||
tenant = "Tenant"
|
tenant = "Tenant"
|
||||||
tenant_slug = "Tenant slug"
|
tenant_slug = "Tenant Slug"
|
||||||
|
|
||||||
[ui.userfront.profile.password]
|
[ui.userfront.profile.password]
|
||||||
change = "Change"
|
change = "Change"
|
||||||
|
|||||||
@@ -588,6 +588,9 @@ notice_emphasis = "지금 한 번만"
|
|||||||
notice_suffix = "표시됩니다."
|
notice_suffix = "표시됩니다."
|
||||||
|
|
||||||
[msg.admin.api_keys.list]
|
[msg.admin.api_keys.list]
|
||||||
|
edit_scopes_desc = "CLIENT_ID는 유지하고 권한만 변경합니다."
|
||||||
|
rotate_confirm = "API 키 \"{{name}}\"의 Secret을 재발급할까요? 기존 Secret은 더 이상 사용할 수 없습니다."
|
||||||
|
rotate_secret_notice = "새 Secret은 지금 한 번만 표시됩니다. CLIENT_ID는 변경되지 않았습니다."
|
||||||
delete_confirm = "API 키 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?"
|
delete_confirm = "API 키 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?"
|
||||||
empty = "등록된 API 키가 없습니다."
|
empty = "등록된 API 키가 없습니다."
|
||||||
fetch_error = "API 키 목록 조회에 실패했습니다."
|
fetch_error = "API 키 목록 조회에 실패했습니다."
|
||||||
@@ -685,6 +688,7 @@ description = "주요 운영 화면으로 바로 이동합니다."
|
|||||||
audit_events_24h = "최근 24시간 감사 로그"
|
audit_events_24h = "최근 24시간 감사 로그"
|
||||||
oidc_clients = "등록된 OIDC 클라이언트"
|
oidc_clients = "등록된 OIDC 클라이언트"
|
||||||
policy_gate = "정책 가이트 상태"
|
policy_gate = "정책 가이트 상태"
|
||||||
|
total_users = "전체 사용자 수"
|
||||||
total_tenants = "전체 테넌트 수"
|
total_tenants = "전체 테넌트 수"
|
||||||
|
|
||||||
[msg.admin.tenants]
|
[msg.admin.tenants]
|
||||||
@@ -694,6 +698,7 @@ delete_confirm = "테넌트 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?"
|
|||||||
delete_success = "테넌트가 삭제되었습니다."
|
delete_success = "테넌트가 삭제되었습니다."
|
||||||
empty = "아직 등록된 테넌트가 없습니다."
|
empty = "아직 등록된 테넌트가 없습니다."
|
||||||
fetch_error = "테넌트 목록 조회에 실패했습니다."
|
fetch_error = "테넌트 목록 조회에 실패했습니다."
|
||||||
|
export_error = "테넌트 내보내기에 실패했습니다."
|
||||||
import_empty = "임포트 파일에 테넌트 행이 없습니다."
|
import_empty = "임포트 파일에 테넌트 행이 없습니다."
|
||||||
import_error = "테넌트 임포트에 실패했습니다: {{error}}"
|
import_error = "테넌트 임포트에 실패했습니다: {{error}}"
|
||||||
import_result = "{{count}}개의 테넌트 행을 처리했습니다."
|
import_result = "{{count}}개의 테넌트 행을 처리했습니다."
|
||||||
@@ -775,6 +780,8 @@ move_success = "{{count}}명의 사용자가 성공적으로 이동되었습니
|
|||||||
parsed_count = "{{count}}행의 데이터가 파싱되었습니다."
|
parsed_count = "{{count}}행의 데이터가 파싱되었습니다."
|
||||||
schema_incompatible = "대상 테넌트 스키마에 없는 필드는 유실될 수 있습니다:"
|
schema_incompatible = "대상 테넌트 스키마에 없는 필드는 유실될 수 있습니다:"
|
||||||
schema_missing = "대상 테넌트의 필수 필드가 누락되어 있습니다:"
|
schema_missing = "대상 테넌트의 필수 필드가 누락되어 있습니다:"
|
||||||
|
status_placeholder = "상태 선택"
|
||||||
|
permission_placeholder = "권한 선택"
|
||||||
update_success = "사용자 정보가 일괄 업데이트되었습니다."
|
update_success = "사용자 정보가 일괄 업데이트되었습니다."
|
||||||
|
|
||||||
[msg.admin.users.create]
|
[msg.admin.users.create]
|
||||||
@@ -1460,6 +1467,10 @@ title = "API 키 생성 완료"
|
|||||||
|
|
||||||
[ui.admin.api_keys.list]
|
[ui.admin.api_keys.list]
|
||||||
add = "API 키 생성"
|
add = "API 키 생성"
|
||||||
|
edit_scopes = "권한 수정"
|
||||||
|
rotate_secret = "Secret 재발급"
|
||||||
|
rotate_secret_done = "Secret 재발급 완료"
|
||||||
|
save_scopes = "권한 저장"
|
||||||
title = "API 키 관리 (M2M)"
|
title = "API 키 관리 (M2M)"
|
||||||
|
|
||||||
[ui.admin.api_keys.list.breadcrumb]
|
[ui.admin.api_keys.list.breadcrumb]
|
||||||
@@ -1606,6 +1617,7 @@ view_audit_logs = "감사 로그 보기"
|
|||||||
audit_events_24h = "24시간 이벤트"
|
audit_events_24h = "24시간 이벤트"
|
||||||
oidc_clients = "OIDC 클라이언트"
|
oidc_clients = "OIDC 클라이언트"
|
||||||
policy_gate = "정책 게이트"
|
policy_gate = "정책 게이트"
|
||||||
|
total_users = "전체 사용자 수"
|
||||||
total_tenants = "전체 테넌트 수"
|
total_tenants = "전체 테넌트 수"
|
||||||
|
|
||||||
[ui.admin.profile]
|
[ui.admin.profile]
|
||||||
@@ -1841,6 +1853,7 @@ add = "하위 테넌트 추가"
|
|||||||
add_dialog_desc = "하위 테넌트로 추가할 테넌트를 선택하세요."
|
add_dialog_desc = "하위 테넌트로 추가할 테넌트를 선택하세요."
|
||||||
add_dialog_title = "하위 테넌트 추가"
|
add_dialog_title = "하위 테넌트 추가"
|
||||||
add_existing = "기존 테넌트 추가"
|
add_existing = "기존 테넌트 추가"
|
||||||
|
export = "하위 조직 CSV"
|
||||||
manage = "관리"
|
manage = "관리"
|
||||||
no_candidates = "추가 가능한 테넌트가 없습니다."
|
no_candidates = "추가 가능한 테넌트가 없습니다."
|
||||||
search_placeholder = "검색..."
|
search_placeholder = "검색..."
|
||||||
@@ -1862,6 +1875,8 @@ slug = "SLUG"
|
|||||||
status = "STATUS"
|
status = "STATUS"
|
||||||
type = "유형"
|
type = "유형"
|
||||||
updated = "UPDATED"
|
updated = "UPDATED"
|
||||||
|
created = "CREATED"
|
||||||
|
created = "CREATED"
|
||||||
|
|
||||||
[ui.admin.users]
|
[ui.admin.users]
|
||||||
|
|
||||||
@@ -1879,6 +1894,8 @@ selected_count = "{{count}}명 선택됨"
|
|||||||
start_upload = "업로드 시작"
|
start_upload = "업로드 시작"
|
||||||
tenant_resolution = "테넌트 매핑"
|
tenant_resolution = "테넌트 매핑"
|
||||||
title = "일괄 작업"
|
title = "일괄 작업"
|
||||||
|
status_placeholder = "상태 선택"
|
||||||
|
permission_placeholder = "권한 선택"
|
||||||
|
|
||||||
[ui.admin.users.create]
|
[ui.admin.users.create]
|
||||||
back = "목록으로 돌아가기"
|
back = "목록으로 돌아가기"
|
||||||
@@ -2757,6 +2774,7 @@ title = "사용자 정보"
|
|||||||
[ui.dev.profile.org]
|
[ui.dev.profile.org]
|
||||||
company_code = "회사 코드"
|
company_code = "회사 코드"
|
||||||
tenant = "테넌트"
|
tenant = "테넌트"
|
||||||
|
tenant_slug = "테넌트 Slug"
|
||||||
title = "조직 정보"
|
title = "조직 정보"
|
||||||
|
|
||||||
[ui.dev.profile.role]
|
[ui.dev.profile.role]
|
||||||
@@ -2938,7 +2956,7 @@ department = "소속"
|
|||||||
email = "이메일"
|
email = "이메일"
|
||||||
name = "이름"
|
name = "이름"
|
||||||
tenant = "소속 테넌트"
|
tenant = "소속 테넌트"
|
||||||
tenant_slug = "테넌트 slug"
|
tenant_slug = "테넌트 Slug"
|
||||||
|
|
||||||
[ui.userfront.profile.password]
|
[ui.userfront.profile.password]
|
||||||
change = "비밀번호 변경"
|
change = "비밀번호 변경"
|
||||||
|
|||||||
@@ -451,6 +451,9 @@ notice_emphasis = ""
|
|||||||
notice_suffix = ""
|
notice_suffix = ""
|
||||||
|
|
||||||
[msg.admin.api_keys.list]
|
[msg.admin.api_keys.list]
|
||||||
|
edit_scopes_desc = ""
|
||||||
|
rotate_confirm = ""
|
||||||
|
rotate_secret_notice = ""
|
||||||
delete_confirm = ""
|
delete_confirm = ""
|
||||||
empty = ""
|
empty = ""
|
||||||
fetch_error = ""
|
fetch_error = ""
|
||||||
@@ -548,6 +551,7 @@ description = ""
|
|||||||
audit_events_24h = ""
|
audit_events_24h = ""
|
||||||
oidc_clients = ""
|
oidc_clients = ""
|
||||||
policy_gate = ""
|
policy_gate = ""
|
||||||
|
total_users = ""
|
||||||
total_tenants = ""
|
total_tenants = ""
|
||||||
|
|
||||||
[msg.admin.tenants]
|
[msg.admin.tenants]
|
||||||
@@ -557,6 +561,7 @@ delete_confirm = ""
|
|||||||
delete_success = ""
|
delete_success = ""
|
||||||
empty = ""
|
empty = ""
|
||||||
fetch_error = ""
|
fetch_error = ""
|
||||||
|
export_error = ""
|
||||||
import_empty = ""
|
import_empty = ""
|
||||||
import_error = ""
|
import_error = ""
|
||||||
import_result = ""
|
import_result = ""
|
||||||
@@ -638,6 +643,8 @@ move_success = ""
|
|||||||
parsed_count = ""
|
parsed_count = ""
|
||||||
schema_incompatible = ""
|
schema_incompatible = ""
|
||||||
schema_missing = ""
|
schema_missing = ""
|
||||||
|
status_placeholder = ""
|
||||||
|
permission_placeholder = ""
|
||||||
update_success = ""
|
update_success = ""
|
||||||
|
|
||||||
[msg.admin.users.create]
|
[msg.admin.users.create]
|
||||||
@@ -1323,6 +1330,10 @@ title = ""
|
|||||||
|
|
||||||
[ui.admin.api_keys.list]
|
[ui.admin.api_keys.list]
|
||||||
add = ""
|
add = ""
|
||||||
|
edit_scopes = ""
|
||||||
|
rotate_secret = ""
|
||||||
|
rotate_secret_done = ""
|
||||||
|
save_scopes = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.admin.api_keys.list.breadcrumb]
|
[ui.admin.api_keys.list.breadcrumb]
|
||||||
@@ -1469,6 +1480,7 @@ view_audit_logs = ""
|
|||||||
audit_events_24h = ""
|
audit_events_24h = ""
|
||||||
oidc_clients = ""
|
oidc_clients = ""
|
||||||
policy_gate = ""
|
policy_gate = ""
|
||||||
|
total_users = ""
|
||||||
total_tenants = ""
|
total_tenants = ""
|
||||||
|
|
||||||
[ui.admin.profile]
|
[ui.admin.profile]
|
||||||
@@ -1487,6 +1499,9 @@ seed_badge = ""
|
|||||||
title = ""
|
title = ""
|
||||||
view_org_chart = ""
|
view_org_chart = ""
|
||||||
|
|
||||||
|
[ui.admin.tenants.sub]
|
||||||
|
export = ""
|
||||||
|
|
||||||
[ui.admin.tenants.view]
|
[ui.admin.tenants.view]
|
||||||
hierarchy = ""
|
hierarchy = ""
|
||||||
list = ""
|
list = ""
|
||||||
@@ -1740,6 +1755,7 @@ slug = ""
|
|||||||
status = ""
|
status = ""
|
||||||
type = ""
|
type = ""
|
||||||
updated = ""
|
updated = ""
|
||||||
|
created = ""
|
||||||
|
|
||||||
[ui.admin.users]
|
[ui.admin.users]
|
||||||
|
|
||||||
@@ -1757,6 +1773,8 @@ selected_count = ""
|
|||||||
start_upload = ""
|
start_upload = ""
|
||||||
tenant_resolution = ""
|
tenant_resolution = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
status_placeholder = ""
|
||||||
|
permission_placeholder = ""
|
||||||
|
|
||||||
[ui.admin.users.create]
|
[ui.admin.users.create]
|
||||||
back = ""
|
back = ""
|
||||||
@@ -2636,6 +2654,7 @@ title = ""
|
|||||||
[ui.dev.profile.org]
|
[ui.dev.profile.org]
|
||||||
company_code = ""
|
company_code = ""
|
||||||
tenant = ""
|
tenant = ""
|
||||||
|
tenant_slug = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.dev.profile.role]
|
[ui.dev.profile.role]
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ const Table = React.forwardRef<
|
|||||||
React.HTMLAttributes<HTMLTableElement>
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className={commonTableWrapperClass}>
|
<div className={commonTableWrapperClass}>
|
||||||
<table
|
<table ref={ref} className={cn(commonTableClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
Table.displayName = "Table";
|
Table.displayName = "Table";
|
||||||
@@ -30,7 +26,11 @@ const TableHeader = React.forwardRef<
|
|||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn(commonTableHeaderClass, className)} {...props} />
|
<thead
|
||||||
|
ref={ref}
|
||||||
|
className={cn(commonTableHeaderClass, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
TableHeader.displayName = "TableHeader";
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
@@ -38,11 +38,7 @@ const TableBody = React.forwardRef<
|
|||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody
|
<tbody ref={ref} className={cn(commonTableBodyClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableBodyClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableBody.displayName = "TableBody";
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
@@ -62,11 +58,7 @@ const TableRow = React.forwardRef<
|
|||||||
HTMLTableRowElement,
|
HTMLTableRowElement,
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr ref={ref} className={cn(commonTableRowClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableRowClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableRow.displayName = "TableRow";
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
@@ -74,11 +66,7 @@ const TableHead = React.forwardRef<
|
|||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th ref={ref} className={cn(commonTableHeadClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableHeadClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableHead.displayName = "TableHead";
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
@@ -86,11 +74,7 @@ const TableCell = React.forwardRef<
|
|||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td ref={ref} className={cn(commonTableCellClass, className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn(commonTableCellClass, className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TableCell.displayName = "TableCell";
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ mkdir -p reports
|
|||||||
rm -rf adminfront/node_modules
|
rm -rf adminfront/node_modules
|
||||||
|
|
||||||
tmp_dir="$(mktemp -d /tmp/baron-sso-adminfront-tests.XXXXXX)"
|
tmp_dir="$(mktemp -d /tmp/baron-sso-adminfront-tests.XXXXXX)"
|
||||||
playwright_browsers_path="$tmp_dir/ms-playwright"
|
|
||||||
mkdir -p "$tmp_dir/scripts"
|
mkdir -p "$tmp_dir/scripts"
|
||||||
cp "$repo_root/scripts/playwrightHostDeps.cjs" "$tmp_dir/scripts/"
|
cp "$repo_root/scripts/playwrightHostDeps.cjs" "$tmp_dir/scripts/"
|
||||||
|
|
||||||
@@ -162,7 +161,7 @@ fi
|
|||||||
set +e
|
set +e
|
||||||
(
|
(
|
||||||
cd "$tmp_dir/adminfront"
|
cd "$tmp_dir/adminfront"
|
||||||
PLAYWRIGHT_BROWSERS_PATH="$playwright_browsers_path" "${playwright_install_cmd[@]}"
|
"${playwright_install_cmd[@]}"
|
||||||
) 2>&1 | tee reports/adminfront-provision.log
|
) 2>&1 | tee reports/adminfront-provision.log
|
||||||
provision_exit_code=${PIPESTATUS[0]}
|
provision_exit_code=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
@@ -197,7 +196,7 @@ fi
|
|||||||
echo "==> adminfront using PORT=$port"
|
echo "==> adminfront using PORT=$port"
|
||||||
(
|
(
|
||||||
cd "$tmp_dir/adminfront"
|
cd "$tmp_dir/adminfront"
|
||||||
PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" PLAYWRIGHT_BROWSERS_PATH="$playwright_browsers_path" \
|
PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" \
|
||||||
node ./node_modules/playwright/cli.js test "${playwright_project_args[@]}"
|
node ./node_modules/playwright/cli.js test "${playwright_project_args[@]}"
|
||||||
) 2>&1 | tee reports/adminfront-test.log
|
) 2>&1 | tee reports/adminfront-test.log
|
||||||
test_exit_code=${PIPESTATUS[0]}
|
test_exit_code=${PIPESTATUS[0]}
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
cli_config:
|
cli_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -276,6 +276,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -328,18 +336,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -661,26 +669,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.30.0"
|
version: "1.26.3"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.7"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.16"
|
version: "0.6.12"
|
||||||
toml:
|
toml:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user