diff --git a/ui/web/src/components/shared/confirm-delete-dialog.tsx b/ui/web/src/components/shared/confirm-delete-dialog.tsx new file mode 100644 index 00000000..0d1a3346 --- /dev/null +++ b/ui/web/src/components/shared/confirm-delete-dialog.tsx @@ -0,0 +1,75 @@ +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +interface ConfirmDeleteDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + confirmValue: string; + confirmLabel?: string; + onConfirm: () => void; + loading?: boolean; +} + +export function ConfirmDeleteDialog({ + open, + onOpenChange, + title, + description, + confirmValue, + confirmLabel = "Delete", + onConfirm, + loading, +}: ConfirmDeleteDialogProps) { + const [inputValue, setInputValue] = useState(""); + + useEffect(() => { + if (!open) setInputValue(""); + }, [open]); + + const isMatch = inputValue.toLowerCase() === confirmValue.toLowerCase(); + + return ( + + + + {title} + {description} + +
+

+ Type {confirmValue} to confirm. +

+ setInputValue(e.target.value)} + placeholder={confirmValue} + autoFocus + /> +
+ + + + +
+
+ ); +} diff --git a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx index 53488368..a7ba76f1 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx @@ -11,10 +11,9 @@ import { AgentFilesTab } from "./agent-files-tab"; import { AgentSharesTab } from "./agent-shares-tab"; import { AgentLinksTab } from "./agent-links-tab"; import { AgentSkillsTab } from "./agent-skills-tab"; -import { AgentDangerTab } from "./agent-danger-tab"; import { SummoningModal } from "../summoning-modal"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; -import { DeferredSpinner } from "@/components/shared/loading-skeleton"; +import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog"; +import { DetailPageSkeleton } from "@/components/shared/loading-skeleton"; interface AgentDetailPageProps { agentId: string; @@ -38,7 +37,7 @@ function agentSubtitle(agent: { display_name?: string; agent_key: string; id: st } export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { - const { agent, files, loading, updateAgent, getFile, setFile, regenerateAgent, resummonAgent, deleteAgent, refresh } = + const { agent, files, loading, updateAgent, getFile, setFile, regenerateAgent, resummonAgent, refresh } = useAgentDetail(agentId); const { deleteAgent: deleteAgentById } = useAgents(); const [summoningOpen, setSummoningOpen] = useState(false); @@ -62,14 +61,7 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { }; if (loading || !agent) { - return ( -
- - -
- ); + return ; } const title = agentDisplayName(agent); @@ -131,7 +123,6 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { Shares Links Skills - Danger @@ -165,9 +156,6 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { - - - @@ -180,13 +168,13 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { onResummon={async () => { await resummonAgent(); }} /> - { await deleteAgentById(agentId); setDeleteOpen(false); diff --git a/ui/web/src/pages/channels/channels-page.tsx b/ui/web/src/pages/channels/channels-page.tsx index d3a8a708..149f361d 100644 --- a/ui/web/src/pages/channels/channels-page.tsx +++ b/ui/web/src/pages/channels/channels-page.tsx @@ -8,7 +8,7 @@ import { EmptyState } from "@/components/shared/empty-state"; import { TableSkeleton } from "@/components/shared/loading-skeleton"; import { SearchInput } from "@/components/shared/search-input"; import { Pagination } from "@/components/shared/pagination"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; +import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog"; import { useChannels } from "./hooks/use-channels"; import { useChannelInstances, type ChannelInstanceData, type ChannelInstanceInput } from "./hooks/use-channel-instances"; import { ChannelInstanceFormDialog } from "./channel-instance-form-dialog"; @@ -264,13 +264,13 @@ export function ChannelsPage() { onUpdate={handleUpdate} /> - !v && setDeleteTarget(null)} title="Delete Channel Instance" - description={`Are you sure you want to delete "${deleteTarget?.display_name || deleteTarget?.name}"?`} + description={`Are you sure you want to delete "${deleteTarget?.display_name || deleteTarget?.name}"? This action cannot be undone.`} + confirmValue={deleteTarget?.display_name || deleteTarget?.name || ""} confirmLabel="Delete" - variant="destructive" onConfirm={handleDelete} loading={deleteLoading} /> diff --git a/ui/web/src/pages/mcp/mcp-page.tsx b/ui/web/src/pages/mcp/mcp-page.tsx index a244e1c3..c19e3990 100644 --- a/ui/web/src/pages/mcp/mcp-page.tsx +++ b/ui/web/src/pages/mcp/mcp-page.tsx @@ -7,7 +7,7 @@ import { EmptyState } from "@/components/shared/empty-state"; import { SearchInput } from "@/components/shared/search-input"; import { Pagination } from "@/components/shared/pagination"; import { TableSkeleton } from "@/components/shared/loading-skeleton"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; +import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog"; import { useMCP, type MCPServerData, type MCPServerInput } from "./hooks/use-mcp"; import { MCPFormDialog } from "./mcp-form-dialog"; import { MCPGrantsDialog } from "./mcp-grants-dialog"; @@ -226,13 +226,13 @@ export function MCPPage() { /> )} - !open && setDeleteTarget(null)} title="Delete MCP Server" description={`Are you sure you want to delete "${deleteTarget?.display_name || deleteTarget?.name}"? This will also remove all associated grants.`} + confirmValue={deleteTarget?.display_name || deleteTarget?.name || ""} confirmLabel="Delete" - variant="destructive" onConfirm={handleDelete} loading={deleteLoading} /> diff --git a/ui/web/src/pages/providers/providers-page.tsx b/ui/web/src/pages/providers/providers-page.tsx index b7755fd5..04583872 100644 --- a/ui/web/src/pages/providers/providers-page.tsx +++ b/ui/web/src/pages/providers/providers-page.tsx @@ -7,7 +7,7 @@ import { EmptyState } from "@/components/shared/empty-state"; import { SearchInput } from "@/components/shared/search-input"; import { Pagination } from "@/components/shared/pagination"; import { TableSkeleton } from "@/components/shared/loading-skeleton"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; +import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog"; import { useProviders, type ProviderData, type ProviderInput } from "./hooks/use-providers"; import { ProviderFormDialog } from "./provider-form-dialog"; import { useMinLoading } from "@/hooks/use-min-loading"; @@ -202,13 +202,13 @@ export function ProvidersPage() { existingProviders={providers} /> - !v && setDeleteTarget(null)} title="Delete Provider" - description={`Are you sure you want to delete "${deleteTarget?.display_name || deleteTarget?.name}"?`} + description={`Are you sure you want to delete "${deleteTarget?.display_name || deleteTarget?.name}"? This action cannot be undone.`} + confirmValue={deleteTarget?.display_name || deleteTarget?.name || ""} confirmLabel="Delete" - variant="destructive" onConfirm={handleDelete} loading={deleteLoading} /> diff --git a/ui/web/src/pages/skills/skills-page.tsx b/ui/web/src/pages/skills/skills-page.tsx index 0c48959f..d36e5b1a 100644 --- a/ui/web/src/pages/skills/skills-page.tsx +++ b/ui/web/src/pages/skills/skills-page.tsx @@ -7,7 +7,7 @@ import { EmptyState } from "@/components/shared/empty-state"; import { SearchInput } from "@/components/shared/search-input"; import { Pagination } from "@/components/shared/pagination"; import { TableSkeleton } from "@/components/shared/loading-skeleton"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; +import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog"; import { useSkills, type SkillInfo } from "./hooks/use-skills"; import { SkillDetailDialog } from "./skill-detail-dialog"; import { SkillUploadDialog } from "./skill-upload-dialog"; @@ -176,13 +176,13 @@ export function SkillsPage() { onUpload={handleUpload} /> - !open && setDeleteTarget(null)} title="Delete Skill" description={`Are you sure you want to delete "${deleteTarget?.name}"? This action cannot be undone.`} + confirmValue={deleteTarget?.name || ""} confirmLabel="Delete" - variant="destructive" onConfirm={handleDelete} loading={deleteLoading} /> diff --git a/ui/web/src/pages/teams/team-detail-page.tsx b/ui/web/src/pages/teams/team-detail-page.tsx index 48f026d5..855fc746 100644 --- a/ui/web/src/pages/teams/team-detail-page.tsx +++ b/ui/web/src/pages/teams/team-detail-page.tsx @@ -3,8 +3,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ArrowLeft, Users, Trash2 } from "lucide-react"; -import { DeferredSpinner } from "@/components/shared/loading-skeleton"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; +import { DetailPageSkeleton } from "@/components/shared/loading-skeleton"; +import { ConfirmDeleteDialog } from "@/components/shared/confirm-delete-dialog"; import { useTeams } from "./hooks/use-teams"; import { TeamMembersTab } from "./team-members-tab"; import { TeamTasksTab } from "./team-tasks-tab"; @@ -65,14 +65,7 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) { }, [teamId, removeMember, reload]); if (loading || !team) { - return ( -
- - -
- ); + return ; } return ( @@ -116,7 +109,7 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) { {/* Tabs */} -
+
Members @@ -148,13 +141,13 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
- { await deleteTeam(teamId); setDeleteOpen(false);