add instances components + start instances page

* create global components that can work with instances card (DetailList.vue, IconStatus, TitleCardContent, ButtonGroup...)
* update some others widgets to work with Container of Flex component instead of own container element and class
* create widget Instances using subcomponents
* add an instances page with a basic instance card + define the translation key
* add translation key for card details
* add missing JSdoc on some components
* update or add some css
This commit is contained in:
Jordan Blasenhauer 2024-05-23 17:51:56 +02:00
parent 38e3b2465e
commit e199cae258
23 changed files with 718 additions and 188 deletions

View file

@ -807,17 +807,42 @@ body {
@apply capitalize-first hover:italic hover:brightness-90 block sm:px-4 py-1 lg:pt-1 lg:pb-1 text-sm tracking-wide font-normal transition duration-300 ease-in-out text-white dark:text-white;
}
/* STATUS COMPONENT */
.status-svg-container {
@apply absolute top-1 right-0;
}
.status-icon {
@apply h-6 w-6 rounded-full;
}
/* CONTENT COMPONENT */
.content-title {
@apply text-lg font-bold mb-2 col-span-12;
}
/* STAT COMPONENT */
/* CONTENT DETAIL LSIT COMPONENT */
.stat-container {
@apply col-span-12 relative;
.content-detail-list-container {
@apply grid grid-cols-12 gap-2 my-4;
}
.content-detail-list-item {
@apply flex items-center col-span-1 py-1 sm:py-0;
}
.content-detail-list-title {
@apply min-w-fit transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500;
}
.content-detail-list-subtitle {
@apply min-w-[2rem] break-all transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-gray-100;
}
/* STAT COMPONENT */
.stat-content-container.no-icon {
@apply mr-1;
}
@ -853,7 +878,11 @@ body {
}
.card-title {
@apply text-2xl font-bold mb-2 col-span-12;
@apply break-words max-w-[80%] font-bold mb-2 col-span-12 dark:text-white/90 transition duration-300 ease-in-out text-2xl;
}
.card-content-title {
@apply break-words max-w-[80%] mb-2 font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl;
}
/* CARD INFO */
@ -900,19 +929,23 @@ body {
@apply bg-white text-red-500 border border-red-500 hover:border-red-500/80 focus:border-red-500/80 hover:text-red-500/80 focus:text-red-500/80;
}
.btn.delete {
.btn.delete,
.btn.error {
@apply bg-red-500 hover:bg-red-500/80 focus:bg-red-500/80;
}
.btn.valid {
.btn.valid,
.btn.success {
@apply bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80;
}
.btn.edit {
.btn.edit,
.btn.warning {
@apply bg-yellow-500 hover:bg-yellow-500/80 focus:bg-yellow-500/80;
}
.btn.info {
.btn.info,
.btn.load {
@apply bg-sky-500 hover:bg-sky-500/80 focus:bg-sky-500/80;
}
@ -1387,21 +1420,25 @@ body {
/* BACKGROUND COLOR MODIFIER */
.success.status-icon,
.success.stat-svg-container,
.success.feedback-alert-wrap {
@apply bg-green-500;
}
.error.status-icon,
.error.stat-svg-container,
.error.feedback-alert-wrap {
@apply bg-red-500;
}
.warning.status-icon,
.warning.stat-svg-container,
.warning.feedback-alert-wrap {
@apply bg-yellow-500;
}
.info.status-icon,
.info.stat-svg-container,
.info.feedback-alert-wrap {
@apply bg-sky-500;

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,30 @@
<script setup>
import { reactive, onBeforeMount } from "vue";
// Containers
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
// Title
import TitleCard from "@components/Title/Card.vue";
import TitleCardContent from "@components/Title/CardContent.vue";
import TitleStat from "@components/Title/Stat.vue";
// Subtitle
import SubtitleStat from "@components/Subtitle/Stat.vue";
// Content
import ContentStat from "@components/Content/Stat.vue";
import ContentDetailList from "@components/Content/DetailList.vue";
// Icon
import IconStat from "@components/Icon/Stat.vue";
import IconStatus from "@components/Icon/Status.vue";
// Form
import Checkbox from "@components/Forms/Field/Checkbox.vue";
import Select from "@components/Forms/Field/Select.vue";
import Input from "@components/Forms/Field/Input.vue";
import Datepicker from "@components/Forms/Field/Datepicker.vue";
// Widget
import Button from "@components/Widget/Button.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import Stat from "@components/Widget/Stat.vue";
import TitleStat from "@components/Title/Stat.vue";
import ContentStat from "@components/Content/Stat.vue";
import SubtitleStat from "@components/Subtitle/Stat.vue";
import IconStat from "@components/Icon/Stat.vue";
import Instance from "@components/Widget/Instance.vue";
/**
@name Builder.vue
@ -74,6 +86,10 @@ const props = defineProps({
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<TitleCard v-if="widget.type === 'TitleCard'" v-bind="widget.data" />
<TitleCardContent
v-if="widget.type === 'TitleCardContent'"
v-bind="widget.data"
/>
<Checkbox v-if="widget.type === 'Checkbox'" v-bind="widget.data" />
<Select v-if="widget.type === 'Select'" v-bind="widget.data" />
<Input v-if="widget.type === 'Input'" v-bind="widget.data" />
@ -90,6 +106,16 @@ const props = defineProps({
v-bind="widget.data"
/>
<IconStat v-if="widget.type === 'IconStat'" v-bind="widget.data" />
<Instance v-if="widget.type === 'Instance'" v-bind="widget.data" />
<IconStatus v-if="widget.type === 'IconStatus'" v-bind="widget.data" />
<ContentDetailList
v-if="widget.type === 'ContentDetailList'"
v-bind="widget.data"
/>
<ButtonGroup
v-if="widget.type === 'ButtonGroup'"
v-bind="widget.data"
/>
</template>
</Grid>
</GridLayout>

View file

@ -0,0 +1,47 @@
<script setup>
import { computed, defineProps } from "vue";
/**
@name Content/DetailList.vue
@description This component is used to display key value information in a list.
@example
{
details : [{key: "Total Users", value: "100"}],
itemColumns: {pc: 12, tablet: 12, mobile: 12}
}
@param {array} details - The list of key value information. The key and value can be a translation key or a raw text.
@param {object} [itemColumns={pc: 12, tablet: 12, mobile: 12}] - Determine the position of the items in the grid system.
*/
const props = defineProps({
details: {
type: Array,
required: true,
},
itemColumns: {
type: Object,
required: false,
default: { pc: 12, tablet: 12, mobile: 12 },
},
});
const gridClass = computed(() => {
return props.itemColumns
? `col-span-${props.itemColumns.mobile} md:col-span-${props.itemColumns.tablet} lg:col-span-${props.itemColumns.pc}`
: "";
});
</script>
<template>
<ul v-if="props.details" :class="['content-detail-list-container']">
<li
v-for="item in props.details"
:class="['content-detail-list-item', gridClass]"
>
<span class="content-detail-list-title">
{{ $t(item["key"], item["key"]) }}
</span>
<span class="content-detail-list-subtitle">
{{ $t(item["value"], item["value"]) }}
</span>
</li>
</ul>
</template>

View file

@ -1,5 +1,6 @@
<script setup>
import Icons from "@/components/Widget/Icons.vue";
/**
@name Icon/Stat.vue
@description This component is a icon used with stats.

View file

@ -0,0 +1,58 @@
<script setup>
import { defineProps, computed } from "vue";
/**
@name Icon/Status.vue
@description This component is a icon used with status.
@example
{
id: "instance-1",
status: "success",
statusClass: "col-span-12",
}
@param {string} id - The id of the status icon.
@param {string} [status="info"] - The color of the icon between error, success, warning, info
@param {string} [statusClass=""] - Additional class, for example to use with grid system.
*/
const props = defineProps({
id: {
type: String,
required: false,
default: "1",
},
status: {
type: String,
required: true,
default: "info",
},
statusClass: {
type: String,
required: false,
default: "",
},
});
const statusDesc = computed(() => {
if (props.status === "success")
return ["dashboard_status_success", "status active or success."];
if (props.status === "error")
return ["dashboard_status_error", "status inactive or error."];
if (props.status === "warning")
return ["dashboard_status_warning", "status warning or alert."];
if (props.status === "info")
return ["dashboard_status_info", "status loading or waiting or unknown."];
});
</script>
<template>
<div :class="[props.statusClass, 'status-svg-container']">
<div
role="img"
:aria-describedby="`instance-status-${props.id}`"
:class="[props.status, 'status-icon']"
></div>
<p :id="`instance-status-${props.id}`" class="sr-only">
{{ $t(statusDesc[0], statusDesc[1]) }}
</p>
</div>
</template>

View file

@ -1,4 +1,17 @@
<script setup>
/**
@name Title/Card.vue
@description This component is a used as card title.
This can be used outside of the card container.
@example
{
title: "Total Users",
titleClass: "text-lg"
}
@param {string} title - Can be a translation key or by default raw text.
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
title: {
type: String,

View file

@ -0,0 +1,32 @@
<script setup>
/**
@name Title/CardContent.vue
@description This component is similar to TitleCard but with a lower size.
This can be used outside of the card container.
@example
{
title: "Total Users",
titleClass: "text-lg"
}
@param {string} title - Can be a translation key or by default raw text.
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
title: {
type: String,
required: true,
},
titleClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<h2 v-if="props.title" :class="[props.titleClass, 'card-content-title']">
{{ $t(props.title, props.title) }}
</h2>
</template>

View file

@ -1,19 +0,0 @@
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
titleClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<h1 v-if="props.title" :class="[props.titleClass, 'content-title']">
{{ $t(props.title, props.title) }}
</h1>
</template>

View file

@ -1,5 +1,5 @@
<script setup>
import { computed, ref, watch, onBeforeMount, onMounted } from 'vue';
import { computed, ref, watch, onBeforeMount, onMounted } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import { useEventStore } from "@store/event.js";
import Container from "@components/Widget/Container.vue";
@ -40,132 +40,156 @@ import Icons from "@components/Widget/Icons.vue";
const eventStore = useEventStore();
const props = defineProps({
id : {
type: String,
required: true,
},
// valid || delete || info
text : {
id: {
type: String,
required: true
required: true,
default: "",
},
type : {
// valid || delete || info
text: {
type: String,
required: true,
default: "",
},
type: {
type: String,
required: false,
default : "button"
default: "button",
},
disabled : {
disabled: {
type: Boolean,
required: false,
default : false
default: false,
},
// case we want only icon but we need to add accessibility data
hideText : {
hideText: {
type: Boolean,
required: false,
default : false
default: false,
},
color: {
type: String,
required: false,
default : "primary"
default: "primary",
},
// sm || normal || lg || xl
size: {
type: String,
required: false,
default : "normal"
default: "normal",
},
// Store on components/Icons/Button
// Check import ones
iconName : {
iconName: {
type: String,
required: false,
default : "",
default: "",
},
// Defined on input.css
iconColor : {
iconColor: {
type: String,
required: false,
default : ""
default: "",
},
// {"store" : <store_name>, "default" : <default_value>, "value" : <value_stored_on_click>, "target"<optional> : <target_id_element>, "valueExpanded" : "expanded_value"}
// type will add additionnal accessibility attributs to the button
// for example, if button open a modal : {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"}
// for example, if button open a modal : {"store" : "modal", "default" : "close", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"}
eventAttr: {
type: Object,
required: false,
default : {}
default: {},
},
tabId : {
tabId: {
type: [String, Number],
required: false,
default : ""
}
default: "",
},
containerClass: {
type: String,
required: false,
default: "",
},
});
const btnEl = ref();
const buttonClass = computed(() => {
return `btn ${props.color} ${props.size}`
})
return `btn ${props.color} ${props.size}`;
});
onMounted(() => {
updateData();
})
updateData();
});
watch(eventStore,() => {
updateData();
})
watch(eventStore, () => {
updateData();
});
function updateData(isClick = false) {
const isStore = props.eventAttr?.store ? true : false;
const isValue = props.eventAttr?.value ? true : false;
const isDefault = props.eventAttr?.default ? true : false;
const isStore = props.eventAttr?.store ? true : false;
const isValue = props.eventAttr?.value ? true : false;
const isDefault = props.eventAttr?.default ? true : false;
if (!isStore || !isValue || !isDefault) return;
if(!isStore || !isValue || !isDefault) return;
isClick
? eventStore.updateEvent(props.eventAttr.store, props.eventAttr.value)
: eventStore.addEvent(props.eventAttr.store, props.eventAttr.default);
isClick ? eventStore.updateEvent(props.eventAttr.store, props.eventAttr.value) : eventStore.addEvent(props.eventAttr.store, props.eventAttr.default);
try {
const expanded = props.eventAttr?.valueExpanded ? props.eventAttr.valueExpanded === eventStore.getEvent(props.eventAttr.store) ? 'true' : 'false' : false;
if(expanded) {
btnEl.value.setAttribute('aria-expanded', expanded);
}
if(!expanded) {
btnEl.value.removeAttribute('aria-expanded');
}
}catch(e) {
try {
const expanded = props.eventAttr?.valueExpanded
? props.eventAttr.valueExpanded ===
eventStore.getEvent(props.eventAttr.store)
? "true"
: "false"
: false;
if (expanded) {
btnEl.value.setAttribute("aria-expanded", expanded);
}
try {
if (!expanded) {
btnEl.value.removeAttribute("aria-expanded");
}
} catch (e) {}
try {
const controls = props.eventAttr?.target ? props.eventAttr.target : false;
if(controls) {
btnEl.value.setAttribute('aria-controls', controls);
}
if(!controls) {
btnEl.value.removeAttribute('aria-controls');
}
}catch(e) {
if (controls) {
btnEl.value.setAttribute("aria-controls", controls);
}
if (!controls) {
btnEl.value.removeAttribute("aria-controls");
}
} catch (e) {}
}
</script>
<template>
<Container :containerClass="`w-full m-2 ${props.containerClass}`" :columns="props.columns">
<button :type="props.type" ref="btnEl" @click="updateData(true)" :id="props.id"
:tabindex="props.tabId || contentIndex"
:class="[buttonClass]"
:disabled="props.disabled || false"
:aria-describedby="`${props.id}-text`"
<Container
:containerClass="`${props.containerClass}`"
:columns="props.columns"
>
<span :class="[props.hideText ? 'sr-only' : '',
props.iconName ? 'mr-2' : ''
]" :id="`${props.id}-text`">{{ $t(props.text, props.text) }}</span>
<Icons :iconName="props.iconName" :iconClass="'btn-svg'" :iconColor="props.iconColor" />
</button>
</Container>
<button
:type="props.type"
ref="btnEl"
@click="updateData(true)"
:id="props.id"
:tabindex="props.tabId || contentIndex"
:class="[buttonClass]"
:disabled="props.disabled || false"
:aria-describedby="`text-${props.id}`"
>
<span
:class="[props.hideText ? 'sr-only' : '', props.iconName ? 'mr-2' : '']"
:id="`text-${props.id}`"
>{{ $t(props.text, props.text) }}</span
>
<Icons
v-if="props.iconName"
:iconName="props.iconName"
:iconClass="'btn-svg'"
:iconColor="props.iconColor"
/>
</button>
</Container>
</template>

View file

@ -0,0 +1,74 @@
<script setup>
import { computed, ref, watch, onBeforeMount, onMounted } from "vue";
import { contentIndex } from "@utils/tabindex.js";
import Flex from "@components/Widget/Flex.vue";
import Button from "@components/Widget/Button.vue";
/**
@name Widget/ButtonGroup.vue
@description This component allow to display multiple buttons in the same row using flex.
We can define additional class too for the flex container.
We need a list of buttons to display.
@example
{
id: "group-btn",
groupClass : "justify-center",
buttons: [
{
id: "open-modal-btn",
text: "Open modal",
disabled: false,
hideText: true,
color: "green",
size: "normal",
iconName: "modal",
iconColor: "white",
eventAttr: {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"},
},
{
id: "close-modal-btn",
text: "Close modal",
disabled: false,
hideText: true,
color: "red",
size: "normal",
iconName: "modal",
iconColor: "white",
eventAttr: {"store" : "modal", "value" : "close", "target" : "modal_id", "valueExpanded" : "close"},
},
],
}
@param {string} id
@param {string} [groupClass="justify-center align-center"] - Additional class for the flex container
@param {array} buttons - List of buttons to display. Button component is used.
*/
const props = defineProps({
id: {
type: String,
required: false,
default: "group-btn",
},
groupClass: {
type: String,
required: false,
default: "justify-center align-center",
},
buttons: {
type: Array,
required: true,
default: [],
},
});
</script>
<template>
<Flex v-if="props.buttons.length > 0" :flexClass="props.groupClass">
<Button
v-for="(button, id) in props.buttons"
:key="button.id"
v-bind="button"
:class="[id === props.buttons.length - 1 ? '' : 'mr-2']"
/>
</Flex>
</template>

View file

@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue';
import { computed } from "vue";
/**
@name Widget/Container.vue
@ -17,25 +17,30 @@ import { computed } from 'vue';
*/
const props = defineProps({
containerClass : {
containerClass: {
type: String,
required: false,
default : ""
default: "",
},
columns: {
type: [Object, Boolean],
required: false,
default: false
default: false,
},
})
});
const gridClass = computed(() => {
return props.columns ? `col-span-${props.columns.mobile} md:col-span-${props.columns.tablet} lg:col-span-${props.columns.pc}` : '';
})
return props.columns
? `col-span-${props.columns.mobile} md:col-span-${props.columns.tablet} lg:col-span-${props.columns.pc}`
: "";
});
</script>
<template>
<div data-container :class="[props.containerClass ? props.containerClass : '', gridClass]">
<div
data-container
:class="[props.containerClass ? props.containerClass : '', gridClass]"
>
<slot></slot>
</div>
</template>
</template>

View file

@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue';
import { computed } from "vue";
/**
@name Widget/Flex.vue
@ -15,21 +15,20 @@ import { computed } from 'vue';
*/
const props = defineProps({
flexClass: {
type: String,
required: false,
default : "flex-start"
},
})
flexClass: {
type: String,
required: false,
default: "flex-start",
},
});
const flexClass = computed(() => {
return `w-full flex ${props.flexClass}`;
})
return `w-full flex ${props.flexClass}`;
});
</script>
<template>
<div data-flex :class="[flexClass]">
<div data-flex :class="[flexClass]">
<slot></slot>
</div>
</div>
</template>

