feat: show active jobs label in top menu (#8169)
Replace the top-menu queue history icon with a localized “N active” label so active jobs are visible at a glance. Requested as part of the new [designs](https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3381-6181&m=dev). I checked all failing snapshots and they are all expected (1 flaky). ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8169-feat-show-active-jobs-label-in-top-menu-2ee6d73d3650812cbf0cda389395c563) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 81 KiB |
@@ -1,12 +1,17 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { computed } from 'vue'
|
import { computed, nextTick } 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 }))
|
||||||
@@ -36,7 +41,8 @@ function createWrapper() {
|
|||||||
sideToolbar: {
|
sideToolbar: {
|
||||||
queueProgressOverlay: {
|
queueProgressOverlay: {
|
||||||
viewJobHistory: 'View job history',
|
viewJobHistory: 'View job history',
|
||||||
expandCollapsedQueue: 'Expand collapsed queue'
|
expandCollapsedQueue: 'Expand collapsed queue',
|
||||||
|
activeJobsShort: '{count} active | {count} active'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +65,19 @@ 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()
|
||||||
@@ -100,4 +119,19 @@ 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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,19 +44,17 @@
|
|||||||
<Button
|
<Button
|
||||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||||
type="destructive"
|
type="destructive"
|
||||||
size="icon"
|
size="md"
|
||||||
:aria-pressed="isQueueOverlayExpanded"
|
:aria-pressed="isQueueOverlayExpanded"
|
||||||
:aria-label="
|
class="px-3"
|
||||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
data-testid="queue-overlay-toggle"
|
||||||
"
|
|
||||||
@click="toggleQueueOverlay"
|
@click="toggleQueueOverlay"
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--history] size-4" />
|
<span class="text-sm font-normal tabular-nums">
|
||||||
<span
|
{{ activeJobsLabel }}
|
||||||
v-if="queuedCount > 0"
|
</span>
|
||||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-base-foreground"
|
<span class="sr-only">
|
||||||
>
|
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
|
||||||
{{ queuedCount }}
|
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<CurrentUserButton
|
<CurrentUserButton
|
||||||
@@ -117,18 +115,26 @@ const rightSidePanelStore = useRightSidePanelStore()
|
|||||||
const managerState = useManagerState()
|
const managerState = useManagerState()
|
||||||
const { isLoggedIn } = useCurrentUser()
|
const { isLoggedIn } = useCurrentUser()
|
||||||
const isDesktop = isElectron()
|
const isDesktop = isElectron()
|
||||||
const { t } = useI18n()
|
const { t, n } = useI18n()
|
||||||
const { toastErrorHandler } = useErrorHandling()
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const queueUIStore = useQueueUIStore()
|
const queueUIStore = useQueueUIStore()
|
||||||
|
const { activeJobsCount } = storeToRefs(queueStore)
|
||||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||||
const releaseStore = useReleaseStore()
|
const releaseStore = useReleaseStore()
|
||||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||||
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
||||||
useConflictAcknowledgment()
|
useConflictAcknowledgment()
|
||||||
const isTopMenuHovered = ref(false)
|
const isTopMenuHovered = ref(false)
|
||||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
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'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -202,6 +202,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import Divider from 'primevue/divider'
|
import Divider from 'primevue/divider'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
@@ -246,6 +247,7 @@ interface JobOutputItem {
|
|||||||
const { t, n } = useI18n()
|
const { t, n } = useI18n()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
|
const { activeJobsCount } = storeToRefs(queueStore)
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
@@ -292,9 +294,6 @@ const formattedExecutionTime = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||||
const activeJobsCount = computed(
|
|
||||||
() => queueStore.pendingTasks.length + queueStore.runningTasks.length
|
|
||||||
)
|
|
||||||
const activeJobsLabel = computed(() => {
|
const activeJobsLabel = computed(() => {
|
||||||
const count = activeJobsCount.value
|
const count = activeJobsCount.value
|
||||||
return t(
|
return t(
|
||||||
|
|||||||
@@ -761,6 +761,7 @@
|
|||||||
"sortJobs": "Sort jobs",
|
"sortJobs": "Sort jobs",
|
||||||
"sortBy": "Sort by",
|
"sortBy": "Sort by",
|
||||||
"activeJobs": "{count} active job | {count} active jobs",
|
"activeJobs": "{count} active job | {count} active jobs",
|
||||||
|
"activeJobsShort": "{count} active | {count} active",
|
||||||
"activeJobsSuffix": "active jobs",
|
"activeJobsSuffix": "active jobs",
|
||||||
"jobQueue": "Job Queue",
|
"jobQueue": "Job Queue",
|
||||||
"expandCollapsedQueue": "Expand job queue",
|
"expandCollapsedQueue": "Expand job queue",
|
||||||
@@ -2666,4 +2667,4 @@
|
|||||||
"tooltip": "You are using a nightly version of ComfyUI. Please use the feedback button to share your thoughts about these features."
|
"tooltip": "You are using a nightly version of ComfyUI. Please use the feedback button to share your thoughts about these features."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,6 +493,9 @@ export const useQueueStore = defineStore('queue', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const hasPendingTasks = computed<boolean>(() => pendingTasks.value.length > 0)
|
const hasPendingTasks = computed<boolean>(() => pendingTasks.value.length > 0)
|
||||||
|
const activeJobsCount = computed(
|
||||||
|
() => pendingTasks.value.length + runningTasks.value.length
|
||||||
|
)
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -572,6 +575,7 @@ export const useQueueStore = defineStore('queue', () => {
|
|||||||
flatTasks,
|
flatTasks,
|
||||||
lastHistoryQueueIndex,
|
lastHistoryQueueIndex,
|
||||||
hasPendingTasks,
|
hasPendingTasks,
|
||||||
|
activeJobsCount,
|
||||||
|
|
||||||
update,
|
update,
|
||||||
clear,
|
clear,
|
||||||
|
|||||||