update vue js POC

* add widget builder JSON example
* start some components example
This commit is contained in:
Jordan Blasenhauer 2024-05-16 17:29:35 +02:00
parent 6396e4779e
commit d017514ccd
38 changed files with 2944 additions and 44 deletions

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "test-ui",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -39,7 +39,7 @@ function buildVite(dir) {
return isErr;
}
// CLIENT : Change dir structure
// Change dir structure for flask app
function updateClientDir() {
let isErr = false;
const srcDir = resolve(`./${clientBuildDir}/src/pages`);
@ -58,6 +58,11 @@ function updateClientDir() {
// And move from static to templates
const templateDir = resolve(`./${clientBuildDir}/templates`);
// Create template dir if not exist
if (!fs.existsSync(resolve("./templates"))) {
fs.mkdirSync(resolve("./templates"));
}
fs.readdir(templateDir, (err, subdirs) => {
subdirs.forEach((subdir) => {
// Get absolute path of current subdir
@ -67,9 +72,11 @@ function updateClientDir() {
// Copy file to move it from /template/page to /template
fs.copyFileSync(
`${currPath}/${subdir}.html`,
resolve(`./static/templates/${subdir}.html`),
resolve(`./templates/${subdir}.html`),
);
// Delete useless dir
fs.rmSync(currPath, { recursive: true, force: true });
fs.rmSync(`./${clientBuildDir}/templates/`, { recursive: true, force: true });
});
});
} catch (err) {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
<script setup>
</script>
<template>
<div class="m-2 p-2">
<slot></slot>
</div>
</template>

View file

@ -0,0 +1,145 @@
<script setup>
import { reactive, defineProps, onMounted, ref } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Base from "@components/Forms/Field/Base.vue";
import Header from "@components/Forms/Header/Field.vue";
/* PROPS ARGUMENTS
*
*
id: string,
value: string,
disabled: boolean,
required: boolean,
label: string,
name: string,
version: string,
hideLabel: boolean,
required: boolean,
headerClass: string,
inpClass: string,
tabId: string || number,
*
*
*/
const props = defineProps({
// id && value && method
id: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
headerClass: {
type: String,
required: false,
},
inpClass: {
type: String,
required: false,
},
tabId: {
type: [String, Number],
required: false,
},
});
const checkboxEl = ref(null);
const checkbox = reactive({
value: props.value,
isValid: false,
});
const emits = defineEmits(["inp"]);
function updateValue() {
checkbox.value = checkbox.value === "yes" ? "no" : "yes";
checkbox.isValid = checkboxEl.value.checkValidity();
return checkbox.value;
}
onMounted(() => {
checkbox.isValid = checkboxEl.value.checkValidity();
});
</script>
<template>
<Base>
<Header :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<div class="relative z-10 flex flex-col items-start">
<input
ref="checkboxEl"
:tabindex="props.tabId || contentIndex"
@keyup.enter="$emit('inp', updateValue())"
@click="$emit('inp', updateValue())"
:id="props.id"
:name="props.name"
:disabled="props.disabled || false"
:checked="checkbox.value === 'yes' ? true : false"
:class="[
'checkbox',
checkbox.value === 'yes' ? 'check' : '',
checkbox.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
type="checkbox"
:value="checkbox.value"
:required="props.required || false"
/>
<svg
role="img"
aria-hidden="true"
v-show="checkbox.value === 'yes'"
class="checkbox-svg"
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>
<p
:aria-hidden="checkbox.isValid ? 'true' : 'false'"
role="alert"
:class="[checkbox.isValid ? 'hidden' : '']"
class="input-error-msg"
>
{{
checkbox.isValid
? $t("inp_input_valid")
: $t("inp_input_error_required")
}}
</p>
</div>
</Base>
</template>

View file

@ -0,0 +1,259 @@
<script setup>
import { reactive, ref, defineEmits, onMounted, defineProps } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Base from "@components/Forms/Field/Base.vue";
import Header from "@components/Forms/Header/Field.vue";
/* PROPS ARGUMENTS
*
*
id: string,
name: string,
type: string<"text"|"email"|"password"|"number"|"tel"|"url">,
disabled: boolean,
value: string,
placeholder: string,
pattern: string,
clipboard: boolean,
readonly: boolean,
label: string,
name: string,
version: string,
hideLabel: boolean,
required: boolean,
headerClass: string,
inpClass: string,
tabId: string || number,
*
*
*/
const props = defineProps({
// id && value && method
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
value: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
},
pattern: {
type: String,
required: false,
},
clipboard: {
type: Boolean,
required: false,
},
readonly: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
version: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
headerClass: {
type: String,
required: false,
},
inpClass: {
type: String,
required: false,
},
tabId: {
type: [String, Number],
required: true,
},
});
const inputEl = ref(null);
const inp = reactive({
value: props.value,
showInp: false,
isClipAllow: false,
isValid: false,
});
const emits = defineEmits(["inp"]);
function copyClipboard() {
if (!inp.clipboard || !inp.isClipAllow) return;
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
if (result.state === "granted" || result.state === "prompt") {
/* write to the clipboard now */
inputEl.select();
inputEl.setSelectionRange(0, 99999); // For mobile devices
// Copy the text inside the text field
return navigator.clipboard.writeText(inputEl.value);
}
});
}
onMounted(() => {
inp.isValid = inputEl.value.checkValidity();
// Clipboard not allowed on http
if (!window.location.href.startsWith("https://")) return;
// Check clipboard permission
navigator.permissions.query({ name: "clipboard-write" }).then((result) => {
if (result.state === "granted" || result.state === "prompt") {
inp.isClipAllow = true;
return;
}
});
});
</script>
<template>
<Base>
<Header :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<div class="relative flex flex-col items-start">
<input
:tabindex="props.tabId || contentIndex"
ref="inputEl"
v-model="inp.value"
@input="
() => {
inp.isValid = inputEl.checkValidity();
$emit('inp', inp.value);
}
"
:id="props.id"
:class="[
'input-regular',
inp.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
:required="props.required || false"
:readonly="props.readonly || false"
:disabled="props.disabled || false"
:placeholder="props.placeholder || ''"
:pattern="props.pattern || '(?s).*'"
:name="props.name"
:value="inp.value"
:type="
props.type === 'password'
? inp.showInp
? 'text'
: 'password'
: props.type
"
/>
<div
v-if="props.clipboard && inp.isClipAllow"
:class="[props.type === 'password' ? 'pw-input' : 'no-pw-input']"
class="input-clipboard-container"
>
<button
:tabindex="contentIndex"
@click="copyClipboard()"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-clipboard-button"
:aria-describedby="`${props.id}-clipboard-text`"
>
<span :id="`${props.id}-clipboard-text`" class="sr-only"
>{{ $t("inp_input_clipboard_desc") }}
</span>
<svg
role="img"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="input-clipboard-svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
/>
</svg>
</button>
</div>
<div v-if="props.type === 'password'" class="input-pw-container">
<button
:tabindex="contentIndex"
:aria-description="$t('inp_input_password_desc')"
:aria-controls="props.id"
@click="inp.showInp = inp.showInp ? false : true"
:class="[props.disabled ? 'disabled' : 'enabled']"
class="input-pw-button"
>
<svg
role="img"
aria-hidden="true"
v-if="!inp.showInp"
class="input-pw-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
>
<path
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
/>
</svg>
<svg
role="img"
aria-hidden="true"
v-if="inp.showInp"
class="input-pw-svg scale-110"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
>
<path
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
/>
</svg>
</button>
</div>
<p
:aria-hidden="inp.isValid ? 'true' : 'false'"
role="alert"
:class="[inp.isValid ? 'hidden' : '']"
class="input-error-msg"
>
{{
inp.isValid
? $t("inp_input_valid")
: !inp.value
? $t("inp_input_error_required")
: $t("inp_input_error_format")
}}
</p>
</div>
</Base>
</template>

View file

@ -0,0 +1,237 @@
<script setup>
import { ref, reactive, watch, onMounted, defineEmits, defineProps } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Base from "@components/Forms/Field/Base.vue";
import Header from "@components/Forms/Header/Field.vue";
/* PROPS ARGUMENTS
*
*
id: string,
value: string,
values: array,
disabled: boolean,
required: boolean,
label: string,
name: string,
version: string,
hideLabel: boolean,
inpClass: string,
headerClass: string,
tabId: string || number,
*
*
*/
const props = defineProps({
// id && value && method
id: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
values: {
type: Array,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
required: {
type: Boolean,
required: false,
},
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
headerClass: {
type: String,
required: false,
},
inpClass: {
type: String,
required: false,
},
tabId: {
type: [String, Number],
required: false,
},
});
// When mounted or when props changed, we want select to display new props values
// When component value change itself, we want to switch to select.value
// To avoid component to send and stick to props values (bad behavior)
// Trick is to use select.value || props.value on template
watch(props, (newProp, oldProp) => {
if (newProp.value !== select.value) {
select.value = "";
}
});
const select = reactive({
isOpen: false,
// On mounted value is null to display props value
// Then on new select we will switch to select.value
// If we use select.value : props.value
// Component will not re-render after props.value change
value: "",
});
const selectBtn = ref();
const selectWidth = ref("");
// EVENTS
function toggleSelect() {
select.isOpen = select.isOpen ? false : true;
}
function closeSelect() {
select.isOpen = false;
}
function changeValue(newValue) {
// Allow on template to switch from prop value to component own value
// Then send the new value to parent
select.value = newValue;
closeSelect();
return newValue;
}
// Close select dropdown when clicked outside element
watch(select, () => {
if (select.isOpen) {
document.querySelector("body").addEventListener("click", closeOutside);
} else {
document.querySelector("body").removeEventListener("click", closeOutside);
}
});
// Close select when clicked outside logic
function closeOutside(e) {
try {
if (e.target !== selectBtn.value) {
select.isOpen = false;
}
} catch (err) {
select.isOpen = false;
}
}
onMounted(() => {
selectWidth.value = `${selectBtn.value.clientWidth}px`;
window.addEventListener("resize", () => {
try {
selectWidth.value = `${selectBtn.value.clientWidth}px`;
} catch (err) {}
});
});
const emits = defineEmits(["inp"]);
</script>
<template>
<Base>
<Header :name="props.name" :label="props.label" :hideLabel="props.hideLabel" :headerClass="props.headerClass" />
<select :name="props.name" class="hidden">
<option
v-for="(value, id) in props.values"
:key="id"
:value="value"
@click="$emit('inp', changeValue(value))"
:selected="select.value && select.value === value || !select.value && value === props.value ? true : false"
>
{{ value }}
</option>
</select>
<!-- end default select -->
<!--custom-->
<div class="relative">
<button
:name="`${props.name}-custom`"
:tabindex="props.tabId || contentIndex"
ref="selectBtn"
:aria-controls="`${props.id}-custom`"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_button_desc')"
data-select-dropdown
:disabled="props.disabled || false"
@click="toggleSelect()"
:class="['select-btn', props.inpClass]"
>
<span
v-if="props.required"
class="font-bold text-red-500 absolute right-[5px] top-[-20px]"
>*
</span>
<span :id="`${props.id}-text`" class="select-btn-name">
{{ select.value || props.value }}
</span>
<!-- chevron -->
<svg
role="img"
aria-hidden="true"
:class="[select.isOpen ? '-rotate-180' : '']"
class="select-btn-svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
/>
</svg>
<!-- end chevron -->
</button>
<!-- dropdown-->
<div
role="radiogroup"
:style="{ width: selectWidth }"
:id="`${props.id}-custom`"
:class="[select.isOpen ? 'flex' : 'hidden']"
class="select-dropdown-container"
:aria-description="$t('inp_select_dropdown_desc')"
>
<button
:tabindex="contentIndex"
v-for="(value, id) in props.values"
role="radio"
@click="$emit('inp', changeValue(value))"
:class="[
id === 0 ? 'first' : '',
id === props.values.length - 1 ? 'last' : '',
value === select.value && select.value === value || !select.value && value === props.value ? 'active' : '',
'select-dropdown-btn',
]"
:aria-controls="`${props.id}-text`"
:aria-checked="select.value && select.value === value || !select.value && value === props.value ? 'true' : 'false'"
>
{{ value }}
</button>
</div>
<!-- end dropdown-->
</div>
<!-- end custom-->
</Base>
</template>

