From fb95ef22b3264cc71fd27843415b8b6340e06cb2 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 25 Jul 2026 16:59:05 +0700 Subject: [PATCH] feat(web): add delete functionality to agent components - Introduced delete functionality across multiple agent components, including AgentCard, AgentCardsGrid, AgentDetailSheet, and AgentDetailView, allowing users to remove agents directly from the UI. - Integrated a confirmation dialog to prevent accidental deletions, enhancing user experience and safety. - Updated relevant props and handlers to manage delete actions consistently across components. These changes improve the overall management of agents, providing users with the ability to easily delete agents while ensuring confirmation for critical actions. --- apps/web/src/components/agents/agent-card.tsx | 178 ++++++++++-------- .../components/agents/agent-cards-grid.tsx | 3 + .../components/agents/agent-detail-sheet.tsx | 4 +- .../components/agents/agent-detail-view.tsx | 17 +- .../agents/agent-fleet-data-grid.tsx | 24 ++- apps/web/src/routes/_auth/agents/index.tsx | 46 +++++ 6 files changed, 186 insertions(+), 86 deletions(-) diff --git a/apps/web/src/components/agents/agent-card.tsx b/apps/web/src/components/agents/agent-card.tsx index c4f60d7..921bbd6 100644 --- a/apps/web/src/components/agents/agent-card.tsx +++ b/apps/web/src/components/agents/agent-card.tsx @@ -1,4 +1,5 @@ import type { Agent } from '@evofw/shared' +import { Trash2 } from 'lucide-react' import { AgentPlatformIcon, platformLabel, @@ -11,6 +12,7 @@ import { import { StatusBadge } from '@/components/status-badge' import { Badge } from '@/components/reui/badge' import { Frame, FramePanel } from '@/components/reui/frame' +import { Button } from '@evofw/ui/components/button' import { Item, ItemContent, @@ -54,9 +56,15 @@ type AgentCardProps = { agent: Agent selected?: boolean onSelect: (id: string) => void + onDelete: (id: string) => void } -export function AgentCard({ agent, selected, onSelect }: AgentCardProps) { +export function AgentCard({ + agent, + selected, + onSelect, + onDelete, +}: AgentCardProps) { const hasApply = agentHasTrafficSample(agent) const dropped = formatPackets(agentTrafficDropped(agent), hasApply) const accepted = formatPackets(agentTrafficAccepted(agent), hasApply) @@ -103,90 +111,102 @@ export function AgentCard({ agent, selected, onSelect }: AgentCardProps) { ] as const return ( - + {agent.last_apply_error ? ( +

+ {agent.last_apply_error} +

+ ) : agent.last_apply_kernel_method ? ( +

+ kernel · {agent.last_apply_kernel_method} +

+ ) : null} + + + + + ) } diff --git a/apps/web/src/components/agents/agent-cards-grid.tsx b/apps/web/src/components/agents/agent-cards-grid.tsx index d9ec073..ebb2f37 100644 --- a/apps/web/src/components/agents/agent-cards-grid.tsx +++ b/apps/web/src/components/agents/agent-cards-grid.tsx @@ -13,6 +13,7 @@ type AgentCardsGridProps = { agents: Agent[] selectedId?: string | null onSelect: (id: string) => void + onDelete: (id: string) => void isLoading?: boolean emptyTitle?: string emptyDescription?: string @@ -23,6 +24,7 @@ export function AgentCardsGrid({ agents, selectedId, onSelect, + onDelete, isLoading, emptyTitle = 'Нет агентов', emptyDescription, @@ -58,6 +60,7 @@ export function AgentCardsGrid({ agent={agent} selected={selectedId === agent.id} onSelect={onSelect} + onDelete={onDelete} /> ))} diff --git a/apps/web/src/components/agents/agent-detail-sheet.tsx b/apps/web/src/components/agents/agent-detail-sheet.tsx index 67fad83..f3cefad 100644 --- a/apps/web/src/components/agents/agent-detail-sheet.tsx +++ b/apps/web/src/components/agents/agent-detail-sheet.tsx @@ -27,12 +27,14 @@ type AgentDetailSheetProps = { agentId: string | null open: boolean onOpenChange: (open: boolean) => void + onDelete?: (id: string) => void } export function AgentDetailSheet({ agentId, open, onOpenChange, + onDelete, }: AgentDetailSheetProps) { const agentQ = useQuery({ ...agentQueryOptions(agentId ?? ''), @@ -81,7 +83,7 @@ export function AgentDetailSheet({
{agentId && open ? ( - + ) : null}
diff --git a/apps/web/src/components/agents/agent-detail-view.tsx b/apps/web/src/components/agents/agent-detail-view.tsx index 1d82534..1fd06fd 100644 --- a/apps/web/src/components/agents/agent-detail-view.tsx +++ b/apps/web/src/components/agents/agent-detail-view.tsx @@ -11,6 +11,7 @@ import { MoreHorizontalIcon, ShieldPlusIcon, TerminalIcon, + Trash2, } from 'lucide-react' import { DetailPanel } from '@/components/reui-kit' import { @@ -48,6 +49,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@evofw/ui/components/dropdown-menu' @@ -60,9 +62,10 @@ import { type AgentDetailViewProps = { agentId: string + onDelete?: (id: string) => void } -export function AgentDetailView({ agentId }: AgentDetailViewProps) { +export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { const qc = useQueryClient() const { copyToClipboard } = useCopyToClipboard() const agentQ = useQuery(agentQueryOptions(agentId)) @@ -229,6 +232,18 @@ export function AgentDetailView({ agentId }: AgentDetailViewProps) { Install curl ) : null} + {onDelete ? ( + <> + + onDelete(agentId)} + > + + Удалить агента + + + ) : null} diff --git a/apps/web/src/components/agents/agent-fleet-data-grid.tsx b/apps/web/src/components/agents/agent-fleet-data-grid.tsx index 6e5bfc9..74031b5 100644 --- a/apps/web/src/components/agents/agent-fleet-data-grid.tsx +++ b/apps/web/src/components/agents/agent-fleet-data-grid.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo, type MouseEvent, type ReactNode } from 'react' import type { ColumnDef } from '@tanstack/react-table' -import { Check, Copy, PanelRight } from 'lucide-react' +import { Check, Copy, PanelRight, Trash2 } from 'lucide-react' import type { Agent } from '@evofw/shared' import type { Filter, FilterFieldConfig } from '@/components/reui/filters' import { ResourcePage } from '@/components/reui-kit' @@ -82,6 +82,7 @@ export type AgentFleetDataGridProps = { onApprove: (id: string) => void approvePending?: boolean onCopyInstall: (curl: string) => void + onDelete: (id: string) => void } export function AgentFleetDataGrid({ @@ -108,6 +109,7 @@ export function AgentFleetDataGrid({ onApprove, approvePending, onCopyInstall, + onDelete, }: AgentFleetDataGridProps) { const handleCopy = useCallback( (curl: string, e?: MouseEvent) => { @@ -284,9 +286,9 @@ export function AgentFleetDataGrid({ }, { id: 'actions', - size: 120, - minSize: 120, - maxSize: 120, + size: 152, + minSize: 152, + maxSize: 160, enableSorting: false, enableResizing: false, header: () => Действия, @@ -329,12 +331,24 @@ export function AgentFleetDataGrid({ > + ) }, }, ], - [approvePending, handleCopy, onApprove, onSelect], + [approvePending, handleCopy, onApprove, onDelete, onSelect], ) return ( diff --git a/apps/web/src/routes/_auth/agents/index.tsx b/apps/web/src/routes/_auth/agents/index.tsx index 46a1c1c..c88185d 100644 --- a/apps/web/src/routes/_auth/agents/index.tsx +++ b/apps/web/src/routes/_auth/agents/index.tsx @@ -47,6 +47,7 @@ import { computeFleetCounts, fleetKpiCards, } from '@/components/agents/agents-fleet-kpis' +import { ConfirmDialog } from '@/components/confirm-dialog' import { agentsQueryOptions } from '@/queries' import { apiFetch } from '@/lib/api' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' @@ -105,6 +106,7 @@ function AgentsPage() { const agentsQ = useQuery(agentsQueryOptions()) const { copyToClipboard } = useCopyToClipboard() const [createOpen, setCreateOpen] = useState(false) + const [deleteAgentId, setDeleteAgentId] = useState(null) const [filters, setFilters] = useState([]) const [searchQuery, setSearchQuery] = useState('') const [activeTab, setActiveTab] = useState('all') @@ -155,6 +157,21 @@ function AgentsPage() { onError: (e: Error) => toast.error(e.message), }) + const removeAgent = useMutation({ + mutationFn: (id: string) => + apiFetch(`/api/v1/agents/${id}`, { method: 'DELETE' }), + onSuccess: (_data, id) => { + toast.success('Агент удалён') + setDeleteAgentId(null) + if (detailAgentId === id) { + setSearch({ agent: '' }) + } + void qc.invalidateQueries({ queryKey: ['agents'] }) + void qc.invalidateQueries({ queryKey: ['dashboard'] }) + }, + onError: (e: Error) => toast.error(e.message), + }) + const items = agentsQ.data?.items ?? [] const counts = useMemo(() => computeFleetCounts(items), [items]) const pendingIds = useMemo( @@ -330,6 +347,15 @@ function AgentsPage() { [setSearch], ) + const handleDeleteAgent = useCallback((id: string) => { + setDeleteAgentId(id) + }, []) + + const deleteTargetName = useMemo( + () => items.find((a) => a.id === deleteAgentId)?.name, + [items, deleteAgentId], + ) + const handleClearFilters = useCallback(() => { setFilters([]) setSearchQuery('') @@ -454,6 +480,7 @@ function AgentsPage() { onApprove={(id) => approve.mutate(id)} approvePending={approve.isPending} onCopyInstall={handleCopyInstall} + onDelete={handleDeleteAgent} /> ) : agentsQ.isError ? ( @@ -533,6 +560,7 @@ function AgentsPage() { agents={filteredItems} selectedId={detailOpen ? detailAgentId : null} onSelect={handleSelectAgent} + onDelete={handleDeleteAgent} isLoading={agentsQ.isLoading} emptyTitle={ items.length === 0 @@ -559,6 +587,24 @@ function AgentsPage() { onOpenChange={(open) => { if (!open) setSearch({ agent: '' }) }} + onDelete={handleDeleteAgent} + /> + + { + if (!open) setDeleteAgentId(null) + }} + title="Удалить агента?" + description={ + deleteTargetName + ? `Агент «${deleteTargetName}» будет удалён вместе с overrides, install-ссылками и статистикой. Это действие нельзя отменить.` + : 'Агент будет удалён вместе с overrides, install-ссылками и статистикой. Это действие нельзя отменить.' + } + onConfirm={() => { + if (deleteAgentId) removeAgent.mutate(deleteAgentId) + }} + disabled={removeAgent.isPending} /> )