diff --git a/frontend/src/AdminLayout.tsx b/frontend/src/AdminLayout.tsx index f732b60..21c381e 100644 --- a/frontend/src/AdminLayout.tsx +++ b/frontend/src/AdminLayout.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; -import { NavLink, Outlet, useNavigate } from "react-router-dom"; -import { Loader2, Server, Users, Wallet, DollarSign, LogOut } from "lucide-react"; +import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; +import { Loader2, Server, Users, Wallet, DollarSign, LogOut, LifeBuoy, Activity } from "lucide-react"; import { fetchMyRole, logout, type StaffRole } from "./api"; -const STAFF_ROLES = ["owner", "moderator", "partner"]; +const STAFF_ROLES = ["owner", "moderator", "partner", "support"]; export default function AdminLayout() { const navigate = useNavigate(); + const location = useLocation(); const [role, setRole] = useState(null); const [loading, setLoading] = useState(true); @@ -17,6 +18,11 @@ export default function AdminLayout() { navigate("/admin/login"); return; } + // Support не видит "Ноды" (индекс-роут /admin) — после логина сразу + // на единственный раздел, который ему доступен. + if (r.role === "support" && location.pathname === "/admin") { + navigate("/admin/tickets", { replace: true }); + } setRole(r); }) .catch(() => navigate("/admin/login")) @@ -66,6 +72,16 @@ export default function AdminLayout() { Выводы )} + {(role.role === "owner" || role.role === "moderator" || role.role === "support") && ( + + Тикеты + + )} + {(role.role === "owner" || role.role === "moderator") && ( + + Мониторинг + + )} {role.role === "partner" && ( Доходы diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c8e68c3..e0a4a5e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,8 @@ const AdminLayout = lazy(() => import("./AdminLayout")); const StaffPage = lazy(() => import("./StaffPage")); const WithdrawalsPage = lazy(() => import("./WithdrawalsPage")); const EarningsPage = lazy(() => import("./EarningsPage")); +const TicketsPage = lazy(() => import("./TicketsPage")); +const ObservabilityPage = lazy(() => import("./ObservabilityPage")); const DICT = { en: { loading: "Loading protocols..." }, @@ -94,6 +96,8 @@ export default function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/src/ObservabilityPage.tsx b/frontend/src/ObservabilityPage.tsx new file mode 100644 index 0000000..33425e7 --- /dev/null +++ b/frontend/src/ObservabilityPage.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from "react"; +import { getPublicConfig } from "./api"; + +const DASHBOARDS = [ + { uid: "netrunner-backend", title: "Backend" }, + { uid: "netrunner-landing", title: "Лендинг" }, + { uid: "netrunner-proxy-nodes", title: "Прокси-ноды (ядро)" }, +] as const; + +export default function ObservabilityPage() { + const [grafanaUrl, setGrafanaUrl] = useState(undefined); + const [active, setActive] = useState<(typeof DASHBOARDS)[number]["uid"]>(DASHBOARDS[0].uid); + + useEffect(() => { + getPublicConfig().then((cfg) => setGrafanaUrl(cfg.grafana_url)); + }, []); + + if (grafanaUrl === undefined) { + return null; + } + + if (!grafanaUrl) { + return ( +
+

+ Grafana ещё не настроена (GRAFANA_PUBLIC_URL не задан на сервере). +

+
+ ); + } + + return ( +
+
+

+ Мониторинг +

+
+ {DASHBOARDS.map((d) => ( + + ))} +
+
+ + {/* Ключ на iframe меняет DOM-узел при смене дашборда — проще, чем + городить логику перезагрузки src внутри одного и того же iframe. */} +