View file

@ -1,6 +1,4 @@
<script setup>
import { computed } from "vue";
/**
@name Widget/Grid.vue
@description This component is a basic container that can be used to wrap other components.

View file

@ -0,0 +1,82 @@
<script setup>
import { defineProps } from "vue";
import Container from "@components/Widget/Container.vue";
import TitleCardContent from "@components/Title/CardContent.vue";
import IconStatus from "@components/Icon/Status.vue";
import ContentDetailList from "@components/Content/DetailList.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
/**
@name Widget/Instance.vue
@description This component is an instance widget.
This component is using the Container, TitleCardContent, IconStatus, ContentDetailList and ButtonGroup components.
@example
{
id: "instance-1",
title: "Instance 1",
status: "success",
details: [
{ key: "Version", value: "1.0.0" },
{ key: "Status", value: "Running" },
{ key: "Created", value: "2021-01-01" },
],
buttons : [
{
id: "open-modal-btn",
text: "Open modal",
disabled: false,
hideText: true,
color: "green",
size: "normal",
iconName: "modal",
iconColor: "white",
eventAttr: {"store" : "modal", "value" : "open", "target" : "modal_id", "valueExpanded" : "open"},
},
]
}
@param {string} id
@param {string} title
@param {string} status
@param {array} details - List of details to display
@param {array} buttons - List of buttons to display
*/
const props = defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
status: {
type: String,
required: true,
default: "",
},
details: {
type: Array,
required: true,
default: [],
},
buttons: {
type: Array,
required: true,
default: [],
},
});
</script>
<template>
<Container :columns="{ pc: 12, tablet: 12, mobile: 12 }">
<IconStatus :id="props.title" :status="props.status" />
<TitleCardContent :title="props.title" />
<ContentDetailList :details="props.details" />
<ButtonGroup
:id="props.id"
:buttons="props.buttons"
:groupClass="'justify-end align-center'"
/>
</Container>
</template>

View file

@ -1,4 +1,5 @@
<script setup>
import Container from "@components/Widget/Container.vue";
import TitleStat from "@components/Title/Stat.vue";
import ContentStat from "@components/Content/Stat.vue";
import SubtitleStat from "@components/Subtitle/Stat.vue";
@ -65,7 +66,7 @@ const props = defineProps({
});
</script>
<template>
<div :class="['stat-container', props.statClass]">
<Container :columns="{ pc: 12, tablet: 12, mobile: 12 }">
<!-- text -->
<div
:class="[
@ -86,5 +87,5 @@ const props = defineProps({
:iconName="props.iconName"
:iconColor="props.iconColor"
/>
</div>
</Container>
</template>

View file

@ -72,6 +72,10 @@
"dashboard_banner_link_text_1": "Check BunkerWeb Panel",
"dashboard_banner_link_text_2": "demo wep app !",
"dashboard_banner_link_text_3": "website !",
"dashboard_status_success": "status active or success.",
"dashboard_status_error": "status inactive or error.",
"dashboard_status_warning": "status warning or alert.",
"dashboard_status_info": "status loading or waiting or unknown.",
"action_send": "send {name}",
"action_disable": "disable {name}",
"action_enable": "enable {name}",
@ -103,5 +107,11 @@
"home_all_methods_included": "all methods included",
"home_plugins": "plugins",
"home_no_error": "no error",
"home_errors_found": "errors found"
"home_errors_found": "errors found",
"instance_hostname": "hostname",
"instances_method": "method",
"instances_port": "port",
"instances_status": "status",
"instances_active": "active",
"instances_inactive": "inactive"
}

View file

@ -1,54 +1,53 @@
{
"dashboard_logo_alt": "Image du logo BunkerWeb",
"dashboard_logo_link_label": "Rediriger vers la page d'accueil",
"dashboard_logo_alt": "BunkerWeb logo image",
"dashboard_logo_link_label": "Redirect to home page",
"dashboard_bw": "BunkerWeb",
"dashboard_docs": "documents",
"dashboard_docs": "docs",
"dashboard_blog": "blog",
"dashboard_privacy": "confidentialité",
"dashboard_license": "licence",
"dashboard_sitemap": "plan du site",
"dashboard_default": "défautt",
"dashboard_privacy": "privacy",
"dashboard_license": "license",
"dashboard_sitemap": "sitemap",
"dashboard_default": "default",
"dashboard_info": "info",
"dashboard_filter": "filtres",
"dashboard_advanced": "avancé",
"dashboard_loading": "chargement",
"dashboard_lang_dropdown_button_desc": "Basculer entre cacher/afficher le groupe de boutons radio (menu déroulant) pour changer la langue.",
"dashboard_home": "accueil",
"dashboard_filter": "filters",
"dashboard_advanced": "advanced",
"dashboard_loading": "loading",
"dashboard_lang_dropdown_button_desc": "Toggle hide/show radio group (dropdown) to change langage.",
"dashboard_manage_account": "manage account",
"dashboard_home": "home",
"dashboard_instances": "instances",
"dashboard_global_config": "config globale",
"dashboard_global_config": "global config",
"dashboard_services": "services",
"dashboard_configs": "configs",
"dashboard_plugins": "plugins",
"dashboard_jobs": "jobs",
"dashboard_bans": "bans",
"dashboard_actions": "actions",
"dashboard_account": "compte",
"dashboard_reports": "rapports",
"dashboard_account": "account",
"dashboard_reports": "reports",
"dashboard_cache": "cache",
"dashboard_logs": "journaux",
"dashboard_feedback_toggle_sidebar": "Basculer la barre latérale de feedback.",
"dashboard_feedback_close_sidebar": "Fermer la barre latérale de feedback.",
"dashboard_feedback_title": "Feedback",
"dashboard_feedback_subtitle": "Actions BunkerWeb",
"dashboard_menu_toggle_sidebar": "Basculer la barre latérale du menu.",
"dashboard_menu_close_sidebar": "Fermer la barre latérale du menu.",
"dashboard_menu_twitter_label": "Rediriger vers le Twitter de BunkerWeb",
"dashboard_menu_linkedin_label": "Rediriger vers le Linkedin de BunkerWeb",
"dashboard_menu_discord_label": "Rediriger vers le Discord de BunkerWeb",
"dashboard_menu_github_label": "Rediriger vers le Github de BunkerWeb",
"dashboard_menu_plugins_title": "Pages des plugins",
"dashboard_menu_plugins_none": "Vous voulez des plugins personnalisés ?",
"dashboard_menu_plugins_none_doc": "consulter la documentation",
"dashboard_menu_mode_light": "mode clair",
"dashboard_menu_mode_dark": "mode sombre",
"dashboard_menu_log_out": "se déconnecter",
"dashboard_logs": "logs",
"dashboard_feedback_toggle_sidebar": "Toggle feedback sidebar.",
"dashboard_feedback_close_sidebar": "Close feedback sidebar.",
"dashboard_feedback_title": "feedback",
"dashboard_feedback_subtitle": "BunkerWeb actions",
"dashboard_menu_toggle_sidebar": "Toggle menu sidebar.",
"dashboard_menu_close_sidebar": "Close menu sidebar.",
"dashboard_menu_twitter_label": "Redirect to BunkerWeb Twitter",
"dashboard_menu_linkedin_label": "Redirect to BunkerWeb Linkedin",
"dashboard_menu_discord_label": "Redirect to BunkerWeb Discord",
"dashboard_menu_github_label": "Redirect to BunkerWeb Github",
"dashboard_menu_plugins_title": "plugin pages",
"dashboard_menu_mode_light": "light mode",
"dashboard_menu_mode_dark": "dark mode",
"dashboard_menu_log_out": "log out",
"dashboard_ui": "ui",
"dashboard_scheduler": "scheduler",
"dashboard_autoconf": "autoconf",
"dashboard_core": "core",
"dashboard_global": "global",
"dashboard_news_toggle_sidebar": "Basculer la barre latérale des nouvelles.",
"dashboard_news_close_sidebar": "Fermer la barre latérale des nouvelles.",
"dashboard_news_toggle_sidebar": "Toggle news sidebar.",
"dashboard_news_close_sidebar": "Close news sidebar.",
"dashboard_news_title": "news",
"dashboard_news_subtitle": "Stay up to date !",
"dashboard_news_fetch_error": "Impossible to retrieve news",
@ -73,22 +72,10 @@
"dashboard_banner_link_text_1": "Check BunkerWeb Panel",
"dashboard_banner_link_text_2": "demo wep app !",
"dashboard_banner_link_text_3": "website !",
"home_version_is_latest": "is the latest version",
"home_version_latest_version": "latest version",
"home_version": "version",
"home_internal": "internal",
"home_external": "external",
"home_card_link_label": "Redirect to page with related data.",
"instances_hostname": "hostname",
"instances_hostname_placeholder": "bwapi",
"instances_method": "method",
"instances_port": "port",
"instances_port_placeholder": "5000",
"instances_active": "Instance is active.",
"instances_inactive": "Instance is inactive.",
"instances_modal_delete_msg": "Are you sure to delete instance with hostname {hostname} ?",
"instances_server_name": "server name",
"instances_server_name_placeholder": "www.example.com",
"dashboard_status_success": "status active or success.",
"dashboard_status_error": "status inactive or error.",
"dashboard_status_warning": "status warning or alert.",
"dashboard_status_info": "status loading or waiting or unknown.",
"action_send": "send {name}",
"action_disable": "disable {name}",
"action_enable": "enable {name}",
@ -105,5 +92,26 @@
"action_ping": "ping {name}",
"action_reload": "reload {name}",
"action_upload": "upload {name}",
"action_delete_all": "delete all {name}"
}
"action_delete_all": "delete all {name}",
"home_version": "version",
"home_all_features_available": "all features are available",
"home_upgrade_pro": "upgrade to pro",
"home_pro": "pro",
"home_free": "free",
"home_version_number": "version number",
"home_latest_version": "latest version",
"home_upgrade_available": "upgrade available",
"home_instances": "instances",
"home_total_number": "total number",
"home_services": "services",
"home_all_methods_included": "all methods included",
"home_plugins": "plugins",
"home_no_error": "no error",
"home_errors_found": "errors found",
"instance_hostname": "hostname",
"instances_method": "method",
"instances_port": "port",
"instances_status": "status",
"instances_active": "active",
"instances_inactive": "inactive"
}

