mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-06 09:04:27 +00:00
Merge pull request #39 from lordmathis/feat/instance-dialog
feat: Redesign create/edit instance dialog
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,13 +9,9 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { BackendType, type CreateInstanceOptions, type Instance } from "@/types/instance";
|
import { BackendType, type CreateInstanceOptions, type Instance } from "@/types/instance";
|
||||||
import { getAdvancedFields, getAdvancedBackendFields } from "@/lib/zodFormUtils";
|
|
||||||
import { ChevronDown, ChevronRight, Terminal } from "lucide-react";
|
|
||||||
import ParseCommandDialog from "@/components/ParseCommandDialog";
|
import ParseCommandDialog from "@/components/ParseCommandDialog";
|
||||||
import AutoRestartConfiguration from "@/components/instance/AutoRestartConfiguration";
|
import InstanceSettingsCard from "@/components/instance/InstanceSettingsCard";
|
||||||
import BasicInstanceFields from "@/components/instance/BasicInstanceFields";
|
import BackendConfigurationCard from "@/components/instance/BackendConfigurationCard";
|
||||||
import BackendConfiguration from "@/components/instance/BackendConfiguration";
|
|
||||||
import AdvancedInstanceFields from "@/components/instance/AdvancedInstanceFields";
|
|
||||||
|
|
||||||
interface InstanceDialogProps {
|
interface InstanceDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -36,13 +30,9 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
|||||||
|
|
||||||
const [instanceName, setInstanceName] = useState("");
|
const [instanceName, setInstanceName] = useState("");
|
||||||
const [formData, setFormData] = useState<CreateInstanceOptions>({});
|
const [formData, setFormData] = useState<CreateInstanceOptions>({});
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
||||||
const [nameError, setNameError] = useState("");
|
const [nameError, setNameError] = useState("");
|
||||||
const [showParseDialog, setShowParseDialog] = useState(false);
|
const [showParseDialog, setShowParseDialog] = useState(false);
|
||||||
|
|
||||||
// Get field lists dynamically from the type
|
|
||||||
const advancedFields = getAdvancedFields();
|
|
||||||
const advancedBackendFields = getAdvancedBackendFields(formData.backend_type);
|
|
||||||
|
|
||||||
// Reset form when dialog opens/closes or when instance changes
|
// Reset form when dialog opens/closes or when instance changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,7 +50,6 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
|||||||
backend_options: {},
|
backend_options: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setShowAdvanced(false); // Always start with basic view
|
|
||||||
setNameError(""); // Reset any name errors
|
setNameError(""); // Reset any name errors
|
||||||
}
|
}
|
||||||
}, [open, instance]);
|
}, [open, instance]);
|
||||||
@@ -151,9 +140,6 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAdvanced = () => {
|
|
||||||
setShowAdvanced(!showAdvanced);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommandParsed = (parsedOptions: CreateInstanceOptions) => {
|
const handleCommandParsed = (parsedOptions: CreateInstanceOptions) => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
@@ -189,91 +175,25 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="grid gap-6 py-4">
|
<div className="space-y-6 py-4">
|
||||||
{/* Instance Name - Special handling since it's not in CreateInstanceOptions */}
|
{/* Instance Settings Card */}
|
||||||
<div className="grid gap-2">
|
<InstanceSettingsCard
|
||||||
<Label htmlFor="name">
|
instanceName={instanceName}
|
||||||
Instance Name <span className="text-red-500">*</span>
|
nameError={nameError}
|
||||||
</Label>
|
isEditing={isEditing}
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={instanceName}
|
|
||||||
onChange={(e) => handleNameChange(e.target.value)}
|
|
||||||
placeholder="my-instance"
|
|
||||||
disabled={isEditing} // Don't allow name changes when editing
|
|
||||||
className={nameError ? "border-red-500" : ""}
|
|
||||||
/>
|
|
||||||
{nameError && <p className="text-sm text-red-500">{nameError}</p>}
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Unique identifier for the instance
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Auto Restart Configuration Section */}
|
|
||||||
<AutoRestartConfiguration
|
|
||||||
formData={formData}
|
formData={formData}
|
||||||
|
onNameChange={handleNameChange}
|
||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Basic Fields */}
|
{/* Backend Configuration Card */}
|
||||||
<BasicInstanceFields
|
<BackendConfigurationCard
|
||||||
formData={formData}
|
|
||||||
onChange={handleFieldChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Backend Configuration Section */}
|
|
||||||
<BackendConfiguration
|
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onBackendFieldChange={handleBackendFieldChange}
|
onBackendFieldChange={handleBackendFieldChange}
|
||||||
showAdvanced={showAdvanced}
|
onChange={handleFieldChange}
|
||||||
|
onParseCommand={() => setShowParseDialog(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Advanced Fields Toggle */}
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setShowParseDialog(true)}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Terminal className="h-4 w-4" />
|
|
||||||
Parse Command
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={toggleAdvanced}
|
|
||||||
className="flex items-center gap-2 p-0 h-auto font-medium"
|
|
||||||
>
|
|
||||||
{showAdvanced ? (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Advanced Configuration
|
|
||||||
<span className="text-muted-foreground text-sm font-normal">
|
|
||||||
(
|
|
||||||
{
|
|
||||||
advancedFields.filter(
|
|
||||||
(f) =>
|
|
||||||
!["max_restarts", "restart_delay", "backend_options"].includes(f as string)
|
|
||||||
).length + advancedBackendFields.length
|
|
||||||
}{" "}
|
|
||||||
options)
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Advanced Fields */}
|
|
||||||
{showAdvanced && (
|
|
||||||
<div className="space-y-4 pl-6 border-l-2 border-muted">
|
|
||||||
<AdvancedInstanceFields
|
|
||||||
formData={formData}
|
|
||||||
onChange={handleFieldChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -299,6 +219,7 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
|
|||||||
open={showParseDialog}
|
open={showParseDialog}
|
||||||
onOpenChange={setShowParseDialog}
|
onOpenChange={setShowParseDialog}
|
||||||
onParsed={handleCommandParsed}
|
onParsed={handleCommandParsed}
|
||||||
|
backendType={formData.backend_type || BackendType.LLAMA_CPP}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,15 +17,16 @@ interface ParseCommandDialogProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onParsed: (options: CreateInstanceOptions) => void;
|
onParsed: (options: CreateInstanceOptions) => void;
|
||||||
|
backendType: BackendTypeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseCommandDialog: React.FC<ParseCommandDialogProps> = ({
|
const ParseCommandDialog: React.FC<ParseCommandDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onParsed,
|
onParsed,
|
||||||
|
backendType,
|
||||||
}) => {
|
}) => {
|
||||||
const [command, setCommand] = useState('');
|
const [command, setCommand] = useState('');
|
||||||
const [backendType, setBackendType] = useState<BackendTypeValue>(BackendType.LLAMA_CPP);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -75,7 +76,6 @@ const ParseCommandDialog: React.FC<ParseCommandDialogProps> = ({
|
|||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setCommand('');
|
setCommand('');
|
||||||
setBackendType(BackendType.LLAMA_CPP);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
onOpenChange(open);
|
onOpenChange(open);
|
||||||
@@ -103,17 +103,13 @@ const ParseCommandDialog: React.FC<ParseCommandDialogProps> = ({
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="backend-type">Backend Type</Label>
|
<Label className="text-sm font-medium">Backend Type:
|
||||||
<select
|
<span className="font-normal text-muted-foreground">
|
||||||
id="backend-type"
|
{backendType === BackendType.LLAMA_CPP && 'Llama Server (llama_cpp)'}
|
||||||
value={backendType}
|
{backendType === BackendType.MLX_LM && 'MLX LM (mlx_lm)'}
|
||||||
onChange={(e) => setBackendType(e.target.value as BackendTypeValue)}
|
{backendType === BackendType.VLLM && 'vLLM (vllm)'}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
</span>
|
||||||
>
|
</Label>
|
||||||
<option value={BackendType.LLAMA_CPP}>Llama Server</option>
|
|
||||||
<option value={BackendType.MLX_LM}>MLX LM</option>
|
|
||||||
<option value={BackendType.VLLM}>vLLM</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -280,29 +280,6 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Advanced Fields Toggle', () => {
|
|
||||||
it('shows advanced fields when toggle clicked', async () => {
|
|
||||||
const user = userEvent.setup()
|
|
||||||
|
|
||||||
render(
|
|
||||||
<InstanceDialog
|
|
||||||
open={true}
|
|
||||||
onOpenChange={mockOnOpenChange}
|
|
||||||
onSave={mockOnSave}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Advanced fields should be hidden initially
|
|
||||||
expect(screen.queryByText(/Advanced Configuration/)).toBeInTheDocument()
|
|
||||||
|
|
||||||
// Click to expand
|
|
||||||
await user.click(screen.getByText(/Advanced Configuration/))
|
|
||||||
|
|
||||||
// Should show more configuration options
|
|
||||||
// Note: Specific fields depend on zodFormUtils configuration
|
|
||||||
// We're testing the toggle behavior, not specific fields
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Form Data Handling', () => {
|
describe('Form Data Handling', () => {
|
||||||
it('cleans up undefined values before submission', async () => {
|
it('cleans up undefined values before submission', async () => {
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import type { CreateInstanceOptions } from '@/types/instance'
|
|
||||||
import { getAdvancedFields, basicFieldsConfig } from '@/lib/zodFormUtils'
|
|
||||||
import { getFieldType } from '@/schemas/instanceOptions'
|
|
||||||
import TextInput from '@/components/form/TextInput'
|
|
||||||
import NumberInput from '@/components/form/NumberInput'
|
|
||||||
import CheckboxInput from '@/components/form/CheckboxInput'
|
|
||||||
import ArrayInput from '@/components/form/ArrayInput'
|
|
||||||
|
|
||||||
interface AdvancedInstanceFieldsProps {
|
|
||||||
formData: CreateInstanceOptions
|
|
||||||
onChange: (key: keyof CreateInstanceOptions, value: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const AdvancedInstanceFields: React.FC<AdvancedInstanceFieldsProps> = ({
|
|
||||||
formData,
|
|
||||||
onChange
|
|
||||||
}) => {
|
|
||||||
const advancedFields = getAdvancedFields()
|
|
||||||
|
|
||||||
const renderField = (fieldKey: keyof CreateInstanceOptions) => {
|
|
||||||
const config = basicFieldsConfig[fieldKey as string] || { label: fieldKey }
|
|
||||||
const fieldType = getFieldType(fieldKey)
|
|
||||||
|
|
||||||
switch (fieldType) {
|
|
||||||
case 'boolean':
|
|
||||||
return (
|
|
||||||
<CheckboxInput
|
|
||||||
key={fieldKey}
|
|
||||||
id={fieldKey}
|
|
||||||
label={config.label}
|
|
||||||
value={formData[fieldKey] as boolean | undefined}
|
|
||||||
onChange={(value) => onChange(fieldKey, value)}
|
|
||||||
description={config.description}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'number':
|
|
||||||
return (
|
|
||||||
<NumberInput
|
|
||||||
key={fieldKey}
|
|
||||||
id={fieldKey}
|
|
||||||
label={config.label}
|
|
||||||
value={formData[fieldKey] as number | undefined}
|
|
||||||
onChange={(value) => onChange(fieldKey, value)}
|
|
||||||
placeholder={config.placeholder}
|
|
||||||
description={config.description}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'array':
|
|
||||||
return (
|
|
||||||
<ArrayInput
|
|
||||||
key={fieldKey}
|
|
||||||
id={fieldKey}
|
|
||||||
label={config.label}
|
|
||||||
value={formData[fieldKey] as string[] | undefined}
|
|
||||||
onChange={(value) => onChange(fieldKey, value)}
|
|
||||||
placeholder={config.placeholder}
|
|
||||||
description={config.description}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
key={fieldKey}
|
|
||||||
id={fieldKey}
|
|
||||||
label={config.label}
|
|
||||||
value={formData[fieldKey] as string | number | undefined}
|
|
||||||
onChange={(value) => onChange(fieldKey, value)}
|
|
||||||
placeholder={config.placeholder}
|
|
||||||
description={config.description}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out restart options and backend_options (handled separately)
|
|
||||||
const fieldsToRender = advancedFields.filter(
|
|
||||||
fieldKey => !['max_restarts', 'restart_delay', 'backend_options'].includes(fieldKey as string)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (fieldsToRender.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h4 className="text-md font-medium">Advanced Instance Configuration</h4>
|
|
||||||
{fieldsToRender
|
|
||||||
.sort()
|
|
||||||
.map(renderField)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AdvancedInstanceFields
|
|
||||||
117
webui/src/components/instance/BackendConfigurationCard.tsx
Normal file
117
webui/src/components/instance/BackendConfigurationCard.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { BackendType, type CreateInstanceOptions } from '@/types/instance'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Terminal, ChevronDown, ChevronRight } from 'lucide-react'
|
||||||
|
import { getBasicBackendFields, getAdvancedBackendFields } from '@/lib/zodFormUtils'
|
||||||
|
import BackendFormField from '@/components/BackendFormField'
|
||||||
|
import SelectInput from '@/components/form/SelectInput'
|
||||||
|
|
||||||
|
interface BackendConfigurationCardProps {
|
||||||
|
formData: CreateInstanceOptions
|
||||||
|
onBackendFieldChange: (key: string, value: unknown) => void
|
||||||
|
onChange: (key: keyof CreateInstanceOptions, value: unknown) => void
|
||||||
|
onParseCommand: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackendConfigurationCard: React.FC<BackendConfigurationCardProps> = ({
|
||||||
|
formData,
|
||||||
|
onBackendFieldChange,
|
||||||
|
onChange,
|
||||||
|
onParseCommand
|
||||||
|
}) => {
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||||
|
const basicBackendFields = getBasicBackendFields(formData.backend_type)
|
||||||
|
const advancedBackendFields = getAdvancedBackendFields(formData.backend_type)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Backend Configuration</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Backend Type Selection */}
|
||||||
|
<SelectInput
|
||||||
|
id="backend_type"
|
||||||
|
label="Backend Type"
|
||||||
|
value={formData.backend_type || BackendType.LLAMA_CPP}
|
||||||
|
onChange={(value) => onChange('backend_type', value)}
|
||||||
|
options={[
|
||||||
|
{ value: BackendType.LLAMA_CPP, label: 'Llama Server (llama_cpp)' },
|
||||||
|
{ value: BackendType.MLX_LM, label: 'MLX LM (mlx_lm)' },
|
||||||
|
{ value: BackendType.VLLM, label: 'vLLM (vllm)' }
|
||||||
|
]}
|
||||||
|
description="Select the backend server type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Parse Command Section */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onParseCommand}
|
||||||
|
className="flex items-center gap-2 w-fit"
|
||||||
|
>
|
||||||
|
<Terminal className="h-4 w-4" />
|
||||||
|
Parse Command
|
||||||
|
</Button>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Import settings from your backend command
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Backend Options */}
|
||||||
|
{basicBackendFields.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-md font-medium">Basic Backend Options</h3>
|
||||||
|
{basicBackendFields.map((fieldKey) => (
|
||||||
|
<BackendFormField
|
||||||
|
key={fieldKey}
|
||||||
|
fieldKey={fieldKey}
|
||||||
|
value={(formData.backend_options as Record<string, unknown>)?.[fieldKey] as string | number | boolean | string[] | undefined}
|
||||||
|
onChange={onBackendFieldChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Advanced Backend Options */}
|
||||||
|
{advancedBackendFields.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
|
className="flex items-center gap-2 p-0 h-auto font-medium"
|
||||||
|
>
|
||||||
|
{showAdvanced ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Advanced Backend Options
|
||||||
|
<span className="text-muted-foreground text-sm font-normal">
|
||||||
|
({advancedBackendFields.length} options)
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showAdvanced && (
|
||||||
|
<div className="space-y-4 pl-6 border-l-2 border-muted">
|
||||||
|
{advancedBackendFields
|
||||||
|
.sort()
|
||||||
|
.map((fieldKey) => (
|
||||||
|
<BackendFormField
|
||||||
|
key={fieldKey}
|
||||||
|
fieldKey={fieldKey}
|
||||||
|
value={(formData.backend_options as Record<string, unknown>)?.[fieldKey] as string | number | boolean | string[] | undefined}
|
||||||
|
onChange={onBackendFieldChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BackendConfigurationCard
|
||||||
84
webui/src/components/instance/InstanceSettingsCard.tsx
Normal file
84
webui/src/components/instance/InstanceSettingsCard.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { CreateInstanceOptions } from '@/types/instance'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import AutoRestartConfiguration from '@/components/instance/AutoRestartConfiguration'
|
||||||
|
import NumberInput from '@/components/form/NumberInput'
|
||||||
|
import CheckboxInput from '@/components/form/CheckboxInput'
|
||||||
|
|
||||||
|
interface InstanceSettingsCardProps {
|
||||||
|
instanceName: string
|
||||||
|
nameError: string
|
||||||
|
isEditing: boolean
|
||||||
|
formData: CreateInstanceOptions
|
||||||
|
onNameChange: (name: string) => void
|
||||||
|
onChange: (key: keyof CreateInstanceOptions, value: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceSettingsCard: React.FC<InstanceSettingsCardProps> = ({
|
||||||
|
instanceName,
|
||||||
|
nameError,
|
||||||
|
isEditing,
|
||||||
|
formData,
|
||||||
|
onNameChange,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Instance Settings</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Instance Name */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="name">
|
||||||
|
Instance Name <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={instanceName}
|
||||||
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
|
placeholder="my-instance"
|
||||||
|
disabled={isEditing}
|
||||||
|
className={nameError ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{nameError && <p className="text-sm text-red-500">{nameError}</p>}
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Unique identifier for the instance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto Restart Configuration */}
|
||||||
|
<AutoRestartConfiguration
|
||||||
|
formData={formData}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Basic Instance Options */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Basic Instance Options</h3>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
id="idle_timeout"
|
||||||
|
label="Idle Timeout (minutes)"
|
||||||
|
value={formData.idle_timeout}
|
||||||
|
onChange={(value) => onChange('idle_timeout', value)}
|
||||||
|
placeholder="30"
|
||||||
|
description="Minutes before stopping an idle instance"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CheckboxInput
|
||||||
|
id="on_demand_start"
|
||||||
|
label="On Demand Start"
|
||||||
|
value={formData.on_demand_start}
|
||||||
|
onChange={(value) => onChange('on_demand_start', value)}
|
||||||
|
description="Start instance only when needed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstanceSettingsCard
|
||||||
Reference in New Issue
Block a user