feat(api, web): enhance agent installation process with invited status and policy support
- 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:
@@ -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)
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user