forked from baron/baron-sso
페이지 헤더 공통 컴포넌트 통일
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
|
import { PageHeader } from "../../../../common/core/components/page";
|
||||||
import { commonStickyTableHeaderClass } from "../../../../common/ui/table";
|
import { commonStickyTableHeaderClass } from "../../../../common/ui/table";
|
||||||
import {
|
import {
|
||||||
type ApiKeySummary,
|
type ApiKeySummary,
|
||||||
@@ -160,35 +161,33 @@ function ApiKeyListPage() {
|
|||||||
|
|
||||||
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">
|
<PageHeader
|
||||||
<div className="space-y-2">
|
sticky
|
||||||
<h2 className="text-3xl font-semibold">
|
titleAs="h2"
|
||||||
{t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")}
|
title={t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")}
|
||||||
</h2>
|
description={t(
|
||||||
<p className="text-sm text-[var(--color-muted)]">
|
"msg.admin.api_keys.list.subtitle",
|
||||||
{t(
|
"서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다.",
|
||||||
"msg.admin.api_keys.list.subtitle",
|
)}
|
||||||
"서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다.",
|
actions={
|
||||||
)}
|
<>
|
||||||
</p>
|
<Button
|
||||||
</div>
|
variant="outline"
|
||||||
<div className="flex items-center gap-2">
|
onClick={() => query.refetch()}
|
||||||
<Button
|
disabled={query.isFetching}
|
||||||
variant="outline"
|
>
|
||||||
onClick={() => query.refetch()}
|
<RefreshCw size={16} />
|
||||||
disabled={query.isFetching}
|
{t("ui.common.refresh", "새로고침")}
|
||||||
>
|
</Button>
|
||||||
<RefreshCw size={16} />
|
<Button asChild>
|
||||||
{t("ui.common.refresh", "새로고침")}
|
<Link to="/api-keys/new">
|
||||||
</Button>
|
<Plus size={16} />
|
||||||
<Button asChild>
|
{t("ui.admin.api_keys.list.add", "API 키 생성")}
|
||||||
<Link to="/api-keys/new">
|
</Link>
|
||||||
<Plus size={16} />
|
</Button>
|
||||||
{t("ui.admin.api_keys.list.add", "API 키 생성")}
|
</>
|
||||||
</Link>
|
}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Card className="bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
|
<Card className="bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
|
import { PageHeader } from "../../../../common/core/components/page";
|
||||||
import type { AuditLog } from "../../lib/adminApi";
|
import type { AuditLog } from "../../lib/adminApi";
|
||||||
import { fetchAuditLogs } from "../../lib/adminApi";
|
import { fetchAuditLogs } from "../../lib/adminApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
@@ -164,33 +165,31 @@ function AuditLogsPage() {
|
|||||||
|
|
||||||
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">
|
<PageHeader
|
||||||
<div>
|
sticky
|
||||||
<h2 className="text-3xl font-semibold">
|
titleAs="h2"
|
||||||
{t("ui.admin.audit.title", "감사 로그")}
|
title={t("ui.admin.audit.title", "감사 로그")}
|
||||||
</h2>
|
description={t(
|
||||||
<p className="text-sm text-[var(--color-muted)]">
|
"msg.admin.audit.subtitle",
|
||||||
{t(
|
"Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.",
|
||||||
"msg.admin.audit.subtitle",
|
)}
|
||||||
"Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.",
|
actions={
|
||||||
)}
|
<>
|
||||||
</p>
|
<Button
|
||||||
</div>
|
variant="outline"
|
||||||
<div className="flex items-center gap-2">
|
onClick={() => refetch()}
|
||||||
<Button
|
disabled={isFetching}
|
||||||
variant="outline"
|
>
|
||||||
onClick={() => refetch()}
|
<RefreshCw size={16} />
|
||||||
disabled={isFetching}
|
{t("ui.common.refresh", "새로고침")}
|
||||||
>
|
</Button>
|
||||||
<RefreshCw size={16} />
|
<Button>
|
||||||
{t("ui.common.refresh", "새로고침")}
|
<ListChecks size={16} />
|
||||||
</Button>
|
{t("ui.admin.audit.export_csv", "Export CSV")}
|
||||||
<Button>
|
</Button>
|
||||||
<ListChecks size={16} />
|
</>
|
||||||
{t("ui.admin.audit.export_csv", "Export CSV")}
|
}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<Card className="glass-panel flex-1 flex flex-col min-h-0 overflow-hidden">
|
<Card className="glass-panel flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
sortItems,
|
sortItems,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
} from "../../../../common/core/utils";
|
} from "../../../../common/core/utils";
|
||||||
|
import { PageHeader } from "../../../../common/core/components/page";
|
||||||
import {
|
import {
|
||||||
commonTableShellClass,
|
commonTableShellClass,
|
||||||
commonTableViewportClass,
|
commonTableViewportClass,
|
||||||
@@ -411,184 +412,156 @@ function UserListPage() {
|
|||||||
|
|
||||||
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">
|
<PageHeader
|
||||||
<div className="space-y-2">
|
sticky
|
||||||
<h2 className="text-3xl font-semibold" data-testid="page-title">
|
titleAs="h2"
|
||||||
|
title={
|
||||||
|
<span data-testid="page-title">
|
||||||
{t("ui.admin.users.list.title", "사용자 관리")}
|
{t("ui.admin.users.list.title", "사용자 관리")}
|
||||||
</h2>
|
</span>
|
||||||
<p className="text-sm text-[var(--color-muted)]">
|
}
|
||||||
{t(
|
description={t(
|
||||||
"msg.admin.users.list.subtitle",
|
"msg.admin.users.list.subtitle",
|
||||||
"시스템 사용자를 조회하고 관리합니다.",
|
"시스템 사용자를 조회하고 관리합니다.",
|
||||||
)}
|
)}
|
||||||
</p>
|
actions={
|
||||||
</div>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="mr-2 flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 mr-2">
|
<div className="relative w-48">
|
||||||
<div className="relative w-48">
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Input
|
||||||
<Input
|
placeholder={t(
|
||||||
placeholder={t(
|
"ui.admin.users.list.search_placeholder",
|
||||||
"ui.admin.users.list.search_placeholder",
|
"이름 또는 이메일 검색...",
|
||||||
"이름 또는 이메일 검색...",
|
)}
|
||||||
)}
|
className="h-9 pl-9"
|
||||||
className="pl-9 h-9"
|
value={searchDraft}
|
||||||
value={searchDraft}
|
onChange={(e) => setSearchDraft(e.target.value)}
|
||||||
onChange={(e) => setSearchDraft(e.target.value)}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyDown={handleKeyDown}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||||
|
value={selectedCompany}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedCompany(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
disabled={profile?.role === "tenant_admin"}
|
||||||
|
>
|
||||||
|
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
|
||||||
|
{tenants.map((t) => (
|
||||||
|
<option key={t.id} value={t.slug}>
|
||||||
|
{t.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="h-9"
|
||||||
|
>
|
||||||
|
{t("ui.common.search", "검색")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select
|
|
||||||
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
|
||||||
value={selectedCompany}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSelectedCompany(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
disabled={profile?.role === "tenant_admin"}
|
|
||||||
>
|
|
||||||
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
|
|
||||||
{tenants.map((t) => (
|
|
||||||
<option key={t.id} value={t.slug}>
|
|
||||||
{t.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSearch}
|
|
||||||
className="h-9"
|
className="h-9"
|
||||||
|
onClick={() => query.refetch()}
|
||||||
|
disabled={query.isFetching}
|
||||||
>
|
>
|
||||||
{t("ui.common.search", "검색")}
|
<RefreshCw size={16} />
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
data-testid="user-data-mgmt-btn"
|
|
||||||
className="gap-2 h-9"
|
|
||||||
>
|
|
||||||
<LayoutDashboard size={16} />
|
|
||||||
{t("ui.admin.users.data_mgmt", "데이터 관리")}
|
|
||||||
<ChevronDown size={14} className="opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleExport(false)}
|
|
||||||
disabled={exportMutation.isPending}
|
|
||||||
data-testid="user-export-menu-item"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<FileDown size={16} className="mr-2 opacity-50" />
|
|
||||||
{t("ui.common.export_without_ids", "UUID 제외 내보내기")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleExport(true)}
|
|
||||||
disabled={exportMutation.isPending}
|
|
||||||
data-testid="user-export-with-ids-menu-item"
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<FileDown size={16} className="mr-2 opacity-50" />
|
|
||||||
{t("ui.common.export_with_ids", "UUID 포함 내보내기")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="px-2 py-1.5 focus:bg-transparent cursor-default"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<UserBulkUploadModal
|
|
||||||
onSuccess={() => query.refetch()}
|
|
||||||
variant="dropdown"
|
|
||||||
/>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-9 px-0 h-9"
|
|
||||||
onClick={() => query.refetch()}
|
|
||||||
disabled={query.isFetching}
|
|
||||||
title={t("ui.common.refresh", "새로고침")}
|
|
||||||
>
|
|
||||||
<RefreshCw size={16} />
|
|
||||||
<span className="sr-only">
|
|
||||||
{t("ui.common.refresh", "새로고침")}
|
{t("ui.common.refresh", "새로고침")}
|
||||||
</span>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
|
variant="outline"
|
||||||
<Dialog>
|
onClick={() => handleExport(false)}
|
||||||
<DialogTrigger asChild>
|
className="gap-2"
|
||||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
disabled={exportMutation.isPending}
|
||||||
<Settings2 size={16} />
|
>
|
||||||
</Button>
|
<FileDown size={16} />
|
||||||
</DialogTrigger>
|
{t("ui.common.export_without_ids", "UUID 제외 내보내기")}
|
||||||
<DialogContent>
|
</Button>
|
||||||
<DialogHeader>
|
<Button
|
||||||
<DialogTitle>
|
variant="outline"
|
||||||
{t("ui.admin.users.list.columns.title", "표시 컬럼 설정")}
|
onClick={() => handleExport(true)}
|
||||||
</DialogTitle>
|
className="gap-2"
|
||||||
<DialogDescription>
|
disabled={exportMutation.isPending}
|
||||||
{t(
|
>
|
||||||
"msg.admin.users.list.columns.description",
|
<FileDown size={16} />
|
||||||
"사용자 목록에 표시할 커스텀 필드를 선택하세요.",
|
{t("ui.common.export_with_ids", "UUID 포함")}
|
||||||
)}
|
</Button>
|
||||||
</DialogDescription>
|
<UserBulkUploadModal onSuccess={() => query.refetch()} />
|
||||||
</DialogHeader>
|
<Dialog>
|
||||||
<div className="grid gap-4 py-4">
|
<DialogTrigger asChild>
|
||||||
{userSchema.length === 0 && (
|
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||||
<p className="text-sm text-muted-foreground text-center py-4">
|
<Settings2 size={16} />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("ui.admin.users.list.columns.title", "표시 컬럼 설정")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
{t(
|
{t(
|
||||||
"msg.admin.users.list.columns.no_custom",
|
"msg.admin.users.list.columns.description",
|
||||||
"현재 테넌트에 정의된 커스텀 필드가 없습니다.",
|
"사용자 목록에 표시할 커스텀 필드를 선택하세요.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</DialogDescription>
|
||||||
)}
|
</DialogHeader>
|
||||||
{userSchema.map((field) => (
|
<div className="grid gap-4 py-4">
|
||||||
<label
|
{userSchema.length === 0 && (
|
||||||
key={field.key}
|
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 cursor-pointer"
|
{t(
|
||||||
>
|
"msg.admin.users.list.columns.no_custom",
|
||||||
<input
|
"현재 테넌트에 정의된 커스텀 필드가 없습니다.",
|
||||||
type="checkbox"
|
)}
|
||||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
</p>
|
||||||
checked={visibleColumns[field.key] !== false}
|
)}
|
||||||
onChange={() => toggleColumn(field.key)}
|
{userSchema.map((field) => (
|
||||||
/>
|
<label
|
||||||
<div className="flex flex-col">
|
key={field.key}
|
||||||
<span className="text-sm font-medium">{field.label}</span>
|
className="flex cursor-pointer items-center gap-3 rounded-lg p-2 hover:bg-muted/50"
|
||||||
<span className="text-xs text-muted-foreground font-mono">
|
>
|
||||||
{field.key}
|
<input
|
||||||
</span>
|
type="checkbox"
|
||||||
</div>
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
</label>
|
checked={visibleColumns[field.key] !== false}
|
||||||
))}
|
onChange={() => toggleColumn(field.key)}
|
||||||
</div>
|
/>
|
||||||
<DialogFooter>
|
<div className="flex flex-col">
|
||||||
<DialogTrigger asChild>
|
<span className="text-sm font-medium">{field.label}</span>
|
||||||
<Button variant="secondary">
|
<span className="font-mono text-xs text-muted-foreground">
|
||||||
{t("ui.common.close", "닫기")}
|
{field.key}
|
||||||
</Button>
|
</span>
|
||||||
</DialogTrigger>
|
</div>
|
||||||
</DialogFooter>
|
</label>
|
||||||
</DialogContent>
|
))}
|
||||||
</Dialog>
|
</div>
|
||||||
<Button asChild size="sm" className="h-9">
|
<DialogFooter>
|
||||||
<Link to="/users/new">
|
<DialogTrigger asChild>
|
||||||
<Plus size={16} />
|
<Button variant="secondary">
|
||||||
{t("ui.admin.users.list.add", "사용자 추가")}
|
{t("ui.common.close", "닫기")}
|
||||||
</Link>
|
</Button>
|
||||||
</Button>
|
</DialogTrigger>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</header>
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<Button asChild size="sm" className="h-9">
|
||||||
|
<Link to="/users/new">
|
||||||
|
<Plus size={16} />
|
||||||
|
{t("ui.admin.users.list.add", "사용자 추가")}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="flex-1 flex flex-col min-h-0 bg-[var(--color-panel)] overflow-hidden">
|
<Card className="flex-1 flex flex-col min-h-0 bg-[var(--color-panel)] overflow-hidden">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user