feat(api, web): enhance agent management and linting capabilities
- 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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user