forked from baron/baron-sso
tenants 목록 툴바 레이아웃 정리
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
|||||||
sortItems,
|
sortItems,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
} from "../../../../../common/core/utils";
|
} from "../../../../../common/core/utils";
|
||||||
|
import { SearchFilterBar } from "../../../../../common/ui/search-filter-bar";
|
||||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||||
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
||||||
import { Badge } from "../../../components/ui/badge";
|
import { Badge } from "../../../components/ui/badge";
|
||||||
@@ -708,174 +709,187 @@ function TenantListPage() {
|
|||||||
"시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.",
|
"시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.",
|
||||||
)}
|
)}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<div className="min-w-0 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<SearchFilterBar
|
||||||
<div className="relative mr-2 w-64">
|
primary={
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<>
|
||||||
<Input
|
<div className="relative w-48">
|
||||||
placeholder={t(
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
"ui.admin.tenants.list.search_placeholder",
|
<Input
|
||||||
"테넌트 이름, 슬러그, UUID 검색...",
|
placeholder={t(
|
||||||
)}
|
"ui.admin.tenants.list.search_placeholder",
|
||||||
className="h-9 pl-9"
|
"테넌트 이름, 슬러그, UUID 검색...",
|
||||||
value={search}
|
)}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
className="h-9 pl-9"
|
||||||
onKeyDown={(e) => {
|
value={search}
|
||||||
if (e.key === "Enter") {
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
query.refetch();
|
onKeyDown={(e) => {
|
||||||
}
|
if (e.key === "Enter") {
|
||||||
}}
|
query.refetch();
|
||||||
/>
|
}
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex rounded-md border bg-background p-0.5"
|
className="flex rounded-md border bg-background p-0.5"
|
||||||
data-testid="tenant-view-mode-toggle"
|
data-testid="tenant-view-mode-toggle"
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={viewMode === "tree" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
className="h-8 gap-1.5"
|
|
||||||
aria-pressed={viewMode === "tree"}
|
|
||||||
onClick={() => setViewMode("tree")}
|
|
||||||
data-testid="tenant-view-tree-btn"
|
|
||||||
>
|
|
||||||
<Network size={14} />
|
|
||||||
{t("ui.admin.tenants.view.tree", "트리")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={viewMode === "table" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
className="h-8 gap-1.5"
|
|
||||||
aria-pressed={viewMode === "table"}
|
|
||||||
onClick={() => setViewMode("table")}
|
|
||||||
data-testid="tenant-view-table-btn"
|
|
||||||
>
|
|
||||||
<List size={14} />
|
|
||||||
{t("ui.admin.tenants.view.table", "평면")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={scopeTenantId ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className="h-9 gap-2"
|
|
||||||
onClick={() => setScopePickerOpen(true)}
|
|
||||||
data-testid="tenant-scope-picker-btn"
|
|
||||||
>
|
|
||||||
<Network size={16} />
|
|
||||||
{selectedScopeTenant
|
|
||||||
? t("ui.admin.tenants.scope.active", "{{name}} 하위", {
|
|
||||||
name: selectedScopeTenant.name,
|
|
||||||
})
|
|
||||||
: t("ui.admin.tenants.scope.pick", "상위 범위 선택")}
|
|
||||||
</Button>
|
|
||||||
{scopeTenantId ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-9"
|
|
||||||
onClick={() => setScopeTenantId("")}
|
|
||||||
data-testid="tenant-scope-clear-btn"
|
|
||||||
>
|
|
||||||
{t("ui.common.clear", "초기화")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<RoleGuard roles={["super_admin"]}>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".csv,text/csv"
|
|
||||||
className="hidden"
|
|
||||||
data-testid="tenant-import-input"
|
|
||||||
onChange={handleImportFile}
|
|
||||||
/>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
data-testid="tenant-data-mgmt-btn"
|
variant={viewMode === "tree" ? "default" : "ghost"}
|
||||||
className="gap-2"
|
size="sm"
|
||||||
|
className="h-9 gap-1.5"
|
||||||
|
aria-pressed={viewMode === "tree"}
|
||||||
|
onClick={() => setViewMode("tree")}
|
||||||
|
data-testid="tenant-view-tree-btn"
|
||||||
>
|
>
|
||||||
<LayoutDashboard size={16} />
|
<Network size={14} />
|
||||||
{t("ui.admin.tenants.data_mgmt", "데이터 관리")}
|
{t("ui.admin.tenants.view.tree", "트리")}
|
||||||
<ChevronDown size={14} className="opacity-50" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
<Button
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
type="button"
|
||||||
<DropdownMenuItem
|
variant={viewMode === "table" ? "default" : "ghost"}
|
||||||
onClick={handleTemplateDownload}
|
size="sm"
|
||||||
data-testid="tenant-template-menu-item"
|
className="h-9 gap-1.5"
|
||||||
className="cursor-pointer"
|
aria-pressed={viewMode === "table"}
|
||||||
|
onClick={() => setViewMode("table")}
|
||||||
|
data-testid="tenant-view-table-btn"
|
||||||
>
|
>
|
||||||
<FileSpreadsheet size={16} className="mr-2 opacity-50" />
|
<List size={14} />
|
||||||
{t("ui.admin.tenants.csv_template", "템플릿 다운로드")}
|
{t("ui.admin.tenants.view.table", "평면")}
|
||||||
</DropdownMenuItem>
|
</Button>
|
||||||
<DropdownMenuSeparator />
|
</div>
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
disabled={importMutation.isPending}
|
|
||||||
data-testid="tenant-import-menu-item"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<Upload size={16} className="mr-2 opacity-50" />
|
|
||||||
{t("ui.admin.tenants.import", "CSV 가져오기")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => exportMutation.mutate(false)}
|
|
||||||
disabled={exportMutation.isPending}
|
|
||||||
data-testid="tenant-export-menu-item"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<Download size={16} className="mr-2 opacity-50" />
|
|
||||||
{t(
|
|
||||||
"ui.admin.tenants.export_without_ids",
|
|
||||||
"UUID 제외 내보내기",
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => exportMutation.mutate(true)}
|
|
||||||
disabled={exportMutation.isPending}
|
|
||||||
data-testid="tenant-export-with-ids-menu-item"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<Download size={16} className="mr-2 opacity-50" />
|
|
||||||
{t(
|
|
||||||
"ui.admin.tenants.export_with_ids",
|
|
||||||
"UUID 포함 내보내기",
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</RoleGuard>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
onClick={() => query.refetch()}
|
variant={scopeTenantId ? "default" : "outline"}
|
||||||
disabled={query.isFetching}
|
size="sm"
|
||||||
className="w-9 px-0"
|
className="h-9 gap-2"
|
||||||
title={t("ui.common.refresh", "새로고침")}
|
onClick={() => setScopePickerOpen(true)}
|
||||||
>
|
data-testid="tenant-scope-picker-btn"
|
||||||
<RefreshCw size={16} />
|
>
|
||||||
<span className="sr-only">
|
<Network size={16} />
|
||||||
{t("ui.common.refresh", "새로고침")}
|
{selectedScopeTenant
|
||||||
</span>
|
? t("ui.admin.tenants.scope.active", "{{name}} 하위", {
|
||||||
</Button>
|
name: selectedScopeTenant.name,
|
||||||
<RoleGuard roles={["super_admin"]}>
|
})
|
||||||
<Button asChild>
|
: t("ui.admin.tenants.scope.pick", "상위 범위 선택")}
|
||||||
<Link to="/tenants/new">
|
</Button>
|
||||||
<Plus size={16} />
|
{scopeTenantId ? (
|
||||||
{t("ui.admin.tenants.add", "테넌트 추가")}
|
<Button
|
||||||
</Link>
|
type="button"
|
||||||
</Button>
|
variant="ghost"
|
||||||
</RoleGuard>
|
size="sm"
|
||||||
</div>
|
className="h-9"
|
||||||
|
onClick={() => setScopeTenantId("")}
|
||||||
|
data-testid="tenant-scope-clear-btn"
|
||||||
|
>
|
||||||
|
{t("ui.common.clear", "초기화")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<RoleGuard roles={["super_admin"]}>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv,text/csv"
|
||||||
|
className="hidden"
|
||||||
|
data-testid="tenant-import-input"
|
||||||
|
onChange={handleImportFile}
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
data-testid="tenant-data-mgmt-btn"
|
||||||
|
className="gap-2 h-9"
|
||||||
|
>
|
||||||
|
<LayoutDashboard size={16} />
|
||||||
|
{t("ui.admin.tenants.data_mgmt", "데이터 관리")}
|
||||||
|
<ChevronDown size={14} className="opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleTemplateDownload}
|
||||||
|
data-testid="tenant-template-menu-item"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileSpreadsheet
|
||||||
|
size={16}
|
||||||
|
className="mr-2 opacity-50"
|
||||||
|
/>
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.csv_template",
|
||||||
|
"템플릿 다운로드",
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={importMutation.isPending}
|
||||||
|
data-testid="tenant-import-menu-item"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Upload size={16} className="mr-2 opacity-50" />
|
||||||
|
{t("ui.admin.tenants.import", "CSV 가져오기")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => exportMutation.mutate(false)}
|
||||||
|
disabled={exportMutation.isPending}
|
||||||
|
data-testid="tenant-export-menu-item"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download size={16} className="mr-2 opacity-50" />
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.export_without_ids",
|
||||||
|
"UUID 제외 내보내기",
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => exportMutation.mutate(true)}
|
||||||
|
disabled={exportMutation.isPending}
|
||||||
|
data-testid="tenant-export-with-ids-menu-item"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download size={16} className="mr-2 opacity-50" />
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.export_with_ids",
|
||||||
|
"UUID 포함 내보내기",
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</RoleGuard>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => query.refetch()}
|
||||||
|
disabled={query.isFetching}
|
||||||
|
className="h-9 w-9 px-0"
|
||||||
|
title={t("ui.common.refresh", "새로고침")}
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} />
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("ui.common.refresh", "새로고침")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<RoleGuard roles={["super_admin"]}>
|
||||||
|
<Button asChild size="sm" className="h-9">
|
||||||
|
<Link to="/tenants/new">
|
||||||
|
<Plus size={16} />
|
||||||
|
{t("ui.admin.tenants.add", "테넌트 추가")}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</RoleGuard>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{importMessage ? (
|
{importMessage ? (
|
||||||
<div
|
<div
|
||||||
className="rounded-md border border-border bg-secondary px-3 py-2 text-sm"
|
className="rounded-md border border-border bg-secondary px-3 py-2 text-sm"
|
||||||
@@ -884,7 +898,7 @@ function TenantListPage() {
|
|||||||
{importMessage}
|
{importMessage}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user