mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 15:29:44 +00:00
[Backport 1.27] Rework desktop install / startup UX (#5292)
## Summary Backporting #5292 to core/1.27 branch to fix desktop installation and startup UX issues. ## What's Changed This is a manual backport of commitb0f81b2245which reworks the desktop install and startup user experience. ### Merge Conflicts Resolved The automatic backport failed with conflicts in: - `src/components/install/GpuPicker.vue` - Resolved by keeping the changes made in the PR - `src/components/install/MirrorsConfiguration.vue` - Resolved by keeping the file deletion from the PR - `src/components/install/mirror/MirrorItem.vue` - Resolved by combination merge (all changes) - `src/views/ServerStartView.vue` - Resolved by combination merge (all changes) ## Original PR - PR: #5292 - Commit:b0f81b2245- Title: Rework desktop install / startup UX ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5756-Backport-1-27-Rework-desktop-install-startup-UX-5292-2786d73d365081668f4dc16694c51185) by [Unito](https://www.unito.io)
This commit is contained in:
423
src/views/InstallView.stories.ts
Normal file
423
src/views/InstallView.stories.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { nextTick, provide } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import InstallView from './InstallView.vue'
|
||||
|
||||
// Create a mock router for stories
|
||||
const createMockRouter = () =>
|
||||
createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
{
|
||||
path: '/server-start',
|
||||
component: { template: '<div>Server Start</div>' }
|
||||
},
|
||||
{
|
||||
path: '/manual-configuration',
|
||||
component: { template: '<div>Manual Configuration</div>' }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const meta: Meta<typeof InstallView> = {
|
||||
title: 'Desktop/Views/InstallView',
|
||||
component: InstallView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
(story) => {
|
||||
// Create router for this story
|
||||
const router = createMockRouter()
|
||||
|
||||
// Mock electron API
|
||||
;(window as any).electronAPI = {
|
||||
getPlatform: () => 'darwin',
|
||||
Config: {
|
||||
getDetectedGpu: () => Promise.resolve('mps')
|
||||
},
|
||||
Events: {
|
||||
trackEvent: (eventName: string, data?: any) => {
|
||||
console.log('Track event:', eventName, data)
|
||||
}
|
||||
},
|
||||
installComfyUI: (options: any) => {
|
||||
console.log('Install ComfyUI with options:', options)
|
||||
},
|
||||
changeTheme: (theme: any) => {
|
||||
console.log('Change theme:', theme)
|
||||
},
|
||||
getSystemPaths: () =>
|
||||
Promise.resolve({
|
||||
defaultInstallPath: '/Users/username/ComfyUI'
|
||||
}),
|
||||
validateInstallPath: () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false
|
||||
}),
|
||||
validateComfyUISource: () =>
|
||||
Promise.resolve({
|
||||
isValid: true
|
||||
}),
|
||||
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
||||
}
|
||||
|
||||
return {
|
||||
setup() {
|
||||
// Provide router for all child components
|
||||
provide('router', router)
|
||||
return {
|
||||
story
|
||||
}
|
||||
},
|
||||
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Default story - start at GPU selection
|
||||
export const GpuSelection: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
// The component will automatically start at step 1
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the install location step
|
||||
export const InstallLocation: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the migration step (currently empty)
|
||||
export const MigrationStep: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons1 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn1) {
|
||||
nextBtn1.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 3
|
||||
const buttons2 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn2) {
|
||||
nextBtn2.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the desktop settings configuration
|
||||
export const DesktopSettings: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons1 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn1) {
|
||||
nextBtn1.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 3
|
||||
const buttons2 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn2) {
|
||||
nextBtn2.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 4
|
||||
const buttons3 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn3) {
|
||||
nextBtn3.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with Windows platform (no Apple Metal option)
|
||||
export const WindowsPlatform: Story = {
|
||||
render: () => {
|
||||
// Override the platform to Windows
|
||||
;(window as any).electronAPI.getPlatform = () => 'win32'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('nvidia')
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with macOS platform (Apple Metal option)
|
||||
export const MacOSPlatform: Story = {
|
||||
name: 'macOS Platform',
|
||||
render: () => {
|
||||
// Override the platform to macOS
|
||||
;(window as any).electronAPI.getPlatform = () => 'darwin'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('mps')
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with CPU selected
|
||||
export const CpuSelected: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Find and click the CPU hardware option
|
||||
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
||||
// CPU is the button with "CPU" text
|
||||
for (const button of hardwareButtons) {
|
||||
if (button.textContent?.includes('CPU')) {
|
||||
button.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with manual install selected
|
||||
export const ManualInstall: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Find and click the Manual Install hardware option
|
||||
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
||||
// Manual Install is the button with "Manual Install" text
|
||||
for (const button of hardwareButtons) {
|
||||
if (button.textContent?.includes('Manual Install')) {
|
||||
button.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with error state (invalid install path)
|
||||
export const ErrorState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return an error
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: false,
|
||||
exists: false,
|
||||
canWrite: false,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false,
|
||||
error: 'Story mock: Example error state'
|
||||
})
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2 where error will be shown
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with warning state (non-default drive)
|
||||
export const WarningState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return a warning about non-default drive
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 500_000_000_000,
|
||||
requiredSpace: 10_000_000_000,
|
||||
isNonDefaultDrive: true
|
||||
})
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2 where warning will be shown
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user