EntityDefinitionSection.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <template>
  2. <Section
  3. :icon="CellsIcon"
  4. :padding="!hasTable"
  5. >
  6. <template #title>
  7. <span class="section-title-with-icon">
  8. Entity:
  9. <ActionIconGlyph
  10. v-if="definition.icon"
  11. class="entity-title-icon"
  12. :glyph="definition.icon"
  13. />
  14. {{ definition.title }}
  15. </span>
  16. </template>
  17. <template
  18. v-if="hasTable"
  19. #toolbar
  20. >
  21. <EntityListFilter v-model="searchText" />
  22. </template>
  23. <p v-if="!hasTable">
  24. {{ definition.instances.length }} instances.
  25. </p>
  26. <template v-if="hasTable">
  27. <p
  28. v-if="tableError"
  29. class="table-error padding"
  30. role="alert"
  31. >
  32. {{ tableError }}
  33. </p>
  34. <EntityInstancesTable
  35. v-else
  36. v-model:page="currentPage"
  37. v-model:page-size="pageSize"
  38. :instances="tableInstances"
  39. :properties="definition.properties"
  40. :total-instances="totalInstances"
  41. />
  42. </template>
  43. <ul v-else>
  44. <li
  45. v-for="inst in definition.instances"
  46. :key="inst.uniqueKey"
  47. >
  48. <router-link :to="entityDetailsRoute(inst)">
  49. {{ inst.title }}
  50. </router-link>
  51. </li>
  52. </ul>
  53. <div
  54. v-if="usedDashboards.length > 0"
  55. :class="{ padding: hasTable }"
  56. >
  57. <h3>Used on Dashboards:</h3>
  58. <ul>
  59. <li
  60. v-for="dash in usedDashboards"
  61. :key="dash"
  62. >
  63. <template v-if="isEntityDirectory(dash)">
  64. {{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
  65. </template>
  66. <router-link
  67. v-else-if="!dash.includes('entity:')"
  68. :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }"
  69. >
  70. {{ getDashboardTitle(dash) }}
  71. </router-link>
  72. <span v-else>{{ dash }}</span>
  73. </li>
  74. </ul>
  75. </div>
  76. </Section>
  77. </template>
  78. <script setup>
  79. import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
  80. import { CellsIcon } from '@hugeicons/core-free-icons'
  81. import Section from 'picocrank/vue/components/Section.vue'
  82. import ActionIconGlyph from './ActionIconGlyph.vue'
  83. import EntityInstancesTable from './EntityInstancesTable.vue'
  84. import EntityListFilter from './EntityListFilter.vue'
  85. import { entityDetailsRoute } from '../utils/entityRoutes.js'
  86. const props = defineProps({
  87. definition: {
  88. type: Object,
  89. required: true
  90. }
  91. })
  92. const searchText = ref('')
  93. const tableInstances = ref([])
  94. const totalInstances = ref(0)
  95. const currentPage = ref(1)
  96. const pageSize = ref(10)
  97. const tableError = ref('')
  98. let fetchTimer = null
  99. let fetchSequence = 0
  100. const hasTable = computed(() => (props.definition.properties?.length ?? 0) > 0)
  101. const usedDashboards = computed(() => filteredDashboards(props.definition.usedOnDashboards ?? []))
  102. watch(searchText, () => {
  103. currentPage.value = 1
  104. scheduleFetchTableInstances()
  105. })
  106. watch([currentPage, pageSize], () => {
  107. scheduleFetchTableInstances()
  108. })
  109. function filteredDashboards (dashboards) {
  110. return dashboards.filter(d => d && !d.includes('{{'))
  111. }
  112. function isEntityDirectory (dashboardTitle) {
  113. return dashboardTitle.endsWith(' [Entity Directory]')
  114. }
  115. function getDashboardTitle (dashboardTitle) {
  116. if (isEntityDirectory(dashboardTitle)) {
  117. return dashboardTitle.slice(0, -' [Entity Directory]'.length)
  118. }
  119. return dashboardTitle
  120. }
  121. function scheduleFetchTableInstances () {
  122. if (!hasTable.value) {
  123. return
  124. }
  125. if (fetchTimer) {
  126. clearTimeout(fetchTimer)
  127. }
  128. fetchTimer = setTimeout(() => {
  129. fetchTableInstances()
  130. }, 250)
  131. }
  132. async function fetchTableInstances () {
  133. if (!hasTable.value) {
  134. return
  135. }
  136. const requestId = ++fetchSequence
  137. tableError.value = ''
  138. try {
  139. const response = await window.client.getEntities({
  140. entityType: props.definition.title,
  141. filter: searchText.value.trim(),
  142. page: currentPage.value,
  143. pageSize: pageSize.value
  144. })
  145. if (requestId !== fetchSequence) {
  146. return
  147. }
  148. const definition = response.entityDefinitions?.find(def => def.title === props.definition.title)
  149. tableInstances.value = definition?.instances ?? []
  150. totalInstances.value = definition?.totalInstances ?? 0
  151. } catch (err) {
  152. if (requestId !== fetchSequence) {
  153. return
  154. }
  155. console.error('Failed to fetch entity instances:', err)
  156. tableError.value = 'Failed to load entity instances.'
  157. tableInstances.value = []
  158. totalInstances.value = 0
  159. }
  160. }
  161. onMounted(() => {
  162. if (hasTable.value) {
  163. fetchTableInstances()
  164. }
  165. })
  166. onBeforeUnmount(() => {
  167. if (fetchTimer) {
  168. clearTimeout(fetchTimer)
  169. fetchTimer = null
  170. }
  171. })
  172. </script>
  173. <style scoped>
  174. .section-title-with-icon {
  175. display: inline-flex;
  176. align-items: center;
  177. gap: 0.5em;
  178. }
  179. .entity-title-icon {
  180. font-size: 1.2em;
  181. }
  182. .table-error {
  183. color: var(--error, #c00);
  184. }
  185. </style>