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 {
|
||||
Command,
|
||||
Download,
|
||||
GitBranch,
|
||||
LayoutDashboard,
|
||||
Rocket,
|
||||
Settings,
|
||||
@@ -46,6 +47,11 @@ export function useSidebarData(): SidebarData {
|
||||
url: '/dashboard',
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: t('Resource binding'),
|
||||
url: '/resources',
|
||||
icon: GitBranch,
|
||||
},
|
||||
{
|
||||
title: t('Agent runs'),
|
||||
url: '/deployments',
|
||||
|
||||
+14
@@ -212,6 +212,20 @@
|
||||
"All Status": "所有状态",
|
||||
"All statuses": "全部状态",
|
||||
"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": "正在运行",
|
||||
"Agent runs in progress": "进行中的 Agent 运行",
|
||||
"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 AuthenticatedSystemSettingsIndexRouteImport } from './routes/_authenticated/system-settings/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 AuthenticatedProfileIndexRouteImport } from './routes/_authenticated/profile/index'
|
||||
import { Route as AuthenticatedPlaygroundIndexRouteImport } from './routes/_authenticated/playground/index'
|
||||
@@ -213,6 +214,12 @@ const AuthenticatedSubscriptionsIndexRoute =
|
||||
path: '/subscriptions/',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedResourcesIndexRoute =
|
||||
AuthenticatedResourcesIndexRouteImport.update({
|
||||
id: '/resources/',
|
||||
path: '/resources/',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedRedemptionCodesIndexRoute =
|
||||
AuthenticatedRedemptionCodesIndexRouteImport.update({
|
||||
id: '/redemption-codes/',
|
||||
@@ -441,6 +448,7 @@ export interface FileRoutesByFullPath {
|
||||
'/playground/': typeof AuthenticatedPlaygroundIndexRoute
|
||||
'/profile/': typeof AuthenticatedProfileIndexRoute
|
||||
'/redemption-codes/': typeof AuthenticatedRedemptionCodesIndexRoute
|
||||
'/resources/': typeof AuthenticatedResourcesIndexRoute
|
||||
'/subscriptions/': typeof AuthenticatedSubscriptionsIndexRoute
|
||||
'/system-settings/': typeof AuthenticatedSystemSettingsIndexRoute
|
||||
'/usage-logs/': typeof AuthenticatedUsageLogsIndexRoute
|
||||
@@ -500,6 +508,7 @@ export interface FileRoutesByTo {
|
||||
'/playground': typeof AuthenticatedPlaygroundIndexRoute
|
||||
'/profile': typeof AuthenticatedProfileIndexRoute
|
||||
'/redemption-codes': typeof AuthenticatedRedemptionCodesIndexRoute
|
||||
'/resources': typeof AuthenticatedResourcesIndexRoute
|
||||
'/subscriptions': typeof AuthenticatedSubscriptionsIndexRoute
|
||||
'/system-settings': typeof AuthenticatedSystemSettingsIndexRoute
|
||||
'/usage-logs': typeof AuthenticatedUsageLogsIndexRoute
|
||||
@@ -563,6 +572,7 @@ export interface FileRoutesById {
|
||||
'/_authenticated/playground/': typeof AuthenticatedPlaygroundIndexRoute
|
||||
'/_authenticated/profile/': typeof AuthenticatedProfileIndexRoute
|
||||
'/_authenticated/redemption-codes/': typeof AuthenticatedRedemptionCodesIndexRoute
|
||||
'/_authenticated/resources/': typeof AuthenticatedResourcesIndexRoute
|
||||
'/_authenticated/subscriptions/': typeof AuthenticatedSubscriptionsIndexRoute
|
||||
'/_authenticated/system-settings/': typeof AuthenticatedSystemSettingsIndexRoute
|
||||
'/_authenticated/usage-logs/': typeof AuthenticatedUsageLogsIndexRoute
|
||||
@@ -625,6 +635,7 @@ export interface FileRouteTypes {
|
||||
| '/playground/'
|
||||
| '/profile/'
|
||||
| '/redemption-codes/'
|
||||
| '/resources/'
|
||||
| '/subscriptions/'
|
||||
| '/system-settings/'
|
||||
| '/usage-logs/'
|
||||
@@ -684,6 +695,7 @@ export interface FileRouteTypes {
|
||||
| '/playground'
|
||||
| '/profile'
|
||||
| '/redemption-codes'
|
||||
| '/resources'
|
||||
| '/subscriptions'
|
||||
| '/system-settings'
|
||||
| '/usage-logs'
|
||||
@@ -746,6 +758,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticated/playground/'
|
||||
| '/_authenticated/profile/'
|
||||
| '/_authenticated/redemption-codes/'
|
||||
| '/_authenticated/resources/'
|
||||
| '/_authenticated/subscriptions/'
|
||||
| '/_authenticated/system-settings/'
|
||||
| '/_authenticated/usage-logs/'
|
||||
@@ -984,6 +997,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedSubscriptionsIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRouteRoute
|
||||
}
|
||||
'/_authenticated/resources/': {
|
||||
id: '/_authenticated/resources/'
|
||||
path: '/resources'
|
||||
fullPath: '/resources/'
|
||||
preLoaderRoute: typeof AuthenticatedResourcesIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRouteRoute
|
||||
}
|
||||
'/_authenticated/redemption-codes/': {
|
||||
id: '/_authenticated/redemption-codes/'
|
||||
path: '/redemption-codes'
|
||||
@@ -1312,6 +1332,7 @@ interface AuthenticatedRouteRouteChildren {
|
||||
AuthenticatedPlaygroundIndexRoute: typeof AuthenticatedPlaygroundIndexRoute
|
||||
AuthenticatedProfileIndexRoute: typeof AuthenticatedProfileIndexRoute
|
||||
AuthenticatedRedemptionCodesIndexRoute: typeof AuthenticatedRedemptionCodesIndexRoute
|
||||
AuthenticatedResourcesIndexRoute: typeof AuthenticatedResourcesIndexRoute
|
||||
AuthenticatedSubscriptionsIndexRoute: typeof AuthenticatedSubscriptionsIndexRoute
|
||||
AuthenticatedUsageLogsIndexRoute: typeof AuthenticatedUsageLogsIndexRoute
|
||||
AuthenticatedUsersIndexRoute: typeof AuthenticatedUsersIndexRoute
|
||||
@@ -1341,6 +1362,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
|
||||
AuthenticatedProfileIndexRoute: AuthenticatedProfileIndexRoute,
|
||||
AuthenticatedRedemptionCodesIndexRoute:
|
||||
AuthenticatedRedemptionCodesIndexRoute,
|
||||
AuthenticatedResourcesIndexRoute: AuthenticatedResourcesIndexRoute,
|
||||
AuthenticatedSubscriptionsIndexRoute: AuthenticatedSubscriptionsIndexRoute,
|
||||
AuthenticatedUsageLogsIndexRoute: AuthenticatedUsageLogsIndexRoute,
|
||||
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