olivetin/frontend/resources/vue/Dashboard.vue

396 lines
9.5 KiB
Vue

<template>
<section
v-if="!dashboard && !initError && !loadError"
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>
<section
v-else-if="loadError"
style="text-align: center; padding: 2em;"
class="bad"
>
<h2 style="color: var(--error);">
Failed to Load Dashboard
</h2>
<p>{{ loadError }}</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)
const loadError = 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 errors since we successfully loaded
initError.value = null
loadError.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
}
console.error('Failed to load dashboard', e)
dashboard.value = null
loadError.value = e.message || 'Failed to load dashboard'
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = 'Error - ' + pageTitle
// Stop the loading timer on error
if (loadingTimer) {
clearInterval(loadingTimer)
loadingTimer = null
}
}
}
function waitForInitAndLoadDashboard () {
document.body.removeAttribute('loaded-dashboard')
loadError.value = null
dashboard.value = null
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>