mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-10 18:38:22 +00:00
add initial test client
This commit is contained in:
parent
0a51ba0beb
commit
fbc899fbe0
28 changed files with 4829 additions and 0 deletions
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
26
client/.gitignore
vendored
Normal file
26
client/.gitignore
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
src/generated
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
18
client/README.md
Normal file
18
client/README.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Type Support For `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension
|
||||||
|
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||||
|
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
15
client/index.html
Normal file
15
client/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="A web-based configuration tool for netfilter tables">
|
||||||
|
<meta name="theme-color" content="#1A237E"/>
|
||||||
|
<link rel="shortcut icon" href="favicon.svg" type="image/svg+xml">
|
||||||
|
<title>nfSense</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
90
client/package.json
Normal file
90
client/package.json
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{
|
||||||
|
"name": "nfsm",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/unplugin-vue-i18n": "^0.8.2",
|
||||||
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"@vueuse/head": "^1.1.15",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"focus-trap": "^7.3.1",
|
||||||
|
"focus-trap-vue": "^4.0.2",
|
||||||
|
"markdown-it-link-attributes": "^4.0.1",
|
||||||
|
"markdown-it-shiki": "^0.8.0",
|
||||||
|
"vue": "^3.2.45",
|
||||||
|
"vue-i18n": "9",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify/json": "^2.2.30",
|
||||||
|
"@types/events": "^3.0.0",
|
||||||
|
"@types/markdown-it-link-attributes": "^3.0.1",
|
||||||
|
"@typescript-eslint/parser": "^5.54.1",
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"@vue-macros/reactivity-transform": "^0.2.4",
|
||||||
|
"@vue-macros/volar": "^0.8.4",
|
||||||
|
"eslint": "^8.35.0",
|
||||||
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"unplugin-auto-import": "^0.15.0",
|
||||||
|
"unplugin-icons": "^0.15.3",
|
||||||
|
"unplugin-vue-components": "^0.24.0",
|
||||||
|
"unplugin-vue-macros": "^1.9.1",
|
||||||
|
"vite": "^4.1.0",
|
||||||
|
"vite-plugin-pages": "^0.28.0",
|
||||||
|
"vite-plugin-vue-markdown": "^0.22.4",
|
||||||
|
"vue-tsc": "^1.0.24"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"parser": "vue-eslint-parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "**/*.+(ts|vue)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-strongly-recommended"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"eslint-plugin-vue"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
"always-multiline"
|
||||||
|
],
|
||||||
|
"no-trailing-spaces": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"vue/html-closing-bracket-spacing": "off",
|
||||||
|
"vue/html-self-closing": "off",
|
||||||
|
"vue/first-attribute-linebreak": "off",
|
||||||
|
"vue/max-attributes-per-line": "off",
|
||||||
|
"vue/html-closing-bracket-newline": "off",
|
||||||
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3969
client/pnpm-lock.yaml
generated
Normal file
3969
client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
1
client/public/favicon.svg
Normal file
1
client/public/favicon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
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;
|
||||||
|
}
|
34
client/tsconfig.json
Normal file
34
client/tsconfig.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"vue/ref-macros",
|
||||||
|
"vite-plugin-pages/client",
|
||||||
|
"unplugin-vue-macros/macros-global"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vueCompilerOptions": {
|
||||||
|
"plugins": [
|
||||||
|
"@vue-macros/volar/define-model",
|
||||||
|
"@vue-macros/volar/define-slots"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": ["dist", "node_modules", "cypress"]
|
||||||
|
}
|
9
client/tsconfig.node.json
Normal file
9
client/tsconfig.node.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
101
client/vite.config.ts
Normal file
101
client/vite.config.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import Vue from '@vitejs/plugin-vue';
|
||||||
|
import Pages from 'vite-plugin-pages';
|
||||||
|
import Markdown from 'vite-plugin-vue-markdown';
|
||||||
|
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver';
|
||||||
|
import I18N from '@intlify/unplugin-vue-i18n/vite';
|
||||||
|
import Macros from 'unplugin-vue-macros/vite';
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
|
|
||||||
|
import Shiki from 'markdown-it-shiki';
|
||||||
|
import LinkAttributes from 'markdown-it-link-attributes';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
"proxy": {
|
||||||
|
"/api": "http://localhost:8080",
|
||||||
|
"/ws": {
|
||||||
|
target: "ws://localhost:8080",
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
Macros({
|
||||||
|
plugins: {
|
||||||
|
vue: Vue({
|
||||||
|
include: [/\.vue$/, /\.md$/],
|
||||||
|
reactivityTransform: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Pages({
|
||||||
|
extensions: ['vue', 'md'],
|
||||||
|
}),
|
||||||
|
Markdown({
|
||||||
|
wrapperClasses: 'prose prose-sm m-auto text-left',
|
||||||
|
headEnabled: true,
|
||||||
|
markdownItSetup(md) {
|
||||||
|
md.use(Shiki, {
|
||||||
|
theme: {
|
||||||
|
light: 'vitesse-light',
|
||||||
|
dark: 'vitesse-dark',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
md.use(LinkAttributes, {
|
||||||
|
matcher: (link: string) => /^https?:\/\//.test(link),
|
||||||
|
attrs: {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
extensions: ['vue', 'md'],
|
||||||
|
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||||
|
dts: 'src/generated/components.d.ts',
|
||||||
|
resolvers: [
|
||||||
|
IconsResolver(),
|
||||||
|
(componentName) => {
|
||||||
|
if (componentName === 'FocusTrap')
|
||||||
|
return { name: 'FocusTrap', from: 'focus-trap-vue' };
|
||||||
|
},
|
||||||
|
],
|
||||||
|
types: [{
|
||||||
|
from: 'focus-trap-vue',
|
||||||
|
names: ['FocusTrap'],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
Icons({
|
||||||
|
}),
|
||||||
|
I18N({
|
||||||
|
runtimeOnly: true,
|
||||||
|
compositionOnly: true,
|
||||||
|
fullInstall: true,
|
||||||
|
include: ['src/locales'],
|
||||||
|
}),
|
||||||
|
AutoImport({
|
||||||
|
include: [
|
||||||
|
/\.[tj]sx?$/,
|
||||||
|
/\.vue$/, /\.vue\?vue/,
|
||||||
|
/\.md$/,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
|
'vue-i18n',
|
||||||
|
'vue/macros',
|
||||||
|
'@vueuse/core',
|
||||||
|
'@vueuse/head',
|
||||||
|
],
|
||||||
|
dts: 'src/generated/auto-imports.d.ts',
|
||||||
|
dirs: ['src/composables'],
|
||||||
|
vueTemplate: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue