feat(web): bulk-операции в таблице агентов и подтверждения опасных действий
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user