diff --git a/src/modules/package-api.ts b/src/modules/package-api.ts new file mode 100644 index 0000000..2e3bd8d --- /dev/null +++ b/src/modules/package-api.ts @@ -0,0 +1,299 @@ +import { createQueryApi } from '@kevisual/query/api'; +const api = { + "cnb": { + /** + * 获取指定制品的详细信息 + * + * @param data - Request parameters + * @param data.slug - {string} 资源路径, 如 my-org/my-registry + * @param data.type - {string} 制品类型 + * @param data.name - {string} 制品名称 + */ + "get-package": { + "path": "cnb", + "key": "get-package", + "description": "获取制品详情, 参数 slug, type, name", + "metadata": { + "tags": [ + "package" + ], + "args": { + "slug": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "资源路径, 如 my-org/my-registry" + }, + "type": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品类型" + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品名称" + } + }, + "skill": "get-package", + "title": "获取制品详情", + "summary": "获取指定制品的详细信息", + "url": "/api/router", + "source": "query-proxy-api" + } + }, + /** + * 获取制品标签的详细信息 + * + * @param data - Request parameters + * @param data.slug - {string} 资源路径, 如 my-org/my-registry + * @param data.type - {string} 制品类型 + * @param data.name - {string} 制品名称 + * @param data.tag - {string} 标签名称 + */ + "get-package-tag": { + "path": "cnb", + "key": "get-package-tag", + "description": "获取制品标签详情, 参数 slug, type, name, tag", + "metadata": { + "tags": [ + "package" + ], + "args": { + "slug": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "资源路径, 如 my-org/my-registry" + }, + "type": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品类型" + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品名称" + }, + "tag": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "标签名称" + } + }, + "skill": "get-package-tag", + "title": "获取制品标签详情", + "summary": "获取制品标签的详细信息", + "url": "/api/router", + "source": "query-proxy-api" + } + }, + /** + * 查询制品列表 + * + * @param data - Request parameters + * @param data.slug - {string} 资源路径, 如 my-org/my-registry + * @param data.type - {string} 制品类型: all, docker, helm, docker-model, maven, npm, ohpm, pypi, nuget, composer, conan, cargo + * @param data.ordering - {string} 排序类型: pull_count, last_push_at, name_ascend, name_descend + * @param data.name - {string} 关键字,搜索制品名称 + * @param data.page - {number} 页码 + * @param data.page_size - {number} 每页数量 + */ + "list-packages": { + "path": "cnb", + "key": "list-packages", + "description": "查询制品列表, 参数 slug, type", + "metadata": { + "tags": [ + "package" + ], + "args": { + "slug": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "资源路径, 如 my-org/my-registry" + }, + "type": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品类型: all, docker, helm, docker-model, maven, npm, ohpm, pypi, nuget, composer, conan, cargo" + }, + "ordering": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "排序类型: pull_count, last_push_at, name_ascend, name_descend", + "optional": true + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "关键字,搜索制品名称", + "optional": true + }, + "page": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "description": "页码", + "optional": true + }, + "page_size": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "description": "每页数量", + "optional": true + } + }, + "skill": "list-packages", + "title": "查询制品列表", + "summary": "查询制品列表", + "url": "/api/router", + "source": "query-proxy-api" + } + }, + /** + * 获取制品的标签列表 + * + * @param data - Request parameters + * @param data.slug - {string} 资源路径, 如 my-org/my-registry + * @param data.type - {string} 制品类型 + * @param data.name - {string} 制品名称 + * @param data.page - {number} 页码 + * @param data.page_size - {number} 每页数量 + */ + "list-package-tags": { + "path": "cnb", + "key": "list-package-tags", + "description": "获取制品标签列表, 参数 slug, type, name", + "metadata": { + "tags": [ + "package" + ], + "args": { + "slug": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "资源路径, 如 my-org/my-registry" + }, + "type": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品类型" + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品名称" + }, + "page": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "description": "页码", + "optional": true + }, + "page_size": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "description": "每页数量", + "optional": true + } + }, + "skill": "list-package-tags", + "title": "获取制品标签列表", + "summary": "获取制品的标签列表", + "url": "/api/router", + "source": "query-proxy-api" + } + }, + /** + * 删除指定的制品 + * + * @param data - Request parameters + * @param data.slug - {string} 资源路径, 如 my-org/my-registry + * @param data.type - {string} 制品类型 + * @param data.name - {string} 制品名称 + */ + "delete-package": { + "path": "cnb", + "key": "delete-package", + "description": "删除制品, 参数 slug, type, name", + "metadata": { + "tags": [ + "package" + ], + "args": { + "slug": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "资源路径, 如 my-org/my-registry" + }, + "type": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品类型" + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品名称" + } + }, + "skill": "delete-package", + "title": "删除制品", + "summary": "删除指定的制品", + "url": "/api/router", + "source": "query-proxy-api" + } + }, + /** + * 删除制品的指定标签 + * + * @param data - Request parameters + * @param data.slug - {string} 资源路径, 如 my-org/my-registry + * @param data.type - {string} 制品类型 + * @param data.name - {string} 制品名称 + * @param data.tag - {string} 标签名称 + */ + "delete-package-tag": { + "path": "cnb", + "key": "delete-package-tag", + "description": "删除制品标签, 参数 slug, type, name, tag", + "metadata": { + "tags": [ + "package" + ], + "args": { + "slug": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "资源路径, 如 my-org/my-registry" + }, + "type": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品类型" + }, + "name": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "制品名称" + }, + "tag": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "description": "标签名称" + } + }, + "skill": "delete-package-tag", + "title": "删除制品标签", + "summary": "删除制品的指定标签", + "url": "/api/router", + "source": "query-proxy-api" + } + } + } +} as const; +const queryApi = createQueryApi({ api }); + +export { queryApi }; + +// 使用例子,方法为对应的方法,data 为对应的 args 的参数的定义数据 +// queryApi['user-app'].delete(data) diff --git a/src/pages/cnb-packages/detail/page.tsx b/src/pages/cnb-packages/detail/page.tsx new file mode 100644 index 0000000..feb020b --- /dev/null +++ b/src/pages/cnb-packages/detail/page.tsx @@ -0,0 +1,235 @@ +import { useSearch } from "@tanstack/react-router"; +import { MarkItem, usePackageStore } from "../store"; +import { PackageItem } from "../store/package-type"; +import { useShallow } from "zustand/shallow"; +import { useEffect, useState } from "react"; +import dayjs from "dayjs"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + CardAction, +} from "@/components/ui/card"; +import { SidebarLayout } from "@/pages/sidebar/components"; +import { ArrowLeftIcon, Copy, MoreVertical, Info, ExternalLink } from "lucide-react"; +import { toast } from "sonner"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +export const App = () => { + const searchParams = useSearch({ strict: false }) as { slug?: string; id?: string }; + const { slug, id } = searchParams; + + const packageStore = usePackageStore(useShallow((state) => ({ + packagesList: state.packagesList, + packagesListLoading: state.packagesListLoading, + getPackagesList: state.getPackagesList, + getItem: state.getItem, + }))); + + const [detailItem, setDetailItem] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + + useEffect(() => { + if (slug) { + packageStore.getPackagesList({ slug }); + } + }, [slug]); + + useEffect(() => { + if (id) { + setDetailLoading(true); + packageStore.getItem(id).then((data) => { + setDetailItem(data); + setDetailLoading(false); + }); + } + }, [id]); + const onCopy = (data: PackageItem) => { + // kevisual/dev-env 取 kevisual,删除最后一个斜杠和后面的内容 + const _group = slug?.split?.('/')?.filter(Boolean); + _group?.shift?.(); + const group = _group?.[0]; + const value = `docker.cnb.cool/${slug}/${data.package}:latest` + } + const onDetail = (item: PackageItem) => { + + } + const onToPage = (item: PackageItem) => { + const type = 'docker'; + const url = `https://cnb.cool/${slug}/-/packages?type=${type}&ordering=last_push_at`; + } + return ( + +
+
+ +

