mirror of
https://github.com/speatzle/nfsense.git
synced 2025-09-13 15:19:08 +00:00
add initial test client
This commit is contained in:
parent
0a51ba0beb
commit
fbc899fbe0
28 changed files with 4829 additions and 0 deletions
133
client/src/App.vue
Normal file
133
client/src/App.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import IDashboard from '~icons/ri/dashboard-2-line';
|
||||
import IRule from '~icons/material-symbols/rule-folder-outline-sharp';
|
||||
import IAddress from '~icons/eos-icons/ip';
|
||||
|
||||
|
||||
|
||||
enum NavState { Open, Reduced, Collapsed };
|
||||
const NavStateCount = 3;
|
||||
let navState = $ref(NavState.Open);
|
||||
let loggedOut = $ref(false);
|
||||
|
||||
const navRoutes = {
|
||||
"/": { icon: IDashboard, caption: "Dashboard" },
|
||||
"/rules": { icon: IRule, caption: "Rules" },
|
||||
"/addresses": { icon: IAddress, caption: "Addresses" },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!loggedOut" :class="{
|
||||
'layout': 1,
|
||||
'nav-state-open': navState === NavState.Open,
|
||||
'nav-state-collapsed': navState === NavState.Collapsed,
|
||||
'nav-state-reduced': navState === NavState.Reduced,
|
||||
}">
|
||||
<button class="nav-head" @click="() => navState = (navState + 1) % NavStateCount">
|
||||
nfSense
|
||||
</button>
|
||||
|
||||
<Portal from="page-header" class="page-header pad gap"/>
|
||||
|
||||
<div class="nav-body">
|
||||
<template v-for="(options, route) in navRoutes" :key="route">
|
||||
<router-link :to="route" class="button">
|
||||
<component :is="options.icon"/>
|
||||
{{ options.caption }}
|
||||
</router-link>
|
||||
</template>
|
||||
<div class="flex-grow"/>
|
||||
<div class="flex-row">
|
||||
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
|
||||
<router-link class="button" to="/settings"><i-material-symbols-settings/></router-link>
|
||||
<button @click="() => loggedOut = true"><i-material-symbols-logout/></button>
|
||||
</div>
|
||||
</div>
|
||||
<router-view v-slot="{ Component, route }" v-if="!loggedOut">
|
||||
<Transition name="fade">
|
||||
<component :is="Component" :key="{route}" class="page-content pad gap"/>
|
||||
</Transition>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div class="login" v-if="loggedOut">
|
||||
<FocusTrap>
|
||||
<form @submit="$event => $event.preventDefault()">
|
||||
<h1>nfSense Login</h1>
|
||||
<label for="username" v-text="'Username'"/>
|
||||
<input name="username"/>
|
||||
<label for="password" v-text="'Password'" type="password"/>
|
||||
<input name="password"/>
|
||||
|
||||
<button @click="() => loggedOut = false">Login</button>
|
||||
</form>
|
||||
</FocusTrap>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Basic Layout */
|
||||
.layout, .login {
|
||||
position: absolute;
|
||||
left: 0px; right: 0px; top: 0px; bottom: 0px;
|
||||
|
||||
display: grid;
|
||||
background-color: var(--cl-bg);
|
||||
}
|
||||
.layout {
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
.login { place-items: center; }
|
||||
|
||||
/* Navigation */
|
||||
.nav-head, .nav-body { background: var(--cl-bg-low); }
|
||||
.nav-head {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.nav-body .button { justify-content: left; }
|
||||
.nav-body .flex-row * { flex: 1; }
|
||||
|
||||
/* Page */
|
||||
.page-header {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
.page-header button svg {
|
||||
margin: -0.25rem;
|
||||
}
|
||||
.page-content {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
background: var(--cl-bg);
|
||||
}
|
||||
|
||||
/* Nav-Body-Collapsing */
|
||||
.nav-body, .page-content {
|
||||
position: relative;
|
||||
left: 0%;
|
||||
width: 100%;
|
||||
transition: left 0.2s ease-out, width 0.2s ease-out;
|
||||
--reduced-width: 2.5rem;
|
||||
}
|
||||
.nav-state-reduced .nav-body { width: calc(0% + var(--reduced-width)); }
|
||||
.nav-state-reduced .page-content {
|
||||
left: calc(calc(-100vw + 100%) + var(--reduced-width));
|
||||
width: calc(calc(0% + 100vw) - var(--reduced-width));
|
||||
}
|
||||
.nav-state-collapsed .nav-body { width: 0%; }
|
||||
.nav-state-collapsed .page-content {
|
||||
left: calc(-100vw + 100%);
|
||||
width: calc(0% + 100vw);
|
||||
}
|
||||
:not(.nav-state-open) > .nav-body > .flex-row {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
14
client/src/api.ts
Normal file
14
client/src/api.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { RequestManager, HTTPTransport, WebSocketTransport, Client } from "@open-rpc/client-js";
|
||||
const httpTransport = new HTTPTransport("http://"+ window.location.host +"/api");
|
||||
const socktransport = new WebSocketTransport("ws://"+ window.location.host + "/ws/api");
|
||||
const manager = new RequestManager([socktransport, httpTransport], () => crypto.randomUUID());
|
||||
const client = new Client(manager);
|
||||
|
||||
export async function apiCall(method: string, params: Record<string, any>){
|
||||
try {
|
||||
const result = await client.request({method, params});
|
||||
console.debug("api call result", result);
|
||||
} catch (ex){
|
||||
console.debug("api call epic fail", ex);
|
||||
}
|
||||
}
|
55
client/src/components/NiceTable.vue
Normal file
55
client/src/components/NiceTable.vue
Normal file
|
@ -0,0 +1,55 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineModel<{
|
||||
columns?: Record<string, string>,
|
||||
data?: Record<string, any>[],
|
||||
sortSelf?: boolean,
|
||||
sortBy?: string,
|
||||
sortDesc?: boolean,
|
||||
}>();
|
||||
let { columns, data, sortSelf, sortBy, sortDesc } = $(props);
|
||||
|
||||
const displayData = $computed(() => (sortSelf && sortBy !== '')
|
||||
? data?.sort((a, b) => {
|
||||
let result;
|
||||
if (a[sortBy ?? ''] > b[sortBy ?? '']) result = 1;
|
||||
else if (a[sortBy ?? ''] === b[sortBy ?? '']) result = 0;
|
||||
else result = -1;
|
||||
|
||||
if (sortDesc) return -result;
|
||||
return result;
|
||||
})
|
||||
: data);
|
||||
|
||||
function toggleSorting(columnName: string) {
|
||||
if (columnName === sortBy) sortDesc = !sortDesc;
|
||||
else {
|
||||
sortDesc = false;
|
||||
sortBy = columnName;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="[name, heading] in Object.entries(columns ?? {})" :key="name" @click="toggleSorting(name)">
|
||||
<div class="flex-row">
|
||||
{{ heading }}
|
||||
<i-mdi-arrow-down v-if="name === sortBy && sortDesc"/>
|
||||
<i-mdi-arrow-up v-else-if="name === sortBy"/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<tr v-for="row of displayData">
|
||||
<td v-for="cell in row" :key="cell" v-text="cell"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
16
client/src/components/meta/PageHeader.vue
Normal file
16
client/src/components/meta/PageHeader.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
const { title, noSpacer } = $(withDefaults(defineProps<{
|
||||
title?: string,
|
||||
noSpacer?: boolean,
|
||||
}>(), {
|
||||
title: "",
|
||||
noSpacer: false,
|
||||
}));
|
||||
watchEffect(() => useTitle(`${title} - nfSense`));
|
||||
</script>
|
||||
<template>
|
||||
<Portal to="page-header">
|
||||
<h1 v-if="title !== ''" v-text="title" :class="{'flex-grow': !noSpacer}"/>
|
||||
<slot/>
|
||||
</Portal>
|
||||
</template>
|
25
client/src/components/meta/Portal.vue
Normal file
25
client/src/components/meta/Portal.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
let activeTargets = $ref<string[]>([]);
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = $defineProps<{
|
||||
from?: string,
|
||||
to?: string,
|
||||
}>();
|
||||
const { from, to } = $(props);
|
||||
|
||||
if (from) {
|
||||
onMounted(() => activeTargets.push(from));
|
||||
onBeforeUnmount(() => activeTargets.splice(activeTargets.indexOf(from), 1));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="from" :id="'portal-' + from">
|
||||
<slot/>
|
||||
</div>
|
||||
<Teleport v-else-if="to && activeTargets.includes(to)" :to="'#portal-' + to">
|
||||
<slot/>
|
||||
</Teleport>
|
||||
</template>
|
6
client/src/global-styles/atomic.css
Normal file
6
client/src/global-styles/atomic.css
Normal file
|
@ -0,0 +1,6 @@
|
|||
/* Atomic Styles */
|
||||
.text-select { user-select: text; }
|
||||
.pad { padding: 0.5rem; }
|
||||
.gap { gap: 0.5rem; }
|
||||
.flex-grow { flex-grow: 1; }
|
||||
.flex-row { flex-direction: row; }
|
34
client/src/global-styles/colors.css
Normal file
34
client/src/global-styles/colors.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
/* Coloring */
|
||||
:root {
|
||||
/* Color Definitions */
|
||||
--cl-md-50: #FAFAFA;
|
||||
--cl-md-100: #F5F5F5;
|
||||
--cl-md-200: #EEEEEE;
|
||||
--cl-md-300: #E0E0E0;
|
||||
--cl-md-400: #BDBDBD;
|
||||
--cl-md-500: #9E9E9E;
|
||||
--cl-md-600: #757575;
|
||||
--cl-md-700: #616161;
|
||||
--cl-md-800: #424242;
|
||||
--cl-md-900: #212121;
|
||||
|
||||
/* Color Uses */
|
||||
--cl-bg: var(--cl-md-900);
|
||||
--cl-bg-mid: var(--cl-md-800);
|
||||
--cl-bg-low: var(--cl-md-700);
|
||||
--cl-fg: var(--cl-md-100);
|
||||
|
||||
/* Apply as default */
|
||||
background-color: var(--cl-bg);
|
||||
color: var(--cl-fg);
|
||||
}
|
||||
|
||||
/* Changes for light mode */
|
||||
@media screen and (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--cl-bg: var(--cl-md-100);
|
||||
--cl-bg-mid: var(--cl-md-200);
|
||||
--cl-bg-low: var(--cl-md-300);
|
||||
--cl-fg: var(--cl-md-900);
|
||||
}
|
||||
}
|
75
client/src/global-styles/components.css
Normal file
75
client/src/global-styles/components.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
/* CSS Components */
|
||||
button, .button {
|
||||
all: unset;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
overflow: hidden;
|
||||
vertical-align: center;
|
||||
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
|
||||
background-color: var(--cl-bg-low);
|
||||
}
|
||||
.button > svg, button > svg {
|
||||
min-width: 1.5rem;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
.button:hover, button:hover {
|
||||
background-color: var(--cl-bg-mid);
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
background-color: var(--cl-bg-low);
|
||||
}
|
||||
form > :is(button, .button, h1) {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
form button, form .button {
|
||||
background-color: var(--cl-bg);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead {
|
||||
background-color: var(--cl-bg-low);
|
||||
}
|
||||
th:hover {
|
||||
background-color: var(--cl-bg-mid);
|
||||
cursor: pointer;
|
||||
}
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
border: 0.125rem solid var(--cl-bg-mid);
|
||||
}
|
||||
th > *{
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
th svg {
|
||||
height: 1rem;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: var(--cl-bg-mid)
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: block;
|
||||
padding: 0.4rem;
|
||||
background-color: var(--cl-bg-low);
|
||||
color: inherit;
|
||||
border: 1px solid var(--cl-fg);
|
||||
}
|
33
client/src/global-styles/mlfe.css
Normal file
33
client/src/global-styles/mlfe.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* MLFE: Marginless FlexEverything (A CSS Reset for creating Layouts) */
|
||||
:root {
|
||||
font-family: sans-serif;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
user-select: inherit;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div, ul, ol, nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
h1 { font-size: calc(1rem + calc(1rem / 1))}
|
||||
h2 { font-size: calc(1rem + calc(1rem / 2))}
|
||||
h3 { font-size: calc(1rem + calc(1rem / 3))}
|
||||
h4 { font-size: calc(1rem + calc(1rem / 4))}
|
||||
h5 { font-size: calc(1rem + calc(1rem / 5))}
|
||||
h6 { font-size: calc(1rem + calc(1rem / 6))}
|
||||
|
||||
ul, ol {
|
||||
gap: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
}
|
6
client/src/global-styles/transitions.css
Normal file
6
client/src/global-styles/transitions.css
Normal file
|
@ -0,0 +1,6 @@
|
|||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.2s ease-out !important;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
3
client/src/locales/en-US.json
Normal file
3
client/src/locales/en-US.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"test": "This is a test translation."
|
||||
}
|
22
client/src/main.ts
Normal file
22
client/src/main.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import './global-styles/atomic.css';
|
||||
import './global-styles/components.css';
|
||||
import './global-styles/colors.css';
|
||||
import './global-styles/mlfe.css';
|
||||
import './global-styles/transitions.css';
|
||||
|
||||
import App from './App.vue';
|
||||
import { createHead } from '@vueuse/head';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import routes from '~pages';
|
||||
|
||||
const app = createApp(App);
|
||||
const head = createHead();
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
app.use(router);
|
||||
app.use(head);
|
||||
|
||||
app.mount('#app');
|
15
client/src/pages/[entity]/[id].vue
Normal file
15
client/src/pages/[entity]/[id].vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
const props = $defineProps<{entity: string, id: string}>();
|
||||
const { entity, id } = $(props);
|
||||
|
||||
const pageTypes: { [key: string]: any } = {
|
||||
"rules": { title: "Rules" },
|
||||
"addresses": { title: "Addresses"},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader :title="pageTypes[entity].title"/>
|
||||
{{ entity }} {{ id }}
|
||||
</div>
|
||||
</template>
|
90
client/src/pages/[entity]/index.vue
Normal file
90
client/src/pages/[entity]/index.vue
Normal file
|
@ -0,0 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
const props = $defineProps<{entity: string}>();
|
||||
const { entity } = $(props);
|
||||
|
||||
const pageTypes: { [key: string]: any } = {
|
||||
"rules": { title: "Rules" },
|
||||
"addresses": { title: "Addresses"},
|
||||
};
|
||||
|
||||
let searchTerm = $ref("");
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader :title="pageTypes[entity].title">
|
||||
<input class="search-bar" placeholder="Search..." v-model="searchTerm"/>
|
||||
<button>
|
||||
<i-material-symbols-add/>
|
||||
</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="{fname: 'First Name', lname: 'Last Name'}" :sort-self="true" :data="[
|
||||
{
|
||||
fname: 'Haynes',
|
||||
lname: 'Chavez'
|
||||
}, {
|
||||
fname: 'Brennan',
|
||||
lname: 'Bradley'
|
||||
}, {
|
||||
fname: 'Blanchard',
|
||||
lname: 'Thornton'
|
||||
}, {
|
||||
fname: 'Benjamin',
|
||||
lname: 'Nash'
|
||||
}, {
|
||||
fname: 'Jan',
|
||||
lname: 'Bradford'
|
||||
}, {
|
||||
fname: 'Zelma',
|
||||
lname: 'Spears'
|
||||
}, {
|
||||
fname: 'Freeman',
|
||||
lname: 'Page'
|
||||
}, {
|
||||
fname: 'Wilson',
|
||||
lname: 'Carlson'
|
||||
}, {
|
||||
fname: 'Lewis',
|
||||
lname: 'Fuentes'
|
||||
}, {
|
||||
fname: 'Vega',
|
||||
lname: 'Villarreal'
|
||||
}, {
|
||||
fname: 'Carolyn',
|
||||
lname: 'Cardenas'
|
||||
}, {
|
||||
fname: 'Angie',
|
||||
lname: 'Adams'
|
||||
}, {
|
||||
fname: 'Richards',
|
||||
lname: 'Leon'
|
||||
}, {
|
||||
fname: 'Velma',
|
||||
lname: 'Fields'
|
||||
}, {
|
||||
fname: 'Witt',
|
||||
lname: 'Lowe'
|
||||
}, {
|
||||
fname: 'Waters',
|
||||
lname: 'Leblanc'
|
||||
}, {
|
||||
fname: 'Henry',
|
||||
lname: 'Lloyd'
|
||||
}, {
|
||||
fname: 'Boone',
|
||||
lname: 'Greer'
|
||||
}, {
|
||||
fname: 'Willis',
|
||||
lname: 'Stark'
|
||||
}, {
|
||||
fname: 'Dickson',
|
||||
lname: 'Spencer'
|
||||
}
|
||||
].filter(x => (`${x.fname} ${x.lname}`).toLowerCase().includes(searchTerm.toLowerCase()))"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
4
client/src/pages/help/index.md
Normal file
4
client/src/pages/help/index.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
<PageHeader title="Help"/>
|
||||
|
||||
## About
|
||||
nfSense is a web-based configuration tool for nfTables. It works by storing its configuration in a .json file, and applies it by transforming it into a proper nfTables config.
|
18
client/src/pages/index.vue
Normal file
18
client/src/pages/index.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../api";
|
||||
|
||||
async function doShit(){
|
||||
apiCall("Firewall.GetForwardRules", {});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="Dashboard">
|
||||
<button @click="doShit">Example Buttons</button>
|
||||
</PageHeader>
|
||||
|
||||
This is the main page, currently written in markdown because that's *pog*.
|
||||
</div>
|
||||
</template>
|
7
client/src/vite-env.d.ts
vendored
Normal file
7
client/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="unplugin-icons/types/vue" />
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue