forked from baron/baron-sso
adminfront ui 개편
This commit is contained in:
@@ -39,6 +39,18 @@ function AppLayout() {
|
||||
return stored === "dark" ? "dark" : "light";
|
||||
});
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||
const [timeLeft, setTimeLeft] = useState<number | null>(null);
|
||||
const expiresAt = auth.user?.expires_at;
|
||||
|
||||
useEffect(() => {
|
||||
if (!expiresAt) return;
|
||||
const updateTimer = () => {
|
||||
setTimeLeft(Math.max(0, Math.floor(expiresAt - Date.now() / 1000)));
|
||||
};
|
||||
updateTimer();
|
||||
const interval = setInterval(updateTimer, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [expiresAt]);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["me"],
|
||||
@@ -156,20 +168,8 @@ function AppLayout() {
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
|
||||
<BadgeCheck size={14} />
|
||||
{t("msg.admin.scope_admin", "Scoped to /admin")}
|
||||
</div>
|
||||
</div>
|
||||
<nav className="px-2 pb-4 md:px-3 md:pb-8">
|
||||
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
|
||||
<span className="rounded-full border border-border px-3 py-1">
|
||||
{t("msg.admin.idp_env_prod", "IDP env: prod")}
|
||||
</span>
|
||||
<span className="rounded-full border border-border px-3 py-1">
|
||||
{t("msg.admin.tenant_headers", "Tenant-aware headers")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{navItems.map(({ label, to, icon: Icon }) => (
|
||||
<NavLink
|
||||
@@ -201,36 +201,11 @@ function AppLayout() {
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
|
||||
<p>
|
||||
{t(
|
||||
"msg.admin.notice.scope",
|
||||
"관리 기능은 /admin 네임스페이스에서만 노출합니다.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"msg.admin.notice.idp_policy",
|
||||
"IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="relative">
|
||||
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
|
||||
<div className="flex items-center justify-between px-5 py-4 md:px-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
|
||||
{t("ui.admin.header.plane", "Admin Plane")}
|
||||
</p>
|
||||
<span className="text-lg font-semibold">
|
||||
{t(
|
||||
"msg.admin.header.subtitle",
|
||||
"Tenant isolation & least privilege by default",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/90 backdrop-blur">
|
||||
<div className="flex items-center justify-end px-5 py-4 md:px-8">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<LanguageSelector />
|
||||
<button
|
||||
@@ -266,7 +241,7 @@ function AppLayout() {
|
||||
{isProfileOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-30"
|
||||
className="fixed inset-0 z-[90]"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") setIsProfileOpen(false);
|
||||
@@ -275,7 +250,7 @@ function AppLayout() {
|
||||
tabIndex={-1}
|
||||
aria-label="Close profile menu"
|
||||
/>
|
||||
<div className="absolute right-0 mt-2 w-56 origin-top-right rounded-xl border border-border bg-card p-2 shadow-xl ring-1 ring-black ring-opacity-5 focus:outline-none z-40 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="absolute right-0 mt-2 w-56 origin-top-right rounded-xl border border-border bg-card p-2 shadow-xl ring-1 ring-black ring-opacity-5 focus:outline-none z-[100] animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="px-3 py-3 border-b border-border/50 mb-1">
|
||||
<p className="text-sm font-semibold truncate">
|
||||
{profile?.name || auth.user?.profile.name}
|
||||
@@ -364,8 +339,10 @@ function AppLayout() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="hidden md:inline-flex rounded-full border border-border px-3 py-2 text-muted-foreground">
|
||||
{t("msg.admin.session_ttl", "Session TTL: 15m admin")}
|
||||
<span className="hidden md:inline-flex rounded-full border border-border px-3 py-2 text-muted-foreground font-mono">
|
||||
{timeLeft !== null
|
||||
? `Session TTL: ${Math.floor(timeLeft / 60)}m ${timeLeft % 60}s`
|
||||
: t("msg.admin.session_ttl", "Session TTL: 15m admin")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,15 +66,6 @@ function ApiKeyListPage() {
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<span>
|
||||
{t("ui.admin.api_keys.list.breadcrumb.section", "API Keys")}
|
||||
</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.api_keys.list.breadcrumb.list", "List")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")}
|
||||
</h2>
|
||||
|
||||
@@ -161,13 +161,6 @@ function AuditLogsPage() {
|
||||
<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">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<span>{t("ui.admin.audit.breadcrumb.section", "Audit")}</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.audit.breadcrumb.logs", "Logs")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.audit.title", "감사 로그")}
|
||||
</h2>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
Box,
|
||||
Database,
|
||||
Key,
|
||||
PlusCircle,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { RoleGuard } from "../../components/auth/RoleGuard";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -22,244 +21,180 @@ import PermissionChecker from "./components/PermissionChecker";
|
||||
|
||||
function GlobalOverviewPage() {
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
|
||||
{t("ui.admin.overview.kicker", "Global Overview")}
|
||||
</p>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.overview.title", "Tenant-independent control plane")}
|
||||
<div className="space-y-8 animate-in fade-in duration-500">
|
||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
{t("ui.admin.overview.title", "Dashboard")}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--color-muted)]">
|
||||
<p className="text-muted-foreground">
|
||||
{t(
|
||||
"msg.admin.overview.description",
|
||||
"모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다.",
|
||||
"시스템 전반의 주요 현황을 확인하고 관리합니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="muted">
|
||||
{t("msg.admin.overview.idp_primary", "IDP: Ory primary")}
|
||||
</Badge>
|
||||
<Badge variant="muted">
|
||||
{t("msg.admin.overview.idp_fallback", "Fallback: Descope")}
|
||||
</Badge>
|
||||
</div>
|
||||
</RoleGuard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardDescription>
|
||||
{t("ui.admin.overview.summary.total_tenants", "Total Tenants")}
|
||||
</CardDescription>
|
||||
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]">
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t("ui.admin.overview.summary.total_tenants", "총 테넌트")}
|
||||
</CardTitle>
|
||||
<div className="rounded-full bg-primary/10 p-2 text-primary">
|
||||
<Users size={16} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">-</div>
|
||||
<p className="mt-1 text-xs text-[var(--color-muted)]">
|
||||
{t(
|
||||
"msg.admin.overview.summary.total_tenants",
|
||||
"Tenant-aware core",
|
||||
)}
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
활성화된 테넌트 수
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardDescription>
|
||||
{t("ui.admin.overview.summary.oidc_clients", "OIDC Clients")}
|
||||
</CardDescription>
|
||||
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]">
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t("ui.admin.overview.summary.oidc_clients", "연동 클라이언트")}
|
||||
</CardTitle>
|
||||
<div className="rounded-full bg-blue-500/10 p-2 text-blue-500">
|
||||
<ShieldCheck size={16} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">-</div>
|
||||
<p className="mt-1 text-xs text-[var(--color-muted)]">
|
||||
{t("msg.admin.overview.summary.oidc_clients", "Hydra registry")}
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
등록된 OIDC 앱
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</RoleGuard>
|
||||
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardDescription>
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t(
|
||||
"ui.admin.overview.summary.audit_events_24h",
|
||||
"Audit Events (24h)",
|
||||
"최근 감사 로그 (24h)",
|
||||
)}
|
||||
</CardDescription>
|
||||
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]">
|
||||
</CardTitle>
|
||||
<div className="rounded-full bg-orange-500/10 p-2 text-orange-500">
|
||||
<Activity size={16} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">-</div>
|
||||
<p className="mt-1 text-xs text-[var(--color-muted)]">
|
||||
{t(
|
||||
"msg.admin.overview.summary.audit_events_24h",
|
||||
"ClickHouse stream",
|
||||
)}
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
발생한 이벤트 수
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardDescription>
|
||||
{t("ui.admin.overview.summary.policy_gate", "Policy Gate")}
|
||||
</CardDescription>
|
||||
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]">
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t("ui.admin.overview.summary.policy_gate", "정책 상태")}
|
||||
</CardTitle>
|
||||
<div className="rounded-full bg-green-500/10 p-2 text-green-500">
|
||||
<Database size={16} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">Planned</div>
|
||||
<p className="mt-1 text-xs text-[var(--color-muted)]">
|
||||
{t(
|
||||
"msg.admin.overview.summary.policy_gate",
|
||||
"Keto + Admin checks",
|
||||
)}
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-500">
|
||||
Active
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
접근 제어 정상 동작
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1.4fr,1fr]">
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">
|
||||
{t("ui.admin.overview.playbook.title", "Admin playbook")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"msg.admin.overview.playbook.description",
|
||||
"운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.",
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-[var(--color-muted)]">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 rounded-full border border-[var(--color-border)] p-2">
|
||||
<ShieldCheck size={14} />
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold tracking-tight">
|
||||
{t("ui.admin.overview.quick_links.title", "빠른 작업")}
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<Link
|
||||
to="/tenants/new"
|
||||
className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
|
||||
<PlusCircle size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">
|
||||
{t(
|
||||
"msg.admin.overview.playbook.idp_title",
|
||||
"Backend-only IDP access",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"msg.admin.overview.playbook.idp_body",
|
||||
"모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos admin 포트는 외부에 노출하지 않습니다.",
|
||||
)}
|
||||
<h4 className="font-semibold transition-colors group-hover:text-primary">
|
||||
테넌트 추가
|
||||
</h4>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
새로운 조직이나 그룹을 생성합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 rounded-full border border-[var(--color-border)] p-2">
|
||||
<Box size={14} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">
|
||||
{t(
|
||||
"msg.admin.overview.playbook.tenant_title",
|
||||
"Tenant isolation",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"msg.admin.overview.playbook.tenant_body",
|
||||
"Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto 정책으로 확장 예정입니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</RoleGuard>
|
||||
|
||||
<Card className="bg-[var(--color-panel)]">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">
|
||||
{t("ui.admin.overview.quick_links.title", "빠른 이동")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"msg.admin.overview.quick_links.description",
|
||||
"주요 운영 화면으로 바로 이동합니다.",
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full justify-between"
|
||||
variant="outline"
|
||||
>
|
||||
<Link to="/tenants/new">
|
||||
{t("ui.admin.overview.quick_links.add_tenant", "테넌트 추가")}
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</RoleGuard>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full justify-between"
|
||||
variant="outline"
|
||||
<Link
|
||||
to="/users"
|
||||
className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 transition-colors group-hover:bg-blue-500 group-hover:text-white">
|
||||
<Users size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold transition-colors group-hover:text-blue-500">
|
||||
사용자 관리
|
||||
</h4>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
전체 사용자를 조회하고 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<Link
|
||||
to="/api-keys"
|
||||
className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-purple-500/50 hover:shadow-md"
|
||||
>
|
||||
<Link to="/users">
|
||||
{t(
|
||||
"ui.admin.overview.quick_links.user_management",
|
||||
"사용자 관리",
|
||||
)}
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full justify-between"
|
||||
variant="outline"
|
||||
>
|
||||
<Link to="/api-keys">
|
||||
{t(
|
||||
"ui.admin.overview.quick_links.api_key_management",
|
||||
"API 키 관리",
|
||||
)}
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</RoleGuard>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full justify-between"
|
||||
variant="outline"
|
||||
>
|
||||
<Link to="/audit-logs">
|
||||
{t(
|
||||
"ui.admin.overview.quick_links.view_audit_logs",
|
||||
"감사 로그 보기",
|
||||
)}
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10 text-purple-500 transition-colors group-hover:bg-purple-500 group-hover:text-white">
|
||||
<Key size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold transition-colors group-hover:text-purple-500">
|
||||
API 키 관리
|
||||
</h4>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
시스템 연동을 위한 키를 발급합니다.
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</RoleGuard>
|
||||
|
||||
<Link
|
||||
to="/audit-logs"
|
||||
className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-orange-500/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-orange-500/10 text-orange-500 transition-colors group-hover:bg-orange-500 group-hover:text-white">
|
||||
<Activity size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold transition-colors group-hover:text-orange-500">
|
||||
감사 로그
|
||||
</h4>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
보안 이벤트를 모니터링합니다.
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<PermissionChecker />
|
||||
<div className="pt-4">
|
||||
<PermissionChecker />
|
||||
</div>
|
||||
</RoleGuard>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -58,15 +58,6 @@ function TenantCreatePage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<span>
|
||||
{t("ui.admin.tenants.create.breadcrumb.section", "Tenants")}
|
||||
</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.tenants.create.breadcrumb.action", "Create")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
|
||||
@@ -32,19 +32,6 @@ function TenantDetailPage() {
|
||||
<div className="space-y-8">
|
||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<Link
|
||||
to="/tenants"
|
||||
className="inline-flex items-center gap-2 hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
{t("ui.admin.tenants.detail.breadcrumb_list", "테넌트 목록")}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.tenants.detail.title", "상세")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{tenantQuery.data?.name ??
|
||||
t("ui.admin.tenants.detail.loading", "불러오는 중...")}
|
||||
|
||||
@@ -119,13 +119,6 @@ function TenantListPage() {
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<span>{t("ui.admin.tenants.breadcrumb.section", "Tenants")}</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.tenants.breadcrumb.list", "List")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.tenants.title", "테넌트 목록")}
|
||||
</h2>
|
||||
|
||||
@@ -177,15 +177,6 @@ function UserCreatePage() {
|
||||
<div className="max-w-3xl space-y-8">
|
||||
<header className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<Link to="/users" className="hover:underline">
|
||||
{t("ui.admin.users.create.breadcrumb.section", "Users")}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.users.create.breadcrumb.new", "New")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.users.create.title", "사용자 추가")}
|
||||
</h2>
|
||||
|
||||
@@ -315,13 +315,6 @@ function UserDetailPage() {
|
||||
<div className="max-w-3xl space-y-8">
|
||||
<header className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<Link to="/users" className="hover:underline">
|
||||
{t("ui.admin.users.detail.breadcrumb.section", "Users")}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">{user.name}</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold">
|
||||
{t("ui.admin.users.detail.title", "사용자 상세")}
|
||||
</h2>
|
||||
|
||||
@@ -257,13 +257,6 @@ function UserListPage() {
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||
<span>{t("ui.admin.users.list.breadcrumb.section", "Users")}</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">
|
||||
{t("ui.admin.users.list.breadcrumb.list", "List")}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold" data-testid="page-title">
|
||||
{t("ui.admin.users.list.title", "사용자 관리")}
|
||||
</h2>
|
||||
|
||||
@@ -880,7 +880,7 @@ start_import = "임포트 시작"
|
||||
|
||||
[ui.admin.overview]
|
||||
kicker = "Global Overview"
|
||||
title = "Tenant-independent control plane"
|
||||
title = "통합 대시보드"
|
||||
|
||||
[ui.admin.overview.playbook]
|
||||
title = "Admin playbook"
|
||||
|
||||
Reference in New Issue
Block a user