View file

@ -0,0 +1,56 @@
<script setup>
import { defineProps } from "vue";
/* PROPS ARGUMENTS
*
*
label: string,
name: string,
version: string,
hideLabel: boolean,
required: boolean,
headerClass: string,
*
*
*/
const props = defineProps({
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
version: {
type: String,
required: false,
},
hideLabel: {
type: Boolean,
required: false,
},
headerClass: {
type: String,
required: false,
},
});
</script>
<template>
<div :class="[props.hideLabel ? 'hidden' : '', props.headerClass]">
<label
:class="[props.label ? '' : 'sr-only']"
:for="props.name"
class="relative lowercase capitalize-first my-1 transition duration-300 ease-in-out text-sm sm:text-md font-bold m-0 dark:text-gray-300"
>
{{ props.label ? props.label : props.name }} <span v-if="props.version">{{ props.version }}</span>
</label>
<span
v-if="props.required"
class="font-bold text-red-500 absolute right-[5px] top-[-20px]"
>*
</span>
</div>
</template>

View file

@ -1,15 +0,0 @@
<script setup>
const props = defineProps({
// id && value && method
title : {
type: String,
required: true,
},
});
</script>
<template>
<div class="flex flex-col items-center justify-center h-full">
<h1 class="text-4xl font-bold">{{ title }}</h1>
</div>
</template>

View file

@ -0,0 +1,45 @@
<script setup>
const builder = [{
// we are starting with the top level container name
// this can be a "card", "modal", "table"... etc
"card" : {
// a card can have an icon and a color at the top right
"icon": ["iconName", "iconColor"],
// grid position for each screen
"columns" : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
// container title
"title" : "My awesome card",
// group of widgets will share same columns
// Case we want a button widget in a one column group and others in like a 3 columns group
"groups" : [{
// determine the number of columns for widgets in group for each screen
"columns" : (1, 2, 3),
"widgets" : [
{
"position" : "left", // "center" or "right" or "default"
"type" : "widgetType", // "widgetType" is the name of the widget, for example "Detail" to render a Detail widget
// Need to be a list in case we have multiple widgets in the same group
// By default, widgetEls are horizontally aligned with flexbox
"widgetEls" : [
// data we need for one widget
{
"id" : "widgetId",
"title" : "Widget title",
"label" : "Widget label",
"values" : ["value1", "value2"],
},
// ... other widgets
]
}
]
},
// ... other groups
]
},
// ... other containers
}]
</script>
<template>
</template>

View file

