Dashboard.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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, watch } 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. let dashboardRequestId = 0
  82. const isDirectory = computed(() => {
  83. if (!dashboard.value || !window.initResponse) {
  84. return false
  85. }
  86. const rootDashboards = window.initResponse.rootDashboards || []
  87. return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
  88. })
  89. function goBack() {
  90. if (window.history.length > 1) {
  91. router.back()
  92. } else {
  93. const rootDashboards = window.initResponse?.rootDashboards || []
  94. if (rootDashboards.length > 0) {
  95. router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
  96. } else {
  97. router.push({ name: 'Actions' })
  98. }
  99. }
  100. }
  101. async function getDashboard() {
  102. const requestId = ++dashboardRequestId
  103. let title = props.title
  104. // If no specific title was provided or it's the placeholder 'default',
  105. // prefer the first configured root dashboard (e.g., "Test").
  106. if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
  107. title = window.initResponse.rootDashboards[0]
  108. }
  109. try {
  110. const request = {
  111. title: title,
  112. }
  113. if (props.entityType && props.entityKey) {
  114. request.entityType = props.entityType
  115. request.entityKey = props.entityKey
  116. }
  117. const ret = await window.client.getDashboard(request)
  118. if (requestId !== dashboardRequestId) {
  119. return
  120. }
  121. if (!ret || !ret.dashboard) {
  122. throw new Error('No dashboard found')
  123. }
  124. dashboard.value = ret.dashboard
  125. const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
  126. document.title = ret.dashboard.title + ' - ' + pageTitle
  127. // Clear any previous init error since we successfully loaded
  128. initError.value = null
  129. // Stop the loading timer once dashboard is loaded
  130. if (loadingTimer) {
  131. clearInterval(loadingTimer)
  132. loadingTimer = null
  133. }
  134. // Set attribute to indicate dashboard is loaded successfully
  135. document.body.setAttribute('loaded-dashboard', title || 'default')
  136. } catch (e) {
  137. if (requestId !== dashboardRequestId) {
  138. return
  139. }
  140. // On error, provide a safe fallback state
  141. console.error('Failed to load dashboard', e)
  142. dashboard.value = { title: title || 'Default', contents: [] }
  143. const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
  144. document.title = 'Error - ' + pageTitle
  145. // Stop the loading timer on error
  146. if (loadingTimer) {
  147. clearInterval(loadingTimer)
  148. loadingTimer = null
  149. }
  150. // Set attribute even on error so tests can proceed
  151. document.body.setAttribute('loaded-dashboard', title || 'error')
  152. }
  153. }
  154. function waitForInitAndLoadDashboard() {
  155. document.body.removeAttribute('loaded-dashboard')
  156. if (loadingTimer) {
  157. clearInterval(loadingTimer)
  158. loadingTimer = null
  159. }
  160. if (checkInitInterval) {
  161. clearInterval(checkInitInterval)
  162. checkInitInterval = null
  163. }
  164. loadingTime.value = 0
  165. loadingTimer = setInterval(() => {
  166. loadingTime.value++
  167. }, 1000)
  168. // Check if init has completed successfully
  169. if (window.initResponse) {
  170. getDashboard()
  171. } else if (window.initError) {
  172. // Init failed, show error immediately
  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. } else {
  180. // Init hasn't completed yet, poll for completion
  181. checkInitInterval = setInterval(() => {
  182. if (window.initResponse) {
  183. clearInterval(checkInitInterval)
  184. checkInitInterval = null
  185. getDashboard()
  186. } else if (window.initError) {
  187. clearInterval(checkInitInterval)
  188. checkInitInterval = null
  189. initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
  190. // Stop the loading timer since we're showing an error
  191. if (loadingTimer) {
  192. clearInterval(loadingTimer)
  193. loadingTimer = null
  194. }
  195. }
  196. }, 100) // Check every 100ms
  197. }
  198. }
  199. onMounted(() => {
  200. waitForInitAndLoadDashboard()
  201. })
  202. watch(
  203. () => [props.title, props.entityType, props.entityKey],
  204. () => {
  205. dashboard.value = null
  206. waitForInitAndLoadDashboard()
  207. }
  208. )
  209. onUnmounted(() => {
  210. document.body.removeAttribute('loaded-dashboard')
  211. // Clean up the timers when component is unmounted
  212. if (loadingTimer) {
  213. clearInterval(loadingTimer)
  214. loadingTimer = null
  215. }
  216. if (checkInitInterval) {
  217. clearInterval(checkInitInterval)
  218. checkInitInterval = null
  219. }
  220. })
  221. </script>
  222. <style scoped>
  223. h2 {
  224. font-weight: bold;
  225. text-align: center;
  226. padding: 1em;
  227. padding-top: 1.5em;
  228. grid-column: 1 / -1;
  229. }
  230. h2 .entity-link {
  231. color: inherit;
  232. text-decoration: none;
  233. transition: opacity 0.2s;
  234. }
  235. h2 .entity-link:hover {
  236. opacity: 0.7;
  237. text-decoration: underline;
  238. }
  239. fieldset {
  240. display: grid;
  241. grid-template-columns: repeat(auto-fit, 180px);
  242. grid-auto-rows: 1fr;
  243. justify-content: center;
  244. place-items: stretch;
  245. }
  246. @keyframes spin {
  247. from {
  248. transform: rotate(0deg);
  249. }
  250. to {
  251. transform: rotate(360deg);
  252. }
  253. }
  254. .back-button-container {
  255. display: flex;
  256. justify-content: flex-start;
  257. padding: 1em;
  258. padding-bottom: 0;
  259. }
  260. .back-button {
  261. display: flex;
  262. align-items: center;
  263. gap: 0.5em;
  264. padding: 0.5em 1em;
  265. background-color: var(--bg, #fff);
  266. border: 1px solid var(--border-color, #ccc);
  267. border-radius: 0.5em;
  268. cursor: pointer;
  269. font-size: 0.9em;
  270. box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
  271. transition: background-color 0.2s, box-shadow 0.2s;
  272. }
  273. .back-button:hover {
  274. background-color: var(--bg-hover, #f5f5f5);
  275. box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
  276. }
  277. @media (prefers-color-scheme: dark) {
  278. .back-button {
  279. background-color: var(--bg, #111);
  280. border-color: var(--border-color, #333);
  281. }
  282. .back-button:hover {
  283. background-color: var(--bg-hover, #222);
  284. }
  285. }
  286. </style>