|
1 | 1 | import { useState, useEffect } from "react"; |
2 | 2 | import { useTranslation } from "react-i18next"; |
3 | | -import { ArrowLeft, Play, Copy, Check, Loader2, AlertCircle, FileText, Wrench, Package } from "lucide-react"; |
| 3 | +import { ArrowLeft, Play, Copy, Check, Loader2, AlertCircle, FileText, Wrench, Package, Trash2 } from "lucide-react"; |
4 | 4 | import { cn } from "@/lib/utils"; |
5 | | -import { useResourceDetail, useResourceResolve } from "@/hooks/useResources"; |
| 5 | +import { useResourceDetail, useResourceResolve, useResourceDelete } from "@/hooks/useResources"; |
6 | 6 | import type { ResolveResponse } from "agentvm/client"; |
7 | 7 |
|
8 | 8 | interface ResourceDetailProps { |
9 | 9 | locator: string; |
10 | 10 | onBack: () => void; |
11 | 11 | } |
12 | 12 |
|
13 | | -type TabType = "resolve" | "content" | "versions"; |
| 13 | +type TabType = "resolve" | "content" | "versions" | "config"; |
14 | 14 |
|
15 | 15 | /** JSON Schema property definition */ |
16 | 16 | interface JSONSchemaProperty { |
@@ -48,10 +48,13 @@ export function ResourceDetail({ locator, onBack }: ResourceDetailProps) { |
48 | 48 | const [result, setResult] = useState<ResolveResponse | null>(null); |
49 | 49 | const [copied, setCopied] = useState(false); |
50 | 50 | const [executeError, setExecuteError] = useState<string | null>(null); |
| 51 | + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); |
| 52 | + const [deleteConfirmInput, setDeleteConfirmInput] = useState(""); |
51 | 53 |
|
52 | 54 | // Load resource details |
53 | 55 | const { data: resource, isLoading: loading, isError, error } = useResourceDetail(locator); |
54 | 56 | const resolveMutation = useResourceResolve(); |
| 57 | + const deleteMutation = useResourceDelete(); |
55 | 58 |
|
56 | 59 | const config = typeConfig[resource?.manifest.type || "default"] || typeConfig.default; |
57 | 60 | const Icon = config.icon; |
@@ -121,8 +124,21 @@ export function ResourceDetail({ locator, onBack }: ResourceDetailProps) { |
121 | 124 | { id: "resolve", labelKey: "resources.resolve" }, |
122 | 125 | { id: "content", labelKey: "resources.content" }, |
123 | 126 | { id: "versions", labelKey: "resources.versions" }, |
| 127 | + { id: "config", labelKey: "resources.config" }, |
124 | 128 | ]; |
125 | 129 |
|
| 130 | + const handleDelete = async () => { |
| 131 | + if (deleteConfirmInput !== locator) return; |
| 132 | + |
| 133 | + try { |
| 134 | + await deleteMutation.mutateAsync(locator); |
| 135 | + setShowDeleteConfirm(false); |
| 136 | + onBack(); |
| 137 | + } catch (err) { |
| 138 | + // Error handled by mutation |
| 139 | + } |
| 140 | + }; |
| 141 | + |
126 | 142 | const schema = resource?.schema as JSONSchema | undefined; |
127 | 143 | const hasSchema = schema?.properties && Object.keys(schema.properties).length > 0; |
128 | 144 |
|
@@ -346,6 +362,38 @@ export function ResourceDetail({ locator, onBack }: ResourceDetailProps) { |
346 | 362 | </p> |
347 | 363 | </div> |
348 | 364 | )} |
| 365 | + |
| 366 | + {activeTab === "config" && ( |
| 367 | + <div className="space-y-6"> |
| 368 | + {/* Danger Zone */} |
| 369 | + <div className="rounded-lg border border-[var(--accent-error)]/30 overflow-hidden"> |
| 370 | + <div className="px-4 py-3 bg-[var(--accent-error)]/5 border-b border-[var(--accent-error)]/30"> |
| 371 | + <h3 className="text-sm font-medium text-[var(--accent-error)]"> |
| 372 | + {t("resources.dangerZone")} |
| 373 | + </h3> |
| 374 | + </div> |
| 375 | + <div className="p-4"> |
| 376 | + <div className="flex items-center justify-between"> |
| 377 | + <div> |
| 378 | + <p className="text-sm font-medium text-[var(--text-primary)]"> |
| 379 | + {t("resources.deleteResource")} |
| 380 | + </p> |
| 381 | + <p className="text-xs text-[var(--text-muted)] mt-1"> |
| 382 | + {t("resources.deleteResourceDesc")} |
| 383 | + </p> |
| 384 | + </div> |
| 385 | + <button |
| 386 | + onClick={() => setShowDeleteConfirm(true)} |
| 387 | + className="h-8 px-3 rounded-lg flex items-center gap-2 text-sm font-medium border border-[var(--accent-error)] text-[var(--accent-error)] hover:bg-[var(--accent-error)] hover:text-white transition-colors" |
| 388 | + > |
| 389 | + <Trash2 className="w-4 h-4" /> |
| 390 | + {t("common.delete")} |
| 391 | + </button> |
| 392 | + </div> |
| 393 | + </div> |
| 394 | + </div> |
| 395 | + </div> |
| 396 | + )} |
349 | 397 | </div> |
350 | 398 |
|
351 | 399 | {/* About Panel - 紧贴内容 */} |
@@ -397,6 +445,91 @@ export function ResourceDetail({ locator, onBack }: ResourceDetailProps) { |
397 | 445 | </div> |
398 | 446 | </div> |
399 | 447 | </div> |
| 448 | + |
| 449 | + {/* Delete Confirmation Modal */} |
| 450 | + {showDeleteConfirm && ( |
| 451 | + <div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50"> |
| 452 | + <div className="bg-[var(--bg-card)] rounded-xl w-[480px] shadow-xl border border-[var(--border-light)]"> |
| 453 | + {/* Header */} |
| 454 | + <div className="px-4 py-3 border-b border-[var(--border-light)]"> |
| 455 | + <h2 className="text-base font-medium text-[var(--accent-error)]"> |
| 456 | + {t("resources.deleteResource")} |
| 457 | + </h2> |
| 458 | + </div> |
| 459 | + |
| 460 | + {/* Content */} |
| 461 | + <div className="p-4 space-y-4"> |
| 462 | + <p className="text-sm text-[var(--text-secondary)]"> |
| 463 | + {t("resources.deleteConfirmMessage")} |
| 464 | + </p> |
| 465 | + |
| 466 | + <div className="p-3 bg-[var(--bg-secondary)] rounded-lg"> |
| 467 | + <code className="text-sm font-mono text-[var(--text-primary)] break-all"> |
| 468 | + {locator} |
| 469 | + </code> |
| 470 | + </div> |
| 471 | + |
| 472 | + <div> |
| 473 | + <label className="block text-sm text-[var(--text-secondary)] mb-2"> |
| 474 | + {t("resources.deleteConfirmLabel")} |
| 475 | + </label> |
| 476 | + <input |
| 477 | + type="text" |
| 478 | + value={deleteConfirmInput} |
| 479 | + onChange={(e) => setDeleteConfirmInput(e.target.value)} |
| 480 | + placeholder={locator} |
| 481 | + className={cn( |
| 482 | + "w-full h-10 px-3 rounded-lg", |
| 483 | + "bg-[var(--bg-card)] border", |
| 484 | + deleteConfirmInput === locator |
| 485 | + ? "border-[var(--accent-error)]" |
| 486 | + : "border-[var(--border-light)]", |
| 487 | + "text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]", |
| 488 | + "outline-none focus:border-[var(--border-medium)] transition-colors" |
| 489 | + )} |
| 490 | + /> |
| 491 | + </div> |
| 492 | + |
| 493 | + {deleteMutation.isError && ( |
| 494 | + <div className="p-3 bg-red-50 rounded-lg flex items-start gap-2"> |
| 495 | + <AlertCircle className="w-4 h-4 text-red-600 shrink-0 mt-0.5" /> |
| 496 | + <p className="text-sm text-red-600"> |
| 497 | + {deleteMutation.error instanceof Error |
| 498 | + ? deleteMutation.error.message |
| 499 | + : t("common.error")} |
| 500 | + </p> |
| 501 | + </div> |
| 502 | + )} |
| 503 | + </div> |
| 504 | + |
| 505 | + {/* Footer */} |
| 506 | + <div className="border-t border-[var(--border-light)] p-3 flex justify-end gap-2"> |
| 507 | + <button |
| 508 | + onClick={() => { |
| 509 | + setShowDeleteConfirm(false); |
| 510 | + setDeleteConfirmInput(""); |
| 511 | + }} |
| 512 | + className="h-8 px-4 rounded text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] transition-colors" |
| 513 | + > |
| 514 | + {t("common.cancel")} |
| 515 | + </button> |
| 516 | + <button |
| 517 | + onClick={handleDelete} |
| 518 | + disabled={deleteConfirmInput !== locator || deleteMutation.isPending} |
| 519 | + className={cn( |
| 520 | + "h-8 px-4 rounded text-sm font-medium transition-colors flex items-center gap-2", |
| 521 | + deleteConfirmInput === locator && !deleteMutation.isPending |
| 522 | + ? "bg-[var(--accent-error)] text-white hover:bg-[var(--accent-error)]/90" |
| 523 | + : "bg-[var(--bg-tertiary)] text-[var(--text-muted)] cursor-not-allowed" |
| 524 | + )} |
| 525 | + > |
| 526 | + {deleteMutation.isPending && <Loader2 className="w-4 h-4 animate-spin" />} |
| 527 | + {t("resources.confirmDelete")} |
| 528 | + </button> |
| 529 | + </div> |
| 530 | + </div> |
| 531 | + </div> |
| 532 | + )} |
400 | 533 | </div> |
401 | 534 | ); |
402 | 535 | } |
0 commit comments