mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
Desktop maintenance: unsafe base path warning (#6750)
Surface unsafe base path validation in the desktop maintenance view and add an installation-fix auto-refresh after successful tasks. <img width="1080" height="870" alt="image" src="https://github.com/user-attachments/assets/26fe61be-fed8-47c0-a921-604f0af018f8" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6750-Desktop-maintenance-unsafe-base-path-warning-2b06d73d36508147aeb4d19d02bbf0f0) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
|||||||
execute: async () => await electron.setBasePath(),
|
execute: async () => await electron.setBasePath(),
|
||||||
name: 'Base path',
|
name: 'Base path',
|
||||||
shortDescription: 'Change the application base path.',
|
shortDescription: 'Change the application base path.',
|
||||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
errorDescription:
|
||||||
|
'The current base path is invalid or unsafe. Please select a new location.',
|
||||||
description:
|
description:
|
||||||
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||||
isInstallationFix: true,
|
isInstallationFix: true,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
|||||||
const electron = electronAPI()
|
const electron = electronAPI()
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
|
const lastUpdate = ref<InstallValidation | null>(null)
|
||||||
const isRefreshing = ref(false)
|
const isRefreshing = ref(false)
|
||||||
const isRunningTerminalCommand = computed(() =>
|
const isRunningTerminalCommand = computed(() =>
|
||||||
tasks.value
|
tasks.value
|
||||||
@@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
|||||||
.some((task) => getRunner(task)?.executing)
|
.some((task) => getRunner(task)?.executing)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const unsafeBasePath = computed(
|
||||||
|
() => lastUpdate.value?.unsafeBasePath === true
|
||||||
|
)
|
||||||
|
const unsafeBasePathReason = computed(
|
||||||
|
() => lastUpdate.value?.unsafeBasePathReason
|
||||||
|
)
|
||||||
|
|
||||||
// Task list
|
// Task list
|
||||||
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
||||||
|
|
||||||
@@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
|||||||
* @param validationUpdate Update details passed in by electron
|
* @param validationUpdate Update details passed in by electron
|
||||||
*/
|
*/
|
||||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||||
|
lastUpdate.value = validationUpdate
|
||||||
const update = validationUpdate as IndexedUpdate
|
const update = validationUpdate as IndexedUpdate
|
||||||
isRefreshing.value = true
|
isRefreshing.value = true
|
||||||
|
|
||||||
@@ -155,7 +164,11 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const execute = async (task: MaintenanceTask) => {
|
const execute = async (task: MaintenanceTask) => {
|
||||||
return getRunner(task).execute(task)
|
const success = await getRunner(task).execute(task)
|
||||||
|
if (success && task.isInstallationFix) {
|
||||||
|
await refreshDesktopTasks()
|
||||||
|
}
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -163,6 +176,8 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
|||||||
isRefreshing,
|
isRefreshing,
|
||||||
isRunningTerminalCommand,
|
isRunningTerminalCommand,
|
||||||
isRunningInstallationFix,
|
isRunningInstallationFix,
|
||||||
|
unsafeBasePath,
|
||||||
|
unsafeBasePathReason,
|
||||||
execute,
|
execute,
|
||||||
getRunner,
|
getRunner,
|
||||||
processUpdate,
|
processUpdate,
|
||||||
|
|||||||
159
apps/desktop-ui/src/views/MaintenanceView.stories.ts
Normal file
159
apps/desktop-ui/src/views/MaintenanceView.stories.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// eslint-disable-next-line storybook/no-renderer-packages
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null
|
||||||
|
type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped'
|
||||||
|
|
||||||
|
type ValidationState = {
|
||||||
|
inProgress: boolean
|
||||||
|
installState: string
|
||||||
|
basePath?: ValidationIssueState
|
||||||
|
unsafeBasePath: boolean
|
||||||
|
unsafeBasePathReason: UnsafeReason
|
||||||
|
venvDirectory?: ValidationIssueState
|
||||||
|
pythonInterpreter?: ValidationIssueState
|
||||||
|
pythonPackages?: ValidationIssueState
|
||||||
|
uv?: ValidationIssueState
|
||||||
|
git?: ValidationIssueState
|
||||||
|
vcRedist?: ValidationIssueState
|
||||||
|
upgradePackages?: ValidationIssueState
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationState: ValidationState = {
|
||||||
|
inProgress: false,
|
||||||
|
installState: 'installed',
|
||||||
|
basePath: 'OK',
|
||||||
|
unsafeBasePath: false,
|
||||||
|
unsafeBasePathReason: null,
|
||||||
|
venvDirectory: 'OK',
|
||||||
|
pythonInterpreter: 'OK',
|
||||||
|
pythonPackages: 'OK',
|
||||||
|
uv: 'OK',
|
||||||
|
git: 'OK',
|
||||||
|
vcRedist: 'OK',
|
||||||
|
upgradePackages: 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockElectronAPI = () => {
|
||||||
|
const logListeners: Array<(message: string) => void> = []
|
||||||
|
|
||||||
|
const getValidationUpdate = () => ({
|
||||||
|
...validationState
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPlatform: () => 'darwin',
|
||||||
|
changeTheme: (_theme: unknown) => {},
|
||||||
|
onLogMessage: (listener: (message: string) => void) => {
|
||||||
|
logListeners.push(listener)
|
||||||
|
},
|
||||||
|
showContextMenu: (_options: unknown) => {},
|
||||||
|
Events: {
|
||||||
|
trackEvent: (_eventName: string, _data?: unknown) => {}
|
||||||
|
},
|
||||||
|
Validation: {
|
||||||
|
onUpdate: (_callback: (update: unknown) => void) => {},
|
||||||
|
async getStatus() {
|
||||||
|
return getValidationUpdate()
|
||||||
|
},
|
||||||
|
async validateInstallation(callback: (update: unknown) => void) {
|
||||||
|
callback(getValidationUpdate())
|
||||||
|
},
|
||||||
|
async complete() {
|
||||||
|
// Only allow completion when the base path is safe
|
||||||
|
return !validationState.unsafeBasePath
|
||||||
|
},
|
||||||
|
dispose: () => {}
|
||||||
|
},
|
||||||
|
setBasePath: () => Promise.resolve(true),
|
||||||
|
reinstall: () => Promise.resolve(),
|
||||||
|
uv: {
|
||||||
|
installRequirements: () => Promise.resolve(),
|
||||||
|
clearCache: () => Promise.resolve(),
|
||||||
|
resetVenv: () => Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureElectronAPI = () => {
|
||||||
|
const globalWindow = window as unknown as { electronAPI?: unknown }
|
||||||
|
if (!globalWindow.electronAPI) {
|
||||||
|
globalWindow.electronAPI = createMockElectronAPI()
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalWindow.electronAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
const MaintenanceView = defineAsyncComponent(async () => {
|
||||||
|
ensureElectronAPI()
|
||||||
|
const module = await import('./MaintenanceView.vue')
|
||||||
|
return module.default
|
||||||
|
})
|
||||||
|
|
||||||
|
const meta: Meta<typeof MaintenanceView> = {
|
||||||
|
title: 'Desktop/Views/MaintenanceView',
|
||||||
|
component: MaintenanceView,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
backgrounds: {
|
||||||
|
default: 'dark',
|
||||||
|
values: [
|
||||||
|
{ name: 'dark', value: '#0a0a0a' },
|
||||||
|
{ name: 'neutral-900', value: '#171717' },
|
||||||
|
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
name: 'All tasks OK',
|
||||||
|
render: () => ({
|
||||||
|
components: { MaintenanceView },
|
||||||
|
setup() {
|
||||||
|
validationState.inProgress = false
|
||||||
|
validationState.installState = 'installed'
|
||||||
|
validationState.basePath = 'OK'
|
||||||
|
validationState.unsafeBasePath = false
|
||||||
|
validationState.unsafeBasePathReason = null
|
||||||
|
validationState.venvDirectory = 'OK'
|
||||||
|
validationState.pythonInterpreter = 'OK'
|
||||||
|
validationState.pythonPackages = 'OK'
|
||||||
|
validationState.uv = 'OK'
|
||||||
|
validationState.git = 'OK'
|
||||||
|
validationState.vcRedist = 'OK'
|
||||||
|
validationState.upgradePackages = 'OK'
|
||||||
|
ensureElectronAPI()
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
template: '<MaintenanceView />'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnsafeBasePathOneDrive: Story = {
|
||||||
|
name: 'Unsafe base path (OneDrive)',
|
||||||
|
render: () => ({
|
||||||
|
components: { MaintenanceView },
|
||||||
|
setup() {
|
||||||
|
validationState.inProgress = false
|
||||||
|
validationState.installState = 'installed'
|
||||||
|
validationState.basePath = 'error'
|
||||||
|
validationState.unsafeBasePath = true
|
||||||
|
validationState.unsafeBasePathReason = 'oneDrive'
|
||||||
|
validationState.venvDirectory = 'OK'
|
||||||
|
validationState.pythonInterpreter = 'OK'
|
||||||
|
validationState.pythonPackages = 'OK'
|
||||||
|
validationState.uv = 'OK'
|
||||||
|
validationState.git = 'OK'
|
||||||
|
validationState.vcRedist = 'OK'
|
||||||
|
validationState.upgradePackages = 'OK'
|
||||||
|
ensureElectronAPI()
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
template: '<MaintenanceView />'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -47,6 +47,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unsafe migration warning -->
|
||||||
|
<div v-if="taskStore.unsafeBasePath" class="my-4">
|
||||||
|
<p class="flex items-start gap-3 text-neutral-300">
|
||||||
|
<Tag
|
||||||
|
icon="pi pi-exclamation-triangle"
|
||||||
|
severity="warn"
|
||||||
|
:value="t('icon.exclamation-triangle')"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<strong class="block mb-1">
|
||||||
|
{{ t('maintenance.unsafeMigration.title') }}
|
||||||
|
</strong>
|
||||||
|
<span class="block mb-1">
|
||||||
|
{{ unsafeReasonText }}
|
||||||
|
</span>
|
||||||
|
<span class="block text-sm text-neutral-400">
|
||||||
|
{{ t('maintenance.unsafeMigration.action') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tasks -->
|
<!-- Tasks -->
|
||||||
<TaskListPanel
|
<TaskListPanel
|
||||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||||
@@ -89,10 +111,10 @@
|
|||||||
import { PrimeIcons } from '@primevue/core/api'
|
import { PrimeIcons } from '@primevue/core/api'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import SelectButton from 'primevue/selectbutton'
|
import SelectButton from 'primevue/selectbutton'
|
||||||
|
import Tag from 'primevue/tag'
|
||||||
import Toast from 'primevue/toast'
|
import Toast from 'primevue/toast'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { watch } from 'vue'
|
|
||||||
|
|
||||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||||
@@ -139,6 +161,27 @@ const filterOptions = ref([
|
|||||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||||
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
||||||
|
|
||||||
|
const unsafeReasonText = computed(() => {
|
||||||
|
const reason = taskStore.unsafeBasePathReason
|
||||||
|
if (!reason) {
|
||||||
|
return t('maintenance.unsafeMigration.generic')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason === 'appInstallDir') {
|
||||||
|
return t('maintenance.unsafeMigration.appInstallDir')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason === 'updaterCache') {
|
||||||
|
return t('maintenance.unsafeMigration.updaterCache')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason === 'oneDrive') {
|
||||||
|
return t('maintenance.unsafeMigration.oneDrive')
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('maintenance.unsafeMigration.generic')
|
||||||
|
})
|
||||||
|
|
||||||
/** If valid, leave the validation window. */
|
/** If valid, leave the validation window. */
|
||||||
const completeValidation = async () => {
|
const completeValidation = async () => {
|
||||||
const isValid = await electron.Validation.complete()
|
const isValid = await electron.Validation.complete()
|
||||||
|
|||||||
@@ -1500,6 +1500,14 @@
|
|||||||
"taskFailed": "Task failed to run.",
|
"taskFailed": "Task failed to run.",
|
||||||
"cannotContinue": "Unable to continue - errors remain",
|
"cannotContinue": "Unable to continue - errors remain",
|
||||||
"defaultDescription": "An error occurred while running a maintenance task."
|
"defaultDescription": "An error occurred while running a maintenance task."
|
||||||
|
},
|
||||||
|
"unsafeMigration": {
|
||||||
|
"title": "Unsafe install location detected",
|
||||||
|
"generic": "Your current ComfyUI base path is in a location that may be deleted or modified during updates. To avoid data loss, move it to a safe folder.",
|
||||||
|
"appInstallDir": "Your base path is inside the ComfyUI Desktop application bundle. This folder may be deleted or overwritten during updates. Choose a directory outside the install folder, such as Documents/ComfyUI.",
|
||||||
|
"updaterCache": "Your base path is inside the ComfyUI updater cache, which is cleared on each update. Choose a different location for your data.",
|
||||||
|
"oneDrive": "Your base path is on OneDrive, which can cause sync issues and accidental data loss. Choose a local folder that is not managed by OneDrive.",
|
||||||
|
"action": "Use the \"Base path\" maintenance task below to move ComfyUI to a safe location."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"missingModelsDialog": {
|
"missingModelsDialog": {
|
||||||
|
|||||||
Reference in New Issue
Block a user