Headlamp 插件开发教程:用 registerRoute 与 registerSidebarEntry 构建自定义页面与侧边栏导航
【免费下载链接】headlampA Kubernetes web UI that is fully-featured, user-friendly and extensible项目地址: https://gitcode.com/GitHub_Trending/he/headlamp
导读
本篇是 Headlamp 插件开发入门系列(Getting Started)的第三篇教程,聚焦于一个核心问题:如何在 Headlamp 中创建独立的自定义页面,并通过侧边栏导航让用户发现这些页面。你将掌握registerRoute()注册路由、registerSidebarEntry()添加侧边栏条目、父子级子条目组织、图标配置,以及 Home View 与 Cluster View 两种视图下导航的差异。学完本篇,你就能为自己的插件搭建出结构清晰、可导航、可扩展的多页面 UI。
前置准备:承接 Tutorial 2 的插件工程
本教程假设你已经完成了 Tutorial 2:创建你的第一个插件,手头有一个可运行的hello-headlamp插件,并且 Headlamp 正在本地运行。如果你还没有运行环境的搭建经验,可以参考 从源码运行。
在 Tutorial 2 中,我们通过registerAppBarAction()在 Headlamp 顶部的 App Bar 中放置了一个 "Say Hello" 按钮,点击后弹出 alert。本教程将在这个基础上:
- 用
registerRoute()创建一个真正的自定义页面; - 让 App Bar 按钮从"弹提示"进化为"跳转页面";
- 用
registerSidebarEntry()把页面挂进左侧导航菜单。
最终目标效果如下:
Sidebar (Home View): ├── [Clusters list] ├── 🆕 My Plugin → expandable parent │ ├── Overview → /my-plugin │ └── Settings → /my-plugin/settings └── 📚 Plugin Docs → /plugin-docs预计耗时:约 20 分钟。
核心概念:路由(Route)与侧边栏条目(SidebarEntry)
在动手写代码之前,先理解 Headlamp 插件扩展导航的两大 API。二者都从@kinvolk/headlamp-plugin/lib导出(见 插件 SDK 入口):
| API | 职责 |
|---|---|
registerRoute(routeSpec) | 把某个 React 组件注册到指定 URL 路径上,形成可访问的页面 |
registerSidebarEntry(entryProps) | 在左侧导航菜单中注册一个条目,点击后跳转到指定 URL |
从源码层面看,这两个函数最终都是向 Headlamp 的 Redux store 派发 action(见 frontend/src/plugin/registry.tsx 与 frontend/src/plugin/registry.tsx):
registerSidebarEntry({...})→store.dispatch(setSidebarItem({ name, label, url, parent, useClusterURL, icon, sidebar, entryType, sx }))registerRoute(routeSpec)→store.dispatch(setRoute(routeSpec))
也就是说,路由决定"哪个 URL 显示什么组件",侧边栏条目决定"导航菜单里有什么、点了去哪",二者通过sidebar字段(路由侧)与name字段(条目侧)建立关联。
Route接口的完整字段定义可以查看 frontend/src/lib/router/Route.tsx:包含path、exact、name、useClusterURL(noCluster已废弃)、noAuthRequired、sidebar、component、hideAppBar、disabled、isFullWidth等;SidebarEntry的完整字段定义见 frontend/src/components/Sidebar/sidebarSlice.ts,包括name、label、parent、url、useClusterURL、icon、sidebar、entryType、sx等。后面的章节会逐一解释这些字段的用法。
创建自定义页面:第一步registerRoute
在 Headlamp 中,一个"页面"本质上就是一个被渲染到特定 URL 上的 React 组件。
Step 1:编写页面组件
打开hello-headlamp插件的src/index.tsx,用下面的内容替换:
import { registerRoute } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Typography } from '@mui/material'; function WelcomePage() { return ( <SectionBox title="Welcome to My Plugin"> <Typography variant="body1"> This is your first custom page! 🎉 </Typography> </SectionBox> ); } registerRoute({ path: '/my-plugin', sidebar: null, component: WelcomePage, useClusterURL: false, noAuthRequired: true, });这段代码在做什么?
| 代码 | 作用 |
|---|---|
registerRoute() | 告诉 Headlamp 在特定 URL 显示某个组件 |
path: '/my-plugin' | 页面可访问的 URL 路径 |
sidebar: null | 暂时不关联任何侧边栏条目(后面会补上) |
component: WelcomePage | 要渲染的 React 组件 |
useClusterURL: false | 页面在/my-plugin访问,而非集群专属 URL |
noAuthRequired: true | 页面无需认证即可访问 |
SectionBox来自@kinvolk/headlamp-plugin/lib/CommonComponents。这个模块提供与 Headlamp 风格一致、开箱即用的 UI 组件,后续教程还会用到NameValueTable、ResourceListView等。它的实现位于仓库的 frontend/src/components/common 目录下。
Step 2:手动访问页面
保存文件后,在浏览器中打开http://localhost:3000/my-plugin,你应该能看到 Welcome 页面:
手动导航的问题
页面能用了,但存在一个明显的体验缺陷:用户必须知道确切的 URL 才能访问。接下来我们逐步改进。
连接 App Bar 按钮:从 alert 到页面跳转
还记得 Tutorial 2 中的 "Say Hello" 按钮吗?现在让它直接导航到新页面,而不是弹出 alert。
更新src/index.tsx:
import { registerAppBarAction, registerRoute } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Button, Typography } from '@mui/material'; function WelcomePage() { return ( <SectionBox title="Welcome to My Plugin"> <Typography variant="body1"> This is your first custom page! 🎉 </Typography> </SectionBox> ); } registerRoute({ path: '/my-plugin', sidebar: null, component: WelcomePage, useClusterURL: false, noAuthRequired: true, }); function HelloButton() { return ( <Button variant="outlined" size="small" href="/my-plugin" sx={{ color: 'inherit', borderColor: 'inherit', mx: 1 }} > My Plugin </Button> ); } registerAppBarAction(<HelloButton />);与 Tutorial 2 的差异
| 之前(Tutorial 2) | 之后(Tutorial 3) |
|---|---|
onClick={() => alert(...)} | href="/my-plugin" |
| 点击弹出 alert 提示 | 点击跳转到自定义页面 |
保存后把鼠标悬停在 App Bar 的"My Plugin"按钮上,浏览器左下角会显示目标链接/my-plugin;点击即可进入 Welcome 页面。
不过你可能会注意到一个细节:这个页面没有侧边栏。原因是我们使用了useClusterURL: false,创建了一个脱离集群上下文的独立页面。下面进入本教程的重点——侧边栏导航。
为什么侧边栏导航如此重要
App Bar 按钮能用,但存在硬伤:App Bar 空间有限。如果插件有多个页面,不可能为每个页面都加一个按钮。
侧边栏——Headlamp 左侧的导航菜单——是标准解决方案,它具备:
- 容纳多个条目的空间;
- 父子层级组织(parent/child)能力;
- 当前页面高亮(highlight)反馈,用户始终知道自己在哪;
- 作为 Headlamp 标准导航模式的一致性体验。
添加侧边栏条目:registerSidebarEntry
把页面挂进侧边栏,需要两步:注册条目,并把路由的sidebar字段指向该条目。
Step 1:注册侧边栏条目并关联路由
更新src/index.tsx:
import { registerAppBarAction, registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Button, Typography } from '@mui/material'; function WelcomePage() { return ( <SectionBox title="Welcome to My Plugin"> <Typography variant="body1"> This is your first custom page! 🎉 </Typography> <Typography variant="body2" sx={{ mt: 2, color: 'text.secondary' }}> Now accessible from the sidebar! </Typography> </SectionBox> ); } // 注册页面——注意 sidebar 现在指向我们的条目 registerRoute({ path: '/my-plugin', sidebar: 'my-plugin', component: WelcomePage, useClusterURL: false, noAuthRequired: true, }); // 注册侧边栏条目 registerSidebarEntry({ name: 'my-plugin', label: 'My Plugin', url: '/my-plugin', useClusterURL: false, }); // 保留 App Bar 按钮(可选) function HelloButton() { return ( <Button variant="outlined" size="small" href="/my-plugin" sx={{ color: 'inherit', borderColor: 'inherit', mx: 1 }} > My Plugin </Button> ); } registerAppBarAction(<HelloButton />);新增内容详解
registerSidebarEntry的选项:
| 属性 | 作用 |
|---|---|
name | 条目的唯一标识(必须与路由中的sidebar匹配) |
label | 侧边栏中显示的文本 |
url | 点击后跳转的 URL |
useClusterURL | 为false时 URL 保持/my-plugin;为true(默认)时 URL 变为/c/:cluster/my-plugin |
registerRoute的变化:
| 属性 | 之前 | 之后 |
|---|---|---|
sidebar | null | 'my-plugin'——把路由与侧边栏条目关联起来 |
注意:
registerRoute还接受可选的name属性(如name: 'My Plugin'),提供人类可读的名称,可用于浏览器标签页标题。这也与仓库中的约定一致——在 frontend/src/lib/router/Route.tsx 中name被注释为"Human readable name. Capitalized and short."(人类可读、首字母大写且简短)。
关键约定:registerRoute中的sidebar值必须与registerSidebarEntry中的name完全一致。这一关联带来三个效果:
- 当你停留在该页面时,侧边栏条目被高亮;
- Headlamp 据此决定显示哪个侧边栏;
- 无论你是点击侧边栏条目、点击 App Bar 按钮,还是手动输入 URL,侧边栏条目都会被正确选中。
Step 2:查看效果
- 保存文件;
- 进入 Headlamp 首页(
http://localhost:3000/); - 左侧侧边栏会看到新的"My Plugin"条目;
- 点击它,进入 Welcome 页面。
连接是双向的:点击侧边栏条目 → 跳转到页面;停留在页面 → 侧边栏条目高亮。
添加图标:Iconify 图标字符串
没有图标的侧边栏条目看起来有些单调。Headlamp 使用Iconify作为图标体系,通过字符串标识符即可使用数千个图标(MDI 系列,Material Design Icons)。
给registerSidebarEntry加上icon字段:
registerSidebarEntry({ name: 'my-plugin', label: 'My Plugin', url: '/my-plugin', icon: 'mdi:new-box', useClusterURL: false, });保存后,侧边栏条目旁边会出现一个 "new"(新盒子)图标。图标字符串的格式为mdi:图标名,例如mdi:book-open-variant(书本)、mdi:comment-quote(评论)。在仓库的 Sidebar 示例插件 中可以看到icon: 'mdi:comment-quote'、icon: 'mdi:hexagon'等更多用法。
从源码角度看,icon字段的类型是 Iconify 的IconProps['icon'](见 frontend/src/components/Sidebar/sidebarSlice.ts),支持字符串标识或图标对象两种形式。
创建子条目:用parent组织层级
随着插件功能增长,你会希望把相关页面归组到父级条目之下。下面把插件扩展为"父级 + 两个子条目"的结构。
Step 1:更新插件代码
用下面的完整版本替换src/index.tsx:
import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Typography } from '@mui/material'; // 页面组件 function OverviewPage() { return ( <SectionBox title="Overview"> <Typography>Welcome to the plugin overview!</Typography> </SectionBox> ); } function SettingsPage() { return ( <SectionBox title="Plugin Settings"> <Typography>Configure your plugin settings here.</Typography> </SectionBox> ); } // 注册路由 registerRoute({ path: '/my-plugin', exact: true, name: 'Plugin Overview', sidebar: 'my-plugin-overview', component: OverviewPage, }); registerRoute({ path: '/my-plugin/settings', name: 'Plugin Settings', exact: true, sidebar: 'my-plugin-settings', component: SettingsPage, }); // 注册父级侧边栏条目 registerSidebarEntry({ name: 'my-plugin', label: 'My Plugin', icon: 'mdi:new-box', url: '/my-plugin', }); // 注册子级侧边栏条目 registerSidebarEntry({ parent: 'my-plugin', name: 'my-plugin-overview', label: 'Overview', url: '/my-plugin', }); registerSidebarEntry({ parent: 'my-plugin', name: 'my-plugin-settings', label: 'Settings', url: '/my-plugin/settings', });新增内容详解
| 代码 | 作用 |
|---|---|
exact: true | 路由只做精确匹配,而不是"以该路径开头"就匹配 |
parent: 'my-plugin' | 让 Overview 和 Settings 成为my-plugin的子条目 |
父条目带url | 父条目本身可点击,跳转到 Overview 页面 |
| 子条目 | Overview 与 Settings 作为子菜单显示在 My Plugin 之下 |
parent字段在SidebarEntry接口中的类型为parent?: string | null(见 frontend/src/components/Sidebar/sidebarSlice.ts)。当不指定parent(或为null)时,条目出现在顶层。
Step 2:查看层级效果
保存后侧边栏会呈现:
My Plugin (🆕) → 可展开的父级 ├── Overview → 点击跳转 /my-plugin └── Settings → 点击跳转 /my-plugin/settings点击 "My Plugin" 或 "Overview" 进入 Overview 页面;点击 "Settings" 进入 Settings 页面。当停留在任一子页面时,父条目会自动展开并高亮当前子条目:
Home View 与 Cluster View:两种导航上下文
Headlamp 存在两个主要上下文:
- Home View(首页视图)——未选择集群时显示(例如集群选择界面);
- Cluster View(集群视图)——正在使用某个具体集群时显示。
默认情况下,侧边栏条目只会出现在 Cluster View 中。如果你希望某些导航在未连接集群时也可见,需要显式指定sidebar: 'HOME'。
从源码看,Headlamp 用DefaultSidebars枚举定义这两个内建侧边栏(见 frontend/src/components/Sidebar/sidebarSlice.ts):
export enum DefaultSidebars { HOME = 'HOME', IN_CLUSTER = 'IN-CLUSTER', }sidebar字段的类型是DefaultSidebars | string,这意味着你既可以指向内建的HOME/集群侧边栏,也可以创建一个全新的命名侧边栏(仓库的 Sidebar 示例插件 就演示了通过sidebar: 'myplugin'创建全新侧边栏并往其中添加条目的玩法)。
添加 Home View 条目
下面注册一个无需集群即可访问的文档页面:
import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Typography, Link } from '@mui/material'; // 文档页面(无需集群即可访问) function DocsPage() { return ( <SectionBox title="Plugin Documentation"> <Typography paragraph> Welcome to the plugin documentation! </Typography> <Typography paragraph> This page is accessible even when no cluster is selected. </Typography> <Link href="/">← Back to Clusters</Link> </SectionBox> ); } // 注册路由(注意 useClusterURL: false) registerRoute({ path: '/plugin-docs', name: 'Plugin Docs', sidebar: { item: 'plugin-docs', sidebar: 'HOME', }, component: DocsPage, useClusterURL: false, noAuthRequired: true, }); // 在 HOME 侧边栏中注册条目 registerSidebarEntry({ name: 'plugin-docs', label: 'Plugin Docs', url: '/plugin-docs', icon: 'mdi:book-open-variant', sidebar: 'HOME', useClusterURL: false, });关键差异
| 属性 | 作用 |
|---|---|
sidebar: { item, sidebar } | 在registerRoute中,指定要高亮的侧边栏条目以及它属于哪个侧边栏 |
sidebar: 'HOME' | 在registerSidebarEntry中,把条目放入首页侧边栏 |
useClusterURL: false | URL 不含/c/:cluster/前缀 |
noAuthRequired: true | 页面无需认证即可访问 |
Route.sidebar字段支持三种形态:string | null | { item, sidebar }(见 frontend/src/lib/router/Route.tsx),其中对象形态允许你同时指定要激活的条目名与所属侧边栏。
验证
- 保存文件;
- 回到首页(点击 Headlamp Logo 或访问
http://localhost:3000/); - 在侧边栏中找到 "Plugin Docs"(带书本图标);
- 点击进入文档页面:
完整示例:Cluster View + Home View 组合
下面是一份完整的src/index.tsx,同时演示集群视图与首页视图导航:
import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Typography, Link } from '@mui/material'; // ========== Cluster View Pages ========== function OverviewPage() { return ( <SectionBox title="Plugin Overview"> <Typography>Welcome to My Plugin! This page is cluster-specific.</Typography> </SectionBox> ); } function SettingsPage() { return ( <SectionBox title="Plugin Settings"> <Typography>Configure your plugin settings here.</Typography> </SectionBox> ); } // ========== Home View Pages ========== function DocsPage() { return ( <SectionBox title="Plugin Documentation"> <Typography paragraph> This page is accessible without selecting a cluster. </Typography> <Link href="/">← Back to Clusters</Link> </SectionBox> ); } // ========== Cluster View Routes & Sidebar ========== registerRoute({ path: '/my-plugin', sidebar: 'my-plugin-overview', component: OverviewPage, exact: true, }); registerRoute({ path: '/my-plugin/settings', sidebar: 'my-plugin-settings', component: SettingsPage, exact: true, }); registerSidebarEntry({ name: 'my-plugin', label: 'My Plugin', icon: 'mdi:new-box', url: '/my-plugin', }); registerSidebarEntry({ parent: 'my-plugin', name: 'my-plugin-overview', label: 'Overview', url: '/my-plugin', }); registerSidebarEntry({ parent: 'my-plugin', name: 'my-plugin-settings', label: 'Settings', url: '/my-plugin/settings', }); // ========== Home View Routes & Sidebar ========== registerRoute({ path: '/plugin-docs', component: DocsPage, useClusterURL: false, noAuthRequired: true, sidebar: { item: 'plugin-docs', sidebar: 'HOME', }, }); registerSidebarEntry({ name: 'plugin-docs', label: 'Plugin Docs', url: '/plugin-docs', icon: 'mdi:book-open-variant', sidebar: 'HOME', useClusterURL: false, });更进阶的能力:从示例插件中挖掘
仓库中的 Sidebar 示例插件 是官方提供的完整参考实现,它展示了本教程之外的多种进阶玩法,值得通读:
entryType: 'subheader':注册不可点击的分组标题条目(配合sx自定义样式),用于在侧边栏中给条目分组(见 示例插件第 84-93 行);registerSidebarEntryFilter/registerRouteFilter:动态移除或修改侧边栏条目与路由,例如在进入某个页面时用useEffect隐藏特定条目(见 示例插件第 216-228 行);registerHomeSidebarEntryFilter:过滤 HOME 侧边栏条目(见 示例插件第 295 行);useClusterURL: false+hideAppBar: true:创建完全脱离集群前缀、甚至隐藏顶部 App Bar 的独立页面(见 示例插件第 257-282 行)。
这些能力对应的底层实现同样位于 frontend/src/plugin/registry.tsx:registerSidebarEntryFilter派发setSidebarItemFilter,registerRouteFilter派发setRouteFilter,返回null即删除条目/路由,返回(可修改的)条目/路由则保留。
Troubleshooting:常见问题排查
侧边栏条目不出现
检查集群上下文:
- 未指定
sidebar: 'HOME'的条目只会在选中集群后出现; - 确保已选择集群,才能看到集群视图条目。
检查拼写:
registerSidebarEntry中的name必须与registerRoute中的sidebar完全一致。
确认插件已加载:
- 进入 Settings → Plugins;
- 确认你的插件已列出且已启用。
页面 404 或空白
检查 URL 模式:
- 集群视图 URL 格式:
/c/:cluster/你的路径; - 首页视图 URL 格式:
/你的路径。
检查useClusterURL:
- 路由中为
useClusterURL: false时,不带集群前缀访问; - 为
useClusterURL: true(默认)时,URL 需要包含集群前缀。
侧边栏条目不高亮
确保sidebar与name匹配:
// 这两处必须一致! registerRoute({ path: '/my-plugin', sidebar: 'my-plugin', // ← 这个... component: MyPage, }); registerSidebarEntry({ name: 'my-plugin', // ← ...必须与这个一致 label: 'My Plugin', url: '/my-plugin', });子条目不显示
检查parent引用:
registerSidebarEntry({ name: 'my-plugin', // ← 父条目 name label: 'My Plugin', }); registerSidebarEntry({ parent: 'my-plugin', // ← 必须与父条目 name 一致 name: 'my-plugin-child', label: 'Child Entry', url: '/my-plugin/child', });parent的值必须与父条目的name精确一致,否则层级关系无法建立。
下一步
通过本教程,你已经掌握了 Headlamp 插件导航的完整基础:
- ✅ 用
registerRoute()创建自定义页面 - ✅ 用
registerSidebarEntry()添加侧边栏条目 - ✅ 用 Iconify 图标提升视觉效果
- ✅ 用
parent组织父子层级 - ✅ 区分 Home View 与 Cluster View 两种导航上下文
目前页面还是静态的。接下来的教程将让页面"活"起来:
- Tutorial 4:使用 Kubernetes 数据——用内置资源类和 ApiProxy 获取集群信息、命名空间等数据(见 working-with-kubernetes-data);
- Tutorial 5:进阶 Kubernetes 交互——创建自定义资源类、通过 API 修改资源(见 working-with-kubernetes-data-advanced)。
Quick Reference:快速参考
registerRoute 选项
registerRoute({ path: '/my-path', // URL 路径(必填) sidebar: 'sidebar-name', // 要高亮的侧边栏条目(必填) component: MyComponent, // React 组件(必填) useClusterURL: true, // 是否包含 /c/:cluster/ 前缀(默认: true) noAuthRequired: false, // 是否允许未认证访问(默认: false) exact: true, // 精确路径匹配(默认: true) name: 'route-name', // 可选的路由标识 });registerSidebarEntry 选项
registerSidebarEntry({ name: 'unique-name', // 唯一标识(必填) label: 'Display Label', // 侧边栏显示的文本(必填) url: '/my-path', // 点击跳转的 URL icon: 'mdi:icon-name', // Iconify 图标字符串 parent: 'parent-name', // 父条目 name(用于子条目) sidebar: 'HOME', // 'HOME' 表示首页视图,省略则为集群视图 useClusterURL: true, // 是否包含 /c/:cluster/ 前缀(默认: true) });URL 模式速查
| 上下文 | 模式 | 示例 |
|---|---|---|
| 集群视图 | /c/:cluster/你的路径 | /c/minikube/my-plugin |
| 首页视图 | /你的路径 | /plugin-docs |
【免费下载链接】headlampA Kubernetes web UI that is fully-featured, user-friendly and extensible项目地址: https://gitcode.com/GitHub_Trending/he/headlamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考