add services page + update builder + add widgets

* update build.py to make services build page work
* add services_builder to builder.py
* move widgets to a dedicated widgets.py script
* update main.py to deliver needed data
* add new service button on services page
* add table with use state of some main plugins
* update i18n
* update table component to work with modal
* fix issues with table cell
This commit is contained in:
Jordan Blasenhauer 2024-07-31 10:24:49 +02:00
parent 7ecf6544b9
commit ce1d8f028f
22 changed files with 1130 additions and 556 deletions

View file

@ -2,69 +2,7 @@ import base64
import json
import copy
from typing import Union
def title_widget(title):
return {
"type": "Title",
"data": {"title": title},
}
def table_widget(positions, header, items, filters, minWidth, title):
return {
"type": "Table",
"data": {
"title": title,
"minWidth": minWidth,
"header": header,
"positions": positions,
"items": items,
"filters": filters,
},
}
def stat_widget(
link: str, containerColums: dict, title: Union[str, int], subtitle: Union[str, int], subtitle_color: str, stat: Union[str, int], icon_name: str
) -> dict:
"""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: dict, pairs: list[dict], status: str, title: Union[str, int], buttons: list[dict]) -> dict:
"""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,
},
}
],
}
from widgets import title_widget, table_widget, stat_widget, instance_widget
def home_builder(data: dict) -> str:
@ -854,3 +792,379 @@ def jobs_builder(jobs):
]
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")
def services_settings(settings: dict) -> dict:
# deep copy settings dict
settings = settings.copy()
# remove "SERVER_NAME" and "IS_DRAFT" key
settings.pop("SERVER_NAME", None)
settings.pop("IS_DRAFT", None)
# Create table with settings remaining keys
settings_table_items = []
for key, value in settings.items():
format_key = key.replace("USE_", "").replace("_", " ")
settings_table_items.append(
[
{
"type": "Text",
"data": {"text": format_key},
},
{
"type": "Icons",
"data": {
"iconName": "check" if value.get("value") == "yes" else "cross",
},
},
]
)
table = table_widget(
positions=[8, 4],
header=["services_settings_table_name", "services_settings_table_status"],
items=settings_table_items,
filters=[],
minWidth="",
title="services_settings_table_title",
)
return table
def services_action(
server_name: str = "",
operation: str = "",
title: str = "",
subtitle: str = "",
additionnal: str = "",
is_draft: Union[bool, None] = None,
service: dict = None,
) -> dict:
buttons = [
{
"id": f"close-service-btn-{server_name}",
"text": "action_close",
"disabled": False,
"color": "close",
"size": "normal",
"attrs": {"data-close-modal": ""},
},
]
if operation == "delete":
buttons.append(
{
"id": f"{operation}-service-btn-{server_name}",
"text": f"action_{operation}",
"disabled": False,
"color": "delete",
"size": "normal",
"attrs": {
"data-submit-form": f"""{{"SERVER_NAME" : {server_name}, "operation" : "{operation}" }}""",
},
},
)
if operation == "draft":
draft_value = "yes" if is_draft else "no"
buttons.append(
{
"id": f"{operation}-service-btn-{server_name}",
"text": "action_switch",
"disabled": False,
"color": "success",
"size": "normal",
"attrs": {
"data-submit-form": f"""{{"SERVER_NAME" : {server_name}, "OLD_SERVER_NAME" : {server_name}, "operation" : "edit", "IS_DRAFT" : {draft_value} }}""",
},
},
)
content = [
{
"type": "Title",
"data": {
"title": title,
},
},
]
if subtitle:
content.append(
{
"type": "Text",
"data": {
"text": subtitle,
},
},
)
if additionnal:
content.append(
{
"type": "Text",
"data": {
"bold": True,
"text": additionnal,
},
}
)
if operation == "plugins":
settings = services_settings(service)
content.append(settings)
if operation == "delete":
content.append(
{
"type": "Text",
"data": {
"text": "",
"bold": True,
"text": server_name,
},
}
)
if operation == "edit" or operation == "create":
modes = ("easy", "advanced", "raw")
mode_buttons = []
for mode in modes:
mode_buttons.append(
{
"id": f"{operation}-service-btn-{server_name}",
"text": f"services_mode_{mode}",
"disabled": False,
"color": "info",
"size": "normal",
"attrs": {
"role": "link",
"data-link": f"services/{mode}/{server_name}",
},
},
)
content.append(
{
"type": "ButtonGroup",
"data": {"buttons": mode_buttons},
}
)
content.append(
{
"type": "ButtonGroup",
"data": {"buttons": buttons},
},
)
modal = {
"widgets": content,
}
return modal
def get_services_list(services):
data = []
for index, service in enumerate(services):
server_name = service["SERVER_NAME"]["value"]
server_method = service["SERVER_NAME"]["method"]
is_draft = True if service["IS_DRAFT"]["value"] == "yes" else False
is_deletable = False if server_method in ("autoconf", "scheduler") else True
item = []
# Get name
item.append({"name": server_name, "type": "Text", "data": {"text": server_name}})
item.append({"method": server_method, "type": "Text", "data": {"text": server_method}})
item.append(
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": f"open-modal-plugins-{index}",
"text": "plugins",
"hideText": True,
"color": "success",
"size": "normal",
"iconName": "eye",
"iconColor": "white",
"modal": services_action(
server_name=server_name,
operation="plugins",
title="services_plugins_title",
subtitle="",
service=service,
),
},
{
"attrs": {"data-server-name": server_name},
"id": f"open-modal-manage-{index}",
"text": "manage",
"hideText": True,
"color": "edit",
"size": "normal",
"iconName": "pen",
"iconColor": "white",
"modal": services_action(
server_name=server_name,
operation="edit",
title="services_edit_title",
subtitle="services_edit_subtitle",
additionnal=server_name,
),
},
{
"attrs": {"data-server-name": server_name, "data-is-draft": "yes" if is_draft else "no"},
"id": f"open-modal-draft-{index}",
"text": "draft" if is_draft else "online",
"hideText": True,
"color": "blue",
"size": "normal",
"iconName": "document" if is_draft else "globe",
"iconColor": "white",
"modal": services_action(
server_name=server_name,
operation="draft",
title="services_draft_title",
subtitle="services_draft_subtitle" if is_draft else "services_online_subtitle",
additionnal="services_draft_switch_subtitle" if is_draft else "services_online_switch_subtitle",
is_draft=is_draft,
),
},
{
"attrs": {"data-server-name": server_name},
"id": f"open-modal-delete-{index}",
"text": "delete",
"disabled": not is_deletable,
"hideText": True,
"color": "red",
"size": "normal",
"iconName": "trash",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="delete", title="services_delete_title", subtitle="services_delete_subtitle"
),
},
]
},
}
)
data.append(item)
return data
def services_builder(services):
# get method for each service["SERVER_NAME"]["method"]
methods = list(set([service["SERVER_NAME"]["method"] for service in services]))
services_list = get_services_list(services)
builder = [
{
"type": "card",
"containerColumns": {"pc": 12, "tablet": 12, "mobile": 12},
"widgets": [
title_widget("services_title"),
{
"type": "Button",
"data": {
"id": "services-new",
"text": "services_new",
"color": "success",
"size": "normal",
"iconName": "plus",
"iconColor": "white",
"modal": services_action(server_name="new", operation="create", title="services_create_title", subtitle="services_create_subtitle"),
"containerClass": "col-span-12 flex justify-center",
},
},
table_widget(
positions=[4, 4, 4],
header=[
"services_table_name",
"services_table_method",
"services_table_actions",
],
items=services_list,
filters=[
{
"filter": "table",
"filterName": "keyword",
"type": "keyword",
"value": "",
"keys": ["name"],
"field": {
"id": "services-keyword",
"value": "",
"type": "text",
"name": "services-keyword",
"label": "services_search",
"placeholder": "inp_keyword",
"isClipboard": False,
"popovers": [
{
"text": "services_search_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
{
"filter": "table",
"filterName": "method",
"type": "select",
"value": "all",
"keys": ["method"],
"field": {
"id": "services-methods",
"value": "all",
"values": methods,
"name": "services-methods",
"onlyDown": True,
"label": "services_methods",
"popovers": [
{
"text": "services_methods_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
{
"filter": "table",
"filterName": "draft",
"type": "select",
"value": "all",
"keys": ["draft"],
"field": {
"id": "services-draft",
"value": "all",
"values": ["all", "online", "draft"],
"name": "services-draft",
"onlyDown": True,
"label": "services_draft",
"popovers": [
{
"text": "services_draft_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
],
minWidth="md",
title="services_table_title",
),
],
},
]
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")

View file

@ -85,7 +85,7 @@ def move_template(folder, target_folder):
</body>
</html>"""
if "global-config" in root or "jobs" in root:
if "global-config" in root or "jobs" in root or "services" in root:
base_html = base_html.replace("data_server_builder[1:-1]", "data_server_builder")
file_path = os.path.join(root, file)

View file

@ -4,6 +4,7 @@ import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Table from "@components/Widget/Table.vue";
import Title from "@components/Widget/Title.vue";
import Button from "@components/Widget/Button.vue";
import { useEqualStr } from "@utils/global.js";
/**
@ -315,8 +316,12 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Table v-if="useEqualStr(widget.type, 'table')" v-bind="widget.data" />
<Title v-if="useEqualStr(widget.type, 'title')" v-bind="widget.data" />
<Table v-if="useEqualStr(widget.type, 'Table')" v-bind="widget.data" />
<Button
v-if="useEqualStr(widget.type, 'Button')"
v-bind="widget.data"
/>
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
</template>
</Grid>
</GridLayout>

View file

@ -39,6 +39,7 @@ import { useUUID } from "@utils/global.js";
@param {Object} [attrs={}] - List of attributs to add to the button. Some attributs will conduct to additionnal script
@param {Object|boolean} [modal=false] - We can link the button to a Modal component. We need to pass the widgets inside the modal. Button click will open the modal.
@param {string|number} [tabId=contentIndex] - The tabindex of the field, by default it is the contentIndex
@param {string} [containerClass=""] - Additionnal class to the container
*/
const props = defineProps({

View file

@ -37,19 +37,15 @@ const props = defineProps({
required: true,
},
});
const cell = reactive({
name: computed(() => props.type.toLowerCase()),
});
</script>
<template>
<Text v-if="useEqualStr(cell.name, 'Text')" v-bind="props.data" />
<Icons v-if="useEqualStr(cell.name, 'Icons')" v-bind="props.data" />
<Fields v-if="useEqualStr(cell.name, 'Fields')" v-bind="props.data" />
<Button v-if="useEqualStr(cell.name, 'Button')" v-bind="props.data" />
<Text v-if="useEqualStr(props.type, 'Text')" v-bind="props.data" />
<Icons v-if="useEqualStr(props.type, 'Icons')" v-bind="props.data" />
<Fields v-if="useEqualStr(props.type, 'Fields')" v-bind="props.data" />
<Button v-if="useEqualStr(props.type, 'Button')" v-bind="props.data" />
<ButtonGroup
v-if="useEqualStr(cell.name, 'ButtonGroup')"
v-if="useEqualStr(props.type, 'ButtonGroup')"
v-bind="props.data"
/>
</template>

View file

@ -17,6 +17,7 @@ import Subtitle from "@components/Widget/Subtitle.vue";
import Button from "@components/Widget/Button.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import MessageUnmatch from "@components/Message/Unmatch.vue";
import Table from "@components/Widget/Table.vue";
/**
@name Builder/Modal.vue
@ -209,6 +210,10 @@ const emits = defineEmits(["close"]);
v-if="useEqualStr(widget.type, 'ButtonGroup')"
v-bind="widget.data"
/>
<Table
v-if="useEqualStr(widget.type, 'Table')"
v-bind="widget.data"
/>
</template>
</Grid>
</div>

View file

@ -136,6 +136,7 @@ const table = reactive({
itemsBase: JSON.parse(JSON.stringify(props.items)),
// items that can be filtered
itemsFormat: JSON.parse(JSON.stringify(props.items)),
bodyClass: "",
});
/**
@ -180,6 +181,12 @@ watch(
onMounted(() => {
getOverflow();
setUnmatchWidth();
table.bodyClass = tableBody.value.closest("[data-is]:not([data-is='table'])")
? `table-content-${tableBody.value
.closest("[data-is]:not([data-is='table'])")
.getAttribute("data-is")}`
: "table-content";
});
</script>
@ -220,7 +227,7 @@ onMounted(() => {
:aria-hidden="!table.itemsFormat.length ? 'true' : 'false'"
data-table-body
ref="tableBody"
class="table-content"
:class="[table.bodyClass]"
>
<tr
v-for="rowId in table.rowLength"

View file

@ -264,6 +264,7 @@
"bans_table_remain": "Remain",
"bans_table_term": "Term",
"bans_table_select": "Select",
"services_new": "new service",
"services_title": "Services",
"services_table_name": "Name",
"services_table_method": "Method",
@ -275,17 +276,22 @@
"services_draft": "draft",
"services_online": "online",
"services_draft_desc": "Only show services of the chosen draft status",
"services_plugins_title": "plugins",
"services_plugins_subtitle": "Main plugins status for this service.",
"services_manage_title": "Manage settings",
"services_manage_subtitle": "Choose a mode to manage service",
"services_plugins_title": "Details",
"services_edit_title": "Edit",
"services_edit_subtitle": "Choose a mode to edit service",
"services_create_title": "create service",
"services_create_subtitle": "Choose a mode to create a new service",
"services_mode_easy": "Easy mode",
"services_mode_raw": "Raw mode",
"services_mode_advanced": "Advanced mode",
"services_draft_title": "Active status",
"services_draft_subtitle": "Service is currently in draft (configuration is not apply).",
"services_draft_switch_subtitle": "Switch to online ?",
"services_online_subtitle": "Service is currently online (configuration is apply).",
"services_online_switch_subtitle": "Switch to draft ?",
"services_delete_title": "Delete service",
"services_delete_subtitle": "Are you sure you want to delete the service below ?"
"services_delete_subtitle": "Are you sure you want to delete the service below ?",
"services_settings_table_title": "Get the activate setting state of main plugins for this service.",
"services_settings_table_name": "Plugin",
"services_settings_table_status": "Status"
}

File diff suppressed because one or more lines are too long

View file

@ -1621,10 +1621,14 @@ body {
@apply appearance-none block dark:text-gray-300 pb-1 text-left text-sm font-bold m-0 border-b border-gray-400;
}
.table-content {
.table-content-card {
@apply relative appearance-none dark:text-gray-400 block w-full rounded col-span-12 overflow-x-hidden overflow-y-auto max-h-[600px] min-h-[460px];
}
.table-content-modal {
@apply relative appearance-none dark:text-gray-400 block w-full rounded col-span-12 overflow-x-hidden overflow-y-auto max-h-[300px] min-h-[200px];
}
.table-content-item {
@apply py-2 appearance-none text-sm col-span-12 block border-b hover:bg-gray-100 dark:hover:bg-slate-700 items-center grid grid-cols-12 border-gray-300 min-h-[65px];
}

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,91 @@
"title": "services_title"
}
},
{
"type": "Button",
"data": {
"id": "services-new",
"text": "services_new",
"color": "success",
"size": "normal",
"iconName": "plus",
"iconColor": "white",
"modal": {
"widgets": [
{
"type": "Title",
"data": {
"title": "services_create_title"
}
},
{
"type": "Text",
"data": {
"text": "services_create_subtitle"
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "create-service-btn-new",
"text": "services_mode_easy",
"disabled": false,
"color": "info",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/easy/new"
}
},
{
"id": "create-service-btn-new",
"text": "services_mode_advanced",
"disabled": false,
"color": "info",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/advanced/new"
}
},
{
"id": "create-service-btn-new",
"text": "services_mode_raw",
"disabled": false,
"color": "info",
"size": "normal",
"attrs": {
"role": "link",
"data-link": "services/raw/new"
}
}
]
}
},
{
"type": "ButtonGroup",
"data": {
"buttons": [
{
"id": "close-service-btn-new",
"text": "action_close",
"disabled": false,
"color": "close",
"size": "normal",
"attrs": {
"data-close-modal": ""
}
}
]
}
}
]
},
"containerClass": "col-span-12 flex justify-center"
}
},
{
"type": "Table",
"data": {
@ -65,9 +150,147 @@
}
},
{
"type": "Text",
"type": "Table",
"data": {
"text": "services_plugins_subtitle"
"title": "services_settings_table_title",
"minWidth": "",
"header": [
"services_settings_table_name",
"services_settings_table_status"
],
"positions": [
8,
4
],
"items": [
[
{
"type": "Text",
"data": {
"text": "REVERSE PROXY"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "SERVE FILES"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "REMOTE PHP"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "AUTO LETS ENCRYPT"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "CUSTOM SSL"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "MODSECURITY"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "BAD BEHAVIOR"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "LIMIT REQ"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "DNSBL"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
]
],
"filters": []
}
},
{
@ -106,13 +329,20 @@
{
"type": "Title",
"data": {
"title": "services_manage_title"
"title": "services_edit_title"
}
},
{
"type": "Text",
"data": {
"text": "services_manage_subtitle"
"text": "services_edit_subtitle"
}
},
{
"type": "Text",
"data": {
"bold": true,
"text": "app1.example.com"
}
},
{
@ -120,7 +350,7 @@
"data": {
"buttons": [
{
"id": "manage-service-btn-app1.example.com",
"id": "edit-service-btn-app1.example.com",
"text": "services_mode_easy",
"disabled": false,
"color": "info",
@ -131,7 +361,7 @@
}
},
{
"id": "manage-service-btn-app1.example.com",
"id": "edit-service-btn-app1.example.com",
"text": "services_mode_advanced",
"disabled": false,
"color": "info",
@ -142,7 +372,7 @@
}
},
{
"id": "manage-service-btn-app1.example.com",
"id": "edit-service-btn-app1.example.com",
"text": "services_mode_raw",
"disabled": false,
"color": "info",
@ -192,7 +422,7 @@
{
"type": "Title",
"data": {
"title": "services_online"
"title": "services_draft_title"
}
},
{
@ -223,7 +453,7 @@
}
},
{
"id": "edit-service-btn-app1.example.com",
"id": "draft-service-btn-app1.example.com",
"text": "action_switch",
"disabled": false,
"color": "success",
@ -341,9 +571,147 @@
}
},
{
"type": "Text",
"type": "Table",
"data": {
"text": "services_plugins_subtitle"
"title": "services_settings_table_title",
"minWidth": "",
"header": [
"services_settings_table_name",
"services_settings_table_status"
],
"positions": [
8,
4
],
"items": [
[
{
"type": "Text",
"data": {
"text": "REVERSE PROXY"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "SERVE FILES"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "REMOTE PHP"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "AUTO LETS ENCRYPT"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "CUSTOM SSL"
}
},
{
"type": "Icons",
"data": {
"iconName": "cross"
}
}
],
[
{
"type": "Text",
"data": {
"text": "MODSECURITY"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "BAD BEHAVIOR"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "LIMIT REQ"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
],
[
{
"type": "Text",
"data": {
"text": "DNSBL"
}
},
{
"type": "Icons",
"data": {
"iconName": "check"
}
}
]
],
"filters": []
}
},
{
@ -382,13 +750,20 @@
{
"type": "Title",
"data": {
"title": "services_manage_title"
"title": "services_edit_title"
}
},
{
"type": "Text",
"data": {
"text": "services_manage_subtitle"
"text": "services_edit_subtitle"
}
},
{
"type": "Text",
"data": {
"bold": true,
"text": "www.example.com"
}
},
{
@ -396,7 +771,7 @@
"data": {
"buttons": [
{
"id": "manage-service-btn-www.example.com",
"id": "edit-service-btn-www.example.com",
"text": "services_mode_easy",
"disabled": false,
"color": "info",
@ -407,7 +782,7 @@
}
},
{
"id": "manage-service-btn-www.example.com",
"id": "edit-service-btn-www.example.com",
"text": "services_mode_advanced",
"disabled": false,
"color": "info",
@ -418,7 +793,7 @@
}
},
{
"id": "manage-service-btn-www.example.com",
"id": "edit-service-btn-www.example.com",
"text": "services_mode_raw",
"disabled": false,
"color": "info",
@ -468,7 +843,7 @@
{
"type": "Title",
"data": {
"title": "services_draft"
"title": "services_draft_title"
}
},
{
@ -499,7 +874,7 @@
}
},
{
"id": "edit-service-btn-www.example.com",
"id": "draft-service-btn-www.example.com",
"text": "action_switch",
"disabled": false,
"color": "success",

View file

@ -55,8 +55,51 @@ def table_widget(positions, header, items, filters, minWidth, title):
}
def services_settings(settings: dict) -> dict:
# deep copy settings dict
settings = settings.copy()
# remove "SERVER_NAME" and "IS_DRAFT" key
settings.pop("SERVER_NAME", None)
settings.pop("IS_DRAFT", None)
# Create table with settings remaining keys
settings_table_items = []
for key, value in settings.items():
format_key = key.replace("USE_", "").replace("_", " ")
settings_table_items.append(
[
{
"type": "Text",
"data": {"text": format_key},
},
{
"type": "Icons",
"data": {
"iconName": "check" if value.get("value") == "yes" else "cross",
},
},
]
)
table = table_widget(
positions=[8, 4],
header=["services_settings_table_name", "services_settings_table_status"],
items=settings_table_items,
filters=[],
minWidth="",
title="services_settings_table_title",
)
return table
def services_action(
server_name: str = "", operation: str = "", title: str = "", subtitle: str = "", additionnal: str = "", is_draft: Union[bool, None] = None
server_name: str = "",
operation: str = "",
title: str = "",
subtitle: str = "",
additionnal: str = "",
is_draft: Union[bool, None] = None,
service: dict = None,
) -> dict:
buttons = [
@ -84,7 +127,7 @@ def services_action(
},
)
if operation == "edit":
if operation == "draft":
draft_value = "yes" if is_draft else "no"
buttons.append(
{
@ -106,14 +149,18 @@ def services_action(
"title": title,
},
},
{
"type": "Text",
"data": {
"text": subtitle,
},
},
]
if subtitle:
content.append(
{
"type": "Text",
"data": {
"text": subtitle,
},
},
)
if additionnal:
content.append(
{
@ -125,6 +172,10 @@ def services_action(
}
)
if operation == "plugins":
settings = services_settings(service)
content.append(settings)
if operation == "delete":
content.append(
{
@ -137,7 +188,7 @@ def services_action(
}
)
if operation == "manage":
if operation == "edit" or operation == "create":
modes = ("easy", "advanced", "raw")
mode_buttons = []
for mode in modes:
@ -202,7 +253,11 @@ def get_services_list(services):
"iconName": "eye",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="plugins", title="services_plugins_title", subtitle="services_plugins_subtitle"
server_name=server_name,
operation="plugins",
title="services_plugins_title",
subtitle="",
service=service,
),
},
{
@ -215,7 +270,11 @@ def get_services_list(services):
"iconName": "pen",
"iconColor": "white",
"modal": services_action(
server_name=server_name, operation="manage", title="services_manage_title", subtitle="services_manage_subtitle"
server_name=server_name,
operation="edit",
title="services_edit_title",
subtitle="services_edit_subtitle",
additionnal=server_name,
),
},
{
@ -229,8 +288,8 @@ def get_services_list(services):
"iconColor": "white",
"modal": services_action(
server_name=server_name,
operation="edit",
title="services_draft" if is_draft else "services_online",
operation="draft",
title="services_draft_title",
subtitle="services_draft_subtitle" if is_draft else "services_online_subtitle",
additionnal="services_draft_switch_subtitle" if is_draft else "services_online_switch_subtitle",
is_draft=is_draft,
@ -272,6 +331,19 @@ def services_builder(services):
"containerColumns": {"pc": 12, "tablet": 12, "mobile": 12},
"widgets": [
title_widget("services_title"),
{
"type": "Button",
"data": {
"id": "services-new",
"text": "services_new",
"color": "success",
"size": "normal",
"iconName": "plus",
"iconColor": "white",
"modal": services_action(server_name="new", operation="create", title="services_create_title", subtitle="services_create_subtitle"),
"containerClass": "col-span-12 flex justify-center",
},
},
table_widget(
positions=[4, 4, 4],
header=[
@ -353,7 +425,7 @@ def services_builder(services):
title="services_table_title",
),
],
}
},
]
return builder

File diff suppressed because one or more lines are too long

View file

@ -50,6 +50,7 @@ export default defineConfig({
"./dashboard/pages/global-config/index.html"
),
jobs: resolve(__dirname, "./dashboard/pages/jobs/index.html"),
services: resolve(__dirname, "./dashboard/pages/services/index.html"),
},
},
},

View file

@ -10,7 +10,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, global_config_builder, jobs_builder
from builder import home_builder, instances_builder, global_config_builder, jobs_builder, services_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:
@ -1052,124 +1052,6 @@ def get_service_data():
return config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged
# @app.route("/services", methods=["GET", "POST"])
# @login_required
# def services():
# if request.method == "POST":
# if DB.readonly:
# return handle_error("Database is in read-only mode", "services")
# verify_data_in_form(
# data={"operation": ("edit", "new", "delete")},
# err_message="Invalid operation parameter on /services.",
# redirect_url="services",
# )
# config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged = get_service_data()
# if request.form["operation"] == "edit":
# if is_draft_unchanged and len(variables) == 1 and "SERVER_NAME" in variables and server_name == old_server_name:
# return handle_error("The service was not edited because no values were changed.", "services", True)
# if request.form["operation"] == "new" and not variables:
# return handle_error("The service was not created because all values had the default value.", "services", True)
# # Delete
# if request.form["operation"] == "delete":
# is_service = app.config["CONFIG"].check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
# if not is_service:
# error_message(f"Error while deleting the service {request.form['SERVER_NAME']}")
# if config.get(f"{request.form['SERVER_NAME'].split(' ')[0]}_SERVER_NAME", {"method": "scheduler"})["method"] != "ui":
# return handle_error("The service cannot be deleted because it has not been created with the UI.", "services", True)
# db_metadata = DB.get_metadata()
# def update_services(threaded: bool = False):
# wait_applying()
# manage_bunkerweb(
# "services",
# variables,
# old_server_name,
# variables.get("SERVER_NAME", ""),
# operation=operation,
# is_draft=is_draft,
# was_draft=was_draft,
# threaded=threaded,
# )
# ui_data = get_ui_data()
# if any(
# v
# for k, v in db_metadata.items()
# if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
# ):
# ui_data["RELOADING"] = True
# ui_data["LAST_RELOAD"] = time()
# Thread(target=update_services, args=(True,)).start()
# else:
# update_services()
# ui_data["CONFIG_CHANGED"] = True
# with LOCK:
# TMP_DATA_FILE.write_text(dumps(ui_data), encoding="utf-8")
# message = ""
# if request.form["operation"] == "new":
# message = f"Creating {'draft ' if is_draft else ''}service {variables.get('SERVER_NAME', '').split(' ')[0]}"
# elif request.form["operation"] == "edit":
# message = f"Saving configuration for {'draft ' if is_draft else ''}service {old_server_name.split(' ')[0]}"
# elif request.form["operation"] == "delete":
# message = f"Deleting {'draft ' if was_draft and is_draft else ''}service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
# return redirect(url_for("loading", next=url_for("services"), message=message))
# # Display services
# services = []
# tmp_config = DB.get_config(methods=True, with_drafts=True).copy()
# service_names = tmp_config["SERVER_NAME"]["value"].split(" ")
# table_settings = (
# "USE_REVERSE_PROXY",
# "IS_DRAFT",
# "SERVE_FILES",
# "REMOTE_PHP",
# "AUTO_LETS_ENCRYPT",
# "USE_CUSTOM_SSL",
# "USE_MODSECURITY",
# "USE_BAD_BEHAVIOR",
# "USE_LIMIT_REQ",
# "USE_DNSBL",
# "SERVER_NAME",
# )
# for service in service_names:
# service_settings = {}
# # For each needed setting, get the service value if one, else the global (value), else default value
# for setting in table_settings:
# value = tmp_config.get(f"{service}_{setting}", tmp_config.get(setting, {"value": None}))["value"]
# method = tmp_config.get(f"{service}_{setting}", tmp_config.get(setting, {"method": None}))["method"]
# is_global = tmp_config.get(f"{service}_{setting}", tmp_config.get(setting, {"global": None}))["global"]
# service_settings[setting] = {"value": value, "method": method, "global": is_global}
# services.append(service_settings)
# services.sort(key=lambda x: x["SERVER_NAME"]["value"])
# return render_template(
# "services.html",
# services=services,
# global_config=global_config,
# )
@app.route("/services", methods=["GET", "POST"])
@login_required
def services():
@ -1248,6 +1130,122 @@ def services():
return redirect(url_for("loading", next=url_for("services"), message=message))
# Display services
services = []
tmp_config = DB.get_config(methods=True, with_drafts=True).copy()
service_names = tmp_config["SERVER_NAME"]["value"].split(" ")
table_settings = (
"USE_REVERSE_PROXY",
"IS_DRAFT",
"SERVE_FILES",
"REMOTE_PHP",
"AUTO_LETS_ENCRYPT",
"USE_CUSTOM_SSL",
"USE_MODSECURITY",
"USE_BAD_BEHAVIOR",
"USE_LIMIT_REQ",
"USE_DNSBL",
"SERVER_NAME",
)
for service in service_names:
service_settings = {}
# For each needed setting, get the service value if one, else the global (value), else default value
for setting in table_settings:
value = tmp_config.get(f"{service}_{setting}", tmp_config.get(setting, {"value": None}))["value"]
method = tmp_config.get(f"{service}_{setting}", tmp_config.get(setting, {"method": None}))["method"]
is_global = tmp_config.get(f"{service}_{setting}", tmp_config.get(setting, {"global": None}))["global"]
service_settings[setting] = {"value": value, "method": method, "global": is_global}
services.append(service_settings)
services.sort(key=lambda x: x["SERVER_NAME"]["value"])
data_server_builder = services_builder(services)
return render_template("services.html", data_server_builder=data_server_builder)
@app.route("/services/raw/{service_name}", methods=["GET", "POST"])
@login_required
def services_raw(service_name: str):
if request.method == "POST":
if DB.readonly:
return handle_error("Database is in read-only mode", "services")
verify_data_in_form(
data={"operation": ("edit", "new", "delete")},
err_message="Invalid operation parameter on /services.",
redirect_url="services",
)
config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged = get_service_data()
if request.form["operation"] == "edit":
if is_draft_unchanged and len(variables) == 1 and "SERVER_NAME" in variables and server_name == old_server_name:
return handle_error("The service was not edited because no values were changed.", "services", True)
if request.form["operation"] == "new" and not variables:
return handle_error("The service was not created because all values had the default value.", "services", True)
# Delete
if request.form["operation"] == "delete":
is_service = app.config["CONFIG"].check_variables({"SERVER_NAME": request.form["SERVER_NAME"]}, config)
if not is_service:
error_message(f"Error while deleting the service {request.form['SERVER_NAME']}")
if config.get(f"{request.form['SERVER_NAME'].split(' ')[0]}_SERVER_NAME", {"method": "scheduler"})["method"] != "ui":
return handle_error("The service cannot be deleted because it has not been created with the UI.", "services", True)
db_metadata = DB.get_metadata()
def update_services(threaded: bool = False):
wait_applying()
manage_bunkerweb(
"services",
variables,
old_server_name,
variables.get("SERVER_NAME", ""),
operation=operation,
is_draft=is_draft,
was_draft=was_draft,
threaded=threaded,
)
ui_data = get_ui_data()
if any(
v
for k, v in db_metadata.items()
if k in ("custom_configs_changed", "external_plugins_changed", "pro_plugins_changed", "plugins_config_changed", "instances_changed")
):
ui_data["RELOADING"] = True
ui_data["LAST_RELOAD"] = time()
Thread(target=update_services, args=(True,)).start()
else:
update_services()
ui_data["CONFIG_CHANGED"] = True
with LOCK:
TMP_DATA_FILE.write_text(dumps(ui_data), encoding="utf-8")
message = ""
if request.form["operation"] == "new":
message = f"Creating {'draft ' if is_draft else ''}service {variables.get('SERVER_NAME', '').split(' ')[0]}"
elif request.form["operation"] == "edit":
message = f"Saving configuration for {'draft ' if is_draft else ''}service {old_server_name.split(' ')[0]}"
elif request.form["operation"] == "delete":
message = f"Deleting {'draft ' if was_draft and is_draft else ''}service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
return redirect(url_for("loading", next=url_for("services"), message=message))
# Display services
services = []
global_config = DB.get_config(methods=True, with_drafts=True)

View file

@ -7,13 +7,10 @@
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | Global config</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/global_config-BiGxQ47g.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-B9u3FFBX.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Subtitle-BwEtagqe.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-C-EyDvow.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Filter-DYZ0nQ3V.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/form-V4O4B50c.js">
<link rel="stylesheet" crossorigin href="assets/Filter-D2kv0NCW.css">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/global_config-_p4bx2iA.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-BvrU_MzZ.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/ButtonGroup-Bmy1qIwo.js">
<link rel="stylesheet" crossorigin href="assets/ButtonGroup-D2kv0NCW.css">
</head>
<body>

View file

@ -7,10 +7,8 @@
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | Home</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/home-BzEODgxW.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-B9u3FFBX.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Subtitle-BwEtagqe.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-C-EyDvow.js">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/home-C5vSVPv_.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-BvrU_MzZ.js">
</head>
<body>

View file

@ -7,9 +7,10 @@
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | Instances</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/instances-BtiykE6Z.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-B9u3FFBX.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/form-V4O4B50c.js">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/instances-DlYdXYfk.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-BvrU_MzZ.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/ButtonGroup-Bmy1qIwo.js">
<link rel="stylesheet" crossorigin href="assets/ButtonGroup-D2kv0NCW.css">
</head>
<body>

View file

@ -7,11 +7,10 @@
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | Jobs</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/jobs-kphU0FKk.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-B9u3FFBX.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-C-EyDvow.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Filter-DYZ0nQ3V.js">
<link rel="stylesheet" crossorigin href="assets/Filter-D2kv0NCW.css">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/jobs-DsETM5wn.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-BvrU_MzZ.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/ButtonGroup-Bmy1qIwo.js">
<link rel="stylesheet" crossorigin href="assets/ButtonGroup-D2kv0NCW.css">
</head>
<body>

View file

@ -1,301 +1,29 @@
{% extends "base.html" %}
{% block content %}
{% set attribute_name = "services" %}
{% set methods = ["all"] %}
{% set states = ["all", "draft", "online"] %}
{% set draft_services = [] %}
{% set online_services = [] %}
{% for service in services %}
{% if service['SERVER_NAME']['method'] not in methods %}
{% if methods.append(service['SERVER_NAME']['method']) %}{% endif %}
{% endif %}
{% if service.get('IS_DRAFT', 'no') == "yes" %}
{% if draft_services.append(1) %}{% endif %}
{% else %}
{% if online_services.append(1) %}{% endif %}
{% endif %}
{% endfor %}
{# Get name of multiples #}
{% set multiple_settings = [] %}
{% for plugin in plugins %}
{% for setting, value in plugin.get('settings', {}).items() %}
{% if value.get("multiple", "") %}
{% if multiple_settings.append(setting) %}{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
<input class="hidden" data-plugins-multiple='{{ multiple_settings }}' />
<!-- actions -->
<div data-{{ attribute_name }}-service data-settings="{}" class="col-span-12 relative flex justify-center min-w-0 break-words rounded-2xl bg-clip-border">
<div data-is-draft class="hidden" data-value="no"></div>
<div data-service-method class="hidden" data-value="ui"></div>
<button {% if is_readonly %}disabled{% endif %} data-{{ attribute_name }}-action="new" data-{{ attribute_name }}-name="service" data-old-name data-value="new" data-settings="{}" type="button" class="disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0 dark:bg-green-500/90 duration-300 dark:text-gray-100 w-80 flex justify-center items-center px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal text-base ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md">
<span class="mr-2">new service</span>
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-7 h-7">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</button>
</div>
<!-- end actions -->
{% if services|length >= 4 %}
<!-- service info and actions -->
<div class="p-0 sm:mx-2 md:mx-4 grid grid-cols-12 col-span-12 md:gap-x-4 gap-y-4 relative min-w-0 break-words rounded-2xl bg-clip-border">
<!-- info-->
{% set infos = [
{"name" : "SERVICES TOTAL", "data" : services|length|string},
{"name" : "TOTAL DRAFT", "data" : draft_services|length|string},
{"name" : "TOTAL ONLINE", "data" : online_services|length|string},
] %}
{% include "card_info.html" %}
<!-- filter -->
{% set filters = [
{
"type": "input",
"name": "Search",
"label": "search",
"id": "service-name-keyword",
"placeholder": "service name",
"pattern": "(.*?)"
},
{
"type": "select",
"name": "Method",
"id": "method",
"value": "all",
"values": methods
},
{
"type": "select",
"name": "State",
"id": "state",
"value": "all",
"values": states
}
] %}
{% include "card_filter.html" %}
{% include "filter_nomatch.html" %}
</div>
{% endif %}
<!-- end service info and actions -->
<!-- services container-->
<div class="p-0 sm:mx-2 md:mx-4 md:px-1 grid grid-cols-12 col-span-12 md:gap-x-4 gap-y-4 relative min-w-0 break-words rounded-2xl bg-clip-border">
{% if services|length == 0 %}
<div class="col-span-12 sm:col-span-4 sm:col-start-5">
<div class="transition duration-300 ease-in-out dark:opacity-90 text-center relative w-full p-4 text-white bg-blue-500 rounded-lg">
No service to show
</div>
</div>
{% endif %}
{% if services|length > 0 %}
<!-- end filter -->
{% for service in services %}
{% set id_server_name = service["SERVER_NAME"]['value'].replace(".", "-") %}
<div data-{{ attribute_name }}-card data-{{ attribute_name }}-name="{{ service["SERVER_NAME"]['value'] }}" data-{{ attribute_name }}-method="{{ service["SERVER_NAME"]['method'] }}" data-{{ attribute_name }}-state="{{ "draft" if service.get('IS_DRAFT', 'no') == "yes" else "online" }}" data-settings="{{ service['settings'] }}" data-{{ attribute_name }}-service="{{ service['SERVER_NAME']['value'] }}" class="flex flex-col justify-between dark:brightness-110 overflow-hidden hover:scale-102 transition col-span-12 lg:col-span-6 3xl:col-span-4 p-4 w-full shadow-md break-words bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border">
<div data-old-name
class="hidden"
data-value="{{ service['SERVER_NAME']['full_value'] }}"></div>
<div data-is-draft
class="hidden"
data-value="{% if service.get('IS_DRAFT', 'no') == 'yes' %}yes{% else %}no{% endif %}"></div>
<div data-service-method
class="hidden"
data-value="{{ service['SERVER_NAME']['method'] }}"></div>
<div class="flex justify-between items-start">
<div class="flex flex-col">
<h5 class="break-all transition duration-300 ease-in-out text-center sm:text-left mb-1 mr-2 font-bold dark:text-white/90">
{{ service["SERVER_NAME"]['value'] }}
</h5>
<h6 class="text-left sm:mb-2 font-semibold text-gray-600 dark:text-white/80">
{{ service["SERVER_NAME"]['method'] }}
</h6>
</div>
{% if service.get('IS_DRAFT', "no") == "yes" and service["SERVER_NAME"]['method'] in ["ui", "default"] %}
<button class="group relative">
<p data-{{ attribute_name }}-state="draft" class="dark:text-gray-300 -z-10 opacity-0 group-hover:z-10 group-hover:opacity-100 transition fixed bg-white dark:bg-slate-800 rounded right-12 px-1 py-0.5">
Draft
</p>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6 fill-gray-700 dark:fill-gray-300 cursor-pointer-none">
<path fill-rule="evenodd" d="M10.5 3.798v5.02a3 3 0 0 1-.879 2.121l-2.377 2.377a9.845 9.845 0 0 1 5.091 1.013 8.315 8.315 0 0 0 5.713.636l.285-.071-3.954-3.955a3 3 0 0 1-.879-2.121v-5.02a23.614 23.614 0 0 0-3 0Zm4.5.138a.75.75 0 0 0 .093-1.495A24.837 24.837 0 0 0 12 2.25a25.048 25.048 0 0 0-3.093.191A.75.75 0 0 0 9 3.936v4.882a1.5 1.5 0 0 1-.44 1.06l-6.293 6.294c-1.62 1.621-.903 4.475 1.471 4.88 2.686.46 5.447.698 8.262.698 2.816 0 5.576-.239 8.262-.697 2.373-.406 3.092-3.26 1.47-4.881L15.44 9.879A1.5 1.5 0 0 1 15 8.818V3.936Z" clip-rule="evenodd" />
</svg>
</button>
{% else %}
<button class="group relative">
<p data-{{ attribute_name }}-state="online" class="dark:text-gray-300 -z-10 opacity-0 group-hover:z-10 group-hover:opacity-100 transition fixed bg-white dark:bg-slate-800 rounded right-12 px-1 py-0.5">
Online
</p>
<svg data-toggle-draft="false"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6 fill-gray-700 dark:fill-gray-300 cursor-pointer-none">
<path d="M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z" />
</svg>
</button>
{% endif %}
</div>
{% set details = [
{
"name": "Reverse proxy",
"settings": [
"USE_REVERSE_PROXY"
]
},
{
"name": "Serve files",
"settings": [
"SERVE_FILES"
]
},
{
"name": "Remote PHP",
"settings": [
"REMOTE_PHP"
]
},
{
"name": "HTTPS",
"settings": [
"AUTO_LETS_ENCRYPT",
"USE_CUSTOM_SSL",
"GENERATE_SELF_SIGNED_SSL"
]
},
{
"name": "ModSecurity",
"settings": [
"USE_MODSECURITY"
]
},
{
"name": "Bad behavior",
"settings": [
"USE_BAD_BEHAVIOR"
]
},
{
"name": "Limit req",
"settings": [
"USE_LIMIT_REQ"
]
},
{
"name": "DNSBL",
"settings": [
"USE_DNSBL"
]
}
] %}
<!-- detail list -->
<div role="grid"
class="w-full grid grid-cols-12 justify-items-center sm:justify-items-start gap-2 mt-4 mb-6 ml-3 sm:ml-1">
{% for detail in details %}
{% set use = [] %}
{% for setting in detail['settings'] %}
{% if service[setting]['value'] == 'yes' %}
{% if use.append(1) %}{% endif %}
{% endif %}
{% endfor %}
<!-- detail -->
<div role="row" class="flex items-center col-span-12 sm:col-span-6">
<p role="gridcell"
class="transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500 ">
{{ detail['name'] }}
</p>
<p role="gridcell"
class="transition duration-300 ease-in-out dark:opacity-90 pl-2 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-gray-500 ">
{% if use %}
<span class="sr-only">yes</span>
<svg class="h-4 w-4 fill-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path 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" />
</svg>
{% else %}
<span class="sr-only">no</span>
<svg class="h-4 w-4 fill-red-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512">
<path d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z" />
</svg>
{% endif %}
</p>
</div>
<!-- end detail -->
{% endfor %}
</div>
<!-- end detail list-->
<!-- button list-->
<div class="relative w-full flex justify-center sm:justify-end">
<a aria-label="access service url"
href="http://{{ service['SERVER_NAME']['value'] }}"
target="_blank"
rel="noopener"
class="dark:brightness-90 z-20 mx-1 bg-sky-500 hover:bg-sky-500/80 focus:bg-sky-500/80 inline-block p-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal text-xs ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 active:opacity-85 hover:shadow-md">
<svg class="h-6 w-6 fill-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path d="M288 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h50.7L169.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L384 141.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H288zM80 64C35.8 64 0 99.8 0 144V400c0 44.2 35.8 80 80 80H336c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v80c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16h80c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z" />
</svg>
</a>
{% set action_buttons = [
{
"name": "clone",
"label": "clone service settings",
"color": "emerald-500"
},
{
"name": "edit",
"label": "edit service settings",
"color": "yellow-500"
}
] %}
{% if service["SERVER_NAME"]['method'] == "ui" %}
{% if action_buttons.append({"name" : "delete", "label" : "delete service settings", "color" : "red-500"}) %}
{% endif %}
{% endif %}
{% for button in action_buttons %}
<button {% if button['name'] == "clone" and is_readonly or button['name'] == "delete" and is_readonly %}disabled{% endif %} {% if button['name'] == "clone" or button['name'] == "edit" %}data-settings="{{ service['settings'] }}"{% endif %} {% if button['name'] == "new" %}data-settings="{}"{% endif %} data-{{ attribute_name }}-action="{{ button['name'] }}" aria-label="{{ button['label'] }}" data-{{ attribute_name }}-name="{{ service['SERVER_NAME']['value'] }}" class="disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0 dark:brightness-90 z-20 mx-1 bg-{{ button['color'] }} hover:bg-{{ button['color'] }}/80 focus:bg-{{ button['color'] }}/80 inline-block p-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal text-xs ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 active:opacity-85 hover:shadow-md">
{% if button['name'] == "clone" %}
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6 fill-white">
<path fill-rule="evenodd" d="M17.663 3.118c.225.015.45.032.673.05C19.876 3.298 21 4.604 21 6.109v9.642a3 3 0 0 1-3 3V16.5c0-5.922-4.576-10.775-10.384-11.217.324-1.132 1.3-2.01 2.548-2.114.224-.019.448-.036.673-.051A3 3 0 0 1 13.5 1.5H15a3 3 0 0 1 2.663 1.618ZM12 4.5A1.5 1.5 0 0 1 13.5 3H15a1.5 1.5 0 0 1 1.5 1.5H12Z" clip-rule="evenodd" />
<path d="M3 8.625c0-1.036.84-1.875 1.875-1.875h.375A3.75 3.75 0 0 1 9 10.5v1.875c0 1.036.84 1.875 1.875 1.875h1.875A3.75 3.75 0 0 1 16.5 18v2.625c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625v-12Z" />
<path d="M10.5 10.5a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963 5.23 5.23 0 0 0-3.434-1.279h-1.875a.375.375 0 0 1-.375-.375V10.5Z" />
</svg>
{% endif %}
{% if button['name'] == "edit" %}
<svg class="h-6 w-6 fill-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336c44.2 0 80-35.8 80-80s-35.8-80-80-80s-80 35.8-80 80s35.8 80 80 80z" />
</svg>
{% endif %}
{% if button['name'] == "delete" %}
<svg class="h-6 w-6 fill-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z" />
</svg>
{% endif %}
</button>
{% endfor %}
</div>
<!-- end button list-->
</div>
{% endfor %}
{% endif %}
</div>
<!-- end services container-->
<!-- modal -->
{% include "services_modal.html" %}
{% endblock content %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="img/favicon.ico" />
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | Services</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/services-CSE-2AMK.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-BvrU_MzZ.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/ButtonGroup-Bmy1qIwo.js">
<link rel="stylesheet" crossorigin href="assets/ButtonGroup-D2kv0NCW.css">
</head>
<body>
{% set data_server_flash = [] %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
{% if data_server_flash.append({"type": "error" if category == "error" else "success", "title": "dashboard_error" if category == "error" else "dashboard_success", "message": message}) %}{% endif %}
{% endfor %}
{% endwith %}
<div class='hidden' data-csrf-token='{{ csrf_token() }}'></div>
<div class='hidden' data-server-global='{{data_server_global if data_server_global else {}}}'></div>
<div class='hidden' data-server-flash='{{data_server_flash|tojson}}'></div>
<div class='hidden' data-server-builder='{{data_server_builder}}'></div>
<div id='app'></div>
</body>
</html>

67
src/ui/widgets.py Normal file
View file

@ -0,0 +1,67 @@
import base64
import json
import copy
from typing import Union
def title_widget(title: str) -> dict:
return {
"type": "Title",
"data": {"title": title},
}
def table_widget(positions: list[int], header: list[str], items: list[dict], filters: list[dict], minWidth: str, title: str) -> dict:
return {
"type": "Table",
"data": {
"title": title,
"minWidth": minWidth,
"header": header,
"positions": positions,
"items": items,
"filters": filters,
},
}
def stat_widget(
link: str, containerColums: dict, title: Union[str, int], subtitle: Union[str, int], subtitle_color: str, stat: Union[str, int], icon_name: str
) -> dict:
"""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: dict, pairs: list[dict], status: str, title: Union[str, int], buttons: list[dict]) -> dict:
"""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,
},
}
],
}