plugins page builder done

This commit is contained in:
Jordan Blasenhauer 2024-08-16 18:21:44 +02:00
parent d85f0bd63e
commit 52862267eb
8 changed files with 249 additions and 3975 deletions

View file

@ -30,7 +30,7 @@ def instances_filter(healths: str, types: Optional[list] = None, methods: Option
filters = [
{
"type": "like",
"fields": ["name", "hostname"],
"fields": ["name"],
"setting": {
"id": "input-search-host-name",
"name": "input-search-host-name",
@ -397,7 +397,7 @@ def fallback_message(msg: str, display: Optional[list] = None) -> dict:
def instances_list(instances: Optional[list] = None, types: Optional[list] = None, methods: Optional[list] = None, healths: Optional[list] = None) -> dict:
if instances is None or (isinstance(instances, list) and len(instances) == 0):
return fallback_message(msg="instances_not_found", display=["main", 0])
return [fallback_message(msg="instances_not_found", display=["main", 0])]
items = []

View file

@ -0,0 +1,208 @@
from .utils.widgets import (
button_widget,
button_group_widget,
title_widget,
subtitle_widget,
text_widget,
tabulator_widget,
input_widget,
icons_widget,
regular_widget,
unmatch_widget,
)
from .utils.table import add_column
from .utils.format import get_fields_from_field
from typing import Optional
columns = [
add_column(title="Name", field="name", formatter="text"),
add_column(title="Version", field="version", formatter="text"),
add_column(title="Description", field="description", formatter="text", maxWidth=400),
add_column(title="Type", field="type", formatter="text"),
add_column(title="Actions", field="actions", formatter="buttongroup"),
]
def plugins_filter(types: Optional[list] = None) -> list:
filters = [
{
"type": "like",
"fields": ["name"],
"setting": {
"id": "input-search-name",
"name": "input-search-name",
"label": "plugins_search", # keep it (a18n)
"placeholder": "inp_keyword", # keep it (a18n)
"value": "",
"inpType": "input",
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
"popovers": [
{
"iconName": "info",
"text": "plugins_search_desc",
}
],
"fieldSize": "sm",
},
}
]
if types is not None and (isinstance(types, list) and len(types) >= 2):
filters.append(
{
"type": "=",
"fields": ["type"],
"setting": {
"id": "select-type",
"name": "select-type",
"label": "plugins_type", # keep it (a18n)
"value": "all", # keep "all"
"values": ["all"] + types,
"inpType": "select",
"onlyDown": True,
"columns": {"pc": 3, "tablet": 4, "mobile": 12},
"popovers": [
{
"iconName": "info",
"text": "plugins_type_desc",
}
],
"fieldSize": "sm",
},
}
)
return filters
def plugin_item(
name: str,
version: str,
description: str,
is_deletable: bool,
page: str,
plugin_type: str,
):
# Actions
actions = []
if page:
actions.append(
button_widget(
id=f"plugin-page-{name}",
text="action_redirect", # keep it (a18n)
color="info",
size="normal",
hideText=True,
iconName="redirect",
iconColor="white",
attrs={"data-link": f"plugins/{name}"},
),
)
if is_deletable:
actions.append(
button_widget(
id=f"plugin-delete-{name}",
text="action_delete", # keep it (a18n)
color="error",
size="normal",
hideText=True,
iconName="trash",
iconColor="white",
modal={
"widgets": [
title_widget(title="plugins_delete_title"), # keep it (a18n)
text_widget(text="plugins_delete_subtitle"), # keep it (a18n)
text_widget(bold=True, text=name),
button_group_widget(
buttons=[
button_widget(
id=f"close-delete-btn-{name}",
text="action_close", # keep it (a18n)
color="close",
size="normal",
attrs={"data-close-modal": ""}, # a11y
),
button_widget(
id=f"delete-btn-{name}",
text="action_delete", # keep it (a18n)
color="delete",
size="normal",
iconName="trash",
iconColor="white",
attrs={
"data-submit-form": f"""{{ "plugin" : "{name}", "type": "{plugin_type}" }}""",
"data-submit-endpoint": "/delete",
"data-submit-method": "DELETE",
},
),
]
),
],
},
),
)
return {
"name": text_widget(text=name)["data"],
"type": text_widget(text=plugin_type)["data"],
"version": text_widget(text=version)["data"],
"description": text_widget(text=description)["data"],
"actions": {"buttons": actions},
}
def fallback_message(msg: str, display: Optional[list] = None) -> dict:
return {
"type": "void",
"display": display if display else [],
"widgets": [
unmatch_widget(text=msg),
],
}
def plugins_list(plugins: Optional[list] = None, types: Optional[list] = None) -> dict:
if plugins is None or (isinstance(plugins, list) and len(plugins) == 0):
return fallback_message(msg="plugins_not_found")
items = []
for plugin in plugins:
items.append(
plugin_item(
name=plugin["name"],
version=plugin["version"],
description=plugin["description"],
is_deletable=plugin["is_deletable"],
page=plugin["page"],
plugin_type=plugin["type"],
)
)
return {
"type": "card",
"widgets": [
title_widget(
title="plugins_list_title", # keep it (a18n)
),
subtitle_widget(
subtitle="plugins_list_subtitle", # keep it (a18n)
),
tabulator_widget(
id="table-plugins",
layout="fitColumns",
columns=columns,
items=items,
filters=plugins_filter(types=types),
),
],
}
def plugins_builder(plugins: Optional[list] = None, types: Optional[list] = None) -> list:
return [plugins_list(plugins=plugins, types=types)]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
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": ""},
]
types = ["core", "external", "pro"]
output = plugins_builder(plugins)
builder = plugins_builder(plugins=plugins, types=types)
save_builder(page_name="plugins", output=builder, script_name="plugins")

View file

@ -4,7 +4,9 @@ import Grid from "@components/Widget/Grid.vue";
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 Text from "@components/Widget/Text.vue";
import Tabulator from "@components/Widget/Tabulator.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import { useEqualStr } from "@utils/global.js";
@ -73,6 +75,14 @@ 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" />
<Subtitle
v-if="useEqualStr(widget.type, 'Subtitle')"
v-bind="widget.data"
/>
<Tabulator
v-if="useEqualStr(widget.type, 'Tabulator')"
v-bind="widget.data"
/>
<ListDetails
v-if="useEqualStr(widget.type, 'ListDetails')"
v-bind="widget.data"

View file

@ -251,17 +251,15 @@
"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_pro_plugin_desc": "Pro plugin",
"plugins_core_plugin_desc": "Core plugin",
"plugins_external_plugin_desc": "External plugin",
"plugins_redirect_page_desc": "Redirect to plugin page",
"plugins_search": "Search plugin",
"plugins_search_desc": "Search the plugin by his name",
"plugins_type": "Plugin type",
"plugins_type_desc": "Only show plugins of the chosen type",
"plugins_delete_desc": "Delete plugin",
"plugins_modal_delete_title": "Delete plugin",
"plugins_modal_delete_confirm": "Are you sure you want to delete the plugin below ?",
"plugins_delete_title": "Delete plugin",
"plugins_delete_subtitle": "Are you sure you want to delete the plugin below ?",
"plugins_not_found": "No plugins found",
"plugins_list_title": "Plugins",
"plugins_list_subtitle": "Get details and manage your plugins.",
"reports_title": "Reports",
"reports_subtitle": "List of reports catch by BunkerWeb.",
"reports_not_found": "No reports found",

View file

@ -14,793 +14,24 @@ const plugins = reactive({
builder: "",
});
// Case we click on redirect icon, go to the redirect plugin page
function redirectPlugin() {
window.addEventListener(
"click",
(e) => {
// Case avoid redirect
if (e.target.tagName !== "A") return;
if (!e.target.closest("[data-plugin-redirect]")) return;
if (
e.target
.closest("[data-plugin-redirect]")
?.getAttribute("data-plugin-redirect") !== "true" ||
!e.target.querySelector('[data-svg="redirect"]')
)
return;
// Prepare redirect
const pluginId = e.target
.closest("[data-plugin-id]")
.getAttribute("data-plugin-id");
window.location.href = `./${pluginId}`;
},
true
);
}
// Case we click on redirect icon, go to the redirect plugin page
function deletePlugin() {
const deleteData = {
name: "pluginName",
id: "pluginId",
type: "pluginType",
operation: "delete",
};
window.addEventListener(
"click",
(e) => {
// Case avoid redirect
if (e.target.tagName !== "A") return;
if (!e.target.closest("[data-plugin-delete]")) return;
if (
e.target
.closest("[data-plugin-delete]")
.getAttribute("data-plugin-delete") !== "true" ||
!e.target.querySelector('[data-svg="trash"]')
)
return;
// Update data
deleteData.name = e.target
.closest("[data-plugin-name]")
.getAttribute("data-plugin-name");
deleteData.id = e.target
.closest("[data-plugin-id]")
.getAttribute("data-plugin-id");
deleteData.type = e.target
.closest("[data-plugin-type]")
.getAttribute("data-plugin-type");
// Attach data to submit button (need to check attributes data-delete-plugin-submit)
const submitBtn = document.querySelector("[data-delete-plugin-submit]");
submitBtn.setAttribute("data-submit-form", JSON.stringify(deleteData));
// Prepare and show modal
const modal = document.querySelector("#modal-delete-plugin");
const modalPluginName = modal.querySelector("[data-modal-plugin-name]");
modalPluginName.textContent = deleteData.name;
modal.classList.remove("hidden");
},
true
);
}
onBeforeMount(() => {
// Get builder data
const dataAtt = "data-server-builder";
const dataEl = document.querySelector(`[${dataAtt}]`);
const data =
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
? JSON.parse(dataEl.getAttribute(dataAtt))
? JSON.parse(atob(dataEl.getAttribute(dataAtt)))
: {};
plugins.builder = data;
});
onMounted(() => {
redirectPlugin();
deletePlugin();
useGlobal();
});
const builder = [
{
type: "modal",
id: "modal-delete-plugin",
widgets: [
{
type: "Title",
data: {
title: "plugins_modal_delete_title",
type: "modal",
},
},
{
type: "Text",
data: {
text: "plugins_modal_delete_confirm",
},
},
{
type: "Text",
data: {
text: "",
bold: true,
attrs: {
"data-modal-plugin-name": "true",
},
},
},
{
type: "ButtonGroup",
data: {
buttons: [
{
id: "delete-plugin-btn",
text: "action_close",
disabled: false,
color: "close",
size: "normal",
attrs: {
"data-hide-el": "modal-delete-plugin",
},
},
{
id: "delete-plugin-btn",
text: "action_delete",
disabled: false,
color: "delete",
size: "normal",
attrs: {
"data-delete-plugin-submit": "",
},
},
],
groupClass: "btn-group-modal",
},
},
],
},
{
type: "card",
widgets: [
{
type: "Title",
data: {
title: "dashboard_plugins",
type: "card",
},
},
{
type: "ListDetails",
data: {
filters: [
{
filter: "default",
filterName: "keyword",
type: "keyword",
value: "",
keys: ["text"],
field: {
id: "filter-plugin-name",
value: "",
type: "text",
name: "filter-plugin-name",
label: "plugins_search",
placeholder: "inp_keyword",
isClipboard: false,
popovers: [
{
text: "plugins_search_desc",
iconName: "info",
},
],
columns: {
pc: 3,
tablet: 4,
mobile: 12,
},
},
},
{
filter: "default",
filterName: "type",
type: "select",
value: "all",
keys: ["type"],
field: {
id: "filter-plugin-type",
value: "all",
values: ["all", "pro", "core", "external"],
name: "filter-plugin-type",
onlyDown: true,
label: "plugins_type",
maxBtnChars: 24,
popovers: [
{
text: "plugins_type_desc",
iconName: "info",
},
],
columns: {
pc: 3,
tablet: 4,
mobile: 12,
},
},
},
],
details: [
{
text: "General",
type: "pro",
attrs: {
"data-plugin-id": "general",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "pro",
"data-plugin-name": "General",
},
disabled: true,
popovers: [
{
text: "plugins_pro_plugin_desc",
iconName: "crown",
},
],
},
{
text: "Antibot",
type: "core",
attrs: {
"data-plugin-id": "antibot",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Antibot",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Auth basic",
type: "core",
attrs: {
"data-plugin-id": "authbasic",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Auth basic",
},
disabled: false,
popovers: [],
},
{
text: "Backup",
type: "pro",
attrs: {
"data-plugin-id": "backup",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "pro",
"data-plugin-name": "Backup",
},
disabled: true,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
{
text: "plugins_pro_plugin_desc",
iconName: "crown",
},
],
},
{
text: "Bad behavior",
type: "external",
attrs: {
"data-plugin-id": "badbehavior",
"data-plugin-delete": "true",
"data-plugin-redirect": "true",
"data-plugin-type": "external",
"data-plugin-name": "Bad behavior",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
{
text: "plugins_delete_desc",
iconName: "trash",
},
],
},
{
text: "Blacklist",
type: "core",
attrs: {
"data-plugin-id": "blacklist",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Blacklist",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Brotli",
type: "core",
attrs: {
"data-plugin-id": "brotli",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Brotli",
},
disabled: false,
popovers: [],
},
{
text: "BunkerNet",
type: "core",
attrs: {
"data-plugin-id": "bunkernet",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "BunkerNet",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "CORS",
type: "core",
attrs: {
"data-plugin-id": "cors",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "CORS",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Client cache",
type: "core",
attrs: {
"data-plugin-id": "clientcache",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Client cache",
},
disabled: false,
popovers: [],
},
{
text: "Country",
type: "core",
attrs: {
"data-plugin-id": "country",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Country",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Custom HTTPS certificate",
type: "core",
attrs: {
"data-plugin-id": "customcert",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Custom HTTPS certificate",
},
disabled: false,
popovers: [],
},
{
text: "DB",
type: "core",
attrs: {
"data-plugin-id": "db",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "DB",
},
disabled: false,
popovers: [],
},
{
text: "DNSBL",
type: "core",
attrs: {
"data-plugin-id": "dnsbl",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "DNSBL",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Errors",
type: "core",
attrs: {
"data-plugin-id": "errors",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Errors",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Greylist",
type: "core",
attrs: {
"data-plugin-id": "greylist",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Greylist",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Gzip",
type: "core",
attrs: {
"data-plugin-id": "gzip",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Gzip",
},
disabled: false,
popovers: [],
},
{
text: "HTML injection",
type: "core",
attrs: {
"data-plugin-id": "inject",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "HTML injection",
},
disabled: false,
popovers: [],
},
{
text: "Headers",
type: "core",
attrs: {
"data-plugin-id": "headers",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Headers",
},
disabled: false,
popovers: [],
},
{
text: "Jobs",
type: "core",
attrs: {
"data-plugin-id": "jobs",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Jobs",
},
disabled: false,
popovers: [],
},
{
text: "Let's Encrypt",
type: "core",
attrs: {
"data-plugin-id": "letsencrypt",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Let's Encrypt",
},
disabled: false,
popovers: [],
},
{
text: "Limit",
type: "core",
attrs: {
"data-plugin-id": "limit",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Limit",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Metrics",
type: "core",
attrs: {
"data-plugin-id": "metrics",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Metrics",
},
disabled: false,
popovers: [],
},
{
text: "Miscellaneous",
type: "core",
attrs: {
"data-plugin-id": "misc",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Miscellaneous",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "ModSecurity",
type: "core",
attrs: {
"data-plugin-id": "modsecurity",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "ModSecurity",
},
disabled: false,
popovers: [],
},
{
text: "PHP",
type: "core",
attrs: {
"data-plugin-id": "php",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "PHP",
},
disabled: false,
popovers: [],
},
{
text: "Pro",
type: "core",
attrs: {
"data-plugin-id": "pro",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Pro",
},
disabled: false,
popovers: [],
},
{
text: "Real IP",
type: "core",
attrs: {
"data-plugin-id": "realip",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Real IP",
},
disabled: false,
popovers: [],
},
{
text: "Redirect",
type: "core",
attrs: {
"data-plugin-id": "redirect",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Redirect",
},
disabled: false,
popovers: [],
},
{
text: "Redis",
type: "core",
attrs: {
"data-plugin-id": "redis",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Redis",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Reverse proxy",
type: "core",
attrs: {
"data-plugin-id": "reverseproxy",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Reverse proxy",
},
disabled: false,
popovers: [],
},
{
text: "Reverse scan",
type: "core",
attrs: {
"data-plugin-id": "reversescan",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Reverse scan",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
{
text: "Self-signed certificate",
type: "core",
attrs: {
"data-plugin-id": "selfsigned",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Self-signed certificate",
},
disabled: false,
popovers: [],
},
{
text: "Sessions",
type: "core",
attrs: {
"data-plugin-id": "sessions",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "Sessions",
},
disabled: false,
popovers: [],
},
{
text: "UI",
type: "core",
attrs: {
"data-plugin-id": "ui",
"data-plugin-delete": "false",
"data-plugin-redirect": "false",
"data-plugin-type": "core",
"data-plugin-name": "UI",
},
disabled: false,
popovers: [],
},
{
text: "Whitelist",
type: "core",
attrs: {
"data-plugin-id": "whitelist",
"data-plugin-delete": "false",
"data-plugin-redirect": "true",
"data-plugin-type": "core",
"data-plugin-name": "Whitelist",
},
disabled: false,
popovers: [
{
text: "plugins_redirect_page_desc",
iconName: "redirect",
},
],
},
],
columns: {
pc: 4,
tablet: 6,
mobile: 12,
},
},
},
],
},
];
</script>
<template>
<DashboardLayout>
<BuilderPlugins v-if="builder" :builder="builder" />
<BuilderPlugins v-if="plugins.builder" :builder="plugins.builder" />
</DashboardLayout>
</template>

File diff suppressed because one or more lines are too long