382 lines
9.2 KiB
Vue
382 lines
9.2 KiB
Vue
<template>
|
|
<section
|
|
v-if="!dashboard && !initError"
|
|
style="text-align: center; padding: 2em;"
|
|
>
|
|
<HugeiconsIcon
|
|
:icon="Loading03Icon"
|
|
width="3em"
|
|
height="3em"
|
|
style="animation: spin 1s linear infinite;"
|
|
/>
|
|
<p>Loading dashboard...</p>
|
|
<p style="color: var(--fg2);">
|
|
{{ loadingTime }}s
|
|
</p>
|
|
</section>
|
|
<section
|
|
v-if="initError"
|
|
style="text-align: center; padding: 2em;"
|
|
class="bad"
|
|
>
|
|
<h2 style="color: var(--error);">
|
|
Initialization Failed
|
|
</h2>
|
|
<p>{{ initError }}</p>
|
|
<p style="color: var(--fg2);">
|
|
Please check your configuration and try again.
|
|
</p>
|
|
</section>
|
|
<template v-else-if="dashboard">
|
|
<section v-if="dashboard.contents.length == 0">
|
|
<div
|
|
v-if="isDirectory"
|
|
class="back-button-container"
|
|
>
|
|
<button
|
|
class="back-button"
|
|
@click="goBack"
|
|
>
|
|
<HugeiconsIcon
|
|
:icon="ArrowLeftIcon"
|
|
width="1.2em"
|
|
height="1.2em"
|
|
/>
|
|
<span>Back</span>
|
|
</button>
|
|
</div>
|
|
<h2>{{ dashboard.title }}</h2>
|
|
<p
|
|
style="text-align: center"
|
|
class="padding"
|
|
>
|
|
This dashboard is empty.
|
|
</p>
|
|
</section>
|
|
|
|
<section
|
|
v-else
|
|
class="transparent"
|
|
>
|
|
<div
|
|
v-if="isDirectory"
|
|
class="back-button-container"
|
|
>
|
|
<button
|
|
class="back-button"
|
|
@click="goBack"
|
|
>
|
|
<HugeiconsIcon
|
|
:icon="ArrowLeftIcon"
|
|
width="1.2em"
|
|
height="1.2em"
|
|
/>
|
|
<span>Back</span>
|
|
</button>
|
|
</div>
|
|
<div
|
|
v-for="component in dashboard.contents"
|
|
:key="component.title"
|
|
class="dashboard-row"
|
|
>
|
|
<h2 v-if="dashboard.title != 'Default'">
|
|
<router-link
|
|
v-if="component.entityType && component.entityKey"
|
|
:to="{
|
|
name: 'EntityDetails',
|
|
params: {
|
|
entityType: component.entityType,
|
|
entityKey: component.entityKey
|
|
}
|
|
}"
|
|
class="entity-link"
|
|
>
|
|
{{ component.title }}
|
|
</router-link>
|
|
<span v-else>{{ component.title }}</span>
|
|
</h2>
|
|
|
|
<fieldset :class="component.cssClass">
|
|
<template
|
|
v-for="subcomponent in component.contents"
|
|
:key="subcomponent.title || subcomponent.type"
|
|
>
|
|
<DashboardComponent :component="subcomponent" />
|
|
</template>
|
|
</fieldset>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</template>
|
|
|
|
<script setup>
|
|
import DashboardComponent from './components/DashboardComponent.vue'
|
|
import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
|
|
|
const props = defineProps({
|
|
title: {
|
|
type: String,
|
|
required: false
|
|
},
|
|
entityType: {
|
|
type: String,
|
|
required: false
|
|
},
|
|
entityKey: {
|
|
type: String,
|
|
required: false
|
|
}
|
|
})
|
|
|
|
const router = useRouter()
|
|
const dashboard = ref(null)
|
|
const loadingTime = ref(0)
|
|
const initError = ref(null)
|
|
let loadingTimer = null
|
|
let checkInitInterval = null
|
|
let dashboardRequestId = 0
|
|
|
|
const isDirectory = computed(() => {
|
|
if (!dashboard.value || !window.initResponse) {
|
|
return false
|
|
}
|
|
const rootDashboards = window.initResponse.rootDashboards || []
|
|
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
|
|
})
|
|
|
|
function goBack () {
|
|
if (window.history.length > 1) {
|
|
router.back()
|
|
} else {
|
|
const rootDashboards = window.initResponse?.rootDashboards || []
|
|
if (rootDashboards.length > 0) {
|
|
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
|
|
} else {
|
|
router.push({ name: 'Actions' })
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getDashboard () {
|
|
const requestId = ++dashboardRequestId
|
|
let title = props.title
|
|
|
|
// If no specific title was provided or it's the placeholder 'default',
|
|
// prefer the first configured root dashboard (e.g., "Test").
|
|
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
|
|
title = window.initResponse.rootDashboards[0]
|
|
}
|
|
|
|
try {
|
|
const request = {
|
|
title
|
|
}
|
|
|
|
if (props.entityType && props.entityKey) {
|
|
request.entityType = props.entityType
|
|
request.entityKey = props.entityKey
|
|
}
|
|
|
|
const ret = await window.client.getDashboard(request)
|
|
|
|
if (requestId !== dashboardRequestId) {
|
|
return
|
|
}
|
|
|
|
if (!ret || !ret.dashboard) {
|
|
throw new Error('No dashboard found')
|
|
}
|
|
|
|
dashboard.value = ret.dashboard
|
|
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
|
document.title = ret.dashboard.title + ' - ' + pageTitle
|
|
|
|
// Clear any previous init error since we successfully loaded
|
|
initError.value = null
|
|
|
|
// Stop the loading timer once dashboard is loaded
|
|
if (loadingTimer) {
|
|
clearInterval(loadingTimer)
|
|
loadingTimer = null
|
|
}
|
|
|
|
// Set attribute to indicate dashboard is loaded successfully
|
|
document.body.setAttribute('loaded-dashboard', title || 'default')
|
|
} catch (e) {
|
|
if (requestId !== dashboardRequestId) {
|
|
return
|
|
}
|
|
|
|
// On error, provide a safe fallback state
|
|
console.error('Failed to load dashboard', e)
|
|
dashboard.value = { title: title || 'Default', contents: [] }
|
|
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
|
document.title = 'Error - ' + pageTitle
|
|
|
|
// Stop the loading timer on error
|
|
if (loadingTimer) {
|
|
clearInterval(loadingTimer)
|
|
loadingTimer = null
|
|
}
|
|
|
|
// Set attribute even on error so tests can proceed
|
|
document.body.setAttribute('loaded-dashboard', title || 'error')
|
|
}
|
|
}
|
|
|
|
function waitForInitAndLoadDashboard () {
|
|
document.body.removeAttribute('loaded-dashboard')
|
|
|
|
if (loadingTimer) {
|
|
clearInterval(loadingTimer)
|
|
loadingTimer = null
|
|
}
|
|
if (checkInitInterval) {
|
|
clearInterval(checkInitInterval)
|
|
checkInitInterval = null
|
|
}
|
|
|
|
loadingTime.value = 0
|
|
loadingTimer = setInterval(() => {
|
|
loadingTime.value++
|
|
}, 1000)
|
|
|
|
// Check if init has completed successfully
|
|
if (window.initResponse) {
|
|
getDashboard()
|
|
} else if (window.initError) {
|
|
// Init failed, show error immediately
|
|
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
|
// Stop the loading timer since we're showing an error
|
|
if (loadingTimer) {
|
|
clearInterval(loadingTimer)
|
|
loadingTimer = null
|
|
}
|
|
} else {
|
|
// Init hasn't completed yet, poll for completion
|
|
checkInitInterval = setInterval(() => {
|
|
if (window.initResponse) {
|
|
clearInterval(checkInitInterval)
|
|
checkInitInterval = null
|
|
getDashboard()
|
|
} else if (window.initError) {
|
|
clearInterval(checkInitInterval)
|
|
checkInitInterval = null
|
|
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
|
// Stop the loading timer since we're showing an error
|
|
if (loadingTimer) {
|
|
clearInterval(loadingTimer)
|
|
loadingTimer = null
|
|
}
|
|
}
|
|
}, 100) // Check every 100ms
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
waitForInitAndLoadDashboard()
|
|
})
|
|
|
|
watch(
|
|
() => [props.title, props.entityType, props.entityKey],
|
|
() => {
|
|
dashboard.value = null
|
|
waitForInitAndLoadDashboard()
|
|
}
|
|
)
|
|
|
|
onUnmounted(() => {
|
|
document.body.removeAttribute('loaded-dashboard')
|
|
|
|
// Clean up the timers when component is unmounted
|
|
if (loadingTimer) {
|
|
clearInterval(loadingTimer)
|
|
loadingTimer = null
|
|
}
|
|
if (checkInitInterval) {
|
|
clearInterval(checkInitInterval)
|
|
checkInitInterval = null
|
|
}
|
|
})
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
|
|
h2 {
|
|
font-weight: bold;
|
|
text-align: center;
|
|
padding: 1em;
|
|
padding-top: 1.5em;
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
h2 .entity-link {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
h2 .entity-link:hover {
|
|
opacity: 0.7;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
fieldset {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, 180px);
|
|
grid-auto-rows: 1fr;
|
|
justify-content: center;
|
|
place-items: stretch;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.back-button-container {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
padding: 1em;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.back-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
padding: 0.5em 1em;
|
|
background-color: var(--bg, #fff);
|
|
border: 1px solid var(--border-color, #ccc);
|
|
border-radius: 0.5em;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
|
|
transition: background-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.back-button:hover {
|
|
background-color: var(--bg-hover, #f5f5f5);
|
|
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.back-button {
|
|
background-color: var(--bg, #111);
|
|
border-color: var(--border-color, #333);
|
|
}
|
|
|
|
.back-button:hover {
|
|
background-color: var(--bg-hover, #222);
|
|
}
|
|
}
|
|
</style>
|