@ -0,0 +1,363 @@
{
"setup_loader_default": "Loading",
"setup_loader_setup": "Setting up...",
"setup_logo_alt": "BunkerWeb logo image",
"setup_title": "Setup Wizard",
"setup_account": "Account",
"setup_username": "username",
"setup_username_placeholder": "username",
"setup_password": "password",
"setup_password_placeholder": "password",
"setup_password_check": "confirm password",
"setup_password_check_placeholder": "same as previous",
"setup_password_check_invalid": "Value does not match previous password",
"setup_settings": "Settings",
"setup_ui_host": "ui host (REVERSE_PROXY_HOST)",
"setup_ui_host_placeholder": "http://[hostname](:[port])",
"setup_ui_url": "ui URL (REVERSE_PROXY_URL)",
"setup_ui_url_placeholder": "/admin",
"setup_server_name": "server name",
"setup_server_name_placeholder": "www.example.com",
"setup_server_name_value": "www.example.com",
"setup_check_dns": "Check server name DNS",
"setup_check_dns_button": "Check DNS",
"setup_check_dns_color_desc": "Result color of the check DNS status.",
"setup_check_dns_result_unknown": "check DNS status is unknown",
"setup_check_dns_result_success": "check DNS status is success",
"setup_check_dns_result_error": "check DNS status is error",
"setup_lets_encrypt": "Auto let's encrypt",
"setup_final_url": "Your BunkerWeb UI final URL will be",
"setup_button": "Setup",
"setup_form_invalid_submit": "Missing or invalid fields to submit",
"setup_failed": "Error while setting up. Try again later.",
"login_error": "Invalid username or password",
"login_logo_alt": "BunkerWeb logo image",
"login_title": "Log in",
"login_username": "username",
"login_username_placeholder": "enter username",
"login_password": "password",
"login_password_placeholder": "enter password",
"login_log_button": "Log in",
"totp_error": "TOTP code is invalid",
"totp_logo_alt": "BunkerWeb logo image",
"totp_title": "2FA",
"totp_code ": "code",
"totp_code_placeholder": "195214",
"totp_button": "Send",
"action_disable": "disable",
"action_enable": "enable",
"action_save": "save",
"action_add": "add",
"action_close": "close",
"action_delete": "delete",
"action_link": "link",
"action_edit": "edit",
"action_download": "download",
"action_create": "create",
"action_view": "view",
"action_stop": "stop",
"action_ping": "ping",
"action_reload": "reload",
"action_upload": "upload",
"action_delete_all": "delete all",
"api_pending": "Trying to retrieve {name}",
"api_error": "Error retrieving {name}",
"api_pending_default": "Trying to retrieve data",
"api_error_default": "Error retrieving data",
"dashboard_logo_alt": "BunkerWeb logo image",
"dashboard_logo_link_label": "Redirect to home page",
"dashboard_bw": "BunkerWeb",
"dashboard_docs": "docs",
"dashboard_blog": "blog",
"dashboard_privacy": "privacy",
"dashboard_license": "license",
"dashboard_sitemap": "sitemap",
"dashboard_default": "default",
"dashboard_info": "info",
"dashboard_filter": "filters",
"dashboard_advanced": "advanced",
"dashboard_loader": "loading",
"dashboard_lang_dropdown_button_desc": "Toggle hide/show radio group (dropdown) to change langage.",
"dashboard_refresh_desc": "refresh and retrieve all datas.",
"dashboard_home": "home",
"dashboard_instances": "instances",
"dashboard_global_config": "global config",
"dashboard_services": "services",
"dashboard_configs": "configs",
"dashboard_plugins": "plugins",
"dashboard_jobs": "jobs",
"dashboard_bans": "bans",
"dashboard_actions": "actions",
"dashboard_account": "account",
"dashboard_reporting": "reporting",
"dashboard_menu_toggle_sidebar": "Toggle menu sidebar.",
"dashboard_menu_close_sidebar": "Close menu sidebar.",
"dashboard_menu_twitter_label": "redirection vers le Twitter de BunkerWeb",
"dashboard_menu_linkedin_label": "redirection vers le Linkedin de BunkerWeb",
"dashboard_menu_discord_label": "redirection vers le Discord de BunkerWeb",
"dashboard_menu_github_label": "redirection vers le Github de BunkerWeb",
"dashboard_menu_plugins_title": "plugin pages",
"dashboard_menu_plugins_none": "Want custom plugins ?",
"dashboard_menu_plugins_none_doc": "check doc",
"dashboard_menu_mode_light": "light mode",
"dashboard_menu_mode_dark": "dark mode",
"dashboard_menu_log_out": "log out",
"dashboard_actions_title": "actions",
"dashboard_actions_subtitle": "feedbacks list",
"dashboard_feedback_close_sidebar": "close feedback sidebar",
"dashboard_feedback_toggle_sidebar": "toggle feedback sidebar",
"dashboard_ui": "ui",
"dashboard_scheduler": "scheduler",
"dashboard_autoconf": "autoconf",
"dashboard_core": "core",
"dashboard_global": "global",
"dashboard_news_toggle_sidebar": "Toggle news sidebar.",
"dashboard_news_close_sidebar": "Close news sidebar.",
"dashboard_news_title": "news",
"dashboard_news_subtitle": "Stay up to date !",
"dashboard_news_fetch_error": "Impossible to retrieve news",
"dashboard_newsletter_title": "Join newsletter",
"dashboard_newsletter_placeholder": "john.doe{'@'}example.com",
"dashboard_newsletter_privacy_text": "I'v read and agree",
"dashboard_newsletter_privacy_text_link": "the privacy policy",
"dashboard_newsletter_subscribe_button": "subscribe",
"dashboard_api_state_desc": "Show details about data retrieving steps.",
"dashboard_alert_close_desc": "Close alert.",
"dashboard_feedback_alert_desc": "Own actions feedbacks.",
"dashboard_feedback_logs_desc": "BunkerWeb actions feedbacks.",
"dashboard_popover_button_desc": "Show setting details on hover.",
"dashboard_popover_button": "Show popover with setting details.",
"dashboard_popover_detail_desc": "Container with setting details.",
"dashboard_up": "up",
"dashboard_down": "down",
"dashboard_banner_title_1": "Need premium support ?",
"dashboard_banner_title_2": "Try BunkerWeb on our",
"dashboard_banner_title_3": "All informations about BunkerWeb on our",
"dashboard_banner_link_1": "https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui",
"dashboard_banner_link_2": "https://demo.bunkerweb.io/link/?utm_campaign=self&utm_source=ui",
"dashboard_banner_link_3": "https://www.bunkerweb.io/?utm_campaign=self&utm_source=ui",
"dashboard_banner_link_text_1": "Check BunkerWeb Panel",
"dashboard_banner_link_text_2": "demo wep app !",
"dashboard_banner_link_text_3": "website !",
"home_version_is_latest": "is the latest version",
"home_version_latest_version": "latest version",
"home_version": "version",
"home_internal": "internal",
"home_external": "external",
"home_card_link_label": "redirect to page with related data",
"instances_hostname": "hostname",
"instances_hostname_placeholder": "bwapi",
"instances_method": "method",
"instances_port": "port",
"instances_port_placeholder": "5000",
"instances_active": "instance is active",
"instances_inactive": "instance is inactive",
"instances_modal_delete_msg": "Are you sure to delete instance with hostname {hostname} ?",
"instances_modal_delete_title": "Delete instance ",
"instances_add_instance": "Add instance",
"instances_modal_add_title": "Create new instance",
"instances_server_name": "server name",
"instances_server_name_placeholder": "www.example.com",
"instances_modal_edit_title": "Edit {name}",
"instances_edit_instance": "Edit instance",
"actions_total": "Actions total",
"actions_ui": "Actions UI",
"actions_core": "Actions CORE",
"actions_filter_search": "search",
"actions_filter_method": "methods",
"actions_filter_api_method": "api methods",
"actions_header_method": "method",
"actions_header_title": "title",
"actions_header_description": "description",
"actions_header_date": "date",
"actions_header_action": "action",
"actions_table_summary": "Actions details done on BunkerWeb (CORE, UI, CLI...)",
"bans_list": "Bans list",
"bans_add_bans": "Add bans",
"bans_add_entry": "Add entry",
"bans_instances_total": "Total instances",
"bans_ip_bans_total": "Total ip bans",
"bans_filter_title": "Filter ban list",
"bans_filter_search_ip": "search by ip",
"bans_filter_reason": "select reason",
"bans_list_no_bans": "no bans found",
"bans_list_select_all": "select all",
"bans_list_unselect_all": "unselect all",
"bans_list_unban": "unban select",
"bans_list_select_desc": "Select ban to perform actions.",
"bans_list_header_check": "check",
"bans_list_header_ip": "ip",
"bans_list_header_reason": "reason",
"bans_list_header_ban_start": "ban start",
"bans_list_header_ban_end": "ban end",
"bans_list_header_remain": "remaining",
"bans_list_table_summary": "List of current bans (ip, reason, ban start, ban end and remaining time).",
"bans_add_remove_ban_desc": "delete related field.",
"bans_add_save_bans_warning": "Bans with conflict (like whitelist) will be added but ignored.",
"bans_add_header_ip": "ip",
"bans_add_header_ban_start": "start ban",
"bans_add_header_ban_end": "end ban",
"bans_add_header_reason": "reason",
"bans_add_table_summary": "List of bans to add (ip, reason and date).",
"bans_add_ip_placeholder": "127.0.0.1",
"bans_add_reason_placeholder": "Manual",
"custom_conf_total": "Configs total",
"custom_conf_global": "Configs globals",
"custom_conf_services": "Configs services",
"custom_conf_filter_search_path": "search by path",
"custom_conf_filter_search_name": "search by name",
"custom_conf_filter_show_services_folders": "Show services",
"custom_conf_filter_show_path_with_conf": "Path with conf file only",
"custom_conf_add_file": "Add file",
"custom_conf_breadcrumb": "Path with all folders to move on click.",
"custom_conf_breadcrumb_item_desc": "Click to move to related path.",
"custom_conf_breadcrumb_back_desc": "Go to previous folder path.",
"custom_conf_modal_title": "{action} file",
"custom_conf_modal_placeholder": "filename",
"custom_conf_modal_path_desc": "Show complete path with input to set or update conf file.",
"custom_conf_modal_editor_desc": "Fill with script that will be executed for this conf file.",
"custom_conf_dot_conf": ".conf",
"custom_conf_path": "Access folder path and show children files.",
"custom_conf_dropdown_action": "Show possible actions on element.",
"global_conf_select_plugin": "select plugin",
"global_conf_select_plugin_placeholder": "search",
"global_conf_filter_search": "search",
"global_conf_filter_search_placeholder": "title, description...",
"global_conf_filter_method": "methods",
"jobs_total": "Total jobs",
"jobs_reload": "Total reload",
"jobs_success": "Total success",
"jobs_filter_search": "search",
"jobs_filter_search_placeholder": "keyword",
"jobs_filter_success_state": "success state",
"jobs_filter_reload_state": "reload state",
"jobs_filter_interval": "interval",
"jobs_headers_name": "name",
"jobs_headers_every": "interval",
"jobs_headers_history": "history",
"jobs_headers_reload": "reload",
"jobs_headers_success": "success",
"jobs_headers_last_run": "last run",
"jobs_headers_cache": "cache",
"jobs_headers_action": "action",
"jobs_state_reload_failed": "reload failed",
"jobs_state_reload_succeed": "reload succeed",
"jobs_state_success_failed": "failed",
"jobs_state_success_succeed": "succeed",
"jobs_actions_run": "run",
"jobs_actions_show_history": "Show within a modal actions history.",
"jobs_actions_cache_download": "Download cache",
"jobs_history_title": "history",
"jobs_history_headers_success": "success",
"jobs_history_headers_start_date": "run started",
"jobs_history_headers_end_date": "run ended",
"jobs_table_summary": "List of jobs (name, interval, history, reload, success, cache and run).",
"plugins_total": "plugins total",
"plugins_internal": "plugins internal",
"plugins_external": "plugins external",
"plugins_filter_search": "search",
"plugins_filter_search_placeholder": "keyword",
"plugins_filter_type": "plugin type",
"act": "plugins list",
"plugins_list_actions_link": "link to custom plugin page.",
"plugins_list_actions_delete": "delete related plugin.",
"plugins_delete_modal_title": "Delete {name} ?",
"plugins_delete_modal_text": "Are you sure to delete plugin {name} ?",
"plugins_page_label": "Access to plugin page.",
"services_total": "Service total",
"services_methods_count": "Methods total",
"services_filter_more_toggle": "Toggle more filters.",
"services_service_search": "Search by name",
"services_service_search_placeholder": "www.example.com",
"services_service_select_method": "Methods",
"services_service_select_bad_behavior": "Use bad behavior",
"services_service_select_limit": "Use limit request",
"services_service_select_reverse_proxy": "Use reverse proxy",
"services_service_select_modsecurity": "Use ModSecurity",
"services_service_select_cors": "Use CORS",
"services_service_select_dnsbl": "Use DNSBL",
"services_list_title": "Services and plugins",
"services_list_switch_warning": "Select another services will reset unsave changes on current one.",
"services_list_select_service": "select service",
"services_list_select_plugin": "select plugin",
"services_filter_search_setting": "search setting",
"services_filter_search_setting_placeholder": "name, title, description...",
"services_filter_method_setting": "Select method",
"services_detail_active": "Setting is active.",
"services_detail_inactive": "Setting is inactive.",
"services_detail_bad_behavior": "Bad behavior",
"services_detail_modsecurity": "ModSecurity",
"services_detail_limit": "Limit request",
"services_detail_reverse_proxy": "Reverse proxy",
"services_detail_cors": "CORS",
"services_detail_dnsbl": "DNSBL",
"services_active_new": "Create new service",
"services_active_clone": "Create new service (clone)",
"services_active_delete": "Delete active service {name}",
"services_active_base": "service {name}",
"services_actions_new": "New service",
"services_actions_warning": "At least an available and valid server name and/or update a setting to save.",
"services_delete_title": "Delete service",
"services_delete_msg": "Are you sure to delete service {name} ?",
"services_redirect_desc": "redirect to service website / http content",
"services_edit_desc": "open a modal to edit service",
"services_delete_desc": "open a modal to delete service",
"services_clone_desc": "Create new service using this one as base.",
"services_draft": "Draft",
"services_draft_desc": "Service is currently in draft mode",
"services_online": "Online",
"services_online_desc": "Service is currently online.",
"reporting_total": "Total reports",
"reporting_country": "Total countries",
"reporting_reason": "Total reasons",
"reporting_filter_search": "Search",
"reporting_filter_search_placeholder": "url, user agent, data",
"reporting_filter_method": "Method",
"reporting_filter_country": "Country",
"reporting_filter_status": "Status",
"reporting_filter_reason": "Reason",
"reporting_header_date": "Date",
"reporting_header_ip": "IP",
"reporting_header_country": "Country",
"reporting_header_method": "Method",
"reporting_header_url": "URL",
"reporting_header_code": "Code",
"reporting_header_user_agent": "User agent",
"reporting_header_reason": "Reason",
"reporting_header_data": "Data",
"reporting_table_summary": "List of reports (date, ip, country, method, url, code, user agent, reason and data).",
"account_settings": "Account settings",
"account_tabs_username": "Username",
"account_tabs_password": "Password",
"account_tabs_totp": "TOTP",
"account_password": "Account password",
"account_password_new": "Account new password",
"account_password_confirm": "Account confirm new password",
"account_totp_qr_code": "TOTP QR code",
"account_totp_secret": "TOTP secret key",
"account_totp_code": "TOTP code",
"account_totp_password": "TOTP code",
"account_username": "Account username",
"account_password_placeholder": "P@ssw0rd",
"account_username_placeholder": "john.doe",
"account_totp_code_placeholder": "123456",
"account_totp_secret_placeholder": "secret key",
"inp_select_default_desc": "Link to a custom visible select component. Shouldn't be used directly.",
"inp_select_dropdown_button_desc": "Custom dropdown toggle button.",
"inp_input_password_desc": "Toggle show/hide password type and so value.",
"inp_input_clipboard_desc": "Copy value to clipboard.",
"inp_input_error_required": "Field is required",
"inp_input_error_format": "Invalid value format",
"inp_input_valid": "Valid input.",
"inp_select_label_empty": "Empty",
"inp_upload_add": "click or drag-n-drop",
"inp_upload_warning": "Uploaded files are directly executed.",
"inp_upload_state_upload": "upload in progress",
"inp_upload_state_fail": "upload failed",
"inp_upload_state_success": "upload success",
"plugin_info": "Plugin info",
"plugin_test": "Test",
"antibot_info": "Antibot plugin is a service that protect your website against bots.",
"antibot_challenge": "Challenges",
"antibot_challenge_detail": "Total number"
}

View file

