Compare commits

...

1 Commits

Author SHA1 Message Date
Eve
94f4003410 fix: simplify cloud runtime create form
Co-authored-by: multica-agent <github@multica.ai>
2026-05-21 16:22:29 +08:00
3 changed files with 55 additions and 71 deletions

View File

@@ -160,7 +160,7 @@
"title": "Cloud Runtime",
"description": "Launch a managed cloud node and watch its Fleet status.",
"create_title": "New node",
"create_hint": "Defaults target the shared us-west-2 Fleet configuration.",
"create_hint": "Choose a name, instance type, and disk size. Fleet applies the rest.",
"nodes_title": "Fleet nodes",
"refresh": "Refresh",
"nodes_empty": "No cloud nodes",
@@ -183,7 +183,7 @@
"bootstrap_pat": "Bootstrap PAT"
},
"placeholders": {
"name": "gpu-dev-01",
"name": "cloud-dev-01",
"key_name": "dev-key"
},
"validation": {

View File

@@ -151,7 +151,7 @@
"title": "Cloud Runtime",
"description": "创建托管云端节点,并查看它的 Fleet 状态。",
"create_title": "新节点",
"create_hint": "默认使用共享的 us-west-2 Fleet 配置。",
"create_hint": "只需要填写名称、实例规格和磁盘大小,其余配置由 Fleet 默认处理。",
"nodes_title": "Fleet 节点",
"refresh": "刷新",
"nodes_empty": "还没有云端节点",
@@ -167,14 +167,14 @@
"name": "名称",
"instance_type": "实例规格",
"region": "Region",
"disk_size": "磁盘 GiB",
"disk_size": "磁盘大小 GiB",
"image_id": "AMI ID",
"subnet_id": "Subnet ID",
"key_name": "Key pair",
"bootstrap_pat": "Bootstrap PAT"
},
"placeholders": {
"name": "gpu-dev-01",
"name": "cloud-dev-01",
"key_name": "dev-key"
},
"validation": {

View File

@@ -23,11 +23,19 @@ import {
} from "@multica/ui/components/ui/dialog";
import { Input } from "@multica/ui/components/ui/input";
import { Label } from "@multica/ui/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@multica/ui/components/ui/select";
import { cn } from "@multica/ui/lib/utils";
import { useT } from "../../i18n";
const DEFAULT_REGION = "us-west-2";
const DEFAULT_INSTANCE_TYPE = "g5.xlarge";
const CLOUD_RUNTIME_INSTANCE_TYPES = ["t4g.medium", "t4g.large"] as const;
const DEFAULT_INSTANCE_TYPE = CLOUD_RUNTIME_INSTANCE_TYPES[0];
const DEFAULT_DISK_SIZE_GB = 20;
export function CloudRuntimeDialog({ onClose }: { onClose: () => void }) {
const { t } = useT("runtimes");
@@ -35,13 +43,10 @@ export function CloudRuntimeDialog({ onClose }: { onClose: () => void }) {
const idPrefix = `cloud-runtime-${useId().replace(/:/g, "")}`;
const formId = `${idPrefix}-form`;
const [name, setName] = useState("");
const [instanceType, setInstanceType] = useState(DEFAULT_INSTANCE_TYPE);
const [region, setRegion] = useState(DEFAULT_REGION);
const [diskSizeGB, setDiskSizeGB] = useState("");
const [imageId, setImageId] = useState("");
const [subnetId, setSubnetId] = useState("");
const [keyName, setKeyName] = useState("");
const [bootstrapPAT, setBootstrapPAT] = useState("");
const [instanceType, setInstanceType] = useState<string>(
DEFAULT_INSTANCE_TYPE,
);
const [diskSizeGB, setDiskSizeGB] = useState(String(DEFAULT_DISK_SIZE_GB));
const nodesQuery = useQuery(
cloudRuntimeNodeListOptions(wsId, { limit: 20, offset: 0 }),
@@ -59,17 +64,10 @@ export function CloudRuntimeDialog({ onClose }: { onClose: () => void }) {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const instance_type = instanceType.trim();
if (!instance_type) {
toast.error(t(($) => $.cloud_runtime.validation.instance_type_required));
return;
}
const diskSize = diskSizeGB.trim() ? Number(diskSizeGB.trim()) : undefined;
if (
diskSize !== undefined &&
(!Number.isInteger(diskSize) || diskSize <= 0)
) {
const diskSize = diskSizeGB.trim()
? Number(diskSizeGB.trim())
: DEFAULT_DISK_SIZE_GB;
if (!Number.isInteger(diskSize) || diskSize <= 0) {
toast.error(t(($) => $.cloud_runtime.validation.disk_size_invalid));
return;
}
@@ -77,19 +75,15 @@ export function CloudRuntimeDialog({ onClose }: { onClose: () => void }) {
try {
await createNode.mutateAsync({
data: {
instance_type,
instance_type: instanceType,
name: valueOrUndefined(name),
region: valueOrUndefined(region),
disk_size_gb: diskSize,
image_id: valueOrUndefined(imageId),
subnet_id: valueOrUndefined(subnetId),
key_name: valueOrUndefined(keyName),
},
userPAT: valueOrUndefined(bootstrapPAT),
});
toast.success(t(($) => $.cloud_runtime.toast_created));
setName("");
setBootstrapPAT("");
setInstanceType(DEFAULT_INSTANCE_TYPE);
setDiskSizeGB(String(DEFAULT_DISK_SIZE_GB));
} catch (error) {
toast.error(
error instanceof Error
@@ -137,53 +131,17 @@ export function CloudRuntimeDialog({ onClose }: { onClose: () => void }) {
label={t(($) => $.cloud_runtime.fields.instance_type)}
value={instanceType}
onChange={setInstanceType}
placeholder={DEFAULT_INSTANCE_TYPE}
required
/>
<LabeledInput
id={`${idPrefix}-region`}
label={t(($) => $.cloud_runtime.fields.region)}
value={region}
onChange={setRegion}
placeholder={DEFAULT_REGION}
options={CLOUD_RUNTIME_INSTANCE_TYPES}
/>
<LabeledInput
id={`${idPrefix}-disk-size`}
label={t(($) => $.cloud_runtime.fields.disk_size)}
value={diskSizeGB}
onChange={setDiskSizeGB}
placeholder="200"
placeholder={String(DEFAULT_DISK_SIZE_GB)}
type="number"
inputMode="numeric"
/>
<LabeledInput
id={`${idPrefix}-image-id`}
label={t(($) => $.cloud_runtime.fields.image_id)}
value={imageId}
onChange={setImageId}
placeholder="ami-..."
/>
<LabeledInput
id={`${idPrefix}-subnet-id`}
label={t(($) => $.cloud_runtime.fields.subnet_id)}
value={subnetId}
onChange={setSubnetId}
placeholder="subnet-..."
/>
<LabeledInput
id={`${idPrefix}-key-name`}
label={t(($) => $.cloud_runtime.fields.key_name)}
value={keyName}
onChange={setKeyName}
placeholder={t(($) => $.cloud_runtime.placeholders.key_name)}
/>
<LabeledInput
id={`${idPrefix}-bootstrap-pat`}
label={t(($) => $.cloud_runtime.fields.bootstrap_pat)}
value={bootstrapPAT}
onChange={setBootstrapPAT}
placeholder="mul_..."
type="password"
/>
</div>
</form>
@@ -279,6 +237,7 @@ function LabeledInput({
required,
type = "text",
inputMode,
options,
}: {
id: string;
label: string;
@@ -288,7 +247,32 @@ function LabeledInput({
required?: boolean;
type?: string;
inputMode?: HTMLAttributes<HTMLInputElement>["inputMode"];
options?: readonly string[];
}) {
if (options) {
return (
<div className="space-y-1.5">
<Label htmlFor={id} className="text-xs text-muted-foreground">
{label}
</Label>
<Select value={value} onValueChange={(next) => onChange(next ?? value)}>
<SelectTrigger id={id} className="h-9 w-full rounded-md text-sm">
<SelectValue>
{() => <span className="truncate">{value}</span>}
</SelectValue>
</SelectTrigger>
<SelectContent align="start">
{options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
return (
<div className="space-y-1.5">
<Label htmlFor={id} className="text-xs text-muted-foreground">