feat(web): bulk-операции в таблице агентов и подтверждения опасных действий

This commit is contained in:
Denozordec
2026-09-25 01:07:05 +07:00
parent 0f7daf77e3
commit 98e4dcdefb
5 changed files with 252 additions and 19 deletions
@@ -38,6 +38,7 @@ import { AgentBlockedPorts } from '@/components/agents/agent-blocked-ports'
import { AgentHostFirewall } from '@/components/agents/agent-host-firewall'
import { AgentPortAcl } from '@/components/agents/agent-port-acl'
import { CountedLineTabs } from '@/components/counted-line-tabs'
import { ConfirmDialog } from '@/components/confirm-dialog'
import {
AgentCloneSetsSheet,
AgentOverrideSheet,
@@ -79,6 +80,8 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) {
const installRef = useRef<HTMLDivElement>(null)
const [overrideOpen, setOverrideOpen] = useState(false)
const [cloneOpen, setCloneOpen] = useState(false)
const [revokeOpen, setRevokeOpen] = useState(false)
const [resetStatsOpen, setResetStatsOpen] = useState(false)
const [fwTab, setFwTab] = useState('host')
const revoke = useMutation({
@@ -187,10 +190,10 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) {
<Button
variant="outline"
size="sm"
onClick={() => revoke.mutate()}
onClick={() => setRevokeOpen(true)}
disabled={revoke.isPending}
>
Revoke
Отозвать
</Button>
) : null}
{a.install_curl ? (
@@ -283,7 +286,7 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) {
size="sm"
variant="outline"
disabled={resetStats.isPending}
onClick={() => resetStats.mutate()}
onClick={() => setResetStatsOpen(true)}
>
Сбросить
</Button>
@@ -374,6 +377,32 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) {
open={cloneOpen}
onOpenChange={setCloneOpen}
/>
<ConfirmDialog
open={revokeOpen}
onOpenChange={setRevokeOpen}
title="Отозвать агента?"
confirmLabel="Отозвать"
description={`Агент «${a.name}» потеряет доступ к API управления и перестанет получать обновления политики. Действие нельзя отменить.`}
onConfirm={() => {
setRevokeOpen(false)
revoke.mutate()
}}
disabled={revoke.isPending}
/>
<ConfirmDialog
open={resetStatsOpen}
onOpenChange={setResetStatsOpen}
title="Сбросить статистику?"
confirmLabel="Сбросить"
description={`Счётчики пакетов и история статистики агента «${a.name}» будут обнулены и удалены. Действие нельзя отменить.`}
onConfirm={() => {
setResetStatsOpen(false)
resetStats.mutate()
}}
disabled={resetStats.isPending}
/>
</>
)
}
@@ -1,5 +1,5 @@
import { useCallback, useMemo, type MouseEvent, type ReactNode } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { ColumnDef, RowSelectionState } from '@tanstack/react-table'
import { Check, Copy, PanelRight, Trash2 } from 'lucide-react'
import type { Agent } from '@evofw/shared'
import type { Filter, FilterFieldConfig } from '@/components/reui/filters'
@@ -55,6 +55,15 @@ export type AgentFleetDataGridProps = {
onRetry?: () => void
emptyAction?: ReactNode
toolbarExtra?: ReactNode
/** Bulk-операции: controlled selection пробрасывается в ResourcePage. */
enableRowSelection?: boolean
rowSelection?: RowSelectionState
onRowSelectionChange?: (rowSelection: RowSelectionState) => void
selectionToolbar?: (ctx: {
selectedIds: string[]
selectedCount: number
clearSelection: () => void
}) => ReactNode
onSelect: (id: string) => void
onApprove: (id: string) => void
approvePending?: boolean
@@ -82,6 +91,10 @@ export function AgentFleetDataGrid({
onRetry,
emptyAction,
toolbarExtra,
enableRowSelection,
rowSelection,
onRowSelectionChange,
selectionToolbar,
onSelect,
onApprove,
approvePending,
@@ -346,6 +359,10 @@ export function AgentFleetDataGrid({
getSearchText={getSearchText}
tableLayout={{ width: 'fixed', columnsResizable: true }}
onRowClick={(row) => onSelect(row.id)}
enableRowSelection={enableRowSelection}
rowSelection={rowSelection}
onRowSelectionChange={onRowSelectionChange}
selectionToolbar={selectionToolbar}
tabs={tabs}
activeTab={activeTab}
onTabChange={onTabChange}
+4 -1
View File
@@ -18,6 +18,8 @@ interface ConfirmDialogProps {
title: string
description: string
confirmLabel?: string
/** 'default' для позитивных действий (утвердить), 'destructive' — для опасных. */
confirmVariant?: 'default' | 'destructive'
cancelLabel?: string
onConfirm: () => void
disabled?: boolean
@@ -30,6 +32,7 @@ export function ConfirmDialog({
title,
description,
confirmLabel = 'Удалить',
confirmVariant = 'destructive',
cancelLabel = 'Отмена',
onConfirm,
disabled,
@@ -46,7 +49,7 @@ export function ConfirmDialog({
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{cancelLabel}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={onConfirm}>
<AlertDialogAction variant={confirmVariant} onClick={onConfirm}>
{confirmLabel}
</AlertDialogAction>
</AlertDialogFooter>
@@ -80,6 +80,9 @@ export interface ResourcePageProps<T extends object> {
emptyState?: { title: string; description?: string; action?: ReactNode }
pageSize?: number
enableRowSelection?: boolean
/** Controlled selection — page owns state, so bulk-действия видят выбор. */
rowSelection?: RowSelectionState
onRowSelectionChange?: (rowSelection: RowSelectionState) => void
selectionToolbar?: (ctx: {
selectedIds: string[]
selectedCount: number
@@ -141,6 +144,8 @@ export function ResourcePage<T extends object>({
emptyState,
pageSize = 10,
enableRowSelection = false,
rowSelection: rowSelectionProp,
onRowSelectionChange: onRowSelectionChangeProp,
selectionToolbar,
toolbarExtra,
hideHeader = false,
@@ -156,7 +161,30 @@ export function ResourcePage<T extends object>({
const activeTab = controlledTab ?? internalTab
const [sorting, setSorting] = useState<SortingState>([])
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const isSelectionControlled = rowSelectionProp !== undefined
const [internalRowSelection, setInternalRowSelection] =
useState<RowSelectionState>({})
const rowSelection = isSelectionControlled
? rowSelectionProp
: internalRowSelection
const handleRowSelectionChange = useCallback(
(
updater: RowSelectionState | ((prev: RowSelectionState) => RowSelectionState),
) => {
const current = isSelectionControlled
? rowSelectionProp
: internalRowSelection
const next = typeof updater === 'function' ? updater(current) : updater
if (isSelectionControlled) onRowSelectionChangeProp?.(next)
else setInternalRowSelection(next)
},
[
isSelectionControlled,
rowSelectionProp,
internalRowSelection,
onRowSelectionChangeProp,
],
)
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(
() => columnPinningProp ?? {},
)
@@ -224,8 +252,8 @@ export function ResourcePage<T extends object>({
const selectedCount = selectedIds.length
const clearSelection = useCallback(() => {
setRowSelection({})
}, [])
handleRowSelectionChange({})
}, [handleRowSelectionChange])
const tableLayout = useMemo(
() => ({
@@ -253,7 +281,7 @@ export function ResourcePage<T extends object>({
// без drag-handles — enableColumnResizing остаётся false.
enableColumnResizing: false,
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
onRowSelectionChange: handleRowSelectionChange,
onPaginationChange: setPagination,
onColumnPinningChange: setColumnPinning,
getCoreRowModel: getCoreRowModel(),