@ -0,0 +1,340 @@
{
"setup_loader_default": "Chargement",
"setup_loader_setup": "Mise en place...",
"setup_logo_alt": "BunkerWeb image logo",
"setup_title": "Assistant d'installation",
"setup_account": "Compte",
"setup_username": "identifiant",
"setup_username_placeholder": "identifiant",
"setup_password": "mot de passe",
"setup_password_placeholder": "mot de passe",
"setup_password_check": "confirmer mot de passe",
"setup_password_check_placeholder": "même que le précédent",
"setup_password_check_invalid": "La valeur ne correspond pas au précédent mot de passe",
"setup_settings": "Paramètres",
"setup_ui_host": "hôte dashboard (REVERSE_PROXY_HOST)",
"setup_ui_host_placeholder": "http://[nom_hôte](:[port])",
"setup_ui_url": "URL dashboard (REVERSE_PROXY_URL)",
"setup_ui_url_placeholder": "/admin",
"setup_server_name": "nom du serveur",
"setup_server_name_placeholder": "www.example.com",
"setup_server_name_value": "www.example.com",
"setup_check_dns": "Vérifier le DNS du serveur ",
"setup_check_dns_button": "Vérifier le DNS",
"setup_check_dns_color_desc": "Couleur du résultat de la vérification du DNS",
"setup_check_dns_result_unknown": "Le résultat de la vérification du DNS est inconnu",
"setup_check_dns_result_success": "Le résultat de la vérification du DNS est un succès",
"setup_check_dns_result_error": "Le résultat de la vérification du DNS est une erreur",
"setup_lets_encrypt": "Auto let's encrypt",
"setup_final_url": "Votre URL BunkerWeb final pour le dashboard est",
"setup_button": "Mettre en place",
"setup_form_invalid_submit": "Au moins un champ invalide ou manquant pour confirmer",
"setup_failed": "Erreur pendant l'installation. Réessayez plus tard.",
"login_title": "connexion",
"login_username": "identifiant",
"login_username_placeholder": "entrer identifiant",
"login_password": "mot de passe",
"login_password_placeholder": "entrer mot de passe",
"login_log_button": "se connecter",
"totp_error": "Le code du TOTP est invalide",
"totp_logo_alt": "BunkerWeb logo",
"totp_title": "Double authentification",
"totp_code ": "code",
"totp_code_placeholder": "954186",
"totp_button": "Envoyer",
"action_disable": "activer",
"action_enable": "désactiver",
"action_save": "sauvegarder",
"action_add": "ajouter",
"action_close": "fermer",
"action_delete": "supprimer",
"action_edit": "editer",
"action_download": "télécharger",
"action_create": "créer",
"action_view": "voir",
"action_stop": "stop",
"action_ping": "ping",
"action_reload": "reload",
"action_upload": "upload",
"action_delete_all": "supprimer tout",
"api_pending": "Tentative de récupération de(s) {name}",
"api_error": "Erreur de récupération de(s) {name}",
"api_pending_default": "Tentative de récupération des données",
"api_error_default": "Erreur de récupération des données",
"dashboard_logo_alt": "image du logo BunkerWeb",
"dashboard_logo_link_label": "Redirection vers l'accueil",
"dashboard_bw": "BunkerWeb",
"dashboard_docs": "docs",
"dashboard_blog": "blog",
"dashboard_privacy": "confidentialité",
"dashboard_license": "licence",
"dashboard_sitemap": "plan du site",
"dashboard_default": "défaut",
"dashboard_info": "info",
"dashboard_filter": "filtres",
"dashboard_advanced": "avancé",
"dashboard_loader": "chargement",
"dashboard_lang_dropdown_button_desc": "Toggle montre/cache le groupe radio (dropdown) pour changer de langue.",
"dashboard_refresh_desc": "rafraîchissement de toutes les données.",
"dashboard_home": "accueil",
"dashboard_instances": "instances",
"dashboard_global_config": "config globale",
"dashboard_services": "services",
"dashboard_configs": "configs",
"dashboard_plugins": "plugins",
"dashboard_jobs": "jobs",
"dashboard_bans": "bans",
"dashboard_actions": "actions",
"dashboard_account": "compte",
"dashboard_reporting": "rapports",
"dashboard_menu_toggle_sidebar": "Ouvrir/fermer la sidebar de menu.",
"dashboard_menu_close_sidebar": "Ferme la sidebar de menu.",
"dashboard_menu_twitter_label": "redirection vers le Twitter de BunkerWeb",
"dashboard_menu_linkedin_label": "redirection vers le Linkedin de BunkerWeb",
"dashboard_menu_discord_label": "redirection vers le Discord de BunkerWeb",
"dashboard_menu_github_label": "redirection vers le Github de BunkerWeb",
"dashboard_menu_plugins_title": "pages plugin",
"dashboard_menu_plugins_none": "envie de plugins custom ?",
"dashboard_menu_plugins_none_doc": "voir la doc",
"dashboard_menu_mode_light": "mode clair",
"dashboard_menu_mode_dark": "mode sombre",
"dashboard_menu_log_out": "se déconnecter",
"dashboard_actions_title": "actions",
"dashboard_actions_subtitle": "liste des feedbacks",
"dashboard_feedback_close_sidebar": "fermer la sidebar de feeback",
"dashboard_feedback_toggle_sidebar": "ouvrir/fermer la sidebar de feeback",
"dashboard_ui": "iu",
"dashboard_scheduler": "scheduler",
"dashboard_autoconf": "autoconf",
"dashboard_core": "core",
"dashboard_global": "global",
"dashboard_news_toggle_sidebar": "Ouvrir/fermer la sidebar de news.",
"dashboard_news_close_sidebar": "Ferme la sidebar de news.",
"dashboard_news_title": "news",
"dashboard_news_subtitle": "restez à jour !",
"dashboard_news_fetch_error": "Impossible de récupérer les news",
"dashboard_newsletter_title": "Rejoindre la newsletter",
"dashboard_newsletter_placeholder": "martin.dupont{'@'}example.com",
"dashboard_newsletter_privacy_text": "j'ai lu et accepte",
"dashboard_newsletter_privacy_text_link": "la politique de confidentialité",
"dashboard_newsletter_subscribe_button": "souscrire",
"dashboard_api_state_desc": "Montre les détails sur la récupération de données.",
"dashboard_alert_close_desc": "Ferme l'alerte.",
"dashboard_feedback_alert_desc": "Retour sur vos propres actions.",
"dashboard_feedback_logs_desc": "Retour sur les actions BunkerWeb.",
"dashboard_popover_button_desc": "Montre des détails sur les paramètres au survol.",
"dashboard_popover_button": "Montre un popover avec des détails sur le paramètre",
"dashboard_popover_detail_desc": "Un conteneur avec les détails sur le paramètre.",
"dashboard_up": "up",
"dashboard_down": "down",
"dashboard_banner_title_1": "Besoin de support premium ?",
"dashboard_banner_title_2": "Essayez Bunkerweb sur notre",
"dashboard_banner_title_3": "Toutes informations disponibles sur",
"dashboard_banner_link_1": "https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui",
"dashboard_banner_link_2": "https://demo.bunkerweb.io/link/?utm_campaign=self&utm_source=ui",
"dashboard_banner_link_3": "https://www.bunkerweb.io/?utm_campaign=self&utm_source=ui",
"dashboard_banner_link_text_1": "BunkerWeb Panel est là !",
"dashboard_banner_link_text_2": "demo web app !",
"dashboard_banner_link_text_3": "notre website !",
"home_version_is_latest": "est la dernière version",
"home_version_latest_version": "dernière version",
"home_version": "version",
"home_internal": "internes",
"home_external": "externes",
"home_card_link_label": "redirection vers la page correspondante aux données",
"instances_hostname": "hostname",
"instances_method": "methode",
"instances_port": "port",
"instances_active": "instance active",
"instances_inactive": "instance inactive",
"instances_modal_delete_msg": "Etes-vous sûr de vouloir supprimer l'instance {hostname} ?",
"instances_modal_delete_title": "Supprimer l'instance",
"actions_total": "Actions total",
"actions_ui": "Actions IU",
"actions_core": "Actions CORE",
"actions_filter_title": "filtres",
"actions_filter_search": "rechercher",
"actions_filter_method": "methodes",
"actions_filter_api_method": "api methodes",
"actions_header_method": "methode",
"actions_header_title": "titre",
"actions_header_description": "description",
"actions_header_date": "date",
"actions_header_action": "action",
"actions_table_summary": "Détails des actions effectuées sur BunkerWeb (CORE, UI, CLI...)",
"bans_list": "Liste des bans",
"bans_add_bans": "Ajouter des bans",
"bans_add_entry": "Ajouter une entrée",
"bans_instances_total": "Total instances",
"bans_ip_bans_total": "Total ip bans",
"bans_filter_title": "Filtres liste des bans",
"bans_filter_search_ip": "rechercher par ip",
"bans_filter_reason": "sélectionner une raison",
"bans_list_no_bans": "Aucun ban actif",
"bans_list_select_all": "sélectionner tout",
"bans_list_unselect_all": "déslectionner tout",
"bans_list_unban": "déban sélection",
"bans_list_select_desc": "Sélectionne un ban pour effectuer des actions.",
"bans_list_header_check": "check",
"bans_list_header_ip": "ip",
"bans_list_header_reason": "raison",
"bans_list_header_ban_start": "début ban",
"bans_list_header_ban_end": "fin ban",
"bans_list_header_remain": "durée restante",
"bans_list_table_summary": "Liste des bans actuels (ip, raison, date et délai restant).",
"bans_add": "Ajouter une entrée",
"bans_add_remove_ban_desc": "Supprime le champ relié d'ajout de ban.",
"bans_add_save_bans_warning": "Les bans avec un conflit (comme la whitelist) seront ajoutés mais ignorés.",
"bans_add_header_ip": "ip",
"bans_add_header_ban_start": "début ban",
"bans_add_header_ban_end": "fin ban",
"bans_add_header_reason": "raison",
"bans_add_table_summary": "Liste des bans à ajouter (ip, raison et date).",
"custom_conf_total": "Configs total",
"custom_conf_global": "Configs globales",
"custom_conf_services": "Configs services",
"custom_conf_filter_search_path": "Rechercher par chemin",
"custom_conf_filter_search_name": "Rechercher par nom",
"custom_conf_filter_show_services_folders": "Voir les services",
"custom_conf_filter_show_path_with_conf": "Chemin .conf seulement",
"custom_conf_add_file": "Ajouter fichier",
"custom_conf_breadcrumb": "Chemin avec chaque dossier cliquable pour s'y rendre.",
"custom_conf_breadcrumb_item_desc": "Cliquer pour se rendre au chemin relié.",
"custom_conf_breadcrumb_back_desc": "Aller au dossier parent.",
"custom_conf_modal_title": "{action} fichier",
"custom_conf_modal_placeholder": "nom du fichier",
"custom_conf_modal_path_desc": "Montre le chemin entier avec un input avec le nom du fichier conf.",
"custom_conf_modal_editor_desc": "Remplir avec le script qui doit s'exécuter pour ce fichier.",
"custom_conf_dot_conf": ".conf",
"custom_conf_path": "Accéder à l'intérieur du dossier pour voir les fichiers enfants.",
"custom_conf_dropdown_action": "Affiche les actions possibles sur l'élément.",
"global_conf_select_plugin": "Sélectionner un plugin",
"global_conf_select_plugin_placeholder": "rechercher",
"global_conf_filter_search": "rechercher",
"global_conf_filter_search_placeholder": "titre, description...",
"global_conf_filter_method": "methodes",
"jobs_total": "Total jobs",
"jobs_reload": "Total rechargés",
"jobs_success": "Total réussis",
"jobs_filter_search": "rechercher",
"jobs_filter_search_placeholder": "mot clé",
"jobs_filter_success_state": "état de réussite",
"jobs_filter_reload_state": "état de rechargement",
"jobs_filter_interval": "interval",
"jobs_headers_name": "nom",
"jobs_headers_every": "interval",
"jobs_headers_history": "historique",
"jobs_headers_reload": "rechargement",
"jobs_headers_success": "réussite",
"jobs_headers_last_run": "dernier tour",
"jobs_headers_cache": "cache",
"jobs_headers_action": "action",
"jobs_state_reload_failed": "recharge échoué",
"jobs_state_reload_succeed": "recharge réussie",
"jobs_state_success_failed": "échec",
"jobs_state_success_succeed": "succès",
"jobs_actions_run": "lancer",
"jobs_actions_show_history": "Montre dans un modal l'historique des actions au clique.",
"jobs_actions_cache_download": "Télécharger le cache",
"jobs_history_title": "historique",
"jobs_history_headers_success": "réussite",
"jobs_history_headers_start_date": "début exécution",
"jobs_history_headers_end_date": "fin exécution",
"jobs_table_summary": "Liste des jobs (nom, interval, historique, rechargement, réussite, cache et lancement).",
"plugins_total": "plugins total",
"plugins_internal": "plugins internes",
"plugins_external": "plugins externes",
"plugins_filter_search": "rechercher",
"plugins_filter_search_placeholder": "mot clé",
"plugins_filter_type": "type de plugin",
"act": "liste des plugins",
"plugins_list_actions_link": "lien vers la page custom du plugin",
"plugins_list_actions_delete": "supprime le plugin relié.",
"plugins_delete_modal_title": "Supprimer le {name} ?",
"plugins_delete_modal_text": "Etes-vous sûr de vouloir supprimer le plugin {name} ?",
"plugins_page_label": "Accède à la page du plugin concerné",
"services_total": "total services",
"services_methods_count": "Total méthodes",
"services_filter_more_toggle": "Ouvrir/fermer les filtres supplémentaires.",
"services_service_search": "rechercher par nom",
"services_service_search_placeholder": "www.example.com",
"services_service_select_method": "Méthodes",
"services_service_select_bad_behavior": "Mauvais comportement",
"services_service_select_limit": "Limite requête",
"services_service_select_reverse_proxy": "Proxy inverse",
"services_service_select_modsecurity": "ModSecurity",
"services_service_select_cors": "CORS",
"services_service_select_dnsbl": "DNSBL",
"services_list_title": "Services and plugins",
"services_list_switch_warning": "Changer de service réinitialise les changements non sauvegardés du service en cours.",
"services_list_select_service": "sélectionner un service",
"services_list_select_plugin": "sélectionner un plugin",
"services_filter_search_setting": "rechercher un paramètre",
"services_filter_search_setting_placeholder": "nom, titre, description...",
"services_filter_method_setting": "Sélectionner une méthode",
"services_detail_active": "Le paramètre est actif.",
"services_detail_inactive": "Le paramètre est inactif.",
"services_detail_bad_behavior": "Mauvais comportement",
"services_detail_modsecurity": "ModSecurity",
"services_detail_limit": "Limite requête",
"services_detail_reverse_proxy": "Proxy inverse",
"services_detail_cors": "CORS",
"services_detail_dnsbl": "DNSBL",
"services_active_new": "Créer un nouveau service",
"services_active_clone": "Créer un nouveau service (clone)",
"services_active_delete": "Supprime le service actif {name}",
"services_active_base": "service {name}",
"services_actions_new": "Nouveau service",
"services_actions_warning": "Un nom de serveur valide et disponible et/ou un changement de paramètre pour sauvegarder.",
"services_delete_title": "Supprimer un service",
"services_delete_msg": "Etes-vous sûr de vouloir supprimer le service {name} ?",
"services_redirect_desc": "redirection vers le site du service ou autre contenu http",
"services_edit_desc": "ouvrir un modal pour éditer le service.",
"services_delete_desc": "Ouvrir un modal pour supprimer le service.",
"services_clone_desc": "Créer un nouveau service basé sur les paramètres de celui-ci.",
"reporting_total": "Total rapports",
"reporting_country": "Total pays",
"reporting_reason": "Total raisons",
"reporting_filter_search": "Recherche",
"reporting_filter_search_placeholder": "url, agent utilisateur, data",
"reporting_filter_method": "Méthode",
"reporting_filter_country": "Pays",
"reporting_filter_status": "Code",
"reporting_filter_reason": "Raison",
"reporting_header_date": "Date",
"reporting_header_ip": "IP",
"reporting_header_country": "Pays",
"reporting_header_method": "Méthode",
"reporting_header_url": "URL",
"reporting_header_code": "Code",
"reporting_header_user_agent": "Agent utilisateur",
"reporting_header_reason": "Raison",
"reporting_header_data": "Data",
"reporting_table_summary": "Liste des rapports (date, ip, pays, méthode, url, code, agent utilisateur, raison, data).",
"account_settings": "Paramètres de compte",
"account_tabs_username": "Nom d'utilisateur",
"account_tabs_password": "Mot de passe",
"account_tabs_totp": "TOTP",
"account_password": "Mot de passe",
"account_password_new": "Nouveau mot de passe",
"account_password_confirm": "Confirmer nouveau mot de passe",
"account_totp_qr_code": "TOTP QR code",
"account_totp_secret": "TOTP clé secrète",
"account_totp_code": "TOTP code",
"account_totp_password": "TOTP code",
"account_username": "Nom d'utilisateur",
"account_password_placeholder": "P@ssw0rd",
"account_username_placeholder": "martin.dupont",
"account_totp_code_placeholder": "123456",
"account_totp_secret_placeholder": "clé secrète",
"inp_select_default_desc": "Relié à un composant select custom. Ne pas utiliser directement.",
"inp_select_dropdown_button_desc": "Bouton toggle de dropdown custom.",
"inp_select_label_empty": "Vide",
"inp_input_password_desc": "Toggle l'input pour montrer/cacher une donnée de type mot de passe.",
"inp_input_clipboard_desc": "Copie la valeur dans le presse-papier.",
"inp_upload_add": "cliquez ou glissez-déposez",
"inp_upload_warning": "Les fichiers upload sont exécutés directement.",
"inp_upload_state_upload": "upload en cours",
"inp_upload_state_fail": "upload ratée",
"inp_upload_state_success": "upload réussie"
}

View file

@ -1,6 +1,8 @@
<script setup>
import { reactive, onBeforeMount } from "vue";
import TestTile from "@components/TestTitle.vue";
import Checkbox from "@components/Forms/Field/Checkbox.vue";
import Select from "@components/Forms/Field/Select.vue";
import Input from "@components/Forms/Field/Input.vue";
// Define reactive properties
const data = reactive({
@ -14,10 +16,48 @@ onBeforeMount(() => {
data.title = dataEl.getAttribute('data-flask') && dataEl.getAttribute('data-flask') !== "{{ flask_data }}" ? dataEl.getAttribute('data-flask') : dataEl.getAttribute('data-default-value');
})
const checkboxData = {
id: 'test-checkbox',
value: 'yes',
name: 'test-checkbox',
disabled: false,
required: false,
label: 'Test checkbox',
tabId: '1',
}
const selectData = {
id: 'test-select',
value: 'yes',
values: ['yes', 'no'],
name: 'test-select',
disabled: false,
required: false,
label: 'Test select',
tabId: '1',
}
const inputData = {
id: 'test-input',
value: 'yes',
type: "text",
name: 'test-input',
disabled: false,
required: false,
label: 'Test input',
pattern : "(test)",
tabId: '1',
}
</script>
<template>
<div class="bg-secondary flex flex-col items-center justify-center h-full">
<TestTile :title="data.title" />
<div style="width: 300px;">
<Checkbox v-bind="checkboxData" />
<Select v-bind="selectData" />
<Input v-bind="inputData" />
</div>
</div>
</template>

View file

@ -1,4 +1,11 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { getI18n } from "@utils/lang.js";
import Test from "./Test.vue";
createApp(Test).mount("#app");
const pinia = createPinia();
createApp(Test)
.use(pinia)
.use(getI18n(["dashboard", "api", "action", "bans", "inp"]))
.mount("#app");

View file

@ -0,0 +1,28 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useSelectIPStore = defineStore("selectIP", () => {
const data = ref(new Set());
function addIP(ip) {
data.value.add(ip);
}
function deleteIP(ip) {
data.value.delete(ip);
}
function $reset() {
data.value.clear();
}
return { data, $reset, addIP, deleteIP };
});
export const useAddModalStore = defineStore("addBanModal", () => {
const isOpen = ref(false);
return {
isOpen,
};
});

View file

@ -0,0 +1,31 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useModalStore = defineStore("fileManagerModal", () => {
const isOpen = ref(false);
const data = ref({
type: "folder",
action: "view",
path: "root/",
pathLevel: 1,
value: "",
name: "root",
});
function $reset() {
data.value = {
type: "folder",
action: "view",
path: "root/",
pathLevel: 1,
value: "",
name: "root",
};
}
return {
isOpen,
data,
$reset,
};
});

View file

@ -0,0 +1,55 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useFeedbackStore = defineStore("feedback", () => {
const data = ref([]);
let feedID = 0;
async function addFeedback(type, status, message) {
feedID++;
data.value.push({
id: feedID,
isNew: true,
type: type,
status: status,
message: message,
});
}
function removeFeedback(id) {
data.value.splice(
data.value.findIndex((item) => item["id"] === id),
1,
);
}
return { data, addFeedback, removeFeedback };
});
export const useRefreshStore = defineStore("refresh", () => {
const count = ref(0);
async function refresh() {
count.value++;
}
return { count, refresh };
});
export const useBannerStore = defineStore("banner", () => {
const isBanner = ref(true);
const bannerClass = ref("banner");
async function setBannerVisible(bool) {
isBanner.value = bool;
bannerClass.value = bool ? "banner" : "no-banner";
}
return { isBanner, bannerClass, setBannerVisible };
});
export const useBackdropStore = defineStore("backdrop", () => {
const clickCount = ref(0);
return { clickCount };
});

View file

@ -0,0 +1,54 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useDelModalStore = defineStore("delInstanceModal", () => {
const isOpen = ref(false);
const data = ref({
hostname: "",
});
function $reset() {
data.value = {
hostname: "",
};
}
return {
isOpen,
data,
$reset,
};
});
export const useAddModalStore = defineStore("addInstanceModal", () => {
const isOpen = ref(false);
return {
isOpen,
};
});
export const useEditModalStore = defineStore("editInstanceModal", () => {
const isOpen = ref(false);
const data = ref({
hostname: "",
old_hostname: "",
server_name: "",
port: "",
});
function $reset() {
data.value = {
hostname: "",
old_hostname: "",
server_name: "",
port: "",
};
}
return {
isOpen,
data,
$reset,
};
});

View file

@ -0,0 +1,23 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useModalStore = defineStore("jobsModal", () => {
const isOpen = ref(false);
const data = ref({
name: "",
history: [],
});
function $reset() {
data.value = {
name: "",
history: [],
};
}
return {
isOpen,
data,
$reset,
};
});

View file

@ -0,0 +1,16 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useLogsStore = defineStore("logs", () => {
const tags = ref([]);
function setTags(arr) {
tags.value = arr;
}
function $reset() {
tags.value = [];
}
return { tags, $reset, setTags };
});

View file

@ -0,0 +1,25 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useDelModalStore = defineStore("delPluginModal", () => {
const isOpen = ref(false);
const data = ref({
id: "",
name: "",
description: "",
});
function $reset() {
data.value = {
id: "",
name: "",
description: "",
};
}
return {
isOpen,
data,
$reset,
};
});

View file

@ -0,0 +1,61 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useModalStore = defineStore("ServiceModal", () => {
const isOpen = ref(false);
const data = ref({
services: [],
service: "",
serviceName: "",
operation: "",
servicesNames: [],
method: "",
isDraft: false,
});
function $reset() {
data.value = {
services: [],
service: "",
serviceName: "",
operation: "",
servicesNames: [],
method: "",
};
}
return {
isOpen,
data,
$reset,
};
});
export const useDelModalStore = defineStore("ServiceDelModal", () => {
const isOpen = ref(false);
const data = ref({
service: "",
method: "",
});
function $reset() {
data.value = {
service: "",
method: "",
};
}
return {
isOpen,
data,
$reset,
};
});
export const useFilterStore = defineStore("serviceCardFilter", () => {
const isOpen = ref(false);
return {
isOpen,
};
});

View file

@ -0,0 +1,60 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useConfigStore = defineStore("config", () => {
const data = ref({ global: {}, services: {} });
function updateConf(name, id, value, regex = ".*") {
// Remove prev value
removeConf(name, id);
// Try to update with another value
const formatID = id.toUpperCase().replaceAll("-", "_");
// Case value is invalid to be added
const validInp = new RegExp(regex);
if (validInp.test(value) === false) return;
// Else add value
if (name === "global") data.value[name][formatID] = value;
if (name !== "global") {
if (!(name in data.value["services"])) data.value["services"][name] = {};
data.value["services"][name][formatID] = value;
}
}
// Case value is default or previous (and already in core config value)
// Or before updating a value
function removeConf(name, id) {
if (!name || !id) return; // Need this to proceed
const formatID = id.toUpperCase().replaceAll("-", "_");
// Remove global value
try {
if (name === "global" && !!(formatID in data.value[name]))
delete data.value[name][formatID];
} catch (err) {}
// Remove service value
try {
if (name !== "global" && !!(formatID in data.value["services"][name]))
delete data.value["services"][name][formatID];
} catch (err) {}
}
function $reset() {
data.value = {
global: {},
services: {},
};
}
return { data, $reset, updateConf, removeConf };
});
export const useModesStore = defineStore("modes", () => {
const data = ref(["AUTOCONF_MODE", "SWARM_MODE", "KUBERNETES_MODE"]);
return { data, updateConf };
});

View file

@ -0,0 +1,38 @@
// Filter actions
export function getActionsByFilter(actions, filters) {
actions.forEach((action, id) => {
let isMatch = true;
// Search filter
if (
(filters.search &&
!action.title.toLowerCase().includes(filters.search.toLowerCase())) ||
!action.description.toLowerCase().includes(filters.search.toLowerCase())
)
isMatch = false;
// Method filter
if (
filters.method !== "all" &&
!action.method.toLowerCase().includes(filters.method.toLowerCase())
)
isMatch = false;
// Action api filter
if (
filters.actionApi !== "all" &&
!action.api_method.toLowerCase().includes(filters.actionApi.toLowerCase())
)
isMatch = false;
action["isMatchFilter"] = isMatch;
});
// Update actions
return actions;
}
export function getSelectList(baseItems, loopArr, keyCheck) {
const arr = baseItems;
loopArr.forEach((item) => {
if (arr.indexOf(item[keyCheck]) === -1) arr.push(item[keyCheck]);
});
return arr;
}

View file

@ -0,0 +1,107 @@
// Setup response json
export async function setResponse(type, status, message, data) {
const res = { type: "", status: "", message: "", data: {} };
res["type"] = type;
res["status"] = status;
res["message"] = message;
res["data"] = data;
return res;
}
// Fetch api
// state args must be reactive variable with isPend and isErr keys
export async function fetchAPI(
api,
method,
body = false,
state = null,
addFeedback = null,
isJSON = true,
fileName = null, // test.json (name + extension)
) {
// Block scope state object if any passed to avoid error
!state ? (state = { isPend: false, isErr: false, data: {} }) : false;
// Fetch
// const baseURL = "http://localhost:7000"; // ? for dev
const baseURL = window.location.origin; // ? for prod
state.isPend = true;
return await fetch(`${baseURL}${api}`, {
method: method.toUpperCase(),
headers: {
"Content-Type": isJSON ? "application/json" : "application/octet-stream",
"X-CSRF-TOKEN": getCookie("csrf_access_token"),
},
// Only when exist and possible
...(body &&
method.toUpperCase() !== "GET" && { body: JSON.stringify(body) }),
})
.then((res) => {
state.isPend = false;
state.isErr = false;
// Can be a json or a file
return isJSON ? res.json() : res.blob();
})
.then((data) => {
// Case no JSON, we handle file download
if (!isJSON) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
a.click();
a.remove(); //afterwards we remove the element again
return true;
}
if (isJSON) {
state.isErr = data["type"] === "error" ? true : false;
state.data = JSON.parse(
typeof data["data"] === "string"
? data["data"]
: JSON.stringify(data["data"]),
);
addFeedback
? addFeedback(data["type"], data["status"], data["message"])
: false;
return data;
}
})
.catch((err) => {
state.isPend = false;
state.isErr = true;
state.data = {};
if (!isJSON) {
addFeedback
? addFeedback(
err["type"] || "error",
err["status"] || 500,
err["message"] ||
"Internal Server Error, impossible to download file",
)
: false;
}
if (isJSON) {
addFeedback
? addFeedback(
err["type"] || "error",
err["status"] || 500,
err["message"] || "Internal Server Error, impossible to get JSON",
)
: false;
}
// Set custom error data before throwing err
return err;
});
}
export function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(";").shift();
}

View file

@ -0,0 +1,14 @@
// Filter bans
export function getBansByFilter(bans, filters) {
bans.forEach((ban, id) => {
let isMatch = true;
if (filters.search && !ban.ip.includes(filters.search)) isMatch = false;
if (filters.reason !== "all" && !ban.reason.includes(filters.reason))
isMatch = false;
ban["isMatchFilter"] = isMatch;
});
return bans;
}

View file

@ -0,0 +1,269 @@
// Custom configs types based on NGINX foundation
// Will determine context and order to execute specific config
export function getTypes() {
return [
"http",
"server_http",
"default_server_http",
"modsec",
"modsec_crs",
"stream",
"server_stream",
];
}
// Base structure for file manager with custom_configs
// For UI we will convert types as base folders where we can create custom conf in it
export function getBaseConfig() {
const baseConfig = [];
// We need a root folder
baseConfig.push(generateItem("folder", "", false, false, false, false));
// Base folders are after root
const types = getTypes();
for (let i = 0; i < types.length; i++) {
baseConfig.push(
generateItem("folder", types[i], true, false, false, false),
);
}
return baseConfig;
}
// Allow to create a valid item for file manager
// type => "folder" || "file"
// path => "path" (<type>/<service_id>/<name> or <type>/<name> or <name>)
// canCreateFile => bool (possible to create a conf file on the folder)
// canEdit => bool (possible to edit item like name or content)
// canDelete => bool (allow to delete item)
// children => [item] (item that need to be display when inside parent item)
export function generateItem(
type,
path,
canCreateFile,
canCreateFolder,
canEdit,
canDelete,
children = [],
data = "",
method = "static",
) {
const fullPath = `root${path ? `/${path}` : ``}`;
return {
type: type,
path: fullPath,
pathLevel: fullPath.split("/").length - 1,
canDelete: canDelete,
canEdit: canEdit,
canCreateFile: canCreateFile,
canCreateFolder: canCreateFolder,
data: data,
children: children,
method: method,
};
}
export function generateConfTree(configs, services) {
const baseConf = getBaseConfig();
// Add services to base folders
// Exclude some base folders that can only have roots
const rootOnly = ["server_stream", "server_http", "modsec", "modsec_crs"];
const servItems = [];
for (let i = 0; i < services.length; i++) {
const servName = services[i];
baseConf.forEach((folder) => {
// Target only base folder (pathLevel 1)
if (folder.pathLevel !== 1) return;
// Case exclude
const folderName = folder["path"].replace("root/", "");
if (rootOnly.includes(folderName)) return;
// Case not exclude
const path = folder["path"].replace("root/", "");
const servItem = generateItem(
"folder",
`${path}/${servName}`,
true,
false,
false,
false,
);
folder.children.push(servItem);
servItems.push(servItem);
});
}
const conf = [...baseConf, ...servItems];
// Fetch config is always file type with data and actions
// Retrieve file data and format
for (let i = 0; i < configs.length; i++) {
conf.push(
generateItem(
"file",
`${configs[i].type}${
configs[i].service_id ? `/${configs[i].service_id}` : ""
}/${configs[i].name}`,
false,
false,
configs[i].method === "static" ? false : true,
configs[i].method === "static" ? false : true,
[],
configs[i].data || "",
configs[i].method || "",
),
);
}
// We need to define parent and children to create tree structure
// First we need to be sure that a parent exist or create it
// (May not exist because fetch only file and not intermediate folders)
const parents = [];
for (let i = 0; i < conf.length; i++) {
// Check only pathLevel > 2 because 0 = root, 1 = base
const item = conf[i];
if (item.pathLevel < 2) continue;
// Get prev item path (parent)
const splitPath = item["path"].split("/");
splitPath.pop();
const prevPath = splitPath.join("/");
// Check if parent on base or on created ones
const isParent =
conf.filter((item) => item["path"] === prevPath).length === 0 &&
parents.filter((item) => item["path"] === prevPath).length === 0
? false
: true;
// Parent is always a folder because fetch return only file conf
if (!isParent) {
// Can create folder only on level 1 and 2
const canCreateFolder =
item.pathLevel === 1 || item.pathLevel === 2 ? true : false;
parents.push(
generateItem(
"folder",
prevPath.replace("root/", ""),
true,
canCreateFolder,
true,
true,
),
);
}
}
// Then we need to set children
// children must be only one path level more than parent
for (let i = 0; i < conf.length; i++) {
const item = conf[i];
// Get children and set them to parent
const children = [];
conf.forEach((child) => {
const isChild =
child["path"].startsWith(item["path"]) &&
child["pathLevel"] === item["pathLevel"] + 1
? true
: false;
if (isChild) children.push(child);
});
item["children"] = children;
}
return conf;
}
// Filter custom configs
export function getCustomConfByFilter(items, filters) {
const itemsToDel = [];
items.forEach((item, id) => {
let isMatch = true;
const path = item.path.replace("root/", "");
const splitPath = path.split("/");
const name = splitPath.pop();
const pathLevel = item.pathLevel;
// root exclude to avoid break
if (pathLevel === 0) return;
// Check every filter
for (const [key, value] of Object.entries(filters)) {
// Case no match by a previous filter
if (!isMatch) continue;
// Case path keyword
if (key === "pathKeyword" && value) {
isMatch = path.includes(value) ? true : false;
}
// Case name keyword
if (key === "nameKeyword" && value) {
isMatch = name.includes(value) ? true : false;
}
// Case services folders
if (key === "showServices" && value === "no" && pathLevel === 2) {
isMatch = false;
}
// Case check for .conf at end of path
if (key === "showOnlyCaseConf" && value === "yes") {
isMatch =
items.filter(
(item) => item.pathLevel === 3 && item.path.includes(path),
).length === 0
? false
: true;
}
}
// Case no match
if (!isMatch) itemsToDel.push(items[id]);
});
// For items that didn't pass filter
// We need to remove them itself as item and as other items children
itemsToDel.forEach((itemDel) => {
const delPath = itemDel.path;
const delPathLevel = itemDel.pathLevel;
// Get prev path level
const prevPathLevel = itemDel.pathLevel - 1;
// Avoid remove root
if (prevPathLevel === -1) return;
// Get prev path
const splitPath = itemDel.path.split("/");
splitPath.pop();
const prevPath = splitPath.join("/");
// Remove item as children
items.forEach((item) => {
const path = item.path;
const pathLevel = item.pathLevel;
if (prevPathLevel !== pathLevel || prevPath !== path) return;
// Get item that match
const children = item.children;
const matchIds = [];
children.forEach((child, id) => {
if (child.path !== delPath || child.pathLevel !== delPathLevel) return;
matchIds.push(id);
});
// Remove them using id
matchIds.forEach((id) => {
children[id] = "";
});
item.children = children.filter((item) => typeof item !== "string");
// At the end remove item itself
item = "";
});
});
// Update items removing empty string
return items
.filter((item) => typeof item !== "string")
.sort((a, b) => {
if (a.type === "file" && b.type === "folder") return -1;
});
}