制品详情

+
+ + {id && ( +
+ {detailLoading ? ( +
加载中...
+ ) : detailItem ? ( + + + {detailItem.title || '未命名'} + {detailItem.summary || '暂无描述'} + + + {detailItem.tags && detailItem.tags.length > 0 && ( +
+ {detailItem.tags.map((tag, index) => ( + {tag} + ))} +
+ )} +

+ ID: {detailItem.id} +

+
+
+ ) : ( +
未找到制品
+ )} +
+ )} + +

制品列表

+ {packageStore.packagesListLoading ? ( +
加载中...
+ ) : packageStore.packagesList.length === 0 ? ( +
暂无制品数据
+ ) : ( +
+ {packageStore.packagesList.map((item: PackageItem) => { + const dockerValue = `docker.cnb.cool/${slug}/${item.package}:latest`; + const dockerPullValue = `docker pull ${dockerValue}`; + return ( + + + {item.package || '未命名'} + 类型: {item.package_type} + +
+ + { + navigator.clipboard.writeText(dockerValue).then(() => { + toast.success('已复制 docker value'); + }); + }} + > + + + } + /> + 复制 + + + { + toast.info('功能开发中'); + }} + > + + + } + /> + 详情 + + + + + + } + /> + + { + navigator.clipboard.writeText(dockerPullValue).then(() => { + toast.success('已复制 docker pull 命令'); + }); + }} + className="cursor-pointer" + > + + 复制 docker registry + + { + const type = 'docker'; + const url = `https://cnb.cool/${slug}/-/packages?type=${type}&ordering=last_push_at`; + window.open(url, '_blank'); + }} + className="cursor-pointer" + > + + 跳转 page + + + +
+
+
+ + {item.labels && item.labels.length > 0 && ( +
+ {item.labels.map((label: string, index: number) => ( + {label} + ))} +
+ )} + {item.description && ( +

{item.description}

+ )} +

