update headings + move data format to tests

This commit is contained in:
Jordan Blasenhauer 2024-06-20 12:07:32 +02:00
parent 1013059e40
commit 5b8a60488b
37 changed files with 35256 additions and 33304 deletions

View file

@ -165,11 +165,11 @@ body {
}
.setting.field-container {
@apply px-3 md:px-4 pt-3 pb-4;
@apply px-3 md:px-4 mt-3 mb-4;
}
.table.field-container {
@apply p-0 -translate-x-1;
@apply p-0 m-0 -translate-x-1;
}
.input-header-container {
@ -513,11 +513,23 @@ body {
@apply pointer-events-none z-0 hover:brightness-95 fill-blue-500;
}
.default.popover-svg {
.lg.popover-svg {
@apply h-8 w-8;
}
.base.popover-svg {
@apply h-7 w-7;
}
.setting.popover-svg {
.md.popover-svg {
@apply h-6 w-6;
}
.sm.popover-svg {
@apply h-5 w-5;
}
.sm.popover-svg {
@apply h-5 w-5;
}
@ -1020,33 +1032,73 @@ body {
}
/* TITLE */
.title-container {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[800px] col-span-12 break-words font-bold mb-2 dark:text-white/90 transition duration-300 ease-in-out text-2xl;
@apply capitalize-first w-full max-w-[80%] sm:max-w-[800px] col-span-12 break-words font-bold dark:text-white/90 transition duration-300 ease-in-out text-[1.3rem] text-[#344767] tracking-normal;
}
.title-card {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[700px] col-span-12 break-words mb-2 font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl;
@apply capitalize-first w-full max-w-[80%] sm:max-w-[700px] col-span-12 break-words font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl text-[#344767] tracking-normal;
}
.title-content {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[700px] col-span-12 break-words font-bold dark:text-white/90 transition duration-300 ease-in-out text-lg text-[#344767];
}
.title-min {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[700px] col-span-12 break-words font-bold dark:text-white/90 transition duration-300 ease-in-out text-[0.95rem] text-[#344767];
}
.title-stat {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[600px] col-span-12 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-gray-400;
@apply capitalize-first w-full max-w-[80%] sm:max-w-[600px] col-span-12 font-sans text-sm font-semibold leading-normal uppercase dark:text-white/90 text-[#344767];
}
.no-subtitle.title-container {
@apply mb-2.5;
}
.no-subtitle.title-container,
.no-subtitle.title-card,
.no-subtitle.title-content,
.no-subtitle.title-min {
@apply mb-2;
}
.no-subtitle.title-stat{
@apply mb-0;
}
.is-subtitle.title-container,
.is-subtitle.title-card,
.is-subtitle.title-stat,
.is-subtitle.title-content,
.is-subtitle.title-min {
@apply mb-0;
}
/* SUBTITLE */
.subtitle-container {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[800px] leading-normal text-xl mb-0 lowercase;
@apply capitalize-first dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[800px] leading-normal text-[1.1rem] mb-0 lowercase;
}
.subtitle-card {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[700px] leading-normal text-base mb-0 lowercase;
@apply capitalize-first dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[700px] leading-normal text-base mb-0 lowercase;
}
.subtitle-content {
@apply capitalize-first dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[700px] leading-normal text-[0.975rem] mb-0 lowercase;
}
.subtitle-min {
@apply capitalize-first dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[700px] leading-normal text-[0.9rem] mb-0 lowercase;
}
.subtitle-stat {
@apply capitalize-first w-full max-w-[80%] sm:max-w-[600px] font-bold leading-normal text-sm mb-0 lowercase;
@apply capitalize-first dark:text-gray-300 col-span-12 break-words w-full max-w-[80%] sm:max-w-[600px] font-bold leading-normal text-sm mb-0 lowercase;
}
/* STAT COMPONENT */
@ -1067,20 +1119,55 @@ body {
@apply absolute top-0 right-0 min-w-12 dark:brightness-90 flex justify-center items-center w-12 h-12 text-center rounded-circle;
}
/* LAYOUT COMPONENT */
/* FORM */
.form-advanced-container {
@apply col-span-12 w-full;
}
.form-advanced-plugin-container {
@apply col-span-12 w-full;
}
.form-advanced-settings-container {
@apply grid grid-cols-12 w-full relative;
}
.form-easy-container {
@apply col-span-12 w-full;
}
.form-easy-step-container {
@apply col-span-12 w-full mt-3 mb-2;
}
.form-easy-step-settings-container {
@apply grid grid-cols-12 w-full relative;
}
.form-raw-container {
@apply col-span-12 w-full;
}
.form-raw-editor-container {
@apply col-span-12 w-full;
}
.form-setting-error {
@apply mt-2 font-bold error text-center text-red-500;
}
/* CARD */
.card {
@apply relative transition dark:brightness-110 shadow-md bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border transform duration-300 ease-in-out;
}
/* CARD INFO */
.card-info-text {
@apply col-span-12 mb-0 leading-normal text-sm text-gray-700 dark:text-gray-500 ml-2 mr-1.5;
}
/* CARD INSTANCE COMPONENT */
.card-instance-container {
@apply h-full overflow-hidden hover:translate-y-1 transition col-span-12 md:col-span-6 3xl:col-span-4 flex p-4 justify-between w-full shadow-md break-words bg-white dark:bg-slate-850 dark:brightness-110 dark:shadow-dark-xl rounded-2xl bg-clip-border;
}
@ -1172,12 +1259,6 @@ body {
@apply w-6.5 h-6.5;
}
/* SETTINGS */
.setting-error {
@apply font-bold error text-center mt-2 text-red-500;
}
/* FILE MANAGER */
.file-manager-breadcrumb {

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Templates from "@components/Form/Templates.vue";
/**
@ -62,6 +63,7 @@ const props = defineProps({
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Title v-if="widget.type === 'Title'" v-bind="widget.data" />
<Subtitle v-if="widget.type === 'Subtitle'" v-bind="widget.data" />
<Templates v-if="widget.type === 'Templates'" v-bind="widget.data" />
</template>
</Grid>

View file

@ -1,39 +0,0 @@
<script setup>
/**
@name Content/Stat.vue
@description This component is used with stats to display the stat value.
This can be used alone in case we don't need a complete stat widget.
In case you have a title, subtitle, stat and icon to display, you can directly use Stat widget.
@example
{
stat: "100",
statClass: "text-3xl"
}
@param {string} stat - The stat value. Can be a translation key or by default raw text.
@param {string} [statClass=""] - Additional class, useful when component is used directly on a grid system
@param {string} [tag="p"] - The tag of the stat. Can be p, span, div, h1, h2, h3, h4, h5, h6
*/
const props = defineProps({
stat: {
type: [String, Number],
required: true,
},
statClass: {
type: String,
required: false,
default: "",
},
tag: {
type: String,
required: false,
default: "p",
},
});
</script>
<template>
<component :is="props.tag" :class="['content-stat', props.statClass]">
{{ $t(props.stat, props.stat) }}
</component>
</template>

View file

@ -80,6 +80,38 @@ const data = reactive({
format: JSON.parse(JSON.stringify(props.template)),
});
const comboboxPlugin = {
id: `advanced-combobox-${uuidv4()}`,
name: `advanced-combobox-${uuidv4()}`,
disabled: false,
required: false,
onlyDown: true,
maxBtnChars: 24,
containerClass: "setting",
label: "dashboard_plugins",
popovers: [
{
text: "inp_combobox_advanced_desc",
iconName: "info",
iconColor: "info",
svgSize: "sm",
},
],
columns: { pc: 3, tablet: 12, mobile: 12 },
};
const buttonSave = {
id: `advanced-save-${uuidv4()}`,
text: "action_save",
color: "success",
size: "normal",
type: "button",
attrs: {
"data-submit-form": JSON.stringify(data.base),
},
containerClass: "flex justify-center",
};
const filters = [
{
filter: "settings",
@ -109,6 +141,7 @@ const filters = [
text: "inp_search_settings_desc",
iconName: "info",
iconColor: "info",
svgSize: "sm",
},
],
columns: { pc: 3, tablet: 4, mobile: 12 },
@ -129,11 +162,13 @@ const filters = [
onlyDown: true,
label: "inp_select_plugin_type",
containerClass: "setting",
maxBtnChars: 24,
popovers: [
{
text: "inp_select_plugin_type_desc",
iconName: "info",
iconColor: "info",
svgSize: "sm",
},
],
columns: { pc: 3, tablet: 4, mobile: 12 },
@ -154,11 +189,13 @@ const filters = [
onlyDown: true,
containerClass: "setting",
label: "inp_select_plugin_context",
maxBtnChars: 24,
popovers: [
{
text: "inp_select_plugin_context_desc",
iconName: "info",
iconColor: "info",
svgSize: "sm",
},
],
columns: { pc: 3, tablet: 4, mobile: 12 },
@ -209,36 +246,6 @@ function updateTemplate(e) {
useUpdateTempSettings(e, data.base);
}
const comboboxPlugin = {
id: `advanced-combobox-${uuidv4()}`,
name: `advanced-combobox-${uuidv4()}`,
disabled: false,
required: false,
onlyDown: true,
containerClass: "setting",
label: "dashboard_plugins",
popovers: [
{
text: "inp_combobox_advanced_desc",
iconName: "info",
iconColor: "info",
},
],
columns: { pc: 3, tablet: 4, mobile: 12 },
};
const buttonSave = {
id: `advanced-save-${uuidv4()}`,
text: "action_save",
color: "success",
size: "normal",
type: "button",
attrs: {
"data-submit-form": JSON.stringify(data.base),
},
containerClass: "flex justify-center",
};
onMounted(() => {
// Get first props.forms template name
data.currPlugin = getFirstPlugin(props.template);
@ -260,33 +267,33 @@ onUnmounted(() => {
data-advanced-form
:tag="'form'"
method="POST"
:containerClass="`col-span-12 w-full m-1 p-1`"
:containerClass="`form-advanced-container`"
:columns="props.columns"
>
<Title type="card" :title="'dashboard_advanced_mode'" />
<Subtitle type="card" :subtitle="'dashboard_advanced_mode_subtitle'" />
<Combobox
v-bind="comboboxPlugin"
:value="data.currPlugin"
:values="data.plugins"
@inp="data.currPlugin = $event"
/>
<Filter
v-if="filters.length"
@filter="(v) => filter(v)"
:data="data.base"
:filters="filters"
/>
>
<Combobox
v-bind="comboboxPlugin"
:value="data.currPlugin"
:values="data.plugins"
@inp="data.currPlugin = $event"
/></Filter>
<template v-for="plugin in data.format">
<Container
data-advanced-form-plugin
v-if="plugin.name === data.currPlugin"
class="col-span-12 w-full"
class="form-advanced-plugin-container"
>
<Title type="card" :title="plugin.name" />
<Subtitle type="card" :subtitle="plugin.description" />
<Title type="content" :title="plugin.name" />
<Subtitle type="content" :subtitle="plugin.description" />
<Container class="grid grid-cols-12 w-full relative">
<Container class="form-advanced-settings-container">
<template
v-for="(setting, name, index) in plugin.settings"
:key="index"
@ -302,7 +309,7 @@ onUnmounted(() => {
/>
<Text
v-if="data.isRegErr || data.isReqErr"
:textClass="'setting-error'"
:textClass="'form-setting-error'"
:text="
data.isReqErr
? $t('dashboard_advanced_required', {

View file

@ -138,7 +138,7 @@ onUnmounted(() => {
data-easy-form
:tag="'form'"
method="POST"
:containerClass="`col-span-12 w-full m-1 p-1`"
:containerClass="`form-easy-container`"
:columns="props.columns"
>
<Title type="card" :title="'dashboard_easy_mode'" />
@ -148,10 +148,10 @@ onUnmounted(() => {
<Container
data-easy-form-step
v-if="data.currStep === id"
class="col-span-12 w-full"
class="form-easy-step-container"
>
<Title
type="card"
type="content"
:title="
$t('dashboard_easy_mode_title', {
step: id + 1,
@ -160,9 +160,9 @@ onUnmounted(() => {
})
"
/>
<Subtitle type="card" :subtitle="step.subtitle" />
<Subtitle type="content" :subtitle="step.subtitle" />
<Container class="grid grid-cols-12 w-full relative">
<Container class="form-easy-step-settings-container">
<template
v-for="(setting, name, index) in step.settings"
:key="index"
@ -192,7 +192,7 @@ onUnmounted(() => {
</Flex>
<Text
v-if="data.isRegErr || data.isReqErr"
:textClass="'setting-error'"
:textClass="'form-setting-error'"
:text="
data.isReqErr
? $t('dashboard_easy_required', {

View file

@ -140,12 +140,13 @@ const buttonSave = {
data-raw-form
:tag="'form'"
method="POST"
:containerClass="`col-span-12 w-full m-1 p-1`"
:containerClass="'form-raw-container'"
:columns="props.columns"
>
<Title type="card" :title="'dashboard_raw_mode'" />
<Subtitle type="card" :subtitle="'dashboard_raw_mode_subtitle'" />
<Container class="col-span-12 w-full">
<Container class="form-raw-editor-container">
<Editor @inp="(v) => (data.inp = v)" v-bind="editorData" />
</Container>
<Button :disabled="data.isValid ? false : true" v-bind="buttonSave" />
@ -153,7 +154,7 @@ const buttonSave = {
<Text
v-if="!data.isValid"
:text="'dashboard_raw_invalid'"
:textClass="'setting-error'"
:textClass="'form-setting-error'"
/>
</Container>
</template>

View file

@ -1,8 +1,7 @@
<script setup>
import { reactive, defineProps, computed, onBeforeMount } from "vue";
import Container from "@components/Widget/Container.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Grid from "@components/Widget/Grid.vue";
import Select from "@components/Forms/Field/Select.vue";
import Combobox from "@components/Forms/Field/Combobox.vue";
import Advanced from "@components/Form/Advanced.vue";
@ -28,17 +27,6 @@ import { v4 as uuidv4 } from "uuid";
*/
const props = defineProps({
// id && value && method
title: {
type: String,
required: false,
default: "dashboard_templates_title_default",
},
subtitle: {
type: String,
required: false,
default: "dashboard_templates_subtitle_default",
},
templates: {
type: Object,
required: true,
@ -50,8 +38,10 @@ const comboboxTemplate = {
id: `combobox-template-${uuidv4()}`,
name: `combobox-template-${uuidv4()}`,
disabled: false,
maxBtnChars: 24,
label: "dashboard_templates",
columns: { pc: 4, tablet: 6, mobile: 12 },
columns: { pc: 3, tablet: 12, mobile: 12 },
containerClass: "setting",
};
const comboboxModes = {
@ -60,8 +50,10 @@ const comboboxModes = {
disabled: false,
required: false,
onlyDown: true,
maxBtnChars: 24,
label: "dashboard_modes",
columns: { pc: 4, tablet: 6, mobile: 12 },
columns: { pc: 3, tablet: 12, mobile: 12 },
containerClass: "setting",
};
const data = reactive({
@ -98,15 +90,10 @@ onBeforeMount(() => {
<template>
<Container
v-if="data.currModeName && data.currTemplateName"
:containerClass="`col-span-12 w-full m-1 p-1`"
:containerClass="`col-span-12 w-full`"
:columns="props.columns"
>
<Title type="container" :title="props.title" />
<Subtitle type="container" :subtitle="props.subtitle" />
<Container
v-if="data.modes.length > 1 || data.templates.length > 1"
:containerClass="`col-span-12 grid grid-cols-12`"
>
<Grid v-if="data.modes.length > 1 || data.templates.length > 1">
<Combobox
v-if="data.templates.length > 1"
v-bind="comboboxTemplate"
@ -121,7 +108,7 @@ onBeforeMount(() => {
:values="data.modes"
@inp="data.currModeName = $event"
/>
</Container>
</Grid>
<Advanced
v-if="data.currModeName === 'advanced'"
:template="props.templates[data.currModeName][data.currTemplateName]"

View file

@ -147,6 +147,7 @@ function filterData(filter, value) {
v-if="filters.base"
:containerClass="`grid grid-cols-12 col-span-12 w-full`"
>
<slot></slot>
<template v-for="filter in filters.base">
<Input
v-if="filter.type === 'keyword'"

View file

@ -25,7 +25,7 @@ const props = defineProps({
<template>
<div
data-grid
:class="[props.gridClass, 'col-span-12 grid grid-cols-12 w-full relative']"
:class="[props.gridClass, 'col-span-12 grid grid-cols-12 w-full relative']"
>
<slot></slot>
</div>

View file

@ -72,15 +72,14 @@ const gridClass = computed(() => {
const gridLayoutEl = ref();
onMounted(() => {
if (props.link) {
gridLayoutEl.value.setAttribute("href", props.link);
gridLayoutEl.value.setAttribute("rel", "noopener");
gridLayoutEl.value.setAttribute("tabindex", props.tabId);
}
if (!props.link) return;
gridLayoutEl.value.setAttribute("href", props.link);
gridLayoutEl.value.setAttribute("rel", "noopener");
gridLayoutEl.value.setAttribute("tabindex", props.tabId);
if (props.link && props.link.startsWith("http")) {
gridLayoutEl.value.setAttribute("target", "_blank");
}
if (!props.link.startsWith("http")) return;
gridLayoutEl.value.setAttribute("target", "_blank");
});
</script>

View file

@ -3,12 +3,12 @@ import { defineProps } from "vue";
import Container from "@components/Widget/Container.vue";
import Title from "@components/Widget/Title.vue";
import Status from "@components/Widget/Status.vue";
import ContentDetailList from "@components/Content/DetailList.vue";
import ListPairs from "@components/List/Pairs.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
/**
@name Widget/Instance.vue
@description This component is an instance widget.
This component is using the Container, TitleCard, IconStatus, ContentDetailList and ButtonGroup components.
This component is using the Container, TitleCard, IconStatus, ListPairs and ButtonGroup components.
@example
{
id: "instance-1",
@ -48,7 +48,7 @@ const props = defineProps({
required: true,
default: "",
},
details: {
pairs: {
type: Array,
required: true,
default: [],
@ -65,7 +65,7 @@ const props = defineProps({
<Container :columns="{ pc: 12, tablet: 12, mobile: 12 }">
<Status :id="props.title" :status="props.status" />
<Title type="card" :title="props.title" />
<ContentDetailList :details="props.details" />
<ListPairs :pairs="props.pairs" />
<ButtonGroup
:buttons="props.buttons"
:groupClass="'justify-end align-center'"

View file

@ -20,7 +20,7 @@ import Icons from "@components/Widget/Icons.vue";
@param {string} iconColor - Color of the icon between tailwind colors
@param {string} [tag="a"] - By default it is a anchor tag, but we can use other tag like div in case of popover on another anchor
@param {string} [popoverClass=""] - Additional class for the popover container
@param {string} [svgClass="default"] - Additional class for the svg icon. "default" or "setting" will change the icon size.
@param {string} [svgSize="base"] - Determine svg size between sm, md, base and lg.
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
*/
@ -53,10 +53,10 @@ const props = defineProps({
required: false,
default: "",
},
svgClass: {
svgSize: {
type: String,
required: false,
default: "default",
default: "base",
},
tabId: {
type: String,
@ -175,7 +175,7 @@ onMounted(() => {
:class="['popover-btn', props.popoverClass]"
>
<Icons
:iconClass="`popover-svg ${props.svgClass}`"
:iconClass="`popover-svg ${props.svgSize}`"
:iconName="props.iconName"
:iconColor="props.iconColor"
/>

View file

@ -1,5 +1,5 @@
<script setup>
import { defineProps, computed, onMounted } from "vue";
import { defineProps, computed, onMounted, reactive } from "vue";
import { useUUID } from "@utils/global.js";
/**

View file

@ -12,7 +12,7 @@ import { computed } from "vue";
tag: "h2"
}
@param {string} subtitle - Can be a translation key or by default raw text.
@param {string} type - The type of subtitle between "card", "container" or "stat"
@param {string} [type="card"] - The type of title between "container", "card", "content", "min" or "stat"
@param {string} [tag=""] - The tag of the subtitle. Can be h1, h2, h3, h4, h5, h6 or p. If empty, will be determine by the type of subtitle.
@param {string} [subtitleColor=""] - The color of the subtitle between error, success, warning, info or tailwind color
@param {string} [subtitleClass=""] - Additional class, useful when component is used directly on a grid system
@ -47,21 +47,22 @@ const props = defineProps({
const tag = computed(() => {
if (props.tag) return props.tag;
if (props.type === "container") return "p";
if (props.type === "card") return "p";
if (props.type === "stat") return "p";
return "p";
});
const baseClass = computed(() => {
if (props.type === "container") return "subtitle-container";
if (props.type === "card") return "subtitle-card";
if (props.type === "stat") return "subtitle-stat";
if (props.type === "min") return "subtitle-min";
if (props.type === "content") return "subtitle-content";
return "subtitle-card";
});
</script>
<template>
<component
data-subtitle
:is="tag"
v-if="props.subtitle"
:class="[props.subtitleClass, props.subtitleColor, baseClass]"

View file

@ -1,5 +1,5 @@
<script setup>
import { computed } from "vue";
import { computed, onMounted, reactive, ref } from "vue";
/**
@name Widget/Title.vue
@description This component is a general title wrapper.
@ -12,7 +12,7 @@ import { computed } from "vue";
tag: "h2"
}
@param {string} title - Can be a translation key or by default raw text.
@param {string} type - The type of title between "card", "container" or "stat"
@param {string} [type="card"] - The type of title between "container", "card", "content", "min" or "stat"
@param {string} [tag=""] - The tag of the title. Can be h1, h2, h3, h4, h5, h6 or p. If empty, will be determine by the type of title.
@param {string} [titleColor=""] - The color of the title between error, success, warning, info or tailwind color
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
@ -45,26 +45,48 @@ const props = defineProps({
},
});
const title = reactive({
isSubtitle: false,
});
const titleEl = ref(null);
const tag = computed(() => {
if (props.tag) return props.tag;
if (props.type === "container") return "h1";
if (props.type === "card") return "h2";
if (props.type === "stat") return "p";
return "p";
});
const baseClass = computed(() => {
if (props.type === "container") return "title-container";
if (props.type === "card") return "title-card";
if (props.type === "content") return "title-content";
if (props.type === "min") return "title-min";
if (props.type === "stat") return "title-stat";
return "title-card";
});
// Add or remove margin bottom
const isSubtitleClass = computed(() => {
return title.isSubtitle ? "is-subtitle" : "no-subtitle";
});
onMounted(() => {
// Check if next sibling is a subtitle
const nextSibling = titleEl.value.nextElementSibling;
title.isSubtitle =
!nextSibling || !nextSibling.hasAttribute("data-subtitle") ? false : true;
});
</script>
<template>
<component
ref="titleEl"
data-title
:is="tag"
v-if="props.title"
:class="[props.titleClass, props.titleColor, baseClass]"
:class="[props.titleClass, props.titleColor, isSubtitleClass, baseClass]"
>
{{ $t(props.title, props.title) }}
</component>

View file

@ -95,8 +95,6 @@
"dashboard_easy_mode_subtitle": "Easy mode will guide you using steps and additional information to help you configure your settings.",
"dashboard_easy_invalid": "{setting} in step {step} is invalid",
"dashboard_easy_required": "{setting} in step {step} is required",
"dashboard_templates_title_default": "Configuration templates",
"dashboard_templates_subtitle_default": "Manage your settings with available templates and using the mode that suits you best.",
"dashboard_table": "Table element",
"inp_input_valid": "input valid",
"inp_input_error_required": "input is required",
@ -123,6 +121,7 @@
"inp_editor_desc": "Editor input behaving like a code editor.",
"inp_input_clipboard_copied": "copied to clipboard",
"inp_input_clipboard_desc": "Copy to clipboard on click.",
"inp_templates_desc": "Choose a template. This will override de",
"icons_cross_desc": "Cross icon representing a close, delete, error or cancel state.",
"icons_check_desc": "Check icon representing a success, valid or active state.",
"icons_core_desc": "Core icon representing a core setting or plugin.",
@ -182,6 +181,8 @@
"instances_status": "status",
"instances_active": "active",
"instances_inactive": "inactive",
"global_config_title": "Global configuration",
"global_config_subtitle": "Manage your global settings.",
"jobs_title": "Jobs list",
"jobs_download_cache_file": "download cache files",
"jobs_search": "search jobs",

File diff suppressed because it is too large Load diff

View file

@ -58,6 +58,5 @@ onMounted(() => {
<template>
<DashboardLayout>
<BuilderInstances v-if="instances.builder" :builder="instances.builder" />
<div id="test-el"></div>
</DashboardLayout>
</template>

View file

@ -14,7 +14,7 @@
data-server-flash='[{"type" : "success", "title" : "title", "message" : "Success feedback"}, {"type" : "error", "title" : "title", "message" : "Error feedback"}, {"type" : "warning", "title" : "title", "message" : "Warning feedback"}, {"type" : "info", "title" : "title", "message" : "Info feedback"}]'>
</div>
<div class="hidden"
data-server-builder='[{"type":"card","containerColumns":{"pc":6,"tablet":6,"mobile":12},"widgets":[{"type":"Instance","data":{"details":[{"key":"instances_hostname","value":"bunkerweb"},{"key":"instances_type","value":"manual"},{"key":"instances_status","value":"instances_active"}],"status":"success","title":"bunkerweb","buttons":[{"attrs":{"data-submit-form": {"operation" : "reload", "INSTANCE_ID" : "bunkerweb"} },"text":"action_reload","color":"warning","size":"normal"},{"attrs":{"data-submit-form": { "operation" : "stop", "INSTANCE_ID" : "bunkerweb"} },"text":"action_stop","color":"error","size":"normal"}]}}]}]'>
data-server-builder='[{"type":"card","containerColumns":{"pc":6,"tablet":6,"mobile":12},"widgets":[{"type":"Instance","data":{"pairs":[{"key":"instances_hostname","value":"bunkerweb"},{"key":"instances_type","value":"manual"},{"key":"instances_status","value":"instances_active"}],"status":"success","title":"bunkerweb","buttons":[{"attrs":{"data-submit-form": {"operation" : "reload", "INSTANCE_ID" : "bunkerweb"} },"text":"action_reload","color":"warning","size":"normal"},{"attrs":{"data-submit-form": { "operation" : "stop", "INSTANCE_ID" : "bunkerweb"} },"text":"action_stop","color":"error","size":"normal"}]}}]}]'>
</div>
<div id="app"></div>
<script type="module" src="instances.js"></script>

View file

@ -196,6 +196,46 @@ export default {
"sm:fixed",
"md:fixed",
"lg:fixed",
"ml-2",
"sm:ml-2",
"md:ml-2",
"lg:ml-2",
"mr-2",
"sm:mr-2",
"md:mr-2",
"lg:mr-2",
"mt-2",
"sm:mt-2",
"md:mt-2",
"lg:mt-2",
"mb-2",
"sm:mb-2",
"md:mb-2",
"lg:mb-2",
"m-2",
"sm:m-2",
"md:m-2",
"lg:m-2",
"pl-2",
"sm:pl-2",
"md:pl-2",
"lg:pl-2",
"pr-2",
"sm:pr-2",
"md:pr-2",
"lg:pr-2",
"pt-2",
"sm:pt-2",
"md:pt-2",
"lg:pt-2",
"pb-2",
"sm:pb-2",
"md:pb-2",
"lg:pb-2",
"p-2",
"sm:p-2",
"md:p-2",
"lg:p-2",
],
important: true,
darkMode: "class",

View file

@ -1,15 +0,0 @@
from flask import Flask, render_template
app = Flask(__name__)
# I want to setup templates and static files
app = Flask(__name__, template_folder="templates", static_url_path="", static_folder="static")
@app.route("/", methods=['GET', 'POST'])
def render_index():
# redirect to test
return render_template("home.html", flask_data="Title from Flask !")
app.debug = True
app.run(host='0.0.0.0')

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

10077
vuejs/tests/globalconfig.json Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1
wtforms/.gitignore vendored
View file

@ -1 +0,0 @@
__pycache__

View file

@ -1 +0,0 @@
# test-ui

View file

@ -1,170 +0,0 @@
from wtforms import Form
from wtforms.fields import Field, StringField, BooleanField, SelectField, PasswordField, FormField
from wtforms.validators import Regexp
from wtforms.widgets import CheckboxInput
from re import search
class CheckboxSettingWidget(CheckboxInput):
def __init__(self, error_class='has_errors'):
super(CheckboxInput, self).__init__()
self.error_class = error_class
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
kwargs.setdefault('type', 'checkbox')
kwargs.setdefault('data-default-method', "mode" if kwargs['name'] in ('AUTOCONF_MODE', 'SWARM_MODE', 'KUBERNETES_MODE') else field.method if hasattr(field, 'method') else 'default')
kwargs.setdefault('value', field.global_config_value)
kwargs.setdefault('data-pattern', field.regex)
kwargs.setdefault('checked', "")
return f"""<div data-checkbox-handler="{kwargs['id']}">
class="relative mb-7 md:mb-0 z-10 ">
{self.input(field, **kwargs)} {self.label(field, {"class": "sr-only", "for": kwargs['name']})}
<input id="{kwargs['name']}"
name="{kwargs['name']}"
data-default-method="{% if inp_name in ['AUTOCONF_MODE', 'SWARM_MODE', 'KUBERNETES_MODE'] %}mode{% else %}{{ global_config_method }}{% endif %}"
data-default-value="{{ global_config[inp_name]['value'] }}"
{% if inp_name in ['AUTOCONF_MODE', 'SWARM_MODE', 'KUBERNETES_MODE'] or global_config_method != 'ui' and global_config_method != 'default' or is_read_only %} disabled {% endif %}
data-checked="{% if global_config_value == "yes" %}true{% else %}false{% endif %}"
{% if global_config_value == "yes" %}checked{% endif %}
id="checkbox-{kwargs['id']}"
class="checkbox"
type="checkbox"
data-pattern="{{ inp_regex|safe }}"
value="{{ global_config_value }}"
{% if is_multiple %} data-is-multiple {% endif %}
/>
<svg data-checkbox-handler="{kwargs['id']}"
class="pointer-events-none absolute fill-white dark:fill-gray-300 left-0 top-0 translate-x-1 translate-y-2 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path class="pointer-events-none" d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z">
</path>
</svg>
</div>
"""
class BWBooleanField(Field):
widget = CheckboxSetting()
false_values = (False, "false", "")
def __init__(self, label=None, validators=None, false_values=None, **kwargs):
super().__init__(label, validators, **kwargs)
if false_values is not None:
self.false_values = false_values
def pre_validate(self, form):
if isinstance(self.data, bool):
self.data = "no"
def process_data(self, value):
self.data = value
def process_formdata(self, valuelist):
if not valuelist or valuelist[0] in self.false_values:
self.data = False
else:
self.data = "yes"
def _value(self):
if self.raw_data:
return str(self.raw_data[0])
return "yes"
def number_from_setting_name(setting):
res = search(r"_([0-9]+)$", setting)
if res:
return res.group(1)
return "0"
def settings_to_form(settings):
class SettingsForm(Form):
pass
bw_multiple_forms = {}
for setting, data in settings.items():
field_type = None
field_data = dict(
label=data["label"],
validators=[Regexp(data["regex"])],
description=data["help"],
id=setting,
default=data["default"],
# widget=None,
render_kw={
"custom-attributes-1": "custom-value-1",
"custom-attributes-2": "custom-value-2",
},
name=setting,
# _form=None,
# _prefix='',
# _translations=None,
# _meta=None
)
if data["type"] == "text":
field_type = StringField
elif data["type"] == "check":
field_type = BWBooleanField
del field_data["default"]
if data["default"] == "yes":
field_data["default"] = "checked"
field_data["render_kw"]["checked"] = ""
field_data["false_values"] = ("no")
elif data["type"] == "select":
field_type = SelectField
field_data["choices"] = data["select"]
elif data["type"] == "password":
field_type = PasswordField
else:
print(f"unsupported type {data['type']}")
continue
if "multiple" not in data:
setattr(
SettingsForm,
setting,
field_type(
**field_data
)
)
else:
class BWMultipleForm(Form):
pass
multiple_key = f"{data['multiple']}-{number_from_setting_name(setting)}"
if multiple_key not in bw_multiple_forms:
bw_multiple_forms[multiple_key] = BWMultipleForm
setattr(
bw_multiple_forms[multiple_key],
setting,
field_type(
**field_data
)
)
for multiple, form in bw_multiple_forms.items():
setattr(
SettingsForm,
multiple,
FormField(form)
)
return SettingsForm
def compute_form(client_form, request_form, settings):
for key, value in request_form.items():
print(key)
real_key = key
res = search(r"([a-z\-]+\-[0-9]+\-).*_([0-9]+)$", key)
if res:
real_key = "_".join(key.replace(res.group(1), "").split("_")[:-1])
if real_key in settings and "multiple" in settings[real_key]:
setattr(
client_form,
key,
StringField(
validators=[Regexp(settings[real_key]["regex"])]
)
)
return client_form

View file

@ -1,114 +0,0 @@
{
"id": "limit",
"name": "Limit",
"description": "Limit maximum number of requests and connections.",
"version": "1.0",
"stream": "partial",
"settings": {
"USE_LIMIT_REQ": {
"context": "multisite",
"default": "yes",
"help": "Activate limit requests feature.",
"id": "use-limit-req",
"label": "Activate limit requests",
"regex": "^(yes|no)$",
"type": "check"
},
"LIMIT_REQ_URL": {
"context": "multisite",
"default": "/",
"help": "URL (PCRE regex) where the limit request will be applied or special value / for all requests.",
"id": "limit-req-url",
"label": "Limit request URL",
"regex": "^.+$",
"type": "text",
"multiple": "limit-req"
},
"LIMIT_REQ_RATE": {
"context": "multisite",
"default": "2r/s",
"help": "Rate to apply to the URL (s for second, m for minute, h for hour and d for day).",
"id": "limit-req-rate",
"label": "Limit request Rate",
"regex": "^\\d+r/[smhd]$",
"type": "text",
"multiple": "limit-req"
},
"LIMIT_REQ_URL_1": {
"context": "multisite",
"default": "/",
"help": "URL (PCRE regex) where the limit request will be applied or special value / for all requests.",
"id": "limit-req-url",
"label": "Limit request URL",
"regex": "^.+$",
"type": "text",
"multiple": "limit-req"
},
"LIMIT_REQ_RATE_1": {
"context": "multisite",
"default": "2r/s",
"help": "Rate to apply to the URL (s for second, m for minute, h for hour and d for day).",
"id": "limit-req-rate",
"label": "Limit request Rate",
"regex": "^\\d+r/[smhd]$",
"type": "text",
"multiple": "limit-req"
},
"LIMIT_REQ_URL_2": {
"context": "multisite",
"default": "/",
"help": "URL (PCRE regex) where the limit request will be applied or special value / for all requests.",
"id": "limit-req-url",
"label": "Limit request URL",
"regex": "^.+$",
"type": "text",
"multiple": "limit-req"
},
"LIMIT_REQ_RATE_2": {
"context": "multisite",
"default": "2r/s",
"help": "Rate to apply to the URL (s for second, m for minute, h for hour and d for day).",
"id": "limit-req-rate",
"label": "Limit request Rate",
"regex": "^\\d+r/[smhd]$",
"type": "text",
"multiple": "limit-req"
},
"USE_LIMIT_CONN": {
"context": "multisite",
"default": "yes",
"help": "Activate limit connections feature.",
"id": "use-limit-conn",
"label": "Activate limit connections",
"regex": "^(yes|no)$",
"type": "check"
},
"LIMIT_CONN_MAX_HTTP1": {
"context": "multisite",
"default": "10",
"help": "Maximum number of connections per IP when using HTTP/1.X protocol.",
"id": "limit-conn-max-http1",
"label": "Maximum number of HTTP/1.X connections",
"regex": "^\\d+$",
"type": "text"
},
"LIMIT_CONN_MAX_HTTP2": {
"context": "multisite",
"default": "100",
"help": "Maximum number of streams per IP when using HTTP/2 protocol.",
"id": "limit-conn-max-http2",
"label": "Maximum number of HTTP/2 streams",
"regex": "^\\d+$",
"type": "text"
},
"LIMIT_CONN_MAX_STREAM": {
"context": "multisite",
"default": "10",
"help": "Maximum number of connections per IP when using stream.",
"id": "limit-conn-max-stream",
"label": "Maximum number of stream connections",
"regex": "^\\d+$",
"type": "text"
}
}
}

View file

@ -1,25 +0,0 @@
from flask import Flask, render_template, request
from json import loads
from forms import settings_to_form, compute_form
app = Flask(__name__)
@app.route("/global", methods=['GET', 'POST'])
def global_settings():
# with open("settings.json") as f:
# settings = loads(f.read())
with open("limit.json") as f:
settings = loads(f.read())["settings"]
form = settings_to_form(settings)(request.form)
if request.method == "POST":
form = compute_form(form, request.form, settings)
if not form.validate():
print("error validate")
for field in form:
print(f"field {field.id} = {field.data}")
print(form.errors)
return render_template("global.html", form=form)
app.debug = True
app.run(host='0.0.0.0')

View file

@ -1,266 +0,0 @@
{
"id": "reverseproxy",
"name": "Reverse proxy",
"description": "Manage reverse proxy configurations.",
"version": "1.0",
"stream": "partial",
"settings": {
"USE_REVERSE_PROXY": {
"context": "multisite",
"default": "no",
"help": "Activate reverse proxy mode.",
"id": "use-reverse-proxy",
"label": "Use reverse proxy",
"regex": "^(yes|no)$",
"type": "check"
},
"REVERSE_PROXY_INTERCEPT_ERRORS": {
"context": "multisite",
"default": "yes",
"help": "Intercept and rewrite errors.",
"id": "reverse-proxy-intercept-errors",
"label": "Intercept errors",
"regex": "^(yes|no)$",
"type": "check"
},
"REVERSE_PROXY_HOST": {
"context": "multisite",
"default": "",
"help": "Full URL of the proxied resource (proxy_pass).",
"id": "reverse-proxy-host",
"label": "Reverse proxy host",
"regex": "^.*$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_URL": {
"context": "multisite",
"default": "",
"help": "Location URL that will be proxied.",
"id": "reverse-proxy-url",
"label": "Reverse proxy url",
"regex": "^.*$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_WS": {
"context": "multisite",
"default": "no",
"help": "Enable websocket on the proxied resource.",
"id": "reverse-proxy-ws",
"label": "Reverse proxy WS",
"regex": "^(yes|no)$",
"type": "check",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_HEADERS": {
"context": "multisite",
"default": "",
"help": "List of HTTP headers to send to proxied resource separated with semicolons (values for proxy_set_header directive).",
"id": "reverse-proxy-headers",
"label": "Reverse proxy headers",
"regex": "^(?![; ])(;? ?([\\w\\-]+)(?!.*\\2 ) [^;]+)*$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_HEADERS_CLIENT": {
"context": "multisite",
"default": "",
"help": "List of HTTP headers to send to client separated with semicolons (values for add_header directive).",
"id": "reverse-proxy-headers-client",
"label": "Reverse proxy headers-client",
"regex": "^(?![; ])(;? ?([\\w\\-]+)(?!.*\\2 ) [^;]+)*$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_BUFFERING": {
"context": "multisite",
"default": "yes",
"help": "Enable or disable buffering of responses from proxied resource.",
"id": "reverse-proxy-buffering",
"label": "Reverse proxy buffering",
"regex": "^(yes|no)$",
"type": "check",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_KEEPALIVE": {
"context": "multisite",
"default": "no",
"help": "Enable or disable keepalive connections with the proxied resource.",
"id": "reverse-proxy-keepalive",
"label": "Reverse proxy keepalive",
"regex": "^(yes|no)$",
"type": "check",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_AUTH_REQUEST": {
"context": "multisite",
"default": "",
"help": "Enable authentication using an external provider (value of auth_request directive).",
"id": "reverse-proxy-auth-request",
"label": "Reverse proxy auth request",
"regex": "^(\\/[\\w\\].~:\\/?#\\[@!$\\&'\\(\\)*+,;=\\-]*|off)?$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_AUTH_REQUEST_SIGNIN_URL": {
"context": "multisite",
"default": "",
"help": "Redirect clients to sign-in URL when using REVERSE_PROXY_AUTH_REQUEST (used when auth_request call returned 401).",
"id": "reverse-proxy-auth-request-signin-url",
"label": "Auth request signin URL",
"regex": "^(https?:\\/\\/[\\-\\w@:%.+~#=]+[\\-\\w\\(\\)!@:%+.~#?&\\/=$]*)?$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_AUTH_REQUEST_SET": {
"context": "multisite",
"default": "",
"help": "List of variables to set from the authentication provider, separated with semicolons (values of auth_request_set directives).",
"id": "reverse-proxy-auth-request-set",
"label": "Reverse proxy auth request set",
"regex": "^(?! ;)(;? ?(\\$[a-z_\\-]+)(?!.*\\2 ) [^;]+)*$",
"type": "text",
"multiple": "reverse-proxy"
},
"USE_PROXY_CACHE": {
"context": "multisite",
"default": "no",
"help": "Enable or disable caching of the proxied resources.",
"id": "use-proxy-cache",
"label": "Reverse proxy cache",
"regex": "^(yes|no)$",
"type": "check"
},
"PROXY_CACHE_PATH_LEVELS": {
"context": "global",
"default": "1:2",
"help": "Hierarchy levels of the cache.",
"id": "proxy-cache-path-levels",
"label": "Hierarchy levels",
"regex": "^(:?[12]){1,3}$",
"type": "text"
},
"PROXY_CACHE_PATH_ZONE_SIZE": {
"context": "global",
"default": "10m",
"help": "Maximum size of cached metadata when caching proxied resources.",
"id": "proxy-cache-path-zone-size",
"label": "Reverse proxy cache zone size",
"regex": "^\\d+[kKmMgG]?$",
"type": "text"
},
"PROXY_CACHE_PATH_PARAMS": {
"context": "global",
"default": "max_size=100m",
"help": "Additional parameters to add to the proxy_cache directive.",
"id": "proxy-cache-path-params",
"label": "Reverse proxy cache params",
"regex": "^.*$",
"type": "text"
},
"PROXY_CACHE_METHODS": {
"context": "multisite",
"default": "GET HEAD",
"help": "HTTP methods that should trigger a cache operation.",
"id": "proxy-cache-methods",
"label": "Reverse proxy cache methods",
"regex": "^(?! )( ?(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH)(?!.*\\2))+$",
"type": "text"
},
"PROXY_CACHE_MIN_USES": {
"context": "multisite",
"default": "2",
"help": "The minimum number of requests before a response is cached.",
"id": "proxy-cache-min-uses",
"label": "Reverse proxy cache minimum uses",
"regex": "^[1-9]\\d*$",
"type": "text"
},
"PROXY_CACHE_KEY": {
"context": "multisite",
"default": "$scheme$host$request_uri",
"help": "The key used to uniquely identify a cached response.",
"id": "proxy-cache-key",
"label": "Reverse proxy cache key",
"regex": "^(?! )( ?(\\$[a-z_]+)(?!.*\\2))+$",
"type": "text"
},
"PROXY_CACHE_VALID": {
"context": "multisite",
"default": "200=24h 301=1h 302=24h",
"help": "Define the caching time depending on the HTTP status code (list of status=time), separated with spaces.",
"id": "proxy-cache-valid",
"label": "Reverse proxy cache valid",
"regex": "^(?! )( ?([1-5]\\d{2})(?!.*\\2=)=\\d+(ms?|[shdwMy]))*$",
"type": "text"
},
"PROXY_NO_CACHE": {
"context": "multisite",
"default": "$http_pragma $http_authorization",
"help": "Conditions to disable caching of responses.",
"id": "proxy-no-cache",
"label": "Reverse proxy no cache",
"regex": "^.*$",
"type": "text"
},
"PROXY_CACHE_BYPASS": {
"context": "multisite",
"default": "0",
"help": "Conditions to bypass caching of responses.",
"id": "proxy-cache-bypass",
"label": "Reverse proxy bypass",
"regex": "^.*$",
"type": "text"
},
"REVERSE_PROXY_CONNECT_TIMEOUT": {
"context": "multisite",
"default": "60s",
"help": "Timeout when connecting to the proxied resource.",
"id": "reverse-proxy-connect-timeout",
"label": "Reverse proxy connect timeout",
"regex": "^\\d+(ms?|[shdwMy])$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_READ_TIMEOUT": {
"context": "multisite",
"default": "60s",
"help": "Timeout when reading from the proxied resource.",
"id": "reverse-proxy-read-timeout",
"label": "Reverse proxy read timeout",
"regex": "^\\d+(ms?|[shdwMy])$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_SEND_TIMEOUT": {
"context": "multisite",
"default": "60s",
"help": "Timeout when sending to the proxied resource.",
"id": "reverse-proxy-send-timeout",
"label": "Reverse proxy send timeout",
"regex": "^\\d+(ms?|[shdwMy])$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_INCLUDES": {
"context": "multisite",
"default": "",
"help": "Additional configuration to include in the location block, separated with spaces.",
"id": "reverse-proxy-includes",
"label": "Reverse proxy includes",
"regex": "^(?! )( ?(\\w+)(?!.*\\b\\2\\b))*$",
"type": "text",
"multiple": "reverse-proxy"
},
"REVERSE_PROXY_CUSTOM_HOST": {
"context": "multisite",
"default": "",
"help": "Override Host header sent to upstream server.",
"id": "reverse-proxy-custom-host",
"label": "Reverse proxy custom host",
"regex": "^.*$",
"type": "text"
}
}
}

View file

@ -1,329 +0,0 @@
{
"IS_LOADING": {
"context": "global",
"default": "no",
"help": "Internal use : set to yes when BW is loading.",
"id": "internal-use-loading",
"label": "internal use loading",
"regex": "^(yes|no)$",
"type": "check"
},
"NGINX_PREFIX": {
"context": "global",
"default": "/etc/nginx/",
"help": "Where nginx will search for configurations.",
"id": "nginx-prefix",
"label": "nginx prefix",
"regex": "^(\\/[\\-\\w.\\s]+)*\\/$",
"type": "text"
},
"HTTP_PORT": {
"context": "global",
"default": "8080",
"help": "HTTP port number which bunkerweb binds to.",
"id": "http-port",
"label": "HTTP port",
"regex": "^\\d+$",
"type": "text"
},
"HTTPS_PORT": {
"context": "global",
"default": "8443",
"help": "HTTPS port number which bunkerweb binds to.",
"id": "https-port",
"label": "HTTPS port",
"regex": "^\\d+$",
"type": "text"
},
"MULTISITE": {
"context": "global",
"default": "no",
"help": "Multi site activation.",
"id": "multisite",
"label": "Multisite",
"regex": "^(yes|no)$",
"type": "check"
},
"SERVER_NAME": {
"context": "multisite",
"default": "www.example.com",
"help": "List of the virtual hosts served by bunkerweb.",
"id": "server-name",
"label": "Server name",
"regex": "^((\\S{1,255})(?!.*\\s\\2(\\s|$)))?(\\s(\\S{1,255})(?!.*\\s\\5(\\s|$)))*$",
"type": "text"
},
"WORKER_PROCESSES": {
"context": "global",
"default": "auto",
"help": "Number of worker processes.",
"id": "worker-processes",
"label": "Worker processes",
"regex": "^(auto|\\d+)$",
"type": "text"
},
"WORKER_RLIMIT_NOFILE": {
"context": "global",
"default": "2048",
"help": "Maximum number of open files for worker processes.",
"id": "worker-rlimit-nofile",
"label": "Open files per worker",
"regex": "^\\d+$",
"type": "text"
},
"WORKER_CONNECTIONS": {
"context": "global",
"default": "1024",
"help": "Maximum number of connections per worker.",
"id": "worker-connections",
"label": "Connections per worker",
"regex": "^\\d+$",
"type": "text"
},
"LOG_FORMAT": {
"context": "global",
"default": "$host $remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\"",
"help": "The format to use for access logs.",
"id": "log-format",
"label": "Log format",
"regex": "^.*$",
"type": "text"
},
"LOG_LEVEL": {
"context": "global",
"default": "notice",
"help": "The level to use for error logs.",
"id": "log-level",
"label": "Log level",
"regex": "^(debug|info|notice|warn|error|crit|alert|emerg)$",
"type": "select",
"select": [
"debug",
"info",
"notice",
"warn",
"error",
"crit",
"alert",
"emerg"
]
},
"DNS_RESOLVERS": {
"context": "global",
"default": "127.0.0.11",
"help": "DNS addresses of resolvers to use.",
"id": "dns-resolvers",
"label": "DNS resolvers",
"regex": "^(?! )(( *[^ ]+)(?!.*\\2))*$",
"type": "text"
},
"DATASTORE_MEMORY_SIZE": {
"context": "global",
"default": "64m",
"help": "Size of the internal datastore.",
"id": "datastore-memory-size",
"label": "Datastore memory size",
"regex": "^\\d+[kKmMgG]?$",
"type": "text"
},
"CACHESTORE_MEMORY_SIZE": {
"context": "global",
"default": "64m",
"help": "Size of the internal cachestore.",
"id": "cachestore-memory-size",
"label": "Cachestore memory size",
"regex": "^\\d+[kKmMgG]?$",
"type": "text"
},
"CACHESTORE_IPC_MEMORY_SIZE": {
"context": "global",
"default": "16m",
"help": "Size of the internal cachestore (ipc).",
"id": "cachestore-ipc-memory-size",
"label": "Cachestore ipc memory size",
"regex": "^\\d+[kKmMgG]?$",
"type": "text"
},
"CACHESTORE_MISS_MEMORY_SIZE": {
"context": "global",
"default": "16m",
"help": "Size of the internal cachestore (miss).",
"id": "cachestore-miss-memory-size",
"label": "Cachestore miss memory size",
"regex": "^\\d+[kKmMgG]?$",
"type": "text"
},
"CACHESTORE_LOCKS_MEMORY_SIZE": {
"context": "global",
"default": "16m",
"help": "Size of the internal cachestore (locks).",
"id": "cachestore-locks-memory-size",
"label": "Cachestore locks memory size",
"regex": "^\\d+[kKmMgG]?$",
"type": "text"
},
"USE_API": {
"context": "global",
"default": "yes",
"help": "Activate the API to control BunkerWeb.",
"id": "use-api",
"label": "Activate API",
"regex": "^(yes|no)$",
"type": "check"
},
"API_HTTP_PORT": {
"context": "global",
"default": "5000",
"help": "Listen port number for the API.",
"id": "api-http-listen",
"label": "API port number",
"regex": "^\\d+$",
"type": "text"
},
"API_LISTEN_IP": {
"context": "global",
"default": "0.0.0.0",
"help": "Listen IP address for the API.",
"id": "api-ip-listen",
"label": "API listen IP",
"regex": "^.*$",
"type": "text"
},
"API_SERVER_NAME": {
"context": "global",
"default": "bwapi",
"help": "Server name (virtual host) for the API.",
"id": "api-server-name",
"label": "API server name",
"regex": "^[^ ]{1,255}$",
"type": "text"
},
"API_WHITELIST_IP": {
"context": "global",
"default": "127.0.0.0/8",
"help": "List of IP/network allowed to contact the API.",
"id": "api-whitelist-ip",
"label": "API whitelist IP",
"regex": "^(?! )( *(((\\b25[0-5]|\\b2[0-4]\\d|\\b[01]?\\d\\d?)(\\.(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)){3})(\\/([1-2][0-9]?|3[0-2]?|[04-9]))?|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]Z{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?\\d)?\\d)\\.){3}(25[0-5]|(2[0-4]|1?\\d)?\\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?\\d)?\\d)\\.){3}(25[0-5]|(2[0-4]|1?\\d)?\\d))(\\/(12[0-8]|1[01][0-9]|[0-9][0-9]?))?)(?!.*\\D\\2([^\\d\\/]|$)) *)*$",
"type": "text"
},
"AUTOCONF_MODE": {
"context": "global",
"default": "no",
"help": "Enable Autoconf Docker integration.",
"id": "autoconf-mode",
"label": "Autoconf mode",
"regex": "^(yes|no)$",
"type": "check"
},
"SWARM_MODE": {
"context": "global",
"default": "no",
"help": "Enable Docker Swarm integration.",
"id": "swarm-mode",
"label": "Swarm mode",
"regex": "^(yes|no)$",
"type": "check"
},
"KUBERNETES_MODE": {
"context": "global",
"default": "no",
"help": "Enable Kubernetes integration.",
"id": "kubernetes-mode",
"label": "Kubernetes mode",
"regex": "^(yes|no)$",
"type": "check"
},
"SERVER_TYPE": {
"context": "multisite",
"default": "http",
"help": "Server type : http or stream.",
"id": "server-type",
"label": "Server type",
"regex": "^(http|stream)$",
"type": "select",
"select": ["http", "stream"]
},
"LISTEN_STREAM": {
"context": "multisite",
"default": "yes",
"help": "Enable listening for non-ssl (passthrough).",
"id": "listen-stream",
"label": "Listen stream",
"regex": "^(yes|no)$",
"type": "check"
},
"LISTEN_STREAM_PORT": {
"context": "multisite",
"default": "1337",
"help": "Listening port for non-ssl (passthrough).",
"id": "listen-stream-port",
"label": "Listen stream port",
"regex": "^[0-9]+$",
"type": "text"
},
"LISTEN_STREAM_PORT_SSL": {
"context": "multisite",
"default": "4242",
"help": "Listening port for ssl (passthrough).",
"id": "listen-stream-port-ssl",
"label": "Listen stream port ssl",
"regex": "^[0-9]+$",
"type": "text"
},
"USE_UDP": {
"context": "multisite",
"default": "no",
"help": "UDP listen instead of TCP (stream).",
"id": "use-udp",
"label": "Listen UDP",
"regex": "^(yes|no)$",
"type": "check"
},
"USE_IPV6": {
"context": "global",
"default": "no",
"help": "Enable IPv6 connectivity.",
"id": "use-ipv6",
"label": "Use IPv6",
"regex": "^(yes|no)$",
"type": "check"
},
"IS_DRAFT": {
"context": "multisite",
"default": "no",
"help": "Internal use : set to yes when the service is in draft mode.",
"id": "internal-use-draft",
"label": "internal use draft",
"regex": "^(yes|no)$",
"type": "check"
},
"TIMERS_LOG_LEVEL": {
"context": "global",
"default": "debug",
"help": "Log level for timers.",
"id": "timers-log-level",
"label": "Timers log level",
"regex": "^(debug|info|notice|warn|err|crit|alert|emerg)$",
"type": "select",
"select": [
"debug",
"info",
"notice",
"warn",
"err",
"crit",
"alert",
"emerg"
]
},
"OVERRIDE_INSTANCES": {
"context": "global",
"default": "",
"help": "List of BunkerWeb instances separated with spaces (format : fqdn-or-ip:5000 fqdn-or-ip:5000)",
"id": "override-instances",
"label": "Override instances",
"regex": "^.*$",
"type": "text"
}
}

View file

@ -1,32 +0,0 @@
<form method="POST" action="/global" id="global-form">
{% for field in form %}
<div>Label is {{ field.label }}</div>
<div>Description is {{ field.description }}</div>
<div>Name is {{ field.name }}</div>
<div>Value is {{ field.default }}</div>
<div>Raw HTML is : {{ field }}</div>
<hr>
{% endfor %}
<input type="submit" value="send">
</form>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.querySelector('#global-form').addEventListener("submit", (event) => {
console.log("ok");
event.preventDefault();
document.querySelectorAll('#global-form input[type=checkbox]').forEach( (checkbox) => {
console.log(checkbox.name);
if (!checkbox.checked) {
var input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", checkbox.name);
input.setAttribute("value", "no");
checkbox.after(input);
}
});
event.currentTarget.submit();
});
});
</script>