View file

@ -0,0 +1,19 @@
export function getDarkMode() {
let darkMode = false;
// Case on storage
if (sessionStorage.getItem("mode")) {
darkMode = sessionStorage.getItem("mode") === "dark" ? true : false;
} else if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
// dark mode
darkMode = true;
sessionStorage.setItem("mode", "dark");
} else {
darkMode = false;
sessionStorage.setItem("mode", "light");
}
return darkMode;
}

View file

@ -0,0 +1,83 @@
export function getJobsIntervalList() {
return ["all", "once", "hour", "day", "week"];
}
// Format to go from dict to array
export async function jobsFormat(jobs) {
const jobsArr = [];
Object.entries(jobs).forEach(([name, data]) => {
jobsArr.push({ [name]: data });
});
return jobsArr;
}
// Format cache data for select
export function getJobsCacheNames(caches) {
const names = [];
caches.forEach((cache) => {
names.push(cache["file_name"]);
});
return names;
}
// Format cache data for select
export function getServId(jobs, jobName, cacheName) {
let servID;
jobs.forEach((jobItem) => {
for (const [jobN, job] of Object.entries(jobItem)) {
if (jobN !== jobName) continue;
for (const [key, value] of Object.entries(job["cache"])) {
if (value["file_name"] !== cacheName) continue;
servID = value["service_id"];
}
}
});
return servID;
}
// Filter plugins
export function getJobsByFilter(jobs, filters) {
jobs.forEach((job, id) => {
const jobName = Object.keys(job).join();
const data = job[jobName];
for (const [key, value] of Object.entries(filters)) {
// Check specific cases
if (
(!(key in data) && key !== "name" && key !== "success") ||
(key === "name" && value === "") ||
value === "all"
)
continue;
const checkType = typeof value;
let isMatch = true;
if (checkType === "string" && key === "name") {
const filterValue = value.toLowerCase().trim();
const checkValue = jobName.toLowerCase().trim();
isMatch = checkValue.includes(filterValue) ? true : false;
}
if (checkType === "string" && key !== "name") {
const filterValue = value.toLowerCase().trim();
const checkValue = data[key].toLowerCase().trim();
isMatch = checkValue.includes(filterValue) ? true : false;
}
if (checkType === "boolean" && key === "success") {
isMatch = value === data["history"][0][key] ? true : false;
}
if (checkType === "boolean" && key !== "success") {
isMatch = value === data[key] ? true : false;
}
// Result
if (!isMatch) delete jobs[id];
}
});
// Update jobs removing empty index (deleted jobs)
return jobs.filter(Object);
}

View file

@ -0,0 +1,70 @@
import { createI18n } from "vue-i18n";
import fr from "@lang/fr.json" assert { type: "json" };
import en from "@lang/en.json" assert { type: "json" };
const availablesLangs = ["en", "fr"];
function getAllLang() {
return { fr: fr, en: en };
}
function getAllLangCurrPage(pagesArr) {
const langs = getAllLang();
// for each lang
for (const [langName, langVal] of Object.entries(langs)) {
const data = {};
for (const [key, value] of Object.entries(langVal)) {
pagesArr.forEach((name) => {
if (key.startsWith(`${name}_`)) data[key] = value;
});
}
langs[langName] = data;
}
return langs;
}
export function getI18n(pagesArr = []) {
const messages =
pagesArr.length > 0 ? getAllLangCurrPage(pagesArr) : getAllLang();
const i18n = createI18n({
legacy: false,
locale: getLocalLang(), // set locale
fallbackLocale: "en",
messages, // set locale messages
availableLocales: availablesLangs,
});
return i18n;
}
export function getLocalLang() {
// get store lang, or local, or default
if (
sessionStorage.getItem("lang") &&
availablesLangs.indexOf(sessionStorage.getItem("lang")) !== -1
) {
return sessionStorage.getItem("lang");
}
if (
navigator.language &&
availablesLangs.indexOf(navigator.language.split("-")[0].toLowerCase()) !==
-1
) {
return navigator.language.split("-")[0].toLowerCase();
}
if (
navigator.languages &&
navigator.languages > 0 &&
availablesLangs.indexOf(
navigator.languages[0].split("-")[0].toLowerCase(),
) !== -1
) {
return navigator.languages[0].split("-")[0].toLowerCase();
}
return "en";
}

View file

@ -0,0 +1,27 @@
// Filter actions
export function getLogsByFilter(actions, filters) {
if (!Array.isArray(actions)) return [];
actions.forEach((action, id) => {
let isMatch = true;
// Tags filter
if (filters.tags.indexOf("all") === -1) {
let oneTagMatch = false;
for (let i = 0; i < action.tags.length; i++) {
const tag = action.tags[i];
if (filters.tags.indexOf(tag) !== -1) oneTagMatch = true;
}
isMatch = oneTagMatch ? isMatch : false;
}
action["isMatchFilter"] = isMatch;
});
// Update actions
return actions;
}
// None limited list of tags
export function getTags() {
return ["plugin", "job", "action", "instance", "config", "custom_config"];
}

View file

@ -0,0 +1,150 @@
// Set additional data to plugins
export function setPluginsData(plugins) {
plugins.forEach((plugin) => {
const settings = plugin["settings"];
// Replace some settings data
Object.entries(settings).forEach(([setting, data]) => {
// Add default method for filter
if (!("method" in data)) data["method"] = "default";
});
});
return plugins;
}
// We want to add config data as value of settings of plugins
export function addConfToPlugins(plugins, config) {
plugins.forEach((plugin) => {
const settings = plugin["settings"];
// Case not multiple, add direct custom value to setting
Object.entries(settings).forEach(([settingName, settingData]) => {
if (!("multiple" in settingData)) {
try {
settingData["value"] = config[settingName]["value"];
settingData["method"] = config[settingName]["method"];
delete config[settingName];
} catch (err) {}
}
});
// Case multiple, format on config is setting_num
// Multiples need to add a setting next to base one
// To avoid loop issue by adding a setting
// We need to add them after loop
const multiples = [];
Object.entries(settings).forEach(([settingName, settingData]) => {
if (!!("multiple" in settingData)) {
// We need to look on config
// When a base name match config custom multiple
Object.entries(config).forEach(([multipleName, multipleData]) => {
if (!multipleName.startsWith(settingName)) return;
// Case match, add multiple
// Using base multiple model
const cloneSetting = JSON.parse(JSON.stringify(settingData));
cloneSetting["value"] = multipleData["value"];
cloneSetting["method"] = multipleData["method"];
multiples.push({ [multipleName]: cloneSetting });
});
}
});
// Append custom multiple as regular settings
// We may have only one custom setting on a group
// We will fill empty settings from a group on settings multiple component logic
for (let i = 0; i < multiples.length; i++) {
const multSetting = multiples[i];
const multName = Object.keys(multSetting).join();
const multData = multSetting[multName];
settings[multName] = multData;
}
});
return plugins;
}
// We want to remove settings that not match context
// Or even entire plugin if all his settings not match context
export function getPluginsByContext(plugins, context) {
plugins.forEach((plugin, id) => {
const settings = plugin["settings"];
Object.entries(settings).forEach(([setting, data]) => {
// Remove settings that not match context
const isContext = data["context"] === context ? true : false;
if (!isContext) delete settings[setting];
});
// Case no setting remaining, remove plugin
if (Object.keys(plugin["settings"]).length === 0) delete plugins[id];
});
// Update plugins removing empty index (deleted plugins)
return plugins.filter(Object);
}
// Filter plugins
export function getPluginsByFilter(plugins, filters) {
plugins.forEach((plugin, id) => {
for (const [key, value] of Object.entries(filters)) {
// Case no key to check
if (!(key in plugin) || value === "all") continue;
const checkType = typeof value;
let isMatch = true;
if (checkType === "string") {
const filterValue = value.toLowerCase().trim();
const checkValue = plugin[key].toLowerCase().trim();
isMatch = checkValue.includes(filterValue) ? true : false;
}
if (checkType === "boolean") {
isMatch = value === plugin[key] ? true : false;
}
// Result
if (!isMatch) plugins[id] = "";
}
});
// Update plugins removing empty index (deleted plugins)
return plugins.filter(String);
}
// Translate keys that support multiple languages
export function pluginI18n(plugins, lang, fallback) {
plugins.forEach((plugin) => {
// Main plugin info
setLangOrFallback(plugin, "name", lang, fallback);
setLangOrFallback(plugin, "description", lang, fallback);
// Each settings info
for (const [key, value] of Object.entries(plugin["settings"])) {
setLangOrFallback(value, "help", lang, fallback);
setLangOrFallback(value, "label", lang, fallback);
}
});
return plugins;
}
function setLangOrFallback(obj, key, lang, fallback) {
try {
if (!!(lang in obj[key])) {
obj[key] = obj[key][lang];
}
} catch (err) {}
// Case didn't find lang, we will get fallback (english)
try {
if (!!(fallback in obj[key])) {
obj[key] = obj[key][fallback];
}
} catch (err) {}
}
export function getRemainFromFilter(filterPlugins) {
const remainPlugins = [];
filterPlugins.forEach((item) => {
item["isMatchFilter"] ? remainPlugins.push(item.name) : false;
});
return remainPlugins;
}

