mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-22 15:54:09 +00:00
Add inline queue progress bar and text summary (#8271)
Add inline queue progress bar and summary per the new designs. This adds an inline 3px progress bar in the actionbar container (docked or floating) and a compact summary line below the top menu that follows when floating, both gated by the QPO V2 flag and hidden while the overlay is expanded. https://github.com/user-attachments/assets/da8ec7b7-35f4-4d52-a83b-15c21b484eba ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8271-Add-inline-queue-progress-bar-and-summary-for-QPO-V2-2f16d73d36508132a6dff247f71e11e4) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
75
src/components/queue/QueueInlineProgress.test.ts
Normal file
75
src/components/queue/QueueInlineProgress.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
|
||||
const mockProgress = vi.hoisted(() => ({
|
||||
totalPercent: null as unknown as Ref<number>,
|
||||
currentNodePercent: null as unknown as Ref<number>
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useQueueProgress', () => ({
|
||||
useQueueProgress: () => ({
|
||||
totalPercent: mockProgress.totalPercent,
|
||||
currentNodePercent: mockProgress.currentNodePercent
|
||||
})
|
||||
}))
|
||||
|
||||
const createWrapper = (props: { hidden?: boolean } = {}) =>
|
||||
mount(QueueInlineProgress, { props })
|
||||
|
||||
describe('QueueInlineProgress', () => {
|
||||
beforeEach(() => {
|
||||
mockProgress.totalPercent = ref(0)
|
||||
mockProgress.currentNodePercent = ref(0)
|
||||
})
|
||||
|
||||
it('renders when total progress is non-zero', () => {
|
||||
mockProgress.totalPercent.value = 12
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders when current node progress is non-zero', () => {
|
||||
mockProgress.currentNodePercent.value = 33
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not render when hidden', () => {
|
||||
mockProgress.totalPercent.value = 45
|
||||
|
||||
const wrapper = createWrapper({ hidden: true })
|
||||
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows when progress becomes non-zero', async () => {
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(false)
|
||||
|
||||
mockProgress.totalPercent.value = 10
|
||||
await nextTick()
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('hides when progress returns to zero', async () => {
|
||||
mockProgress.totalPercent.value = 10
|
||||
|
||||
const wrapper = createWrapper()
|
||||
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true)
|
||||
|
||||
mockProgress.totalPercent.value = 0
|
||||
mockProgress.currentNodePercent.value = 0
|
||||
await nextTick()
|
||||
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
36
src/components/queue/QueueInlineProgress.vue
Normal file
36
src/components/queue/QueueInlineProgress.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="shouldShow"
|
||||
aria-hidden="true"
|
||||
:class="
|
||||
cn('pointer-events-none absolute inset-0 overflow-hidden', radiusClass)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-0 left-0 h-[3px] bg-interface-panel-job-progress-primary transition-[width]"
|
||||
:style="{ width: `${totalPercent}%` }"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-0 left-0 h-[3px] bg-interface-panel-job-progress-secondary transition-[width]"
|
||||
:style="{ width: `${currentNodePercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { hidden = false, radiusClass = 'rounded-[7px]' } = defineProps<{
|
||||
hidden?: boolean
|
||||
radiusClass?: string
|
||||
}>()
|
||||
|
||||
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||
|
||||
const shouldShow = computed(
|
||||
() => !hidden && (totalPercent.value > 0 || currentNodePercent.value > 0)
|
||||
)
|
||||
</script>
|
||||
70
src/components/queue/QueueInlineProgressSummary.vue
Normal file
70
src/components/queue/QueueInlineProgressSummary.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div v-if="shouldShow" class="flex justify-end">
|
||||
<div
|
||||
class="flex items-center whitespace-nowrap text-[0.75rem] leading-[normal] drop-shadow-[1px_1px_8px_rgba(0,0,0,0.4)]"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<div class="flex items-center text-base-foreground">
|
||||
<span class="font-normal">
|
||||
{{ t('sideToolbar.queueProgressOverlay.inlineTotalLabel') }}:
|
||||
</span>
|
||||
<span class="w-[5ch] shrink-0 text-right font-bold tabular-nums">
|
||||
{{ totalPercentFormatted }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span
|
||||
class="w-[16ch] shrink-0 truncate text-right"
|
||||
:title="currentNodeName"
|
||||
>
|
||||
{{ currentNodeName }}:
|
||||
</span>
|
||||
<span class="w-[5ch] shrink-0 text-right tabular-nums">
|
||||
{{ currentNodePercentFormatted }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
hidden?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const executionStore = useExecutionStore()
|
||||
const {
|
||||
totalPercent,
|
||||
totalPercentFormatted,
|
||||
currentNodePercent,
|
||||
currentNodePercentFormatted
|
||||
} = useQueueProgress()
|
||||
|
||||
const currentNodeName = computed(() => {
|
||||
return resolveNodeDisplayName(executionStore.executingNode, {
|
||||
emptyLabel: t('g.emDash'),
|
||||
untitledLabel: t('g.untitled'),
|
||||
st
|
||||
})
|
||||
})
|
||||
|
||||
const shouldShow = computed(
|
||||
() =>
|
||||
!props.hidden &&
|
||||
(!executionStore.isIdle ||
|
||||
totalPercent.value > 0 ||
|
||||
currentNodePercent.value > 0)
|
||||
)
|
||||
</script>
|
||||
Reference in New Issue
Block a user