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:
2026-06-03 00:39:28 +08:00
co-authored by Claude Opus 4.8
parent ae251cf70e
commit 8710de7c20
5 changed files with 487 additions and 0 deletions
@@ -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>
)
}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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,
})