add initial test client

This commit is contained in:
Samuel Lorch 2023-03-06 23:56:36 +01:00
parent 0a51ba0beb
commit fbc899fbe0
28 changed files with 4829 additions and 0 deletions

133
client/src/App.vue Normal file
View 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
View 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);
}
}

View 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>

View 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>

View 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>

View 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; }

View 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);
}
}

View 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);
}

View 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;
}

View 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;
}

View file

@ -0,0 +1,3 @@
{
"test": "This is a test translation."
}

22
client/src/main.ts Normal file
View 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');

View 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>

View 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>

View 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.

View 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
View 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;
}