Dashboard.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <template>
  2. <section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;">
  3. <HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" />
  4. <p>Loading dashboard...</p>
  5. <p style="color: var(--fg2);">{{ loadingTime }}s</p>
  6. </section>
  7. <section v-if="initError" style="text-align: center; padding: 2em;" class = "bad">
  8. <h2 style="color: var(--error);">Initialization Failed</h2>
  9. <p>{{ initError }}</p>
  10. <p style="color: var(--fg2);">Please check your configuration and try again.</p>
  11. </section>
  12. <template v-else-if="dashboard">
  13. <section v-if="dashboard.contents.length == 0">
  14. <div class="back-button-container" v-if="isDirectory">
  15. <button @click="goBack" class="back-button">
  16. <HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
  17. <span>Back</span>
  18. </button>
  19. </div>
  20. <h2>{{ dashboard.title }}</h2>
  21. <p style = "text-align: center" class = "padding">This dashboard is empty.</p>
  22. </section>
  23. <section class="transparent" v-else>
  24. <div class="back-button-container" v-if="isDirectory">
  25. <button @click="goBack" class="back-button">
  26. <HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
  27. <span>Back</span>
  28. </button>
  29. </div>
  30. <div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title">
  31. <h2 v-if = "dashboard.title != 'Default'">
  32. <router-link
  33. v-if="component.entityType && component.entityKey"
  34. :to="{
  35. name: 'EntityDetails',
  36. params: {
  37. entityType: component.entityType,
  38. entityKey: component.entityKey
  39. }
  40. }"
  41. class="entity-link">
  42. {{ component.title }}
  43. </router-link>
  44. <span v-else>{{ component.title }}</span>
  45. </h2>
  46. <fieldset :class="component.cssClass">
  47. <template v-for="subcomponent in component.contents">
  48. <DashboardComponent :component="subcomponent" />
  49. </template>
  50. </fieldset>
  51. </div>
  52. </section>
  53. </template>
  54. </template>
  55. <script setup>
  56. import DashboardComponent from './components/DashboardComponent.vue'
  57. import { onMounted, onUnmounted, ref, computed } from 'vue'
  58. import { useRouter } from 'vue-router'
  59. import { HugeiconsIcon } from '@hugeicons/vue'
  60. import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
  61. const props = defineProps({
  62. title: {
  63. type: String,
  64. required: false
  65. },
  66. entityType: {
  67. type: String,
  68. required: false
  69. },
  70. entityKey: {
  71. type: String,
  72. required: false
  73. }
  74. })
  75. const router = useRouter()
  76. const dashboard = ref(null)
  77. const loadingTime = ref(0)
  78. const initError = ref(null)
  79. let loadingTimer = null
  80. let checkInitInterval = null
  81. const isDirectory = computed(() => {
  82. if (!dashboard.value || !window.initResponse) {
  83. return false
  84. }
  85. const rootDashboards = window.initResponse.rootDashboards || []
  86. return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
  87. })
  88. function goBack() {
  89. if (window.history.length > 1) {
  90. router.back()
  91. } else {
  92. const rootDashboards = window.initResponse?.rootDashboards || []
  93. if (rootDashboards.length > 0) {
  94. router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
  95. } else {
  96. router.push({ name: 'Actions' })
  97. }
  98. }
  99. }
  100. async function getDashboard() {
  101. let title = props.title
  102. // If no specific title was provided or it's the placeholder 'default',
  103. // prefer the first configured root dashboard (e.g., "Test").
  104. if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
  105. title = window.initResponse.rootDashboards[0]
  106. }
  107. try {
  108. const request = {
  109. title: title,
  110. }
  111. if (props.entityType && props.entityKey) {
  112. request.entityType = props.entityType
  113. request.entityKey = props.entityKey
  114. }
  115. const ret = await window.client.getDashboard(request)
  116. if (!ret || !ret.dashboard) {
  117. throw new Error('No dashboard found')
  118. }
  119. dashboard.value = ret.dashboard
  120. const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
  121. document.title = ret.dashboard.title + ' - ' + pageTitle
  122. // Clear any previous init error since we successfully loaded
  123. initError.value = null
  124. // Stop the loading timer once dashboard is loaded
  125. if (loadingTimer) {
  126. clearInterval(loadingTimer)
  127. loadingTimer = null
  128. }
  129. // Set attribute to indicate dashboard is loaded successfully
  130. document.body.setAttribute('loaded-dashboard', title || 'default')
  131. } catch (e) {
  132. // On error, provide a safe fallback state
  133. console.error('Failed to load dashboard', e)
  134. dashboard.value = { title: title || 'Default', contents: [] }
  135. const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
  136. document.title = 'Error - ' + pageTitle
  137. // Stop the loading timer on error
  138. if (loadingTimer) {
  139. clearInterval(loadingTimer)
  140. loadingTimer = null
  141. }
  142. // Set attribute even on error so tests can proceed
  143. document.body.setAttribute('loaded-dashboard', title || 'error')
  144. }
  145. }
  146. function waitForInitAndLoadDashboard() {
  147. // Start the loading timer
  148. loadingTime.value = 0
  149. loadingTimer = setInterval(() => {
  150. loadingTime.value++
  151. }, 1000)
  152. // Check if init has completed successfully
  153. if (window.initResponse) {
  154. getDashboard()
  155. } else if (window.initError) {
  156. // Init failed, show error immediately
  157. initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
  158. // Stop the loading timer since we're showing an error
  159. if (loadingTimer) {
  160. clearInterval(loadingTimer)
  161. loadingTimer = null
  162. }
  163. } else {
  164. // Init hasn't completed yet, poll for completion
  165. checkInitInterval = setInterval(() => {
  166. if (window.initResponse) {
  167. clearInterval(checkInitInterval)
  168. checkInitInterval = null
  169. getDashboard()
  170. } else if (window.initError) {
  171. clearInterval(checkInitInterval)
  172. checkInitInterval = null
  173. initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
  174. // Stop the loading timer since we're showing an error
  175. if (loadingTimer) {
  176. clearInterval(loadingTimer)
  177. loadingTimer = null
  178. }
  179. }
  180. }, 100) // Check every 100ms
  181. }
  182. }
  183. onMounted(() => {
  184. waitForInitAndLoadDashboard()
  185. })
  186. onUnmounted(() => {
  187. // Clean up the timers when component is unmounted
  188. if (loadingTimer) {
  189. clearInterval(loadingTimer)
  190. loadingTimer = null
  191. }
  192. if (checkInitInterval) {
  193. clearInterval(checkInitInterval)
  194. checkInitInterval = null
  195. }
  196. })
  197. </script>
  198. <style scoped>
  199. h2 {
  200. font-weight: bold;
  201. text-align: center;
  202. padding: 1em;
  203. padding-top: 1.5em;
  204. grid-column: 1 / -1;
  205. }
  206. h2 .entity-link {
  207. color: inherit;
  208. text-decoration: none;
  209. transition: opacity 0.2s;
  210. }
  211. h2 .entity-link:hover {
  212. opacity: 0.7;
  213. text-decoration: underline;
  214. }
  215. fieldset {
  216. display: grid;
  217. grid-template-columns: repeat(auto-fit, 180px);
  218. grid-auto-rows: 1fr;
  219. justify-content: center;
  220. place-items: stretch;
  221. }
  222. @keyframes spin {
  223. from {
  224. transform: rotate(0deg);
  225. }
  226. to {
  227. transform: rotate(360deg);
  228. }
  229. }
  230. .back-button-container {
  231. display: flex;
  232. justify-content: flex-start;
  233. padding: 1em;
  234. padding-bottom: 0;
  235. }
  236. .back-button {
  237. display: flex;
  238. align-items: center;
  239. gap: 0.5em;
  240. padding: 0.5em 1em;
  241. background-color: var(--bg, #fff);
  242. border: 1px solid var(--border-color, #ccc);
  243. border-radius: 0.5em;
  244. cursor: pointer;
  245. font-size: 0.9em;
  246. box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
  247. transition: background-color 0.2s, box-shadow 0.2s;
  248. }
  249. .back-button:hover {
  250. background-color: var(--bg-hover, #f5f5f5);
  251. box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
  252. }
  253. @media (prefers-color-scheme: dark) {
  254. .back-button {
  255. background-color: var(--bg, #111);
  256. border-color: var(--border-color, #333);
  257. }
  258. .back-button:hover {
  259. background-color: var(--bg-hover, #222);
  260. }
  261. }
  262. </style>