fix easy mode + refactor builder + add widget py

This commit is contained in:
Jordan Blasenhauer 2024-07-22 15:55:06 +02:00
parent e8b2ba95bc
commit ab1208d7f2
4 changed files with 475 additions and 193 deletions

171
src/ui/builder.py Normal file
View file

@ -0,0 +1,171 @@
import base64
import json
def stat_widget(link, containerColums, title, subtitle, subtitle_color, stat, icon_name):
"""Return a valid format to render a Stat widget"""
return {
"type": "card",
"link": link,
"containerColumns": containerColums,
"widgets": [
{
"type": "Stat",
"data": {
"title": title,
"subtitle": subtitle,
"subtitleColor": subtitle_color,
"stat": stat,
"iconName": icon_name,
},
}
],
}
def instance_widget(containerColumns, pairs, status, title, buttons):
"""Return a valid format to render an Instance widget"""
return {
"type": "card",
"containerColumns": containerColumns,
"widgets": [
{
"type": "Instance",
"data": {
"pairs": pairs,
"status": status,
"title": title,
"buttons": buttons,
},
}
],
}
def home_builder(data):
"""
It returns the needed format from data to render the home page in JSON format for the Vue.js builder
"""
version_card = stat_widget(
link="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro",
containerColums={"pc": 4, "tablet": 6, "mobile": 12},
title="home_version",
subtitle=(
"home_all_features_available"
if data.get("is_pro_version")
else (
"home_awaiting_compliance"
if data.get("pro_status") == "active" and data.get("pro_overlapped")
else (
"home_renew_license"
if data.get("pro_status") == "expired"
else "home_talk_to_team" if data.get("pro_status") == "suspended" else "home_upgrade_to_pro"
)
)
),
subtitle_color="success" if data.get("is_pro_version") else "warning",
stat=(
"home_pro"
if data.get("is_pro_version")
else (
"home_pro_locked"
if data.get("pro_status") == "active" and data.get("pro_overlapped")
else "home_expired" if data.get("pro_status") == "expired" else "home_suspended" if data.get("pro_status") == "suspended" else "home_free"
)
),
icon_name="crown" if data.get("is_pro_version") else "key",
)
version_num_card = stat_widget(
link="https://github.com/bunkerity/bunkerweb",
containerColums={"pc": 4, "tablet": 6, "mobile": 12},
title="home_version_number",
subtitle=(
"home_couldnt_find_remote"
if not data.get("remote_version")
else "home_latest_version" if data.get("remote_version") and data.get("check_version") else "home_update_available"
),
subtitle_color=("error" if not data.get("remote_version") else "success" if data.get("remote_version") and data.get("check_version") else "warning"),
stat=data.get("version"),
icon_name="wire",
)
instances_card = stat_widget(
link="instances",
containerColums={"pc": 4, "tablet": 6, "mobile": 12},
title="home_instances",
subtitle="home_total_number",
subtitle_color="info",
stat=data.get("instances_number"),
icon_name="box",
)
services_card = stat_widget(
link="services",
containerColums={"pc": 4, "tablet": 6, "mobile": 12},
title="home_services",
subtitle="home_all_methods_included",
subtitle_color="info",
stat=data.get("services_number"),
icon_name="disk",
)
plugins_card = stat_widget(
link="plugins",
containerColums={"pc": 4, "tablet": 6, "mobile": 12},
title="home_plugins",
subtitle="home_errors_found" if data.get("plugins_errors") > 0 else "home_no_error",
subtitle_color="error" if data.get("plugins_errors") > 0 else "success",
stat="42",
icon_name="puzzle",
)
builder = [version_card, version_num_card, instances_card, services_card, plugins_card]
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")
def instances_builder(instances: list):
"""
It returns the needed format from data to render the instances page in JSON format for the Vue.js builder
"""
builder = []
for instance in instances:
# setup actions buttons
actions = (
["restart", "stop"]
if instance._type == "local" and instance.health
else (
["reload", "stop"]
if not instance._type == "local" and instance.health
else ["start"] if instance._type == "local" and not instance.health else []
)
)
buttons = [
{
"attrs": {
"data-submit-form": f"""{{"INSTANCE_ID" : "{instance._id}", "operation" : "{action}" }}""",
},
"text": f"action_{action}",
"color": "success" if action == "start" else "error" if action == "stop" else "warning",
}
for action in actions
]
instance = instance_widget(
containerColumns={"pc": 6, "tablet": 6, "mobile": 12},
pairs=[
{"key": "instances_hostname", "value": instance.hostname},
{"key": "instances_type", "value": instance._type},
{"key": "instances_status", "value": "instances_active" if instance.health else "instances_inactive"},
],
status="success" if instance.health else "error",
title=instance.name,
buttons=buttons,
)
builder.append(instance)
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")

