EntityDefinitionSection.vue 4.7 KB

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