Add inline progress text and progress bar

This commit is contained in:
Benjamin Lu
2026-01-23 01:46:22 -08:00
parent 76095542f4
commit c63e27f4bb
6 changed files with 245 additions and 85 deletions

View File

@@ -60,6 +60,7 @@ function createWrapper(pinia = createTestingPinia({ createSpy: vi.fn })) {
stubs: { stubs: {
SubgraphBreadcrumb: true, SubgraphBreadcrumb: true,
QueueProgressOverlay: true, QueueProgressOverlay: true,
QueueInlineProgressSummary: true,
CurrentUserButton: true, CurrentUserButton: true,
LoginButton: true, LoginButton: true,
ContextMenu: { ContextMenu: {

View File

@@ -1,10 +1,11 @@
<template> <template>
<div <div
v-if="!workspaceStore.focusMode" v-if="!workspaceStore.focusMode"
class="ml-1 flex gap-x-0.5 pt-1" class="ml-1 flex flex-col gap-1 pt-1"
@mouseenter="isTopMenuHovered = true" @mouseenter="isTopMenuHovered = true"
@mouseleave="isTopMenuHovered = false" @mouseleave="isTopMenuHovered = false"
> >
<div class="flex gap-x-0.5">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<SubgraphBreadcrumb /> <SubgraphBreadcrumb />
</div> </div>
@@ -32,7 +33,8 @@
</div> </div>
<div <div
class="actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface" ref="actionbarContainerRef"
class="actionbar-container relative pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
> >
<ActionBarButtons /> <ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present --> <!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
@@ -40,7 +42,10 @@
ref="legacyCommandsContainerRef" ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden" class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div> ></div>
<ComfyActionbar /> <ComfyActionbar
:top-menu-container="actionbarContainerRef"
:queue-overlay-expanded="isQueueOverlayExpanded"
/>
<Button <Button
v-tooltip.bottom="queueHistoryTooltipConfig" v-tooltip.bottom="queueHistoryTooltipConfig"
type="destructive" type="destructive"
@@ -68,7 +73,10 @@
}} }}
</span> </span>
</Button> </Button>
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" /> <ContextMenu
ref="queueContextMenu"
:model="queueContextMenuItems"
/>
<CurrentUserButton <CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar" v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0" class="shrink-0"
@@ -93,9 +101,21 @@
/> />
</div> </div>
</div> </div>
<div>
<QueueInlineProgressSummary
v-if="
isInlineProgressVisible && isActionbarEnabled && !isActionbarFloating
"
class="pr-1"
:hidden="isQueueOverlayExpanded"
/>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import ContextMenu from 'primevue/contextmenu' import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem' import type { MenuItem } from 'primevue/menuitem'
@@ -104,6 +124,7 @@ import { useI18n } from 'vue-i18n'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue' import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue' import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue' import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue' import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue' import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
@@ -147,6 +168,15 @@ const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
const { shouldShowRedDot: shouldShowConflictRedDot } = const { shouldShowRedDot: shouldShowConflictRedDot } =
useConflictAcknowledgment() useConflictAcknowledgment()
const isTopMenuHovered = ref(false) const isTopMenuHovered = ref(false)
const actionbarContainerRef = ref<HTMLElement>()
const isActionbarDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
const actionbarPosition = computed(() => settingStore.get('Comfy.UseNewMenu'))
const isActionbarEnabled = computed(
() => actionbarPosition.value !== 'Disabled'
)
const isActionbarFloating = computed(
() => isActionbarEnabled.value && !isActionbarDocked.value
)
const activeJobsLabel = computed(() => { const activeJobsLabel = computed(() => {
const count = activeJobsCount.value const count = activeJobsCount.value
return t( return t(
@@ -164,6 +194,7 @@ const isQueuePanelV2Enabled = computed(() =>
const isQueueProgressOverlayVisible = computed( const isQueueProgressOverlayVisible = computed(
() => !isQueuePanelV2Enabled.value () => !isQueuePanelV2Enabled.value
) )
const isInlineProgressVisible = computed(() => isQueuePanelV2Enabled.value)
const queueHistoryTooltipConfig = computed(() => const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory')) buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
) )

View File

@@ -18,7 +18,7 @@
content: { class: isDocked ? 'p-0' : 'p-1' } content: { class: isDocked ? 'p-0' : 'p-1' }
}" }"
> >
<div ref="panelRef" class="flex items-center select-none gap-2"> <div ref="panelRef" class="relative flex items-center select-none gap-2">
<span <span
ref="dragHandleRef" ref="dragHandleRef"
:class=" :class="
@@ -43,6 +43,13 @@
</Button> </Button>
</div> </div>
</Panel> </Panel>
<Teleport v-if="inlineProgressTarget" :to="inlineProgressTarget">
<QueueInlineProgress
:hidden="queueOverlayExpanded"
data-testid="queue-inline-progress"
/>
</Teleport>
</div> </div>
</template> </template>
@@ -59,6 +66,7 @@ import Panel from 'primevue/panel'
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig' import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
@@ -69,6 +77,11 @@ import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton' import ComfyRunButton from './ComfyRunButton'
const { topMenuContainer, queueOverlayExpanded = false } = defineProps<{
topMenuContainer?: HTMLElement | null
queueOverlayExpanded?: boolean
}>()
const settingsStore = useSettingStore() const settingsStore = useSettingStore()
const commandStore = useCommandStore() const commandStore = useCommandStore()
const { t } = useI18n() const { t } = useI18n()
@@ -76,6 +89,9 @@ const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
const position = computed(() => settingsStore.get('Comfy.UseNewMenu')) const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
const visible = computed(() => position.value !== 'Disabled') const visible = computed(() => position.value !== 'Disabled')
const isQueuePanelV2Enabled = computed(() =>
settingsStore.get('Comfy.Queue.QPOV2')
)
const panelRef = ref<HTMLElement | null>(null) const panelRef = ref<HTMLElement | null>(null)
const dragHandleRef = ref<HTMLElement | null>(null) const dragHandleRef = ref<HTMLElement | null>(null)
@@ -252,6 +268,12 @@ const onMouseLeaveDropZone = () => {
} }
} }
const inlineProgressTarget = computed(() => {
if (!visible.value || !isQueuePanelV2Enabled.value) return null
if (isDocked.value) return topMenuContainer ?? null
return panelRef.value
})
// Handle drag state changes // Handle drag state changes
watch(isDragging, (dragging) => { watch(isDragging, (dragging) => {
if (dragging) { if (dragging) {

View File

@@ -0,0 +1,33 @@
<template>
<div
v-if="shouldShow"
aria-hidden="true"
class="pointer-events-none absolute inset-0 overflow-hidden rounded-[7px]"
>
<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'
const props = defineProps<{
hidden?: boolean
}>()
const { totalPercent, currentNodePercent } = useQueueProgress()
const shouldShow = computed(
() =>
!props.hidden && (totalPercent.value > 0 || currentNodePercent.value > 0)
)
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div v-if="shouldShow" class="flex justify-end">
<div
class="flex items-center gap-4 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 gap-1 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 gap-1 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 { normalizeI18nKey } from '@/utils/formatUtil'
const props = defineProps<{
hidden?: boolean
}>()
const { t } = useI18n()
const executionStore = useExecutionStore()
const {
totalPercent,
totalPercentFormatted,
currentNodePercent,
currentNodePercentFormatted
} = useQueueProgress()
const currentNodeName = computed(() => {
const node = executionStore.executingNode
if (!node) return t('g.emDash')
const title = (node.title ?? '').toString().trim()
if (title) return title
const nodeType = (node.type ?? '').toString().trim() || t('g.untitled')
const key = `nodeDefs.${normalizeI18nKey(nodeType)}.display_name`
return st(key, nodeType)
})
const shouldShow = computed(
() =>
!props.hidden &&
(!executionStore.isIdle ||
totalPercent.value > 0 ||
currentNodePercent.value > 0)
)
</script>

View File

@@ -742,6 +742,7 @@
"title": "Queue Progress", "title": "Queue Progress",
"total": "Total: {percent}", "total": "Total: {percent}",
"colonPercent": ": {percent}", "colonPercent": ": {percent}",
"inlineTotalLabel": "Total",
"currentNode": "Current node:", "currentNode": "Current node:",
"viewAllJobs": "View all jobs", "viewAllJobs": "View all jobs",
"viewList": "List view", "viewList": "List view",