319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
import m, { ClosureComponent } from "mithril";
|
|
import drawerComponent from "./drawer-component.ts";
|
|
import * as overlay from "./overlay.ts";
|
|
import { version as VERSION } from "../package.json";
|
|
import datalist from "./datalist.ts";
|
|
import {
|
|
transitionRoot,
|
|
transitionChild,
|
|
dialog,
|
|
dialogOverlay,
|
|
icon,
|
|
} from "./tailwind-utility-components.ts";
|
|
import * as store from "./store.ts";
|
|
import * as notifications from "./notifications.ts";
|
|
import { LOGO_SVG } from "../build/assets.ts";
|
|
|
|
function tsxComponent<T>(
|
|
c: ClosureComponent<T>,
|
|
): (attrs: T) => ReturnType<typeof m> {
|
|
return c as any;
|
|
}
|
|
|
|
const TransitionRoot = tsxComponent(transitionRoot);
|
|
const TransitionChild = tsxComponent(transitionChild);
|
|
const Dialog = tsxComponent(dialog);
|
|
const DialogOverlay = tsxComponent(dialogOverlay);
|
|
const Icon = tsxComponent(icon);
|
|
|
|
function classNames(...classes: string[]): string {
|
|
return classes.filter(Boolean).join(" ");
|
|
}
|
|
|
|
interface Attrs {
|
|
page: string;
|
|
}
|
|
|
|
const component: ClosureComponent<Attrs> = () => {
|
|
let sidebarOpen = false;
|
|
|
|
function setSidebarOpen(open: boolean): void {
|
|
sidebarOpen = open;
|
|
setTimeout(m.redraw);
|
|
}
|
|
|
|
return {
|
|
view: (vnode) => {
|
|
const navigation = [
|
|
{
|
|
name: "Overview",
|
|
href: "#!/overview",
|
|
enabled: window.authorizer.hasAccess("devices", 1),
|
|
},
|
|
{
|
|
name: "Devices",
|
|
href: "#!/devices",
|
|
enabled: window.authorizer.hasAccess("devices", 2),
|
|
},
|
|
{
|
|
name: "Faults",
|
|
href: "#!/faults",
|
|
enabled: window.authorizer.hasAccess("faults", 2),
|
|
},
|
|
{
|
|
name: "Presets",
|
|
href: "#!/presets",
|
|
enabled: window.authorizer.hasAccess("presets", 2),
|
|
},
|
|
{
|
|
name: "Provisions",
|
|
href: "#!/provisions",
|
|
enabled: window.authorizer.hasAccess("provisions", 2),
|
|
},
|
|
{
|
|
name: "Virtual Parameters",
|
|
href: "#!/virtualParameters",
|
|
enabled: window.authorizer.hasAccess("virtualParameters", 2),
|
|
},
|
|
{
|
|
name: "Files",
|
|
href: "#!/files",
|
|
enabled: window.authorizer.hasAccess("files", 2),
|
|
},
|
|
{
|
|
name: "Config",
|
|
href: "#!/config",
|
|
enabled: window.authorizer.hasAccess("config", 2),
|
|
},
|
|
{
|
|
name: "Permissions",
|
|
href: "#!/permissions",
|
|
enabled: window.authorizer.hasAccess("permissions", 2),
|
|
},
|
|
{
|
|
name: "Users",
|
|
href: "#!/users",
|
|
enabled: window.authorizer.hasAccess("users", 2),
|
|
},
|
|
]
|
|
.filter((item) => item.enabled)
|
|
.map(({ name, href }) => {
|
|
const n = href.slice(3);
|
|
return { name, href, active: vnode.attrs["page"] === n };
|
|
});
|
|
|
|
// --- BEGIN UNIFI THEME ---
|
|
const NAV_ICONS: Record<string, string> = {
|
|
"Overview": "nav-overview",
|
|
"Devices": "nav-devices",
|
|
"Faults": "nav-faults",
|
|
"Presets": "nav-presets",
|
|
"Provisions": "nav-provisions",
|
|
"Virtual Parameters": "nav-virtualparams",
|
|
"Files": "nav-files",
|
|
"Config": "nav-config",
|
|
"Permissions": "nav-permissions",
|
|
"Users": "nav-users",
|
|
};
|
|
// --- END UNIFI THEME ---
|
|
|
|
return [
|
|
<div>
|
|
<TransitionRoot show={!!sidebarOpen} duration={300}>
|
|
<Dialog
|
|
as="div"
|
|
class="fixed inset-0 flex z-40 md:hidden"
|
|
onClose={() => setSidebarOpen(false)}
|
|
>
|
|
<TransitionChild
|
|
enter="transition-opacity ease-linear duration-300"
|
|
enterFrom="opacity-0"
|
|
enterTo="opacity-100"
|
|
leave="transition-opacity ease-linear duration-300"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
<DialogOverlay class="fixed inset-0 bg-black/50" />
|
|
</TransitionChild>
|
|
<TransitionChild
|
|
enter="transition ease-in-out duration-300 transform"
|
|
enterFrom="-translate-x-full"
|
|
enterTo="translate-x-0"
|
|
leave="transition ease-in-out duration-300 transform"
|
|
leaveFrom="translate-x-0"
|
|
leaveTo="-translate-x-full"
|
|
>
|
|
{/* --- BEGIN UNIFI THEME --- */}
|
|
<div class="relative flex-1 flex flex-col max-w-xs w-full unifi-sidebar">
|
|
<TransitionChild
|
|
enter="ease-in-out duration-300"
|
|
enterFrom="opacity-0"
|
|
enterTo="opacity-100"
|
|
leave="ease-in-out duration-300"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
<div class="absolute top-0 right-0 -mr-12 pt-2">
|
|
<button
|
|
type="button"
|
|
class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-hidden focus:ring-2 focus:ring-inset focus:ring-white"
|
|
onclick={(e) => {
|
|
e.redraw = false;
|
|
setSidebarOpen(false);
|
|
}}
|
|
>
|
|
<span class="sr-only">Close sidebar</span>
|
|
<Icon name="close" class="h-6 w-6 text-white" />
|
|
</button>
|
|
</div>
|
|
</TransitionChild>
|
|
<div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
|
|
<div class="flex-shrink-0 flex items-center px-4">
|
|
<img class="h-10 w-auto" src={LOGO_SVG} alt="GenieACS" />
|
|
</div>
|
|
<nav class="mt-5 px-2 flex flex-col gap-1">
|
|
{navigation.map((item) => (
|
|
<a
|
|
key={item.name}
|
|
href={item.href}
|
|
class={`unifi-nav-item${item.active ? " active" : ""}`}
|
|
>
|
|
<Icon name={NAV_ICONS[item.name] || "nav-overview"} class="h-[18px] w-[18px]" />
|
|
{item.name}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
<div class="unifi-sidebar-footer">
|
|
{window.username ? (
|
|
<div class="unifi-user-info">
|
|
<Icon name="nav-users" class="h-4 w-4" />
|
|
{window.username}
|
|
<button
|
|
class="unifi-logout-btn"
|
|
onclick={(e) => {
|
|
e.target.disabled = true;
|
|
store
|
|
.logOut()
|
|
.then(() => {
|
|
location.hash = "";
|
|
location.reload();
|
|
})
|
|
.catch((err) => {
|
|
e.target.disabled = false;
|
|
notifications.push("error", err.message);
|
|
});
|
|
return false;
|
|
}}
|
|
>
|
|
Log out
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div class="unifi-user-info">
|
|
<a class="unifi-logout-btn" href="">Log in</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div class="unifi-version">v{VERSION}</div>
|
|
{/* --- END UNIFI THEME --- */}
|
|
</div>
|
|
</TransitionChild>
|
|
<div class="flex-shrink-0 w-14">
|
|
{/* Force sidebar to shrink to fit close icon */}
|
|
</div>
|
|
</Dialog>
|
|
</TransitionRoot>
|
|
|
|
{/* Static sidebar for desktop */}
|
|
{/* --- BEGIN UNIFI THEME --- */}
|
|
<div class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
|
<div class="flex-1 flex flex-col min-h-0 unifi-sidebar">
|
|
<div class="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
|
|
<div class="unifi-brand">
|
|
<img src={LOGO_SVG} alt="GenieACS" />
|
|
</div>
|
|
<nav class="mt-5 flex-1 px-2 flex flex-col gap-1">
|
|
{navigation.map((item) => (
|
|
<a
|
|
key={item.name}
|
|
href={item.href}
|
|
class={`unifi-nav-item${item.active ? " active" : ""}`}
|
|
>
|
|
<Icon name={NAV_ICONS[item.name] || "nav-overview"} class="h-[18px] w-[18px]" />
|
|
{item.name}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
<div class="unifi-sidebar-footer">
|
|
{window.username ? (
|
|
<div class="unifi-user-info">
|
|
<Icon name="nav-users" class="h-4 w-4" />
|
|
{window.username}
|
|
<button
|
|
class="unifi-logout-btn"
|
|
onclick={(e) => {
|
|
e.target.disabled = true;
|
|
store
|
|
.logOut()
|
|
.then(() => {
|
|
location.hash = "";
|
|
location.reload();
|
|
})
|
|
.catch((err) => {
|
|
e.target.disabled = false;
|
|
notifications.push("error", err.message);
|
|
});
|
|
return false;
|
|
}}
|
|
>
|
|
Log out
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div class="unifi-user-info">
|
|
<a class="unifi-logout-btn" href="">Log in</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div class="unifi-version">v{VERSION}</div>
|
|
</div>
|
|
</div>
|
|
{/* --- END UNIFI THEME --- */}
|
|
<div class="md:pl-64 flex flex-col flex-1 unifi-main-bg">
|
|
{/* --- BEGIN UNIFI THEME --- */}
|
|
<div class="sticky top-0 z-10 md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3">
|
|
<button
|
|
type="button"
|
|
class="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-stone-500 hover:text-stone-900 focus:outline-hidden focus:ring-2 focus:ring-inset focus:ring-cyan-500"
|
|
onclick={(e) => {
|
|
setSidebarOpen(true);
|
|
e.redraw = false;
|
|
return false;
|
|
}}
|
|
>
|
|
<span class="sr-only">Open sidebar</span>
|
|
<Icon name="menu" class="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
{/* --- END UNIFI THEME --- */}
|
|
<main class="flex-1">
|
|
<div class="py-6">
|
|
<div class="px-4 sm:px-6 md:px-8">
|
|
{m(drawerComponent)}
|
|
{vnode.children}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>,
|
|
overlay.render(),
|
|
m(datalist),
|
|
];
|
|
},
|
|
};
|
|
};
|
|
|
|
export default component;
|