forked from baron/baron-sso
admin/dev 사이드바 프레임 공통화
This commit is contained in:
@@ -21,12 +21,14 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
AppSidebar,
|
||||
type ShellTranslator,
|
||||
applyShellTheme,
|
||||
buildShellProfileSummary,
|
||||
buildShellSessionStatus,
|
||||
readShellSessionExpiryEnabled,
|
||||
readShellTheme,
|
||||
type ShellSidebarNavItem,
|
||||
shellLayoutClasses,
|
||||
writeShellSessionExpiryEnabled,
|
||||
} from "../../../../common/shell";
|
||||
@@ -41,19 +43,38 @@ import {
|
||||
import LanguageSelector from "../common/LanguageSelector";
|
||||
import RoleSwitcher from "./RoleSwitcher";
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
to: string;
|
||||
icon: React.ComponentType<{ size?: number | string }>;
|
||||
isExternal?: boolean;
|
||||
}
|
||||
|
||||
const staticNavItems: NavItem[] = [
|
||||
{ label: "ui.admin.nav.overview", to: "/", icon: LayoutDashboard },
|
||||
{ label: "ui.admin.nav.users", to: "/users", icon: Users },
|
||||
{ label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key },
|
||||
{ label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs },
|
||||
{ label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound },
|
||||
const staticNavItems: ShellSidebarNavItem[] = [
|
||||
{
|
||||
labelKey: "ui.admin.nav.overview",
|
||||
labelFallback: "Overview",
|
||||
to: "/",
|
||||
icon: LayoutDashboard,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
labelKey: "ui.admin.nav.users",
|
||||
labelFallback: "Users",
|
||||
to: "/users",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
labelKey: "ui.admin.nav.api_keys",
|
||||
labelFallback: "API Keys",
|
||||
to: "/api-keys",
|
||||
icon: Key,
|
||||
},
|
||||
{
|
||||
labelKey: "ui.admin.nav.audit_logs",
|
||||
labelFallback: "Audit Logs",
|
||||
to: "/audit-logs",
|
||||
icon: NotebookTabs,
|
||||
},
|
||||
{
|
||||
labelKey: "ui.admin.nav.auth_guard",
|
||||
labelFallback: "Auth Guard",
|
||||
to: "/auth",
|
||||
icon: KeyRound,
|
||||
},
|
||||
];
|
||||
|
||||
type SessionStatusProps = {
|
||||
@@ -145,7 +166,7 @@ function AppLayout() {
|
||||
._IS_TEST_MODE === true,
|
||||
});
|
||||
|
||||
const navItems = React.useMemo(() => {
|
||||
const navItems = React.useMemo<ShellSidebarNavItem[]>(() => {
|
||||
const items = [...staticNavItems];
|
||||
const isTest =
|
||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||
@@ -167,36 +188,42 @@ function AppLayout() {
|
||||
|
||||
if (isSuperAdmin) {
|
||||
filteredItems.splice(1, 0, {
|
||||
label: "ui.admin.nav.tenants",
|
||||
labelKey: "ui.admin.nav.tenants",
|
||||
labelFallback: "Tenants",
|
||||
to: "/tenants",
|
||||
icon: Building2,
|
||||
});
|
||||
filteredItems.splice(2, 0, {
|
||||
label: "ui.admin.nav.org_chart",
|
||||
labelKey: "ui.admin.nav.org_chart",
|
||||
labelFallback: "Org Chart",
|
||||
to: orgfrontUrl,
|
||||
icon: Network,
|
||||
isExternal: true,
|
||||
});
|
||||
filteredItems.splice(4, 0, {
|
||||
label: "ui.admin.nav.user_projection",
|
||||
labelKey: "ui.admin.nav.user_projection",
|
||||
labelFallback: "User Projection",
|
||||
to: "/system/projections/users",
|
||||
icon: Database,
|
||||
});
|
||||
filteredItems.splice(5, 0, {
|
||||
label: "ui.admin.nav.data_integrity",
|
||||
labelKey: "ui.admin.nav.data_integrity",
|
||||
labelFallback: "Data Integrity",
|
||||
to: "/system/data-integrity",
|
||||
icon: ShieldCheck,
|
||||
});
|
||||
} else if (isTenantAdmin || manageableCount > 0) {
|
||||
if (manageableCount <= 1 && profile?.tenantId) {
|
||||
filteredItems.splice(1, 0, {
|
||||
label: "ui.admin.nav.my_tenant",
|
||||
labelKey: "ui.admin.nav.my_tenant",
|
||||
labelFallback: "My Tenant",
|
||||
to: `/tenants/${profile.tenantId}`,
|
||||
icon: Building2,
|
||||
});
|
||||
} else if (manageableCount > 1) {
|
||||
filteredItems.splice(1, 0, {
|
||||
label: "ui.admin.nav.tenants",
|
||||
labelKey: "ui.admin.nav.tenants",
|
||||
labelFallback: "Tenants",
|
||||
to: "/tenants",
|
||||
icon: Building2,
|
||||
});
|
||||
@@ -205,7 +232,8 @@ function AppLayout() {
|
||||
manageableCount <= 1 && profile?.tenantId ? 2 : 2,
|
||||
0,
|
||||
{
|
||||
label: "ui.admin.nav.org_chart",
|
||||
labelKey: "ui.admin.nav.org_chart",
|
||||
labelFallback: "Org Chart",
|
||||
to: orgfrontUrl,
|
||||
icon: Network,
|
||||
isExternal: true,
|
||||
@@ -214,7 +242,8 @@ function AppLayout() {
|
||||
} else {
|
||||
// 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
|
||||
filteredItems.splice(1, 0, {
|
||||
label: "ui.admin.nav.org_chart",
|
||||
labelKey: "ui.admin.nav.org_chart",
|
||||
labelFallback: "Org Chart",
|
||||
to: orgfrontUrl,
|
||||
icon: Network,
|
||||
isExternal: true,
|
||||
@@ -442,6 +471,66 @@ function AppLayout() {
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const sidebarNavContent = (
|
||||
<div className={shellLayoutClasses.navList}>
|
||||
{navItems.map((item) => {
|
||||
const { labelKey, labelFallback, to, icon: Icon, isExternal } = item;
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
key={to}
|
||||
href={to}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={[
|
||||
shellLayoutClasses.navItemBase,
|
||||
shellLayoutClasses.navItemIdle,
|
||||
].join(" ")}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{t(labelKey, labelFallback)}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={item.end ?? to === "/"}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
shellLayoutClasses.navItemBase,
|
||||
item.isActive !== undefined
|
||||
? item.isActive
|
||||
? shellLayoutClasses.navItemActive
|
||||
: shellLayoutClasses.navItemIdle
|
||||
: isActive
|
||||
? shellLayoutClasses.navItemActive
|
||||
: shellLayoutClasses.navItemIdle,
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{t(labelKey, labelFallback)}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
const sidebarFooterContent = (
|
||||
<div className="border-t border-border/50 px-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className={shellLayoutClasses.logoutButton}
|
||||
>
|
||||
<LogOut size={18} />
|
||||
<span>{t("ui.admin.nav.logout", "Logout")}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (auth.isLoading) {
|
||||
return (
|
||||
@@ -453,84 +542,13 @@ function AppLayout() {
|
||||
|
||||
return (
|
||||
<div className={shellLayoutClasses.root}>
|
||||
<aside className={shellLayoutClasses.asideStatic}>
|
||||
<div className={shellLayoutClasses.brandSection}>
|
||||
<div className={shellLayoutClasses.brandWrap}>
|
||||
<div className={shellLayoutClasses.brandIcon}>
|
||||
<ShieldHalf size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||
{t("ui.admin.brand", "Baron 로그인")}
|
||||
</p>
|
||||
<h1 className="text-lg font-semibold">
|
||||
{t("ui.admin.title", "Admin Control")}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={shellLayoutClasses.navWrap}>
|
||||
<div className={shellLayoutClasses.navList}>
|
||||
{navItems.map((item: NavItem) => {
|
||||
const { label, to, icon: Icon, isExternal } = item;
|
||||
const isOrgChart = location.pathname === "/tenants/org-chart";
|
||||
const isTenantsRoot = to === "/tenants";
|
||||
const isCustomActive = isTenantsRoot
|
||||
? location.pathname.startsWith("/tenants") && !isOrgChart
|
||||
: to === "/"
|
||||
? location.pathname === "/"
|
||||
: location.pathname.startsWith(to);
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
key={to}
|
||||
href={to}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={[
|
||||
shellLayoutClasses.navItemBase,
|
||||
shellLayoutClasses.navItemIdle,
|
||||
].join(" ")}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{t(label, label)}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={() =>
|
||||
[
|
||||
shellLayoutClasses.navItemBase,
|
||||
isCustomActive
|
||||
? shellLayoutClasses.navItemActive
|
||||
: shellLayoutClasses.navItemIdle,
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<span>{t(label, label)}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/50 px-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className={shellLayoutClasses.logoutButton}
|
||||
>
|
||||
<LogOut size={18} />
|
||||
<span>{t("ui.admin.nav.logout", "Logout")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<AppSidebar
|
||||
brandLabel={t("ui.admin.brand", "Baron 로그인")}
|
||||
brandTitle={t("ui.admin.title", "Admin Control")}
|
||||
brandIcon={<ShieldHalf size={20} />}
|
||||
navContent={sidebarNavContent}
|
||||
footerContent={sidebarFooterContent}
|
||||
/>
|
||||
|
||||
<div className={shellLayoutClasses.contentWide}>
|
||||
<header className={shellLayoutClasses.headerElevated}>
|
||||
|
||||
Reference in New Issue
Block a user