LogsQueueView.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <template>
  2. <Section
  3. :title="t('logs.queue-title')"
  4. :icon="Queue01Icon"
  5. :padding="false"
  6. >
  7. <template #toolbar>
  8. <router-link
  9. to="/logs"
  10. class="button neutral inline-icon"
  11. >
  12. <svg
  13. xmlns="http://www.w3.org/2000/svg"
  14. width="1em"
  15. height="1em"
  16. viewBox="0 0 24 24"
  17. >
  18. <path
  19. fill="currentColor"
  20. d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"
  21. />
  22. </svg>
  23. {{ t('logs.back-to-list') }}
  24. </router-link>
  25. </template>
  26. <p class="padding">
  27. {{ t('logs.queue-page-description') }}
  28. </p>
  29. <div
  30. v-if="groups.length === 0 && !loading"
  31. class="empty-state padding"
  32. >
  33. <p>{{ t('logs.queue-empty') }}</p>
  34. <router-link to="/logs">
  35. {{ t('logs.back-to-list') }}
  36. </router-link>
  37. </div>
  38. </Section>
  39. <section
  40. v-for="actionGroup in groups"
  41. :key="actionGroup.name"
  42. class="with-header-and-content queue-action-group-section"
  43. >
  44. <div class="section-header flex-row">
  45. <div class="fg1 queue-action-group-heading">
  46. <ActionIconGlyph
  47. class="icon"
  48. :glyph="actionGroup.icon"
  49. />
  50. <h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
  51. </div>
  52. <ActionGroupLimitsLabel
  53. :max-concurrent="actionGroup.maxConcurrent"
  54. :queue-size="actionGroup.queueSize"
  55. />
  56. </div>
  57. <div class="section-content">
  58. <ExecutionLogsTable
  59. :logs="queueRowsForGroup(actionGroup)"
  60. :loading="loading"
  61. />
  62. </div>
  63. </section>
  64. </template>
  65. <script setup>
  66. import { ref, onMounted, onUnmounted } from 'vue'
  67. import { Queue01Icon } from '@hugeicons/core-free-icons'
  68. import Section from 'picocrank/vue/components/Section.vue'
  69. import ActionIconGlyph from '../components/ActionIconGlyph.vue'
  70. import ActionGroupLimitsLabel from '../components/ActionGroupLimitsLabel.vue'
  71. import ExecutionLogsTable from '../components/ExecutionLogsTable.vue'
  72. import { useI18n } from 'vue-i18n'
  73. import { getExecutionLogEntry, cloneLogEntry, updateLogEntryInGroups } from '../utils/executionLogEvents.js'
  74. const defaultActionGroupName = 'default'
  75. const { t } = useI18n()
  76. const groups = ref([])
  77. const loading = ref(false)
  78. function displayActionGroupName (name) {
  79. if (name === defaultActionGroupName) {
  80. return t('logs.queue-default-group')
  81. }
  82. return name
  83. }
  84. function collectCompletedEntries (currentGroups) {
  85. const completed = []
  86. for (const group of currentGroups || []) {
  87. for (const action of group.actions || []) {
  88. for (const entry of action.entries || []) {
  89. if (entry.executionFinished) {
  90. completed.push(cloneLogEntry(entry))
  91. }
  92. }
  93. }
  94. }
  95. return completed
  96. }
  97. function sortGroupEntries (entries) {
  98. entries.sort((left, right) => {
  99. if (left.executionFinished !== right.executionFinished) {
  100. return left.executionFinished ? 1 : -1
  101. }
  102. return (left.datetimeStarted || '').localeCompare(right.datetimeStarted || '')
  103. })
  104. }
  105. function sortGroups (groupList) {
  106. groupList.sort((left, right) => {
  107. if (left.name === defaultActionGroupName) {
  108. return 1
  109. }
  110. if (right.name === defaultActionGroupName) {
  111. return -1
  112. }
  113. return (left.name || '').localeCompare(right.name || '')
  114. })
  115. }
  116. function sumActionEntries (actions) {
  117. return (actions || []).reduce((total, action) => total + (action.entries || []).length, 0)
  118. }
  119. function cloneActionGroup (group) {
  120. return {
  121. ...group,
  122. actions: (group.actions || []).map(action => ({
  123. ...action,
  124. entries: [...(action.entries || [])]
  125. }))
  126. }
  127. }
  128. function findActionInGroups (groupList, bindingId) {
  129. const matches = []
  130. for (const group of groupList) {
  131. for (const action of group.actions || []) {
  132. if (action.bindingId === bindingId) {
  133. matches.push({ group, action })
  134. }
  135. }
  136. }
  137. return matches
  138. }
  139. function mergeCompletedEntries (apiGroups, completedEntries) {
  140. const merged = (apiGroups || []).map(cloneActionGroup)
  141. for (const entry of completedEntries) {
  142. const alreadyPresent = merged.some(group =>
  143. (group.actions || []).some(action =>
  144. (action.entries || []).some(item => item.executionTrackingId === entry.executionTrackingId)
  145. )
  146. )
  147. if (alreadyPresent) {
  148. continue
  149. }
  150. const matches = findActionInGroups(merged, entry.bindingId)
  151. if (matches.length === 0) {
  152. continue
  153. }
  154. for (const { action } of matches) {
  155. action.entries.push(entry)
  156. }
  157. }
  158. for (const group of merged) {
  159. for (const action of group.actions || []) {
  160. sortGroupEntries(action.entries)
  161. action.activeCount = action.entries.length
  162. }
  163. refreshGroupCounts(group)
  164. }
  165. sortGroups(merged)
  166. return merged
  167. }
  168. function forEachActionWithEntry (groupList, executionTrackingId, callback) {
  169. for (const group of groupList) {
  170. for (const action of group.actions || []) {
  171. const hasEntry = (action.entries || []).some(
  172. item => item.executionTrackingId === executionTrackingId
  173. )
  174. if (hasEntry) {
  175. callback(group, action)
  176. }
  177. }
  178. }
  179. }
  180. function refreshGroupCounts (group) {
  181. let queued = 0
  182. for (const action of group.actions || []) {
  183. action.activeCount = (action.entries || []).length
  184. for (const entry of action.entries || []) {
  185. if (entry.queued) {
  186. queued++
  187. }
  188. }
  189. }
  190. group.activeCount = sumActionEntries(group.actions)
  191. group.queuedCount = queued
  192. }
  193. function applyQueueEntryUpdate (logEntry, afterUpdate) {
  194. const result = updateLogEntryInGroups(groups.value, logEntry)
  195. if (!result) {
  196. return false
  197. }
  198. if (afterUpdate) {
  199. afterUpdate(result)
  200. }
  201. forEachActionWithEntry(groups.value, logEntry.executionTrackingId, (group, action) => {
  202. sortGroupEntries(action.entries)
  203. refreshGroupCounts(group)
  204. })
  205. return true
  206. }
  207. function insertActiveQueueEntry (logEntry) {
  208. if (!logEntry?.bindingId || !logEntry.executionTrackingId || logEntry.executionFinished) {
  209. fetchQueue()
  210. return
  211. }
  212. const matches = findActionInGroups(groups.value, logEntry.bindingId)
  213. if (matches.length === 0) {
  214. fetchQueue()
  215. return
  216. }
  217. const touchedGroups = new Set()
  218. for (const { group, action } of matches) {
  219. action.entries.push(cloneLogEntry(logEntry))
  220. touchedGroups.add(group)
  221. sortGroupEntries(action.entries)
  222. }
  223. for (const group of touchedGroups) {
  224. refreshGroupCounts(group)
  225. }
  226. sortGroups(groups.value)
  227. }
  228. function onExecutionStarted (evt) {
  229. const logEntry = getExecutionLogEntry(evt)
  230. if (!logEntry) {
  231. return
  232. }
  233. if (!applyQueueEntryUpdate(logEntry)) {
  234. insertActiveQueueEntry(logEntry)
  235. }
  236. }
  237. function onExecutionFinished (evt) {
  238. const logEntry = getExecutionLogEntry(evt)
  239. if (!logEntry) {
  240. return
  241. }
  242. applyQueueEntryUpdate(logEntry)
  243. }
  244. function queueRowsForGroup (actionGroup) {
  245. const rows = []
  246. for (const action of actionGroup.actions || []) {
  247. for (const [index, entry] of (action.entries || []).entries()) {
  248. rows.push({
  249. ...entry,
  250. actionTitle: action.actionTitle,
  251. actionIcon: action.actionIcon,
  252. entityPrefix: action.entityPrefix,
  253. queuePosition: index + 1
  254. })
  255. }
  256. }
  257. return rows
  258. }
  259. async function fetchQueue () {
  260. loading.value = true
  261. try {
  262. const completedEntries = collectCompletedEntries(groups.value)
  263. const response = await window.client.getExecutionQueue({})
  264. groups.value = mergeCompletedEntries(response.groups || [], completedEntries)
  265. } catch (err) {
  266. console.error('Failed to fetch execution queue:', err)
  267. window.showBigError('fetch-queue', 'getting execution queue', err, false)
  268. } finally {
  269. loading.value = false
  270. }
  271. }
  272. onMounted(() => {
  273. fetchQueue()
  274. window.addEventListener('EventExecutionStarted', onExecutionStarted)
  275. window.addEventListener('EventExecutionFinished', onExecutionFinished)
  276. })
  277. onUnmounted(() => {
  278. window.removeEventListener('EventExecutionStarted', onExecutionStarted)
  279. window.removeEventListener('EventExecutionFinished', onExecutionFinished)
  280. })
  281. </script>
  282. <style scoped>
  283. .queue-action-group-heading {
  284. display: flex;
  285. align-items: center;
  286. gap: 0.5rem;
  287. min-width: 0;
  288. }
  289. .queue-action-group-heading h2 {
  290. margin: 0;
  291. }
  292. .empty-state {
  293. text-align: center;
  294. padding: 2rem;
  295. color: #666;
  296. }
  297. .empty-state a {
  298. color: #007bff;
  299. text-decoration: none;
  300. }
  301. .empty-state a:hover {
  302. text-decoration: underline;
  303. }
  304. </style>