add new jobs page + better JSdoc

* add JSdoc to client components/pages functions
* add jobs to vite build
* created table and title widget to builder.py
* add jobs_builder to builder.py and update route
* Jobs client page now retrieve real data
* update input components to handle props.attrs + use this to create the download cache file url
This commit is contained in:
Jordan Blasenhauer 2024-07-24 13:55:57 +02:00
parent 809d0d98cf
commit 7bb282405e
36 changed files with 990 additions and 1680 deletions

View file

@ -4,6 +4,27 @@ 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:
@ -604,3 +625,214 @@ def global_config_builder(plugins: list, settings: dict) -> str:
}
]
return base64.b64encode(bytes(json.dumps(builder), "utf-8")).decode("ascii")
def get_jobs_list(jobs):
data = []
# loop on each dict
for key, value in jobs.items():
item = []
item.append({"name": key, "type": "Text", "data": {"text": key}})
# loop on each value
for k, v in value.items():
# override widget type for some keys
if k in ("reload", "success"):
item.append(
{
k: "success" if v else "failed",
"type": "Icons",
"data": {
"iconName": "check" if v else "cross",
},
}
)
continue
if k in ("plugin_id", "every", "last_run"):
item.append({k: v, "type": "Text", "data": {"text": v}})
continue
if k in ("cache") and len(v) <= 0:
item.append({k: v, "type": "Text", "data": {"text": ""}})
continue
if k in ("cache") and len(v) > 0:
files = []
# loop on each cache item
for cache in v:
file_name = f"{cache['file_name']} [{cache['service_id']}]" if cache["service_id"] else f"{cache['file_name']}"
files.append(file_name)
item.append(
{
k: " ".join(files),
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": value.get("plugin_id", ""),
"data-job-name": key,
},
"id": f"{key}_cache",
"label": f"{key}_cache",
"hideLabel": True,
"inpType": "select",
"name": f"{key}_cache",
"value": "download file",
"values": files,
"columns": {
"pc": 12,
"tablet": 12,
"mobile": 12,
},
"overflowAttrEl": "data-table-body",
"containerClass": "table download-cache-file",
"maxBtnChars": 12,
"popovers": [
{
"iconName": "info",
"text": "jobs_download_cache_file",
},
],
}
},
}
)
continue
data.append(item)
return data
def jobs_builder(jobs):
jobs_list = get_jobs_list(jobs)
intervals = ["all"]
# loop on each job
for job in jobs_list:
# loop on each item
for item in job:
# get the interval if not already in intervals
if item.get("every") and item.get("every") not in intervals:
intervals.append(item.get("every"))
builder = [
{
"type": "card",
"containerColumns": {"pc": 12, "tablet": 12, "mobile": 12},
"widgets": [
title_widget("jobs_title"),
table_widget(
positions=[2, 2, 1, 1, 1, 2, 3],
header=[
"jobs_table_name",
"jobs_table_plugin_id",
"jobs_table_interval",
"jobs_table_reload",
"jobs_table_success",
"jobs_table_last_run_date",
"jobs_table_cache_downloadable",
],
items=jobs_list,
filters=[
{
"filter": "table",
"filterName": "keyword",
"type": "keyword",
"value": "",
"keys": ["name", "plugin_id", "last_run"],
"field": {
"id": "jobs-keyword",
"value": "",
"type": "text",
"name": "jobs-keyword",
"label": "jobs_search",
"placeholder": "inp_keyword",
"isClipboard": False,
"popovers": [
{
"text": "jobs_search_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
{
"filter": "table",
"filterName": "every",
"type": "select",
"value": "all",
"keys": ["every"],
"field": {
"id": "jobs-every",
"value": "all",
"values": intervals,
"name": "jobs-every",
"onlyDown": True,
"label": "jobs_interval",
"popovers": [
{
"text": "jobs_interval_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
{
"filter": "table",
"filterName": "reload",
"type": "select",
"value": "all",
"keys": ["reload"],
"field": {
"id": "jobs-last-run",
"value": "all",
"values": ["all", "success", "failed"],
"name": "jobs-last-run",
"onlyDown": True,
"label": "jobs_reload",
"popovers": [
{
"text": "jobs_reload_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
{
"filter": "table",
"filterName": "success",
"type": "select",
"value": "all",
"keys": ["success"],
"field": {
"id": "jobs-success",
"value": "all",
"values": ["all", "success", "failed"],
"name": "jobs-success",
"onlyDown": True,
"label": "jobs_success",
"popovers": [
{
"text": "jobs_success_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
},
],
minWidth="lg",
title="jobs_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:
if "global-config" in root or "jobs" in root:
base_html = base_html.replace("data_server_builder[1:-1]", "data_server_builder")
file_path = os.path.join(root, file)

View file

@ -50,8 +50,11 @@ const banner = reactive({
}),
});
// I want to replace the content class content by banner-item-text
/**
@name setupBanner
@description This function will try to retrieve banner news from the local storage, and in case it is not available or older than one hour, it will fetch the news from the api.
@returns {void}
*/
function setupBanner() {
// Check if data, and if case, that data is not older than one hour
// Case it is, refetch
@ -94,7 +97,11 @@ function setupBanner() {
});
}
// Banner animation effect
/**
@name runBanner
@description Run the banner animation to display all news at a regular interval.
@returns {void}
*/
function runBanner() {
const nextDelay = 8000;
const transDuration = 1000;
@ -129,8 +136,12 @@ function runBanner() {
runBanner();
}, nextDelay);
}
// Observe banner and set is visible or not to
// Update float button and menu position
/**
@name observeBanner
@description Check if the banner is visible in the viewport and set the state in the global bannerStore to update related components.
@returns {void}
*/
function observeBanner() {
const options = {
root: null,
@ -148,6 +159,11 @@ function observeBanner() {
observer.observe(document.getElementById("banner"));
}
/**
@name noTabindex
@description Stop highlighting a banner item that was focused with tabindex.
@returns {void}
*/
function noTabindex() {
const bannerItems = document.querySelectorAll(".banner-item");
bannerItems.forEach((item) => {
@ -155,6 +171,11 @@ function noTabindex() {
});
}
/**
@name isTabindex
@description Highlighting a banner item that is focused with tabindex.
@returns {void}
*/
function isTabindex() {
const activeElement = document.activeElement;
const bannerItems = document.querySelectorAll(".banner-item");
@ -171,9 +192,13 @@ function isTabindex() {
.classList.remove("banner-tabindex-hide");
}
// Focus with tabindex break banner animation
// When a banner is focused, we need to add in front of the current banner the focus element
// And remove it when the focus is lost
/**
@name isTabindex
@description Focus with tabindex break banner animation.
When a banner is focused, we need to add in front of the current banner the focus element.
And remove it when the focus is lost.
@returns {void}
*/
function handleTabIndex() {
// Get the active element after tabindex click
document.addEventListener("keyup", (e) => {

View file

@ -14,6 +14,13 @@ const lang = reactive({
curr: "",
});
/**
@name updateLangStorage
@description This function will update the language in the session storage and reload the page.
On reload, we will retrieve the language from the session storage and set it.
@param {string} lang - The language to set.
@returns {void}
*/
function updateLangStorage(lang) {
sessionStorage.setItem("lang", lang);
document.location.reload();

View file

@ -16,6 +16,11 @@ const loader = reactive({
const logo = ref();
const logoContainer = ref();
/**
@name loading
@description This function will toggle the loading animation.
@returns {void}
*/
function loading() {
// delay before stopping the loading
setTimeout(() => {

View file

@ -109,6 +109,11 @@ const menu = reactive({
username: "",
});
/**
@name getDarkMode
@description Get the dark mode state from the session storage or the user's preferences.
@returns {void}
*/
function getDarkMode() {
let darkMode = false;
// Case on storage
@ -129,12 +134,22 @@ function getDarkMode() {
return darkMode;
}
/**
@name switchMode
@description Switch between dark and light mode. Handle by a button.
@returns {void}
*/
function switchMode() {
menu.darkMode = menu.darkMode ? false : true;
sessionStorage.setItem("mode", menu.darkMode ? "dark" : "light");
updateMode();
}
/**
@name updateMode
@description Update the mode of the page.
@returns {void}
*/
function updateMode() {
try {
menu.darkMode
@ -143,10 +158,20 @@ function updateMode() {
} catch (err) {}
}
/**
@name closeMenu
@description Close menu when we are on mobile device (else always visible).
@returns {void}
*/
function closeMenu() {
menu.isActive = false;
}
/**
@name closeMenu
@description Toggle menu when we are on mobile device (else always visible).
@returns {void}
*/
function toggleMenu() {
menu.isActive = menu.isActive ? false : true;
}

View file

@ -18,6 +18,11 @@ const news = reactive({
posts: [],
});
/**
@name loadNews
@description Retrieve blog news from storage or fetch from the API.
@returns {void}
*/
function loadNews() {
// Check if data, and if case, that data is not older than one hour
// Case it is, refetch

View file

@ -189,11 +189,24 @@ const filters = [
},
];
/**
@name filter
@description Get the filter data from the <Filter /> component and store the result in the advanced store.
After that, update some UI states like disabled state.
@param {object} filterData - The filter data from the <Filter /> component.
@returns {void}
*/
function filter(filterData) {
advancedForm.templateUIFormat = filterData;
updateStates();
}
/**
@name updateStates
@description Update some UI states, usually after a filter, a reload, a remount or a change in the template.
We will check to set the current plugins available and update the current plugin if needed.
@returns {void}
*/
function updateStates() {
data.plugins = getPluginNames(advancedForm.templateUIFormat);
// Check after a filter if previous plugin is still in the list and if at least one plugin is available
@ -204,15 +217,27 @@ function updateStates() {
setValidity();
}
/**
@name setValidity
@description Check template settings and return if there is any error.
Error will disabled save button and display an error message.
@returns {void}
*/
function setValidity() {
const [isRegErr, isReqErr, settingErr, settingNameErr, pluginErr, id] =
useCheckPluginsValidity(advancedForm.templateUI);
useCheckPluginsValidity(advancedForm.templateBase);
data.isRegErr = isRegErr;
data.isReqErr = isReqErr;
data.settingErr = settingErr;
data.pluginErr = pluginErr;
}
/**
@name getFirstPlugin
@description Get the first available plugin in the template.
@param {object} template - The template object.
@returns {string} - The first plugin name.
*/
function getFirstPlugin(template) {
try {
return template[0]["name"];
@ -221,6 +246,12 @@ function getFirstPlugin(template) {
}
}
/**
@name getPluginNames
@description Get the first available plugin in the template.
@param {object} template - The template object.
@returns {array} - The list of plugin names.
*/
function getPluginNames(template) {
try {
const pluginNames = [];

View file

@ -79,6 +79,12 @@ const data = reactive({
}),
});
/**
@name setValidity
@description Check template settings and return if there is any error.
Error will disabled save button and display an error message.
@returns {void}
*/
function setValidity() {
const [isRegErr, isReqErr, settingErr, settingNameErr, pluginErr, id] =
useCheckPluginsValidity(easyForm.templateUI);

View file

@ -57,6 +57,7 @@ const props = defineProps({
:headerClass="props.setting.headerClass || ''"
:inpClass="props.setting.inpClass || ''"
:tabId="props.setting.tabId || contentIndex"
:attrs="props.setting.attrs || {}"
/>
<Select
v-if="props.setting.inpType === 'select'"
@ -79,6 +80,7 @@ const props = defineProps({
:headerClass="props.setting.headerClass || ''"
:inpClass="props.setting.inpClass || ''"
:tabId="props.setting.tabId || contentIndex"
:attrs="props.setting.attrs || {}"
/>
<Datepicker
v-if="props.setting.inpType === 'datepicker'"
@ -99,6 +101,7 @@ const props = defineProps({
:disabled="props.setting.disabled || false"
:required="props.setting.required || false"
:tabId="props.setting.tabId || contentIndex"
:attrs="props.setting.attrs || {}"
/>
<Input
v-if="props.setting.inpType === 'input'"
@ -121,6 +124,7 @@ const props = defineProps({
:headerClass="props.setting.headerClass || ''"
:inpClass="props.setting.inpClass || ''"
:tabId="props.setting.tabId || contentIndex"
:attrs="props.setting.attrs || {}"
/>
<Editor
v-if="props.setting.inpType === 'editor'"
@ -140,5 +144,6 @@ const props = defineProps({
:headerClass="props.setting.headerClass || ''"
:editorClass="props.setting.editorClass || ''"
:tabId="props.setting.tabId || contentIndex"
:attrs="props.setting.attrs || {}"
/>
</template>

View file

@ -50,6 +50,12 @@ const data = reactive({
isValid: true,
});
/**
@name updateRaw
@description Get the raw data from editor, update the raw store with it and check if it is valid JSON.
@param {string} v - The raw data to update.
@returns {void}
*/
function updateRaw(v) {
console.log("update");
// Transform to a possible valid JSON
@ -89,6 +95,14 @@ function updateRaw(v) {
}
}
/**
@name json2raw
@description Convert a JSON object to a raw string that can be passed to the editor.
This will convert JSON to key value pairs (format key=value).
This is only used at first mount when there is no raw data.
@param {string} json - The template json to convert
@returns {string} - The raw string
*/
function json2raw(json) {
let dataStr = JSON.stringify(json);
// Remove first and last curly brackets

View file

@ -77,10 +77,20 @@ const data = reactive({
}),
});
/**
@name getFirstTemplateName
@description Get the first template name from the first mode.
@returns {string} - The first template name
*/
function getFirstTemplateName() {
return Object.keys(props.templates[data.currModeName])[0];
}
/**
@name getFirstTemplateName
@description Get the first mode name from the first key in props.templates dict.
@returns {string} - The first mode name
*/
function getFirstModeName() {
// Get first key in props.templates dict
return Object.keys(props.templates)[0];

View file

@ -33,6 +33,7 @@ import { useUUID } from "@utils/global.js";
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@param {object} [attrs={}] - Additional attributes to add to the field
@param {array} [popovers] - List of popovers to display more information
@param {string} [inpType="checkbox"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {boolean} [disabled=false]
@ -85,6 +86,11 @@ const props = defineProps({
type: String,
required: true,
},
attrs: {
type: Object,
required: false,
default: {},
},
name: {
type: String,
required: true,
@ -126,6 +132,12 @@ const checkbox = reactive({
const emits = defineEmits(["inp"]);
/**
@name updateValue
@description This will convert the boolean checkbox value to a "yes" or "no" string value.
We will check the validity of the checkbox too.
@returns {string} - The new string value of the checkbox 'yes' or 'no'
*/
function updateValue() {
checkbox.value = checkbox.value === "yes" ? "no" : "yes";
checkbox.isValid = checkboxEl.value.checkValidity();
@ -158,6 +170,7 @@ onMounted(() => {
<div class="checkbox-container">
<input
v-bind="props.attrs"
ref="checkboxEl"
:tabindex="props.tabId"
@keyup.enter="$emit('inp', updateValue())"

View file

@ -43,6 +43,7 @@ import { useUUID } from "@utils/global.js";
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@param {array} values
@param {object} [attrs={}] - Additional attributes to add to the field
@param {string} [maxBtnChars=""] - Max char to display in the dropdown button handler.
@param {array} [popovers] - List of popovers to display more information
@param {string} [inpType="select"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@ -80,6 +81,11 @@ const props = defineProps({
required: true,
default: [],
},
attrs: {
type: Object,
required: false,
default: {},
},
maxBtnChars: {
type: [String, Number],
required: false,
@ -194,7 +200,11 @@ const selectBtn = ref();
const selectWidth = ref("");
const selectDropdown = ref();
// EVENTS
/**
@name toggleSelect
@description This will toggle the custom select dropdown component.
@returns {void}
*/
function toggleSelect() {
select.isOpen = select.isOpen ? false : true;
// Position dropdown relative to btn on open on fixed position
@ -240,10 +250,22 @@ function toggleSelect() {
}
}
/**
@name closeSelect
@description This will close the custom select dropdown component.
@returns {void}
*/
function closeSelect() {
select.isOpen = false;
}
/**
@name changeValue
@description This will change the value of the select when a new value is selected from dropdown button.
Check the validity of the select too. Close select after it.
@param {string} newValue - The new value to set to the select.
@returns {string} - The new value of the select
*/
function changeValue(newValue) {
// Allow on template to switch from prop value to component own value
// Then send the new value to parent
@ -260,7 +282,13 @@ function changeValue(newValue) {
return newValue;
}
// Close select when clicked outside logic
/**
@name closeOutside
@description This function is linked to a click event and will check if the target is part of the select component.
Case not and select is open, will close the select.
@param {event} e - The event object.
@returns {void}
*/
function closeOutside(e) {
try {
if (e.target !== selectBtn.value && e.target !== inputEl.value) {
@ -271,6 +299,12 @@ function closeOutside(e) {
}
}
/**
@name closeScroll
@description This function is linked to a scroll event and will close the select in case a scroll is detected and the scroll is not the dropdown.
@param {event} e - The event object.
@returns {void}
*/
function closeScroll(e) {
if (!e.target) return;
// Case not a DOM element (like the document itself)
@ -284,13 +318,24 @@ function closeScroll(e) {
select.isOpen = false;
}
/**
@name closeEscape
@description This function is linked to a key event and will close the select in case "Escape" key is pressed.
@param {event} e - The event object.
@returns {void}
*/
function closeEscape(e) {
if (e.key !== "Escape") return;
select.isOpen = false;
}
// Check after a key is pressed if the current active element is the select button
// If not close the select
/**
@name closeTab
@description This function is linked to a key event and will listen to tabindex change.
In case the new tabindex is not part of the select component, will close the select.
@param {event} e - The event object.
@returns {void}
*/
function closeTab(e) {
if (e.key !== "Tab" && e.key !== "Shift-Tab") return;
setTimeout(() => {
@ -371,6 +416,7 @@ const emits = defineEmits(["inp"]);
<!--custom-->
<div class="relative">
<button
v-bind="props.attrs"
data-toggle-dropdown
:name="`${props.name}-custom`"
:tabindex="props.tabId"

View file

@ -45,6 +45,7 @@ import "@assets/css/flatpickr.dark.min.css";
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {array} popovers - List of popovers to display more information
@param {object} [attrs={}] - Additional attributes to add to the field
@param {string} [inpType="datepicker"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {number<timestamp>} [value=""] - Default date when instanciate
@param {number<timestamp>} [minDate=""] - Impossible to pick a date before this date.
@ -79,6 +80,11 @@ const props = defineProps({
required: false,
default: "",
},
attrs: {
type: Object,
required: false,
default: {},
},
popovers: {
type: Array,
required: false,
@ -158,9 +164,16 @@ const picker = reactive({
let datepicker;
function setMonthSelect(calendar, id) {
// Hide default select and optionss
const defaultSelect = calendar.querySelector(
/**
@name setMonthSelect
@description Create a custom select for month dropdown and hide default one.
@param {element} calendarEl - The calendar element.
@param {string} id - The id of the datepicker.
@returns {void}
*/
function setMonthSelect(calendarEl, id) {
// Hide default select and options
const defaultSelect = calendarEl.querySelector(
".flatpickr-monthDropdown-months"
);
defaultSelect.classList.add("hidden");
@ -195,7 +208,7 @@ function setMonthSelect(calendar, id) {
optCtnr.classList.add("select-dropdown-container", "hidden", "flex");
container.appendChild(optCtnr);
// Options
calendar
calendarEl
.querySelector(".flatpickr-monthDropdown-months")
.querySelectorAll("option")
.forEach((option) => {
@ -229,6 +242,13 @@ function setMonthSelect(calendar, id) {
defaultSelect.parentNode.insertBefore(container, defaultSelect.nextSibling);
}
/**
@name setPickerAtt
@description Set attributes to the calendar element to make it more accessible.
@param {element} calendarEl - The calendar element.
@param {string|boolean} [id=false] - The id of the datepicker.
@returns {void}
*/
function setPickerAtt(calendarEl, id = false) {
// change error non-standard attributes
const inps = calendarEl.querySelectorAll(
@ -258,6 +278,16 @@ function setPickerAtt(calendarEl, id = false) {
}
}
/**
@name handleEvents
@description Handle events on the calendar element, like tabindex.
This will update the tabindex and focus on the right element.
This will update the custom select and options.
@param {element} calendarEl - The calendar element.
@param {string} id - The id of the datepicker.
@param {object} datepicker - The datepicker instance.
@returns {void}
*/
function handleEvents(calendarEl, id, datepicker) {
calendarEl.addEventListener("click", (e) => {
// Close dropdown month select if click outside
@ -471,6 +501,14 @@ function handleEvents(calendarEl, id, datepicker) {
});
}
/**
@name toggleSelect
@description Toggle the custom select dropdown.
@param {element} calendarEl - The calendar element.
@param {string} id - The id of the datepicker.
@param {event} e - The event.
@returns {void}
*/
function toggleSelect(calendar, id, e) {
if (e.target.hasAttribute("data-months-select")) {
const optCtnr = calendar.querySelector(`#${id}-custom`);
@ -482,6 +520,14 @@ function toggleSelect(calendar, id, e) {
}
}
/**
@name closeSelectByDefault
@description Close the custom select dropdown by default.
@param {element} calendarEl - The calendar element.
@param {string} id - The id of the datepicker.
@param {event} e - The event.
@returns {void}
*/
function closeSelectByDefault(calendar, id, e) {
if (!e.target.hasAttribute("data-months-select")) {
const optCtnr = calendar.querySelector(`#${id}-custom`);
@ -492,6 +538,15 @@ function closeSelectByDefault(calendar, id, e) {
}
}
/**
@name updateMonth
@description Update the month when click on custom select option.
@param {element} calendarEl - The calendar element.
@param {string} id - The id of the datepicker.
@param {event} e - The event.
@param {object} datepicker - The datepicker instance.
@returns {void}
*/
function updateMonth(calendar, id, e, datepicker) {
if (e.target.hasAttribute("data-month")) {
// Close dropdown
@ -523,6 +578,13 @@ function updateMonth(calendar, id, e, datepicker) {
}
}
/**
@name updateIndex
@description Update the tabindex on the calendar element.
@param {element} calendarEl - The calendar element.
@param {string} target - The event target.
@returns {void}
*/
function updateIndex(calendarEl, target) {
if (target.hasAttribute("tabindex")) {
calendarEl.querySelectorAll("[data-tabindex-active]").forEach((el) => {
@ -533,6 +595,13 @@ function updateIndex(calendarEl, target) {
}
}
/**
@name setIndex
@description Set the tabindex on the calendar element to work with keyboard.
@param {element} calendarEl - The calendar element.
@param {string} tabindex - the tabindex to set.
@returns {void}
*/
function setIndex(calendarEl, tabindex) {
try {
const days = calendarEl.querySelectorAll(".flatpickr-day");
@ -675,6 +744,7 @@ onUnmounted(() => {
<div class="relative flex flex-col items-start">
<input
v-bind="props.attrs"
:data-timestamp="date.currStamp"
:tabindex="props.tabId"
:aria-controls="`${date.id}-calendar`"

View file

@ -40,6 +40,7 @@ import "@assets/script/editor/theme-dawn.js";
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text. @param {string} label
@param {string} value
@param {object} [attrs={}] - Additional attributes to add to the field
@param {array} [popovers] - List of popovers to display more information
@param {string} [inpType="editor"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {object} [columns={"pc": "12", "tablet": "12", "mobile": "12}] - Field has a grid system. This allow to get multiple field in the same row if needed.
@ -70,6 +71,11 @@ const props = defineProps({
type: String,
required: true,
},
attrs: {
type: Object,
required: false,
default: {},
},
inpType: {
type: String,
required: false,
@ -271,6 +277,11 @@ class Editor {
}
}
/**
@name removeErrCSS
@description Remove useless CSS from the editor to avoid accessibility issues.
@returns {void}
*/
function removeErrCSS() {
setTimeout(() => {
try {
@ -294,6 +305,11 @@ function removeErrCSS() {
}, 100);
}
/**
@name setEditorAttrs
@description Override editor attributes by adding or deleting some for better accessibility.
@returns {void}
*/
function setEditorAttrs() {
// Add tabindex to editor
try {
@ -370,6 +386,7 @@ onUnmounted(() => {
class="input-editor-error"
></div>
<div
v-bind="props.attrs"
:class="[
'input-editor',
props.disabled ? 'disabled' : 'enabled',

View file

@ -44,6 +44,7 @@ import { useUUID } from "@utils/global.js";
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text. @param {string} label
@param {string} value
@param {object} [attrs={}] - Additional attributes to add to the field
@param {array} [popovers] - List of popovers to display more information
@param {string} [inpType="input"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {object} [columns={"pc": "12", "tablet": "12", "mobile": "12}] - Field has a grid system. This allow to get multiple field in the same row if needed.
@ -80,6 +81,11 @@ const props = defineProps({
type: String,
required: true,
},
attrs: {
type: Object,
required: false,
default: {},
},
inpType: {
type: String,
required: false,
@ -195,6 +201,7 @@ onMounted(() => {
<div class="input-regular-container">
<input
v-bind="props.attrs"
:tabindex="props.tabId"
ref="inputEl"
v-model="inp.value"

View file

@ -38,6 +38,7 @@ import ErrorDropdown from "@components/Forms/Error/Dropdown.vue";
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@param {object} [attrs={}] - Additional attributes to add to the field
@param {string} [separator=" "] - Separator to split the value, by default it is a space
@param {string} [maxBtnChars=""] - Max char to display in the dropdown button handler.
@param {array} [popovers] - List of popovers to display more information
@ -70,6 +71,11 @@ const props = defineProps({
type: String,
required: true,
},
attrs: {
type: Object,
required: false,
default: {},
},
separator: {
type: String,
required: false,
@ -187,7 +193,11 @@ const inputEl = ref();
const selectWidth = ref("");
const selectDropdown = ref();
// EVENTS
/**
@name openSelect
@description Open select dropdown, calculate the dropdown position and update it if needed.
@returns {void}
*/
function openSelect() {
inp.isOpen = true;
// Reset input value
@ -226,8 +236,13 @@ function openSelect() {
}, 10);
}
// Close select when clicked outside logic
function closeOutside(e) {
/**
@name closeOutside
@description This function is linked to a click event and will check if the target is part of the select component.
Case not and select is open, will close the select.
@param {event} e - The event object.
@returns {void}
*/ function closeOutside(e) {
if (
e.target.hasAttribute("data-select-item") ||
e.target.hasAttribute("data-delete-entry")
@ -242,6 +257,12 @@ function closeOutside(e) {
}
}
/**
@name closeScroll
@description This function is linked to a scroll event and will close the select in case a scroll is detected and the scroll is not the dropdown.
@param {event} e - The event object.
@returns {void}
*/
function closeScroll(e) {
if (!e.target) return;
// Case not a DOM element (like the document itself)
@ -255,13 +276,24 @@ function closeScroll(e) {
inp.isOpen = false;
}
/**
@name closeEscape
@description This function is linked to a key event and will close the select in case "Escape" key is pressed.
@param {event} e - The event object.
@returns {void}
*/
function closeEscape(e) {
if (e.key !== "Escape") return;
inp.isOpen = false;
}
// Check after a key is pressed if the current active element is the select button
// If not close the select
/**
@name closeTab
@description This function is linked to a key event and will listen to tabindex change.
In case the new tabindex is not part of the select component, will close the select.
@param {event} e - The event object.
@returns {void}
*/
function closeTab(e) {
if (e.key !== "Tab" && e.key !== "Shift-Tab") return;
setTimeout(() => {
@ -276,7 +308,12 @@ function closeTab(e) {
}, 10);
}
// Case the entry is focus and value is valid, add it to the list
/**
@name addEntry
@description When clicking add entry or key "Enter", will add the current input value to list.
@param {e} e - The event object.
@returns {void}
*/
function addEntry(e) {
// check if keyboard event
if (e.key && e.key !== "Enter") return;
@ -292,8 +329,12 @@ function addEntry(e) {
inputEl.value.focus();
}
// Case the entry is focus and value is valid, add it to the list
function deleteValue(value) {
/**
@name deleteValue
@description Delete a value from the list.
@param {string} value - The value to delete.
@returns {void}
*/ function deleteValue(value) {
inp.value = inp.value
.split(props.separator)
.filter((val) => val !== value)
@ -357,6 +398,7 @@ const emits = defineEmits(["inp"]);
<div class="relative">
<div data-input-container class="input-regular-container">
<input
v-bind="props.attrs"
data-toggle-dropdown
:aria-controls="`${inp.id}-custom`"
:aria-expanded="inp.isOpen ? 'true' : 'false'"

View file

@ -42,6 +42,7 @@ import { useUUID } from "@utils/global";
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@param {array} values
@param {object} [attrs={}] - Additional attributes to add to the field
@param {array} [popovers] - List of popovers to display more information
@param {string} [inpType="select"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {string} [maxBtnChars=""] - Max char to display in the dropdown button handler.
@ -78,6 +79,11 @@ const props = defineProps({
type: Array,
required: true,
},
attrs: {
type: Object,
required: false,
default: {},
},
inpType: {
type: String,
required: false,
@ -174,7 +180,11 @@ const selectBtn = ref();
const selectWidth = ref("");
const selectDropdown = ref();
// EVENTS
/**
@name toggleSelect
@description This will toggle the custom select dropdown component.
@returns {void}
*/
function toggleSelect() {
select.isOpen = select.isOpen ? false : true;
// Check if parent has overflow
@ -217,10 +227,22 @@ function toggleSelect() {
}
}
/**
@name closeSelect
@description This will close the custom select dropdown component.
@returns {void}
*/
function closeSelect() {
select.isOpen = false;
}
/**
@name changeValue
@description This will change the value of the select when a new value is selected from dropdown button.
Check the validity of the select too. Close select after it.
@param {string} newValue - The new value to set to the select.
@returns {string} - The new value of the select
*/
function changeValue(newValue) {
// Allow on template to switch from prop value to component own value
// Then send the new value to parent
@ -237,7 +259,13 @@ function changeValue(newValue) {
return newValue;
}
// Close select when clicked outside logic
/**
@name closeOutside
@description This function is linked to a click event and will check if the target is part of the select component.
Case not and select is open, will close the select.
@param {event} e - The event object.
@returns {void}
*/
function closeOutside(e) {
try {
if (e.target !== selectBtn.value) {
@ -248,6 +276,12 @@ function closeOutside(e) {
}
}
/**
@name closeScroll
@description This function is linked to a scroll event and will close the select in case a scroll is detected and the scroll is not the dropdown.
@param {event} e - The event object.
@returns {void}
*/
function closeScroll(e) {
if (!e.target) return;
// Case not a DOM element (like the document itself)
@ -257,13 +291,25 @@ function closeScroll(e) {
select.isOpen = false;
}
/**
@name closeEscape
@description This function is linked to a key event and will close the select in case "Escape" key is pressed.
@param {event} e - The event object.
@returns {void}
*/
function closeEscape(e) {
if (e.key !== "Escape") return;
select.isOpen = false;
}
// Check after a key is pressed if the current active element is the select button
// If not close the select
/**
@name closeTab
@description This function is linked to a key event and will listen to tabindex change.
In case the new tabindex is not part of the select component, will close the select.
@param {event} e - The event object.
@returns {void}
*/
function closeTab(e) {
if (e.key !== "Tab" && e.key !== "Shift-Tab") return;
setTimeout(() => {
@ -342,6 +388,7 @@ const emits = defineEmits(["inp"]);
<!--custom-->
<div class="relative">
<button
v-bind="props.attrs"
:name="`${props.name}-custom`"
:tabindex="props.tabId"
ref="selectBtn"

View file

@ -215,8 +215,12 @@ const buttonDelete = {
// emits
const emits = defineEmits(["delete", "add"]);
// Check if at least one input is disabled (this means a method different than ui, default or manual is used)
// If true, disable the delete button
/**
@name setDeleteState
@description Will determine if the group can be deleted. If at least one input is disabled, the delete button will be disabled.
@param {object} group - The multiple group with all settings
@returns {object} - Return delete button data
*/
function setDeleteState(group) {
// Loop on group keys and check if at least one input is disabled
let isDisabled = false;
@ -231,18 +235,43 @@ function setDeleteState(group) {
return delBtn;
}
/**
@name setInvisible
@description Will set a multiple group as invisible.
@param {string|number} id - The multiple group with all settings
@returns {void}
*/
function setInvisible(id) {
multiples.invisible.push(id);
}
/**
@name delInvisible
@description Will remove a multiple group from invisible list.
@param {string|number} id - The multiple group with all settings
@returns {void}
*/
function delInvisible(id) {
multiples.invisible = multiples.invisible.filter((v) => v !== id);
}
/**
@name toggleVisible
@description Will toggle a multiple group visibility.
@param {string|number} id - The multiple group with all settings
@returns {void}
*/
function toggleVisible(id) {
multiples.invisible.includes(id) ? delInvisible(id) : setInvisible(id);
}
/**
@name delGroup
@description Will emit a delete event to the parent component. The parent will update the template and multiples, then the component will rerender.
@param {string} multName - The multiple group name
@param {string} groupName - The multiple group id
@returns {void}
*/
function delGroup(multName, groupName) {
emits("delete", multName, groupName);
}

View file

@ -61,13 +61,23 @@ const gridClass = computed(() => {
return `col-span-${props.columns.mobile} md:col-span-${props.columns.tablet} lg:col-span-${props.columns.pc}`;
});
// When we focus or pointerover an item, we will add a higher z-index than others items in order to avoid to crop popovers
// In case we leave the item, for few moments the item will get an higher z-index than this in order to get a smooth transition
/**
@name indexUp
@description When we focus or pointerover an item, we will add a higher z-index than others items in order to avoid to crop popovers.
In case we leave the item, for few moments the item will get an higher z-index than this in order to get a smooth transition.
@param {string|number} id - The id of the item.
@returns {void}
*/
function indexUp(id) {
data.upIndex = id;
}
// This will add a higher z-index for 100ms when cursor is out of the item in order to avoid to crop popovers
/**
@name indexPending
@description This will add a higher z-index for 100ms when cursor is out of the item in order to avoid to crop popovers.
@param {string|number} id - The id of the item.
@returns {void}
*/
function indexPending(id) {
data.pendingIndex.push(id);
// Remove id from pendingIndex after a moment

View file

@ -83,6 +83,13 @@ watch(
{ deep: true }
);
/**
@name startFilter
@description Filter the given data using the available filters from a filter object.
@param {object} filter - Filter object to apply.
@param {string} value - Value to filter.
@returns {emits} - Emit a filter event with the filtered data.
*/
function startFilter(filter = {}, value) {
// Case we have new filter value, update it
// Loop on filter.base and update the "value" key when matching filterName
@ -140,8 +147,14 @@ function startFilter(filter = {}, value) {
emits("filter", template);
}
// Case we are changing a filter value
// We only have to check data for this filter
/**
@name filterData
@description Add a buffer to wait for multiple inputs before filtering the data.
Then filter data with the given filter and value.
@param {object} filter - Filter object to apply.
@param {string} value - Value to filter.
@returns {void}
*/
function filterData(filter = {}, value = null) {
// Wait for buffer input
filters.bufferCount++;
@ -154,6 +167,13 @@ function filterData(filter = {}, value = null) {
}, 50);
}
/**
@name filterRegularSettings
@description Allow to filter plugin settings from a regular template.
@param {object} filterSettings - Filters to apply
@param {object} template - Template to filter
@returns {void}
*/
function filterRegularSettings(filterSettings, template) {
template.forEach((plugin, id) => {
// loop on plugin settings dict
@ -172,6 +192,13 @@ function filterRegularSettings(filterSettings, template) {
});
}
/**
@name filterMultiplesSettings
@description Allow to filter plugin multiples settings from a regular template.
@param {object} filterSettings - Filters to apply
@param {object} template - Template to filter
@returns {void}
*/
function filterMultiplesSettings(filterSettings, template) {
const multiples = [];
// Format to filter

View file

@ -77,6 +77,11 @@ const popover = reactive({
const popoverContainer = ref();
const popoverBtn = ref();
/**
@name showPopover
@description Show the popover and set the position of the popover relative to the container.
@returns {void}
*/
function showPopover() {
popover.isHover = true;
@ -128,6 +133,11 @@ function showPopover() {
}, 450);
}
/**
@name hidePopover
@description Hide the popover.
@returns {void}
*/
function hidePopover() {
popover.isHover = false;
popover.isOpen = false;

View file

@ -141,6 +141,11 @@ const table = reactive({
itemsFormat: JSON.parse(JSON.stringify(props.items)),
});
/**
@name setUnmatchWidth
@description Determine the width of the unmatch element based on the parent container.
@returns {void}
*/
function setUnmatchWidth() {
try {
const value = tableBody.value.closest("[data-is='card']").clientWidth - 60;
@ -148,6 +153,11 @@ function setUnmatchWidth() {
} catch (e) {}
}
/**
@name getOverflow
@description Handle the overflow of the table and update padding in consequence.
@returns {void}
*/
function getOverflow() {
setTimeout(() => {
const overflow =

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { getI18n } from "@utils/lang.js";
import Jobs from "./jobs.vue";
import Jobs from "./Jobs.vue";
const pinia = createPinia();

View file

@ -10,9 +10,7 @@
{
"type": "Title",
"data": {
"title": "jobs_title",
"tag": "h1",
"type": "card"
"title": "jobs_title"
}
},
{
@ -542,10 +540,14 @@
}
},
{
"cache": "default-server-cert.pem (misc) default-server-cert.key (misc)",
"cache": "default-server-cert.pem default-server-cert.key",
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": "misc",
"data-job-name": "default-server-cert"
},
"id": "default-server-cert_cache",
"label": "default-server-cert_cache",
"hideLabel": true,
@ -553,8 +555,8 @@
"name": "default-server-cert_cache",
"value": "download file",
"values": [
"default-server-cert.pem (misc)",
"default-server-cert.key (misc)"
"default-server-cert.pem",
"default-server-cert.key"
],
"columns": {
"pc": 12,
@ -720,10 +722,14 @@
}
},
{
"cache": "folder:/var/tmp/bunkerweb/failover.tgz (jobs)",
"cache": "folder:/var/tmp/bunkerweb/failover.tgz",
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": "jobs",
"data-job-name": "failover-backup"
},
"id": "failover-backup_cache",
"label": "failover-backup_cache",
"hideLabel": true,
@ -731,7 +737,7 @@
"name": "failover-backup_cache",
"value": "download file",
"values": [
"folder:/var/tmp/bunkerweb/failover.tgz (jobs)"
"folder:/var/tmp/bunkerweb/failover.tgz"
],
"columns": {
"pc": 12,
@ -846,10 +852,14 @@
}
},
{
"cache": "asn.mmdb (jobs)",
"cache": "asn.mmdb",
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": "jobs",
"data-job-name": "mmdb-asn"
},
"id": "mmdb-asn_cache",
"label": "mmdb-asn_cache",
"hideLabel": true,
@ -857,7 +867,7 @@
"name": "mmdb-asn_cache",
"value": "download file",
"values": [
"asn.mmdb (jobs)"
"asn.mmdb"
],
"columns": {
"pc": 12,
@ -921,10 +931,14 @@
}
},
{
"cache": "country.mmdb (jobs)",
"cache": "country.mmdb",
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": "jobs",
"data-job-name": "mmdb-country"
},
"id": "mmdb-country_cache",
"label": "mmdb-country_cache",
"hideLabel": true,
@ -932,7 +946,7 @@
"name": "mmdb-country_cache",
"value": "download file",
"values": [
"country.mmdb (jobs)"
"country.mmdb"
],
"columns": {
"pc": 12,
@ -1047,10 +1061,14 @@
}
},
{
"cache": "cert.pem (selfsigned) [www.example.com] key.pem (selfsigned) [www.example.com]",
"cache": "cert.pem [www.example.com] key.pem [www.example.com]",
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": "selfsigned",
"data-job-name": "self-signed"
},
"id": "self-signed_cache",
"label": "self-signed_cache",
"hideLabel": true,
@ -1058,8 +1076,8 @@
"name": "self-signed_cache",
"value": "download file",
"values": [
"cert.pem (selfsigned) [www.example.com]",
"key.pem (selfsigned) [www.example.com]"
"cert.pem [www.example.com]",
"key.pem [www.example.com]"
],
"columns": {
"pc": 12,
@ -1198,7 +1216,6 @@
"value": "",
"type": "text",
"name": "jobs-keyword",
"containerClass": "setting",
"label": "jobs_search",
"placeholder": "inp_keyword",
"isClipboard": false,
@ -1235,7 +1252,6 @@
"name": "jobs-every",
"onlyDown": true,
"label": "jobs_interval",
"containerClass": "setting",
"popovers": [
{
"text": "jobs_interval_desc",
@ -1267,7 +1283,6 @@
],
"name": "jobs-last-run",
"onlyDown": true,
"containerClass": "setting",
"label": "jobs_reload",
"popovers": [
{
@ -1300,7 +1315,6 @@
],
"name": "jobs-success",
"onlyDown": true,
"containerClass": "setting",
"label": "jobs_success",
"popovers": [
{

View file

@ -1,4 +1,26 @@
import json
import base64
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,
},
}
jobs = {
@ -238,11 +260,7 @@ def get_jobs_list(jobs):
files = []
# loop on each cache item
for cache in v:
file_name = (
f"{cache['file_name']} ({value['plugin_id']}) [{cache['service_id']}]"
if cache["service_id"]
else f"{cache['file_name']} ({value['plugin_id']})"
)
file_name = f"{cache['file_name']} [{cache['service_id']}]" if cache["service_id"] else f"{cache['file_name']}"
files.append(file_name)
item.append(
@ -251,6 +269,10 @@ def get_jobs_list(jobs):
"type": "Fields",
"data": {
"setting": {
"attrs": {
"data-plugin-id": value.get("plugin_id", ""),
"data-job-name": key,
},
"id": f"{key}_cache",
"label": f"{key}_cache",
"hideLabel": True,
@ -283,7 +305,7 @@ def get_jobs_list(jobs):
return data
def job_builder(jobs):
def jobs_builder(jobs):
jobs_list = get_jobs_list(jobs)
@ -302,128 +324,126 @@ def job_builder(jobs):
"type": "card",
"containerColumns": {"pc": 12, "tablet": 12, "mobile": 12},
"widgets": [
{
"type": "Title",
"data": {"title": "jobs_title", "tag": "h1", "type": "card"},
},
{
"type": "Table",
"data": {
"title": "jobs_table_title",
"minWidth": "lg",
"header": [
"jobs_table_name",
"jobs_table_plugin_id",
"jobs_table_interval",
"jobs_table_reload",
"jobs_table_success",
"jobs_table_last_run_date",
"jobs_table_cache_downloadable",
],
"positions": [2, 2, 1, 1, 1, 2, 3],
"items": jobs_list,
"filters": [
{
"filter": "table",
"filterName": "keyword",
"type": "keyword",
title_widget("jobs_title"),
table_widget(
positions=[2, 2, 1, 1, 1, 2, 3],
header=[
"jobs_table_name",
"jobs_table_plugin_id",
"jobs_table_interval",
"jobs_table_reload",
"jobs_table_success",
"jobs_table_last_run_date",
"jobs_table_cache_downloadable",
],
items=jobs_list,
filters=[
{
"filter": "table",
"filterName": "keyword",
"type": "keyword",
"value": "",
"keys": ["name", "plugin_id", "last_run"],
"field": {
"id": "jobs-keyword",
"value": "",
"keys": ["name", "plugin_id", "last_run"],
"field": {
"id": "jobs-keyword",
"value": "",
"type": "text",
"name": "jobs-keyword",
"label": "jobs_search",
"placeholder": "inp_keyword",
"isClipboard": False,
"popovers": [
{
"text": "jobs_search_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
"type": "text",
"name": "jobs-keyword",
"label": "jobs_search",
"placeholder": "inp_keyword",
"isClipboard": False,
"popovers": [
{
"text": "jobs_search_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
{
"filter": "table",
"filterName": "every",
"type": "select",
},
{
"filter": "table",
"filterName": "every",
"type": "select",
"value": "all",
"keys": ["every"],
"field": {
"id": "jobs-every",
"value": "all",
"keys": ["every"],
"field": {
"id": "jobs-every",
"value": "all",
"values": intervals,
"name": "jobs-every",
"onlyDown": True,
"label": "jobs_interval",
"popovers": [
{
"text": "jobs_interval_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
"values": intervals,
"name": "jobs-every",
"onlyDown": True,
"label": "jobs_interval",
"popovers": [
{
"text": "jobs_interval_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
{
"filter": "table",
"filterName": "reload",
"type": "select",
},
{
"filter": "table",
"filterName": "reload",
"type": "select",
"value": "all",
"keys": ["reload"],
"field": {
"id": "jobs-last-run",
"value": "all",
"keys": ["reload"],
"field": {
"id": "jobs-last-run",
"value": "all",
"values": ["all", "success", "failed"],
"name": "jobs-last-run",
"onlyDown": True,
"label": "jobs_reload",
"popovers": [
{
"text": "jobs_reload_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
"values": ["all", "success", "failed"],
"name": "jobs-last-run",
"onlyDown": True,
"label": "jobs_reload",
"popovers": [
{
"text": "jobs_reload_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
{
"filter": "table",
"filterName": "success",
"type": "select",
},
{
"filter": "table",
"filterName": "success",
"type": "select",
"value": "all",
"keys": ["success"],
"field": {
"id": "jobs-success",
"value": "all",
"keys": ["success"],
"field": {
"id": "jobs-success",
"value": "all",
"values": ["all", "success", "failed"],
"name": "jobs-success",
"onlyDown": True,
"label": "jobs_success",
"popovers": [
{
"text": "jobs_success_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
"values": ["all", "success", "failed"],
"name": "jobs-success",
"onlyDown": True,
"label": "jobs_success",
"popovers": [
{
"text": "jobs_success_desc",
"iconName": "info",
},
],
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
},
],
},
},
},
],
minWidth="lg",
title="jobs_table_title",
),
],
}
]
return builder
output = job_builder(jobs)
output = jobs_builder(jobs)
# store on a file
with open("jobs.json", "w") as f:
json.dump(output, f, indent=4)
output_base64_bytes = base64.b64encode(bytes(json.dumps(output), "utf-8"))
output_base64_string = output_base64_bytes.decode("ascii")
with open("jobs.txt", "w") as f:
f.write(output_base64_string)

File diff suppressed because one or more lines are too long

View file

@ -45,6 +45,7 @@ export default defineConfig({
__dirname,
"./dashboard/pages/global-config/index.html"
),
jobs: resolve(__dirname, "./dashboard/pages/jobs/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
from builder import home_builder, instances_builder, global_config_builder, jobs_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:
@ -2388,7 +2388,8 @@ def bans():
@app.route("/jobs", methods=["GET"])
@login_required
def jobs():
return render_template("jobs.html", jobs=app.config["DB"].get_jobs(), jobs_errors=app.config["DB"].get_plugins_errors(), username=current_user.get_id())
data_server_builder = jobs_builder(app.config["DB"].get_jobs())
return render_template("jobs.html", data_server_builder=data_server_builder)
@app.route("/jobs/download", methods=["GET"])

View file

@ -7,11 +7,13 @@
<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-BRcItP4U.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-DELxfw1F.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-Dad4BU7k.js">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/global_config-WMS8GSXH.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-Dk3fE87D.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Subtitle-EK-_CGmV.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-CrOYIQ2t.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Filter-ByMTlYtF.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/form-DBYbte-L.js">
<link rel="stylesheet" crossorigin href="assets/global_config-D2kv0NCW.css">
<link rel="stylesheet" crossorigin href="assets/Filter-D2kv0NCW.css">
</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 | Home</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/home-B2G9Juhp.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-DELxfw1F.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-Dad4BU7k.js">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/home-BLCQMyYQ.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-Dk3fE87D.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Subtitle-EK-_CGmV.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-CrOYIQ2t.js">
</head>
<body>

View file

@ -7,8 +7,8 @@
<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-DaTfTNdd.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-DELxfw1F.js">
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/instances-ByhYrW6H.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-Dk3fE87D.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/form-DBYbte-L.js">
</head>

View file

@ -1,178 +1,30 @@
{% extends "base.html" %}
{% block content %}
{% set attribute_name = "jobs" %}
{% set run_times = ["all"] %}
{% for job_name, value in jobs.items() %}
{% if value['every'] not in run_times %}
{% if run_times.append(value['every']) %}{% endif %}
{% endif %}
{% endfor %}
<!-- info-->
{% set infos = [
{"name" : "JOBS TOTAL", "data" : jobs|length|string},
{"name" : "JOBS ERRORS", "data" : jobs_errors|string},
] %}
{% include "card_info.html" %}
<!-- filter -->
{% set filters = [
{
"type": "input",
"name": "Search",
"label": "search",
"id": "keyword",
"placeholder": "keyword",
"pattern": "(.*?)"
},
{
"type": "select",
"name": "Success state",
"id": "success",
"value": "all",
"values": [
"all",
"false",
"true"
]
},
{
"type": "select",
"name": "Reload state",
"id": "reload",
"value": "all",
"values": [
"all",
"false",
"true"
]
},
{
"type": "select",
"name": "Run time",
"id": "every",
"value": "all",
"values": run_times
}
] %}
{% include "card_filter.html" %}
{% include "filter_nomatch.html" %}
<div data-{{ attribute_name }}-list-container class="overflow-auto w-full col-span-12 p-4 relative break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border">
<div class="col-span-12">
<h5 class="mx-2 font-bold dark:text-white/90 mx-2">JOBS LIST</h5>
</div>
<div class="relative min-w-[900px] w-full overflow-auto grid grid-cols-12 max-h-100 sm:max-h-125">
<div class="col-span-12">
<!-- list container-->
{% set job_headers = [
{
"name": "Name",
"custom_class": "col-span-3"
},
{
"name": "Last run",
"custom_class": "col-span-3"
},
{
"name": "Every",
"custom_class": "col-span-1"
},
{
"name": "Reload",
"custom_class": "flex justify-center col-span-1"
},
{
"name": "Success",
"custom_class": "flex justify-center col-span-1"
},
{
"name": "Files",
"custom_class": "col-span-3"
}
] %}
<div class="w-full grid grid-cols-12 rounded p-2">
<!-- header-->
{% for header in job_headers %}
<p class="{{ header['custom_class'] }} dark:text-gray-100 h-8 text-sm font-bold m-0 pb-2 border-b border-gray-400">
{{ header['name'] }}
</p>
{% endfor %}
<!-- end header-->
<!-- list -->
<ul class="col-span-12 w-full" data-{{ attribute_name }}-list>
{% for job_name, value in jobs.items() %}
<!-- job item-->
{% set jobs_data = [
{"type" : "text", "filter_name" : "name", "value" : job_name, "custom_class" : "col-span-3"},
{"type" : "text", "filter_name" : "last_run", "value" : value['last_run'], "custom_class" : "col-span-3"},
{"type" : "text", "filter_name" : "every", "value" : value['every'], "custom_class" : "col-span-1"},
{"type" : "check", "filter_name" : "reload", "value" : value['reload'], "custom_class" : "col-span-1"},
{"type" : "check", "filter_name" : "success", "value" : value['success'], "custom_class" : "col-span-1"},
{"type" : "select", "filter_name" : "success", "value" : value['success'], "custom_class" : "col-span-3"},
] %}
<li data-{{ attribute_name }}-item {% for data in jobs_data %}data-{{ attribute_name }}-{{ data['filter_name'] }}="{{ data['value'] }}"{% endfor %} class="items-center grid grid-cols-12 border-b border-gray-300 py-2.5 break-all">
{% for data in jobs_data %}
{% if data['type'] == "text" %}
<p class="{{ data['custom_class'] }} dark:text-gray-400 text-sm m-0 my-1 mr-1">{{ data['value'] }}</p>
{% endif %}
{% if data['type'] == "check" and data['value'] %}
<p class="{{ data['custom_class'] }} flex justify-center dark:text-gray-400 text-sm m-0 my-1 mr-1">
<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>
</p>
{% endif %}
{% if data['type'] == "check" and not data['value'] %}
<p class="{{ data['custom_class'] }} flex justify-center dark:text-gray-400 text-sm m-0 my-1 mr-1">
<svg class="fill-red-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 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>
</p>
{% endif %}
{% if data['type'] == "select" %}
<div class="{{ data['custom_class'] }} relative dark:text-gray-400 text-sm m-0 my-1 mr-1" data-{{ attribute_name }}-files>
{% if value['cache'] %}
<button data-{{ attribute_name }}-setting-select="{{ job_name }}" class="py-1 text-sm disabled:opacity-75 dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 duration-300 ease-in-out dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 focus:border-green-500 flex justify-between align-middle items-center text-left leading-6 ease w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 md:px-3 font-normal text-gray-700 transition-all placeholder:text-gray-500">
<span id="jobs-{{ job_name }}" data-name="jobs-{{ job_name }}" data-{{ attribute_name }}-setting-select-text="{{ job_name }}">files</span>
<!-- chevron -->
<svg data-{{ attribute_name }}-setting-select="{{ job_name }}" class="transition-transform h-4 w-4 fill-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z" />
</svg>
</button>
<!-- end chevron -->
<!-- dropdown-->
<div data-{{ attribute_name }}-setting-select-dropdown="{{ job_name }}" class="hidden z-100 absolute h-full flex-col w-full translate-y-0.5">
{% for file in value['cache'] %}
<button data-{{ attribute_name }}-plugin="{{ value['plugin_id'] }}" data-{{ attribute_name }}-download="{{ job_name }}" data-{{ attribute_name }}-file="{{ file['file_name'] }}" data-{{ attribute_name }}-setting-select-dropdown-btn="{{ job_name }}" value="files" class="{% if loop.index == loop.length %}rounded-b-lg {% endif %}{% if loop.first %}rounded-t-lg{% endif %} border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:text-gray-300 bg-white dark:bg-slate-700 text-gray-700">
<span class="flex justify-start items-center">
<svg class="min-w-fit h-5.5 w-5.5 stroke-sky-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="overflow-hidden break-word transition duration-300 ease-in-out text-gray-700 dark:text-gray-300 ml-2">{{ file['file_name'] }}</span>
</span>
</button>
{% endfor %}
</div>
<!-- end dropdown-->
{% endif %}
</div>
{% endif %}
{% endfor %}
</li>
<!-- end job item-->
{% endfor %}
</ul>
<!-- end list-->
</div>
<!-- end list container-->
</div>
</div>
</div>
{% 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 | Jobs</title>
<script type="module" crossorigin nonce="{{ script_nonce }}" src="assets/jobs-BiZruuR5.js"></script>
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Title-Dk3fE87D.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Text-CrOYIQ2t.js">
<link rel="modulepreload" crossorigin nonce="{{ script_nonce }}" href="assets/Filter-ByMTlYtF.js">
<link rel="stylesheet" crossorigin href="assets/Filter-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>