feat(web): add "部署 Sub Agent" menu + page (pick resources + template -> deploy)
Customer-facing, plain Chinese (no env/technical jargon). New sidebar item
"部署 Sub Agent" -> /deploy-agent. The page:
- Step 1: pick which bound resources the agent may use (multi-select cards from
/api/resources).
- Step 2: pick an agent template (cards show Chinese name + description from
/api/heicode/agent-templates).
- Deploy -> POST /api/heicode/agents {template_id, binding_ids}.
- "我的 Agent" list (/api/heicode/agents) with status, copy-address, stop, delete.
Frontend builds clean (tsc + rsbuild; routeTree regenerated). zh label added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Bot,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
Database,
|
||||
GitBranch,
|
||||
RefreshCw,
|
||||
Rocket,
|
||||
Server,
|
||||
Square,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { api } from '@/lib/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
type ResourceItem = {
|
||||
id: number
|
||||
name: string
|
||||
resource_type: string
|
||||
provider: string
|
||||
external_id: string
|
||||
}
|
||||
|
||||
type TemplateItem = {
|
||||
template_id: string
|
||||
name: string
|
||||
description: string
|
||||
model: string
|
||||
}
|
||||
|
||||
type AgentItem = {
|
||||
agent_id: string
|
||||
template_id: string
|
||||
subdomain: string
|
||||
access_token: string
|
||||
binding_ids: number[]
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const RESOURCE_ICON: Record<string, typeof GitBranch> = {
|
||||
git: GitBranch,
|
||||
vm: Server,
|
||||
database: Database,
|
||||
blob: Database,
|
||||
}
|
||||
|
||||
function resourceTypeLabel(t: string): string {
|
||||
return (
|
||||
{ git: '代码仓库', vm: '虚拟机', database: '数据库', blob: '对象存储' }[t] ??
|
||||
t
|
||||
)
|
||||
}
|
||||
|
||||
function statusLabel(s: string): { text: string; cls: string } {
|
||||
const v = (s || '').toLowerCase()
|
||||
if (['running', 'active', 'ready'].includes(v))
|
||||
return { text: '运行中', cls: 'bg-emerald-500/15 text-emerald-300 ring-emerald-500/30' }
|
||||
if (['stopped'].includes(v))
|
||||
return { text: '已停止', cls: 'bg-muted/40 text-muted-foreground ring-border' }
|
||||
if (['failed', 'error', 'unhealthy', 'crashed'].includes(v))
|
||||
return { text: '异常', cls: 'bg-rose-500/15 text-rose-300 ring-rose-500/30' }
|
||||
return { text: s || '—', cls: 'bg-amber-500/15 text-amber-300 ring-amber-500/30' }
|
||||
}
|
||||
|
||||
async function listResources(): Promise<ResourceItem[]> {
|
||||
const res = await api.get<{ data?: { items?: ResourceItem[] } }>(
|
||||
'/api/resources/?status=active'
|
||||
)
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
async function listTemplates(): Promise<TemplateItem[]> {
|
||||
const res = await api.get<{ data?: { items?: TemplateItem[] } }>(
|
||||
'/api/heicode/agent-templates'
|
||||
)
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
async function listAgents(): Promise<AgentItem[]> {
|
||||
const res = await api.get<{ data?: { items?: AgentItem[] } }>(
|
||||
'/api/heicode/agents'
|
||||
)
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
export function DeploySubAgentPage() {
|
||||
const qc = useQueryClient()
|
||||
const [selectedBindings, setSelectedBindings] = useState<number[]>([])
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
||||
|
||||
const resourcesQ = useQuery({
|
||||
queryKey: ['resources', 'active'],
|
||||
queryFn: listResources,
|
||||
retry: false,
|
||||
})
|
||||
const templatesQ = useQuery({
|
||||
queryKey: ['agent-templates'],
|
||||
queryFn: listTemplates,
|
||||
retry: false,
|
||||
})
|
||||
const agentsQ = useQuery({
|
||||
queryKey: ['heicode-agents'],
|
||||
queryFn: listAgents,
|
||||
refetchInterval: 30_000,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const deploy = useMutation({
|
||||
mutationFn: () =>
|
||||
api.post('/api/heicode/agents', {
|
||||
template_id: selectedTemplate,
|
||||
binding_ids: selectedBindings,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('Agent 已开始部署')
|
||||
setSelectedBindings([])
|
||||
setSelectedTemplate('')
|
||||
void qc.invalidateQueries({ queryKey: ['heicode-agents'] })
|
||||
},
|
||||
onError: (e) =>
|
||||
toast.error(e instanceof Error ? e.message : '部署失败,请稍后重试'),
|
||||
})
|
||||
|
||||
const toggleBinding = (id: number) =>
|
||||
setSelectedBindings((s) =>
|
||||
s.includes(id) ? s.filter((x) => x !== id) : [...s, id]
|
||||
)
|
||||
|
||||
const resources = resourcesQ.data ?? []
|
||||
const templates = templatesQ.data ?? []
|
||||
const canDeploy = Boolean(selectedTemplate) && !deploy.isPending
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5'>
|
||||
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||
Heicode Manager
|
||||
</p>
|
||||
<h2 className='mt-1 flex items-center gap-2 text-xl font-semibold'>
|
||||
<Bot className='text-primary h-5 w-5' /> 部署 Sub Agent
|
||||
</h2>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
选择要让 Agent 使用的资源,挑一个 Agent 模板,一键部署到云端。部署后即可在桌面客户端里使用。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Step 1: pick resources */}
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5'>
|
||||
<h3 className='text-base font-semibold'>1. 选择资源</h3>
|
||||
<p className='text-muted-foreground mt-0.5 text-xs'>
|
||||
勾选这个 Agent 可以使用的已绑定资源(可多选,也可不选)。没绑定过的资源请先去「资源绑定」添加。
|
||||
</p>
|
||||
{resourcesQ.isLoading ? (
|
||||
<div className='mt-4 grid gap-3 sm:grid-cols-2'>
|
||||
{[0, 1].map((i) => (
|
||||
<Skeleton key={i} className='h-16 rounded-xl' />
|
||||
))}
|
||||
</div>
|
||||
) : resources.length === 0 ? (
|
||||
<p className='text-muted-foreground mt-4 rounded-xl border border-dashed p-6 text-center text-sm'>
|
||||
还没有绑定任何资源。
|
||||
</p>
|
||||
) : (
|
||||
<div className='mt-4 grid gap-3 sm:grid-cols-2'>
|
||||
{resources.map((r) => {
|
||||
const Icon = RESOURCE_ICON[r.resource_type] ?? Database
|
||||
const on = selectedBindings.includes(r.id)
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
key={r.id}
|
||||
onClick={() => toggleBinding(r.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-xl border p-3 text-left transition',
|
||||
on
|
||||
? 'border-primary/60 bg-[color-mix(in_oklch,var(--primary)_12%,transparent)]'
|
||||
: 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] hover:border-primary/40'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||
on ? 'bg-primary/20 text-primary' : 'bg-muted/50 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon className='h-4 w-4' />
|
||||
</span>
|
||||
<span className='min-w-0 flex-1'>
|
||||
<span className='block truncate text-sm font-medium'>{r.name}</span>
|
||||
<span className='text-muted-foreground block truncate text-[11px]'>
|
||||
{resourceTypeLabel(r.resource_type)} · {r.provider}
|
||||
</span>
|
||||
</span>
|
||||
{on && <CheckCircle2 className='text-primary h-4 w-4 shrink-0' />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Step 2: pick template */}
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5'>
|
||||
<h3 className='text-base font-semibold'>2. 选择 Agent 模板</h3>
|
||||
<p className='text-muted-foreground mt-0.5 text-xs'>选择一个 Agent 的角色与能力。</p>
|
||||
{templatesQ.isLoading ? (
|
||||
<div className='mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<Skeleton key={i} className='h-20 rounded-xl' />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
{templates.map((tpl) => {
|
||||
const on = selectedTemplate === tpl.template_id
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
key={tpl.template_id}
|
||||
onClick={() => setSelectedTemplate(tpl.template_id)}
|
||||
className={cn(
|
||||
'flex flex-col gap-1 rounded-xl border p-4 text-left transition',
|
||||
on
|
||||
? 'border-primary/60 bg-[color-mix(in_oklch,var(--primary)_12%,transparent)]'
|
||||
: 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] hover:border-primary/40'
|
||||
)}
|
||||
>
|
||||
<span className='flex items-center justify-between'>
|
||||
<span className='text-sm font-semibold'>{tpl.name}</span>
|
||||
{on && <CheckCircle2 className='text-primary h-4 w-4' />}
|
||||
</span>
|
||||
<span className='text-muted-foreground text-xs leading-relaxed'>
|
||||
{tpl.description}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Deploy bar */}
|
||||
<div className='flex items-center justify-between rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-4'>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
已选 <span className='text-foreground font-semibold'>{selectedBindings.length}</span> 个资源
|
||||
{selectedTemplate ? ',模板已选择' : ',未选择模板'}
|
||||
</p>
|
||||
<Button onClick={() => deploy.mutate()} disabled={!canDeploy} className='gap-1.5'>
|
||||
<Rocket className='h-4 w-4' />
|
||||
{deploy.isPending ? '部署中…' : '部署 Agent'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* My agents */}
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h3 className='text-base font-semibold'>我的 Agent</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='gap-1 text-xs'
|
||||
onClick={() => void qc.invalidateQueries({ queryKey: ['heicode-agents'] })}
|
||||
>
|
||||
<RefreshCw className='h-3.5 w-3.5' /> 刷新
|
||||
</Button>
|
||||
</div>
|
||||
{agentsQ.isLoading ? (
|
||||
<div className='mt-4 space-y-3'>
|
||||
{[0, 1].map((i) => (
|
||||
<Skeleton key={i} className='h-16 rounded-xl' />
|
||||
))}
|
||||
</div>
|
||||
) : (agentsQ.data ?? []).length === 0 ? (
|
||||
<p className='text-muted-foreground mt-4 rounded-xl border border-dashed p-6 text-center text-sm'>
|
||||
还没有部署过 Agent。
|
||||
</p>
|
||||
) : (
|
||||
<div className='mt-4 space-y-3'>
|
||||
{(agentsQ.data ?? []).map((a) => (
|
||||
<AgentRow key={a.agent_id} agent={a} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentRow({ agent }: { agent: AgentItem }) {
|
||||
const qc = useQueryClient()
|
||||
const st = statusLabel(agent.status)
|
||||
|
||||
const stop = useMutation({
|
||||
mutationFn: () => api.post(`/api/heicode/agents/${agent.agent_id}/stop`),
|
||||
onSuccess: () => {
|
||||
toast.success('已停止')
|
||||
void qc.invalidateQueries({ queryKey: ['heicode-agents'] })
|
||||
},
|
||||
onError: (e) => toast.error(e instanceof Error ? e.message : '操作失败'),
|
||||
})
|
||||
const del = useMutation({
|
||||
mutationFn: () => api.delete(`/api/heicode/agents/${agent.agent_id}`),
|
||||
onSuccess: () => {
|
||||
toast.success('已删除')
|
||||
void qc.invalidateQueries({ queryKey: ['heicode-agents'] })
|
||||
},
|
||||
onError: (e) => toast.error(e instanceof Error ? e.message : '操作失败'),
|
||||
})
|
||||
|
||||
const copy = () => {
|
||||
if (!agent.subdomain) return
|
||||
void navigator.clipboard.writeText(agent.subdomain)
|
||||
toast.success('已复制访问地址')
|
||||
}
|
||||
|
||||
return (
|
||||
<article className='flex flex-col gap-2 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='truncate text-sm font-medium'>{agent.template_id}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1 ring-inset',
|
||||
st.cls
|
||||
)}
|
||||
>
|
||||
{st.text}
|
||||
</span>
|
||||
</div>
|
||||
<p className='text-muted-foreground mt-0.5 truncate font-mono text-[11px]'>
|
||||
{agent.subdomain || '地址生成中…'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center gap-1'>
|
||||
<Button variant='outline' size='sm' className='h-8 gap-1 text-xs' onClick={copy} disabled={!agent.subdomain}>
|
||||
<Copy className='h-3.5 w-3.5' /> 复制地址
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-8 gap-1 text-xs'
|
||||
onClick={() => stop.mutate()}
|
||||
disabled={stop.isPending}
|
||||
>
|
||||
<Square className='h-3.5 w-3.5' /> 停止
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8 text-rose-400'
|
||||
onClick={() => del.mutate()}
|
||||
disabled={del.isPending}
|
||||
title='删除'
|
||||
>
|
||||
<Trash2 className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Bot,
|
||||
Command,
|
||||
Download,
|
||||
GitBranch,
|
||||
@@ -52,6 +53,11 @@ export function useSidebarData(): SidebarData {
|
||||
url: '/resources',
|
||||
icon: GitBranch,
|
||||
},
|
||||
{
|
||||
title: t('Deploy sub agent'),
|
||||
url: '/deploy-agent',
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
title: t('Agent runs'),
|
||||
url: '/deployments',
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"All Status": "所有状态",
|
||||
"All statuses": "全部状态",
|
||||
"Agent runs": "运行总览",
|
||||
"Deploy sub agent": "部署 Sub Agent",
|
||||
"Resource binding": "资源绑定",
|
||||
"Bind your git repos / VMs / databases / blob. Credentials go straight to Azure Key Vault — only a secret_ref is stored.": "绑定你的 git 仓库 / 虚拟机 / 数据库 / 对象存储。凭据直接写入 Azure Key Vault,本地只保存 secret_ref。",
|
||||
"No resources bound yet": "还没有绑定任何资源",
|
||||
|
||||
+22
@@ -46,6 +46,7 @@ import { Route as AuthenticatedKeysIndexRouteImport } from './routes/_authentica
|
||||
import { Route as AuthenticatedDevicesIndexRouteImport } from './routes/_authenticated/devices/index'
|
||||
import { Route as AuthenticatedDesktopClientIndexRouteImport } from './routes/_authenticated/desktop-client/index'
|
||||
import { Route as AuthenticatedDeploymentsIndexRouteImport } from './routes/_authenticated/deployments/index'
|
||||
import { Route as AuthenticatedDeployAgentIndexRouteImport } from './routes/_authenticated/deploy-agent/index'
|
||||
import { Route as AuthenticatedDashboardIndexRouteImport } from './routes/_authenticated/dashboard/index'
|
||||
import { Route as AuthenticatedChannelsIndexRouteImport } from './routes/_authenticated/channels/index'
|
||||
import { Route as AuthenticatedAvailableModelsIndexRouteImport } from './routes/_authenticated/available-models/index'
|
||||
@@ -267,6 +268,12 @@ const AuthenticatedDeploymentsIndexRoute =
|
||||
path: '/deployments/',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedDeployAgentIndexRoute =
|
||||
AuthenticatedDeployAgentIndexRouteImport.update({
|
||||
id: '/deploy-agent/',
|
||||
path: '/deploy-agent/',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedDashboardIndexRoute =
|
||||
AuthenticatedDashboardIndexRouteImport.update({
|
||||
id: '/dashboard/',
|
||||
@@ -440,6 +447,7 @@ export interface FileRoutesByFullPath {
|
||||
'/available-models/': typeof AuthenticatedAvailableModelsIndexRoute
|
||||
'/channels/': typeof AuthenticatedChannelsIndexRoute
|
||||
'/dashboard/': typeof AuthenticatedDashboardIndexRoute
|
||||
'/deploy-agent/': typeof AuthenticatedDeployAgentIndexRoute
|
||||
'/deployments/': typeof AuthenticatedDeploymentsIndexRoute
|
||||
'/desktop-client/': typeof AuthenticatedDesktopClientIndexRoute
|
||||
'/devices/': typeof AuthenticatedDevicesIndexRoute
|
||||
@@ -500,6 +508,7 @@ export interface FileRoutesByTo {
|
||||
'/available-models': typeof AuthenticatedAvailableModelsIndexRoute
|
||||
'/channels': typeof AuthenticatedChannelsIndexRoute
|
||||
'/dashboard': typeof AuthenticatedDashboardIndexRoute
|
||||
'/deploy-agent': typeof AuthenticatedDeployAgentIndexRoute
|
||||
'/deployments': typeof AuthenticatedDeploymentsIndexRoute
|
||||
'/desktop-client': typeof AuthenticatedDesktopClientIndexRoute
|
||||
'/devices': typeof AuthenticatedDevicesIndexRoute
|
||||
@@ -564,6 +573,7 @@ export interface FileRoutesById {
|
||||
'/_authenticated/available-models/': typeof AuthenticatedAvailableModelsIndexRoute
|
||||
'/_authenticated/channels/': typeof AuthenticatedChannelsIndexRoute
|
||||
'/_authenticated/dashboard/': typeof AuthenticatedDashboardIndexRoute
|
||||
'/_authenticated/deploy-agent/': typeof AuthenticatedDeployAgentIndexRoute
|
||||
'/_authenticated/deployments/': typeof AuthenticatedDeploymentsIndexRoute
|
||||
'/_authenticated/desktop-client/': typeof AuthenticatedDesktopClientIndexRoute
|
||||
'/_authenticated/devices/': typeof AuthenticatedDevicesIndexRoute
|
||||
@@ -627,6 +637,7 @@ export interface FileRouteTypes {
|
||||
| '/available-models/'
|
||||
| '/channels/'
|
||||
| '/dashboard/'
|
||||
| '/deploy-agent/'
|
||||
| '/deployments/'
|
||||
| '/desktop-client/'
|
||||
| '/devices/'
|
||||
@@ -687,6 +698,7 @@ export interface FileRouteTypes {
|
||||
| '/available-models'
|
||||
| '/channels'
|
||||
| '/dashboard'
|
||||
| '/deploy-agent'
|
||||
| '/deployments'
|
||||
| '/desktop-client'
|
||||
| '/devices'
|
||||
@@ -750,6 +762,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticated/available-models/'
|
||||
| '/_authenticated/channels/'
|
||||
| '/_authenticated/dashboard/'
|
||||
| '/_authenticated/deploy-agent/'
|
||||
| '/_authenticated/deployments/'
|
||||
| '/_authenticated/desktop-client/'
|
||||
| '/_authenticated/devices/'
|
||||
@@ -1060,6 +1073,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedDeploymentsIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRouteRoute
|
||||
}
|
||||
'/_authenticated/deploy-agent/': {
|
||||
id: '/_authenticated/deploy-agent/'
|
||||
path: '/deploy-agent'
|
||||
fullPath: '/deploy-agent/'
|
||||
preLoaderRoute: typeof AuthenticatedDeployAgentIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRouteRoute
|
||||
}
|
||||
'/_authenticated/dashboard/': {
|
||||
id: '/_authenticated/dashboard/'
|
||||
path: '/dashboard'
|
||||
@@ -1324,6 +1344,7 @@ interface AuthenticatedRouteRouteChildren {
|
||||
AuthenticatedAvailableModelsIndexRoute: typeof AuthenticatedAvailableModelsIndexRoute
|
||||
AuthenticatedChannelsIndexRoute: typeof AuthenticatedChannelsIndexRoute
|
||||
AuthenticatedDashboardIndexRoute: typeof AuthenticatedDashboardIndexRoute
|
||||
AuthenticatedDeployAgentIndexRoute: typeof AuthenticatedDeployAgentIndexRoute
|
||||
AuthenticatedDeploymentsIndexRoute: typeof AuthenticatedDeploymentsIndexRoute
|
||||
AuthenticatedDesktopClientIndexRoute: typeof AuthenticatedDesktopClientIndexRoute
|
||||
AuthenticatedDevicesIndexRoute: typeof AuthenticatedDevicesIndexRoute
|
||||
@@ -1353,6 +1374,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
|
||||
AuthenticatedAvailableModelsIndexRoute,
|
||||
AuthenticatedChannelsIndexRoute: AuthenticatedChannelsIndexRoute,
|
||||
AuthenticatedDashboardIndexRoute: AuthenticatedDashboardIndexRoute,
|
||||
AuthenticatedDeployAgentIndexRoute: AuthenticatedDeployAgentIndexRoute,
|
||||
AuthenticatedDeploymentsIndexRoute: AuthenticatedDeploymentsIndexRoute,
|
||||
AuthenticatedDesktopClientIndexRoute: AuthenticatedDesktopClientIndexRoute,
|
||||
AuthenticatedDevicesIndexRoute: AuthenticatedDevicesIndexRoute,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { DeploySubAgentPage } from '@/features/deploy-agent/deploy-agent-page'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/deploy-agent/')({
|
||||
component: DeploySubAgentPage,
|
||||
})
|
||||
Reference in New Issue
Block a user