mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 07:19:26 +00:00
Frontend refactor and additions
This commit is contained in:
parent
5de99b32cd
commit
2a9813e7ac
12 changed files with 195 additions and 26 deletions
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import MainMenu from '@/components/base/MainMenu.vue'
|
import MainMenu from '@/components/base/MainMenu.vue'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, onUpdated, ref } from 'vue'
|
||||||
import { RouterView, useRoute } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
import { useDeviceStore } from './stores/device'
|
import { useDeviceStore } from './stores/device'
|
||||||
import { isLocal } from './services/ApiService'
|
import { isLocal } from './services/ApiService'
|
||||||
|
|
@ -30,16 +30,20 @@ const route = useRoute()
|
||||||
const handshake = ref(false)
|
const handshake = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// Setting device uuid from localstorage
|
||||||
|
// If not present in LocalStorage a new uuidV4 will be generated
|
||||||
device.uuid()
|
device.uuid()
|
||||||
|
|
||||||
const hsReq = await device.remoteHandshake()
|
const hsReq = await device.remoteHandshake()
|
||||||
handshake.value = hsReq
|
handshake.value = hsReq
|
||||||
|
|
||||||
device.$subscribe((mutation, state) => {
|
device.$subscribe((mutation, state) => {
|
||||||
console.log(mutation)
|
if (device.key()) handshake.value = true
|
||||||
})
|
})
|
||||||
// Setting device uuid from localstorage
|
})
|
||||||
// If not present in LocalStorage a new uuidV4 will be generated
|
|
||||||
|
onUpdated(() => {
|
||||||
|
console.log(device.key())
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ function toggleAccordion(open = false) {
|
||||||
header {
|
header {
|
||||||
@apply grid
|
@apply grid
|
||||||
grid-cols-[1fr_auto]
|
grid-cols-[1fr_auto]
|
||||||
px-4 py-2;
|
px-4 py-2
|
||||||
|
cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion__wrapper {
|
.accordion__wrapper {
|
||||||
|
|
|
||||||
38
fe/src/components/base/LoadComp.vue
Normal file
38
fe/src/components/base/LoadComp.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="loading" class="loading-component">
|
||||||
|
<span v-if="text">
|
||||||
|
{{ text }}
|
||||||
|
</span>
|
||||||
|
<IconLoader3 class="duration-1000 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { IconLoader3 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
loading: Boolean,
|
||||||
|
text: String,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
&:has(.loading-component) {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-component {
|
||||||
|
@apply absolute
|
||||||
|
inset-0
|
||||||
|
size-full
|
||||||
|
flex gap-2
|
||||||
|
flex-col
|
||||||
|
justify-center
|
||||||
|
items-center
|
||||||
|
text-sm
|
||||||
|
bg-black/50
|
||||||
|
backdrop-blur-md;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -21,10 +21,18 @@
|
||||||
<AlertComp v-if="server.status === 'unauthorized'" variant="info">
|
<AlertComp v-if="server.status === 'unauthorized'" variant="info">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<strong>Access requested</strong>
|
<strong>Access requested</strong>
|
||||||
<p>
|
<ul class="mb-4">
|
||||||
Navigate to <em class="font-semibold">http://localhost:6970/devices</em> on your pc to
|
<li>Navigate to <em class="font-semibold">http://localhost:6970/devices</em>.</li>
|
||||||
authorize.
|
<li>
|
||||||
</p>
|
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
|
||||||
|
Click on
|
||||||
|
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
|
||||||
|
<IconLink class="size-4" /> Link device
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>Enter the the pin shown on the desktop in the dialog that will appear.</li>
|
||||||
|
</ul>
|
||||||
<template v-if="server.link == 'checking'">
|
<template v-if="server.link == 'checking'">
|
||||||
<div class="grid grid-cols-[2rem_1fr] gap-2">
|
<div class="grid grid-cols-[2rem_1fr] gap-2">
|
||||||
<IconReload class="animate-spin" />
|
<IconReload class="animate-spin" />
|
||||||
|
|
@ -53,11 +61,13 @@
|
||||||
<h3>Server link pin:</h3>
|
<h3>Server link pin:</h3>
|
||||||
<form class="grid gap-4" @submit.prevent="decryptKey()">
|
<form class="grid gap-4" @submit.prevent="decryptKey()">
|
||||||
<input
|
<input
|
||||||
|
ref="linkPinInput"
|
||||||
class="input"
|
class="input"
|
||||||
id="input-pin"
|
id="input-pin"
|
||||||
type="text"
|
type="text"
|
||||||
pattern="[0-9]{4}"
|
pattern="[0-9]{4}"
|
||||||
v-model="server.inputPin"
|
v-model="server.inputPin"
|
||||||
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<ButtonComp variant="primary">Enter</ButtonComp>
|
<ButtonComp variant="primary">Enter</ButtonComp>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -75,7 +85,7 @@
|
||||||
// - - if checkAccess -> pingLink -> check for device.tmp (go)
|
// - - if checkAccess -> pingLink -> check for device.tmp (go)
|
||||||
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status
|
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status
|
||||||
|
|
||||||
import { IconKey, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
|
import { IconKey, IconLink, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
|
||||||
import AlertComp from '../base/AlertComp.vue'
|
import AlertComp from '../base/AlertComp.vue'
|
||||||
import ButtonComp from '../base/ButtonComp.vue'
|
import ButtonComp from '../base/ButtonComp.vue'
|
||||||
import { onMounted, onUpdated, reactive, ref } from 'vue'
|
import { onMounted, onUpdated, reactive, ref } from 'vue'
|
||||||
|
|
@ -89,6 +99,7 @@ import { appUrl } from '@/services/ApiService'
|
||||||
const device = useDeviceStore()
|
const device = useDeviceStore()
|
||||||
|
|
||||||
const linkPinDialog = ref()
|
const linkPinDialog = ref()
|
||||||
|
const linkPinInput = ref()
|
||||||
|
|
||||||
const server = reactive({
|
const server = reactive({
|
||||||
host: '',
|
host: '',
|
||||||
|
|
@ -105,6 +116,8 @@ onMounted(async () => {
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
if (!server.status) checkServerStatus()
|
if (!server.status) checkServerStatus()
|
||||||
|
|
||||||
|
if (server.status === 'authorized' && server.inputPin) server.inputPin = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
async function checkServerStatus(request = true) {
|
async function checkServerStatus(request = true) {
|
||||||
|
|
@ -150,6 +163,7 @@ function pingLink() {
|
||||||
server.encryptedKey = encryptedKey
|
server.encryptedKey = encryptedKey
|
||||||
|
|
||||||
linkPinDialog.value.toggleDialog(true)
|
linkPinDialog.value.toggleDialog(true)
|
||||||
|
linkPinInput.value.focus()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,13 @@
|
||||||
|
|
||||||
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
|
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
|
||||||
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
|
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
|
||||||
<span class="flex gap-4"> <IconDevices />Remote devices </span>
|
<span class="flex gap-4" v-if="Object.keys(remote.devices).length > 0">
|
||||||
|
<IconDevices />{{ Object.keys(remote.devices).length }}
|
||||||
|
{{ Object.keys(remote.devices).length > 1 ? 'Devices' : 'Device' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp>
|
<ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp>
|
||||||
</h4>
|
</h4>
|
||||||
<!-- {{ Object.keys(remote.devices).length }} -->
|
|
||||||
<template v-if="Object.keys(remote.devices).length > 0">
|
<template v-if="Object.keys(remote.devices).length > 0">
|
||||||
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
|
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
|
||||||
<div
|
<div
|
||||||
|
|
@ -57,6 +60,46 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<AccordionComp
|
||||||
|
class="w-full mt-8 border-t border-t-white/50"
|
||||||
|
title="How to connect a device?"
|
||||||
|
>
|
||||||
|
<div class="grid py-4">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li>
|
||||||
|
To connect a device, open <strong>http://{{ server.ip }}:{{ server.port }}</strong> in
|
||||||
|
a browser on the device.
|
||||||
|
</li>
|
||||||
|
<li>Open the menu, and click on <strong>Server.</strong></li>
|
||||||
|
<li>
|
||||||
|
The device will automatically request access, if you see "Access requested" on the
|
||||||
|
device.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="inline-flex items-center gap-2">
|
||||||
|
Click the
|
||||||
|
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
|
||||||
|
the devices.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
|
||||||
|
Click on
|
||||||
|
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
|
||||||
|
<IconLink class="size-4" /> Link device
|
||||||
|
</span>
|
||||||
|
to generate a one-time-pin to link the device.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Enter the pin that is shown on this server in the dialog that will appear on the
|
||||||
|
device.
|
||||||
|
</li>
|
||||||
|
<li>Congratulations! You have linked a device! (Hopefully)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</AccordionComp>
|
||||||
|
|
||||||
<DialogComp ref="pinDialog">
|
<DialogComp ref="pinDialog">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
|
|
@ -87,19 +130,29 @@ import ButtonComp from '../base/ButtonComp.vue'
|
||||||
import DialogComp from '../base/DialogComp.vue'
|
import DialogComp from '../base/DialogComp.vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { appUrl } from '@/services/ApiService'
|
import { appUrl } from '@/services/ApiService'
|
||||||
|
import AccordionComp from '../base/AccordionComp.vue'
|
||||||
|
|
||||||
const device = useDeviceStore()
|
const device = useDeviceStore()
|
||||||
|
|
||||||
const pinDialog = ref()
|
const pinDialog = ref()
|
||||||
|
|
||||||
|
const server = reactive({
|
||||||
|
ip: '',
|
||||||
|
port: '',
|
||||||
|
})
|
||||||
const remote = reactive({ devices: [], pinlink: false })
|
const remote = reactive({ devices: [], pinlink: false })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
device.serverGetRemotes()
|
device.serverGetRemotes()
|
||||||
|
|
||||||
device.$subscribe((mutation, state) => {
|
device.$subscribe((mutation, state) => {
|
||||||
if (state.remote !== remote.devices) remote.devices = device.remote
|
if (state.remote !== remote.devices) remote.devices = device.remote
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const serverIP = await device.serverGetIP()
|
||||||
|
server.ip = serverIP
|
||||||
|
|
||||||
|
server.port = import.meta.env.VITE_MCRM__PORT
|
||||||
})
|
})
|
||||||
|
|
||||||
async function startLink(deviceUuid) {
|
async function startLink(deviceUuid) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="macro-overview mcrm-block block__dark">
|
<div class="macro-overview mcrm-block block__dark">
|
||||||
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
|
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
|
||||||
<div class="macro-overview__list">
|
<div class="macro-overview__list">
|
||||||
|
<LoadComp :loading="macros.loading" text="Loading macros..." />
|
||||||
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
|
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
|
||||||
<ButtonComp variant="dark" class="w-full" size="sm">
|
<ButtonComp variant="dark" class="w-full" size="sm">
|
||||||
<IconKeyboard /> {{ macro.name }}
|
<IconKeyboard /> {{ macro.name }}
|
||||||
|
|
@ -23,15 +24,22 @@ import axios from 'axios'
|
||||||
import { appUrl, isLocal } from '@/services/ApiService'
|
import { appUrl, isLocal } from '@/services/ApiService'
|
||||||
import { AuthCall } from '@/services/EncryptService'
|
import { AuthCall } from '@/services/EncryptService'
|
||||||
import { GetMacroList, RunMacro } from '@/services/MacroService'
|
import { GetMacroList, RunMacro } from '@/services/MacroService'
|
||||||
|
import LoadComp from '../base/LoadComp.vue'
|
||||||
|
|
||||||
const macros = reactive({
|
const macros = reactive({
|
||||||
|
loading: true,
|
||||||
list: [],
|
list: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
|
loadMacroList()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadMacroList = async () => {
|
||||||
const list = await GetMacroList()
|
const list = await GetMacroList()
|
||||||
macros.list = list
|
macros.list = list
|
||||||
})
|
macros.loading = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,11 @@ function panelItemClick(dir) {
|
||||||
w-full
|
w-full
|
||||||
aspect-[4/3];
|
aspect-[4/3];
|
||||||
|
|
||||||
|
img {
|
||||||
|
@apply size-full
|
||||||
|
object-cover;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:has(img)) {
|
&:not(:has(img)) {
|
||||||
@apply bg-sky-950;
|
@apply bg-sky-950;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomeView from '../views/HomeView.vue'
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
import { useDeviceStore } from '@/stores/device'
|
||||||
|
import { checkAuth, isLocal } from '@/services/ApiService'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
|
@ -13,32 +15,36 @@ const router = createRouter({
|
||||||
path: '/panels',
|
path: '/panels',
|
||||||
name: 'panels',
|
name: 'panels',
|
||||||
component: () => import('../views/PanelsView.vue'),
|
component: () => import('../views/PanelsView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/panel/edit/:dirname',
|
path: '/panel/edit/:dirname',
|
||||||
name: 'panel-edit',
|
name: 'panel-edit',
|
||||||
component: () => import('../views/PanelsView.vue'),
|
component: () => import('../views/PanelsView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/panel/view/:dirname',
|
path: '/panel/view/:dirname',
|
||||||
name: 'panel-view',
|
name: 'panel-view',
|
||||||
component: () => import('../views/PanelsView.vue'),
|
component: () => import('../views/PanelsView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/macros',
|
path: '/macros',
|
||||||
name: 'macros',
|
name: 'macros',
|
||||||
component: () => import('../views/MacrosView.vue'),
|
component: () => import('../views/MacrosView.vue'),
|
||||||
|
meta: { localOnly: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/devices',
|
path: '/devices',
|
||||||
name: 'devices',
|
name: 'devices',
|
||||||
component: () => import('../views/DevicesView.vue'),
|
component: () => import('../views/DevicesView.vue'),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
path: '/settings',
|
// path: '/settings',
|
||||||
name: 'settings',
|
// name: 'settings',
|
||||||
component: () => import('../views/SettingsView.vue'),
|
// component: () => import('../views/SettingsView.vue'),
|
||||||
},
|
// },
|
||||||
// {
|
// {
|
||||||
// path: '/about',
|
// path: '/about',
|
||||||
// name: 'about',
|
// name: 'about',
|
||||||
|
|
@ -50,4 +56,12 @@ const router = createRouter({
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const auth = await checkAuth()
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth && !auth && !isLocal()) next('/devices')
|
||||||
|
else if (to.meta.localOnly && !isLocal()) next('/')
|
||||||
|
else next()
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useDeviceStore } from '@/stores/device'
|
||||||
import CryptoJS from 'crypto-js'
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
export const appUrl = () => {
|
export const appUrl = () => {
|
||||||
|
|
@ -17,3 +18,15 @@ export const encrypt = (data, key = false) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const checkAuth = async () => {
|
||||||
|
const device = useDeviceStore()
|
||||||
|
|
||||||
|
const handshake = await device.remoteHandshake()
|
||||||
|
|
||||||
|
if (handshake === true) return true
|
||||||
|
|
||||||
|
if (device.key()) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,19 @@ export const useDeviceStore = defineStore('device', () => {
|
||||||
localStorage.removeItem('deviceKey')
|
localStorage.removeItem('deviceKey')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverGetIP = async () => {
|
||||||
|
const request = await axios.post(appUrl() + '/device/server/ip')
|
||||||
|
return request.data
|
||||||
|
}
|
||||||
|
|
||||||
// Server application
|
// Server application
|
||||||
const serverGetRemotes = async (remoteUuid) => {
|
const serverGetRemotes = async (remoteUuid) => {
|
||||||
axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
|
axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
|
||||||
if (data.data.devices) remote.value = data.data.devices
|
// console.log(data.data.devices)
|
||||||
|
if (data.data.devices) {
|
||||||
|
console.log(data.data.devices)
|
||||||
|
remote.value = data.data.devices
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +109,8 @@ export const useDeviceStore = defineStore('device', () => {
|
||||||
shake: encryptAES(keyStr, getDateStr()),
|
shake: encryptAES(keyStr, getDateStr()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!handshake.data) removeDeviceKey()
|
||||||
|
|
||||||
return handshake.data
|
return handshake.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +122,7 @@ export const useDeviceStore = defineStore('device', () => {
|
||||||
key,
|
key,
|
||||||
setDeviceKey,
|
setDeviceKey,
|
||||||
removeDeviceKey,
|
removeDeviceKey,
|
||||||
|
serverGetIP,
|
||||||
serverGetRemotes,
|
serverGetRemotes,
|
||||||
serverStartLink,
|
serverStartLink,
|
||||||
remoteCheckServerAccess,
|
remoteCheckServerAccess,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="dashboard"></div>
|
<div id="dashboard" class="panel">
|
||||||
|
<h1 class="panel__title">Dashboard</h1>
|
||||||
|
|
||||||
|
<div class="panel__content">
|
||||||
|
<div class="grid gap-1 opacity-50 h-fit">
|
||||||
|
<em>Hello nothing to see here. Something will be added in the future.</em>
|
||||||
|
<em>Use the menu to navigate.</em>
|
||||||
|
<em>Have a nice day!</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script setup></script>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="panels" class="panel">
|
<div id="panels" class="panel">
|
||||||
<h1 class="flex items-end justify-between !w-full panel__title">
|
<h1 class="flex items-end justify-between !w-full panel__title">
|
||||||
<div>
|
<div>Panels</div>
|
||||||
Panels
|
|
||||||
<span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
|
|
||||||
</div>
|
|
||||||
<ButtonComp
|
<ButtonComp
|
||||||
v-if="panel.function != 'overview'"
|
v-if="panel.function != 'overview'"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue