feat(api, web): enhance agent installation process with invited status and policy support
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m48s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Updated the agent enrollment process to include an 'invited' status, allowing for better tracking of agent states.
- Implemented support for install links that can now include an `install_link_id`, facilitating the transition from invited to pending status upon enrollment.
- Enhanced the MikroTik installation script to include the `EvofwInstallLinkId` for better tracking and management.
- Added new API endpoints for fetching agent policies and serving MikroTik-specific installation scripts.
- Improved the web UI to reflect the new agent statuses and provide copyable installation commands for agents.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 01:45:38 +07:00
co-authored by Cursor
parent ef56da4d91
commit d5784b9f35
20 changed files with 793 additions and 104 deletions
+58 -7
View File
@@ -1,7 +1,7 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Check, Plus, Trash2 } from 'lucide-react'
import { Check, Copy, Plus, Trash2 } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { Filter, FilterFieldConfig } from '@/components/reui/filters'
@@ -20,15 +20,21 @@ import { ConfirmDialog } from '@/components/confirm-dialog'
import { AddAgentSheet } from '@/components/agents/add-agent-sheet'
import { agentsQueryOptions } from '@/queries'
import { apiFetch } from '@/lib/api'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@evofw/ui/components/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@evofw/ui/components/tooltip'
import type { Agent } from '@evofw/shared'
/**
* Agents list — ResourcePage (Frame + tabs + Filters + DataGrid).
* Preview: https://reui.io/preview/base/data-grid-filtering-2
* Copyable install: https://reui.io/preview/base/settings-14
* Empty: https://reui.io/preview/base/empty-state-7
* Create Sheet: https://reui.io/preview/base/sheet-1 · sheet-8
* Docs: https://reui.io/blocks · https://reui.io/components/sheet
*/
export const Route = createFileRoute('/_auth/agents/')({
component: AgentsPage,
@@ -37,6 +43,7 @@ export const Route = createFileRoute('/_auth/agents/')({
function AgentsPage() {
const qc = useQueryClient()
const agentsQ = useQuery(agentsQueryOptions())
const { copyToClipboard } = useCopyToClipboard()
const [createOpen, setCreateOpen] = useState(false)
const [filters, setFilters] = useState<Filter[]>([])
const [activeTab, setActiveTab] = useState('all')
@@ -88,6 +95,7 @@ function AgentsPage() {
label: 'Статус',
type: 'select',
options: [
{ value: 'invited', label: 'invited' },
{ value: 'approved', label: 'approved' },
{ value: 'pending', label: 'pending' },
{ value: 'revoked', label: 'revoked' },
@@ -118,6 +126,15 @@ function AgentsPage() {
return item.status === tabId
}, [])
const handleCopyCurl = useCallback(
(curl: string) => {
if (!curl) return
copyToClipboard(curl)
toast.success('Скопировано')
},
[copyToClipboard],
)
const columns: ColumnDef<Agent>[] = useMemo(
() => [
{
@@ -152,6 +169,39 @@ function AgentsPage() {
),
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
id: 'install',
enableSorting: false,
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Install" />
),
cell: ({ row }) => {
const curl = row.original.install_curl
if (!curl) {
return <DataGridMutedCell></DataGridMutedCell>
}
return (
<Tooltip>
<TooltipTrigger
render={
<Button
size="sm"
variant="outline"
className="max-w-[14rem] font-mono text-xs"
onClick={() => handleCopyCurl(curl)}
/>
}
>
<Copy data-icon="inline-start" className="size-3.5" />
<span className="truncate">{curl}</span>
</TooltipTrigger>
<TooltipContent className="max-w-sm break-all font-mono text-xs">
{curl}
</TooltipContent>
</Tooltip>
)
},
},
{
accessorKey: 'policy_mode',
header: ({ column }) => (
@@ -217,7 +267,7 @@ function AgentsPage() {
},
},
],
[approve, revoke],
[approve, revoke, handleCopyCurl],
)
const addButton = (
@@ -231,7 +281,7 @@ function AgentsPage() {
<PageShell>
<PageHeader
title="Агенты"
description="Linux / MikroTik — short install, approve, policy mode"
description="Linux / MikroTik — invite, install, approve"
actions={addButton}
/>
@@ -248,8 +298,9 @@ function AgentsPage() {
getFilterFieldValue={getFilterFieldValue}
tabs={[
{ id: 'all', label: 'Все' },
{ id: 'approved', label: 'Approved' },
{ id: 'invited', label: 'Invited' },
{ id: 'pending', label: 'Pending' },
{ id: 'approved', label: 'Approved' },
{ id: 'revoked', label: 'Revoked' },
]}
activeTab={activeTab}
@@ -262,7 +313,7 @@ function AgentsPage() {
emptyState={{
title: 'Нет агентов',
description:
'Создайте install-ссылку, выполните curl на хосте и одобрите запрос.',
'Создайте агента — он появится в списке как Invited с командой установки.',
action: addButton,
}}
/>
@@ -275,7 +326,7 @@ function AgentsPage() {
if (!open) setDeleteId(null)
}}
title="Удалить агента?"
description="Агент и связанные назначения будут удалены."
description="Агент, install-ссылка и связанные назначения будут удалены."
onConfirm={() => {
if (deleteId) remove.mutate(deleteId)
}}