View file

@ -23,7 +23,6 @@ const builder = [
{
// we are starting with the top level container name
// this can be a "card", "modal", "table"... etc
type: "card",
containerColumns: { pc: 12, tablet: 12, mobile: 12 },
// Each widget need a name (here type) and associated data
// We need to send specific data for each widget type
@ -71,7 +70,6 @@ const builder = [
containerClass: "", // tailwind css grid class (items-start, ...)
containerColumns: { pc: 12, tablet: 12, mobile: 12 },
// container title
title: "My awesome card",
// Each widget need a name (here type) and associated data
// We need to send specific data for each widget type
widgets: [
@ -93,8 +91,8 @@ const builder = [
{
type: "ContentStat",
data: {
value: "20",
valueClass: "col-span-12",
stat: "20",
statClass: "col-span-12",
},
},
{
@ -106,12 +104,58 @@ const builder = [
},
],
},
{
// we are starting with the top level container name
// this can be a "card", "modal", "table"... etc
type: "card",
containerClass: "", // tailwind css grid class (items-start, ...)
containerColumns: { pc: 12, tablet: 12, mobile: 12 },
// container title
// Each widget need a name (here type) and associated data
// We need to send specific data for each widget type
widgets: [
{
type: "Instance",
data: {
details: [
{ key: "HOSTNAME", value: "www.example.com" },
{ key: "METHOD", value: "UI" },
{ key: "PORT", value: "1084" },
{ key: "STATUS", value: "active" },
],
status: "success",
title: "www.example.com",
buttons: [
{
text: "reload",
color: "edit",
size: "normal",
},
{
text: "Stop",
color: "error",
size: "normal",
eventAttr: {
store: "modal",
default: "close",
value: "open",
target: "modal_id",
valueExpanded: "open",
},
},
],
},
},
],
},
];
</script>
<template>
<div class="bg-secondary flex flex-col items-center justify-center h-full">
<div style="width: 600px">
<div
class="bg-secondary flex flex-col items-center justify-center h-full gap-4"
>
<div style="width: 600px" class="gap-4 grid grid-cols-12">
<Builder :builder="builder" />
</div>
</div>

View file

@ -0,0 +1,55 @@
<script setup>
import { reactive, onBeforeMount } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import Builder from "@components/Builder.vue";
/**
@name Page/Home.vue
@description This component is the home page.
This page displays an overview of multiple stats related to BunkerWeb.
*/
const home = reactive({
builder: "",
});
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))
: {};
home.builder = data;
});
// const data = [
// {
// type: "Instance",
// data: {
// details: [
// { key: <instances_hostname="hostname">, value: "www.example.com" },
// { key: <instances_method="method">, value: <dashboard_ui> or <dashboard_scheduler>...},
// { key: <instances_port="port">, value: "1084" },
// { key: <instances_status="status">, value: <instances_active="active"> or <instances_inactive="inactive"> },
// ],
// status: "success",
// title: "www.example.com",
// buttons: [
// {
// text: <action_*>,
// color: "edit",
// size: "normal",
// },
// ],
// },
// },
// ];
</script>
<template>
<DashboardLayout>
<Builder v-if="home.builder" :builder="home.builder" />
</DashboardLayout>
</template>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/images/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 | DASHBOARD</title>
</head>
<body>
<div class="hidden" data-server-global='{"username" : "admin"}'></div>
<div class="hidden"
data-server-flash='[{"type" : "success", "title" : "title", "message" : "Success feedback"}, {"type" : "error", "title" : "title", "message" : "Error feedback"}, {"type" : "warning", "title" : "title", "message" : "Warning feedback"}, {"type" : "info", "title" : "title", "message" : "Info feedback"}]'>
</div>
<div class="hidden"
data-server-builder='[{"type":"card", "link" : "/services", "containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat", "link": "https://github.com/bunkerity/bunkerweb","data":{"title":"home_version","subtitle":"home_all_features_available","subtitleColor":"success","value":"home_pro","iconName":"crown","iconColor":"amber"}}]},{"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_version_number","subtitle":"home_latest_version","subtitleColor":"success","value":"1.5.7","iconName":"wire","iconColor":"teal"}}]},{"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_instances","subtitle":"home_total_number","subtitleColor":"info", "value":"1","iconName":"box","iconColor":"dark"}}]}, {"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_services","subtitle":"home_all_methods_included","subtitleColor":"info","value":"2","iconName":"disk","iconColor":"orange"}}]}, {"type":"card","containerClass":"","containerColumns":{"pc":4,"tablet":6,"mobile":12},"widgets":[{"type":"Stat","data":{"title":"home_plugins","subtitle":"home_no_error","subtitleColor":"success","value":"42","iconName":"puzzle","iconColor":"yellow"}}]}]'>
</div>
<div id="app"></div>
<script type="module" src="instances.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { getI18n } from "@utils/lang.js";
import Instances from "./Instances.vue";
const pinia = createPinia();
createApp(Instances)
.use(pinia)
.use(getI18n(["dashboard", "action", "instances"]))
.mount("#app");

View file

@ -14,7 +14,7 @@ import fr from "@lang/fr.json" assert { type: "json" };
const availablesLangs = ["en", "fr"];
function getAllLang() {
return { en: en, fr : fr };
return { en: en, fr: fr };
}
function getAllLangCurrPage(pagesArr) {
@ -42,6 +42,8 @@ export function getI18n(pagesArr = []) {
fallbackLocale: "en",
messages, // set locale messages
availableLocales: availablesLangs,
fallbackWarn: false,
missingWarn: false,
});
return i18n;
@ -68,7 +70,7 @@ export function getLocalLang() {
navigator.languages &&
navigator.languages > 0 &&
availablesLangs.indexOf(
navigator.languages[0].split("-")[0].toLowerCase(),
navigator.languages[0].split("-")[0].toLowerCase()
) !== -1
) {
return navigator.languages[0].split("-")[0].toLowerCase();