fix: action group display fixes
This commit is contained in:
parent
de68ba7433
commit
03a9a05e38
|
|
@ -834,9 +834,9 @@ export declare type GetExecutionQueueRequest = Message<"olivetin.api.v1.GetExecu
|
||||||
export declare const GetExecutionQueueRequestSchema: GenMessage<GetExecutionQueueRequest>;
|
export declare const GetExecutionQueueRequestSchema: GenMessage<GetExecutionQueueRequest>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message olivetin.api.v1.ExecutionQueueGroup
|
* @generated from message olivetin.api.v1.ExecutionQueueAction
|
||||||
*/
|
*/
|
||||||
export declare type ExecutionQueueGroup = Message<"olivetin.api.v1.ExecutionQueueGroup"> & {
|
export declare type ExecutionQueueAction = Message<"olivetin.api.v1.ExecutionQueueAction"> & {
|
||||||
/**
|
/**
|
||||||
* @generated from field: string binding_id = 1;
|
* @generated from field: string binding_id = 1;
|
||||||
*/
|
*/
|
||||||
|
|
@ -873,6 +873,47 @@ export declare type ExecutionQueueGroup = Message<"olivetin.api.v1.ExecutionQueu
|
||||||
entries: LogEntry[];
|
entries: LogEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the message olivetin.api.v1.ExecutionQueueAction.
|
||||||
|
* Use `create(ExecutionQueueActionSchema)` to create a new message.
|
||||||
|
*/
|
||||||
|
export declare const ExecutionQueueActionSchema: GenMessage<ExecutionQueueAction>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from message olivetin.api.v1.ExecutionQueueGroup
|
||||||
|
*/
|
||||||
|
export declare type ExecutionQueueGroup = Message<"olivetin.api.v1.ExecutionQueueGroup"> & {
|
||||||
|
/**
|
||||||
|
* @generated from field: string name = 1;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string icon = 2;
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: int32 max_concurrent = 3;
|
||||||
|
*/
|
||||||
|
maxConcurrent: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: int32 active_count = 4;
|
||||||
|
*/
|
||||||
|
activeCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated olivetin.api.v1.ExecutionQueueAction actions = 5;
|
||||||
|
*/
|
||||||
|
actions: ExecutionQueueAction[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: int32 queued_count = 6;
|
||||||
|
*/
|
||||||
|
queuedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the message olivetin.api.v1.ExecutionQueueGroup.
|
* Describes the message olivetin.api.v1.ExecutionQueueGroup.
|
||||||
* Use `create(ExecutionQueueGroupSchema)` to create a new message.
|
* Use `create(ExecutionQueueGroupSchema)` to create a new message.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -36,19 +36,25 @@ export function updateLogEntryInGroups (groups, logEntry) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const group of groups) {
|
let firstMatch = null
|
||||||
const entries = group.entries || []
|
|
||||||
const index = entries.findIndex(
|
|
||||||
item => item.executionTrackingId === entry.executionTrackingId
|
|
||||||
)
|
|
||||||
if (index < 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const previous = entries[index]
|
for (const group of groups) {
|
||||||
entries[index] = entry
|
for (const action of group.actions || []) {
|
||||||
return { group, index, previous }
|
const entries = action.entries || []
|
||||||
|
const index = entries.findIndex(
|
||||||
|
item => item.executionTrackingId === entry.executionTrackingId
|
||||||
|
)
|
||||||
|
if (index < 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = entries[index]
|
||||||
|
entries[index] = entry
|
||||||
|
if (!firstMatch) {
|
||||||
|
firstMatch = { group, action, index, previous }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return firstMatch
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,74 +18,73 @@
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
v-for="group in groups"
|
v-for="actionGroup in groups"
|
||||||
:key="group.bindingId"
|
:key="actionGroup.name"
|
||||||
class="with-header-and-content queue-group-section"
|
class="with-header-and-content queue-action-group-section"
|
||||||
>
|
>
|
||||||
<div class="section-header flex-row">
|
<div class="section-header flex-row">
|
||||||
<div class="fg1 queue-group-heading">
|
<div class="fg1 queue-action-group-heading">
|
||||||
<ActionIconGlyph class="queue-group-icon" :glyph="group.actionIcon" />
|
<ActionIconGlyph class="icon" :glyph="actionGroup.icon" />
|
||||||
<div class="queue-group-title">
|
<h2>{{ displayActionGroupName(actionGroup.name) }}</h2>
|
||||||
<h2 :title="group.entityPrefix ? `${t('logs.queue-entity')}: ${group.entityPrefix}` : ''">
|
|
||||||
{{ group.actionTitle }}
|
|
||||||
</h2>
|
|
||||||
<p v-if="group.entityPrefix" class="queue-entity">
|
|
||||||
{{ t('logs.queue-entity') }}: {{ group.entityPrefix }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div role="toolbar" class="queue-group-toolbar">
|
|
||||||
<router-link
|
|
||||||
v-if="group.bindingId"
|
|
||||||
:to="`/action/${group.bindingId}`"
|
|
||||||
class="button neutral"
|
|
||||||
:title="t('logs.queue-action-details')"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.22 2.34 1.8 0 .53-.39 1.39-2.1 1.39-1.6 0-2.05-.56-2.13-1.45H8.04c.08 1.5 1.18 2.37 2.82 2.69V19h2.34v-1.63c1.65-.35 2.48-1.24 2.48-2.77-.01-1.88-1.51-2.87-3.7-3.23z"/>
|
|
||||||
</svg>
|
|
||||||
{{ t('logs.queue-action-details') }}
|
|
||||||
</router-link>
|
|
||||||
<span class="queue-group-limit annotation">
|
|
||||||
{{ t('logs.queue-group-active', { active: group.activeCount, max: group.maxConcurrent }) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="actionGroup.maxConcurrent > 0"
|
||||||
|
class="queue-action-group-limit annotation"
|
||||||
|
>
|
||||||
|
{{ t('logs.queue-group-limit', { max: actionGroup.maxConcurrent, queued: actionGroup.queuedCount }) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<table class="logs-table row-hover">
|
<table class="logs-table row-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('logs.timestamp') }}</th>
|
<th>{{ t('logs.timestamp') }}</th>
|
||||||
<th>{{ t('logs.metadata') }}</th>
|
<th>{{ t('logs.action') }}</th>
|
||||||
<th>{{ t('logs.status') }}</th>
|
<th>{{ t('logs.metadata') }}</th>
|
||||||
</tr>
|
<th>{{ t('logs.status') }}</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="(entry, index) in group.entries" :key="entry.executionTrackingId" class="log-row">
|
<tbody>
|
||||||
<td class="timestamp">{{ formatTimestamp(entry.datetimeStarted) }}</td>
|
<template v-for="action in actionGroup.actions" :key="`${actionGroup.name}:${action.bindingId}`">
|
||||||
<td class="tags">
|
<tr
|
||||||
<span class="annotation">
|
v-for="(entry, index) in action.entries"
|
||||||
<span class="annotation-key">User:</span>
|
:key="entry.executionTrackingId"
|
||||||
<span class="annotation-val">{{ entry.user }}</span>
|
class="log-row"
|
||||||
</span>
|
:title="action.actionTitle"
|
||||||
<span v-if="entry.tags && entry.tags.length > 0" class="tag-list">
|
>
|
||||||
<span v-for="tag in entry.tags" :key="tag" class="tag">{{ tag }}</span>
|
<td class="timestamp">{{ formatTimestamp(entry.datetimeStarted) }}</td>
|
||||||
</span>
|
<td>
|
||||||
<span class="annotation">
|
<ActionIconGlyph class="icon" :glyph="action.actionIcon" />
|
||||||
<span class="annotation-key">ID:</span>
|
<router-link :to="`/logs/${entry.executionTrackingId}`">
|
||||||
<router-link :to="`/logs/${entry.executionTrackingId}`">
|
<LogActionTitle
|
||||||
{{ entry.executionTrackingId }}
|
:action-title="action.actionTitle"
|
||||||
</router-link>
|
:justification="entry.justification"
|
||||||
</span>
|
/>
|
||||||
</td>
|
</router-link>
|
||||||
<td class="exit-code">
|
<span v-if="action.entityPrefix" class="queue-entity annotation">
|
||||||
<span v-if="!entry.executionFinished" class="queue-position">{{ t('logs.queue-position', { position: index + 1 }) }}</span>
|
{{ action.entityPrefix }}
|
||||||
<ActionStatusDisplay :logEntry="entry" />
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td class="tags">
|
||||||
</tbody>
|
<span class="annotation">
|
||||||
</table>
|
<span class="annotation-key">User:</span>
|
||||||
|
<span class="annotation-val">{{ entry.user }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="entry.tags && entry.tags.length > 0" class="tag-list">
|
||||||
|
<span v-for="tag in entry.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="exit-code">
|
||||||
|
<span v-if="!entry.executionFinished" class="queue-position">
|
||||||
|
{{ t('logs.queue-position', { position: index + 1 }) }}
|
||||||
|
</span>
|
||||||
|
<ActionStatusDisplay :logEntry="entry" link-queued-status />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -95,20 +94,33 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
import ActionIconGlyph from '../components/ActionIconGlyph.vue'
|
||||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||||
|
import LogActionTitle from '../components/LogActionTitle.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { getExecutionLogEntry, cloneLogEntry, updateLogEntryInGroups } from '../utils/executionLogEvents.js'
|
import { getExecutionLogEntry, cloneLogEntry, updateLogEntryInGroups } from '../utils/executionLogEvents.js'
|
||||||
|
|
||||||
|
const defaultActionGroupName = 'default'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const groups = ref([])
|
const groups = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
function displayActionGroupName (name) {
|
||||||
|
if (name === defaultActionGroupName) {
|
||||||
|
return t('logs.queue-default-group')
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
function collectCompletedEntries (currentGroups) {
|
function collectCompletedEntries (currentGroups) {
|
||||||
const completed = []
|
const completed = []
|
||||||
for (const group of currentGroups || []) {
|
for (const group of currentGroups || []) {
|
||||||
for (const entry of group.entries || []) {
|
for (const action of group.actions || []) {
|
||||||
if (entry.executionFinished) {
|
for (const entry of action.entries || []) {
|
||||||
completed.push(cloneLogEntry(entry))
|
if (entry.executionFinished) {
|
||||||
|
completed.push(cloneLogEntry(entry))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,53 +138,108 @@ function sortGroupEntries (entries) {
|
||||||
|
|
||||||
function sortGroups (groupList) {
|
function sortGroups (groupList) {
|
||||||
groupList.sort((left, right) => {
|
groupList.sort((left, right) => {
|
||||||
const byTitle = (left.actionTitle || '').localeCompare(right.actionTitle || '')
|
if (left.name === defaultActionGroupName) {
|
||||||
if (byTitle !== 0) {
|
return 1
|
||||||
return byTitle
|
|
||||||
}
|
}
|
||||||
return (left.entityPrefix || '').localeCompare(right.entityPrefix || '')
|
if (right.name === defaultActionGroupName) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return (left.name || '').localeCompare(right.name || '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCompletedEntries (apiGroups, completedEntries) {
|
function sumActionEntries (actions) {
|
||||||
const merged = (apiGroups || []).map(group => ({
|
return (actions || []).reduce((total, action) => total + (action.entries || []).length, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneActionGroup (group) {
|
||||||
|
return {
|
||||||
...group,
|
...group,
|
||||||
entries: [...(group.entries || [])]
|
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) {
|
for (const entry of completedEntries) {
|
||||||
const alreadyPresent = merged.some(group =>
|
const alreadyPresent = merged.some(group =>
|
||||||
group.entries.some(item => item.executionTrackingId === entry.executionTrackingId)
|
(group.actions || []).some(action =>
|
||||||
|
(action.entries || []).some(item => item.executionTrackingId === entry.executionTrackingId)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if (alreadyPresent) {
|
if (alreadyPresent) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let group = merged.find(item => item.bindingId === entry.bindingId)
|
const matches = findActionInGroups(merged, entry.bindingId)
|
||||||
if (!group) {
|
if (matches.length === 0) {
|
||||||
group = {
|
continue
|
||||||
bindingId: entry.bindingId,
|
|
||||||
actionTitle: entry.actionTitle,
|
|
||||||
actionIcon: entry.actionIcon,
|
|
||||||
entityPrefix: '',
|
|
||||||
maxConcurrent: 0,
|
|
||||||
activeCount: 0,
|
|
||||||
entries: []
|
|
||||||
}
|
|
||||||
merged.push(group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.entries.push(entry)
|
for (const { action } of matches) {
|
||||||
|
action.entries.push(entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const group of merged) {
|
for (const group of merged) {
|
||||||
sortGroupEntries(group.entries)
|
for (const action of group.actions || []) {
|
||||||
|
sortGroupEntries(action.entries)
|
||||||
|
action.activeCount = action.entries.length
|
||||||
|
}
|
||||||
|
refreshGroupCounts(group)
|
||||||
}
|
}
|
||||||
sortGroups(merged)
|
sortGroups(merged)
|
||||||
|
|
||||||
return 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) {
|
function applyQueueEntryUpdate (logEntry, afterUpdate) {
|
||||||
const result = updateLogEntryInGroups(groups.value, logEntry)
|
const result = updateLogEntryInGroups(groups.value, logEntry)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|
@ -182,16 +249,13 @@ function applyQueueEntryUpdate (logEntry, afterUpdate) {
|
||||||
if (afterUpdate) {
|
if (afterUpdate) {
|
||||||
afterUpdate(result)
|
afterUpdate(result)
|
||||||
}
|
}
|
||||||
sortGroupEntries(result.group.entries)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustActiveCountOnStart (group, previous, logEntry) {
|
forEachActionWithEntry(groups.value, logEntry.executionTrackingId, (group, action) => {
|
||||||
const wasActive = !previous.executionFinished
|
sortGroupEntries(action.entries)
|
||||||
const isActive = !logEntry.executionFinished
|
refreshGroupCounts(group)
|
||||||
if (!wasActive && isActive) {
|
})
|
||||||
group.activeCount++
|
|
||||||
}
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertActiveQueueEntry (logEntry) {
|
function insertActiveQueueEntry (logEntry) {
|
||||||
|
|
@ -200,23 +264,22 @@ function insertActiveQueueEntry (logEntry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let group = groups.value.find(item => item.bindingId === logEntry.bindingId)
|
const matches = findActionInGroups(groups.value, logEntry.bindingId)
|
||||||
if (!group) {
|
if (matches.length === 0) {
|
||||||
group = {
|
fetchQueue()
|
||||||
bindingId: logEntry.bindingId,
|
return
|
||||||
actionTitle: logEntry.actionTitle || '',
|
|
||||||
actionIcon: logEntry.actionIcon || '',
|
|
||||||
entityPrefix: logEntry.entityPrefix || '',
|
|
||||||
maxConcurrent: 0,
|
|
||||||
activeCount: 0,
|
|
||||||
entries: []
|
|
||||||
}
|
|
||||||
groups.value.push(group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.entries.push(cloneLogEntry(logEntry))
|
const touchedGroups = new Set()
|
||||||
adjustActiveCountOnStart(group, { executionFinished: true }, logEntry)
|
for (const { group, action } of matches) {
|
||||||
sortGroupEntries(group.entries)
|
action.entries.push(cloneLogEntry(logEntry))
|
||||||
|
touchedGroups.add(group)
|
||||||
|
sortGroupEntries(action.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of touchedGroups) {
|
||||||
|
refreshGroupCounts(group)
|
||||||
|
}
|
||||||
sortGroups(groups.value)
|
sortGroups(groups.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,9 +289,7 @@ function onExecutionStarted (evt) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyQueueEntryUpdate(logEntry, ({ group, previous }) => {
|
if (!applyQueueEntryUpdate(logEntry)) {
|
||||||
adjustActiveCountOnStart(group, previous, logEntry)
|
|
||||||
})) {
|
|
||||||
insertActiveQueueEntry(logEntry)
|
insertActiveQueueEntry(logEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -239,12 +300,7 @@ function onExecutionFinished (evt) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
applyQueueEntryUpdate(logEntry, ({ group, previous }) => {
|
applyQueueEntryUpdate(logEntry)
|
||||||
const wasActive = !previous.executionFinished
|
|
||||||
if (wasActive && logEntry.executionFinished && group.activeCount > 0) {
|
|
||||||
group.activeCount--
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimestamp (timestamp) {
|
function formatTimestamp (timestamp) {
|
||||||
|
|
@ -285,50 +341,43 @@ onUnmounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.queue-group-heading {
|
.queue-action-group-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-group-title h2 {
|
.queue-action-group-heading h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-group-toolbar {
|
.queue-action-group-limit {
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-group-limit {
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entity {
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-group-icon {
|
|
||||||
font-size: 1.5em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
.annotation {
|
.annotation {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.queue-entity {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.exit-code {
|
.exit-code {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,13 @@
|
||||||
"logs.no-logs-to-display": "Es gibt keine Protokolle zu anzeigen.",
|
"logs.no-logs-to-display": "Es gibt keine Protokolle zu anzeigen.",
|
||||||
"logs.page-description": "Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.",
|
"logs.page-description": "Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.",
|
||||||
"logs.queue": "Warteschlange",
|
"logs.queue": "Warteschlange",
|
||||||
|
"logs.queue-default-group": "Standard",
|
||||||
"logs.queue-empty": "Derzeit gibt es keine aktiven oder wartenden Ausführungen.",
|
"logs.queue-empty": "Derzeit gibt es keine aktiven oder wartenden Ausführungen.",
|
||||||
"logs.queue-entity": "Entität",
|
"logs.queue-entity": "Entität",
|
||||||
"logs.queue-group-active": "{active} aktiv (max. {max})",
|
"logs.queue-group-active": "{active} aktiv (max. {max})",
|
||||||
"logs.queue-page-description": "Aktive und wartende Ausführungen, nach Aktion gruppiert. Einträge ohne Berechtigung werden ausgeblendet.",
|
"logs.queue-group-active-unlimited": "{active} aktiv",
|
||||||
|
"logs.queue-group-limit": "Limit {max}, wartend {queued}",
|
||||||
|
"logs.queue-page-description": "Aktive und wartende Ausführungen, nach Aktionsgruppe gruppiert. Einträge ohne Berechtigung werden ausgeblendet.",
|
||||||
"logs.queue-position": "#{position}",
|
"logs.queue-position": "#{position}",
|
||||||
"logs.queue-running": "Läuft",
|
"logs.queue-running": "Läuft",
|
||||||
"logs.queue-title": "Ausführungswarteschlange",
|
"logs.queue-title": "Ausführungswarteschlange",
|
||||||
|
|
@ -120,10 +123,13 @@
|
||||||
"logs.page-description": "This is a list of logs from actions that have been executed. Use the filter box for search terms and expressions.",
|
"logs.page-description": "This is a list of logs from actions that have been executed. Use the filter box for search terms and expressions.",
|
||||||
"logs.queue": "Queue",
|
"logs.queue": "Queue",
|
||||||
"logs.queue-action-details": "Action Details",
|
"logs.queue-action-details": "Action Details",
|
||||||
|
"logs.queue-default-group": "Default",
|
||||||
"logs.queue-empty": "There are no active or waiting executions right now.",
|
"logs.queue-empty": "There are no active or waiting executions right now.",
|
||||||
"logs.queue-entity": "Entity",
|
"logs.queue-entity": "Entity",
|
||||||
"logs.queue-group-active": "{active} active (max {max})",
|
"logs.queue-group-active": "{active} active (max {max})",
|
||||||
"logs.queue-page-description": "Active and waiting executions grouped by action. Entries you are not permitted to view are hidden.",
|
"logs.queue-group-active-unlimited": "{active} active",
|
||||||
|
"logs.queue-group-limit": "limit {max}, queued {queued}",
|
||||||
|
"logs.queue-page-description": "Active and waiting executions grouped by action group. Entries you are not permitted to view are hidden.",
|
||||||
"logs.queue-position": "#{position}",
|
"logs.queue-position": "#{position}",
|
||||||
"logs.queue-running": "Running",
|
"logs.queue-running": "Running",
|
||||||
"logs.queue-title": "Execution Queue",
|
"logs.queue-title": "Execution Queue",
|
||||||
|
|
@ -188,10 +194,13 @@
|
||||||
"logs.no-logs-to-display": "No hay registros para mostrar.",
|
"logs.no-logs-to-display": "No hay registros para mostrar.",
|
||||||
"logs.page-description": "Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.",
|
"logs.page-description": "Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.",
|
||||||
"logs.queue": "Cola",
|
"logs.queue": "Cola",
|
||||||
|
"logs.queue-default-group": "Predeterminado",
|
||||||
"logs.queue-empty": "No hay ejecuciones activas o en espera en este momento.",
|
"logs.queue-empty": "No hay ejecuciones activas o en espera en este momento.",
|
||||||
"logs.queue-entity": "Entidad",
|
"logs.queue-entity": "Entidad",
|
||||||
"logs.queue-group-active": "{active} activas (máx. {max})",
|
"logs.queue-group-active": "{active} activas (máx. {max})",
|
||||||
"logs.queue-page-description": "Ejecuciones activas y en espera agrupadas por acción. Las entradas que no puede ver se ocultan.",
|
"logs.queue-group-active-unlimited": "{active} activas",
|
||||||
|
"logs.queue-group-limit": "límite {max}, en espera {queued}",
|
||||||
|
"logs.queue-page-description": "Ejecuciones activas y en espera agrupadas por grupo de acciones. Las entradas que no puede ver se ocultan.",
|
||||||
"logs.queue-position": "#{position}",
|
"logs.queue-position": "#{position}",
|
||||||
"logs.queue-running": "En ejecución",
|
"logs.queue-running": "En ejecución",
|
||||||
"logs.queue-title": "Cola de ejecución",
|
"logs.queue-title": "Cola de ejecución",
|
||||||
|
|
@ -256,10 +265,13 @@
|
||||||
"logs.no-logs-to-display": "Non ci sono registri da mostrare.",
|
"logs.no-logs-to-display": "Non ci sono registri da mostrare.",
|
||||||
"logs.page-description": "Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.",
|
"logs.page-description": "Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.",
|
||||||
"logs.queue": "Coda",
|
"logs.queue": "Coda",
|
||||||
|
"logs.queue-default-group": "Predefinito",
|
||||||
"logs.queue-empty": "Non ci sono esecuzioni attive o in attesa al momento.",
|
"logs.queue-empty": "Non ci sono esecuzioni attive o in attesa al momento.",
|
||||||
"logs.queue-entity": "Entità",
|
"logs.queue-entity": "Entità",
|
||||||
"logs.queue-group-active": "{active} attive (max {max})",
|
"logs.queue-group-active": "{active} attive (max {max})",
|
||||||
"logs.queue-page-description": "Esecuzioni attive e in attesa raggruppate per azione. Le voci non autorizzate sono nascoste.",
|
"logs.queue-group-active-unlimited": "{active} attive",
|
||||||
|
"logs.queue-group-limit": "limite {max}, in coda {queued}",
|
||||||
|
"logs.queue-page-description": "Esecuzioni attive e in attesa raggruppate per gruppo di azioni. Le voci non autorizzate sono nascoste.",
|
||||||
"logs.queue-position": "#{position}",
|
"logs.queue-position": "#{position}",
|
||||||
"logs.queue-running": "In esecuzione",
|
"logs.queue-running": "In esecuzione",
|
||||||
"logs.queue-title": "Coda di esecuzione",
|
"logs.queue-title": "Coda di esecuzione",
|
||||||
|
|
@ -324,10 +336,13 @@
|
||||||
"logs.no-logs-to-display": "没有日志可显示。",
|
"logs.no-logs-to-display": "没有日志可显示。",
|
||||||
"logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。",
|
"logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。",
|
||||||
"logs.queue": "队列",
|
"logs.queue": "队列",
|
||||||
|
"logs.queue-default-group": "默认",
|
||||||
"logs.queue-empty": "当前没有正在运行或等待中的执行。",
|
"logs.queue-empty": "当前没有正在运行或等待中的执行。",
|
||||||
"logs.queue-entity": "实体",
|
"logs.queue-entity": "实体",
|
||||||
"logs.queue-group-active": "{active} 个活动(上限 {max})",
|
"logs.queue-group-active": "{active} 个活动(上限 {max})",
|
||||||
"logs.queue-page-description": "按动作分组显示正在运行和等待中的执行。您无权查看的条目会被隐藏。",
|
"logs.queue-group-active-unlimited": "{active} 个活动",
|
||||||
|
"logs.queue-group-limit": "上限 {max},排队 {queued}",
|
||||||
|
"logs.queue-page-description": "按动作组分组显示正在运行和等待中的执行。您无权查看的条目会被隐藏。",
|
||||||
"logs.queue-position": "第 {position} 位",
|
"logs.queue-position": "第 {position} 位",
|
||||||
"logs.queue-running": "运行中",
|
"logs.queue-running": "运行中",
|
||||||
"logs.queue-title": "执行队列",
|
"logs.queue-title": "执行队列",
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ translations:
|
||||||
logs.back-to-list: Zurück zur Liste
|
logs.back-to-list: Zurück zur Liste
|
||||||
logs.queue: Warteschlange
|
logs.queue: Warteschlange
|
||||||
logs.queue-title: Ausführungswarteschlange
|
logs.queue-title: Ausführungswarteschlange
|
||||||
logs.queue-page-description: Aktive und wartende Ausführungen, nach Aktion gruppiert. Einträge ohne Berechtigung werden ausgeblendet.
|
logs.queue-page-description: Aktive und wartende Ausführungen, nach Aktionsgruppe gruppiert. Einträge ohne Berechtigung werden ausgeblendet.
|
||||||
|
logs.queue-default-group: Standard
|
||||||
|
logs.queue-group-limit: "Limit {max}, wartend {queued}"
|
||||||
|
logs.queue-group-active-unlimited: "{active} aktiv"
|
||||||
logs.queue-empty: Derzeit gibt es keine aktiven oder wartenden Ausführungen.
|
logs.queue-empty: Derzeit gibt es keine aktiven oder wartenden Ausführungen.
|
||||||
logs.queue-group-active: "{active} aktiv (max. {max})"
|
logs.queue-group-active: "{active} aktiv (max. {max})"
|
||||||
logs.queue-waiting: Wartend
|
logs.queue-waiting: Wartend
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,12 @@ translations:
|
||||||
logs.back-to-list: Back to List
|
logs.back-to-list: Back to List
|
||||||
logs.queue: Queue
|
logs.queue: Queue
|
||||||
logs.queue-title: Execution Queue
|
logs.queue-title: Execution Queue
|
||||||
logs.queue-page-description: Active and waiting executions grouped by action. Entries you are not permitted to view are hidden.
|
logs.queue-page-description: Active and waiting executions grouped by action group. Entries you are not permitted to view are hidden.
|
||||||
logs.queue-empty: There are no active or waiting executions right now.
|
logs.queue-empty: There are no active or waiting executions right now.
|
||||||
|
logs.queue-default-group: Default
|
||||||
|
logs.queue-group-limit: "limit {max}, queued {queued}"
|
||||||
logs.queue-group-active: "{active} active (max {max})"
|
logs.queue-group-active: "{active} active (max {max})"
|
||||||
|
logs.queue-group-active-unlimited: "{active} active"
|
||||||
logs.queue-waiting: Waiting
|
logs.queue-waiting: Waiting
|
||||||
logs.queue-running: Running
|
logs.queue-running: Running
|
||||||
logs.queue-position: "#{position}"
|
logs.queue-position: "#{position}"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ translations:
|
||||||
logs.back-to-list: Volver a la Lista
|
logs.back-to-list: Volver a la Lista
|
||||||
logs.queue: Cola
|
logs.queue: Cola
|
||||||
logs.queue-title: Cola de ejecución
|
logs.queue-title: Cola de ejecución
|
||||||
logs.queue-page-description: Ejecuciones activas y en espera agrupadas por acción. Las entradas que no puede ver se ocultan.
|
logs.queue-page-description: Ejecuciones activas y en espera agrupadas por grupo de acciones. Las entradas que no puede ver se ocultan.
|
||||||
|
logs.queue-default-group: Predeterminado
|
||||||
|
logs.queue-group-limit: "límite {max}, en espera {queued}"
|
||||||
|
logs.queue-group-active-unlimited: "{active} activas"
|
||||||
logs.queue-empty: No hay ejecuciones activas o en espera en este momento.
|
logs.queue-empty: No hay ejecuciones activas o en espera en este momento.
|
||||||
logs.queue-group-active: "{active} activas (máx. {max})"
|
logs.queue-group-active: "{active} activas (máx. {max})"
|
||||||
logs.queue-waiting: En espera
|
logs.queue-waiting: En espera
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ translations:
|
||||||
logs.back-to-list: Torna all'Elenco
|
logs.back-to-list: Torna all'Elenco
|
||||||
logs.queue: Coda
|
logs.queue: Coda
|
||||||
logs.queue-title: Coda di esecuzione
|
logs.queue-title: Coda di esecuzione
|
||||||
logs.queue-page-description: Esecuzioni attive e in attesa raggruppate per azione. Le voci non autorizzate sono nascoste.
|
logs.queue-page-description: Esecuzioni attive e in attesa raggruppate per gruppo di azioni. Le voci non autorizzate sono nascoste.
|
||||||
|
logs.queue-default-group: Predefinito
|
||||||
|
logs.queue-group-limit: "limite {max}, in coda {queued}"
|
||||||
|
logs.queue-group-active-unlimited: "{active} attive"
|
||||||
logs.queue-empty: Non ci sono esecuzioni attive o in attesa al momento.
|
logs.queue-empty: Non ci sono esecuzioni attive o in attesa al momento.
|
||||||
logs.queue-group-active: "{active} attive (max {max})"
|
logs.queue-group-active: "{active} attive (max {max})"
|
||||||
logs.queue-waiting: In attesa
|
logs.queue-waiting: In attesa
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ translations:
|
||||||
logs.back-to-list: 返回列表
|
logs.back-to-list: 返回列表
|
||||||
logs.queue: 队列
|
logs.queue: 队列
|
||||||
logs.queue-title: 执行队列
|
logs.queue-title: 执行队列
|
||||||
logs.queue-page-description: 按动作分组显示正在运行和等待中的执行。您无权查看的条目会被隐藏。
|
logs.queue-page-description: 按动作组分组显示正在运行和等待中的执行。您无权查看的条目会被隐藏。
|
||||||
|
logs.queue-default-group: 默认
|
||||||
|
logs.queue-group-limit: "上限 {max},排队 {queued}"
|
||||||
|
logs.queue-group-active-unlimited: "{active} 个活动"
|
||||||
logs.queue-empty: 当前没有正在运行或等待中的执行。
|
logs.queue-empty: 当前没有正在运行或等待中的执行。
|
||||||
logs.queue-group-active: "{active} 个活动(上限 {max})"
|
logs.queue-group-active: "{active} 个活动(上限 {max})"
|
||||||
logs.queue-waiting: 等待中
|
logs.queue-waiting: 等待中
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ message GetActionLogsResponse {
|
||||||
|
|
||||||
message GetExecutionQueueRequest {}
|
message GetExecutionQueueRequest {}
|
||||||
|
|
||||||
message ExecutionQueueGroup {
|
message ExecutionQueueAction {
|
||||||
string binding_id = 1;
|
string binding_id = 1;
|
||||||
string action_title = 2;
|
string action_title = 2;
|
||||||
string action_icon = 3;
|
string action_icon = 3;
|
||||||
|
|
@ -202,6 +202,15 @@ message ExecutionQueueGroup {
|
||||||
repeated LogEntry entries = 7;
|
repeated LogEntry entries = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ExecutionQueueGroup {
|
||||||
|
string name = 1;
|
||||||
|
string icon = 2;
|
||||||
|
int32 max_concurrent = 3;
|
||||||
|
int32 active_count = 4;
|
||||||
|
repeated ExecutionQueueAction actions = 5;
|
||||||
|
int32 queued_count = 6;
|
||||||
|
}
|
||||||
|
|
||||||
message GetExecutionQueueResponse {
|
message GetExecutionQueueResponse {
|
||||||
repeated ExecutionQueueGroup groups = 1;
|
repeated ExecutionQueueGroup groups = 1;
|
||||||
int32 total_active = 2;
|
int32 total_active = 2;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,9 +8,17 @@ import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
"github.com/OliveTin/OliveTin/internal/auth"
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultActionGroupName = "default"
|
||||||
|
|
||||||
|
type executionQueueBucketKey struct {
|
||||||
|
groupName string
|
||||||
|
bindingID string
|
||||||
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetExecutionQueue(ctx ctx.Context, req *connect.Request[apiv1.GetExecutionQueueRequest]) (*connect.Response[apiv1.GetExecutionQueueResponse], error) {
|
func (api *oliveTinAPI) GetExecutionQueue(ctx ctx.Context, req *connect.Request[apiv1.GetExecutionQueueRequest]) (*connect.Response[apiv1.GetExecutionQueueResponse], error) {
|
||||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
|
|
@ -28,23 +36,66 @@ func (api *oliveTinAPI) GetExecutionQueue(ctx ctx.Context, req *connect.Request[
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildExecutionQueueGroups(active []*executor.InternalLogEntry, user *authpublic.AuthenticatedUser, api *oliveTinAPI) []*apiv1.ExecutionQueueGroup {
|
func buildExecutionQueueGroups(active []*executor.InternalLogEntry, user *authpublic.AuthenticatedUser, api *oliveTinAPI) []*apiv1.ExecutionQueueGroup {
|
||||||
grouped := make(map[string]*apiv1.ExecutionQueueGroup)
|
actionBuckets := make(map[executionQueueBucketKey]*apiv1.ExecutionQueueAction)
|
||||||
|
|
||||||
for _, entry := range active {
|
for _, entry := range active {
|
||||||
bindingID := entry.GetBindingId()
|
addActiveEntryToActionBuckets(actionBuckets, entry, api.cfg, user, api)
|
||||||
group := grouped[bindingID]
|
}
|
||||||
if group == nil {
|
|
||||||
group = newExecutionQueueGroup(entry)
|
return buildExecutionQueueGroupsFromBuckets(actionBuckets, api.cfg)
|
||||||
grouped[bindingID] = group
|
}
|
||||||
|
|
||||||
|
func addActiveEntryToActionBuckets(
|
||||||
|
buckets map[executionQueueBucketKey]*apiv1.ExecutionQueueAction,
|
||||||
|
entry *executor.InternalLogEntry,
|
||||||
|
cfg *config.Config,
|
||||||
|
user *authpublic.AuthenticatedUser,
|
||||||
|
api *oliveTinAPI,
|
||||||
|
) {
|
||||||
|
for _, groupName := range enforcedActionGroupNames(entry, cfg) {
|
||||||
|
key := executionQueueBucketKey{
|
||||||
|
groupName: groupName,
|
||||||
|
bindingID: entry.GetBindingId(),
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Entries = append(group.Entries, api.internalLogEntryToPb(entry, user))
|
action := buckets[key]
|
||||||
|
if action == nil {
|
||||||
|
action = newExecutionQueueAction(entry)
|
||||||
|
buckets[key] = action
|
||||||
|
}
|
||||||
|
|
||||||
|
action.Entries = append(action.Entries, api.internalLogEntryToPb(entry, user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeExecutionQueueGroup(group *apiv1.ExecutionQueueGroup) {
|
||||||
|
sortExecutionQueueActions(group.Actions)
|
||||||
|
group.ActiveCount = sumExecutionQueueActionEntries(group.Actions)
|
||||||
|
group.QueuedCount = countQueuedGroupEntries(group.Actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildExecutionQueueGroupsFromBuckets(
|
||||||
|
buckets map[executionQueueBucketKey]*apiv1.ExecutionQueueAction,
|
||||||
|
cfg *config.Config,
|
||||||
|
) []*apiv1.ExecutionQueueGroup {
|
||||||
|
grouped := make(map[string]*apiv1.ExecutionQueueGroup)
|
||||||
|
|
||||||
|
for key, action := range buckets {
|
||||||
|
sortQueueEntries(action.Entries)
|
||||||
|
action.ActiveCount = int32(len(action.Entries))
|
||||||
|
|
||||||
|
group := grouped[key.groupName]
|
||||||
|
if group == nil {
|
||||||
|
group = newExecutionQueueGroup(key.groupName, cfg)
|
||||||
|
grouped[key.groupName] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Actions = append(group.Actions, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make([]*apiv1.ExecutionQueueGroup, 0, len(grouped))
|
groups := make([]*apiv1.ExecutionQueueGroup, 0, len(grouped))
|
||||||
for _, group := range grouped {
|
for _, group := range grouped {
|
||||||
sortQueueEntries(group.Entries)
|
finalizeExecutionQueueGroup(group)
|
||||||
group.ActiveCount = int32(len(group.Entries))
|
|
||||||
groups = append(groups, group)
|
groups = append(groups, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,8 +103,56 @@ func buildExecutionQueueGroups(active []*executor.InternalLogEntry, user *authpu
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
func newExecutionQueueGroup(entry *executor.InternalLogEntry) *apiv1.ExecutionQueueGroup {
|
func hasExecutionQueueBinding(entry *executor.InternalLogEntry, cfg *config.Config) bool {
|
||||||
group := &apiv1.ExecutionQueueGroup{
|
return entry != nil && entry.Binding != nil && entry.Binding.Action != nil && cfg != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectEnforcedActionGroupNames(groups []string, cfg *config.Config) []string {
|
||||||
|
names := make([]string, 0, len(groups))
|
||||||
|
for _, groupName := range groups {
|
||||||
|
if isEnforcedActionGroup(cfg, groupName) {
|
||||||
|
names = append(names, groupName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func enforcedActionGroupNames(entry *executor.InternalLogEntry, cfg *config.Config) []string {
|
||||||
|
if !hasExecutionQueueBinding(entry, cfg) {
|
||||||
|
return []string{defaultActionGroupName}
|
||||||
|
}
|
||||||
|
|
||||||
|
names := collectEnforcedActionGroupNames(entry.Binding.Action.Groups, cfg)
|
||||||
|
if len(names) == 0 {
|
||||||
|
return []string{defaultActionGroupName}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEnforcedActionGroup(cfg *config.Config, groupName string) bool {
|
||||||
|
group, found := cfg.ActionGroups[groupName]
|
||||||
|
return found && group != nil && group.MaxConcurrent >= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecutionQueueGroup(name string, cfg *config.Config) *apiv1.ExecutionQueueGroup {
|
||||||
|
group := &apiv1.ExecutionQueueGroup{Name: name}
|
||||||
|
if name == defaultActionGroupName {
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
actionGroup, found := cfg.ActionGroups[name]
|
||||||
|
if !found || actionGroup == nil {
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Icon = actionGroup.Icon
|
||||||
|
group.MaxConcurrent = int32(actionGroup.MaxConcurrent)
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecutionQueueAction(entry *executor.InternalLogEntry) *apiv1.ExecutionQueueAction {
|
||||||
|
action := &apiv1.ExecutionQueueAction{
|
||||||
BindingId: entry.GetBindingId(),
|
BindingId: entry.GetBindingId(),
|
||||||
ActionTitle: entry.ActionTitle,
|
ActionTitle: entry.ActionTitle,
|
||||||
ActionIcon: entry.ActionIcon,
|
ActionIcon: entry.ActionIcon,
|
||||||
|
|
@ -61,10 +160,34 @@ func newExecutionQueueGroup(entry *executor.InternalLogEntry) *apiv1.ExecutionQu
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.Binding != nil && entry.Binding.Action != nil {
|
if entry.Binding != nil && entry.Binding.Action != nil {
|
||||||
group.MaxConcurrent = int32(entry.Binding.Action.MaxConcurrent)
|
action.MaxConcurrent = int32(entry.Binding.Action.MaxConcurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return group
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumExecutionQueueActionEntries(actions []*apiv1.ExecutionQueueAction) int32 {
|
||||||
|
var total int32
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
total += int32(len(action.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func countQueuedGroupEntries(actions []*apiv1.ExecutionQueueAction) int32 {
|
||||||
|
var total int32
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
for _, entry := range action.Entries {
|
||||||
|
if entry.Queued {
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortQueueEntries(entries []*apiv1.LogEntry) {
|
func sortQueueEntries(entries []*apiv1.LogEntry) {
|
||||||
|
|
@ -73,13 +196,31 @@ func sortQueueEntries(entries []*apiv1.LogEntry) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortExecutionQueueGroups(groups []*apiv1.ExecutionQueueGroup) {
|
func sortExecutionQueueActions(actions []*apiv1.ExecutionQueueAction) {
|
||||||
sort.Slice(groups, func(i, j int) bool {
|
sort.Slice(actions, func(i, j int) bool {
|
||||||
left := groups[i].ActionTitle
|
left := actions[i].ActionTitle
|
||||||
right := groups[j].ActionTitle
|
right := actions[j].ActionTitle
|
||||||
if left == right {
|
if left == right {
|
||||||
return groups[i].EntityPrefix < groups[j].EntityPrefix
|
return actions[i].EntityPrefix < actions[j].EntityPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return left < right
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortExecutionQueueGroups(groups []*apiv1.ExecutionQueueGroup) {
|
||||||
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
|
left := groups[i].Name
|
||||||
|
right := groups[j].Name
|
||||||
|
|
||||||
|
if left == defaultActionGroupName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if right == defaultActionGroupName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return left < right
|
return left < right
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,13 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetExecutionQueueGroupsByBinding(t *testing.T) {
|
func TestGetExecutionQueueGroupsByActionGroup(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.ActionGroups = map[string]*config.ActionGroup{
|
||||||
|
"deploy": {MaxConcurrent: 2, Icon: "backup"},
|
||||||
|
}
|
||||||
cfg.Actions = []*config.Action{
|
cfg.Actions = []*config.Action{
|
||||||
{Title: "backup", Shell: "sleep 1", MaxConcurrent: 1},
|
{Title: "backup", Shell: "sleep 1", MaxConcurrent: 1, Groups: []string{"deploy"}},
|
||||||
{Title: "ping", Shell: "echo ping"},
|
{Title: "ping", Shell: "echo ping"},
|
||||||
}
|
}
|
||||||
cfg.Sanitize()
|
cfg.Sanitize()
|
||||||
|
|
@ -32,6 +35,7 @@ func TestGetExecutionQueueGroupsByBinding(t *testing.T) {
|
||||||
|
|
||||||
backupRunning := newAPIQueueLogEntry(backupBinding, true, false)
|
backupRunning := newAPIQueueLogEntry(backupBinding, true, false)
|
||||||
backupWaiting := newAPIQueueLogEntry(backupBinding, false, false)
|
backupWaiting := newAPIQueueLogEntry(backupBinding, false, false)
|
||||||
|
backupWaiting.Queued = true
|
||||||
pingRunning := newAPIQueueLogEntry(pingBinding, true, false)
|
pingRunning := newAPIQueueLogEntry(pingBinding, true, false)
|
||||||
|
|
||||||
ex.SetLog(backupRunning.ExecutionTrackingID, backupRunning)
|
ex.SetLog(backupRunning.ExecutionTrackingID, backupRunning)
|
||||||
|
|
@ -47,20 +51,32 @@ func TestGetExecutionQueueGroupsByBinding(t *testing.T) {
|
||||||
assert.Equal(t, int32(3), resp.Msg.TotalActive)
|
assert.Equal(t, int32(3), resp.Msg.TotalActive)
|
||||||
require.Len(t, resp.Msg.Groups, 2)
|
require.Len(t, resp.Msg.Groups, 2)
|
||||||
|
|
||||||
var backupGroup *apiv1.ExecutionQueueGroup
|
deployGroup := findExecutionQueueGroup(resp.Msg.Groups, "deploy")
|
||||||
for _, group := range resp.Msg.Groups {
|
defaultGroup := findExecutionQueueGroup(resp.Msg.Groups, defaultActionGroupName)
|
||||||
if group.BindingId == backupBinding.ID {
|
require.NotNil(t, deployGroup)
|
||||||
backupGroup = group
|
require.NotNil(t, defaultGroup)
|
||||||
|
|
||||||
|
assert.Equal(t, int32(2), deployGroup.MaxConcurrent)
|
||||||
|
assert.Equal(t, "💾", deployGroup.Icon)
|
||||||
|
assert.Equal(t, int32(2), deployGroup.ActiveCount)
|
||||||
|
assert.Equal(t, int32(1), deployGroup.QueuedCount)
|
||||||
|
require.Len(t, deployGroup.Actions, 1)
|
||||||
|
assert.Equal(t, "backup", deployGroup.Actions[0].ActionTitle)
|
||||||
|
require.Len(t, deployGroup.Actions[0].Entries, 2)
|
||||||
|
|
||||||
|
require.Len(t, defaultGroup.Actions, 1)
|
||||||
|
assert.Equal(t, "ping", defaultGroup.Actions[0].ActionTitle)
|
||||||
|
assert.Equal(t, int32(1), defaultGroup.Actions[0].ActiveCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findExecutionQueueGroup(groups []*apiv1.ExecutionQueueGroup, name string) *apiv1.ExecutionQueueGroup {
|
||||||
|
for _, group := range groups {
|
||||||
|
if group.Name == name {
|
||||||
|
return group
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NotNil(t, backupGroup)
|
return nil
|
||||||
assert.Equal(t, "backup", backupGroup.ActionTitle)
|
|
||||||
assert.Equal(t, int32(1), backupGroup.MaxConcurrent)
|
|
||||||
assert.Equal(t, int32(2), backupGroup.ActiveCount)
|
|
||||||
require.Len(t, backupGroup.Entries, 2)
|
|
||||||
assert.False(t, backupGroup.Entries[1].ExecutionStarted)
|
|
||||||
assert.True(t, backupGroup.Entries[0].ExecutionStarted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIQueueLogEntry(binding *executor.ActionBinding, started bool, finished bool) *executor.InternalLogEntry {
|
func newAPIQueueLogEntry(binding *executor.ActionBinding, started bool, finished bool) *executor.InternalLogEntry {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ func (cfg *Config) Sanitize() {
|
||||||
|
|
||||||
cfg.sanitizeDashboardsForInlineActions()
|
cfg.sanitizeDashboardsForInlineActions()
|
||||||
|
|
||||||
|
cfg.sanitizeActionGroups()
|
||||||
cfg.sanitizeActionGroupReferences()
|
cfg.sanitizeActionGroupReferences()
|
||||||
|
|
||||||
if err := cfg.validateReservedActionArgumentNames(); err != nil {
|
if err := cfg.validateReservedActionArgumentNames(); err != nil {
|
||||||
|
|
@ -218,6 +219,16 @@ func appendUniqueString(out []string, seen map[string]struct{}, value string) []
|
||||||
return append(out, value)
|
return append(out, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) sanitizeActionGroups() {
|
||||||
|
for _, group := range cfg.ActionGroups {
|
||||||
|
if group == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Icon = lookupHTMLIcon(group.Icon, cfg.DefaultIconForActions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *Config) sanitizeActionGroupReferences() {
|
func (cfg *Config) sanitizeActionGroupReferences() {
|
||||||
for _, action := range cfg.Actions {
|
for _, action := range cfg.Actions {
|
||||||
for _, groupName := range action.Groups {
|
for _, groupName := range action.Groups {
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,17 @@ func TestSanitizeActionGroupsDedupesGroupNames(t *testing.T) {
|
||||||
assert.Equal(t, []string{"unity"}, action.Groups)
|
assert.Equal(t, []string{"unity"}, action.Groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSanitizeActionGroupsResolvesIcons(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.ActionGroups = map[string]*ActionGroup{
|
||||||
|
"backup-jobs": {MaxConcurrent: 1, Icon: "backup"},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Sanitize()
|
||||||
|
|
||||||
|
assert.Equal(t, "💾", c.ActionGroups["backup-jobs"].Icon)
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateReservedActionArgumentNamesAllowsNonReserved(t *testing.T) {
|
func TestValidateReservedActionArgumentNamesAllowsNonReserved(t *testing.T) {
|
||||||
c := DefaultConfig()
|
c := DefaultConfig()
|
||||||
c.Actions = append(c.Actions, &Action{
|
c.Actions = append(c.Actions, &Action{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue