[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:
Chenlei Hu
2024-11-24 15:14:05 -08:00
committed by GitHub
parent c61ed4da37
commit e01c8f06c7
6 changed files with 238 additions and 99 deletions

View File

@@ -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 () => {

View File

@@ -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>

View File

@@ -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.'
},

View 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
}
}

View File

@@ -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
}
})

View File

@@ -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', () => {