mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-07 17:18:21 +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