View file

@ -0,0 +1,37 @@
// Filter reports
export function getReportsByFilter(reports, filters) {
reports.forEach((report, id) => {
let isMatch = true;
// Search filter
if (
(filters.search &&
!report.title.toLowerCase().includes(filters.search.toLowerCase())) ||
!report.description.toLowerCase().includes(filters.search.toLowerCase())
)
isMatch = false;
// Select filters
const selectFilters = ["country", "method", "status", "reason"];
selectFilters.forEach((filter) => {
if (
filters[filter] !== "all" &&
!report[filter].toLowerCase().includes(filters[filter].toLowerCase())
)
isMatch = false;
});
report["isMatchFilter"] = isMatch;
});
// Update reports
return reports;
}
export function getSelectList(baseItems, loopArr, keyCheck) {
const arr = baseItems;
loopArr.forEach((item) => {
if (arr.indexOf(item[keyCheck]) === -1) arr.push(item[keyCheck]);
});
return arr;
}

View file

@ -0,0 +1,61 @@
// Filter services
export function getServicesByFilter(services, filters, details) {
const result = {};
for (const [key, plugins] of Object.entries(services)) {
let isMatch = true;
// Check keyword
if (filters.servName && !key.includes(filters.servName)) {
isMatch = false;
}
// Check is draft
if (
filters.isDraft !== "all" &&
filters.isDraft !== services[key]["is_draft"]
) {
isMatch = false;
}
// Check method
if (filters.servMethod !== "all") {
plugins.forEach((plugin) => {
if (plugin.id !== "general") return;
const method = plugin.settings.SERVER_NAME.method;
if (method !== filters.servMethod) isMatch = false;
});
}
// Remove details with "all" filter
const filteredDetails = details.filter(
(detail) => filters[detail.id] !== "all",
);
filteredDetails.forEach((detail) => {
plugins.forEach((plugin) => {
if (plugin.id !== detail.id) return;
const isSetting =
plugin.settings[detail.setting].value === "yes" ? "true" : "false";
if (isSetting !== filters[detail.id]) isMatch = false;
});
});
result[key] = isMatch;
}
return result;
}
// Get methods
export function getServicesMethods(services) {
const methods = [];
for (const [key, plugins] of Object.entries(services)) {
plugins.forEach((plugin) => {
if (plugin.id !== "general") return;
const method = plugin.settings.SERVER_NAME.method;
if (!methods.includes(method)) methods.push(method);
});
}
return methods;
}

View file

@ -0,0 +1,166 @@
export function getModes() {
return ["AUTOCONF_MODE", "SWARM_MODE", "KUBERNETES_MODE"];
}
export function getMethodList() {
return ["all", "ui", "default", "scheduler"];
}
export function getDefaultMethod() {
return "default";
}
// Filter plugins by settings
export function getSettingsByFilter(plugins, filters) {
plugins.forEach((plugin, id) => {
const settings = plugin["settings"];
// We will check if all settings failed to determine plugin display
const settingsNum = Object.keys(settings).length;
let noMatchCount = 0;
Object.entries(settings).forEach(([setting, data]) => {
// Check if setting match
let isMatch = true;
// Look for every filter
for (const [key, value] of Object.entries(filters)) {
// Case one filter already fail
if (!isMatch) continue;
// Case nothing to check
if ((!(key in data) && key !== "keyword") || value === "all") {
settings[setting]["isMatchFilter"] = true;
continue;
}
const checkType = typeof value;
// Case filter is string like input or select
if (checkType === "string") {
const filterValue = value.toLowerCase().trim();
// Case keyword filter, check multiple keys
if (key === "keyword") {
const label = !!("label" in data)
? data["label"].toLowerCase().trim()
: "";
const help = !!("help" in data)
? data["help"].toLowerCase().trim()
: "";
isMatch =
label.includes(filterValue) || help.includes(filterValue)
? true
: false;
}
// Case individual filter like method
if (key !== "keyword") {
const settingValue = data[key].toLowerCase().trim();
isMatch = settingValue.includes(filterValue) ? true : false;
}
}
// Case filter is checkbox-like
if (checkType === "boolean") {
isMatch = data[key] === value ? true : false;
}
}
// After every filter check
settings[setting]["isMatchFilter"] = isMatch;
isMatch ? true : noMatchCount++;
});
// Case no settings match, hide plugin
plugin["isMatchFilter"] = settingsNum === noMatchCount ? false : true;
});
// Update plugins removing empty index (deleted plugins)
return plugins;
}
// Keep only simple settings for specific plugin
export function getSettingsSimple(settings) {
Object.entries(settings).forEach(([setting, data]) => {
// Remove settings that are multiple
if (!!("multiple" in data)) delete settings[setting];
});
return settings;
}
// Keep only multiple settings for specific plugin
export function getSettingsMultiple(settings) {
Object.entries(settings).forEach(([setting, data]) => {
// Remove settings that aren't multiple
if (!("multiple" in data)) delete settings[setting];
});
return settings;
}
// For a specific plugin, loop on settings
// Get the number of different multiple names
// Move multiple settings from plugin.settings to plugin.multiples
// Every settings is order by multiple name like plugin.multiples.name = {settings}
export function getSettingsMultipleList(settings) {
// Check to keep only multiple settings
const multipleSettings = getSettingsMultiple(settings);
// Case no multiple settings
if (Object.keys(multipleSettings).length === 0) return false;
// Case multiple, create better list
const multiples = {};
const multGroups = {};
// Get group names and base data
// Match only when end by _num
const regex = new RegExp(/(_\d*).$/gm);
Object.entries(multipleSettings).forEach(([setting, data]) => {
if (!!("multiple" in data)) {
// Add name group if doesn't exist
const multName = data["multiple"];
if (!(multName in multGroups)) multGroups[multName] = {};
// Add setting if base one
const suffix =
setting.match(regex) === null ? null : setting.match(regex).join();
// Case base
if (!suffix && !("base" in multGroups[multName]))
multGroups[multName]["base"] = {};
if (!suffix) return (multGroups[multName]["base"][setting] = data);
const suffixNum = suffix.replace("_", "");
if (suffix && !(suffixNum in multGroups[multName]))
multGroups[multName][suffixNum] = {};
if (suffix) multGroups[multName][suffixNum][setting] = data;
}
});
// Case no multiple group
if (multGroups.length === 0) return false;
// Some group can have only few settings custom
// We have to fill missing settings using base data
Object.entries(multGroups).forEach(([groupName, groupSettings]) => {
// Base to compare
const baseSettings = groupSettings["base"];
const baseLength = Object.keys(baseSettings).length;
Object.entries(groupSettings).forEach(([settingName, settings]) => {
// Stop if base itself or already fill
if (settingName === "base" || Object.keys(settings).length === baseLength)
return;
// Else, check for every setting if exist, if not create it
Object.entries(settings).forEach(([settingName, data]) => {
const suffix = settingName.match(regex).join();
Object.entries(baseSettings).forEach(
([baseSettingName, baseSettingData]) => {
// Case setting match base
if (settingName.startsWith(baseSettingName)) return;
// Case not, create
settings[`${baseSettingName}${suffix}`] = baseSettingData;
},
);
});
});
});
return multGroups;
}

View file

@ -0,0 +1,22 @@
// Determine the tab index for each component
const bannerIndex = "-1";
const footerIndex = "0";
const menuIndex = "1";
const langIndex = "2";
const menuFloatIndex = "3";
const refreshIndex = "4";
const feedbackIndex = "5";
const newsIndex = "6";
const contentIndex = "7";
export {
bannerIndex,
menuIndex,
menuFloatIndex,
langIndex,
refreshIndex,
feedbackIndex,
newsIndex,
footerIndex,
contentIndex,
};

View file

@ -12,8 +12,14 @@ export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"@store": resolve(__dirname, "./src/store"),
"@utils": resolve(__dirname, "./src/utils"),
"@layouts": resolve(__dirname, "./src/layouts"),
"@pages": resolve(__dirname, "./src/pages"),
"@components": resolve(__dirname, "./src/components"),
"@assets": resolve(__dirname, "./src/assets"),
"@lang": resolve(__dirname, "./src/lang"),
"@public": resolve(__dirname, "./public"),
},
},
build: {

View file

@ -1,35 +1,14 @@
from flask import Flask, render_template
from flask import Blueprint
from flask import redirect, url_for
app = Flask(__name__)
# I want to setup templates and static files
app = Flask(__name__, template_folder="templates", static_folder="static")
templates = Blueprint("static", __name__, template_folder="static/templates")
assets = Blueprint("assets", __name__, static_folder="static/assets")
images = Blueprint("images", __name__, static_folder="static/images")
style = Blueprint("style", __name__, static_folder="static/css")
js = Blueprint("js", __name__, static_folder="static/js")
app.register_blueprint(templates)
app.register_blueprint(assets)
app.register_blueprint(images)
app.register_blueprint(style)
app.register_blueprint(js)
app = Flask(__name__, template_folder="templates", static_url_path="", static_folder="static")
@app.route("/", methods=['GET', 'POST'])
def render_index():
# redirect to test
return redirect(url_for('render_test'))
@app.route("/test", methods=['GET', 'POST'])
def render_test():
return render_template("test.html", flask_data="Title from Flask !")
@app.route("/test2", methods=['GET', 'POST'])
def render_test2():
return render_template("test.html")
return render_template("home.html", flask_data="Title from Flask !")
app.debug = True