Added Nav Dropdowns

- Also centered nfsense logo
- TODO: Find way for nested dropdowns to not look dumb in reduced state
This commit is contained in:
adro 2023-11-03 03:11:02 +01:00
parent eac98192be
commit db4d33e354
3 changed files with 127 additions and 25 deletions

View file

@ -16,28 +16,44 @@ import ITimeServer from '~icons/carbon/server-time';
import IWireguard from '~icons/simple-icons/wireguard'; import IWireguard from '~icons/simple-icons/wireguard';
import IDHCPServer from '~icons/material-symbols/book-rounded'; import IDHCPServer from '~icons/material-symbols/book-rounded';
import IUser from '~icons/mdi/user'; import IUser from '~icons/mdi/user';
import IServer from '~icons/ri/server-line';
import INodes from '~icons/fa6-solid/share-nodes';
import IList from '~icons/material-symbols/format-list-bulleted';
import IFirewall from '~icons/mdi/wall-fire';
import INetwork from '~icons/mdi/lan';
enum NavState { Open, Reduced, Collapsed }; enum NavState { Open, Reduced, Collapsed };
const NavStateCount = 3; const NavStateCount = 3;
let navState = $ref(NavState.Open); let navState = $ref(NavState.Open);
const navRoutes = {
'/': { icon: IDashboard, caption: 'Dashboard' }, const navRoutesNew = [
'/firewall/forward_rules': { icon: IRule, caption: 'Rules' }, { caption: 'Dashboard', icon: IDashboard, href: '/' },
'/firewall/source_nat_rules': { icon: ISNAT, caption: 'SNAT' }, { caption: 'Firewall', icon: IFirewall, children: [
'/firewall/destination_nat_rules': { icon: IDNAT, caption: 'DNAT' }, { caption: 'Rules', icon: IRule, href: '/firewall/forward_rules' },
'/network/interfaces': { icon: IEthernet, caption: 'Interfaces' }, { caption: 'SNAT', icon: ISNAT, href: '/firewall/source_nat_rules' },
'/network/static_routes': { icon: IStaticRoutes, caption: 'Static Routes' }, { caption: 'DNAT', icon: IDNAT, href: '/firewall/destination_nat_rules' },
'/object/addresses': { icon: IAddress, caption: 'Addresses' }, ]},
'/object/services': { icon: IService, caption: 'Services' }, { caption: 'Network', icon: INetwork, children: [
'/service/dhcp_servers': { icon: IDHCPServer, caption: 'DHCP Server' }, { caption: 'Interfaces', icon: IEthernet, href: '/network/interfaces' },
'/service/dns_servers': { icon: IDNSServer, caption: 'DNS Server' }, { caption: 'Static Routes', icon: IStaticRoutes, href: '/network/static_routes' },
'/service/ntp_servers': { icon: ITimeServer, caption: 'NTP Server' }, ] },
'/vpn/wireguard_status': { icon: IWireguard, caption: 'Wireguard Status' }, { caption: 'Objects', icon: IList, children: [
'/vpn/wireguard_interfaces': { icon: IWireguard, caption: 'Wireguard Interfaces' }, { caption: 'Addresses', icon: IAddress, href: '/object/addresses' },
'/vpn/wireguard_peers': { icon: IWireguard, caption: 'Wireguard Peers' }, { caption: 'Services', icon: IService, href: '/object/services' },
'/system/users': { icon: IUser, caption: 'Users' }, ] },
'/config/config': { icon: IConfig, caption: 'Config' }, { caption: 'Services', icon: IServer, children: [
}; { caption: 'DHCP', icon: IDHCPServer, href: '/service/dhcp_servers' },
{ caption: 'DNS', icon: IDNSServer, href: '/service/dns_servers' },
{ caption: 'NTP', icon: ITimeServer, href: '/service/ntp_servers' },
{ caption: 'Wireguard', icon: IWireguard, children: [
{ caption: 'Status', icon: IDashboard, href: '/vpn/wireguard_status' },
{ caption: 'Interfaces', icon: IEthernet, href: '/vpn/wireguard_interfaces' },
{ caption: 'Peers', icon: INodes, href: '/vpn/wireguard_peers' },
]},
] },
{ caption: 'Users', icon: IUser, href: '/system/users' },
{ caption: 'Config', icon: IConfig, href: '/config/config' },
];
enum AuthState { Unauthenticated, MfaRequired, Authenticated }; enum AuthState { Unauthenticated, MfaRequired, Authenticated };
let authState = $ref(AuthState.Unauthenticated); let authState = $ref(AuthState.Unauthenticated);
@ -130,12 +146,7 @@ onMounted(async() => {
<div class="nav-body cl-secondary cl-force-dark"> <div class="nav-body cl-secondary cl-force-dark">
<div> <div>
<template v-for="(options, route) in navRoutes" :key="route"> <NavElements :routes="navRoutesNew" :click-handler="collapseNavIfMobile"/>
<router-link :to="route" class="button" @click="collapseNavIfMobile">
<component :is="options.icon"/>
{{ options.caption }}
</router-link>
</template>
</div> </div>
<div class="flex-row"> <div class="flex-row">
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link> <router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
@ -190,7 +201,7 @@ onMounted(async() => {
.page-header { grid-area: PH; } .page-header { grid-area: PH; }
.page-content { grid-area: PC; } .page-content { grid-area: PC; }
.nav-head { font-weight: bold; } .nav-head { font-weight: bold; text-align: center; }
.nav-head > svg { display: none; } .nav-head > svg { display: none; }
.nav-head > h1 { flex-grow: 1; } .nav-head > h1 { flex-grow: 1; }

View file

@ -0,0 +1,63 @@
<script setup lang="ts">
import { NavRoute } from './NavElements.vue';
withDefaults(defineProps<{
icon?: string | Component,
caption?: string,
children: NavRoute[],
clickHandler?: () => void,
}>(), {
icon: '',
caption: '',
children: () => [],
clickHandler: () => {},
});
let expanded = $ref(false);
function tallyChildren(routes: NavRoute[]) {
let count = routes.length;
for (const route of routes)
if (route.children)
count += tallyChildren(route.children);
return count;
}
</script>
<template>
<div :class="{'nav-dropdown-expanded': expanded}" :style="`--predicted-height: ${2.5 * tallyChildren(children)}rem;`">
<div class="button" @click="expanded = !expanded">
<component v-if="(typeof icon !== 'string')" :is="icon"/>
<template v-else>{{ icon }}</template>
<span v-text="caption"/>
<i-material-symbols-expand-more class="nav-dropdown-expand-icon" width="1em" height="1em"/>
</div>
<div class="nav-dropdown-body">
<NavElements :routes="children" :click-handler="clickHandler"/>
</div>
</div>
</template>
<style>
span {
flex-grow: 1;
}
.nav-dropdown-body > :is(button, .button) {
padding-left: calc(0.5rem - 1px);
}
.nav-dropdown-body {
transition: all 0.1s ease-out;
max-height: 0px;
border-left: 1px solid white;
}
.nav-dropdown-expanded > .nav-dropdown-body {
max-height: var(--predicted-height);
}
.nav-dropdown-expand-icon {
transition: all 0.1s ease-out;
}
.nav-dropdown-expanded > div > .nav-dropdown-expand-icon {
transform: rotate(180deg);
}
</style>

View file

@ -0,0 +1,28 @@
<script lang="ts">
export type NavRoute = {
icon?: Component,
caption?: string,
children?: NavRoute[],
href?: string,
};
</script>
<script setup lang="ts">
withDefaults(defineProps<{
routes?: NavRoute[],
clickHandler?: () => void,
}>(), {
routes: () => [],
clickHandler: () => {},
});
</script>
<template>
<template v-if="routes">
<template v-for="route of routes" :key="route.href">
<router-link v-if="route.href" :to="route.href" class="button" @click="clickHandler" :title="route.caption">
<component :is="route.icon"/>
{{ route.caption }}
</router-link>
<NavDropdown v-else v-bind="route"/>
</template>
</template>
</template>