feat(api, web): enhance agent management and linting capabilities
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 2m17s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Added a new linting command for OpenAPI specifications in the package.json, improving code quality checks.
- Updated frontend documentation to clarify component usage and structure, including detailed descriptions for `SettingsShell` and `Auth callback`.
- Refactored agent-related API routes to streamline control-plane functionalities, consolidating multiple routes for better organization.
- Improved error handling in the API to provide more informative responses for validation errors, enhancing user feedback during interactions.

These changes enhance the overall development experience and improve the management of agents within the application.
This commit is contained in:
Denozordec
2026-07-30 14:13:05 +07:00
parent fb95ef22b3
commit f160992d94
68 changed files with 3155 additions and 6899 deletions
+45 -109
View File
@@ -1,24 +1,21 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { useCallback, useMemo, useState } from 'react'
import type { Filter, FilterFieldConfig } from '@/components/reui/filters'
import {
Check,
CheckCircle2,
CircleAlertIcon,
FilterIcon,
Inbox,
LayoutGridIcon,
ListIcon,
Plus,
SearchIcon,
ShieldIcon,
TableIcon,
UserPlus,
WifiOff,
} from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import type { Filter, FilterFieldConfig } from '@/components/reui/filters'
import { Filters } from '@/components/reui/filters'
import {
applyFiltersToData,
KpiStatGrid,
@@ -30,7 +27,6 @@ import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import {
@@ -38,26 +34,21 @@ import {
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
import { CountedLineTabs } from '@/components/counted-line-tabs'
import { AddAgentSheet } from '@/components/agents/add-agent-sheet'
import { AgentCardsGrid } from '@/components/agents/agent-cards-grid'
import { AgentDetailSheet } from '@/components/agents/agent-detail-sheet'
import { AgentFleetDataGrid } from '@/components/agents/agent-fleet-data-grid'
import { AgentsFleetChrome } from '@/components/agents/agents-fleet-chrome'
import {
computeFleetCounts,
fleetKpiCards,
} from '@/components/agents/agents-fleet-kpis'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { agentsQueryOptions } from '@/queries'
import { QueryState } from '@/components/query-state'
import { agentsQueryOptions, settingsQueryOptions } from '@/queries'
import { apiFetch } from '@/lib/api'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@evofw/ui/components/button'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from '@evofw/ui/components/input-group'
import { Separator } from '@evofw/ui/components/separator'
import {
ToggleGroup,
ToggleGroupItem,
@@ -104,6 +95,7 @@ function AgentsPage() {
const { agent: detailAgentId, view } = Route.useSearch()
const qc = useQueryClient()
const agentsQ = useQuery(agentsQueryOptions())
const settingsQ = useQuery(settingsQueryOptions())
const { copyToClipboard } = useCopyToClipboard()
const [createOpen, setCreateOpen] = useState(false)
const [deleteAgentId, setDeleteAgentId] = useState<string | null>(null)
@@ -112,6 +104,7 @@ function AgentsPage() {
const [activeTab, setActiveTab] = useState('all')
const detailOpen = Boolean(detailAgentId)
const showQuickActions = settingsQ.data?.show_quick_actions !== 'false'
const setSearch = useCallback(
(next: Partial<AgentsSearch>) => {
@@ -410,7 +403,7 @@ function AgentsPage() {
<KpiStatGrid cards={kpiCards} isLoading={agentsQ.isLoading} />
<QuickActionGrid actions={quickActions} />
{showQuickActions ? <QuickActionGrid actions={quickActions} /> : null}
{counts.pending > 0 ? (
<Frame dense spacing="sm">
@@ -482,101 +475,44 @@ function AgentsPage() {
onCopyInstall={handleCopyInstall}
onDelete={handleDeleteAgent}
/>
) : agentsQ.isError ? (
<Alert variant="destructive">
<CircleAlertIcon />
<AlertTitle>Ошибка загрузки</AlertTitle>
<AlertDescription className="flex flex-col gap-2">
<span>
{agentsQ.error?.message ?? 'Не удалось загрузить данные'}
</span>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => void agentsQ.refetch()}
>
Повторить
</Button>
</AlertDescription>
</Alert>
) : (
<Frame dense variant="default" spacing="sm" className="w-full">
<FramePanel className="p-0 shadow-none!">
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
<CountedLineTabs
tabs={countedTabs}
value={activeTab}
onValueChange={setActiveTab}
/>
</div>
<Separator />
<div className="flex flex-wrap items-center justify-between gap-3 px-(--frame-panel-header-px) py-(--frame-panel-header-py)">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<Filters
filters={filters}
fields={filterFields}
onChange={setFilters}
size="default"
trigger={
<Button
type="button"
variant="outline"
aria-label="Фильтры"
>
<FilterIcon className="size-4" aria-hidden="true" />
Фильтры
</Button>
}
/>
<InputGroup className="h-8 max-w-xs min-w-[12rem] flex-1">
<InputGroupAddon align="inline-start">
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск агентов…"
aria-label="Поиск агентов…"
/>
</InputGroup>
</div>
<div className="flex flex-wrap items-center gap-2">
{viewToggle}
{filters.length > 0 || searchQuery ? (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleClearFilters}
>
Сбросить
</Button>
) : null}
</div>
</div>
<div className="px-(--frame-panel-header-px) pb-(--frame-panel-header-py)">
<AgentCardsGrid
agents={filteredItems}
selectedId={detailOpen ? detailAgentId : null}
onSelect={handleSelectAgent}
onDelete={handleDeleteAgent}
isLoading={agentsQ.isLoading}
emptyTitle={
items.length === 0
? 'Нет агентов'
: 'Нет записей по фильтрам'
}
emptyDescription={
items.length === 0
? 'Создайте агента — он появится в каталоге как Invited с командой установки.'
: undefined
}
emptyAction={items.length === 0 ? addButton : undefined}
/>
</div>
</FramePanel>
</Frame>
<QueryState
isError={agentsQ.isError}
error={agentsQ.error}
onRetry={() => void agentsQ.refetch()}
>
<AgentsFleetChrome
tabs={countedTabs}
activeTab={activeTab}
onTabChange={setActiveTab}
filterFields={filterFields}
filters={filters}
onFiltersChange={setFilters}
onClearFilters={handleClearFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
toolbarExtra={viewToggle}
>
<AgentCardsGrid
agents={filteredItems}
selectedId={detailOpen ? detailAgentId : null}
onSelect={handleSelectAgent}
onDelete={handleDeleteAgent}
isLoading={agentsQ.isLoading}
emptyTitle={
items.length === 0
? 'Нет агентов'
: 'Нет записей по фильтрам'
}
emptyDescription={
items.length === 0
? 'Создайте агента — он появится в каталоге как Invited с командой установки.'
: undefined
}
emptyAction={items.length === 0 ? addButton : undefined}
/>
</AgentsFleetChrome>
</QueryState>
)}
<AddAgentSheet open={createOpen} onOpenChange={setCreateOpen} />