mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
[Electron] Show server launch args in server config panel (#1669)
* Move revertChanges * Show launch args * Explicit ServerConfigValue type * nit * nit * Add tests
This commit is contained in:
@@ -38,7 +38,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
@@ -50,6 +49,7 @@ import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
@@ -65,7 +65,6 @@ const showReport = () => {
|
||||
const showSendError = isElectron()
|
||||
|
||||
const toast = useToast()
|
||||
const { copy, isSupported } = useClipboard()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -140,30 +139,9 @@ ${workflowText}
|
||||
`
|
||||
}
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const copyReportToClipboard = async () => {
|
||||
if (isSupported) {
|
||||
try {
|
||||
await copy(reportContent.value)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Report copied to clipboard',
|
||||
life: 3000
|
||||
})
|
||||
} catch (err) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to copy report'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Clipboard API not supported in your browser'
|
||||
})
|
||||
}
|
||||
await copyToClipboard(reportContent.value)
|
||||
}
|
||||
|
||||
const openNewGithubIssue = async () => {
|
||||
|
||||
@@ -1,42 +1,62 @@
|
||||
<template>
|
||||
<Message v-if="modifiedConfigs.length > 0" severity="info" pt:text="w-full">
|
||||
<p>
|
||||
{{ $t('serverConfig.modifiedConfigs') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="config in modifiedConfigs" :key="config.id">
|
||||
{{ config.name }}: {{ config.initialValue }} → {{ config.value }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
:label="$t('serverConfig.revertChanges')"
|
||||
@click="revertChanges"
|
||||
outlined
|
||||
/>
|
||||
<Button
|
||||
:label="$t('serverConfig.restart')"
|
||||
@click="restartApp"
|
||||
outlined
|
||||
severity="danger"
|
||||
/>
|
||||
</div>
|
||||
</Message>
|
||||
<div
|
||||
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
|
||||
:key="label"
|
||||
>
|
||||
<Divider v-if="i > 0" />
|
||||
<h3>{{ formatCamelCase(label) }}</h3>
|
||||
<div v-for="item in items" :key="item.name" class="flex items-center mb-4">
|
||||
<FormItem
|
||||
:item="item"
|
||||
v-model:formValue="item.value"
|
||||
:id="item.id"
|
||||
:labelClass="{
|
||||
'text-highlight': item.initialValue !== item.value
|
||||
}"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Message v-if="modifiedConfigs.length > 0" severity="info" pt:text="w-full">
|
||||
<p>
|
||||
{{ $t('serverConfig.modifiedConfigs') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="config in modifiedConfigs" :key="config.id">
|
||||
{{ config.name }}: {{ config.initialValue }} → {{ config.value }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
:label="$t('serverConfig.revertChanges')"
|
||||
@click="revertChanges"
|
||||
outlined
|
||||
/>
|
||||
<Button
|
||||
:label="$t('serverConfig.restart')"
|
||||
@click="restartApp"
|
||||
outlined
|
||||
severity="danger"
|
||||
/>
|
||||
</div>
|
||||
</Message>
|
||||
<Message v-if="commandLineArgs" severity="secondary" pt:text="w-full">
|
||||
<template #icon>
|
||||
<i-lucide:terminal class="text-xl font-bold" />
|
||||
</template>
|
||||
<div class="flex items-center justify-between">
|
||||
<p>{{ commandLineArgs }}</p>
|
||||
<Button
|
||||
icon="pi pi-clipboard"
|
||||
@click="copyCommandLineArgs"
|
||||
severity="secondary"
|
||||
text
|
||||
/>
|
||||
</div>
|
||||
</Message>
|
||||
<div
|
||||
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
|
||||
:key="label"
|
||||
>
|
||||
<Divider v-if="i > 0" />
|
||||
<h3>{{ formatCamelCase(label) }}</h3>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.name"
|
||||
class="flex items-center mb-4"
|
||||
>
|
||||
<FormItem
|
||||
:item="item"
|
||||
v-model:formValue="item.value"
|
||||
:id="item.id"
|
||||
:labelClass="{
|
||||
'text-highlight': item.initialValue !== item.value
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,6 +72,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { watch } from 'vue'
|
||||
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const serverConfigStore = useServerConfigStore()
|
||||
@@ -59,13 +80,12 @@ const {
|
||||
serverConfigsByCategory,
|
||||
serverConfigValues,
|
||||
launchArgs,
|
||||
commandLineArgs,
|
||||
modifiedConfigs
|
||||
} = storeToRefs(serverConfigStore)
|
||||
|
||||
const revertChanges = () => {
|
||||
for (const config of modifiedConfigs.value) {
|
||||
config.value = config.initialValue
|
||||
}
|
||||
serverConfigStore.revertChanges()
|
||||
}
|
||||
|
||||
const restartApp = () => {
|
||||
@@ -79,4 +99,9 @@ watch(launchArgs, (newVal) => {
|
||||
watch(serverConfigValues, (newVal) => {
|
||||
settingStore.set('Comfy.Server.ServerConfigValues', newVal)
|
||||
})
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const copyCommandLineArgs = async () => {
|
||||
await copyToClipboard(commandLineArgs.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,15 +10,17 @@ import {
|
||||
VramManagement
|
||||
} from '@/types/serverArgs'
|
||||
|
||||
export type ServerConfigValue = string | number | true | null | undefined
|
||||
|
||||
export interface ServerConfig<T> extends FormItem {
|
||||
id: string
|
||||
defaultValue: T
|
||||
category?: string[]
|
||||
// Override the default value getter with a custom function.
|
||||
getValue?: (value: T) => Record<string, any>
|
||||
getValue?: (value: T) => Record<string, ServerConfigValue>
|
||||
}
|
||||
|
||||
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<ServerConfigValue>[] = [
|
||||
// We only need these settings in the web version. Desktop app manages them already.
|
||||
{
|
||||
id: 'listen',
|
||||
@@ -43,21 +45,21 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'TLS Key File: Path to TLS key file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
id: 'tls-certfile',
|
||||
name: 'TLS Certificate File: Path to TLS certificate file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
id: 'enable-cors-header',
|
||||
name: 'Enable CORS header: Use "*" for all origins or specify domain',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
id: 'max-upload-size',
|
||||
@@ -97,7 +99,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'CUDA device index to use',
|
||||
category: ['CUDA'],
|
||||
type: 'number',
|
||||
defaultValue: undefined
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
id: 'cuda-malloc',
|
||||
@@ -253,7 +255,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'DirectML device index',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: undefined
|
||||
defaultValue: null
|
||||
},
|
||||
{
|
||||
id: 'disable-ipex-optimize',
|
||||
@@ -295,10 +297,10 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cache-lru',
|
||||
name: 'Use LRU caching with a maximum of N node results cached. (0 to disable).',
|
||||
name: 'Use LRU caching with a maximum of N node results cached.',
|
||||
category: ['Cache'],
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
defaultValue: null,
|
||||
tooltip: 'May use more RAM/VRAM.'
|
||||
},
|
||||
|
||||
@@ -366,7 +368,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
name: 'Reserved VRAM (GB)',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: undefined,
|
||||
defaultValue: null,
|
||||
tooltip:
|
||||
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.'
|
||||
},
|
||||
|
||||
37
src/hooks/clipboardHooks.ts
Normal file
37
src/hooks/clipboardHooks.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
export function useCopyToClipboard() {
|
||||
const { copy, isSupported } = useClipboard()
|
||||
const toast = useToast()
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
if (isSupported) {
|
||||
try {
|
||||
await copy(text)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Copied to clipboard',
|
||||
life: 3000
|
||||
})
|
||||
} catch (err) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to copy report'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Clipboard API not supported in your browser'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
copyToClipboard
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerConfig } from '@/constants/serverConfig'
|
||||
import { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -14,18 +14,26 @@ export type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
}
|
||||
|
||||
export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
const serverConfigById = ref<Record<string, ServerConfigWithValue<any>>>({})
|
||||
const serverConfigById = ref<
|
||||
Record<string, ServerConfigWithValue<ServerConfigValue>>
|
||||
>({})
|
||||
const serverConfigs = computed(() => {
|
||||
return Object.values(serverConfigById.value)
|
||||
})
|
||||
const modifiedConfigs = computed<ServerConfigWithValue<any>[]>(() => {
|
||||
return serverConfigs.value.filter((config) => {
|
||||
return config.initialValue !== config.value
|
||||
})
|
||||
})
|
||||
|
||||
const modifiedConfigs = computed<ServerConfigWithValue<ServerConfigValue>[]>(
|
||||
() => {
|
||||
return serverConfigs.value.filter((config) => {
|
||||
return config.initialValue !== config.value
|
||||
})
|
||||
}
|
||||
)
|
||||
const revertChanges = () => {
|
||||
for (const config of modifiedConfigs.value) {
|
||||
config.value = config.initialValue
|
||||
}
|
||||
}
|
||||
const serverConfigsByCategory = computed<
|
||||
Record<string, ServerConfigWithValue<any>[]>
|
||||
Record<string, ServerConfigWithValue<ServerConfigValue>[]>
|
||||
>(() => {
|
||||
return serverConfigs.value.reduce(
|
||||
(acc, config) => {
|
||||
@@ -34,15 +42,17 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
acc[category].push(config)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ServerConfigWithValue<any>[]>
|
||||
{} as Record<string, ServerConfigWithValue<ServerConfigValue>[]>
|
||||
)
|
||||
})
|
||||
const serverConfigValues = computed<Record<string, any>>(() => {
|
||||
const serverConfigValues = computed<Record<string, ServerConfigValue>>(() => {
|
||||
return Object.fromEntries(
|
||||
serverConfigs.value.map((config) => {
|
||||
return [
|
||||
config.id,
|
||||
config.value === config.defaultValue || !config.value
|
||||
config.value === config.defaultValue ||
|
||||
config.value === null ||
|
||||
config.value === undefined
|
||||
? undefined
|
||||
: config.value
|
||||
]
|
||||
@@ -50,10 +60,18 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
)
|
||||
})
|
||||
const launchArgs = computed<Record<string, string>>(() => {
|
||||
return Object.assign(
|
||||
const args: Record<
|
||||
string,
|
||||
Omit<ServerConfigValue, 'undefined' | 'null'>
|
||||
> = Object.assign(
|
||||
{},
|
||||
...serverConfigs.value.map((config) => {
|
||||
if (config.value === config.defaultValue || !config.value) {
|
||||
// Filter out configs that have the default value or undefined | null value
|
||||
if (
|
||||
config.value === config.defaultValue ||
|
||||
config.value === null ||
|
||||
config.value === undefined
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
return config.getValue
|
||||
@@ -61,11 +79,29 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
: { [config.id]: config.value }
|
||||
})
|
||||
)
|
||||
|
||||
// Convert true to empty string
|
||||
// Convert number to string
|
||||
return Object.fromEntries(
|
||||
Object.entries(args).map(([key, value]) => {
|
||||
if (value === true) {
|
||||
return [key, '']
|
||||
}
|
||||
return [key, value.toString()]
|
||||
})
|
||||
) as Record<string, string>
|
||||
})
|
||||
const commandLineArgs = computed<string>(() => {
|
||||
return Object.entries(launchArgs.value)
|
||||
.map(([key, value]) => [`--${key}`, value])
|
||||
.flat()
|
||||
.filter((arg: string) => arg !== '')
|
||||
.join(' ')
|
||||
})
|
||||
|
||||
function loadServerConfig(
|
||||
configs: ServerConfig<any>[],
|
||||
values: Record<string, any>
|
||||
configs: ServerConfig<ServerConfigValue>[],
|
||||
values: Record<string, ServerConfigValue>
|
||||
) {
|
||||
for (const config of configs) {
|
||||
const value = values[config.id] ?? config.defaultValue
|
||||
@@ -84,6 +120,8 @@ export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
serverConfigsByCategory,
|
||||
serverConfigValues,
|
||||
launchArgs,
|
||||
commandLineArgs,
|
||||
revertChanges,
|
||||
loadServerConfig
|
||||
}
|
||||
})
|
||||
|
||||
@@ -170,21 +170,80 @@ describe('useServerConfigStore', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{ ...dummyFormItem, id: 'test.config1', defaultValue: 'default1' },
|
||||
{ ...dummyFormItem, id: 'test.config2', defaultValue: 'default2' },
|
||||
{ ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' }
|
||||
{ ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' },
|
||||
{ ...dummyFormItem, id: 'test.config4', defaultValue: null }
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': undefined,
|
||||
'test.config2': null,
|
||||
'test.config3': ''
|
||||
'test.config3': '',
|
||||
'test.config4': 0
|
||||
})
|
||||
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(0)
|
||||
expect(Object.keys(store.serverConfigValues)).toEqual([
|
||||
'test.config1',
|
||||
'test.config2',
|
||||
'test.config3'
|
||||
expect(Object.keys(store.launchArgs)).toEqual([
|
||||
'test.config3',
|
||||
'test.config4'
|
||||
])
|
||||
expect(Object.values(store.launchArgs)).toEqual(['', '0'])
|
||||
expect(store.serverConfigById['test.config3'].value).toBe('')
|
||||
expect(store.serverConfigById['test.config4'].value).toBe(0)
|
||||
expect(Object.values(store.serverConfigValues)).toEqual([
|
||||
undefined,
|
||||
undefined,
|
||||
'',
|
||||
0
|
||||
])
|
||||
})
|
||||
|
||||
it('should convert true to empty string in launch arguments', () => {
|
||||
store.loadServerConfig(
|
||||
[
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 0
|
||||
}
|
||||
],
|
||||
{
|
||||
'test.config1': true
|
||||
}
|
||||
)
|
||||
expect(store.launchArgs['test.config1']).toBe('')
|
||||
expect(store.commandLineArgs).toBe('--test.config1')
|
||||
})
|
||||
|
||||
it('should convert number to string in launch arguments', () => {
|
||||
store.loadServerConfig(
|
||||
[
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 1
|
||||
}
|
||||
],
|
||||
{
|
||||
'test.config1': 123
|
||||
}
|
||||
)
|
||||
expect(store.launchArgs['test.config1']).toBe('123')
|
||||
expect(store.commandLineArgs).toBe('--test.config1 123')
|
||||
})
|
||||
|
||||
it('should drop nullish values in launch arguments', () => {
|
||||
store.loadServerConfig(
|
||||
[
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 1
|
||||
}
|
||||
],
|
||||
{
|
||||
'test.config1': null
|
||||
}
|
||||
)
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should track modified configs', () => {
|
||||
|
||||
Reference in New Issue
Block a user