+ 拉取次数: {item.pull_count} +

+

+ 最新推送: {item.last_pusher?.nickname || '-'} +

+

+ 推送时间: {item.last_pusher?.push_at ? dayjs(item.last_pusher.push_at).format('YYYY-MM-DD HH:mm:ss') : '-'} +

+
+
+ ); + })} +
+ )} +
+
+ ); +}; + +export default App; diff --git a/src/pages/cnb-packages/page.tsx b/src/pages/cnb-packages/page.tsx index e518323..adb77f0 100644 --- a/src/pages/cnb-packages/page.tsx +++ b/src/pages/cnb-packages/page.tsx @@ -13,10 +13,13 @@ import { CardTitle, } from "@/components/ui/card"; import { CreateDialog, EditDialog } from "./components"; -import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react"; +import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon, EyeIcon } from "lucide-react"; +import { useNavigate } from "@tanstack/react-router"; import { SidebarLayout } from "@/pages/sidebar/components"; +import { toast } from "sonner"; export const App = () => { + const navigate = useNavigate(); const packageStore = usePackageStore(useShallow((state: PackageState) => { return { list: state.list, @@ -31,6 +34,7 @@ export const App = () => { setShowEditDialog: state.setShowEditDialog, editingItem: state.editingItem, setEditingItem: state.setEditingItem, + getItem: state.getItem, } })); const [search, setSearch] = useState(''); @@ -49,8 +53,13 @@ export const App = () => { packageStore.getList({ search }); }; - const handleEdit = (item: any) => { - packageStore.setEditingItem(item); + const handleEdit = async (item: any) => { + const data = await packageStore.getItem(item.id); + if (!data) { + toast.error('未找到制品数据'); + return; + } + packageStore.setEditingItem(data); packageStore.setShowEditDialog(true); }; @@ -107,9 +116,6 @@ export const App = () => { {item.summary && (

{item.summary}

)} - {item.description && ( -

{item.description}

- )}

创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}

@@ -117,6 +123,9 @@ export const App = () => { 更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}

