mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-11 19:08:20 +00:00
Nice form (#11)
* wip * vee-validate experiments * get test enabled form working * Make PillBar Properly work with ModelValue * Register NumberBox Globally * Rework NiceForm * Use new form props * Rework Definitions for new Form
This commit is contained in:
parent
2fb089ba73
commit
4c78d1da66
7 changed files with 153 additions and 74 deletions
|
@ -2,38 +2,60 @@
|
||||||
|
|
||||||
const props = defineModel<{
|
const props = defineModel<{
|
||||||
title: string
|
title: string
|
||||||
fields: {
|
sections: {
|
||||||
key: string,
|
title: string
|
||||||
label: string,
|
fields: {
|
||||||
component: () => Component,
|
key: string,
|
||||||
props: any,
|
label: string,
|
||||||
}[]
|
as: string,
|
||||||
|
props: any,
|
||||||
|
default: any,
|
||||||
|
enabled?: (values: Record<string, any>) => Boolean,
|
||||||
|
rules?: (value: any) => true | string,
|
||||||
|
}[],
|
||||||
|
}[],
|
||||||
|
modelValue: any,
|
||||||
}>();
|
}>();
|
||||||
let { title, fields } = $(props);
|
|
||||||
|
let { sections } = $(props);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="form">
|
<ValidationForm as="div" v-slot="{ values }" @submit="false">
|
||||||
<h2>{{ title }}</h2>
|
<template v-for="(section, index) in sections" :key="index">
|
||||||
<template v-for="(field, index) in fields" :key="index">
|
<h4 v-if="section.title">{{ section.title }}</h4>
|
||||||
<label :for="field.key" v-text="field.label"/>
|
<div class="section">
|
||||||
<component :name="field.key" :is="field.component()" v-bind="field.props"/>
|
<template v-for="(field, index) in section.fields" :key="index">
|
||||||
|
<template v-if="field.enabled ? field.enabled(values) : true">
|
||||||
|
<label :for="field.key" v-text="field.label" />
|
||||||
|
<Field :name="field.key" :as="field.as" :rules="field.rules" v-bind="field.props" />
|
||||||
|
<ErrorMessage :name="field.key" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
<p>{{ values }}</p>
|
||||||
|
</ValidationForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.form {
|
.section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
.form > :is(button, .button, h2) {
|
|
||||||
|
h4,
|
||||||
|
p {
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
}
|
}
|
||||||
.form > :is(label) {
|
|
||||||
grid-column: 1;
|
h4 {
|
||||||
|
background-color: var(--cl-bg-hl);
|
||||||
|
padding: 0.3rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -10,25 +10,17 @@ const props = defineModel<{
|
||||||
}>();
|
}>();
|
||||||
let { options, modelValue } = $(props);
|
let { options, modelValue } = $(props);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
onMounted(async() => {
|
||||||
(event: 'selectionChanged'): void
|
if (modelValue === undefined) {
|
||||||
}>();
|
modelValue = 0
|
||||||
|
|
||||||
function select(option: any, index: number) {
|
|
||||||
for(let opt of options) {
|
|
||||||
opt.selected = false;
|
|
||||||
}
|
}
|
||||||
option.selected = true;
|
});
|
||||||
modelValue = index;
|
|
||||||
emit('selectionChanged');
|
|
||||||
console.debug("selected", options);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button class="option" v-for="(option, index) in options" :key="index" :class="{selected:option.selected}" @click="select(option, index)">
|
<button class="option" v-for="(option, index) in options" :key="index" :class="{selected: modelValue == index}" @click="modelValue = index">
|
||||||
<i class="material-icons" v-if="option.icon">{{ option.icon }}</i>
|
<i class="material-icons" v-if="option.icon">{{ option.icon }}</i>
|
||||||
{{ option.name }}
|
{{ option.name }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,35 +1,42 @@
|
||||||
import PillBar from "./components/inputs/PillBar.vue";
|
export const editTypes: { [key: string]: { [key: string]: any } } = {
|
||||||
import TextBox from "./components/inputs/TextBox.vue";
|
"firewall": {
|
||||||
import NumberBox from "./components/inputs/NumberBox.vue";
|
"forwardrules": {
|
||||||
import MultilineTextBox from "./components/inputs/MultilineTextBox.vue";
|
title: "Forward Rule",
|
||||||
import CheckBox from "./components/inputs/CheckBox.vue";
|
sections: [
|
||||||
|
{
|
||||||
export const editTypes: { [key: string]: {[key: string]: any} } = {
|
fields: [
|
||||||
"firewall": {
|
{ key: "name", label: "Name", as: "TextBox" },
|
||||||
"forwardrules": {
|
{ key: "verdict", label: "Verdict", as: "PillBar", props: { options: [{ name: 'Accept' }, { name: 'Drop' }, { name: 'Continue' }] } },
|
||||||
title: "Forward Rule",
|
{ key: "counter", label: "Counter", as: "CheckBox", },
|
||||||
fields: [
|
{ key: "comment", label: "Comment", as: "MultilineTextBox", },
|
||||||
{key: "name", label: "Name", component: () => TextBox },
|
|
||||||
{key: "verdict", label: "Verdict", component: () => PillBar, props: {options: [{name: 'Accept'}, {name: 'Drop'}, {name: 'Continue'}]}},
|
|
||||||
{key: "counter", label: "Counter", component: () => CheckBox },
|
|
||||||
{key: "comment", label: "Comment", component: () => MultilineTextBox },
|
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"network": {
|
},
|
||||||
"interfaces": {
|
"network": {
|
||||||
title: "Interfaces",
|
"interfaces": {
|
||||||
fields: [
|
title: "Interfaces",
|
||||||
{key: "name", label: "Name", component: () => TextBox },
|
sections: [
|
||||||
{key: "type", label: "Type", component: () => PillBar, props: {options: [{name: 'Hardware', selected: true}, {name: 'VLAN'}, {name: 'Bond'}, {name: 'Brdige'}]}},
|
{
|
||||||
{key: "hardware_interface", label: "Hardware Interface", component: () => TextBox },
|
fields: [
|
||||||
{key: "vlan_id", label: "VLAN ID", component: () => NumberBox, props: {min: 1, max: 4094} },
|
{ key: "name", label: "Name", as: "TextBox", default: "placeholder" },
|
||||||
{key: "bond_members", label: "Nond Members", component: () => TextBox },
|
{ key: "type", label: "Type", as: "PillBar", props: { options: [{ name: 'Hardware' }, { name: 'VLAN' }, { name: 'Bond' }, { name: 'Bridge' }] } },
|
||||||
{key: "bridge_members", label: "Bridge Memebers", component: () => TextBox },
|
{ key: "hardware_interface", label: "Hardware Interface", as: "TextBox", enabled: (values: any) => (values["type"] == 0) },
|
||||||
{key: "addressing_mode", label: "Addressing Mode", component: () => PillBar, props: {options: [{name: 'None', selected: true}, {name: 'Static'}, {name: 'DHCP'}]}},
|
{ key: "vlan_id", label: "VLAN ID", as: "NumberBox", props: { min: 1, max: 4094 }, enabled: (values: any) => (values["type"] == 1) },
|
||||||
{key: "address", label: "Address", component: () => TextBox },
|
{ key: "bond_members", label: "Bond Members", as: "TextBox", enabled: (values: any) => (values["type"] == 2) },
|
||||||
{key: "comment", label: "Comment", component: () => MultilineTextBox },
|
{ key: "bridge_members", label: "Bridge Members", as: "TextBox", enabled: (values: any) => (values["type"] == 3) },
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
title: "Addressing",
|
||||||
|
fields: [
|
||||||
|
{ key: "addressing_mode", label: "Addressing Mode", as: "PillBar", props: { options: [{ name: 'None', selected: true }, { name: 'Static' }, { name: 'DHCP' }] } },
|
||||||
|
{ key: "address", label: "Address", as: "TextBox", enabled: (values: any) => (values["addressing_mode"] == 1) },
|
||||||
|
{ key: "comment", label: "Comment", as: "MultilineTextBox" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
|
@ -5,6 +5,14 @@ import './global-styles/mlfe.css';
|
||||||
import './global-styles/transitions.css';
|
import './global-styles/transitions.css';
|
||||||
import 'vue-toast-notification/dist/theme-default.css';
|
import 'vue-toast-notification/dist/theme-default.css';
|
||||||
|
|
||||||
|
import PillBar from "./components/inputs/PillBar.vue";
|
||||||
|
import TextBox from "./components/inputs/TextBox.vue";
|
||||||
|
import NumberBox from "./components/inputs/NumberBox.vue";
|
||||||
|
import MultilineTextBox from "./components/inputs/MultilineTextBox.vue";
|
||||||
|
import CheckBox from "./components/inputs/CheckBox.vue";
|
||||||
|
|
||||||
|
import { Form, Field, ErrorMessage } from 'vee-validate';
|
||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { createHead } from '@vueuse/head';
|
import { createHead } from '@vueuse/head';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
@ -22,4 +30,14 @@ app.use(router);
|
||||||
app.use(head);
|
app.use(head);
|
||||||
app.use(ToastPlugin);
|
app.use(ToastPlugin);
|
||||||
|
|
||||||
|
// Global Components
|
||||||
|
app.component('PillBar', PillBar)
|
||||||
|
app.component('TextBox', TextBox)
|
||||||
|
app.component('NumberBox', NumberBox)
|
||||||
|
app.component('MultilineTextBox', MultilineTextBox)
|
||||||
|
app.component('CheckBox', CheckBox)
|
||||||
|
app.component('ValidationForm', Form)
|
||||||
|
app.component('Field', Field)
|
||||||
|
app.component('ErrorMessage', ErrorMessage)
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { editTypes } from "../../../../definitions";
|
||||||
const props = $defineProps<{subsystem: string, entity: string, id: string}>();
|
const props = $defineProps<{subsystem: string, entity: string, id: string}>();
|
||||||
const { subsystem, entity, id } = $(props);
|
const { subsystem, entity, id } = $(props);
|
||||||
|
|
||||||
|
let data = $ref({} as {});
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +18,7 @@ async function update() {
|
||||||
<button @click="update">Update</button>
|
<button @click="update">Update</button>
|
||||||
<button @click="$router.go(-1)">Discard</button>
|
<button @click="$router.go(-1)">Discard</button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<NiceForm class="scroll cl-secondary" :title="editTypes[subsystem][entity].title" :fields="editTypes[subsystem][entity].fields"/>
|
<NiceForm class="scroll cl-secondary" :title="editTypes[subsystem][entity].title" :sections="editTypes[subsystem][entity].sections" v-model="data"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<PageHeader title="Error"/>
|
<PageHeader title="Error"/>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { editTypes } from "../../../../definitions";
|
||||||
const props = $defineProps<{subsystem: string, entity: string}>();
|
const props = $defineProps<{subsystem: string, entity: string}>();
|
||||||
const { subsystem, entity } = $(props);
|
const { subsystem, entity } = $(props);
|
||||||
|
|
||||||
|
let data = $ref({});
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +17,7 @@ async function create() {
|
||||||
<button @click="create">Create</button>
|
<button @click="create">Create</button>
|
||||||
<button @click="$router.go(-1)">Discard</button>
|
<button @click="$router.go(-1)">Discard</button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<NiceForm class="scroll cl-secondary" :title="editTypes[subsystem][entity].title" :fields="editTypes[subsystem][entity].fields"/>
|
<NiceForm class="scroll cl-secondary" :title="editTypes[subsystem][entity].title" :sections="editTypes[subsystem][entity].sections" v-model="data"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<PageHeader title="Error"/>
|
<PageHeader title="Error"/>
|
||||||
|
|
|
@ -1,21 +1,37 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { apiCall } from "../api";
|
import { apiCall } from "../api";
|
||||||
import PillBar from "../components/inputs/PillBar.vue";
|
import { Form as ValidationForm, Field, ErrorMessage } from 'vee-validate';
|
||||||
import TextBox from "../components/inputs/TextBox.vue";
|
|
||||||
import MultilineTextBox from "../components/inputs/MultilineTextBox.vue";
|
|
||||||
import CheckBox from "../components/inputs/CheckBox.vue";
|
|
||||||
|
|
||||||
async function doShit(){
|
async function doShit(){
|
||||||
apiCall("Firewall.GetForwardRules", {});
|
apiCall("Firewall.GetForwardRules", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
let fields = $ref([
|
let fields = $ref([
|
||||||
{key: "name", label: "Name", component: () => TextBox },
|
{key: "name", label: "Name", as: "TextBox", rules: validateEmail},
|
||||||
{key: "verdict", label: "Verdict", component: () => PillBar, props: {options: [{name: 'Accept'}, {name: 'Drop'}, {name: 'Continue'}]}},
|
{key: "verdict", label: "Verdict", as: "PillBar", props: {options: [{name: 'Accept'}, {name: 'Drop'}, {name: 'Continue'}]}},
|
||||||
{key: "counter", label: "Counter", component: () => CheckBox },
|
{key: "counter", label: "Counter", as: "CheckBox" },
|
||||||
{key: "comment", label: "Comment", component: () => MultilineTextBox },
|
{key: "comment", label: "Comment", as: "MultilineTextBox", enabled: (values: Record<string, any>) => (values["verdict"] == 2) as Boolean },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
function validateEmail(value: any) {
|
||||||
|
// if the field is empty
|
||||||
|
if (!value) {
|
||||||
|
return 'This field is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the field is not a valid email
|
||||||
|
const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
|
||||||
|
if (!regex.test(value)) {
|
||||||
|
return 'This field must be a valid email';
|
||||||
|
}
|
||||||
|
|
||||||
|
// All is good
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -23,10 +39,30 @@ let fields = $ref([
|
||||||
<PageHeader title="Dashboard">
|
<PageHeader title="Dashboard">
|
||||||
<button @click="doShit">Example Buttons</button>
|
<button @click="doShit">Example Buttons</button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<NiceForm class="scroll cl-secondary" title="Testasdfdsfsdf" :fields="fields"/>
|
<ValidationForm class="form" v-slot="{ values }">
|
||||||
|
<template v-for="(field, index) in fields" :key="index">
|
||||||
|
<template v-if="field.enabled ? field.enabled(values) : true">
|
||||||
|
<label :for="field.key" v-text="field.label"/>
|
||||||
|
<Field :name="field.key" :as="field.as" :rules="field.rules" v-bind="field.props"/>
|
||||||
|
<ErrorMessage :name="field.key" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
{{ values }}
|
||||||
|
</ValidationForm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
ValidationForm {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
ValidationForm > :is(button, .button, h2) {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
ValidationForm > :is(label) {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
Loading…
Add table
Reference in a new issue