mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-24 08:19:51 +00:00
Compare commits
2 Commits
drjkl/roun
...
queue-over
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6737730f6 | ||
|
|
f548af2f1d |
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
|
||||||
v-if="!workspaceStore.focusMode"
|
|
||||||
class="ml-1 flex gap-x-0.5 pt-1"
|
|
||||||
@mouseenter="isTopMenuHovered = true"
|
|
||||||
@mouseleave="isTopMenuHovered = false"
|
|
||||||
>
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<SubgraphBreadcrumb />
|
<SubgraphBreadcrumb />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,10 +48,7 @@
|
|||||||
<i class="icon-[lucide--panel-right] size-4" />
|
<i class="icon-[lucide--panel-right] size-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<QueueProgressOverlay
|
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
||||||
v-model:expanded="isQueueOverlayExpanded"
|
|
||||||
:menu-hovered="isTopMenuHovered"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,7 +80,6 @@ const isDesktop = isElectron()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const isQueueOverlayExpanded = ref(false)
|
const isQueueOverlayExpanded = ref(false)
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const isTopMenuHovered = ref(false)
|
|
||||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||||
const queueHistoryTooltipConfig = computed(() =>
|
const queueHistoryTooltipConfig = computed(() =>
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
|
||||||
import { createI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import QueueOverlayActive from './QueueOverlayActive.vue'
|
|
||||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
|
||||||
|
|
||||||
const i18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
sideToolbar: {
|
|
||||||
queueProgressOverlay: {
|
|
||||||
total: 'Total: {percent}',
|
|
||||||
currentNode: 'Current node:',
|
|
||||||
running: 'running',
|
|
||||||
interruptAll: 'Interrupt all running jobs',
|
|
||||||
queuedSuffix: 'queued',
|
|
||||||
clearQueued: 'Clear queued',
|
|
||||||
viewAllJobs: 'View all jobs',
|
|
||||||
cancelJobTooltip: 'Cancel job',
|
|
||||||
clearQueueTooltip: 'Clear queue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const tooltipDirectiveStub = {
|
|
||||||
mounted: vi.fn(),
|
|
||||||
updated: vi.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
const SELECTORS = {
|
|
||||||
interruptAllButton: 'button[aria-label="Interrupt all running jobs"]',
|
|
||||||
clearQueuedButton: 'button[aria-label="Clear queued"]',
|
|
||||||
summaryRow: '.flex.items-center.gap-2',
|
|
||||||
currentNodeRow: '.flex.items-center.gap-1.text-text-secondary'
|
|
||||||
}
|
|
||||||
|
|
||||||
const COPY = {
|
|
||||||
viewAllJobs: 'View all jobs'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountComponent = (props: Record<string, unknown> = {}) =>
|
|
||||||
mount(QueueOverlayActive, {
|
|
||||||
props: {
|
|
||||||
totalProgressStyle: { width: '65%' },
|
|
||||||
currentNodeProgressStyle: { width: '40%' },
|
|
||||||
totalPercentFormatted: '65%',
|
|
||||||
currentNodePercentFormatted: '40%',
|
|
||||||
currentNodeName: 'Sampler',
|
|
||||||
runningCount: 1,
|
|
||||||
queuedCount: 2,
|
|
||||||
bottomRowClass: 'flex custom-bottom-row',
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
global: {
|
|
||||||
plugins: [i18n],
|
|
||||||
directives: {
|
|
||||||
tooltip: tooltipDirectiveStub
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('QueueOverlayActive', () => {
|
|
||||||
it('renders progress metrics and emits actions when buttons clicked', async () => {
|
|
||||||
const wrapper = mountComponent({ runningCount: 2, queuedCount: 3 })
|
|
||||||
|
|
||||||
const progressBars = wrapper.findAll('.absolute.inset-0')
|
|
||||||
expect(progressBars[0].attributes('style')).toContain('width: 65%')
|
|
||||||
expect(progressBars[1].attributes('style')).toContain('width: 40%')
|
|
||||||
|
|
||||||
const content = wrapper.text().replace(/\s+/g, ' ')
|
|
||||||
expect(content).toContain('Total: 65%')
|
|
||||||
|
|
||||||
const [runningSection, queuedSection] = wrapper.findAll(
|
|
||||||
SELECTORS.summaryRow
|
|
||||||
)
|
|
||||||
expect(runningSection.text()).toContain('2')
|
|
||||||
expect(runningSection.text()).toContain('running')
|
|
||||||
expect(queuedSection.text()).toContain('3')
|
|
||||||
expect(queuedSection.text()).toContain('queued')
|
|
||||||
|
|
||||||
const currentNodeSection = wrapper.find(SELECTORS.currentNodeRow)
|
|
||||||
expect(currentNodeSection.text()).toContain('Current node:')
|
|
||||||
expect(currentNodeSection.text()).toContain('Sampler')
|
|
||||||
expect(currentNodeSection.text()).toContain('40%')
|
|
||||||
|
|
||||||
const interruptButton = wrapper.get(SELECTORS.interruptAllButton)
|
|
||||||
await interruptButton.trigger('click')
|
|
||||||
expect(wrapper.emitted('interruptAll')).toHaveLength(1)
|
|
||||||
|
|
||||||
const clearQueuedButton = wrapper.get(SELECTORS.clearQueuedButton)
|
|
||||||
await clearQueuedButton.trigger('click')
|
|
||||||
expect(wrapper.emitted('clearQueued')).toHaveLength(1)
|
|
||||||
|
|
||||||
const buttons = wrapper.findAll('button')
|
|
||||||
const viewAllButton = buttons.find((btn) =>
|
|
||||||
btn.text().includes(COPY.viewAllJobs)
|
|
||||||
)
|
|
||||||
expect(viewAllButton).toBeDefined()
|
|
||||||
await viewAllButton!.trigger('click')
|
|
||||||
expect(wrapper.emitted('viewAllJobs')).toHaveLength(1)
|
|
||||||
|
|
||||||
expect(wrapper.find('.custom-bottom-row').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hides action buttons when counts are zero', () => {
|
|
||||||
const wrapper = mountComponent({ runningCount: 0, queuedCount: 0 })
|
|
||||||
|
|
||||||
expect(wrapper.find(SELECTORS.interruptAllButton).exists()).toBe(false)
|
|
||||||
expect(wrapper.find(SELECTORS.clearQueuedButton).exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('builds tooltip configs with translated strings', () => {
|
|
||||||
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
|
|
||||||
|
|
||||||
mountComponent()
|
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith('Cancel job')
|
|
||||||
expect(spy).toHaveBeenCalledWith('Clear queue')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col gap-3 p-2">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div
|
|
||||||
class="relative h-2 w-full overflow-hidden rounded-full border border-interface-stroke bg-interface-panel-surface"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 h-full rounded-full transition-[width]"
|
|
||||||
:style="totalProgressStyle"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 h-full rounded-full transition-[width]"
|
|
||||||
:style="currentNodeProgressStyle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start justify-end gap-4 text-[12px] leading-none">
|
|
||||||
<div class="flex items-center gap-1 text-text-primary opacity-90">
|
|
||||||
<i18n-t keypath="sideToolbar.queueProgressOverlay.total">
|
|
||||||
<template #percent>
|
|
||||||
<span class="font-bold">{{ totalPercentFormatted }}</span>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1 text-text-secondary">
|
|
||||||
<span>{{ t('sideToolbar.queueProgressOverlay.currentNode') }}</span>
|
|
||||||
<span class="inline-block max-w-[10rem] truncate">{{
|
|
||||||
currentNodeName
|
|
||||||
}}</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<span>{{ currentNodePercentFormatted }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :class="bottomRowClass">
|
|
||||||
<div class="flex items-center gap-4 text-[12px] text-text-primary">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="opacity-90">
|
|
||||||
<span class="font-bold">{{ runningCount }}</span>
|
|
||||||
<span class="ml-1">{{
|
|
||||||
t('sideToolbar.queueProgressOverlay.running')
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<IconButton
|
|
||||||
v-if="runningCount > 0"
|
|
||||||
v-tooltip.top="cancelJobTooltip"
|
|
||||||
type="secondary"
|
|
||||||
size="sm"
|
|
||||||
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
|
|
||||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
|
||||||
@click="$emit('interruptAll')"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="opacity-90">
|
|
||||||
<span class="font-bold">{{ queuedCount }}</span>
|
|
||||||
<span class="ml-1">{{
|
|
||||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<IconButton
|
|
||||||
v-if="queuedCount > 0"
|
|
||||||
v-tooltip.top="clearQueueTooltip"
|
|
||||||
type="secondary"
|
|
||||||
size="sm"
|
|
||||||
class="size-6 bg-secondary-background hover:bg-destructive-background"
|
|
||||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
|
||||||
@click="$emit('clearQueued')"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextButton
|
|
||||||
class="h-6 min-w-[120px] flex-1 px-2 py-0 text-[12px]"
|
|
||||||
type="secondary"
|
|
||||||
:label="t('sideToolbar.queueProgressOverlay.viewAllJobs')"
|
|
||||||
@click="$emit('viewAllJobs')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import IconButton from '@/components/button/IconButton.vue'
|
|
||||||
import TextButton from '@/components/button/TextButton.vue'
|
|
||||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
totalProgressStyle: Record<string, string>
|
|
||||||
currentNodeProgressStyle: Record<string, string>
|
|
||||||
totalPercentFormatted: string
|
|
||||||
currentNodePercentFormatted: string
|
|
||||||
currentNodeName: string
|
|
||||||
runningCount: number
|
|
||||||
queuedCount: number
|
|
||||||
bottomRowClass: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
(e: 'interruptAll'): void
|
|
||||||
(e: 'clearQueued'): void
|
|
||||||
(e: 'viewAllJobs'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const cancelJobTooltip = computed(() =>
|
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.cancelJobTooltip'))
|
|
||||||
)
|
|
||||||
const clearQueueTooltip = computed(() =>
|
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearQueueTooltip'))
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -46,18 +46,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<JobFiltersBar
|
|
||||||
:selected-job-tab="selectedJobTab"
|
|
||||||
:selected-workflow-filter="selectedWorkflowFilter"
|
|
||||||
:selected-sort-mode="selectedSortMode"
|
|
||||||
:has-failed-jobs="hasFailedJobs"
|
|
||||||
@update:selected-job-tab="$emit('update:selectedJobTab', $event)"
|
|
||||||
@update:selected-workflow-filter="
|
|
||||||
$emit('update:selectedWorkflowFilter', $event)
|
|
||||||
"
|
|
||||||
@update:selected-sort-mode="$emit('update:selectedSortMode', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex-1 min-h-0 overflow-y-auto">
|
<div class="flex-1 min-h-0 overflow-y-auto">
|
||||||
<JobGroupsList
|
<JobGroupsList
|
||||||
:displayed-job-groups="displayedJobGroups"
|
:displayed-job-groups="displayedJobGroups"
|
||||||
@@ -82,18 +70,12 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import IconButton from '@/components/button/IconButton.vue'
|
import IconButton from '@/components/button/IconButton.vue'
|
||||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||||
import type {
|
import type { JobGroup, JobListItem } from '@/composables/queue/useJobList'
|
||||||
JobGroup,
|
|
||||||
JobListItem,
|
|
||||||
JobSortMode,
|
|
||||||
JobTab
|
|
||||||
} from '@/composables/queue/useJobList'
|
|
||||||
import type { MenuEntry } from '@/composables/queue/useJobMenu'
|
import type { MenuEntry } from '@/composables/queue/useJobMenu'
|
||||||
import { useJobMenu } from '@/composables/queue/useJobMenu'
|
import { useJobMenu } from '@/composables/queue/useJobMenu'
|
||||||
|
|
||||||
import QueueOverlayHeader from './QueueOverlayHeader.vue'
|
import QueueOverlayHeader from './QueueOverlayHeader.vue'
|
||||||
import JobContextMenu from './job/JobContextMenu.vue'
|
import JobContextMenu from './job/JobContextMenu.vue'
|
||||||
import JobFiltersBar from './job/JobFiltersBar.vue'
|
|
||||||
import JobGroupsList from './job/JobGroupsList.vue'
|
import JobGroupsList from './job/JobGroupsList.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -101,20 +83,13 @@ defineProps<{
|
|||||||
showConcurrentIndicator: boolean
|
showConcurrentIndicator: boolean
|
||||||
concurrentWorkflowCount: number
|
concurrentWorkflowCount: number
|
||||||
queuedCount: number
|
queuedCount: number
|
||||||
selectedJobTab: JobTab
|
|
||||||
selectedWorkflowFilter: 'all' | 'current'
|
|
||||||
selectedSortMode: JobSortMode
|
|
||||||
displayedJobGroups: JobGroup[]
|
displayedJobGroups: JobGroup[]
|
||||||
hasFailedJobs: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'showAssets'): void
|
(e: 'showAssets'): void
|
||||||
(e: 'clearHistory'): void
|
(e: 'clearHistory'): void
|
||||||
(e: 'clearQueued'): void
|
(e: 'clearQueued'): void
|
||||||
(e: 'update:selectedJobTab', value: JobTab): void
|
|
||||||
(e: 'update:selectedWorkflowFilter', value: 'all' | 'current'): void
|
|
||||||
(e: 'update:selectedSortMode', value: JobSortMode): void
|
|
||||||
(e: 'cancelItem', item: JobListItem): void
|
(e: 'cancelItem', item: JobListItem): void
|
||||||
(e: 'deleteItem', item: JobListItem): void
|
(e: 'deleteItem', item: JobListItem): void
|
||||||
(e: 'viewItem', item: JobListItem): void
|
(e: 'viewItem', item: JobListItem): void
|
||||||
|
|||||||
@@ -6,22 +6,16 @@
|
|||||||
<div
|
<div
|
||||||
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
|
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
|
||||||
:class="containerClass"
|
:class="containerClass"
|
||||||
@mouseenter="isHovered = true"
|
|
||||||
@mouseleave="isHovered = false"
|
|
||||||
>
|
>
|
||||||
<!-- Expanded state -->
|
<!-- Expanded state -->
|
||||||
<QueueOverlayExpanded
|
<QueueOverlayExpanded
|
||||||
v-if="isExpanded"
|
v-if="isExpanded"
|
||||||
v-model:selected-job-tab="selectedJobTab"
|
|
||||||
v-model:selected-workflow-filter="selectedWorkflowFilter"
|
|
||||||
v-model:selected-sort-mode="selectedSortMode"
|
|
||||||
class="flex-1 min-h-0"
|
class="flex-1 min-h-0"
|
||||||
:header-title="headerTitle"
|
:header-title="headerTitle"
|
||||||
:show-concurrent-indicator="showConcurrentIndicator"
|
:show-concurrent-indicator="showConcurrentIndicator"
|
||||||
:concurrent-workflow-count="concurrentWorkflowCount"
|
:concurrent-workflow-count="concurrentWorkflowCount"
|
||||||
:queued-count="queuedCount"
|
:queued-count="queuedCount"
|
||||||
:displayed-job-groups="displayedJobGroups"
|
:displayed-job-groups="displayedJobGroups"
|
||||||
:has-failed-jobs="hasFailedJobs"
|
|
||||||
@show-assets="openAssetsSidebar"
|
@show-assets="openAssetsSidebar"
|
||||||
@clear-history="onClearHistoryFromMenu"
|
@clear-history="onClearHistoryFromMenu"
|
||||||
@clear-queued="cancelQueuedWorkflows"
|
@clear-queued="cancelQueuedWorkflows"
|
||||||
@@ -30,21 +24,6 @@
|
|||||||
@view-item="inspectJobAsset"
|
@view-item="inspectJobAsset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QueueOverlayActive
|
|
||||||
v-else-if="hasActiveJob"
|
|
||||||
:total-progress-style="totalProgressStyle"
|
|
||||||
:current-node-progress-style="currentNodeProgressStyle"
|
|
||||||
:total-percent-formatted="totalPercentFormatted"
|
|
||||||
:current-node-percent-formatted="currentNodePercentFormatted"
|
|
||||||
:current-node-name="currentNodeName"
|
|
||||||
:running-count="runningCount"
|
|
||||||
:queued-count="queuedCount"
|
|
||||||
:bottom-row-class="bottomRowClass"
|
|
||||||
@interrupt-all="interruptAll"
|
|
||||||
@clear-queued="cancelQueuedWorkflows"
|
|
||||||
@view-all-jobs="viewAllJobs"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<QueueOverlayEmpty
|
<QueueOverlayEmpty
|
||||||
v-else-if="completionSummary"
|
v-else-if="completionSummary"
|
||||||
:summary="completionSummary"
|
:summary="completionSummary"
|
||||||
@@ -63,7 +42,6 @@
|
|||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import QueueOverlayActive from '@/components/queue/QueueOverlayActive.vue'
|
|
||||||
import QueueOverlayEmpty from '@/components/queue/QueueOverlayEmpty.vue'
|
import QueueOverlayEmpty from '@/components/queue/QueueOverlayEmpty.vue'
|
||||||
import QueueOverlayExpanded from '@/components/queue/QueueOverlayExpanded.vue'
|
import QueueOverlayExpanded from '@/components/queue/QueueOverlayExpanded.vue'
|
||||||
import QueueClearHistoryDialog from '@/components/queue/dialogs/QueueClearHistoryDialog.vue'
|
import QueueClearHistoryDialog from '@/components/queue/dialogs/QueueClearHistoryDialog.vue'
|
||||||
@@ -71,11 +49,9 @@ import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
|||||||
import { useCompletionSummary } from '@/composables/queue/useCompletionSummary'
|
import { useCompletionSummary } from '@/composables/queue/useCompletionSummary'
|
||||||
import { useJobList } from '@/composables/queue/useJobList'
|
import { useJobList } from '@/composables/queue/useJobList'
|
||||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
|
||||||
import { useResultGallery } from '@/composables/queue/useResultGallery'
|
import { useResultGallery } from '@/composables/queue/useResultGallery'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { useAssetSelectionStore } from '@/platform/assets/composables/useAssetSelectionStore'
|
import { useAssetSelectionStore } from '@/platform/assets/composables/useAssetSelectionStore'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { useAssetsStore } from '@/stores/assetsStore'
|
import { useAssetsStore } from '@/stores/assetsStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
@@ -84,17 +60,11 @@ import { useExecutionStore } from '@/stores/executionStore'
|
|||||||
import { useQueueStore } from '@/stores/queueStore'
|
import { useQueueStore } from '@/stores/queueStore'
|
||||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||||
|
|
||||||
type OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'
|
type OverlayState = 'hidden' | 'empty' | 'expanded'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<{
|
expanded?: boolean
|
||||||
expanded?: boolean
|
}>()
|
||||||
menuHovered?: boolean
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
menuHovered: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:expanded', value: boolean): void
|
(e: 'update:expanded', value: boolean): void
|
||||||
@@ -110,14 +80,6 @@ const assetsStore = useAssetsStore()
|
|||||||
const assetSelectionStore = useAssetSelectionStore()
|
const assetSelectionStore = useAssetSelectionStore()
|
||||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||||
|
|
||||||
const {
|
|
||||||
totalPercentFormatted,
|
|
||||||
currentNodePercentFormatted,
|
|
||||||
totalProgressStyle,
|
|
||||||
currentNodeProgressStyle
|
|
||||||
} = useQueueProgress()
|
|
||||||
const isHovered = ref(false)
|
|
||||||
const isOverlayHovered = computed(() => isHovered.value || props.menuHovered)
|
|
||||||
const internalExpanded = ref(false)
|
const internalExpanded = ref(false)
|
||||||
const isExpanded = computed({
|
const isExpanded = computed({
|
||||||
get: () =>
|
get: () =>
|
||||||
@@ -141,16 +103,12 @@ const activeJobsCount = computed(() => runningCount.value + queuedCount.value)
|
|||||||
|
|
||||||
const overlayState = computed<OverlayState>(() => {
|
const overlayState = computed<OverlayState>(() => {
|
||||||
if (isExpanded.value) return 'expanded'
|
if (isExpanded.value) return 'expanded'
|
||||||
if (hasActiveJob.value) return 'active'
|
|
||||||
if (hasCompletionSummary.value) return 'empty'
|
if (hasCompletionSummary.value) return 'empty'
|
||||||
return 'hidden'
|
return 'hidden'
|
||||||
})
|
})
|
||||||
|
|
||||||
const showBackground = computed(
|
const showBackground = computed(
|
||||||
() =>
|
() => overlayState.value === 'expanded' || overlayState.value === 'empty'
|
||||||
overlayState.value === 'expanded' ||
|
|
||||||
overlayState.value === 'empty' ||
|
|
||||||
(overlayState.value === 'active' && isOverlayHovered.value)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const isVisible = computed(() => overlayState.value !== 'hidden')
|
const isVisible = computed(() => overlayState.value !== 'hidden')
|
||||||
@@ -161,14 +119,6 @@ const containerClass = computed(() =>
|
|||||||
: 'border-transparent bg-transparent shadow-none'
|
: 'border-transparent bg-transparent shadow-none'
|
||||||
)
|
)
|
||||||
|
|
||||||
const bottomRowClass = computed(
|
|
||||||
() =>
|
|
||||||
`flex items-center justify-end gap-4 transition-opacity duration-200 ease-in-out ${
|
|
||||||
overlayState.value === 'active' && isOverlayHovered.value
|
|
||||||
? 'opacity-100 pointer-events-auto'
|
|
||||||
: 'opacity-0 pointer-events-none'
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
const headerTitle = computed(() =>
|
const headerTitle = computed(() =>
|
||||||
hasActiveJob.value
|
hasActiveJob.value
|
||||||
? `${activeJobsCount.value} ${t('sideToolbar.queueProgressOverlay.activeJobsSuffix')}`
|
? `${activeJobsCount.value} ${t('sideToolbar.queueProgressOverlay.activeJobsSuffix')}`
|
||||||
@@ -182,15 +132,7 @@ const showConcurrentIndicator = computed(
|
|||||||
() => concurrentWorkflowCount.value > 1
|
() => concurrentWorkflowCount.value > 1
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const { filteredTasks, groupedJobItems } = useJobList()
|
||||||
selectedJobTab,
|
|
||||||
selectedWorkflowFilter,
|
|
||||||
selectedSortMode,
|
|
||||||
hasFailedJobs,
|
|
||||||
filteredTasks,
|
|
||||||
groupedJobItems,
|
|
||||||
currentNodeName
|
|
||||||
} = useJobList()
|
|
||||||
|
|
||||||
const displayedJobGroups = computed(() => groupedJobItems.value)
|
const displayedJobGroups = computed(() => groupedJobItems.value)
|
||||||
|
|
||||||
@@ -219,10 +161,6 @@ const openExpandedFromEmpty = () => {
|
|||||||
setExpanded(true)
|
setExpanded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewAllJobs = () => {
|
|
||||||
setExpanded(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSummaryClick = () => {
|
const onSummaryClick = () => {
|
||||||
openExpandedFromEmpty()
|
openExpandedFromEmpty()
|
||||||
clearSummary()
|
clearSummary()
|
||||||
@@ -262,25 +200,6 @@ const cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => {
|
|||||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||||
})
|
})
|
||||||
|
|
||||||
const interruptAll = wrapWithErrorHandlingAsync(async () => {
|
|
||||||
const tasks = queueStore.runningTasks
|
|
||||||
const promptIds = tasks
|
|
||||||
.map((task) => task.promptId)
|
|
||||||
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
||||||
|
|
||||||
if (!promptIds.length) return
|
|
||||||
|
|
||||||
// Cloud backend supports cancelling specific jobs via /queue delete,
|
|
||||||
// while /interrupt always targets the "first" job. Use the targeted API
|
|
||||||
// on cloud to ensure we cancel the workflow the user clicked.
|
|
||||||
if (isCloud) {
|
|
||||||
await Promise.all(promptIds.map((id) => api.deleteItem('queue', id)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promptIds.map((id) => api.interrupt(id)))
|
|
||||||
})
|
|
||||||
|
|
||||||
const showClearHistoryDialog = () => {
|
const showClearHistoryDialog = () => {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'queue-clear-history',
|
key: 'queue-clear-history',
|
||||||
|
|||||||
@@ -1,231 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center justify-between gap-2 px-3">
|
|
||||||
<div class="min-w-0 flex-1 overflow-x-auto">
|
|
||||||
<div class="inline-flex items-center gap-1 whitespace-nowrap">
|
|
||||||
<TextButton
|
|
||||||
v-for="tab in visibleJobTabs"
|
|
||||||
:key="tab"
|
|
||||||
class="h-6 px-3 py-1 text-[12px] leading-none hover:opacity-90"
|
|
||||||
:type="selectedJobTab === tab ? 'secondary' : 'transparent'"
|
|
||||||
:class="[
|
|
||||||
selectedJobTab === tab ? 'text-text-primary' : 'text-text-secondary'
|
|
||||||
]"
|
|
||||||
:label="tabLabel(tab)"
|
|
||||||
@click="$emit('update:selectedJobTab', tab)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 flex shrink-0 items-center gap-2">
|
|
||||||
<IconButton
|
|
||||||
v-if="showWorkflowFilter"
|
|
||||||
v-tooltip.top="filterTooltipConfig"
|
|
||||||
type="secondary"
|
|
||||||
size="sm"
|
|
||||||
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
|
|
||||||
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
|
|
||||||
@click="onFilterClick"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon-[lucide--list-filter] block size-4 leading-none text-text-primary"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="selectedWorkflowFilter !== 'all'"
|
|
||||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<Popover
|
|
||||||
v-if="showWorkflowFilter"
|
|
||||||
ref="filterPopoverRef"
|
|
||||||
:dismissable="true"
|
|
||||||
:close-on-escape="true"
|
|
||||||
unstyled
|
|
||||||
:pt="{
|
|
||||||
root: { class: 'absolute z-50' },
|
|
||||||
content: {
|
|
||||||
class: [
|
|
||||||
'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg font-inter'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
|
||||||
>
|
|
||||||
<IconTextButton
|
|
||||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
|
||||||
type="transparent"
|
|
||||||
icon-position="right"
|
|
||||||
:label="t('sideToolbar.queueProgressOverlay.filterAllWorkflows')"
|
|
||||||
:aria-label="
|
|
||||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
|
||||||
"
|
|
||||||
@click="selectWorkflowFilter('all')"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<i
|
|
||||||
v-if="selectedWorkflowFilter === 'all'"
|
|
||||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</IconTextButton>
|
|
||||||
<div class="mx-2 mt-1 h-px" />
|
|
||||||
<IconTextButton
|
|
||||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
|
||||||
type="transparent"
|
|
||||||
icon-position="right"
|
|
||||||
:label="t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')"
|
|
||||||
:aria-label="
|
|
||||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
|
||||||
"
|
|
||||||
@click="selectWorkflowFilter('current')"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<i
|
|
||||||
v-if="selectedWorkflowFilter === 'current'"
|
|
||||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</IconTextButton>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<IconButton
|
|
||||||
v-tooltip.top="sortTooltipConfig"
|
|
||||||
type="secondary"
|
|
||||||
size="sm"
|
|
||||||
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
|
|
||||||
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
|
|
||||||
@click="onSortClick"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="icon-[lucide--arrow-up-down] block size-4 leading-none text-text-primary"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="selectedSortMode !== 'mostRecent'"
|
|
||||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<Popover
|
|
||||||
ref="sortPopoverRef"
|
|
||||||
:dismissable="true"
|
|
||||||
:close-on-escape="true"
|
|
||||||
unstyled
|
|
||||||
:pt="{
|
|
||||||
root: { class: 'absolute z-50' },
|
|
||||||
content: {
|
|
||||||
class: [
|
|
||||||
'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg font-inter'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
|
||||||
>
|
|
||||||
<template v-for="(mode, index) in jobSortModes" :key="mode">
|
|
||||||
<IconTextButton
|
|
||||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
|
||||||
type="transparent"
|
|
||||||
icon-position="right"
|
|
||||||
:label="sortLabel(mode)"
|
|
||||||
:aria-label="sortLabel(mode)"
|
|
||||||
@click="selectSortMode(mode)"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<i
|
|
||||||
v-if="selectedSortMode === mode"
|
|
||||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</IconTextButton>
|
|
||||||
<div
|
|
||||||
v-if="index < jobSortModes.length - 1"
|
|
||||||
class="mx-2 mt-1 h-px"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Popover from 'primevue/popover'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import IconButton from '@/components/button/IconButton.vue'
|
|
||||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
|
||||||
import TextButton from '@/components/button/TextButton.vue'
|
|
||||||
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'
|
|
||||||
import type { JobSortMode, JobTab } from '@/composables/queue/useJobList'
|
|
||||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
selectedJobTab: JobTab
|
|
||||||
selectedWorkflowFilter: 'all' | 'current'
|
|
||||||
selectedSortMode: JobSortMode
|
|
||||||
hasFailedJobs: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:selectedJobTab', value: JobTab): void
|
|
||||||
(e: 'update:selectedWorkflowFilter', value: 'all' | 'current'): void
|
|
||||||
(e: 'update:selectedSortMode', value: JobSortMode): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const filterPopoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
|
||||||
const sortPopoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
|
||||||
|
|
||||||
const filterTooltipConfig = computed(() =>
|
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.filterBy'))
|
|
||||||
)
|
|
||||||
const sortTooltipConfig = computed(() =>
|
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.sortBy'))
|
|
||||||
)
|
|
||||||
|
|
||||||
// This can be removed when cloud implements /jobs and we switch to it.
|
|
||||||
const showWorkflowFilter = !isCloud
|
|
||||||
|
|
||||||
const visibleJobTabs = computed(() =>
|
|
||||||
props.hasFailedJobs ? jobTabs : jobTabs.filter((tab) => tab !== 'Failed')
|
|
||||||
)
|
|
||||||
|
|
||||||
const onFilterClick = (event: Event) => {
|
|
||||||
if (filterPopoverRef.value) {
|
|
||||||
filterPopoverRef.value.toggle(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const selectWorkflowFilter = (value: 'all' | 'current') => {
|
|
||||||
;(filterPopoverRef.value as any)?.hide?.()
|
|
||||||
emit('update:selectedWorkflowFilter', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSortClick = (event: Event) => {
|
|
||||||
if (sortPopoverRef.value) {
|
|
||||||
sortPopoverRef.value.toggle(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectSortMode = (value: JobSortMode) => {
|
|
||||||
;(sortPopoverRef.value as any)?.hide?.()
|
|
||||||
emit('update:selectedSortMode', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabLabel = (tab: JobTab) => {
|
|
||||||
if (tab === 'All') return t('g.all')
|
|
||||||
if (tab === 'Completed') return t('g.completed')
|
|
||||||
return t('g.failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortLabel = (mode: JobSortMode) => {
|
|
||||||
if (mode === 'mostRecent') {
|
|
||||||
return t('queue.jobList.sortMostRecent')
|
|
||||||
}
|
|
||||||
if (mode === 'totalGenerationTime') {
|
|
||||||
return t('queue.jobList.sortTotalGenerationTime')
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useQueueStore } from '@/stores/queueStore'
|
import { useQueueStore } from '@/stores/queueStore'
|
||||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||||
@@ -20,13 +19,6 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
|||||||
import { buildJobDisplay } from '@/utils/queueDisplay'
|
import { buildJobDisplay } from '@/utils/queueDisplay'
|
||||||
import { jobStateFromTask } from '@/utils/queueUtil'
|
import { jobStateFromTask } from '@/utils/queueUtil'
|
||||||
|
|
||||||
/** Tabs for job list filtering */
|
|
||||||
export const jobTabs = ['All', 'Completed', 'Failed'] as const
|
|
||||||
export type JobTab = (typeof jobTabs)[number]
|
|
||||||
|
|
||||||
export const jobSortModes = ['mostRecent', 'totalGenerationTime'] as const
|
|
||||||
export type JobSortMode = (typeof jobSortModes)[number]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI item in the job list. Mirrors data previously prepared inline.
|
* UI item in the job list. Mirrors data previously prepared inline.
|
||||||
*/
|
*/
|
||||||
@@ -88,14 +80,11 @@ type TaskWithState = {
|
|||||||
state: JobState
|
state: JobState
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Builds the reactive job list and grouped view for the queue overlay. */
|
||||||
* Builds the reactive job list, filters, and grouped view for the queue overlay.
|
|
||||||
*/
|
|
||||||
export function useJobList() {
|
export function useJobList() {
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
|
|
||||||
const seenPendingIds = ref<Set<string>>(new Set())
|
const seenPendingIds = ref<Set<string>>(new Set())
|
||||||
const recentlyAddedPendingIds = ref<Set<string>>(new Set())
|
const recentlyAddedPendingIds = ref<Set<string>>(new Set())
|
||||||
@@ -193,10 +182,6 @@ export function useJobList() {
|
|||||||
return st(key, nodeType)
|
return st(key, nodeType)
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedJobTab = ref<JobTab>('All')
|
|
||||||
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
|
|
||||||
const selectedSortMode = ref<JobSortMode>('mostRecent')
|
|
||||||
|
|
||||||
const allTasksSorted = computed<TaskItemImpl[]>(() => {
|
const allTasksSorted = computed<TaskItemImpl[]>(() => {
|
||||||
const all = [
|
const all = [
|
||||||
...queueStore.pendingTasks,
|
...queueStore.pendingTasks,
|
||||||
@@ -213,38 +198,10 @@ export function useJobList() {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasFailedJobs = computed(() =>
|
const filteredTaskEntries = computed<TaskWithState[]>(
|
||||||
tasksWithJobState.value.some(({ state }) => state === 'failed')
|
() => tasksWithJobState.value
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => hasFailedJobs.value,
|
|
||||||
(hasFailed) => {
|
|
||||||
if (!hasFailed && selectedJobTab.value === 'Failed') {
|
|
||||||
selectedJobTab.value = 'All'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredTaskEntries = computed<TaskWithState[]>(() => {
|
|
||||||
let entries = tasksWithJobState.value
|
|
||||||
if (selectedJobTab.value === 'Completed') {
|
|
||||||
entries = entries.filter(({ state }) => state === 'completed')
|
|
||||||
} else if (selectedJobTab.value === 'Failed') {
|
|
||||||
entries = entries.filter(({ state }) => state === 'failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedWorkflowFilter.value === 'current') {
|
|
||||||
const activeId = workflowStore.activeWorkflow?.activeState?.id
|
|
||||||
if (!activeId) return []
|
|
||||||
entries = entries.filter(({ task }) => {
|
|
||||||
const wid = task.workflow?.id
|
|
||||||
return !!wid && wid === activeId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return entries
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredTasks = computed<TaskItemImpl[]>(() =>
|
const filteredTasks = computed<TaskItemImpl[]>(() =>
|
||||||
filteredTaskEntries.value.map(({ task }) => task)
|
filteredTaskEntries.value.map(({ task }) => task)
|
||||||
)
|
)
|
||||||
@@ -330,27 +287,10 @@ export function useJobList() {
|
|||||||
if (ji) groups[groupIdx].items.push(ji)
|
if (ji) groups[groupIdx].items.push(ji)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSortMode.value === 'totalGenerationTime') {
|
|
||||||
const valueOrDefault = (value: JobListItem['executionTimeMs']) =>
|
|
||||||
typeof value === 'number' && !Number.isNaN(value) ? value : -1
|
|
||||||
const sortByExecutionTimeDesc = (a: JobListItem, b: JobListItem) =>
|
|
||||||
valueOrDefault(b.executionTimeMs) - valueOrDefault(a.executionTimeMs)
|
|
||||||
|
|
||||||
groups.forEach((group) => {
|
|
||||||
group.items.sort(sortByExecutionTimeDesc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// filters/state
|
|
||||||
selectedJobTab,
|
|
||||||
selectedWorkflowFilter,
|
|
||||||
selectedSortMode,
|
|
||||||
hasFailedJobs,
|
|
||||||
// data sources
|
|
||||||
allTasksSorted,
|
allTasksSorted,
|
||||||
filteredTasks,
|
filteredTasks,
|
||||||
jobItems,
|
jobItems,
|
||||||
|
|||||||
@@ -158,23 +158,6 @@ vi.mock('@/stores/executionStore', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
let workflowStoreMock: {
|
|
||||||
activeWorkflow: null | { activeState?: { id?: string } }
|
|
||||||
}
|
|
||||||
const ensureWorkflowStore = () => {
|
|
||||||
if (!workflowStoreMock) {
|
|
||||||
workflowStoreMock = reactive({
|
|
||||||
activeWorkflow: null as null | { activeState?: { id?: string } }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return workflowStoreMock
|
|
||||||
}
|
|
||||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
|
||||||
useWorkflowStore: () => {
|
|
||||||
return ensureWorkflowStore()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const createTask = (
|
const createTask = (
|
||||||
overrides: Partial<TestTask> & { mockState?: JobState } = {}
|
overrides: Partial<TestTask> & { mockState?: JobState } = {}
|
||||||
): TestTask => ({
|
): TestTask => ({
|
||||||
@@ -210,9 +193,6 @@ const resetStores = () => {
|
|||||||
executionStore.activePromptId = null
|
executionStore.activePromptId = null
|
||||||
executionStore.executingNode = null
|
executionStore.executingNode = null
|
||||||
|
|
||||||
const workflowStore = ensureWorkflowStore()
|
|
||||||
workflowStore.activeWorkflow = null
|
|
||||||
|
|
||||||
ensureProgressRefs()
|
ensureProgressRefs()
|
||||||
totalPercent.value = 0
|
totalPercent.value = 0
|
||||||
currentNodePercent.value = 0
|
currentNodePercent.value = 0
|
||||||
@@ -353,65 +333,6 @@ describe('useJobList', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('filters by job tab and resets failed tab when failures disappear', async () => {
|
|
||||||
queueStoreMock.historyTasks = [
|
|
||||||
createTask({ promptId: 'c', queueIndex: 3, mockState: 'completed' }),
|
|
||||||
createTask({ promptId: 'f', queueIndex: 2, mockState: 'failed' }),
|
|
||||||
createTask({ promptId: 'p', queueIndex: 1, mockState: 'pending' })
|
|
||||||
]
|
|
||||||
|
|
||||||
const instance = initComposable()
|
|
||||||
await flush()
|
|
||||||
|
|
||||||
instance.selectedJobTab.value = 'Completed'
|
|
||||||
await flush()
|
|
||||||
expect(instance.filteredTasks.value.map((t) => t.promptId)).toEqual(['c'])
|
|
||||||
|
|
||||||
instance.selectedJobTab.value = 'Failed'
|
|
||||||
await flush()
|
|
||||||
expect(instance.filteredTasks.value.map((t) => t.promptId)).toEqual(['f'])
|
|
||||||
expect(instance.hasFailedJobs.value).toBe(true)
|
|
||||||
|
|
||||||
queueStoreMock.historyTasks = [
|
|
||||||
createTask({ promptId: 'c', queueIndex: 3, mockState: 'completed' })
|
|
||||||
]
|
|
||||||
await flush()
|
|
||||||
|
|
||||||
expect(instance.hasFailedJobs.value).toBe(false)
|
|
||||||
expect(instance.selectedJobTab.value).toBe('All')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('filters by active workflow when requested', async () => {
|
|
||||||
queueStoreMock.pendingTasks = [
|
|
||||||
createTask({
|
|
||||||
promptId: 'wf-1',
|
|
||||||
queueIndex: 2,
|
|
||||||
mockState: 'pending',
|
|
||||||
workflow: { id: 'workflow-1' }
|
|
||||||
}),
|
|
||||||
createTask({
|
|
||||||
promptId: 'wf-2',
|
|
||||||
queueIndex: 1,
|
|
||||||
mockState: 'pending',
|
|
||||||
workflow: { id: 'workflow-2' }
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
const instance = initComposable()
|
|
||||||
await flush()
|
|
||||||
|
|
||||||
instance.selectedWorkflowFilter.value = 'current'
|
|
||||||
await flush()
|
|
||||||
expect(instance.filteredTasks.value).toEqual([])
|
|
||||||
|
|
||||||
workflowStoreMock.activeWorkflow = { activeState: { id: 'workflow-1' } }
|
|
||||||
await flush()
|
|
||||||
|
|
||||||
expect(instance.filteredTasks.value.map((t) => t.promptId)).toEqual([
|
|
||||||
'wf-1'
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hydrates job items with active progress and compute hours', async () => {
|
it('hydrates job items with active progress and compute hours', async () => {
|
||||||
queueStoreMock.runningTasks = [
|
queueStoreMock.runningTasks = [
|
||||||
createTask({
|
createTask({
|
||||||
@@ -501,7 +422,6 @@ describe('useJobList', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const instance = initComposable()
|
const instance = initComposable()
|
||||||
instance.selectedSortMode.value = 'totalGenerationTime'
|
|
||||||
await flush()
|
await flush()
|
||||||
|
|
||||||
const groups = instance.groupedJobItems.value
|
const groups = instance.groupedJobItems.value
|
||||||
@@ -513,8 +433,8 @@ describe('useJobList', () => {
|
|||||||
|
|
||||||
const todayGroup = groups[0]
|
const todayGroup = groups[0]
|
||||||
expect(todayGroup.items.map((item) => item.id)).toEqual([
|
expect(todayGroup.items.map((item) => item.id)).toEqual([
|
||||||
'today-large',
|
'today-small',
|
||||||
'today-small'
|
'today-large'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user