From 54b7fa3b3192a60b40928966d95256eb1d228f82 Mon Sep 17 00:00:00 2001 From: shivansh-gupta4 Date: Sun, 6 Jul 2025 13:34:40 +0530 Subject: [PATCH] Feature Implemented: Warning displayed when frontend version mismatches --- .../dialog/content/VersionMismatchWarning.vue | 80 ++++++ src/constants/coreSettings.ts | 6 + src/locales/en/main.json | 7 + src/schemas/apiSchema.ts | 3 + src/stores/README.md | 1 + src/stores/versionCompatibilityStore.ts | 125 ++++++++ src/views/GraphView.vue | 7 + tests-ui/tests/store/systemStatsStore.test.ts | 27 ++ .../store/versionCompatibilityStore.test.ts | 267 ++++++++++++++++++ 9 files changed, 523 insertions(+) create mode 100644 src/components/dialog/content/VersionMismatchWarning.vue create mode 100644 src/stores/versionCompatibilityStore.ts create mode 100644 tests-ui/tests/store/versionCompatibilityStore.test.ts diff --git a/src/components/dialog/content/VersionMismatchWarning.vue b/src/components/dialog/content/VersionMismatchWarning.vue new file mode 100644 index 000000000..eaf2c2a57 --- /dev/null +++ b/src/components/dialog/content/VersionMismatchWarning.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index b01b6ab0e..9720208a7 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -873,5 +873,11 @@ export const CORE_SETTINGS: SettingParams[] = [ name: 'Release seen timestamp', type: 'hidden', defaultValue: 0 + }, + { + id: 'Comfy.VersionMismatch.DismissedVersion', + name: 'Dismissed version mismatch warning', + type: 'hidden', + defaultValue: '' } ] diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 92b48f703..3765dc285 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1233,6 +1233,13 @@ "outdatedVersionGeneric": "Some nodes require a newer version of ComfyUI. Please update to use all nodes.", "coreNodesFromVersion": "Requires ComfyUI {version}:" }, + "versionMismatchWarning": { + "title": "Version Compatibility Warning", + "frontendOutdated": "Frontend version {frontendVersion} is outdated. Backend requires version {requiredVersion} or higher.", + "frontendNewer": "Frontend version {frontendVersion} may not be compatible with backend version {backendVersion}.", + "updateFrontend": "Update Frontend", + "dismiss": "Dismiss" + }, "errorDialog": { "defaultTitle": "An error occurred", "loadWorkflowTitle": "Loading aborted due to error reloading workflow data", diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index db55f61f0..8fe990103 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -317,6 +317,7 @@ export const zSystemStats = z.object({ embedded_python: z.boolean(), comfyui_version: z.string(), pytorch_version: z.string(), + required_frontend_version: z.string().optional(), argv: z.array(z.string()), ram_total: z.number(), ram_free: z.number() @@ -481,6 +482,8 @@ const zSettings = z.object({ "what's new seen" ]), 'Comfy.Release.Timestamp': z.number(), + /** Version compatibility settings */ + 'Comfy.VersionMismatch.DismissedVersion': z.string(), /** Settings used for testing */ 'test.setting': z.any(), 'main.sub.setting.name': z.any(), diff --git a/src/stores/README.md b/src/stores/README.md index 3a61d7ae6..de45fdc71 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -135,6 +135,7 @@ The following table lists ALL stores in the system as of 2025-01-30: | toastStore.ts | Manages toast notifications | UI | | userFileStore.ts | Manages user file operations | Files | | userStore.ts | Manages user data and preferences | User | +| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core | | widgetStore.ts | Manages widget configurations | Widgets | | workflowStore.ts | Handles workflow data and operations | Workflows | | workflowTemplatesStore.ts | Manages workflow templates | Workflows | diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts new file mode 100644 index 000000000..c57cd886c --- /dev/null +++ b/src/stores/versionCompatibilityStore.ts @@ -0,0 +1,125 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +import config from '@/config' +import { useSettingStore } from '@/stores/settingStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { compareVersions } from '@/utils/formatUtil' + +export const useVersionCompatibilityStore = defineStore( + 'versionCompatibility', + () => { + const systemStatsStore = useSystemStatsStore() + const settingStore = useSettingStore() + + const isDismissed = ref(false) + const dismissedVersion = ref(null) + + const frontendVersion = computed(() => config.app_version) + const backendVersion = computed( + () => systemStatsStore.systemStats?.system?.comfyui_version ?? '' + ) + const requiredFrontendVersion = computed( + () => + systemStatsStore.systemStats?.system?.required_frontend_version ?? '' + ) + + const isFrontendOutdated = computed(() => { + if (!frontendVersion.value || !requiredFrontendVersion.value) { + return false + } + return ( + compareVersions(requiredFrontendVersion.value, frontendVersion.value) > + 0 + ) + }) + + const isFrontendNewer = computed(() => { + if (!frontendVersion.value || !backendVersion.value) { + return false + } + const versionDiff = compareVersions( + frontendVersion.value, + backendVersion.value + ) + return versionDiff > 0 + }) + + const hasVersionMismatch = computed(() => { + return isFrontendOutdated.value || isFrontendNewer.value + }) + + const shouldShowWarning = computed(() => { + if (!hasVersionMismatch.value || isDismissed.value) { + return false + } + + const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + return dismissedVersion.value !== currentVersionKey + }) + + const warningMessage = computed(() => { + if (isFrontendOutdated.value) { + return { + type: 'outdated' as const, + frontendVersion: frontendVersion.value, + requiredVersion: requiredFrontendVersion.value + } + } else if (isFrontendNewer.value) { + return { + type: 'newer' as const, + frontendVersion: frontendVersion.value, + backendVersion: backendVersion.value + } + } + return null + }) + + async function checkVersionCompatibility() { + if (!systemStatsStore.systemStats) { + await systemStatsStore.fetchSystemStats() + } + } + + async function dismissWarning() { + isDismissed.value = true + const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + dismissedVersion.value = currentVersionKey + + await settingStore.set( + 'Comfy.VersionMismatch.DismissedVersion', + currentVersionKey + ) + } + + function restoreDismissalState() { + const dismissed = settingStore.get( + 'Comfy.VersionMismatch.DismissedVersion' + ) + if (dismissed) { + dismissedVersion.value = dismissed + const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + isDismissed.value = dismissed === currentVersionKey + } + } + + async function initialize() { + await checkVersionCompatibility() + restoreDismissalState() + } + + return { + frontendVersion, + backendVersion, + requiredFrontendVersion, + hasVersionMismatch, + shouldShowWarning, + warningMessage, + isFrontendOutdated, + isFrontendNewer, + checkVersionCompatibility, + dismissWarning, + initialize + } + } +) diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 5d6719a48..cdf91028e 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -9,6 +9,7 @@
+
@@ -28,6 +29,7 @@ import { useI18n } from 'vue-i18n' import MenuHamburger from '@/components/MenuHamburger.vue' import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue' +import VersionMismatchWarning from '@/components/dialog/content/VersionMismatchWarning.vue' import GraphCanvas from '@/components/graph/GraphCanvas.vue' import GlobalToast from '@/components/toast/GlobalToast.vue' import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue' @@ -54,6 +56,7 @@ import { } from '@/stores/queueStore' import { useServerConfigStore } from '@/stores/serverConfigStore' import { useSettingStore } from '@/stores/settingStore' +import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' @@ -70,6 +73,7 @@ const settingStore = useSettingStore() const executionStore = useExecutionStore() const colorPaletteStore = useColorPaletteStore() const queueStore = useQueueStore() +const versionCompatibilityStore = useVersionCompatibilityStore() watch( () => colorPaletteStore.completedActivePalette, @@ -206,6 +210,9 @@ onMounted(() => { } catch (e) { console.error('Failed to init ComfyUI frontend', e) } + + // Initialize version compatibility checking (fire-and-forget) + void versionCompatibilityStore.initialize() }) onBeforeUnmount(() => { diff --git a/tests-ui/tests/store/systemStatsStore.test.ts b/tests-ui/tests/store/systemStatsStore.test.ts index 3376a19c0..84e84ec44 100644 --- a/tests-ui/tests/store/systemStatsStore.test.ts +++ b/tests-ui/tests/store/systemStatsStore.test.ts @@ -41,6 +41,7 @@ describe('useSystemStatsStore', () => { embedded_python: false, comfyui_version: '1.0.0', pytorch_version: '2.0.0', + required_frontend_version: '1.24.0', argv: [], ram_total: 16000000000, ram_free: 8000000000 @@ -92,6 +93,32 @@ describe('useSystemStatsStore', () => { expect(store.isLoading).toBe(false) }) + + it('should handle system stats updates', async () => { + const updatedStats = { + system: { + os: 'Windows', + python_version: '3.11.0', + embedded_python: false, + comfyui_version: '1.1.0', + pytorch_version: '2.1.0', + required_frontend_version: '1.25.0', + argv: [], + ram_total: 16000000000, + ram_free: 7000000000 + }, + devices: [] + } + + vi.mocked(api.getSystemStats).mockResolvedValue(updatedStats) + + await store.fetchSystemStats() + + expect(store.systemStats).toEqual(updatedStats) + expect(store.isLoading).toBe(false) + expect(store.error).toBeNull() + expect(api.getSystemStats).toHaveBeenCalled() + }) }) describe('getFormFactor', () => { diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts new file mode 100644 index 000000000..d044bfb02 --- /dev/null +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -0,0 +1,267 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useSettingStore } from '@/stores/settingStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' + +vi.mock('@/config', () => ({ + default: { + app_version: '1.24.0' + } +})) + +vi.mock('@/stores/systemStatsStore') +vi.mock('@/stores/settingStore') + +describe('useVersionCompatibilityStore', () => { + let store: ReturnType + let mockSystemStatsStore: any + let mockSettingStore: any + + beforeEach(() => { + setActivePinia(createPinia()) + + mockSystemStatsStore = { + systemStats: null, + fetchSystemStats: vi.fn() + } + + mockSettingStore = { + get: vi.fn(), + set: vi.fn() + } + + vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) + vi.mocked(useSettingStore).mockReturnValue(mockSettingStore) + + store = useVersionCompatibilityStore() + }) + + describe('version compatibility detection', () => { + it('should detect frontend is outdated when required version is higher', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(true) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(true) + }) + + it('should detect frontend is newer when frontend version is higher than backend', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.23.0', + required_frontend_version: '1.23.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(true) + expect(store.hasVersionMismatch).toBe(true) + }) + + it('should not detect mismatch when versions are compatible', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(false) + }) + + it('should handle missing version information gracefully', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '', + required_frontend_version: '' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(false) + }) + }) + + describe('warning display logic', () => { + beforeEach(() => { + mockSettingStore.get.mockReturnValue('') + }) + + it('should show warning when there is a version mismatch and not dismissed', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.shouldShowWarning).toBe(true) + }) + + it('should not show warning when dismissed', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + void store.dismissWarning() + + expect(store.shouldShowWarning).toBe(false) + }) + + it('should not show warning when no version mismatch', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.shouldShowWarning).toBe(false) + }) + }) + + describe('warning messages', () => { + it('should generate outdated message when frontend is outdated', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.warningMessage).toEqual({ + type: 'outdated', + frontendVersion: '1.24.0', + requiredVersion: '1.25.0' + }) + }) + + it('should generate newer message when frontend is newer', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.23.0', + required_frontend_version: '1.23.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.warningMessage).toEqual({ + type: 'newer', + frontendVersion: '1.24.0', + backendVersion: '1.23.0' + }) + }) + + it('should return null when no mismatch', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.warningMessage).toBeNull() + }) + }) + + describe('dismissal persistence', () => { + it('should save dismissal to settings', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + await store.dismissWarning() + + expect(mockSettingStore.set).toHaveBeenCalledWith( + 'Comfy.VersionMismatch.DismissedVersion', + '1.24.0-1.25.0-1.25.0' + ) + }) + + it('should restore dismissal state from settings', async () => { + mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0') + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.initialize() + + expect(store.shouldShowWarning).toBe(false) + }) + + it('should show warning for different version combinations even if previous was dismissed', async () => { + mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0') + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.26.0', + required_frontend_version: '1.26.0' + } + } + + await store.initialize() + + expect(store.shouldShowWarning).toBe(true) + }) + }) + + describe('initialization', () => { + it('should fetch system stats if not available', async () => { + mockSystemStatsStore.systemStats = null + + await store.initialize() + + expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + }) + + it('should not fetch system stats if already available', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.initialize() + + expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled() + }) + }) +})