update plugins page

This commit is contained in:
Jordan Blasenhauer 2024-08-19 16:16:59 +02:00
parent 5ea761d735
commit cfdde17efb
13 changed files with 466 additions and 17 deletions

View file

@ -9,6 +9,7 @@ from .utils.widgets import (
icons_widget,
regular_widget,
unmatch_widget,
upload_widget,
)
from .utils.table import add_column
from .utils.format import get_fields_from_field
@ -47,7 +48,7 @@ def plugins_filter(types: List[str]) -> list:
}
]
if len(types) >= 2:
if types is not None and (isinstance(types, list) and len(types) >= 2):
filters.append(
{
"type": "=",
@ -165,13 +166,12 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
}
def plugins_list(plugins: Optional[list] = None) -> dict:
def plugins_list(plugins: Optional[list] = None, types: Optional[list] = None) -> dict:
if not plugins:
return fallback_message(msg="plugins_not_found")
items = []
types = set()
for plugin in plugins:
items.append(
@ -179,15 +179,15 @@ def plugins_list(plugins: Optional[list] = None) -> dict:
name=plugin["id"],
version=plugin["version"],
description=plugin["description"],
is_deletable=plugin["method"] == "ui",
is_deletable=plugin["type"] in ("manual", "default", "ui", "external"),
page=f"/plugins/{plugin['id']}" if plugin["page"] else "",
plugin_type=plugin["type"],
)
)
types.add(plugin["type"])
return {
"type": "card",
"display": ["main", 0],
"widgets": [
title_widget(
title="plugins_list_title", # keep it (a18n)
@ -200,11 +200,71 @@ def plugins_list(plugins: Optional[list] = None) -> dict:
layout="fitColumns",
columns=columns,
items=items,
filters=plugins_filter(list(types)),
filters=plugins_filter(types),
),
],
}
def plugins_builder(plugins: Optional[list] = None) -> list:
return [plugins_list(plugins=plugins)]
def plugins_tabs():
return {
"type": "tabs",
"widgets": [
button_group_widget(
buttons=[
button_widget(
text="plugins_list_tab",
display=["main", 0],
size="tab",
color="info",
iconColor="white",
iconName="list",
),
button_widget(
text="plugins_upload_tab",
color="success",
display=["main", 1],
size="tab",
iconColor="white",
iconName="plus",
),
]
)
],
}
def plugins_upload():
return {
"type": "card",
"display": ["main", 1],
"widgets": [
title_widget(
title="plugins_upload_title", # keep it (a18n)
),
subtitle_widget(
subtitle="plugins_upload_subtitle", # keep it (a18n)
),
upload_widget(
maxScreenW="sm",
),
button_group_widget(
buttons=[
button_widget(
text="action_reload", # keep it (a18n)
color="info",
size="normal",
attrs={
"data-submit-data": "{}",
"data-submit-endpoint": "",
},
disabled=True,
),
]
),
],
}
def plugins_builder(plugins: Optional[list] = None, types: Optional[list] = None) -> list:
return [plugins_tabs(), plugins_list(plugins=plugins, types=types), plugins_upload()]

View file

