feat: add delete button to fleet nodes list

- Add deleteCloudRuntimeNode method to API client (DELETE /api/cloud-runtime/nodes/:nodeId)
- Add useDeleteCloudRuntimeNode mutation hook in cloud-runtime.ts
- Add delete button with Trash2 icon to CloudRuntimeNodeRow component
- Include confirmation dialog, loading state, and toast notifications
- Add i18n keys for en and zh-Hans locales

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
yushen
2026-05-21 17:29:16 +08:00
parent ae530ef057
commit ca42352118
5 changed files with 55 additions and 3 deletions

View File

@@ -869,6 +869,12 @@ export class ApiClient {
);
}
async deleteCloudRuntimeNode(nodeId: string): Promise<void> {
await this.fetch(`/api/cloud-runtime/nodes/${nodeId}`, {
method: "DELETE",
});
}
async deleteRuntime(runtimeId: string): Promise<void> {
await this.fetch(`/api/runtimes/${runtimeId}`, { method: "DELETE" });
}

View File

@@ -79,3 +79,13 @@ export function useCreateCloudRuntimeNode(wsId: string) {
},
});
}
export function useDeleteCloudRuntimeNode(wsId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (nodeId: string) => api.deleteCloudRuntimeNode(nodeId),
onSettled: () => {
qc.invalidateQueries({ queryKey: cloudRuntimeKeys.all(wsId) });
},
});
}

View File

@@ -172,6 +172,10 @@
"cancel": "Cancel",
"toast_created": "Cloud Runtime node created",
"toast_create_failed": "Failed to create Cloud Runtime node",
"delete": "Delete node",
"delete_confirm": "Are you sure you want to delete this cloud node? This action cannot be undone.",
"toast_deleted": "Cloud node deleted",
"toast_delete_failed": "Failed to delete cloud node",
"fields": {
"name": "Name",
"instance_type": "Instance type",

View File

@@ -163,6 +163,10 @@
"cancel": "取消",
"toast_created": "Cloud Runtime 节点已创建",
"toast_create_failed": "创建 Cloud Runtime 节点失败",
"delete": "删除节点",
"delete_confirm": "确定要删除此云节点吗?此操作无法撤销。",
"toast_deleted": "云节点已删除",
"toast_delete_failed": "删除云节点失败",
"fields": {
"name": "名称",
"instance_type": "实例规格",

View File

@@ -3,12 +3,13 @@
import { useId, useMemo, useState } from "react";
import type { FormEvent, HTMLAttributes } from "react";
import { useQuery } from "@tanstack/react-query";
import { Cloud, Loader2, RefreshCw, Rocket } from "lucide-react";
import { Cloud, Loader2, RefreshCw, Rocket, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { CloudRuntimeNode } from "@multica/core/runtimes";
import {
cloudRuntimeNodeListOptions,
useCreateCloudRuntimeNode,
useDeleteCloudRuntimeNode,
} from "@multica/core/runtimes";
import { useWorkspaceId } from "@multica/core/hooks";
import { Badge } from "@multica/ui/components/ui/badge";
@@ -192,7 +193,7 @@ export function CloudRuntimeDialog({ onClose }: { onClose: () => void }) {
<div className="max-h-[410px] overflow-y-auto p-2">
<div className="space-y-2">
{sortedNodes.map((node) => (
<CloudRuntimeNodeRow key={node.id} node={node} />
<CloudRuntimeNodeRow key={node.id} node={node} wsId={wsId} />
))}
</div>
</div>
@@ -290,8 +291,9 @@ function LabeledInput({
);
}
function CloudRuntimeNodeRow({ node }: { node: CloudRuntimeNode }) {
function CloudRuntimeNodeRow({ node, wsId }: { node: CloudRuntimeNode; wsId: string }) {
const { t } = useT("runtimes");
const deleteNode = useDeleteCloudRuntimeNode(wsId);
const title =
node.name.trim() ||
node.instance_id.trim() ||
@@ -317,6 +319,32 @@ function CloudRuntimeNodeRow({ node }: { node: CloudRuntimeNode }) {
)}
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-7 shrink-0 p-0 text-muted-foreground hover:text-destructive"
disabled={deleteNode.isPending}
onClick={() => {
if (!confirm(t(($) => $.cloud_runtime.delete_confirm))) return;
deleteNode.mutate(node.id, {
onSuccess: () => toast.success(t(($) => $.cloud_runtime.toast_deleted)),
onError: (err) =>
toast.error(
err instanceof Error
? err.message
: t(($) => $.cloud_runtime.toast_delete_failed),
),
});
}}
aria-label={t(($) => $.cloud_runtime.delete)}
>
{deleteNode.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
</Button>
</div>
{node.instance_id && (
<div className="mt-2 truncate font-mono text-[11px] text-muted-foreground/80">