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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { Agent } from '@evofw/shared'
|
import type { Agent } from '@evofw/shared'
|
||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
AgentPlatformIcon,
|
AgentPlatformIcon,
|
||||||
platformLabel,
|
platformLabel,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||||
|
import { Button } from '@evofw/ui/components/button'
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
@@ -54,9 +56,15 @@ type AgentCardProps = {
|
|||||||
agent: Agent
|
agent: Agent
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
onSelect: (id: string) => void
|
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 hasApply = agentHasTrafficSample(agent)
|
||||||
const dropped = formatPackets(agentTrafficDropped(agent), hasApply)
|
const dropped = formatPackets(agentTrafficDropped(agent), hasApply)
|
||||||
const accepted = formatPackets(agentTrafficAccepted(agent), hasApply)
|
const accepted = formatPackets(agentTrafficAccepted(agent), hasApply)
|
||||||
@@ -103,90 +111,102 @@ export function AgentCard({ agent, selected, onSelect }: AgentCardProps) {
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div className="relative">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => onSelect(agent.id)}
|
type="button"
|
||||||
className={cn(
|
onClick={() => onSelect(agent.id)}
|
||||||
'w-full text-left outline-none',
|
|
||||||
'focus-visible:ring-ring rounded-[calc(var(--frame-radius)+2px)] focus-visible:ring-2 focus-visible:ring-offset-2',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Frame
|
|
||||||
spacing="xs"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-muted/50 dark:bg-muted/10 w-full transition-shadow',
|
'w-full text-left outline-none',
|
||||||
selected
|
'focus-visible:ring-ring rounded-[calc(var(--frame-radius)+2px)] focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||||
? 'ring-primary/30 ring-2'
|
|
||||||
: 'hover:ring-border hover:ring-1',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FramePanel className="text-card-foreground isolate flex flex-col gap-4 px-4 py-4">
|
<Frame
|
||||||
<div className="flex items-start gap-3">
|
spacing="xs"
|
||||||
<AgentPlatformIcon platform={agent.platform} />
|
className={cn(
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
'bg-muted/50 dark:bg-muted/10 w-full transition-shadow',
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
selected
|
||||||
<h3 className="truncate text-sm leading-tight font-semibold">
|
? 'ring-primary/30 ring-2'
|
||||||
{agent.name}
|
: 'hover:ring-border hover:ring-1',
|
||||||
</h3>
|
)}
|
||||||
<StatusBadge status={agent.status} />
|
>
|
||||||
<Badge
|
<FramePanel className="text-card-foreground isolate flex flex-col gap-4 px-4 py-4 pr-12">
|
||||||
variant={
|
<div className="flex items-start gap-3">
|
||||||
agent.default_action === 'drop'
|
<AgentPlatformIcon platform={agent.platform} />
|
||||||
? 'warning-light'
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
: 'success-light'
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
}
|
<h3 className="truncate text-sm leading-tight font-semibold">
|
||||||
size="xs"
|
{agent.name}
|
||||||
radius="full"
|
</h3>
|
||||||
>
|
<StatusBadge status={agent.status} />
|
||||||
{defaultAction}
|
<Badge
|
||||||
</Badge>
|
variant={
|
||||||
|
agent.default_action === 'drop'
|
||||||
|
? 'warning-light'
|
||||||
|
: 'success-light'
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
radius="full"
|
||||||
|
>
|
||||||
|
{defaultAction}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground truncate text-xs">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground truncate text-xs">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-muted/60 grid grid-cols-[1fr_auto_1fr] overflow-hidden rounded-lg border">
|
<div className="bg-muted/60 grid grid-cols-[1fr_auto_1fr] overflow-hidden rounded-lg border">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
<div key={stat.label} className="contents">
|
<div key={stat.label} className="contents">
|
||||||
<Item
|
<Item
|
||||||
variant="muted"
|
variant="muted"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="justify-center bg-transparent px-2 py-3"
|
className="justify-center bg-transparent px-2 py-3"
|
||||||
>
|
>
|
||||||
<ItemContent className="items-center gap-1">
|
<ItemContent className="items-center gap-1">
|
||||||
<ItemTitle
|
<ItemTitle
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sm leading-none font-medium tabular-nums',
|
'text-sm leading-none font-medium tabular-nums',
|
||||||
stat.valueClass,
|
stat.valueClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{stat.valueNode}
|
{stat.valueNode}
|
||||||
</ItemTitle>
|
</ItemTitle>
|
||||||
<ItemDescription className="line-clamp-1 text-xs leading-tight">
|
<ItemDescription className="line-clamp-1 text-xs leading-tight">
|
||||||
{stat.label}
|
{stat.label}
|
||||||
</ItemDescription>
|
</ItemDescription>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
{index < stats.length - 1 ? (
|
{index < stats.length - 1 ? (
|
||||||
<Separator orientation="vertical" className="my-2.5" />
|
<Separator orientation="vertical" className="my-2.5" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{agent.last_apply_error ? (
|
{agent.last_apply_error ? (
|
||||||
<p className="text-destructive truncate text-xs">
|
<p className="text-destructive truncate text-xs">
|
||||||
{agent.last_apply_error}
|
{agent.last_apply_error}
|
||||||
</p>
|
</p>
|
||||||
) : agent.last_apply_kernel_method ? (
|
) : agent.last_apply_kernel_method ? (
|
||||||
<p className="text-muted-foreground truncate text-xs">
|
<p className="text-muted-foreground truncate text-xs">
|
||||||
kernel · {agent.last_apply_kernel_method}
|
kernel · {agent.last_apply_kernel_method}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
</Frame>
|
</Frame>
|
||||||
</button>
|
</button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-destructive absolute top-3 right-3 z-10"
|
||||||
|
aria-label="Удалить"
|
||||||
|
onClick={() => onDelete(agent.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type AgentCardsGridProps = {
|
|||||||
agents: Agent[]
|
agents: Agent[]
|
||||||
selectedId?: string | null
|
selectedId?: string | null
|
||||||
onSelect: (id: string) => void
|
onSelect: (id: string) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
emptyTitle?: string
|
emptyTitle?: string
|
||||||
emptyDescription?: string
|
emptyDescription?: string
|
||||||
@@ -23,6 +24,7 @@ export function AgentCardsGrid({
|
|||||||
agents,
|
agents,
|
||||||
selectedId,
|
selectedId,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onDelete,
|
||||||
isLoading,
|
isLoading,
|
||||||
emptyTitle = 'Нет агентов',
|
emptyTitle = 'Нет агентов',
|
||||||
emptyDescription,
|
emptyDescription,
|
||||||
@@ -58,6 +60,7 @@ export function AgentCardsGrid({
|
|||||||
agent={agent}
|
agent={agent}
|
||||||
selected={selectedId === agent.id}
|
selected={selectedId === agent.id}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ type AgentDetailSheetProps = {
|
|||||||
agentId: string | null
|
agentId: string | null
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
|
onDelete?: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentDetailSheet({
|
export function AgentDetailSheet({
|
||||||
agentId,
|
agentId,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
onDelete,
|
||||||
}: AgentDetailSheetProps) {
|
}: AgentDetailSheetProps) {
|
||||||
const agentQ = useQuery({
|
const agentQ = useQuery({
|
||||||
...agentQueryOptions(agentId ?? ''),
|
...agentQueryOptions(agentId ?? ''),
|
||||||
@@ -81,7 +83,7 @@ export function AgentDetailSheet({
|
|||||||
<div className="min-h-0 flex-1">
|
<div className="min-h-0 flex-1">
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
{agentId && open ? (
|
{agentId && open ? (
|
||||||
<AgentDetailView agentId={agentId} />
|
<AgentDetailView agentId={agentId} onDelete={onDelete} />
|
||||||
) : null}
|
) : null}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
ShieldPlusIcon,
|
ShieldPlusIcon,
|
||||||
TerminalIcon,
|
TerminalIcon,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { DetailPanel } from '@/components/reui-kit'
|
import { DetailPanel } from '@/components/reui-kit'
|
||||||
import {
|
import {
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@evofw/ui/components/dropdown-menu'
|
} from '@evofw/ui/components/dropdown-menu'
|
||||||
|
|
||||||
@@ -60,9 +62,10 @@ import {
|
|||||||
|
|
||||||
type AgentDetailViewProps = {
|
type AgentDetailViewProps = {
|
||||||
agentId: string
|
agentId: string
|
||||||
|
onDelete?: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentDetailView({ agentId }: AgentDetailViewProps) {
|
export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { copyToClipboard } = useCopyToClipboard()
|
const { copyToClipboard } = useCopyToClipboard()
|
||||||
const agentQ = useQuery(agentQueryOptions(agentId))
|
const agentQ = useQuery(agentQueryOptions(agentId))
|
||||||
@@ -229,6 +232,18 @@ export function AgentDetailView({ agentId }: AgentDetailViewProps) {
|
|||||||
Install curl
|
Install curl
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
|
{onDelete ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDelete(agentId)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
Удалить агента
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo, type MouseEvent, type ReactNode } from 'react'
|
import { useCallback, useMemo, type MouseEvent, type ReactNode } from 'react'
|
||||||
import type { ColumnDef } from '@tanstack/react-table'
|
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 { Agent } from '@evofw/shared'
|
||||||
import type { Filter, FilterFieldConfig } from '@/components/reui/filters'
|
import type { Filter, FilterFieldConfig } from '@/components/reui/filters'
|
||||||
import { ResourcePage } from '@/components/reui-kit'
|
import { ResourcePage } from '@/components/reui-kit'
|
||||||
@@ -82,6 +82,7 @@ export type AgentFleetDataGridProps = {
|
|||||||
onApprove: (id: string) => void
|
onApprove: (id: string) => void
|
||||||
approvePending?: boolean
|
approvePending?: boolean
|
||||||
onCopyInstall: (curl: string) => void
|
onCopyInstall: (curl: string) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentFleetDataGrid({
|
export function AgentFleetDataGrid({
|
||||||
@@ -108,6 +109,7 @@ export function AgentFleetDataGrid({
|
|||||||
onApprove,
|
onApprove,
|
||||||
approvePending,
|
approvePending,
|
||||||
onCopyInstall,
|
onCopyInstall,
|
||||||
|
onDelete,
|
||||||
}: AgentFleetDataGridProps) {
|
}: AgentFleetDataGridProps) {
|
||||||
const handleCopy = useCallback(
|
const handleCopy = useCallback(
|
||||||
(curl: string, e?: MouseEvent) => {
|
(curl: string, e?: MouseEvent) => {
|
||||||
@@ -284,9 +286,9 @@ export function AgentFleetDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
size: 120,
|
size: 152,
|
||||||
minSize: 120,
|
minSize: 152,
|
||||||
maxSize: 120,
|
maxSize: 160,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
enableResizing: false,
|
enableResizing: false,
|
||||||
header: () => <span className="sr-only">Действия</span>,
|
header: () => <span className="sr-only">Действия</span>,
|
||||||
@@ -329,12 +331,24 @@ export function AgentFleetDataGrid({
|
|||||||
>
|
>
|
||||||
<PanelRight className="size-3.5" />
|
<PanelRight className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-destructive"
|
||||||
|
aria-label="Удалить"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onDelete(a.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[approvePending, handleCopy, onApprove, onSelect],
|
[approvePending, handleCopy, onApprove, onDelete, onSelect],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
computeFleetCounts,
|
computeFleetCounts,
|
||||||
fleetKpiCards,
|
fleetKpiCards,
|
||||||
} from '@/components/agents/agents-fleet-kpis'
|
} from '@/components/agents/agents-fleet-kpis'
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { agentsQueryOptions } from '@/queries'
|
import { agentsQueryOptions } from '@/queries'
|
||||||
import { apiFetch } from '@/lib/api'
|
import { apiFetch } from '@/lib/api'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
@@ -105,6 +106,7 @@ function AgentsPage() {
|
|||||||
const agentsQ = useQuery(agentsQueryOptions())
|
const agentsQ = useQuery(agentsQueryOptions())
|
||||||
const { copyToClipboard } = useCopyToClipboard()
|
const { copyToClipboard } = useCopyToClipboard()
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [deleteAgentId, setDeleteAgentId] = useState<string | null>(null)
|
||||||
const [filters, setFilters] = useState<Filter[]>([])
|
const [filters, setFilters] = useState<Filter[]>([])
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [activeTab, setActiveTab] = useState('all')
|
const [activeTab, setActiveTab] = useState('all')
|
||||||
@@ -155,6 +157,21 @@ function AgentsPage() {
|
|||||||
onError: (e: Error) => toast.error(e.message),
|
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 items = agentsQ.data?.items ?? []
|
||||||
const counts = useMemo(() => computeFleetCounts(items), [items])
|
const counts = useMemo(() => computeFleetCounts(items), [items])
|
||||||
const pendingIds = useMemo(
|
const pendingIds = useMemo(
|
||||||
@@ -330,6 +347,15 @@ function AgentsPage() {
|
|||||||
[setSearch],
|
[setSearch],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleDeleteAgent = useCallback((id: string) => {
|
||||||
|
setDeleteAgentId(id)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const deleteTargetName = useMemo(
|
||||||
|
() => items.find((a) => a.id === deleteAgentId)?.name,
|
||||||
|
[items, deleteAgentId],
|
||||||
|
)
|
||||||
|
|
||||||
const handleClearFilters = useCallback(() => {
|
const handleClearFilters = useCallback(() => {
|
||||||
setFilters([])
|
setFilters([])
|
||||||
setSearchQuery('')
|
setSearchQuery('')
|
||||||
@@ -454,6 +480,7 @@ function AgentsPage() {
|
|||||||
onApprove={(id) => approve.mutate(id)}
|
onApprove={(id) => approve.mutate(id)}
|
||||||
approvePending={approve.isPending}
|
approvePending={approve.isPending}
|
||||||
onCopyInstall={handleCopyInstall}
|
onCopyInstall={handleCopyInstall}
|
||||||
|
onDelete={handleDeleteAgent}
|
||||||
/>
|
/>
|
||||||
) : agentsQ.isError ? (
|
) : agentsQ.isError ? (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
@@ -533,6 +560,7 @@ function AgentsPage() {
|
|||||||
agents={filteredItems}
|
agents={filteredItems}
|
||||||
selectedId={detailOpen ? detailAgentId : null}
|
selectedId={detailOpen ? detailAgentId : null}
|
||||||
onSelect={handleSelectAgent}
|
onSelect={handleSelectAgent}
|
||||||
|
onDelete={handleDeleteAgent}
|
||||||
isLoading={agentsQ.isLoading}
|
isLoading={agentsQ.isLoading}
|
||||||
emptyTitle={
|
emptyTitle={
|
||||||
items.length === 0
|
items.length === 0
|
||||||
@@ -559,6 +587,24 @@ function AgentsPage() {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) setSearch({ agent: '' })
|
if (!open) setSearch({ agent: '' })
|
||||||
}}
|
}}
|
||||||
|
onDelete={handleDeleteAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={deleteAgentId !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setDeleteAgentId(null)
|
||||||
|
}}
|
||||||
|
title="Удалить агента?"
|
||||||
|
description={
|
||||||
|
deleteTargetName
|
||||||
|
? `Агент «${deleteTargetName}» будет удалён вместе с overrides, install-ссылками и статистикой. Это действие нельзя отменить.`
|
||||||
|
: 'Агент будет удалён вместе с overrides, install-ссылками и статистикой. Это действие нельзя отменить.'
|
||||||
|
}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (deleteAgentId) removeAgent.mutate(deleteAgentId)
|
||||||
|
}}
|
||||||
|
disabled={removeAgent.isPending}
|
||||||
/>
|
/>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user