1
0
forked from baron/baron-sso

adminfront ui 개편

This commit is contained in:
2026-03-20 10:58:52 +09:00
parent 918f133867
commit d0e4f8f86a
11 changed files with 142 additions and 298 deletions

View File

@@ -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>