+ diff --git a/src/pages/cnb-packages/store/index.ts b/src/pages/cnb-packages/store/index.ts index 4ef8522..5c30379 100644 --- a/src/pages/cnb-packages/store/index.ts +++ b/src/pages/cnb-packages/store/index.ts @@ -1,37 +1,43 @@ import { create } from 'zustand'; -import { queryApi } from '@/modules/mark-api'; +import { queryApi as markApi } from '@/modules/mark-api'; +import { queryApi as cnbApi } from '@/modules/package-api'; import { toast } from 'sonner'; +import { PackageItem } from './package-type'; -type PackageItem = { +export type MarkItem = { id: string; title: string; - tags?: string[]; - link?: string; - summary?: string; + tags: string[]; + link: string; + summary: string; description?: string; + data?: any; createdAt: string; updatedAt: string; } - type PackageState = { edit: boolean; setEdit: (edit: boolean) => void; - list: PackageItem[]; + list: MarkItem[]; loading: boolean; setLoading: (loading: boolean) => void; + // CNB packages list + packagesList: PackageItem[]; + packagesListLoading: boolean; + getPackagesList: (params: { slug: string, type?: string, ordering?: string, name?: string, page?: number, pageSize?: number }) => Promise; // Dialog states showCreateDialog: boolean; setShowCreateDialog: (show: boolean) => void; showEditDialog: boolean; setShowEditDialog: (show: boolean) => void; - editingItem: PackageItem | null; - setEditingItem: (item: PackageItem | null) => void; - // Data operations + editingItem: MarkItem | null; + setEditingItem: (item: MarkItem | null) => void; + // Data operations MarkItem getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise; createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise; updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise; deleteItem: (id: string) => Promise; - getItem: (id: string) => Promise; + getItem: (id: string) => Promise; } export type { PackageState, PackageItem }; @@ -42,6 +48,35 @@ export const usePackageStore = create((set, get) => ({ list: [], loading: false, setLoading: (loading) => set({ loading }), + packagesList: [], + packagesListLoading: false, + getPackagesList: async (params: { slug: string, type?: string, ordering?: string, name?: string, page?: number, pageSize?: number }) => { + const { slug, type = 'all', ordering, name, page = 1, pageSize = 20 } = params; + set({ packagesListLoading: true }); + try { + const res = await cnbApi.cnb['list-packages']({ + slug, + type, + ordering, + name, + page, + page_size: pageSize, + }); + if (res.code === 200) { + set({ packagesList: res.data?.list || [] }); + return res.data?.list || []; + } else { + toast.error(res.message || '获取制品列表失败'); + return []; + } + } catch (e) { + console.error('获取制品列表失败', e); + toast.error('获取制品列表失败'); + return []; + } finally { + set({ packagesListLoading: false }); + } + }, showCreateDialog: false, setShowCreateDialog: (show) => set({ showCreateDialog: show }), showEditDialog: false, @@ -53,7 +88,7 @@ export const usePackageStore = create((set, get) => ({ const { page = 1, pageSize = 20, search } = params; set({ loading: true }); try { - const res = await queryApi.mark.list({ + const res = await markApi.mark.list({ markType: 'cnb-packages', page, pageSize, @@ -75,7 +110,7 @@ export const usePackageStore = create((set, get) => ({ createItem: async (data) => { try { - const res = await queryApi.mark.create({ + const res = await markApi.mark.create({ title: data.title, markType: 'cnb-packages', tags: data.tags || [], @@ -98,7 +133,7 @@ export const usePackageStore = create((set, get) => ({ updateItem: async (id, data) => { try { - const res = await queryApi.mark.update({ + const res = await markApi.mark.update({ data: { // @ts-ignore id, @@ -124,7 +159,7 @@ export const usePackageStore = create((set, get) => ({ deleteItem: async (id) => { try { - const res = await queryApi.mark.delete({ id }); + const res = await markApi.mark.delete({ id }); if (res.code === 200) { toast.success('删除成功'); get().getList(); @@ -139,7 +174,7 @@ export const usePackageStore = create((set, get) => ({ getItem: async (id) => { try { - const res = await queryApi.mark.get({ id }); + const res = await markApi.mark.get({ id }); if (res.code === 200) { return res.data; } else { diff --git a/src/pages/cnb-packages/store/package-type.ts b/src/pages/cnb-packages/store/package-type.ts new file mode 100644 index 0000000..4cc83fd --- /dev/null +++ b/src/pages/cnb-packages/store/package-type.ts @@ -0,0 +1,35 @@ +type PackageType = 'docker' | 'npm'; +export type DockerPackage = { + package: string; + pull_count: number; + description: string; + labels: string[]; + package_type: PackageType; + count: number; + last_pusher: { + name: string; + nickname: string; + push_at: string; + }; + recent_pull_count: number; + last_artifact_name: string; +} + +export type NpmRegistry = { + name: string; + package: string; + pull_count: number; + description: string; + labels: string[] | null; + package_type: PackageType; + count: number; + last_pusher: { + name: string; + nickname: string; + push_at: string; + }; + recent_pull_count: number; + last_artifact_name: string; +} + +export type PackageItem = DockerPackage | NpmRegistry; \ No newline at end of file diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index ebc85be..8ca541f 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as ConfigIndexRouteImport } from './routes/config/index' import { Route as CnbPackagesIndexRouteImport } from './routes/cnb-packages/index' import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index' import { Route as ConfigGiteaRouteImport } from './routes/config/gitea' +import { Route as CnbPackagesDetailIndexRouteImport } from './routes/cnb-packages/detail/index' const LoginRoute = LoginRouteImport.update({ id: '/login', @@ -64,6 +65,11 @@ const ConfigGiteaRoute = ConfigGiteaRouteImport.update({ path: '/config/gitea', getParentRoute: () => rootRouteImport, } as any) +const CnbPackagesDetailIndexRoute = CnbPackagesDetailIndexRouteImport.update({ + id: '/cnb-packages/detail/', + path: '/cnb-packages/detail/', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -75,6 +81,7 @@ export interface FileRoutesByFullPath { '/config/': typeof ConfigIndexRoute '/repo/': typeof RepoIndexRoute '/workspaces/': typeof WorkspacesIndexRoute + '/cnb-packages/detail/': typeof CnbPackagesDetailIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -86,6 +93,7 @@ export interface FileRoutesByTo { '/config': typeof ConfigIndexRoute '/repo': typeof RepoIndexRoute '/workspaces': typeof WorkspacesIndexRoute + '/cnb-packages/detail': typeof CnbPackagesDetailIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -98,6 +106,7 @@ export interface FileRoutesById { '/config/': typeof ConfigIndexRoute '/repo/': typeof RepoIndexRoute '/workspaces/': typeof WorkspacesIndexRoute + '/cnb-packages/detail/': typeof CnbPackagesDetailIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -111,6 +120,7 @@ export interface FileRouteTypes { | '/config/' | '/repo/' | '/workspaces/' + | '/cnb-packages/detail/' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -122,6 +132,7 @@ export interface FileRouteTypes { | '/config' | '/repo' | '/workspaces' + | '/cnb-packages/detail' id: | '__root__' | '/' @@ -133,6 +144,7 @@ export interface FileRouteTypes { | '/config/' | '/repo/' | '/workspaces/' + | '/cnb-packages/detail/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -145,6 +157,7 @@ export interface RootRouteChildren { ConfigIndexRoute: typeof ConfigIndexRoute RepoIndexRoute: typeof RepoIndexRoute WorkspacesIndexRoute: typeof WorkspacesIndexRoute + CnbPackagesDetailIndexRoute: typeof CnbPackagesDetailIndexRoute } declare module '@tanstack/react-router' { @@ -212,6 +225,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ConfigGiteaRouteImport parentRoute: typeof rootRouteImport } + '/cnb-packages/detail/': { + id: '/cnb-packages/detail/' + path: '/cnb-packages/detail' + fullPath: '/cnb-packages/detail/' + preLoaderRoute: typeof CnbPackagesDetailIndexRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -225,6 +245,7 @@ const rootRouteChildren: RootRouteChildren = { ConfigIndexRoute: ConfigIndexRoute, RepoIndexRoute: RepoIndexRoute, WorkspacesIndexRoute: WorkspacesIndexRoute, + CnbPackagesDetailIndexRoute: CnbPackagesDetailIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/cnb-packages/detail/index.tsx b/src/routes/cnb-packages/detail/index.tsx new file mode 100644 index 0000000..5dff065 --- /dev/null +++ b/src/routes/cnb-packages/detail/index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import App from '@/pages/cnb-packages/detail/page' + +export const Route = createFileRoute('/cnb-packages/detail/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return +}