mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-26 07:57:36 +00:00
Compare commits
4 Commits
austin/fe-
...
glary/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3103a8f499 | ||
|
|
de5f25ec60 | ||
|
|
945eb63d27 | ||
|
|
23a31d8e4b |
60
src/components/common/CopySystemInfoButton.test.ts
Normal file
60
src/components/common/CopySystemInfoButton.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
|
||||
const mockCopySystemInfo = vi.fn()
|
||||
|
||||
vi.mock('@/composables/useCopySystemInfo', () => ({
|
||||
useCopySystemInfo: () => ({ copySystemInfo: mockCopySystemInfo })
|
||||
}))
|
||||
|
||||
import CopySystemInfoButton from './CopySystemInfoButton.vue'
|
||||
|
||||
const stats: SystemStats = {
|
||||
system: {
|
||||
os: 'Linux',
|
||||
python_version: '3.11.5',
|
||||
embedded_python: false,
|
||||
comfyui_version: 'v0.3.0',
|
||||
pytorch_version: '2.1.0',
|
||||
argv: ['main.py'],
|
||||
ram_total: 16_000_000_000,
|
||||
ram_free: 8_000_000_000
|
||||
},
|
||||
devices: []
|
||||
}
|
||||
|
||||
function renderComponent() {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
return render(CopySystemInfoButton, {
|
||||
props: { stats },
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
}
|
||||
|
||||
describe('CopySystemInfoButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('renders the localized label', () => {
|
||||
renderComponent()
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Copy System Info')
|
||||
})
|
||||
|
||||
it('invokes copySystemInfo when clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
await user.click(screen.getByRole('button'))
|
||||
expect(mockCopySystemInfo).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
18
src/components/common/CopySystemInfoButton.vue
Normal file
18
src/components/common/CopySystemInfoButton.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<Button variant="secondary" @click="copySystemInfo">
|
||||
<i class="pi pi-copy" />
|
||||
{{ $t('g.copySystemInfo') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopySystemInfo } from '@/composables/useCopySystemInfo'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
|
||||
const { stats } = defineProps<{
|
||||
stats: SystemStats
|
||||
}>()
|
||||
|
||||
const { copySystemInfo } = useCopySystemInfo(() => stats)
|
||||
</script>
|
||||
@@ -5,10 +5,7 @@
|
||||
<h2 class="text-2xl font-semibold">
|
||||
{{ $t('g.systemInfo') }}
|
||||
</h2>
|
||||
<Button variant="secondary" @click="copySystemInfo">
|
||||
<i class="pi pi-copy" />
|
||||
{{ $t('g.copySystemInfo') }}
|
||||
</Button>
|
||||
<CopySystemInfoButton :stats="props.stats" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<template v-for="col in systemColumns" :key="col.field">
|
||||
@@ -16,7 +13,7 @@
|
||||
{{ col.header }}
|
||||
</div>
|
||||
<div :class="cn(isOutdated(col) && 'text-danger-100')">
|
||||
{{ getDisplayValue(col) }}
|
||||
{{ getColumnDisplayValue(props.stats, col) }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -51,116 +48,28 @@ import TabPanel from 'primevue/tabpanel'
|
||||
import TabView from 'primevue/tabview'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import CopySystemInfoButton from '@/components/common/CopySystemInfoButton.vue'
|
||||
import DeviceInfo from '@/components/common/DeviceInfo.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { SystemStatsColumn } from '@/components/common/systemStatsColumns'
|
||||
import {
|
||||
getColumnDisplayValue,
|
||||
getSystemStatsColumns
|
||||
} from '@/components/common/systemStatsColumns'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const frontendCommit = __COMFYUI_FRONTEND_COMMIT__
|
||||
|
||||
const props = defineProps<{
|
||||
stats: SystemStats
|
||||
}>()
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
|
||||
const systemInfo = computed(() => ({
|
||||
...props.stats.system,
|
||||
argv: props.stats.system.argv.join(' ')
|
||||
}))
|
||||
const systemColumns = getSystemStatsColumns()
|
||||
|
||||
const hasDevices = computed(() => props.stats.devices.length > 0)
|
||||
|
||||
type SystemInfoKey = keyof SystemStats['system']
|
||||
|
||||
type ColumnDef = {
|
||||
field: SystemInfoKey
|
||||
header: string
|
||||
getValue?: () => string
|
||||
format?: (value: string) => string
|
||||
formatNumber?: (value: number) => string
|
||||
}
|
||||
|
||||
/** Columns for local distribution */
|
||||
const localColumns: ColumnDef[] = [
|
||||
{ field: 'os', header: 'OS' },
|
||||
{ field: 'python_version', header: 'Python Version' },
|
||||
{ field: 'embedded_python', header: 'Embedded Python' },
|
||||
{ field: 'pytorch_version', header: 'Pytorch Version' },
|
||||
{ field: 'argv', header: 'Arguments' },
|
||||
{ field: 'ram_total', header: 'RAM Total', formatNumber: formatSize },
|
||||
{ field: 'ram_free', header: 'RAM Free', formatNumber: formatSize },
|
||||
{ field: 'installed_templates_version', header: 'Templates Version' }
|
||||
]
|
||||
|
||||
/** Columns for cloud distribution */
|
||||
const cloudColumns: ColumnDef[] = [
|
||||
{ field: 'cloud_version', header: 'Cloud Version' },
|
||||
{
|
||||
field: 'comfyui_version',
|
||||
header: 'ComfyUI Version',
|
||||
format: formatCommitHash
|
||||
},
|
||||
{
|
||||
field: 'comfyui_frontend_version',
|
||||
header: 'Frontend Version',
|
||||
getValue: () => frontendCommit,
|
||||
format: formatCommitHash
|
||||
},
|
||||
{ field: 'workflow_templates_version', header: 'Templates Version' }
|
||||
]
|
||||
|
||||
const systemColumns = computed(() => (isCloud ? cloudColumns : localColumns))
|
||||
|
||||
function isOutdated(column: ColumnDef): boolean {
|
||||
function isOutdated(column: SystemStatsColumn): boolean {
|
||||
if (column.field !== 'installed_templates_version') return false
|
||||
const installed = props.stats.system.installed_templates_version
|
||||
const required = props.stats.system.required_templates_version
|
||||
return !!installed && !!required && installed !== required
|
||||
}
|
||||
|
||||
function getDisplayValue(column: ColumnDef) {
|
||||
const value = column.getValue
|
||||
? column.getValue()
|
||||
: systemInfo.value[column.field]
|
||||
if (column.formatNumber && typeof value === 'number') {
|
||||
return column.formatNumber(value)
|
||||
}
|
||||
if (column.format && typeof value === 'string') {
|
||||
return column.format(value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function formatSystemInfoText(): string {
|
||||
const lines: string[] = ['## System Info']
|
||||
|
||||
for (const col of systemColumns.value) {
|
||||
const display = getDisplayValue(col)
|
||||
if (display !== undefined && display !== '') {
|
||||
lines.push(`${col.header}: ${display}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDevices.value) {
|
||||
lines.push('')
|
||||
lines.push('## Devices')
|
||||
for (const device of props.stats.devices) {
|
||||
lines.push(`- ${device.name} (${device.type})`)
|
||||
lines.push(` VRAM Total: ${formatSize(device.vram_total)}`)
|
||||
lines.push(` VRAM Free: ${formatSize(device.vram_free)}`)
|
||||
lines.push(` Torch VRAM Total: ${formatSize(device.torch_vram_total)}`)
|
||||
lines.push(` Torch VRAM Free: ${formatSize(device.torch_vram_free)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function copySystemInfo() {
|
||||
copyToClipboard(formatSystemInfoText())
|
||||
}
|
||||
</script>
|
||||
|
||||
64
src/components/common/systemStatsColumns.ts
Normal file
64
src/components/common/systemStatsColumns.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const frontendCommit = __COMFYUI_FRONTEND_COMMIT__
|
||||
|
||||
type SystemInfoKey = keyof SystemStats['system']
|
||||
|
||||
export type SystemStatsColumn = {
|
||||
field: SystemInfoKey
|
||||
header: string
|
||||
getValue?: () => string
|
||||
format?: (value: string) => string
|
||||
formatNumber?: (value: number) => string
|
||||
}
|
||||
|
||||
const localColumns: SystemStatsColumn[] = [
|
||||
{ field: 'os', header: 'OS' },
|
||||
{ field: 'python_version', header: 'Python Version' },
|
||||
{ field: 'embedded_python', header: 'Embedded Python' },
|
||||
{ field: 'pytorch_version', header: 'Pytorch Version' },
|
||||
{ field: 'argv', header: 'Arguments' },
|
||||
{ field: 'ram_total', header: 'RAM Total', formatNumber: formatSize },
|
||||
{ field: 'ram_free', header: 'RAM Free', formatNumber: formatSize },
|
||||
{ field: 'installed_templates_version', header: 'Templates Version' }
|
||||
]
|
||||
|
||||
const cloudColumns: SystemStatsColumn[] = [
|
||||
{ field: 'cloud_version', header: 'Cloud Version' },
|
||||
{
|
||||
field: 'comfyui_version',
|
||||
header: 'ComfyUI Version',
|
||||
format: formatCommitHash
|
||||
},
|
||||
{
|
||||
field: 'comfyui_frontend_version',
|
||||
header: 'Frontend Version',
|
||||
getValue: () => frontendCommit,
|
||||
format: formatCommitHash
|
||||
},
|
||||
{ field: 'workflow_templates_version', header: 'Templates Version' }
|
||||
]
|
||||
|
||||
export function getSystemStatsColumns(): SystemStatsColumn[] {
|
||||
return isCloud ? cloudColumns : localColumns
|
||||
}
|
||||
|
||||
export function getColumnDisplayValue(
|
||||
stats: SystemStats,
|
||||
column: SystemStatsColumn
|
||||
): string | number | boolean | undefined {
|
||||
const systemInfo = {
|
||||
...stats.system,
|
||||
argv: stats.system.argv.join(' ')
|
||||
}
|
||||
const value = column.getValue ? column.getValue() : systemInfo[column.field]
|
||||
if (column.formatNumber && typeof value === 'number') {
|
||||
return column.formatNumber(value)
|
||||
}
|
||||
if (column.format && typeof value === 'string') {
|
||||
return column.format(value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
144
src/composables/useCopySystemInfo.test.ts
Normal file
144
src/composables/useCopySystemInfo.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
|
||||
const mockCopyToClipboard = vi.fn()
|
||||
const distributionFlags = vi.hoisted(() => ({ isCloud: false }))
|
||||
|
||||
vi.mock('@/composables/useCopyToClipboard', () => ({
|
||||
useCopyToClipboard: () => ({ copyToClipboard: mockCopyToClipboard })
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => distributionFlags)
|
||||
|
||||
const localStats: SystemStats = {
|
||||
system: {
|
||||
os: 'Linux',
|
||||
python_version: '3.11.5',
|
||||
embedded_python: false,
|
||||
comfyui_version: 'v0.3.0',
|
||||
pytorch_version: '2.1.0',
|
||||
argv: ['main.py', '--cpu'],
|
||||
ram_total: 16_000_000_000,
|
||||
ram_free: 8_000_000_000,
|
||||
installed_templates_version: '1.0.0'
|
||||
},
|
||||
devices: [
|
||||
{
|
||||
name: 'cpu',
|
||||
type: 'cpu',
|
||||
index: 0,
|
||||
vram_total: 0,
|
||||
vram_free: 0,
|
||||
torch_vram_total: 0,
|
||||
torch_vram_free: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const fullCommitHash = 'a'.repeat(40)
|
||||
|
||||
const cloudStats: SystemStats = {
|
||||
system: {
|
||||
os: 'Linux',
|
||||
python_version: '3.11.5',
|
||||
embedded_python: false,
|
||||
comfyui_version: fullCommitHash,
|
||||
pytorch_version: '2.1.0',
|
||||
argv: [],
|
||||
ram_total: 0,
|
||||
ram_free: 0,
|
||||
cloud_version: '1.2.3',
|
||||
comfyui_frontend_version: fullCommitHash,
|
||||
workflow_templates_version: '5.0.0'
|
||||
},
|
||||
devices: []
|
||||
}
|
||||
|
||||
async function loadComposable(isCloud: boolean) {
|
||||
distributionFlags.isCloud = isCloud
|
||||
vi.resetModules()
|
||||
const mod = await import('./useCopySystemInfo')
|
||||
return mod.useCopySystemInfo
|
||||
}
|
||||
|
||||
describe('useCopySystemInfo', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
distributionFlags.isCloud = false
|
||||
})
|
||||
|
||||
describe('local distribution', () => {
|
||||
it('formats system info as markdown and copies to clipboard', async () => {
|
||||
const useCopySystemInfo = await loadComposable(false)
|
||||
const { copySystemInfo } = useCopySystemInfo(localStats)
|
||||
await copySystemInfo()
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledTimes(1)
|
||||
const text = mockCopyToClipboard.mock.calls[0][0] as string
|
||||
expect(text).toContain('## System Info')
|
||||
expect(text).toContain('OS: Linux')
|
||||
expect(text).toContain('Python Version: 3.11.5')
|
||||
expect(text).toContain('Arguments: main.py --cpu')
|
||||
expect(text).toContain('## Devices')
|
||||
expect(text).toContain('- cpu (cpu)')
|
||||
})
|
||||
|
||||
it('omits the Devices section when no devices are present', async () => {
|
||||
const useCopySystemInfo = await loadComposable(false)
|
||||
const stats: SystemStats = { ...localStats, devices: [] }
|
||||
const { copySystemInfo } = useCopySystemInfo(stats)
|
||||
await copySystemInfo()
|
||||
|
||||
const text = mockCopyToClipboard.mock.calls[0][0] as string
|
||||
expect(text).not.toContain('## Devices')
|
||||
})
|
||||
|
||||
it('reflects updates when stats are passed as a getter', async () => {
|
||||
const useCopySystemInfo = await loadComposable(false)
|
||||
const statsRef = ref<SystemStats>(localStats)
|
||||
const { copySystemInfo } = useCopySystemInfo(() => statsRef.value)
|
||||
|
||||
await copySystemInfo()
|
||||
expect(mockCopyToClipboard.mock.calls[0][0]).toContain('OS: Linux')
|
||||
|
||||
statsRef.value = {
|
||||
...localStats,
|
||||
system: { ...localStats.system, os: 'Windows' }
|
||||
}
|
||||
await copySystemInfo()
|
||||
expect(mockCopyToClipboard.mock.calls[1][0]).toContain('OS: Windows')
|
||||
})
|
||||
|
||||
it('re-reads stats on each call when given a plain mutable object', async () => {
|
||||
const useCopySystemInfo = await loadComposable(false)
|
||||
const stats: SystemStats = JSON.parse(JSON.stringify(localStats))
|
||||
const { copySystemInfo } = useCopySystemInfo(stats)
|
||||
|
||||
await copySystemInfo()
|
||||
expect(mockCopyToClipboard.mock.calls[0][0]).toContain('OS: Linux')
|
||||
|
||||
stats.system.os = 'Windows'
|
||||
await copySystemInfo()
|
||||
expect(mockCopyToClipboard.mock.calls[1][0]).toContain('OS: Windows')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cloud distribution', () => {
|
||||
it('formats cloud-specific columns and formats commit hashes', async () => {
|
||||
const useCopySystemInfo = await loadComposable(true)
|
||||
const { copySystemInfo } = useCopySystemInfo(cloudStats)
|
||||
await copySystemInfo()
|
||||
|
||||
const text = mockCopyToClipboard.mock.calls[0][0] as string
|
||||
const truncated = fullCommitHash.slice(0, 7)
|
||||
expect(text).toContain('## System Info')
|
||||
expect(text).toContain('Cloud Version: 1.2.3')
|
||||
expect(text).toContain(`ComfyUI Version: ${truncated}`)
|
||||
expect(text).toContain('Templates Version: 5.0.0')
|
||||
expect(text).not.toContain('OS: Linux')
|
||||
expect(text).not.toContain('Python Version')
|
||||
})
|
||||
})
|
||||
})
|
||||
45
src/composables/useCopySystemInfo.ts
Normal file
45
src/composables/useCopySystemInfo.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import {
|
||||
getColumnDisplayValue,
|
||||
getSystemStatsColumns
|
||||
} from '@/components/common/systemStatsColumns'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
function formatSystemInfoText(stats: SystemStats): string {
|
||||
const lines: string[] = ['## System Info']
|
||||
|
||||
for (const col of getSystemStatsColumns()) {
|
||||
const display = getColumnDisplayValue(stats, col)
|
||||
if (display !== undefined && display !== '') {
|
||||
lines.push(`${col.header}: ${display}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.devices.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('## Devices')
|
||||
for (const device of stats.devices) {
|
||||
lines.push(`- ${device.name} (${device.type})`)
|
||||
lines.push(` VRAM Total: ${formatSize(device.vram_total)}`)
|
||||
lines.push(` VRAM Free: ${formatSize(device.vram_free)}`)
|
||||
lines.push(` Torch VRAM Total: ${formatSize(device.torch_vram_total)}`)
|
||||
lines.push(` Torch VRAM Free: ${formatSize(device.torch_vram_free)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function useCopySystemInfo(stats: MaybeRefOrGetter<SystemStats>) {
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
|
||||
function copySystemInfo() {
|
||||
return copyToClipboard(formatSystemInfoText(toValue(stats)))
|
||||
}
|
||||
|
||||
return { copySystemInfo }
|
||||
}
|
||||
Reference in New Issue
Block a user