@ -2133,6 +2133,7 @@ def templates_widget(
templates: dict,
operation: str = "edit",
oldServerName: str = "",
isDraft: Union[str, bool] = False,
display: Optional[list] = None
):
"""
@ -2143,6 +2144,7 @@ def templates_widget(
- `templates` **Object** List of advanced templates that contains settings. Must be a dict with mode as key, then the template name as key with a list of data (different for each modes).
- `operation` **String** Operation type (edit, new, delete). (optional, default `"edit"`)
- `oldServerName` **String** Old server name. This is a server name before any changes. (optional, default `""`)
- `isDraft` **(String | Boolean)** Is draft mode. "yes" or "no" to set a draft select. Else will be ignored. (optional, default `false`)
- `display` **Array** Array need two values : "groupName" in index 0 and "compId" in index 1 in order to be displayed using the display store. More info on the display store itslef. (optional, default `[]`)
EXAMPLE
@ -2166,7 +2168,7 @@ def templates_widget(
# List of params that will be add only if not default value
list_params = [("operation", operation, "edit"),("oldServerName", oldServerName, ""),("display", display, None)]
list_params = [("operation", operation, "edit"),("oldServerName", oldServerName, ""),("isDraft", isDraft, False),("display", display, None)]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
@ -2310,3 +2312,39 @@ def unmatch_widget(
return { "type" : "Unmatch", "data" : data }
def upload_widget(
disabled: bool = False,
columns: dict = {"pc":"12","tablet":"12","mobile":"12"},
containerClass: str = "",
maxScreenW: str = "2xl"
):
"""
This component is used to upload files to the server. ATM only used to upload plugins.
PARAMETERS
- `disabled` **Boolean** If true, the upload will be disabled. (optional, default `False`)
- `columns` **Object** Columns object. (optional, default `{"pc":"12","tablet":"12","mobile":"12"}`)
- `containerClass` **String** Container additional class (optional, default `""`)
- `maxScreenW` **String** Max screen width within sm, md, lg, xl, 2xl, 3xl (optional, default `"2xl"`)
EXAMPLE
{
disabled : True
}
"""
data = {
}
# List of params that will be add only if not default value
list_params = [("disabled", disabled, False),("columns", columns, {"pc":"12","tablet":"12","mobile":"12"}),("containerClass", containerClass, ""),("maxScreenW", maxScreenW, "2xl")]
for param in list_params:
add_key_value(data, param[0], param[1], param[2])
return { "type" : "Upload", "data" : data }

View file

@ -3,10 +3,10 @@ from utils import save_builder
from pages.plugins import plugins_builder
plugins = [
{"name": "plugin1", "version": "1.0.0", "description": "This is plugin1", "type": "core", "is_deletable": False, "page": "/mypage"},
{"name": "plugin2", "version": "1.0.0", "description": "This is plugin2", "type": "external", "is_deletable": False, "page": "/mypag1"},
{"name": "plugin3", "version": "1.0.0", "description": "This is plugin3", "type": "pro", "is_deletable": True, "page": ""},
{"name": "plugin4", "version": "1.0.0", "description": "This is plugin4", "type": "pro", "is_deletable": True, "page": ""},
{"id": "plugin1", "name": "plugin1", "version": "1.0.0", "description": "This is plugin1", "type": "core", "is_deletable": False, "page": "/mypage"},
{"id": "plugin2", "name": "plugin2", "version": "1.0.0", "description": "This is plugin2", "type": "external", "is_deletable": False, "page": "/mypag1"},
{"id": "plugin3", "name": "plugin3", "version": "1.0.0", "description": "This is plugin3", "type": "pro", "is_deletable": True, "page": ""},
{"id": "plugin4", "name": "plugin4", "version": "1.0.0", "description": "This is plugin4", "type": "pro", "is_deletable": True, "page": ""},
]
types = ["core", "external", "pro"]

View file

@ -5,6 +5,7 @@ import GridLayout from "@components/Widget/GridLayout.vue";
import ListDetails from "@components/List/Details.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import Upload from "@components/Widget/Upload.vue";
import Text from "@components/Widget/Text.vue";
import Tabulator from "@components/Widget/Tabulator.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
@ -75,6 +76,10 @@ const props = defineProps({
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<Title v-if="useEqualStr(widget.type, 'Title')" v-bind="widget.data" />
<Upload
v-if="useEqualStr(widget.type, 'Upload')"
v-bind="widget.data"
/>
<Subtitle
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"

View file

@ -0,0 +1,313 @@
<script setup>
import { onMounted, ref } from "vue";
import DOMPurify from "dompurify";
import Container from "@components/Widget/Container.vue";
/**
* @name Widget/Upload.vue
* @description This component is used to upload files to the server. ATM only used to upload plugins.
* @example
* {
* disabled : True
* }
* @param {Boolean} [disabled=False] - If true, the upload will be disabled.
* @param {Object} [columns={ "pc": "12", "tablet": "12", "mobile": "12" }] - Columns object.
* @param {String} [containerClass=""] - Container additional class
* @param {String} [maxScreenW="2xl"] - Max screen width within sm, md, lg, xl, 2xl, 3xl
*/
const props = defineProps({
disabled: {
type: Boolean,
required: false,
default: false,
},
containerClass: {
type: String,
required: false,
default: "",
},
columns: {
type: Object,
required: false,
default: { pc: "12", tablet: "12", mobile: "12" },
},
maxScreenW: {
type: String,
required: false,
default: "2xl",
},
});
const containerEl = ref(null);
class Upload {
constructor(containerEl) {
this.container = containerEl;
this.form = containerEl.querySelector("#dropzone-form");
this.dropZoneElement = containerEl.querySelector(".drop-zone");
this.fileInput = containerEl.querySelector(".file-input");
this.progressArea = containerEl.querySelector(".progress-area");
this.uploadedArea = containerEl.querySelector(".uploaded-area");
this.init();
}
init() {
//form click launch input file
this.form.addEventListener("click", () => {
this.fileInput.click();
});
//dropzone logic
this.dropZoneElement.addEventListener("dragover", (e) => {
e.preventDefault();
this.dragInStyle();
});
["dragleave", "dragend"].forEach((type) => {
this.dropZoneElement.addEventListener(type, (e) => {
this.dragOutStyle();
});
});
this.dropZoneElement.addEventListener("drop", (e) => {
e.preventDefault();
this.fileInput.files = e.dataTransfer.files;
this.fileInput.dispatchEvent(new Event("change"));
this.dragOutStyle();
});
//when added file, set upload logic
this.fileInput.addEventListener("change", () => {
this.dragOutStyle();
const timeout = 500;
for (let i = 0; i < this.fileInput.files.length; i++) {
setTimeout(() => this.uploadFile(this.fileInput.files[i]), timeout * i);
}
});
//close fail/success log
this.container.addEventListener("click", (e) => {
try {
if (
e.target.closest("button").hasAttribute("data-upload-message-delete")
) {
const message = e.target.closest("div[data-upload-message]");
message.remove();
}
} catch (err) {}
});
}
dragOutStyle() {
this.dropZoneElement.classList.remove(
"border-solid",
"bg-gray-100",
"dark:bg-slate-700/50"
);
this.dropZoneElement.classList.add("border-dashed");
}
dragInStyle() {
this.dropZoneElement.classList.add(
"border-solid",
"bg-gray-100",
"dark:bg-slate-700/50"
);
this.dropZoneElement.classList.remove("border-dashed");
}
uploadFile(file) {
let name = file.name;
if (name.length >= 12) {
let splitName = name.split(".");
name = splitName[0].substring(0, 13) + "... ." + splitName[1];
}
let xhr = new XMLHttpRequest();
xhr.open("POST", "plugins/upload");
let fileSize;
xhr.upload.addEventListener("progress", ({ loaded, total }) => {
let fileTotal = Math.floor(total / 1000);
fileTotal < 1024
? (fileSize = fileTotal + " KB")
: (fileSize = (loaded / (1024 * 1024)).toFixed(2) + " MB");
const progressHTML = this.fileLoad(name, fileSize);
let cleanHTML = DOMPurify.sanitize(progressHTML);
this.uploadedArea.classList.add("onprogress");
this.progressArea.innerHTML = cleanHTML;
});
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
this.progressArea.innerHTML = "";
if (xhr.status == 201) {
this.uploadedArea.insertAdjacentHTML(
"afterbegin",
this.fileSuccess(name, fileSize)
);
this.allowReload();
} else {
this.uploadedArea.insertAdjacentHTML(
"afterbegin",
this.fileFail(name, fileSize)
);
}
}
});
let data = new FormData();
data.set("file", file);
data.set(
"csrf_token",
document
.querySelector("[data-csrf-token]")
.getAttribute("data-csrf-token")
);
xhr.send(data);
}
allowReload() {
const reloadBtn = document.querySelector("[data-reload-btn]");
reloadBtn.removeAttribute("disabled");
}
fileLoad(name, fileSize) {
const str = `<div class="mt-2 rounded p-2 w-full bg-gray-100 dark:bg-gray-800">
<div class="flex items-center justify-between">
<svg class="fill-sky-500 stroke-sky-500 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
<span class="text-sm text-slate-700 dark:text-gray-300 mr-4">${name} </span>
<span class="text-sm text-slate-700 dark:text-gray-300">${fileSize}</span>
<svg class=" fill-gray-600 dark:fill-gray-300 dark:opacity-80 h-3 w-3 " viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50"/>
</svg>
</div>
</div>
</div>`;
return str;
}
fileSuccess(name, fileSize) {
const str = `<div data-upload-message class="mt-2 rounded p-2 w-full bg-gray-100 dark:bg-gray-800">
<div class="flex items-center justify-between">
<svg
class="fill-green-500 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
/>
</svg>
<span class="text-sm text-slate-700 dark:text-gray-300 mr-4">${name} </span>
<span class="text-sm text-slate-700 dark:text-gray-300">${fileSize}</span>
<button type="button" data-upload-message-delete>
<svg class="cursor-pointer fill-gray-600 dark:fill-gray-300 dark:opacity-80 h-4 w-4 " 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"></path>
</svg>
</button>
</div>
</div>
</div>`;
let cleanHTML = DOMPurify.sanitize(str);
return cleanHTML;
}
fileFail(name, fileSize) {
const str = `<div data-upload-message class="mt-2 rounded p-2 w-full bg-gray-100 dark:bg-gray-800">
<div class="flex items-center justify-between">
<svg
class="fill-red-500 h-5 w-5 mr-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"
/>
</svg>
<span class="text-sm text-slate-700 dark:text-gray-300 mr-4">${name} </span>
<span class="text-sm text-slate-700 dark:text-gray-300">${fileSize}</span>
<button type="button" data-upload-message-delete>
<svg class="cursor-pointer fill-gray-600 dark:fill-gray-300 dark:opacity-80 h-4 w-4 " 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"></path>
</svg>
</button>
</div>
</div>
</div>`;
let cleanHTML = DOMPurify.sanitize(str);
return cleanHTML;
}
}
onMounted(() => {
new Upload(containerEl.value);
});
</script>
<template>
<Container
:containerClass="`${props.containerClass} flex justify-center`"
:columns="props.columns"
>
<div
ref="containerEl"
:class="['upload-container', `max-w-screen-${props.maxScreenW}`]"
>
<h5 class="upload-title">UPLOAD / RELOAD</h5>
<div class="upload-dropzone-container">
<!-- dropzone -->
<form
id="dropzone-form"
action="#"
:class="[
props.disabled
? 'cursor-not-allowed'
: 'cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700/50',
'upload-dropzone-form drop-zone',
]"
>
<input
:disabled="props.disabled"
class="file-input drop-zone__input"
type="file"
name="file"
multiple="multiple"
hidden
/>
<div class="flex justify-center mt-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
:class="['h-7 w-7', props.disabled ? '' : 'fill-primary']"
>
<path
fill-rule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm.53 5.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72v5.69a.75.75 0 0 0 1.5 0v-5.69l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z"
clip-rule="evenodd"
/>
</svg>
</div>
<p class="dark:text-gray-500 text-sm text-center mb-3">
{{
props.disabled ? "upload not available" : "click or drag and drop"
}}
</p>
</form>
<div class="col-span-12 progress-area"></div>
<div class="col-span-12 uploaded-area"></div>
<!-- end dropzone -->
</div>
</div>
</Container>
</template>

View file

@ -255,6 +255,10 @@
"jobs_table_cache_downloadable": "Cache (downloadable)",
"jobs_history_subtitle": "Job history details.",
"jobs_history_table_title": "Job history list with start run date, end run date and success state.",
"plugins_upload_title": "Upload plugin",
"plugins_upload_subtitle": "Extend BunkerWeb features.",
"plugins_list_tab": "Plugins",
"plugins_upload_tab": "Upload",
"plugins_search": "Search plugin",
"plugins_search_desc": "Search the plugin by his name",
"plugins_type": "Plugin type",

View file

@ -152,8 +152,6 @@ function addBanHandler() {
.querySelector(`#${tableAddbanId}`)
.querySelectorAll(".tabulator-row");
console.log();
for (let i = 0; i < tabulatorRows.length; i++) {
const row = tabulatorRows[i];
const ip = row.querySelector("input[name=ip]").value;

View file

@ -3,6 +3,7 @@ import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import BuilderPlugins from "@components/Builder/Plugins.vue";
import { useGlobal } from "@utils/global.js";
import { useDisplayStore } from "@store/global.js";
/**
* @name Page/PLugins.vue
@ -10,6 +11,10 @@ import { useGlobal } from "@utils/global.js";
This page displays global information about plugins, and allow to delete or upload some plugins.
*/
// Set default store
const displayStore = useDisplayStore();
displayStore.setDisplay("main", 0);
const plugins = reactive({
builder: "",
});

File diff suppressed because one or more lines are too long

View file

@ -557,6 +557,24 @@ body {
@apply flex scale-110 h-5 w-5 items-center align-middle;
}
/* UPLOAD */
.upload-container {
@apply p-4 w-full col-span-12 md:col-span-7 2xl:col-span-4 grid grid-cols-12 relative min-w-0 break-words bg-white dark:bg-slate-850 bg-clip-border;
}
.upload-title {
@apply col-span-12 mb-4 font-bold dark:text-gray-100;
}
.upload-dropzone-container {
@apply mx-2 p-0 col-span-12 grid grid-cols-12;
}
.upload-dropzone-form {
@apply col-span-12 border-2 rounded-lg p-2 border-dashed border-primary dark:brightness-125;
}
/* LAYOUT */
.layout-grid {

View file

@ -11,6 +11,7 @@
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@vueuse/core": "^10.11.0",
"ace-builds": "^1.24.2",
"dompurify": "^3.1.6",
"flag-icons": "^6.15.0",
"flatpickr": "^4.6.13",
"pinia": "^2.1.6",
@ -1333,6 +1334,12 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/dompurify": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
},
"node_modules/electron-to-chromium": {
"version": "1.4.508",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz",

View file

@ -15,6 +15,7 @@
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@vueuse/core": "^10.11.0",
"ace-builds": "^1.24.2",
"dompurify": "^3.1.6",
"flag-icons": "^6.15.0",
"flatpickr": "^4.6.13",
"pinia": "^2.1.6",

File diff suppressed because one or more lines are too long