SiteMenu.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {
  2. IconBook,
  3. IconDeviceDesktop,
  4. IconHome,
  5. IconLock,
  6. IconSettings,
  7. IconShield,
  8. IconUser,
  9. } from "@tabler/icons-react";
  10. import cn from "classnames";
  11. import React from "react";
  12. import { HasPermission, NavLink } from "src/components";
  13. import { T } from "src/locale";
  14. import {
  15. ACCESS_LISTS,
  16. ADMIN,
  17. CERTIFICATES,
  18. DEAD_HOSTS,
  19. type MANAGE,
  20. PROXY_HOSTS,
  21. REDIRECTION_HOSTS,
  22. type Section,
  23. STREAMS,
  24. VIEW,
  25. } from "src/modules/Permissions";
  26. interface MenuItem {
  27. label: string;
  28. icon?: React.ElementType;
  29. to?: string;
  30. items?: MenuItem[];
  31. permissionSection?: Section | typeof ADMIN;
  32. permission?: typeof VIEW | typeof MANAGE;
  33. }
  34. const menuItems: MenuItem[] = [
  35. {
  36. to: "/",
  37. icon: IconHome,
  38. label: "dashboard",
  39. },
  40. {
  41. icon: IconDeviceDesktop,
  42. label: "hosts",
  43. items: [
  44. {
  45. to: "/nginx/proxy",
  46. label: "proxy-hosts",
  47. permissionSection: PROXY_HOSTS,
  48. permission: VIEW,
  49. },
  50. {
  51. to: "/nginx/redirection",
  52. label: "redirection-hosts",
  53. permissionSection: REDIRECTION_HOSTS,
  54. permission: VIEW,
  55. },
  56. {
  57. to: "/nginx/stream",
  58. label: "streams",
  59. permissionSection: STREAMS,
  60. permission: VIEW,
  61. },
  62. {
  63. to: "/nginx/404",
  64. label: "dead-hosts",
  65. permissionSection: DEAD_HOSTS,
  66. permission: VIEW,
  67. },
  68. ],
  69. },
  70. {
  71. to: "/access",
  72. icon: IconLock,
  73. label: "access-lists",
  74. permissionSection: ACCESS_LISTS,
  75. permission: VIEW,
  76. },
  77. {
  78. to: "/certificates",
  79. icon: IconShield,
  80. label: "certificates",
  81. permissionSection: CERTIFICATES,
  82. permission: VIEW,
  83. },
  84. {
  85. to: "/users",
  86. icon: IconUser,
  87. label: "users",
  88. permissionSection: ADMIN,
  89. },
  90. {
  91. to: "/audit-log",
  92. icon: IconBook,
  93. label: "auditlogs",
  94. permissionSection: ADMIN,
  95. },
  96. {
  97. to: "/settings",
  98. icon: IconSettings,
  99. label: "settings",
  100. permissionSection: ADMIN,
  101. },
  102. ];
  103. const getMenuItem = (item: MenuItem, onClick?: () => void) => {
  104. if (item.items && item.items.length > 0) {
  105. return getMenuDropown(item, onClick);
  106. }
  107. return (
  108. <HasPermission
  109. key={`item-${item.label}`}
  110. section={item.permissionSection}
  111. permission={item.permission || VIEW}
  112. hideError
  113. >
  114. <li className="nav-item">
  115. <NavLink to={item.to} onClick={onClick}>
  116. <span className="nav-link-icon d-md-none d-lg-inline-block">
  117. {item.icon && React.createElement(item.icon, { height: 24, width: 24 })}
  118. </span>
  119. <span className="nav-link-title">
  120. <T id={item.label} />
  121. </span>
  122. </NavLink>
  123. </li>
  124. </HasPermission>
  125. );
  126. };
  127. const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
  128. const cns = cn("nav-item", "dropdown");
  129. return (
  130. <HasPermission
  131. key={`item-${item.label}`}
  132. section={item.permissionSection}
  133. permission={item.permission || VIEW}
  134. hideError
  135. >
  136. <li className={cns}>
  137. <a
  138. className="nav-link dropdown-toggle"
  139. href={item.to}
  140. data-bs-toggle="dropdown"
  141. data-bs-auto-close="outside"
  142. aria-expanded="false"
  143. role="button"
  144. >
  145. <span className="nav-link-icon d-md-none d-lg-inline-block">
  146. <IconDeviceDesktop height={24} width={24} />
  147. </span>
  148. <span className="nav-link-title">
  149. <T id={item.label} />
  150. </span>
  151. </a>
  152. <div className="dropdown-menu">
  153. {item.items?.map((subitem, idx) => {
  154. return (
  155. <HasPermission
  156. key={`${idx}-${subitem.to}`}
  157. section={subitem.permissionSection}
  158. permission={subitem.permission || VIEW}
  159. hideError
  160. >
  161. <NavLink to={subitem.to} isDropdownItem onClick={onClick}>
  162. <T id={subitem.label} />
  163. </NavLink>
  164. </HasPermission>
  165. );
  166. })}
  167. </div>
  168. </li>
  169. </HasPermission>
  170. );
  171. };
  172. export function SiteMenu() {
  173. // This is hacky AF. But that's the price of using a non-react UI kit.
  174. const closeMenus = () => {
  175. const navMenus = document.querySelectorAll(".nav-item.dropdown");
  176. navMenus.forEach((menu) => {
  177. menu.classList.remove("show");
  178. const dropdown = menu.querySelector(".dropdown-menu");
  179. if (dropdown) {
  180. dropdown.classList.remove("show");
  181. }
  182. });
  183. };
  184. return (
  185. <header className="navbar-expand-md">
  186. <div className="collapse navbar-collapse" id="navbar-menu">
  187. <div className="navbar">
  188. <div className="container-xl">
  189. <div className="row flex-column flex-md-row flex-fill align-items-center">
  190. <div className="col">
  191. <ul className="navbar-nav">
  192. {menuItems.length > 0 &&
  193. menuItems.map((item) => {
  194. return getMenuItem(item, closeMenus);
  195. })}
  196. </ul>
  197. </div>
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. </header>
  203. );
  204. }