View file

@ -8,6 +8,7 @@ import Button from "@components/Widget/Button.vue";
import Text from "@components/Widget/Text.vue";
import { v4 as uuidv4 } from "uuid";
import { useCheckPluginsValidity } from "@utils/form.js";
import { useEasyForm } from "@store/easy.js";
/**
@name Form/Easy.vue
@ -42,6 +43,8 @@ import { useCheckPluginsValidity } from "@utils/form.js";
@param {object} columns - Columns object.
*/
const easyForm = useEasyForm();
const props = defineProps({
// id && value && method
template: {
@ -78,7 +81,7 @@ const data = reactive({
function setValidity() {
const [isRegErr, isReqErr, settingErr, settingNameErr, pluginErr, id] =
useCheckPluginsValidity(data.base);
useCheckPluginsValidity(easyForm.templateUI);
data.stepErr = id;
data.isRegErr = isRegErr;
@ -86,11 +89,6 @@ function setValidity() {
data.settingErr = `"${settingNameErr}"`;
}
function updateTemplate(e) {
if (!e.target.closest("[data-easy-form-step]")) return;
useUpdateTemp(e, data.base);
}
const buttonSave = {
id: uuidv4(),
text: "action_save",
@ -116,13 +114,15 @@ const buttonNext = {
onMounted(() => {
// Restart step one every time the component is mounted
easyForm.setTemplate(props.template);
data.currStep = 0;
setValidity();
useListenTempFields(updateTemplate);
// I want updatInp to access event, data.base and the container attribut
easyForm.useListenTempFields();
});
onUnmounted(() => {
useUnlistenTempFields(updateTemplate);
easyForm.useUnlistenTempFields();
});
</script>
@ -138,7 +138,7 @@ onUnmounted(() => {
<Title type="card" :title="'dashboard_easy_mode'" />
<Subtitle type="card" :subtitle="'dashboard_easy_mode_subtitle'" />
<template v-for="(step, id) in data.base">
<template v-for="(step, id) in easyForm.templateUI">
<Container
data-is="content"
data-easy-form-step

View file

@ -0,0 +1,294 @@
import { defineStore } from "pinia";
import { ref } from "vue";
/**
@name useEasyForm
@description Store to share the template and others form data.
*/
export const useEasyForm = defineStore("easy", () => {
// Default template, don't touch it. It will be used to reset the template.
const template = ref({});
// Base template will keep every data (data that doesn't need to be render on UI but need to be there for backend).
const templateBase = ref({});
// UI template will keep the data that will be render on UI.
const templateUI = ref({});
// UI template will keep the data that will be render on UI with additionnal format like filtering.
const templateUIFormat = ref({});
const updateCount = ref(0);
/**
@name setTemplate
@description Set the template we are going to use to generate the form and update it (like adding multiples).
@param template - Template with plugins list and detail settings
*/
function setTemplate(template) {
template.value = template;
templateBase.value = template;
templateUI.value = JSON.parse(JSON.stringify(template));
templateUIFormat.value = JSON.parse(JSON.stringify(template));
}
/**
@name delMultiple
@description This function will delete a group of multiples in the template.
The way the backend is working is that to delete a group, we need to send the group name with all default values.
This function needs to be call from the multiples component parent with the template and the group name to delete.
We will update the values of the group to default values.
@param pluginId - id of the plugin on the template array.
@param multName - Input id to update
@param groupName - Input value to update
*/
function delMultiple(pluginId, multName, groupName) {
// Get the index of plugin using pluginId
const index = templateBase.value.findIndex(
(plugin) => plugin.id === pluginId
);
// For back end, we need to keep the group but updating values to default in order to delete it
for (const [key, value] of Object.entries(
templateBase.value[index].multiples[multName][groupName]
)) {
value.value = value.default;
}
// For UI, we can delete the group to avoid rendering it
delete templateUI.value[index].multiples[multName][groupName];
updateCount.value++;
}
/**
@name addMultiple
@description This function will add a group of multiple in the template with default values.
Each plugin has a key "multiples_schema" with each multiples group and their default values.
We will retrieve the wanted multiple group and add it on the "multiples" key that contains the multiples that apply to the plugin.
@param pluginId - id of the plugin on the template array.
@param multName - multiple group name to add
*/
function addMultiple(pluginId, multName) {
// Get the index of plugin using pluginId
const index = templateBase.value.findIndex(
(plugin) => plugin.id === pluginId
);
// Get the right multiple schema
const multipleSchema = JSON.parse(
JSON.stringify(templateBase.value[index]?.multiples_schema[multName])
);
const newMultiple = {};
// Get the highest id in Object.keys(plugin?.multiples[multName])
const nextGroupId =
Math.max(...Object.keys(templateBase.value[index]?.multiples[multName])) +
1;
// Set the default values as value
for (const [key, value] of Object.entries(multipleSchema)) {
value.value = value.default;
newMultiple[`${key}${nextGroupId > 0 ? "_" + nextGroupId : ""}`] = value;
}
// Add new group as first key of plugin.multiples.multName
templateBase.value[index].multiples[multName][nextGroupId] = newMultiple;
// We need to show the new group on UI too
templateUI.value[index].multiples[multName][nextGroupId] = newMultiple;
updateCount.value++;
}
/**
@name useListenTempFields
@description This will add an handler to all needed event listeners to listen to input, select... fields in order to update the template settings.
@example
function hander(e) {
// some code before calling useUpdateTemp
if (!e.target.closest("[data-easy-form-plugin]")) return;
useUpdateTemp(e, data.base);
}
*/
function useListenTempFields() {
window.addEventListener("input", useUpdateTemp);
window.addEventListener("change", useUpdateTemp);
window.addEventListener("click", useUpdateTemp);
}
/**
@name useUnlistenTempFields
@description This will stop listening to input, select... fields. Performance optimization and avoid duplicate calls conflicts.
@example
function hander(e) {
// some code before calling useUpdateTemp
if (!e.target.closest("[data-easy-form-plugin]")) return;
useUpdateTemp(e, data.base);
}
*/
function useUnlistenTempFields() {
window.removeEventListener("change", useUpdateTemp);
window.removeEventListener("click", useUpdateTemp);
}
/**
@name useUpdateTemp
@description This function will check if the target is a setting input-like field.
In case it is, it will get the id and value for each field case, this will allow to update the template settings.
@example
template = [
{
name: "test",
settings: {
test: {
required: true,
value: "",
pattern: "^[a-zA-Z0-9]*$",
},
},
},
];
@param e - Event object, get it by default in the event listener.
@param templates - Array of templates to update
*/
function useUpdateTemp(e, templates) {
templates = [templateBase.value, templateUI.value];
// Avoid event that are not in the form
if (!e.target.closest("[data-easy-form]")) return;
// Avoid multiple calls on datepicker
if (e.type === "change" && e.target.tagName === "INPUT") return;
// Avoid multiple calls on checkbox
if (
e.type !== "input" &&
e.target.tagName === "INPUT" &&
e.target.type === "checkbox"
)
return;
// Wait some ms that previous update logic is done like datepicker
setTimeout(() => {
let inpId, inpValue;
// Case target is input (a little different for datepicker)
if (e.target.tagName === "INPUT") {
inpId = e.target.id;
inpValue = e.target.hasAttribute("data-timestamp")
? e.target.getAttribute("data-timestamp")
: e.target.value;
}
// Case target is select
if (
e.target.closest("[data-field-container]") &&
e.target.hasAttribute("data-setting-id") &&
e.target.hasAttribute("data-setting-value")
) {
inpId = e.target.getAttribute("data-setting-id");
inpValue = e.target.getAttribute("data-setting-value");
}
// Case target is not an input-like
if (!inpId) return;
// update settings
useUpdateTempSettings(templates, inpId, inpValue, e.target);
useUpdateTempMultiples(templates, inpId, inpValue, e.target);
}, 50);
}
/**
@name useUpdateTempSettings
@description This function will loop on template settings in order to update the setting value.
This will check each plugin.settings (what I call regular) instead of other type of settings like multiples (in plugin.multiples).
This function needs to be call in useUpdateTemp.
@param templates - Templates array with plugins list and detail settings
@param inpId - Input id to update
@param inpValue - Input value to update
*/
function useUpdateTempSettings(templates, inpId, inpValue, target) {
// Case get data-group attribut, this is not a regular setting
if (target.closest("[data-group]")) return;
for (let i = 0; i < templates.length; i++) {
const template = templates[i];
// Try to update settings
let isSettingUpdated = false;
for (let i = 0; i < template.length; i++) {
const plugin = template[i];
const settings = plugin?.settings;
if (!settings) continue;
for (const [key, value] of Object.entries(settings)) {
if (value.id === inpId) {
value.value = inpValue;
isSettingUpdated = true;
break;
}
}
if (isSettingUpdated) break;
}
}
}
/**
@name useUpdateTempMultiples
@description This function will loop on template multiples in order to update the setting value.
This will check each plugin.multiples that can be found in the template.
This function needs to be call in useUpdateTemp.
@param templates - Templates array with plugins list and detail settings
@param inpId - Input id to update
@param inpValue - Input value to update
*/
function useUpdateTempMultiples(templates, inpId, inpValue, target) {
// Case get data-group attribut, this is not a regular setting
if (!target.closest("[data-group='multiple']")) return;
const multName =
target
.closest("[data-group='multiple']")
.getAttribute("data-mult-name") || "";
const groupName =
target
.closest("[data-group='multiple']")
.getAttribute("data-group-name") || "";
for (let i = 0; i < templates.length; i++) {
const template = templates[i];
// Check at the same time the inpId without prefix group he is part of
// And try to update an existing inpId
// Case we found the inpId, we update the value
// Case we didn't find existing inpId, we create a new one
let isSettingUpdated = false;
for (let i = 0; i < template.length; i++) {
const plugin = template[i];
const multiples = plugin?.multiples;
// Case no multiples, continue
if (!multiples || Object.keys(multiples).length <= 0) continue;
// Check if can find mult name in multiples
if (!(multName in multiples)) continue;
// Check if can find group name in multiples
if (!(groupName in multiples[multName])) continue;
const settings = multiples[multName][groupName];
for (const [key, value] of Object.entries(settings)) {
if (value.id !== inpId) continue;
value.value = inpValue;
isSettingUpdated = true;
break;
}
if (isSettingUpdated) break;
}
}
}
/**
@name $reset
@description Will reset the template to the original one using the default template. The default template need to be set once.
*/
function $reset() {
templateBase.value = template.value;
templateUI.value = template.value;
updateCount.value++;
}
return {
templateBase,
templateUI,
templateUIFormat,
setTemplate,
addMultiple,
delMultiple,
useListenTempFields,
useUnlistenTempFields,
$reset,
};
});

View file

@ -11,7 +11,7 @@ from sys import path as sys_path, modules as sys_modules
from pathlib import Path
from typing import Union
from uuid import uuid4
from builder import home_builder, instances_builder
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("api",), ("db",))]:
if deps_path not in sys_path:
sys_path.append(deps_path)
@ -676,135 +676,6 @@ def totp():
return render_template("totp.html")
def home_builder(data):
"""
It returns the home page in JSON format for the Vue.js builder
"""
version_card = {
"type": "card",
"link": "https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro",
"containerColumns": {"pc": 4, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Stat",
"data": {
"title": "home_version",
"subtitle": (
"home_all_features_available"
if data.get("is_pro_version")
else (
"home_awaiting_compliance"
if data.get("pro_status") == "active" and data.get("pro_overlapped")
else (
"home_renew_license"
if data.get("pro_status") == "expired"
else "home_talk_to_team" if data.get("pro_status") == "suspended" else "home_upgrade_to_pro"
)
)
),
"subtitleColor": "success" if data.get("is_pro_version") else "warning",
"stat": (
"home_pro"
if data.get("is_pro_version")
else (
"home_pro_locked"
if data.get("pro_status") == "active" and data.get("pro_overlapped")
else (
"home_expired"
if data.get("pro_status") == "expired"
else "home_suspended" if data.get("pro_status") == "suspended" else "home_free"
)
)
),
"iconName": "crown" if data.get("is_pro_version") else "key",
},
}
],
}
version_num_card = {
"type": "card",
"link": "https://github.com/bunkerity/bunkerweb",
"containerColumns": {"pc": 4, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Stat",
"data": {
"title": "home_version_number",
"subtitle": (
"home_couldnt_find_remote"
if not data.get("remote_version")
else "home_latest_version" if data.get("remote_version") and data.get("check_version") else "home_update_available"
),
"subtitleColor": (
"error" if not data.get("remote_version") else "success" if data.get("remote_version") and data.get("check_version") else "warning"
),
"stat": data.get("version"),
"iconName": "wire",
},
}
],
}
instances_card = {
"type": "card",
"link": "instances",
"containerColumns": {"pc": 4, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Stat",
"data": {
"title": "home_instances",
"subtitle": "home_total_number",
"subtitleColor": "info",
"stat": data.get("instances_number"),
"iconName": "box",
},
}
],
}
services_card = {
"type": "card",
"link": "services",
"containerColumns": {"pc": 4, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Stat",
"data": {
"title": "home_services",
"subtitle": "home_all_methods_included",
"subtitleColor": "info",
"stat": data.get("services_number"),
"iconName": "disk",
},
}
],
}
plugins_card = {
"type": "card",
"link": "plugins",
"containerColumns": {"pc": 4, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Stat",
"data": {
"title": "home_plugins",
"subtitle": "home_errors_found" if data.get("plugins_errors") > 0 else "home_no_error",
"subtitleColor": "error" if data.get("plugins_errors") > 0 else "success",
"stat": "42",
"iconName": "puzzle",
},
}
],
}
builder = [version_card, version_num_card, instances_card, services_card, plugins_card]
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")
@app.route("/home")
@login_required
def home():
@ -1035,60 +906,6 @@ def account():
totp_qr_image=totp_qr_image,
)
def instances_builder(instances: list):
"""
It returns the home page in JSON format for the Vue.js builder
"""
builder = []
for instance in instances:
# setup actions buttons
actions = (
["restart", "stop"]
if instance._type == "local" and instance.health
else (
["reload", "stop"]
if not instance._type == "local" and instance.health
else ["start"] if instance._type == "local" and not instance.health else []
)
)
buttons = [
{
"attrs": {
"data-submit-form": f"""{{"INSTANCE_ID" : "{instance._id}", "operation" : "{action}" }}""",
},
"text": f"action_{action}",
"color": "success" if action == "start" else "error" if action == "stop" else "warning",
}
for action in actions
]
component = {
"type": "card",
"containerColumns": {"pc": 6, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Instance",
"data": {
"pairs": [
{"key": "instances_hostname", "value": instance.hostname},
{"key": "instances_type", "value": instance._type},
{"key": "instances_status", "value": "instances_active" if instance.health else "instances_inactive"},
],
"status": "success" if instance.health else "error",
"title": instance.name,
"buttons": buttons,
},
}
],
}
builder.append(component)
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")
@app.route("/instances", methods=["GET", "POST"])
@login_required
def instances():