feat(web): resource binding page in the Manager console (5 types)
Re-adds Resource binding to the Heicode cockpit (/resources). Add bindings for GitHub / Gitea (type=git + provider), VM (ssh), database, blob; non-secret fields go to metadata, the credential is written to Azure Key Vault via POST /api/resources/:id/secret and only the secret_ref is shown (masked). List + unbind. Backend already supported this; now functional since KV is up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,439 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import {
|
||||||
|
Cloud,
|
||||||
|
Database,
|
||||||
|
GitBranch,
|
||||||
|
Plus,
|
||||||
|
Server,
|
||||||
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from '@/components/ui/sheet'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
|
// A bound resource the user has registered. Credentials never live here — only
|
||||||
|
// a secret_ref pointer into Azure Key Vault.
|
||||||
|
type ResourceItem = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
resource_type: string
|
||||||
|
provider: string
|
||||||
|
external_id: string
|
||||||
|
secret_ref: string
|
||||||
|
status: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Field = { k: string; label: string; ph?: string; textarea?: boolean }
|
||||||
|
|
||||||
|
// The 5 binding kinds (gitea / github / vm / database / blob). github + gitea
|
||||||
|
// map to resource_type=git distinguished by provider. Non-secret inputs go to
|
||||||
|
// metadata; secret inputs go to the separate /secret call (written to KV).
|
||||||
|
type Kind = {
|
||||||
|
key: string
|
||||||
|
resource_type: string
|
||||||
|
provider: string
|
||||||
|
label: string
|
||||||
|
icon: typeof GitBranch
|
||||||
|
fields: Field[]
|
||||||
|
secret: Field[]
|
||||||
|
externalFrom: string // which field becomes external_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const KINDS: Kind[] = [
|
||||||
|
{
|
||||||
|
key: 'github',
|
||||||
|
resource_type: 'git',
|
||||||
|
provider: 'github',
|
||||||
|
label: 'GitHub 仓库',
|
||||||
|
icon: GitBranch,
|
||||||
|
externalFrom: 'repo_url',
|
||||||
|
fields: [
|
||||||
|
{ k: 'repo_url', label: '仓库 URL', ph: 'https://github.com/owner/repo' },
|
||||||
|
{ k: 'default_branch', label: '默认分支', ph: 'main' },
|
||||||
|
],
|
||||||
|
secret: [
|
||||||
|
{ k: 'token', label: 'Personal Access Token (PAT)', ph: 'ghp_…' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitea',
|
||||||
|
resource_type: 'git',
|
||||||
|
provider: 'gitea',
|
||||||
|
label: 'Gitea 仓库',
|
||||||
|
icon: GitBranch,
|
||||||
|
externalFrom: 'repo_url',
|
||||||
|
fields: [
|
||||||
|
{ k: 'repo_url', label: '仓库 URL', ph: 'https://gitea.example.com/owner/repo' },
|
||||||
|
{ k: 'api_base', label: 'Gitea 地址', ph: 'https://gitea.example.com' },
|
||||||
|
{ k: 'default_branch', label: '默认分支', ph: 'main' },
|
||||||
|
],
|
||||||
|
secret: [{ k: 'token', label: '访问令牌 (PAT)', ph: '…' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vm',
|
||||||
|
resource_type: 'vm',
|
||||||
|
provider: 'ssh',
|
||||||
|
label: '虚拟机 (SSH)',
|
||||||
|
icon: Server,
|
||||||
|
externalFrom: 'host',
|
||||||
|
fields: [
|
||||||
|
{ k: 'host', label: '主机 / IP', ph: '20.24.50.121' },
|
||||||
|
{ k: 'port', label: '端口', ph: '22' },
|
||||||
|
{ k: 'user', label: '用户名', ph: 'root' },
|
||||||
|
],
|
||||||
|
secret: [
|
||||||
|
{ k: 'ssh_key', label: 'SSH 私钥', ph: '-----BEGIN OPENSSH PRIVATE KEY-----', textarea: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'database',
|
||||||
|
resource_type: 'database',
|
||||||
|
provider: 'postgres',
|
||||||
|
label: '数据库',
|
||||||
|
icon: Database,
|
||||||
|
externalFrom: 'host',
|
||||||
|
fields: [
|
||||||
|
{ k: 'engine', label: '引擎', ph: 'postgres / mysql' },
|
||||||
|
{ k: 'host', label: '主机', ph: 'db.example.com' },
|
||||||
|
{ k: 'port', label: '端口', ph: '5432' },
|
||||||
|
{ k: 'db_name', label: '数据库名', ph: 'app' },
|
||||||
|
{ k: 'username', label: '用户名', ph: 'app' },
|
||||||
|
],
|
||||||
|
secret: [{ k: 'database_password', label: '数据库密码', ph: '…' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blob',
|
||||||
|
resource_type: 'blob',
|
||||||
|
provider: 'azure',
|
||||||
|
label: '对象存储 (Blob)',
|
||||||
|
icon: Cloud,
|
||||||
|
externalFrom: 'account',
|
||||||
|
fields: [
|
||||||
|
{ k: 'account', label: '账号', ph: 'mystorage' },
|
||||||
|
{ k: 'container', label: '容器', ph: 'artifacts' },
|
||||||
|
],
|
||||||
|
secret: [{ k: 'access_key', label: '存储密钥 / SAS', ph: '…' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function maskRef(ref: string): string {
|
||||||
|
if (!ref) return '—'
|
||||||
|
const i = ref.lastIndexOf('/')
|
||||||
|
const head = i >= 0 ? ref.slice(0, i + 1) : ''
|
||||||
|
const leaf = i >= 0 ? ref.slice(i + 1) : ref
|
||||||
|
return head + (leaf.length <= 8 ? leaf : leaf.slice(0, 8) + '…')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listResources(): Promise<ResourceItem[]> {
|
||||||
|
const res = await api.get<{ data?: { items?: ResourceItem[] } }>(
|
||||||
|
'/api/resources/?status=active'
|
||||||
|
)
|
||||||
|
return res.data?.data?.items ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourceBindingsPage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const [openKind, setOpenKind] = useState<Kind | null>(null)
|
||||||
|
|
||||||
|
const { data = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['resources', 'active'],
|
||||||
|
queryFn: listResources,
|
||||||
|
refetchInterval: 60_000,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const revoke = useMutation({
|
||||||
|
mutationFn: (id: number) => api.delete(`/api/resources/${id}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('Resource unbound'))
|
||||||
|
void qc.invalidateQueries({ queryKey: ['resources'] })
|
||||||
|
},
|
||||||
|
onError: (e) => toast.error(e instanceof Error ? e.message : t('Failed')),
|
||||||
|
})
|
||||||
|
|
||||||
|
const grouped = useMemo(() => {
|
||||||
|
const by: Record<string, ResourceItem[]> = {}
|
||||||
|
for (const r of data) (by[r.resource_type] ??= []).push(r)
|
||||||
|
return by
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='space-y-5 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5'>
|
||||||
|
<header className='flex flex-col gap-3 border-b border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pb-4 sm:flex-row sm:items-end sm:justify-between'>
|
||||||
|
<div>
|
||||||
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||||
|
Heicode Manager
|
||||||
|
</p>
|
||||||
|
<h2 className='mt-1 text-xl font-semibold'>{t('Resource binding')}</h2>
|
||||||
|
<p className='text-muted-foreground mt-1 text-sm'>
|
||||||
|
{t(
|
||||||
|
'Bind your git repos / VMs / databases / blob. Credentials go straight to Azure Key Vault — only a secret_ref is stored.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{KINDS.map((k) => (
|
||||||
|
<Button
|
||||||
|
key={k.key}
|
||||||
|
type='button'
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
className='h-9 gap-1.5 rounded-xl text-xs'
|
||||||
|
onClick={() => setOpenKind(k)}
|
||||||
|
>
|
||||||
|
<Plus className='h-3.5 w-3.5' />
|
||||||
|
{k.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
|
{[0, 1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className='h-24 rounded-xl' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-10 text-center'>
|
||||||
|
<ShieldCheck className='text-muted-foreground mx-auto h-6 w-6' />
|
||||||
|
<p className='mt-3 text-sm font-medium'>{t('No resources bound yet')}</p>
|
||||||
|
<p className='text-muted-foreground mt-1 text-xs'>
|
||||||
|
{t('Use the buttons above to bind a repo, VM, database or blob.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-5'>
|
||||||
|
{Object.entries(grouped).map(([type, items]) => (
|
||||||
|
<div key={type}>
|
||||||
|
<p className='text-muted-foreground mb-2 text-[11px] font-semibold tracking-[0.14em] uppercase'>
|
||||||
|
{type} · {items.length}
|
||||||
|
</p>
|
||||||
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
|
{items.map((r) => (
|
||||||
|
<article
|
||||||
|
key={r.id}
|
||||||
|
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'
|
||||||
|
>
|
||||||
|
<div className='flex items-start justify-between gap-2'>
|
||||||
|
<div className='min-w-0'>
|
||||||
|
<p className='truncate text-sm font-medium'>{r.name}</p>
|
||||||
|
<p className='text-muted-foreground mt-0.5 truncate font-mono text-[11px]'>
|
||||||
|
{r.provider} · {r.external_id || '—'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
size='icon'
|
||||||
|
className='h-7 w-7 shrink-0 text-rose-400'
|
||||||
|
disabled={revoke.isPending}
|
||||||
|
onClick={() => revoke.mutate(r.id)}
|
||||||
|
title={t('Unbind')}
|
||||||
|
>
|
||||||
|
<Trash2 className='h-3.5 w-3.5' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'inline-flex w-fit items-center gap-1 rounded-full px-2 py-0.5 font-mono text-[10px] ring-1 ring-inset',
|
||||||
|
r.secret_ref
|
||||||
|
? 'bg-emerald-500/10 text-emerald-300 ring-emerald-500/25'
|
||||||
|
: 'bg-amber-500/10 text-amber-300 ring-amber-500/25'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ShieldCheck className='h-3 w-3' />
|
||||||
|
{r.secret_ref ? maskRef(r.secret_ref) : t('no credential')}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<BindSheet
|
||||||
|
kind={openKind}
|
||||||
|
onClose={() => setOpenKind(null)}
|
||||||
|
onDone={() => {
|
||||||
|
setOpenKind(null)
|
||||||
|
void qc.invalidateQueries({ queryKey: ['resources'] })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BindSheet({
|
||||||
|
kind,
|
||||||
|
onClose,
|
||||||
|
onDone,
|
||||||
|
}: {
|
||||||
|
kind: Kind | null
|
||||||
|
onClose: () => void
|
||||||
|
onDone: () => void
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [vals, setVals] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
const submit = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!kind) return
|
||||||
|
const metadata: Record<string, string> = {}
|
||||||
|
for (const f of kind.fields) {
|
||||||
|
const v = (vals[f.k] || '').trim()
|
||||||
|
if (v) metadata[f.k] = v
|
||||||
|
}
|
||||||
|
const external_id = (vals[kind.externalFrom] || '').trim()
|
||||||
|
// 1) create the binding (no secret)
|
||||||
|
const created = await api.post<{ data?: { id: number } }>(
|
||||||
|
'/api/resources/',
|
||||||
|
{
|
||||||
|
name: name.trim() || kind.label,
|
||||||
|
resource_type: kind.resource_type,
|
||||||
|
provider: kind.provider,
|
||||||
|
external_id,
|
||||||
|
metadata,
|
||||||
|
permission_scope: {},
|
||||||
|
constraints: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const id = created.data?.data?.id
|
||||||
|
if (!id) throw new Error(t('Failed to create binding'))
|
||||||
|
// 2) write the credential to Key Vault (only secret fields)
|
||||||
|
const secretData: Record<string, string> = {}
|
||||||
|
for (const s of kind.secret) {
|
||||||
|
const v = (vals[s.k] || '').trim()
|
||||||
|
if (v) secretData[s.k] = v
|
||||||
|
}
|
||||||
|
if (Object.keys(secretData).length > 0) {
|
||||||
|
await api.post(`/api/resources/${id}/secret`, { data: secretData })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('Resource bound'))
|
||||||
|
setName('')
|
||||||
|
setVals({})
|
||||||
|
onDone()
|
||||||
|
},
|
||||||
|
onError: (e) =>
|
||||||
|
toast.error(e instanceof Error ? e.message : t('Failed to bind')),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!kind) return null
|
||||||
|
const Icon = kind.icon
|
||||||
|
return (
|
||||||
|
<Sheet open={Boolean(kind)} onOpenChange={(o) => !o && onClose()}>
|
||||||
|
<SheetContent className='w-[min(520px,96vw)] overflow-y-auto sm:max-w-none'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle className='flex items-center gap-2'>
|
||||||
|
<Icon className='text-primary h-5 w-5' />
|
||||||
|
{t('Bind')} {kind.label}
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
{t(
|
||||||
|
'Credentials are written to Azure Key Vault. Only a secret_ref is kept here — never the plaintext.'
|
||||||
|
)}
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className='mt-4 grid gap-3 text-sm'>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='rb-name'>{t('Display name')}</Label>
|
||||||
|
<Input
|
||||||
|
id='rb-name'
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder={kind.label}
|
||||||
|
className='mt-1'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{kind.fields.map((f) => (
|
||||||
|
<div key={f.k}>
|
||||||
|
<Label htmlFor={`rb-${f.k}`}>{f.label}</Label>
|
||||||
|
<Input
|
||||||
|
id={`rb-${f.k}`}
|
||||||
|
value={vals[f.k] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVals((v) => ({ ...v, [f.k]: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder={f.ph}
|
||||||
|
className='mt-1 font-mono text-xs'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className='bg-card/40 mt-1 rounded-lg border border-dashed p-3'>
|
||||||
|
<p className='text-foreground flex items-center gap-1.5 text-xs font-medium'>
|
||||||
|
<ShieldCheck className='text-primary h-3.5 w-3.5' />
|
||||||
|
{t('Credential (goes to Key Vault)')}
|
||||||
|
</p>
|
||||||
|
{kind.secret.map((s) =>
|
||||||
|
s.textarea ? (
|
||||||
|
<div key={s.k} className='mt-2'>
|
||||||
|
<Label htmlFor={`rb-${s.k}`}>{s.label}</Label>
|
||||||
|
<textarea
|
||||||
|
id={`rb-${s.k}`}
|
||||||
|
rows={4}
|
||||||
|
value={vals[s.k] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVals((v) => ({ ...v, [s.k]: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder={s.ph}
|
||||||
|
className='border-input mt-1 w-full rounded-md border bg-transparent px-3 py-2 font-mono text-xs outline-none focus:ring-2 focus:ring-[color-mix(in_oklch,var(--primary)_40%,transparent)]'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={s.k} className='mt-2'>
|
||||||
|
<Label htmlFor={`rb-${s.k}`}>{s.label}</Label>
|
||||||
|
<Input
|
||||||
|
id={`rb-${s.k}`}
|
||||||
|
type='password'
|
||||||
|
value={vals[s.k] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVals((v) => ({ ...v, [s.k]: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder={s.ph}
|
||||||
|
className='mt-1 font-mono text-xs'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 flex justify-end gap-2'>
|
||||||
|
<Button type='button' variant='outline' onClick={onClose}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
disabled={submit.isPending}
|
||||||
|
onClick={() => submit.mutate()}
|
||||||
|
>
|
||||||
|
{submit.isPending ? t('Binding...') : t('Bind')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
Download,
|
Download,
|
||||||
|
GitBranch,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Rocket,
|
Rocket,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -46,6 +47,11 @@ export function useSidebarData(): SidebarData {
|
|||||||
url: '/dashboard',
|
url: '/dashboard',
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('Resource binding'),
|
||||||
|
url: '/resources',
|
||||||
|
icon: GitBranch,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('Agent runs'),
|
title: t('Agent runs'),
|
||||||
url: '/deployments',
|
url: '/deployments',
|
||||||
|
|||||||
+14
@@ -212,6 +212,20 @@
|
|||||||
"All Status": "所有状态",
|
"All Status": "所有状态",
|
||||||
"All statuses": "全部状态",
|
"All statuses": "全部状态",
|
||||||
"Agent runs": "运行总览",
|
"Agent runs": "运行总览",
|
||||||
|
"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": "还没有绑定任何资源",
|
||||||
|
"Use the buttons above to bind a repo, VM, database or blob.": "用上方按钮绑定仓库 / 虚拟机 / 数据库 / 对象存储。",
|
||||||
|
"no credential": "未配置凭据",
|
||||||
|
"Resource bound": "绑定成功",
|
||||||
|
"Resource unbound": "已解绑",
|
||||||
|
"Failed to bind": "绑定失败",
|
||||||
|
"Failed to create binding": "创建绑定失败",
|
||||||
|
"Unbind": "解绑",
|
||||||
|
"Bind": "绑定",
|
||||||
|
"Binding...": "绑定中…",
|
||||||
|
"Credential (goes to Key Vault)": "凭据(写入 Key Vault)",
|
||||||
|
"Bind ": "绑定 ",
|
||||||
"Running agents": "正在运行",
|
"Running agents": "正在运行",
|
||||||
"Agent runs in progress": "进行中的 Agent 运行",
|
"Agent runs in progress": "进行中的 Agent 运行",
|
||||||
"Finished with a deliverable": "已完成且有交付物",
|
"Finished with a deliverable": "已完成且有交付物",
|
||||||
|
|||||||
+22
@@ -37,6 +37,7 @@ import { Route as AuthenticatedUsersIndexRouteImport } from './routes/_authentic
|
|||||||
import { Route as AuthenticatedUsageLogsIndexRouteImport } from './routes/_authenticated/usage-logs/index'
|
import { Route as AuthenticatedUsageLogsIndexRouteImport } from './routes/_authenticated/usage-logs/index'
|
||||||
import { Route as AuthenticatedSystemSettingsIndexRouteImport } from './routes/_authenticated/system-settings/index'
|
import { Route as AuthenticatedSystemSettingsIndexRouteImport } from './routes/_authenticated/system-settings/index'
|
||||||
import { Route as AuthenticatedSubscriptionsIndexRouteImport } from './routes/_authenticated/subscriptions/index'
|
import { Route as AuthenticatedSubscriptionsIndexRouteImport } from './routes/_authenticated/subscriptions/index'
|
||||||
|
import { Route as AuthenticatedResourcesIndexRouteImport } from './routes/_authenticated/resources/index'
|
||||||
import { Route as AuthenticatedRedemptionCodesIndexRouteImport } from './routes/_authenticated/redemption-codes/index'
|
import { Route as AuthenticatedRedemptionCodesIndexRouteImport } from './routes/_authenticated/redemption-codes/index'
|
||||||
import { Route as AuthenticatedProfileIndexRouteImport } from './routes/_authenticated/profile/index'
|
import { Route as AuthenticatedProfileIndexRouteImport } from './routes/_authenticated/profile/index'
|
||||||
import { Route as AuthenticatedPlaygroundIndexRouteImport } from './routes/_authenticated/playground/index'
|
import { Route as AuthenticatedPlaygroundIndexRouteImport } from './routes/_authenticated/playground/index'
|
||||||
@@ -213,6 +214,12 @@ const AuthenticatedSubscriptionsIndexRoute =
|
|||||||
path: '/subscriptions/',
|
path: '/subscriptions/',
|
||||||
getParentRoute: () => AuthenticatedRouteRoute,
|
getParentRoute: () => AuthenticatedRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthenticatedResourcesIndexRoute =
|
||||||
|
AuthenticatedResourcesIndexRouteImport.update({
|
||||||
|
id: '/resources/',
|
||||||
|
path: '/resources/',
|
||||||
|
getParentRoute: () => AuthenticatedRouteRoute,
|
||||||
|
} as any)
|
||||||
const AuthenticatedRedemptionCodesIndexRoute =
|
const AuthenticatedRedemptionCodesIndexRoute =
|
||||||
AuthenticatedRedemptionCodesIndexRouteImport.update({
|
AuthenticatedRedemptionCodesIndexRouteImport.update({
|
||||||
id: '/redemption-codes/',
|
id: '/redemption-codes/',
|
||||||
@@ -441,6 +448,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/playground/': typeof AuthenticatedPlaygroundIndexRoute
|
'/playground/': typeof AuthenticatedPlaygroundIndexRoute
|
||||||
'/profile/': typeof AuthenticatedProfileIndexRoute
|
'/profile/': typeof AuthenticatedProfileIndexRoute
|
||||||
'/redemption-codes/': typeof AuthenticatedRedemptionCodesIndexRoute
|
'/redemption-codes/': typeof AuthenticatedRedemptionCodesIndexRoute
|
||||||
|
'/resources/': typeof AuthenticatedResourcesIndexRoute
|
||||||
'/subscriptions/': typeof AuthenticatedSubscriptionsIndexRoute
|
'/subscriptions/': typeof AuthenticatedSubscriptionsIndexRoute
|
||||||
'/system-settings/': typeof AuthenticatedSystemSettingsIndexRoute
|
'/system-settings/': typeof AuthenticatedSystemSettingsIndexRoute
|
||||||
'/usage-logs/': typeof AuthenticatedUsageLogsIndexRoute
|
'/usage-logs/': typeof AuthenticatedUsageLogsIndexRoute
|
||||||
@@ -500,6 +508,7 @@ export interface FileRoutesByTo {
|
|||||||
'/playground': typeof AuthenticatedPlaygroundIndexRoute
|
'/playground': typeof AuthenticatedPlaygroundIndexRoute
|
||||||
'/profile': typeof AuthenticatedProfileIndexRoute
|
'/profile': typeof AuthenticatedProfileIndexRoute
|
||||||
'/redemption-codes': typeof AuthenticatedRedemptionCodesIndexRoute
|
'/redemption-codes': typeof AuthenticatedRedemptionCodesIndexRoute
|
||||||
|
'/resources': typeof AuthenticatedResourcesIndexRoute
|
||||||
'/subscriptions': typeof AuthenticatedSubscriptionsIndexRoute
|
'/subscriptions': typeof AuthenticatedSubscriptionsIndexRoute
|
||||||
'/system-settings': typeof AuthenticatedSystemSettingsIndexRoute
|
'/system-settings': typeof AuthenticatedSystemSettingsIndexRoute
|
||||||
'/usage-logs': typeof AuthenticatedUsageLogsIndexRoute
|
'/usage-logs': typeof AuthenticatedUsageLogsIndexRoute
|
||||||
@@ -563,6 +572,7 @@ export interface FileRoutesById {
|
|||||||
'/_authenticated/playground/': typeof AuthenticatedPlaygroundIndexRoute
|
'/_authenticated/playground/': typeof AuthenticatedPlaygroundIndexRoute
|
||||||
'/_authenticated/profile/': typeof AuthenticatedProfileIndexRoute
|
'/_authenticated/profile/': typeof AuthenticatedProfileIndexRoute
|
||||||
'/_authenticated/redemption-codes/': typeof AuthenticatedRedemptionCodesIndexRoute
|
'/_authenticated/redemption-codes/': typeof AuthenticatedRedemptionCodesIndexRoute
|
||||||
|
'/_authenticated/resources/': typeof AuthenticatedResourcesIndexRoute
|
||||||
'/_authenticated/subscriptions/': typeof AuthenticatedSubscriptionsIndexRoute
|
'/_authenticated/subscriptions/': typeof AuthenticatedSubscriptionsIndexRoute
|
||||||
'/_authenticated/system-settings/': typeof AuthenticatedSystemSettingsIndexRoute
|
'/_authenticated/system-settings/': typeof AuthenticatedSystemSettingsIndexRoute
|
||||||
'/_authenticated/usage-logs/': typeof AuthenticatedUsageLogsIndexRoute
|
'/_authenticated/usage-logs/': typeof AuthenticatedUsageLogsIndexRoute
|
||||||
@@ -625,6 +635,7 @@ export interface FileRouteTypes {
|
|||||||
| '/playground/'
|
| '/playground/'
|
||||||
| '/profile/'
|
| '/profile/'
|
||||||
| '/redemption-codes/'
|
| '/redemption-codes/'
|
||||||
|
| '/resources/'
|
||||||
| '/subscriptions/'
|
| '/subscriptions/'
|
||||||
| '/system-settings/'
|
| '/system-settings/'
|
||||||
| '/usage-logs/'
|
| '/usage-logs/'
|
||||||
@@ -684,6 +695,7 @@ export interface FileRouteTypes {
|
|||||||
| '/playground'
|
| '/playground'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/redemption-codes'
|
| '/redemption-codes'
|
||||||
|
| '/resources'
|
||||||
| '/subscriptions'
|
| '/subscriptions'
|
||||||
| '/system-settings'
|
| '/system-settings'
|
||||||
| '/usage-logs'
|
| '/usage-logs'
|
||||||
@@ -746,6 +758,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authenticated/playground/'
|
| '/_authenticated/playground/'
|
||||||
| '/_authenticated/profile/'
|
| '/_authenticated/profile/'
|
||||||
| '/_authenticated/redemption-codes/'
|
| '/_authenticated/redemption-codes/'
|
||||||
|
| '/_authenticated/resources/'
|
||||||
| '/_authenticated/subscriptions/'
|
| '/_authenticated/subscriptions/'
|
||||||
| '/_authenticated/system-settings/'
|
| '/_authenticated/system-settings/'
|
||||||
| '/_authenticated/usage-logs/'
|
| '/_authenticated/usage-logs/'
|
||||||
@@ -984,6 +997,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthenticatedSubscriptionsIndexRouteImport
|
preLoaderRoute: typeof AuthenticatedSubscriptionsIndexRouteImport
|
||||||
parentRoute: typeof AuthenticatedRouteRoute
|
parentRoute: typeof AuthenticatedRouteRoute
|
||||||
}
|
}
|
||||||
|
'/_authenticated/resources/': {
|
||||||
|
id: '/_authenticated/resources/'
|
||||||
|
path: '/resources'
|
||||||
|
fullPath: '/resources/'
|
||||||
|
preLoaderRoute: typeof AuthenticatedResourcesIndexRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRouteRoute
|
||||||
|
}
|
||||||
'/_authenticated/redemption-codes/': {
|
'/_authenticated/redemption-codes/': {
|
||||||
id: '/_authenticated/redemption-codes/'
|
id: '/_authenticated/redemption-codes/'
|
||||||
path: '/redemption-codes'
|
path: '/redemption-codes'
|
||||||
@@ -1312,6 +1332,7 @@ interface AuthenticatedRouteRouteChildren {
|
|||||||
AuthenticatedPlaygroundIndexRoute: typeof AuthenticatedPlaygroundIndexRoute
|
AuthenticatedPlaygroundIndexRoute: typeof AuthenticatedPlaygroundIndexRoute
|
||||||
AuthenticatedProfileIndexRoute: typeof AuthenticatedProfileIndexRoute
|
AuthenticatedProfileIndexRoute: typeof AuthenticatedProfileIndexRoute
|
||||||
AuthenticatedRedemptionCodesIndexRoute: typeof AuthenticatedRedemptionCodesIndexRoute
|
AuthenticatedRedemptionCodesIndexRoute: typeof AuthenticatedRedemptionCodesIndexRoute
|
||||||
|
AuthenticatedResourcesIndexRoute: typeof AuthenticatedResourcesIndexRoute
|
||||||
AuthenticatedSubscriptionsIndexRoute: typeof AuthenticatedSubscriptionsIndexRoute
|
AuthenticatedSubscriptionsIndexRoute: typeof AuthenticatedSubscriptionsIndexRoute
|
||||||
AuthenticatedUsageLogsIndexRoute: typeof AuthenticatedUsageLogsIndexRoute
|
AuthenticatedUsageLogsIndexRoute: typeof AuthenticatedUsageLogsIndexRoute
|
||||||
AuthenticatedUsersIndexRoute: typeof AuthenticatedUsersIndexRoute
|
AuthenticatedUsersIndexRoute: typeof AuthenticatedUsersIndexRoute
|
||||||
@@ -1341,6 +1362,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
|
|||||||
AuthenticatedProfileIndexRoute: AuthenticatedProfileIndexRoute,
|
AuthenticatedProfileIndexRoute: AuthenticatedProfileIndexRoute,
|
||||||
AuthenticatedRedemptionCodesIndexRoute:
|
AuthenticatedRedemptionCodesIndexRoute:
|
||||||
AuthenticatedRedemptionCodesIndexRoute,
|
AuthenticatedRedemptionCodesIndexRoute,
|
||||||
|
AuthenticatedResourcesIndexRoute: AuthenticatedResourcesIndexRoute,
|
||||||
AuthenticatedSubscriptionsIndexRoute: AuthenticatedSubscriptionsIndexRoute,
|
AuthenticatedSubscriptionsIndexRoute: AuthenticatedSubscriptionsIndexRoute,
|
||||||
AuthenticatedUsageLogsIndexRoute: AuthenticatedUsageLogsIndexRoute,
|
AuthenticatedUsageLogsIndexRoute: AuthenticatedUsageLogsIndexRoute,
|
||||||
AuthenticatedUsersIndexRoute: AuthenticatedUsersIndexRoute,
|
AuthenticatedUsersIndexRoute: AuthenticatedUsersIndexRoute,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { ResourceBindingsPage } from '@/features/resources/resources-page'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/_authenticated/resources/')({
|
||||||
|
component: ResourceBindingsPage,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user