| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- <template>
- <Section
- :title="t('logs.queue-title')"
- :icon="Queue01Icon"
- :padding="false"
- >
- <template #toolbar>
- <router-link
- to="/logs"
- class="button neutral inline-icon"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="1em"
- height="1em"
- viewBox="0 0 24 24"
- >
- <path
- fill="currentColor"
- d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"
- />
- </svg>
- {{ t('logs.back-to-list') }}
- </router-link>
- </template>
- <p class="padding">
- {{ t('logs.queue-page-description') }}
- </p>
- <div
- v-if="groups.length === 0 && !loading"
- class="empty-state padding"
- >
- <p>{{ t('logs.queue-empty') }}</p>
- <router-link to="/logs">
- {{ t('logs.back-to-list') }}
- </router-link>
- </div>
- </Section>
- <section
- v-for="actionGroup in groups"
- :key="actionGroup.name"
- class="with-header-and-content queue-action-group-section"
- >
- <div class="section-header flex-row">
- <div class="fg1 queue-action-group-heading">
- <ActionIconGlyph
- class="icon"
- :glyph="actionGroup.icon"
- />
- <h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
- </div>
- <ActionGroupLimitsLabel
- :max-concurrent="actionGroup.maxConcurrent"
- :queue-size="actionGroup.queueSize"
- />
- </div>
- <div class="section-content">
- <ExecutionLogsTable
- :logs="queueRowsForGroup(actionGroup)"
- :loading="loading"
- />
- </div>
- </section>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted } from 'vue'
- import { Queue01Icon } from '@hugeicons/core-free-icons'
- import Section from 'picocrank/vue/components/Section.vue'
- import ActionIconGlyph from '../components/ActionIconGlyph.vue'
- import ActionGroupLimitsLabel from '../components/ActionGroupLimitsLabel.vue'
- import ExecutionLogsTable from '../components/ExecutionLogsTable.vue'
- import { useI18n } from 'vue-i18n'
- import { getExecutionLogEntry, cloneLogEntry, updateLogEntryInGroups } from '../utils/executionLogEvents.js'
- const defaultActionGroupName = 'default'
- const { t } = useI18n()
- const groups = ref([])
- const loading = ref(false)
- function displayActionGroupName (name) {
- if (name === defaultActionGroupName) {
- return t('logs.queue-default-group')
- }
- return name
- }
- function collectCompletedEntries (currentGroups) {
- const completed = []
- for (const group of currentGroups || []) {
- for (const action of group.actions || []) {
- for (const entry of action.entries || []) {
- if (entry.executionFinished) {
- completed.push(cloneLogEntry(entry))
- }
- }
- }
- }
- return completed
- }
- function sortGroupEntries (entries) {
- entries.sort((left, right) => {
- if (left.executionFinished !== right.executionFinished) {
- return left.executionFinished ? 1 : -1
- }
- return (left.datetimeStarted || '').localeCompare(right.datetimeStarted || '')
- })
- }
- function sortGroups (groupList) {
- groupList.sort((left, right) => {
- if (left.name === defaultActionGroupName) {
- return 1
- }
- if (right.name === defaultActionGroupName) {
- return -1
- }
- return (left.name || '').localeCompare(right.name || '')
- })
- }
- function sumActionEntries (actions) {
- return (actions || []).reduce((total, action) => total + (action.entries || []).length, 0)
- }
- function cloneActionGroup (group) {
- return {
- ...group,
- actions: (group.actions || []).map(action => ({
- ...action,
- entries: [...(action.entries || [])]
- }))
- }
- }
- function findActionInGroups (groupList, bindingId) {
- const matches = []
- for (const group of groupList) {
- for (const action of group.actions || []) {
- if (action.bindingId === bindingId) {
- matches.push({ group, action })
- }
- }
- }
- return matches
- }
- function mergeCompletedEntries (apiGroups, completedEntries) {
- const merged = (apiGroups || []).map(cloneActionGroup)
- for (const entry of completedEntries) {
- const alreadyPresent = merged.some(group =>
- (group.actions || []).some(action =>
- (action.entries || []).some(item => item.executionTrackingId === entry.executionTrackingId)
- )
- )
- if (alreadyPresent) {
- continue
- }
- const matches = findActionInGroups(merged, entry.bindingId)
- if (matches.length === 0) {
- continue
- }
- for (const { action } of matches) {
- action.entries.push(entry)
- }
- }
- for (const group of merged) {
- for (const action of group.actions || []) {
- sortGroupEntries(action.entries)
- action.activeCount = action.entries.length
- }
- refreshGroupCounts(group)
- }
- sortGroups(merged)
- return merged
- }
- function forEachActionWithEntry (groupList, executionTrackingId, callback) {
- for (const group of groupList) {
- for (const action of group.actions || []) {
- const hasEntry = (action.entries || []).some(
- item => item.executionTrackingId === executionTrackingId
- )
- if (hasEntry) {
- callback(group, action)
- }
- }
- }
- }
- function refreshGroupCounts (group) {
- let queued = 0
- for (const action of group.actions || []) {
- action.activeCount = (action.entries || []).length
- for (const entry of action.entries || []) {
- if (entry.queued) {
- queued++
- }
- }
- }
- group.activeCount = sumActionEntries(group.actions)
- group.queuedCount = queued
- }
- function applyQueueEntryUpdate (logEntry, afterUpdate) {
- const result = updateLogEntryInGroups(groups.value, logEntry)
- if (!result) {
- return false
- }
- if (afterUpdate) {
- afterUpdate(result)
- }
- forEachActionWithEntry(groups.value, logEntry.executionTrackingId, (group, action) => {
- sortGroupEntries(action.entries)
- refreshGroupCounts(group)
- })
- return true
- }
- function insertActiveQueueEntry (logEntry) {
- if (!logEntry?.bindingId || !logEntry.executionTrackingId || logEntry.executionFinished) {
- fetchQueue()
- return
- }
- const matches = findActionInGroups(groups.value, logEntry.bindingId)
- if (matches.length === 0) {
- fetchQueue()
- return
- }
- const touchedGroups = new Set()
- for (const { group, action } of matches) {
- action.entries.push(cloneLogEntry(logEntry))
- touchedGroups.add(group)
- sortGroupEntries(action.entries)
- }
- for (const group of touchedGroups) {
- refreshGroupCounts(group)
- }
- sortGroups(groups.value)
- }
- function onExecutionStarted (evt) {
- const logEntry = getExecutionLogEntry(evt)
- if (!logEntry) {
- return
- }
- if (!applyQueueEntryUpdate(logEntry)) {
- insertActiveQueueEntry(logEntry)
- }
- }
- function onExecutionFinished (evt) {
- const logEntry = getExecutionLogEntry(evt)
- if (!logEntry) {
- return
- }
- applyQueueEntryUpdate(logEntry)
- }
- function queueRowsForGroup (actionGroup) {
- const rows = []
- for (const action of actionGroup.actions || []) {
- for (const [index, entry] of (action.entries || []).entries()) {
- rows.push({
- ...entry,
- actionTitle: action.actionTitle,
- actionIcon: action.actionIcon,
- entityPrefix: action.entityPrefix,
- queuePosition: index + 1
- })
- }
- }
- return rows
- }
- async function fetchQueue () {
- loading.value = true
- try {
- const completedEntries = collectCompletedEntries(groups.value)
- const response = await window.client.getExecutionQueue({})
- groups.value = mergeCompletedEntries(response.groups || [], completedEntries)
- } catch (err) {
- console.error('Failed to fetch execution queue:', err)
- window.showBigError('fetch-queue', 'getting execution queue', err, false)
- } finally {
- loading.value = false
- }
- }
- onMounted(() => {
- fetchQueue()
- window.addEventListener('EventExecutionStarted', onExecutionStarted)
- window.addEventListener('EventExecutionFinished', onExecutionFinished)
- })
- onUnmounted(() => {
- window.removeEventListener('EventExecutionStarted', onExecutionStarted)
- window.removeEventListener('EventExecutionFinished', onExecutionFinished)
- })
- </script>
- <style scoped>
- .queue-action-group-heading {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- min-width: 0;
- }
- .queue-action-group-heading h2 {
- margin: 0;
- }
- .empty-state {
- text-align: center;
- padding: 2rem;
- color: #666;
- }
- .empty-state a {
- color: #007bff;
- text-decoration: none;
- }
- .empty-state a:hover {
- text-decoration: underline;
- }
- </style>
|