mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-06 09:04:27 +00:00
Add EnvironmentVariablesInput component and integrate into InstanceSettingsCard
This commit is contained in:
144
webui/src/components/form/EnvironmentVariablesInput.tsx
Normal file
144
webui/src/components/form/EnvironmentVariablesInput.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { X, Plus } from 'lucide-react'
|
||||||
|
|
||||||
|
interface EnvironmentVariablesInputProps {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: Record<string, string> | undefined
|
||||||
|
onChange: (value: Record<string, string> | undefined) => void
|
||||||
|
description?: string
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnvVar {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentVariablesInput: React.FC<EnvironmentVariablesInputProps> = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
description,
|
||||||
|
disabled = false,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
// Convert the value object to an array of key-value pairs for editing
|
||||||
|
const envVarsFromValue = value
|
||||||
|
? Object.entries(value).map(([key, val]) => ({ key, value: val }))
|
||||||
|
: []
|
||||||
|
|
||||||
|
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
||||||
|
envVarsFromValue.length > 0 ? envVarsFromValue : [{ key: '', value: '' }]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update parent component when env vars change
|
||||||
|
const updateParent = (newEnvVars: EnvVar[]) => {
|
||||||
|
// Filter out empty entries
|
||||||
|
const validVars = newEnvVars.filter(env => env.key.trim() !== '' && env.value.trim() !== '')
|
||||||
|
|
||||||
|
if (validVars.length === 0) {
|
||||||
|
onChange(undefined)
|
||||||
|
} else {
|
||||||
|
const envObject = validVars.reduce((acc, env) => {
|
||||||
|
acc[env.key.trim()] = env.value.trim()
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
onChange(envObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyChange = (index: number, newKey: string) => {
|
||||||
|
const newEnvVars = [...envVars]
|
||||||
|
newEnvVars[index].key = newKey
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
|
updateParent(newEnvVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValueChange = (index: number, newValue: string) => {
|
||||||
|
const newEnvVars = [...envVars]
|
||||||
|
newEnvVars[index].value = newValue
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
|
updateParent(newEnvVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEnvVar = () => {
|
||||||
|
const newEnvVars = [...envVars, { key: '', value: '' }]
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeEnvVar = (index: number) => {
|
||||||
|
if (envVars.length === 1) {
|
||||||
|
// Reset to empty if it's the last one
|
||||||
|
const newEnvVars = [{ key: '', value: '' }]
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
|
updateParent(newEnvVars)
|
||||||
|
} else {
|
||||||
|
const newEnvVars = envVars.filter((_, i) => i !== index)
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
|
updateParent(newEnvVars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`grid gap-2 ${className || ''}`}>
|
||||||
|
<Label htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{envVars.map((envVar, index) => (
|
||||||
|
<div key={index} className="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
placeholder="Variable name"
|
||||||
|
value={envVar.key}
|
||||||
|
onChange={(e) => handleKeyChange(index, e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Variable value"
|
||||||
|
value={envVar.value}
|
||||||
|
onChange={(e) => handleValueChange(index, e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeEnvVar(index)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addEnvVar}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Variable
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Environment variables that will be passed to the backend process
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnvironmentVariablesInput
|
||||||
@@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration'
|
import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration'
|
||||||
import NumberInput from '@/components/form/NumberInput'
|
import NumberInput from '@/components/form/NumberInput'
|
||||||
import CheckboxInput from '@/components/form/CheckboxInput'
|
import CheckboxInput from '@/components/form/CheckboxInput'
|
||||||
|
import EnvironmentVariablesInput from '@/components/form/EnvironmentVariablesInput'
|
||||||
|
|
||||||
interface InstanceSettingsCardProps {
|
interface InstanceSettingsCardProps {
|
||||||
instanceName: string
|
instanceName: string
|
||||||
@@ -75,6 +76,14 @@ const InstanceSettingsCard: React.FC<InstanceSettingsCardProps> = ({
|
|||||||
onChange={(value) => onChange('on_demand_start', value)}
|
onChange={(value) => onChange('on_demand_start', value)}
|
||||||
description="Start instance only when needed"
|
description="Start instance only when needed"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<EnvironmentVariablesInput
|
||||||
|
id="environment"
|
||||||
|
label="Environment Variables"
|
||||||
|
value={formData.environment}
|
||||||
|
onChange={(value) => onChange('environment', value)}
|
||||||
|
description="Custom environment variables for the instance"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
type CreateInstanceOptions,
|
|
||||||
type LlamaCppBackendOptions,
|
type LlamaCppBackendOptions,
|
||||||
type MlxBackendOptions,
|
type MlxBackendOptions,
|
||||||
type VllmBackendOptions,
|
type VllmBackendOptions,
|
||||||
LlamaCppBackendOptionsSchema,
|
LlamaCppBackendOptionsSchema,
|
||||||
MlxBackendOptionsSchema,
|
MlxBackendOptionsSchema,
|
||||||
VllmBackendOptionsSchema,
|
VllmBackendOptionsSchema,
|
||||||
getAllFieldKeys,
|
|
||||||
getAllLlamaCppFieldKeys,
|
getAllLlamaCppFieldKeys,
|
||||||
getAllMlxFieldKeys,
|
getAllMlxFieldKeys,
|
||||||
getAllVllmFieldKeys,
|
getAllVllmFieldKeys,
|
||||||
@@ -15,41 +13,6 @@ import {
|
|||||||
getVllmFieldType
|
getVllmFieldType
|
||||||
} from '@/schemas/instanceOptions'
|
} from '@/schemas/instanceOptions'
|
||||||
|
|
||||||
// Instance-level basic fields (not backend-specific)
|
|
||||||
export const basicFieldsConfig: Record<string, {
|
|
||||||
label: string
|
|
||||||
description?: string
|
|
||||||
placeholder?: string
|
|
||||||
}> = {
|
|
||||||
auto_restart: {
|
|
||||||
label: 'Auto Restart',
|
|
||||||
description: 'Automatically restart the instance on failure'
|
|
||||||
},
|
|
||||||
max_restarts: {
|
|
||||||
label: 'Max Restarts',
|
|
||||||
placeholder: '3',
|
|
||||||
description: 'Maximum number of restart attempts (0 = unlimited)'
|
|
||||||
},
|
|
||||||
restart_delay: {
|
|
||||||
label: 'Restart Delay (seconds)',
|
|
||||||
placeholder: '5',
|
|
||||||
description: 'Delay in seconds before attempting restart'
|
|
||||||
},
|
|
||||||
idle_timeout: {
|
|
||||||
label: 'Idle Timeout (minutes)',
|
|
||||||
placeholder: '60',
|
|
||||||
description: 'Time in minutes before instance is considered idle and stopped'
|
|
||||||
},
|
|
||||||
on_demand_start: {
|
|
||||||
label: 'On-Demand Start',
|
|
||||||
description: 'Start instance upon receiving OpenAI-compatible API request'
|
|
||||||
},
|
|
||||||
backend_type: {
|
|
||||||
label: 'Backend Type',
|
|
||||||
description: 'Type of backend to use for this instance'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LlamaCpp backend-specific basic fields
|
// LlamaCpp backend-specific basic fields
|
||||||
const basicLlamaCppFieldsConfig: Record<string, {
|
const basicLlamaCppFieldsConfig: Record<string, {
|
||||||
label: string
|
label: string
|
||||||
@@ -152,18 +115,6 @@ const backendFieldGetters = {
|
|||||||
llama_cpp: getAllLlamaCppFieldKeys,
|
llama_cpp: getAllLlamaCppFieldKeys,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
function isBasicField(key: keyof CreateInstanceOptions): boolean {
|
|
||||||
return key in basicFieldsConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBasicFields(): (keyof CreateInstanceOptions)[] {
|
|
||||||
return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdvancedFields(): (keyof CreateInstanceOptions)[] {
|
|
||||||
return getAllFieldKeys().filter(key => !isBasicField(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBasicBackendFields(backendType?: string): string[] {
|
export function getBasicBackendFields(backendType?: string): string[] {
|
||||||
const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldConfigs
|
const normalizedType = (backendType || 'llama_cpp') as keyof typeof backendFieldConfigs
|
||||||
const config = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig
|
const config = backendFieldConfigs[normalizedType] || basicLlamaCppFieldsConfig
|
||||||
@@ -222,5 +173,3 @@ export function getBackendFieldType(key: string): 'text' | 'number' | 'boolean'
|
|||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export the Zod-based functions
|
|
||||||
export { getFieldType } from '@/schemas/instanceOptions'
|
|
||||||
@@ -33,6 +33,9 @@ export const CreateInstanceOptionsSchema = z.object({
|
|||||||
idle_timeout: z.number().optional(),
|
idle_timeout: z.number().optional(),
|
||||||
on_demand_start: z.boolean().optional(),
|
on_demand_start: z.boolean().optional(),
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
environment: z.record(z.string(), z.string()).optional(),
|
||||||
|
|
||||||
// Backend configuration
|
// Backend configuration
|
||||||
backend_type: z.enum([BackendType.LLAMA_CPP, BackendType.MLX_LM, BackendType.VLLM]).optional(),
|
backend_type: z.enum([BackendType.LLAMA_CPP, BackendType.MLX_LM, BackendType.VLLM]).optional(),
|
||||||
backend_options: BackendOptionsSchema.optional(),
|
backend_options: BackendOptionsSchema.optional(),
|
||||||
@@ -75,5 +78,6 @@ export function getFieldType(key: keyof CreateInstanceOptions): 'text' | 'number
|
|||||||
if (innerSchema instanceof z.ZodNumber) return 'number'
|
if (innerSchema instanceof z.ZodNumber) return 'number'
|
||||||
if (innerSchema instanceof z.ZodArray) return 'array'
|
if (innerSchema instanceof z.ZodArray) return 'array'
|
||||||
if (innerSchema instanceof z.ZodObject) return 'object'
|
if (innerSchema instanceof z.ZodObject) return 'object'
|
||||||
|
if (innerSchema instanceof z.ZodRecord) return 'object' // Handle ZodRecord as object
|
||||||
return 'text' // ZodString and others default to text
|
return 'text' // ZodString and others default to text
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user