mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 21:22:07 +00:00
feat: add inline queue progress
This commit is contained in:
@@ -1,18 +1,12 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import type { MenuItem } from 'primevue/menuitem'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { computed, nextTick } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import TopMenuSection from '@/components/TopMenuSection.vue'
|
import TopMenuSection from '@/components/TopMenuSection.vue'
|
||||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||||
import type {
|
|
||||||
JobListItem,
|
|
||||||
JobStatus
|
|
||||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
|
||||||
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
|
||||||
import { isElectron } from '@/utils/envUtil'
|
import { isElectron } from '@/utils/envUtil'
|
||||||
|
|
||||||
const mockData = vi.hoisted(() => ({ isLoggedIn: false }))
|
const mockData = vi.hoisted(() => ({ isLoggedIn: false }))
|
||||||
@@ -43,8 +37,7 @@ function createWrapper() {
|
|||||||
queueProgressOverlay: {
|
queueProgressOverlay: {
|
||||||
viewJobHistory: 'View job history',
|
viewJobHistory: 'View job history',
|
||||||
expandCollapsedQueue: 'Expand collapsed queue',
|
expandCollapsedQueue: 'Expand collapsed queue',
|
||||||
activeJobsShort: '{count} active | {count} active',
|
activeJobsShort: '{count} active | {count} active'
|
||||||
clearQueueTooltip: 'Clear queue'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,14 +49,11 @@ function createWrapper() {
|
|||||||
plugins: [createTestingPinia({ createSpy: vi.fn }), i18n],
|
plugins: [createTestingPinia({ createSpy: vi.fn }), i18n],
|
||||||
stubs: {
|
stubs: {
|
||||||
SubgraphBreadcrumb: true,
|
SubgraphBreadcrumb: true,
|
||||||
|
ComfyActionbar: true,
|
||||||
|
QueueInlineProgressSummary: true,
|
||||||
QueueProgressOverlay: true,
|
QueueProgressOverlay: true,
|
||||||
CurrentUserButton: true,
|
CurrentUserButton: true,
|
||||||
LoginButton: true,
|
LoginButton: true
|
||||||
ContextMenu: {
|
|
||||||
name: 'ContextMenu',
|
|
||||||
props: ['model'],
|
|
||||||
template: '<div />'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
tooltip: () => {}
|
tooltip: () => {}
|
||||||
@@ -72,19 +62,6 @@ function createWrapper() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createJob(id: string, status: JobStatus): JobListItem {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
status,
|
|
||||||
create_time: 0,
|
|
||||||
priority: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTask(id: string, status: JobStatus): TaskItemImpl {
|
|
||||||
return new TaskItemImpl(createJob(id, status))
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('TopMenuSection', () => {
|
describe('TopMenuSection', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks()
|
vi.resetAllMocks()
|
||||||
@@ -126,39 +103,4 @@ describe('TopMenuSection', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows the active jobs label with the current count', async () => {
|
|
||||||
const wrapper = createWrapper()
|
|
||||||
const queueStore = useQueueStore()
|
|
||||||
queueStore.pendingTasks = [createTask('pending-1', 'pending')]
|
|
||||||
queueStore.runningTasks = [
|
|
||||||
createTask('running-1', 'in_progress'),
|
|
||||||
createTask('running-2', 'in_progress')
|
|
||||||
]
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]')
|
|
||||||
expect(queueButton.text()).toContain('3 active')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('disables the clear queue context menu item when no queued jobs exist', () => {
|
|
||||||
const wrapper = createWrapper()
|
|
||||||
const menu = wrapper.findComponent({ name: 'ContextMenu' })
|
|
||||||
const model = menu.props('model') as MenuItem[]
|
|
||||||
expect(model[0]?.label).toBe('Clear queue')
|
|
||||||
expect(model[0]?.disabled).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('enables the clear queue context menu item when queued jobs exist', async () => {
|
|
||||||
const wrapper = createWrapper()
|
|
||||||
const queueStore = useQueueStore()
|
|
||||||
queueStore.pendingTasks = [createTask('pending-1', 'pending')]
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
const menu = wrapper.findComponent({ name: 'ContextMenu' })
|
|
||||||
const model = menu.props('model') as MenuItem[]
|
|
||||||
expect(model[0]?.disabled).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,126 +1,94 @@
|
|||||||
<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="min-w-0 flex-1">
|
<div class="flex gap-x-0.5">
|
||||||
<SubgraphBreadcrumb />
|
<div class="min-w-0 flex-1">
|
||||||
|
<SubgraphBreadcrumb />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-1 flex flex-col items-end gap-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
v-if="managerState.shouldShowManagerButtons.value"
|
||||||
|
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-tooltip.bottom="customNodesManagerTooltipConfig"
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
:aria-label="t('menu.customNodesManager')"
|
||||||
|
class="relative"
|
||||||
|
@click="openCustomNodeManager"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--puzzle] size-4" />
|
||||||
|
<span
|
||||||
|
v-if="shouldShowRedDot"
|
||||||
|
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="actionbarContainerRef"
|
||||||
|
class="actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 overflow-hidden rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||||
|
>
|
||||||
|
<ActionBarButtons />
|
||||||
|
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||||
|
<div
|
||||||
|
ref="legacyCommandsContainerRef"
|
||||||
|
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||||
|
></div>
|
||||||
|
<ComfyActionbar
|
||||||
|
v-model:docked="isActionbarDocked"
|
||||||
|
v-model:queue-overlay-expanded="isQueueOverlayExpanded"
|
||||||
|
:top-menu-container="actionbarContainerRef"
|
||||||
|
/>
|
||||||
|
<CurrentUserButton
|
||||||
|
v-if="isLoggedIn && !isIntegratedTabBar"
|
||||||
|
class="shrink-0"
|
||||||
|
/>
|
||||||
|
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
|
||||||
|
<Button
|
||||||
|
v-if="!isRightSidePanelOpen"
|
||||||
|
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||||
|
type="secondary"
|
||||||
|
size="icon"
|
||||||
|
:aria-label="t('rightSidePanel.togglePanel')"
|
||||||
|
@click="rightSidePanelStore.togglePanel"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--panel-right] size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<QueueProgressOverlay
|
||||||
|
v-model:expanded="isQueueOverlayExpanded"
|
||||||
|
:menu-hovered="isTopMenuHovered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-1 flex flex-col items-end gap-1">
|
<div v-if="isActionbarEnabled && !isActionbarFloating">
|
||||||
<div class="flex items-center gap-2">
|
<QueueInlineProgressSummary
|
||||||
<div
|
class="pr-1"
|
||||||
v-if="managerState.shouldShowManagerButtons.value"
|
:hidden="isQueueOverlayExpanded"
|
||||||
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
v-tooltip.bottom="customNodesManagerTooltipConfig"
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
:aria-label="t('menu.customNodesManager')"
|
|
||||||
class="relative"
|
|
||||||
@click="openCustomNodeManager"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--puzzle] size-4" />
|
|
||||||
<span
|
|
||||||
v-if="shouldShowRedDot"
|
|
||||||
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</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"
|
|
||||||
>
|
|
||||||
<ActionBarButtons />
|
|
||||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
|
||||||
<div
|
|
||||||
ref="legacyCommandsContainerRef"
|
|
||||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
|
||||||
></div>
|
|
||||||
<ComfyActionbar />
|
|
||||||
<Button
|
|
||||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
|
||||||
type="destructive"
|
|
||||||
size="md"
|
|
||||||
:aria-pressed="isQueueOverlayExpanded"
|
|
||||||
class="px-3"
|
|
||||||
data-testid="queue-overlay-toggle"
|
|
||||||
@click="toggleQueueOverlay"
|
|
||||||
@contextmenu.stop.prevent="showQueueContextMenu"
|
|
||||||
>
|
|
||||||
<span class="text-sm font-normal tabular-nums">
|
|
||||||
{{ activeJobsLabel }}
|
|
||||||
</span>
|
|
||||||
<span class="sr-only">
|
|
||||||
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<ContextMenu
|
|
||||||
ref="queueContextMenu"
|
|
||||||
:model="queueContextMenuItems"
|
|
||||||
unstyled
|
|
||||||
:pt="{
|
|
||||||
root: {
|
|
||||||
class:
|
|
||||||
'rounded-lg border border-border-default bg-base-background p-2 shadow-[0px_2px_12px_0_rgba(0,0,0,0.1)] font-inter'
|
|
||||||
},
|
|
||||||
rootList: { class: 'm-0 flex list-none flex-col gap-1 p-0' },
|
|
||||||
item: { class: 'm-0 p-0' }
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #item="{ item, props }">
|
|
||||||
<a
|
|
||||||
v-bind="props.action"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'flex h-8 w-full items-center gap-2 rounded-sm px-2 text-sm font-normal',
|
|
||||||
item.class,
|
|
||||||
item.disabled && 'opacity-50'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<i v-if="item.icon" :class="cn(item.icon, 'size-4')" />
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</ContextMenu>
|
|
||||||
<CurrentUserButton
|
|
||||||
v-if="isLoggedIn && !isIntegratedTabBar"
|
|
||||||
class="shrink-0"
|
|
||||||
/>
|
|
||||||
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
|
|
||||||
<Button
|
|
||||||
v-if="!isRightSidePanelOpen"
|
|
||||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
|
||||||
type="secondary"
|
|
||||||
size="icon"
|
|
||||||
:aria-label="t('rightSidePanel.togglePanel')"
|
|
||||||
@click="rightSidePanelStore.togglePanel"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--panel-right] size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<QueueProgressOverlay
|
|
||||||
v-model:expanded="isQueueOverlayExpanded"
|
|
||||||
:menu-hovered="isTopMenuHovered"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 type { MenuItem } from 'primevue/menuitem'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
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'
|
||||||
@@ -132,13 +100,10 @@ import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
|||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useQueueUIStore } from '@/stores/queueStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
|
||||||
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
|
|
||||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { isElectron } from '@/utils/envUtil'
|
import { isElectron } from '@/utils/envUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
|
||||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||||
@@ -149,11 +114,8 @@ const rightSidePanelStore = useRightSidePanelStore()
|
|||||||
const managerState = useManagerState()
|
const managerState = useManagerState()
|
||||||
const { isLoggedIn } = useCurrentUser()
|
const { isLoggedIn } = useCurrentUser()
|
||||||
const isDesktop = isElectron()
|
const isDesktop = isElectron()
|
||||||
const { t, n } = useI18n()
|
const { t } = useI18n()
|
||||||
const { toastErrorHandler } = useErrorHandling()
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
const commandStore = useCommandStore()
|
|
||||||
const queueStore = useQueueStore()
|
|
||||||
const executionStore = useExecutionStore()
|
|
||||||
const queueUIStore = useQueueUIStore()
|
const queueUIStore = useQueueUIStore()
|
||||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||||
const releaseStore = useReleaseStore()
|
const releaseStore = useReleaseStore()
|
||||||
@@ -161,38 +123,21 @@ const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
|||||||
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
||||||
useConflictAcknowledgment()
|
useConflictAcknowledgment()
|
||||||
const isTopMenuHovered = ref(false)
|
const isTopMenuHovered = ref(false)
|
||||||
const activeJobsCount = computed(
|
const actionbarContainerRef = ref<HTMLElement>()
|
||||||
() => queueStore.pendingTasks.length + queueStore.runningTasks.length
|
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 count = activeJobsCount.value
|
|
||||||
return t(
|
|
||||||
'sideToolbar.queueProgressOverlay.activeJobsShort',
|
|
||||||
{ count: n(count) },
|
|
||||||
count
|
|
||||||
)
|
|
||||||
})
|
|
||||||
const isIntegratedTabBar = computed(
|
const isIntegratedTabBar = computed(
|
||||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||||
)
|
)
|
||||||
const queueHistoryTooltipConfig = computed(() =>
|
|
||||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
|
||||||
)
|
|
||||||
const customNodesManagerTooltipConfig = computed(() =>
|
const customNodesManagerTooltipConfig = computed(() =>
|
||||||
buildTooltipConfig(t('menu.customNodesManager'))
|
buildTooltipConfig(t('menu.customNodesManager'))
|
||||||
)
|
)
|
||||||
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
|
||||||
const queueContextMenuItems = computed<MenuItem[]>(() => [
|
|
||||||
{
|
|
||||||
label: t('sideToolbar.queueProgressOverlay.clearQueueTooltip'),
|
|
||||||
icon: 'icon-[lucide--list-x]',
|
|
||||||
class: 'text-destructive-background',
|
|
||||||
disabled: queueStore.pendingTasks.length === 0,
|
|
||||||
command: () => {
|
|
||||||
void handleClearQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// Use either release red dot or conflict red dot
|
// Use either release red dot or conflict red dot
|
||||||
const shouldShowRedDot = computed((): boolean => {
|
const shouldShowRedDot = computed((): boolean => {
|
||||||
@@ -215,23 +160,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleQueueOverlay = () => {
|
|
||||||
commandStore.execute('Comfy.Queue.ToggleOverlay')
|
|
||||||
}
|
|
||||||
|
|
||||||
const showQueueContextMenu = (event: MouseEvent) => {
|
|
||||||
queueContextMenu.value?.show(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClearQueue = async () => {
|
|
||||||
const pendingPromptIds = queueStore.pendingTasks
|
|
||||||
.map((task) => task.promptId)
|
|
||||||
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
||||||
|
|
||||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
|
||||||
executionStore.clearInitializationByPromptIds(pendingPromptIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openCustomNodeManager = async () => {
|
const openCustomNodeManager = async () => {
|
||||||
try {
|
try {
|
||||||
await managerState.openManager({
|
await managerState.openManager({
|
||||||
|
|||||||
129
src/components/actionbar/ComfyActionbar.test.ts
Normal file
129
src/components/actionbar/ComfyActionbar.test.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { nextTick, reactive } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||||
|
|
||||||
|
const queueStoreMock = reactive({
|
||||||
|
pendingTasks: [] as Array<{ promptId?: string }>,
|
||||||
|
runningTasks: [] as Array<{ promptId?: string }>
|
||||||
|
})
|
||||||
|
|
||||||
|
const executionStoreMock = reactive({
|
||||||
|
isIdle: true,
|
||||||
|
clearInitializationByPromptIds: vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/platform/settings/settingStore', () => ({
|
||||||
|
useSettingStore: () => ({
|
||||||
|
get: vi.fn((key: string) => (key === 'Comfy.UseNewMenu' ? 'Top' : null))
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/telemetry', () => ({
|
||||||
|
useTelemetry: () => ({
|
||||||
|
trackUiButtonClicked: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/commandStore', () => ({
|
||||||
|
useCommandStore: () => ({
|
||||||
|
execute: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/executionStore', () => ({
|
||||||
|
useExecutionStore: () => executionStoreMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/queueStore', () => ({
|
||||||
|
useQueueStore: () => queueStoreMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
sideToolbar: {
|
||||||
|
queueProgressOverlay: {
|
||||||
|
activeJobsShort: '{count} active | {count} active',
|
||||||
|
clearQueueTooltip: 'Clear queue',
|
||||||
|
viewJobHistory: 'View job history',
|
||||||
|
expandCollapsedQueue: 'Expand job queue'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
interrupt: 'Interrupt'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return mount(ComfyActionbar, {
|
||||||
|
props: {
|
||||||
|
docked: true,
|
||||||
|
queueOverlayExpanded: false
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
plugins: [i18n],
|
||||||
|
stubs: {
|
||||||
|
Panel: true,
|
||||||
|
ComfyRunButton: true,
|
||||||
|
QueueInlineProgress: true,
|
||||||
|
QueueInlineProgressSummary: true,
|
||||||
|
ContextMenu: {
|
||||||
|
name: 'ContextMenu',
|
||||||
|
props: ['model'],
|
||||||
|
template: '<div />'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ComfyActionbar', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
queueStoreMock.pendingTasks = []
|
||||||
|
queueStoreMock.runningTasks = []
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the active jobs label with the current count', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
queueStoreMock.pendingTasks = [{ promptId: 'pending-1' }]
|
||||||
|
queueStoreMock.runningTasks = [
|
||||||
|
{ promptId: 'running-1' },
|
||||||
|
{ promptId: 'running-2' }
|
||||||
|
]
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]')
|
||||||
|
expect(queueButton.text()).toContain('3 active')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables the clear queue context menu item when no queued jobs exist', () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const menu = wrapper.findComponent({ name: 'ContextMenu' })
|
||||||
|
const model = menu.props('model') as MenuItem[]
|
||||||
|
expect(model[0]?.label).toBe('Clear queue')
|
||||||
|
expect(model[0]?.disabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enables the clear queue context menu item when queued jobs exist', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
queueStoreMock.pendingTasks = [{ promptId: 'pending-1' }]
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const menu = wrapper.findComponent({ name: 'ContextMenu' })
|
||||||
|
const model = menu.props('model') as MenuItem[]
|
||||||
|
expect(model[0]?.disabled).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,40 +9,108 @@
|
|||||||
{{ t('actionbar.dockToTop') }}
|
{{ t('actionbar.dockToTop') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Panel
|
<div
|
||||||
class="pointer-events-auto"
|
ref="actionbarWrapperRef"
|
||||||
|
class="flex flex-col items-stretch"
|
||||||
:style="style"
|
:style="style"
|
||||||
:class="panelClass"
|
:class="panelClass"
|
||||||
:pt="{
|
|
||||||
header: { class: 'hidden' },
|
|
||||||
content: { class: isDocked ? 'p-0' : 'p-1' }
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div ref="panelRef" class="flex items-center select-none gap-2">
|
<Panel
|
||||||
<span
|
class="pointer-events-auto"
|
||||||
ref="dragHandleRef"
|
:class="panelRootClass"
|
||||||
:class="
|
:pt="{
|
||||||
cn(
|
header: { class: 'hidden' },
|
||||||
'drag-handle cursor-grab w-3 h-max',
|
content: { class: isDocked ? 'p-0' : 'p-1' }
|
||||||
isDragging && 'cursor-grabbing'
|
}"
|
||||||
)
|
>
|
||||||
"
|
<div
|
||||||
/>
|
ref="panelRef"
|
||||||
<Suspense @resolve="comfyRunButtonResolved">
|
class="relative flex items-center select-none gap-2 overflow-hidden"
|
||||||
<ComfyRunButton />
|
|
||||||
</Suspense>
|
|
||||||
<Button
|
|
||||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
|
||||||
variant="destructive"
|
|
||||||
size="icon"
|
|
||||||
:disabled="isExecutionIdle"
|
|
||||||
:aria-label="t('menu.interrupt')"
|
|
||||||
@click="cancelCurrentJob"
|
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--x] size-4" />
|
<span
|
||||||
</Button>
|
ref="dragHandleRef"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'drag-handle cursor-grab w-3 h-max',
|
||||||
|
isDragging && 'cursor-grabbing'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Suspense @resolve="comfyRunButtonResolved">
|
||||||
|
<ComfyRunButton />
|
||||||
|
</Suspense>
|
||||||
|
<Button
|
||||||
|
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
:disabled="isExecutionIdle"
|
||||||
|
:aria-label="t('menu.interrupt')"
|
||||||
|
@click="cancelCurrentJob"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--x] size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||||
|
variant="destructive"
|
||||||
|
size="md"
|
||||||
|
class="px-3"
|
||||||
|
data-testid="queue-overlay-toggle"
|
||||||
|
:aria-pressed="queueOverlayExpanded"
|
||||||
|
@click="toggleQueueOverlay"
|
||||||
|
@contextmenu.stop.prevent="showQueueContextMenu"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-normal tabular-nums">
|
||||||
|
{{ activeJobsLabel }}
|
||||||
|
</span>
|
||||||
|
<span class="sr-only">
|
||||||
|
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<ContextMenu
|
||||||
|
ref="queueContextMenu"
|
||||||
|
:model="queueContextMenuItems"
|
||||||
|
unstyled
|
||||||
|
:pt="{
|
||||||
|
root: {
|
||||||
|
class:
|
||||||
|
'rounded-lg border border-border-default bg-base-background p-2 shadow-[0px_2px_12px_0_rgba(0,0,0,0.1)] font-inter'
|
||||||
|
},
|
||||||
|
rootList: { class: 'm-0 flex list-none flex-col gap-1 p-0' },
|
||||||
|
item: { class: 'm-0 p-0' }
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #item="{ item, props }">
|
||||||
|
<a
|
||||||
|
v-bind="props.action"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex h-8 w-full items-center gap-2 rounded-sm px-2 text-sm font-normal',
|
||||||
|
item.class,
|
||||||
|
item.disabled && 'opacity-50'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i v-if="item.icon" :class="cn(item.icon, 'size-4')" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</ContextMenu>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
<div v-if="isFloating" class="flex justify-end pt-1 pr-1">
|
||||||
|
<QueueInlineProgressSummary
|
||||||
|
class="pr-1"
|
||||||
|
:hidden="queueOverlayExpanded"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</div>
|
||||||
|
|
||||||
|
<Teleport v-if="inlineProgressTarget" :to="inlineProgressTarget">
|
||||||
|
<QueueInlineProgress
|
||||||
|
:hidden="queueOverlayExpanded"
|
||||||
|
data-testid="queue-inline-progress"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -55,41 +123,121 @@ import {
|
|||||||
} from '@vueuse/core'
|
} from '@vueuse/core'
|
||||||
import { clamp } from 'es-toolkit/compat'
|
import { clamp } from 'es-toolkit/compat'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import ContextMenu from 'primevue/contextmenu'
|
||||||
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
import Panel from 'primevue/panel'
|
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 QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.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'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
|
import { useQueueStore } from '@/stores/queueStore'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import ComfyRunButton from './ComfyRunButton'
|
import ComfyRunButton from './ComfyRunButton'
|
||||||
|
|
||||||
|
const { topMenuContainer = null } = defineProps<{
|
||||||
|
topMenuContainer?: HTMLElement | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const queueOverlayExpanded = defineModel<boolean>('queueOverlayExpanded', {
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
const isDocked = defineModel<boolean>('docked', { default: true })
|
||||||
|
|
||||||
const settingsStore = useSettingStore()
|
const settingsStore = useSettingStore()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const { t } = useI18n()
|
const queueStore = useQueueStore()
|
||||||
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
|
const executionStore = useExecutionStore()
|
||||||
|
const { t, n } = useI18n()
|
||||||
|
const { isIdle: isExecutionIdle } = storeToRefs(executionStore)
|
||||||
|
|
||||||
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 actionbarWrapperRef = ref<HTMLElement | null>(null)
|
||||||
const panelRef = ref<HTMLElement | null>(null)
|
const panelRef = ref<HTMLElement | null>(null)
|
||||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
|
||||||
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0
|
y: 0
|
||||||
})
|
})
|
||||||
const { x, y, style, isDragging } = useDraggable(panelRef, {
|
const { x, y, style, isDragging } = useDraggable(actionbarWrapperRef, {
|
||||||
initialValue: { x: 0, y: 0 },
|
initialValue: { x: 0, y: 0 },
|
||||||
handle: dragHandleRef,
|
handle: dragHandleRef,
|
||||||
containerElement: document.body
|
containerElement: document.body
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const wrapperElement = computed(() => {
|
||||||
|
const element = actionbarWrapperRef.value
|
||||||
|
return element instanceof HTMLElement ? element : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const panelElement = computed(() => {
|
||||||
|
const element = panelRef.value
|
||||||
|
return element instanceof HTMLElement ? element : null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Queue and Execution logic
|
||||||
|
const activeJobsCount = computed(
|
||||||
|
() => queueStore.pendingTasks.length + queueStore.runningTasks.length
|
||||||
|
)
|
||||||
|
const activeJobsLabel = computed(() => {
|
||||||
|
const count = activeJobsCount.value
|
||||||
|
return t(
|
||||||
|
'sideToolbar.queueProgressOverlay.activeJobsShort',
|
||||||
|
{ count: n(count) },
|
||||||
|
count
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const queueHistoryTooltipConfig = computed(() =>
|
||||||
|
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||||
|
)
|
||||||
|
const cancelJobTooltipConfig = computed(() =>
|
||||||
|
buildTooltipConfig(t('menu.interrupt'))
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggleQueueOverlay() {
|
||||||
|
void commandStore.execute('Comfy.Queue.ToggleOverlay')
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||||
|
const queueContextMenuItems = computed<MenuItem[]>(() => [
|
||||||
|
{
|
||||||
|
label: t('sideToolbar.queueProgressOverlay.clearQueueTooltip'),
|
||||||
|
icon: 'icon-[lucide--list-x]',
|
||||||
|
class: 'text-destructive-background',
|
||||||
|
disabled: queueStore.pendingTasks.length === 0,
|
||||||
|
command: () => {
|
||||||
|
void handleClearQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
function showQueueContextMenu(event: MouseEvent) {
|
||||||
|
queueContextMenu.value?.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClearQueue() {
|
||||||
|
const pendingPromptIds = queueStore.pendingTasks
|
||||||
|
.map((task) => task.promptId)
|
||||||
|
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
||||||
|
|
||||||
|
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||||
|
executionStore.clearInitializationByPromptIds(pendingPromptIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelCurrentJob() {
|
||||||
|
if (isExecutionIdle.value) return
|
||||||
|
await commandStore.execute('Comfy.Interrupt')
|
||||||
|
}
|
||||||
|
|
||||||
// Update storedPosition when x or y changes
|
// Update storedPosition when x or y changes
|
||||||
watchDebounced(
|
watchDebounced(
|
||||||
[x, y],
|
[x, y],
|
||||||
@@ -101,11 +249,12 @@ watchDebounced(
|
|||||||
|
|
||||||
// Set initial position to bottom center
|
// Set initial position to bottom center
|
||||||
const setInitialPosition = () => {
|
const setInitialPosition = () => {
|
||||||
if (panelRef.value) {
|
const containerEl = wrapperElement.value
|
||||||
|
if (containerEl) {
|
||||||
const screenWidth = window.innerWidth
|
const screenWidth = window.innerWidth
|
||||||
const screenHeight = window.innerHeight
|
const screenHeight = window.innerHeight
|
||||||
const menuWidth = panelRef.value.offsetWidth
|
const menuWidth = containerEl.offsetWidth
|
||||||
const menuHeight = panelRef.value.offsetHeight
|
const menuHeight = containerEl.offsetHeight
|
||||||
|
|
||||||
if (menuWidth === 0 || menuHeight === 0) {
|
if (menuWidth === 0 || menuHeight === 0) {
|
||||||
return
|
return
|
||||||
@@ -181,11 +330,12 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const adjustMenuPosition = () => {
|
const adjustMenuPosition = () => {
|
||||||
if (panelRef.value) {
|
const containerEl = wrapperElement.value
|
||||||
|
if (containerEl) {
|
||||||
const screenWidth = window.innerWidth
|
const screenWidth = window.innerWidth
|
||||||
const screenHeight = window.innerHeight
|
const screenHeight = window.innerHeight
|
||||||
const menuWidth = panelRef.value.offsetWidth
|
const menuWidth = containerEl.offsetWidth
|
||||||
const menuHeight = panelRef.value.offsetHeight
|
const menuHeight = containerEl.offsetHeight
|
||||||
|
|
||||||
// Calculate distances to all edges
|
// Calculate distances to all edges
|
||||||
const distanceLeft = lastDragState.value.x
|
const distanceLeft = lastDragState.value.x
|
||||||
@@ -269,14 +419,12 @@ watch(isDragging, (dragging) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const cancelJobTooltipConfig = computed(() =>
|
const isFloating = computed(() => visible.value && !isDocked.value)
|
||||||
buildTooltipConfig(t('menu.interrupt'))
|
const inlineProgressTarget = computed(() => {
|
||||||
)
|
if (!visible.value) return null
|
||||||
|
if (isFloating.value) return panelElement.value
|
||||||
const cancelCurrentJob = async () => {
|
return topMenuContainer
|
||||||
if (isExecutionIdle.value) return
|
})
|
||||||
await commandStore.execute('Comfy.Interrupt')
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionbarClass = computed(() =>
|
const actionbarClass = computed(() =>
|
||||||
cn(
|
cn(
|
||||||
@@ -292,9 +440,15 @@ const panelClass = computed(() =>
|
|||||||
cn(
|
cn(
|
||||||
'actionbar pointer-events-auto z-1300',
|
'actionbar pointer-events-auto z-1300',
|
||||||
isDragging.value && 'select-none pointer-events-none',
|
isDragging.value && 'select-none pointer-events-none',
|
||||||
|
isDocked.value ? 'p-0 static border-none bg-transparent' : 'fixed'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const panelRootClass = computed(() =>
|
||||||
|
cn(
|
||||||
|
'overflow-hidden rounded-[var(--p-panel-border-radius)]',
|
||||||
isDocked.value
|
isDocked.value
|
||||||
? 'p-0 static border-none bg-transparent'
|
? 'border-none bg-transparent'
|
||||||
: 'fixed shadow-interface'
|
: 'border border-interface-stroke bg-comfy-menu-bg shadow-interface'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
32
src/components/queue/QueueInlineProgress.vue
Normal file
32
src/components/queue/QueueInlineProgress.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="shouldShow"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pointer-events-none absolute inset-x-0 bottom-0 h-[3px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-0 h-full bg-interface-panel-job-progress-primary transition-[width]"
|
||||||
|
:style="{ width: `${totalPercent}%` }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-0 h-full 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 { hidden = false } = defineProps<{
|
||||||
|
hidden?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||||
|
|
||||||
|
const shouldShow = computed(
|
||||||
|
() => !hidden && (totalPercent.value > 0 || currentNodePercent.value > 0)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
62
src/components/queue/QueueInlineProgressSummary.vue
Normal file
62
src/components/queue/QueueInlineProgressSummary.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<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 { useCurrentNodeName } from '@/composables/queue/useCurrentNodeName'
|
||||||
|
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
|
|
||||||
|
const { hidden = false } = defineProps<{
|
||||||
|
hidden?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const executionStore = useExecutionStore()
|
||||||
|
const { currentNodeName } = useCurrentNodeName()
|
||||||
|
const {
|
||||||
|
totalPercent,
|
||||||
|
totalPercentFormatted,
|
||||||
|
currentNodePercent,
|
||||||
|
currentNodePercentFormatted
|
||||||
|
} = useQueueProgress()
|
||||||
|
|
||||||
|
const shouldShow = computed(
|
||||||
|
() =>
|
||||||
|
!hidden &&
|
||||||
|
(!executionStore.isIdle ||
|
||||||
|
totalPercent.value > 0 ||
|
||||||
|
currentNodePercent.value > 0)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
23
src/composables/queue/useCurrentNodeName.ts
Normal file
23
src/composables/queue/useCurrentNodeName.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { st } from '@/i18n'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
|
|
||||||
|
export function useCurrentNodeName() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const executionStore = useExecutionStore()
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { currentNodeName }
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useCurrentNodeName } from '@/composables/queue/useCurrentNodeName'
|
||||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||||
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 { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
isToday,
|
isToday,
|
||||||
isYesterday
|
isYesterday
|
||||||
} from '@/utils/dateTimeUtil'
|
} from '@/utils/dateTimeUtil'
|
||||||
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'
|
||||||
|
|
||||||
@@ -168,6 +167,7 @@ export function useJobList() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { totalPercent, currentNodePercent } = useQueueProgress()
|
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||||
|
const { currentNodeName } = useCurrentNodeName()
|
||||||
|
|
||||||
const relativeTimeFormatter = computed(() => {
|
const relativeTimeFormatter = computed(() => {
|
||||||
const localeValue = locale.value
|
const localeValue = locale.value
|
||||||
@@ -183,16 +183,6 @@ export function useJobList() {
|
|||||||
const isJobInitializing = (promptId: string | number | undefined) =>
|
const isJobInitializing = (promptId: string | number | undefined) =>
|
||||||
executionStore.isPromptInitializing(promptId)
|
executionStore.isPromptInitializing(promptId)
|
||||||
|
|
||||||
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 selectedJobTab = ref<JobTab>('All')
|
const selectedJobTab = ref<JobTab>('All')
|
||||||
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
|
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
|
||||||
const selectedSortMode = ref<JobSortMode>('mostRecent')
|
const selectedSortMode = ref<JobSortMode>('mostRecent')
|
||||||
|
|||||||
@@ -733,6 +733,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user