PageSourceSearch

https://headlamp.dev/assets/js/c2e9e662.58b41676.js

js headlamp.dev collected 2026-10-02 06:05:20 UTC 50,287 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkheadlamp_website=self.webpackChunkheadlamp_website||[]).push([[21271],{5232:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/pod-detail-section-aba667fb6582a325de115c50ab05bd62.png"},7267:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/pods-list-column-4eb762aa899a04a3fdd83eb391bc85c8.png"},19089:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/pods-list-column-tooltip-30e748c59498d72c83d07de7689998df.png"},25362:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/pod-detail-action-button-2e3672464ff08de4063a20fd6933de6f.png"},28453:(e,n,i)=>{i.d(n,{R:()=>r,x:()=>l});var t=i(96540);const s={},o=t.createContext(s);function r(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),t.createElement(o.Provider,{value:n},e.children)}},63373:(e,n,i)=>{i.d(n,{A:()=>t});const t=i.p+"assets/images/pod-detail-image-dialog-a25a889cd92932de6db06dd1323594ac.png"},99611:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>l,toc:()=>d});var t=i(74848),s=i(28453);const o={title:"Tutorial 7: Extending Existing Resource Views",sidebar_label:"7. Extending Existing Resource Views",sidebar_position:8},r="Extending Existing Resource Views",l={id:"latest/tutorials/plugin-development/getting-started/extending-existing-resource-views/index",title:"Tutorial 7: Extending Existing Resource Views",description:"In Tutorial 6 we covered how to build entirely new list and detail pages for custom resources. In this tutorial we will cover how to extend and customise existing pages and views that are already built into Headlamp, without replacing them. This is one of the most powerful patterns in Headlamp plugin development: you can enrich Headlamp's built-in Pod, Deployment, Node, and other resource views with your own columns, actions, and sections by registering processors that Headlamp calls when it renders those views.",source:"@site/docs/latest/tutorials/plugin-development/getting-started/extending-existing-resource-views/index.md",sourceDirName:"latest/tutorials/plugin-development/getting-started/extending-existing-resource-views",slug:"/latest/tutorials/plugin-development/getting-started/extending-existing-resource-views/",permalink:"/docs/latest/tutorials/plugin-development/getting-started/extending-existing-resource-views/",draft:!1,unlisted:!1,editUrl:"https://github.com/kubernetes-sigs/headlamp/edit/main/docs/tutorials/plugin-development/getting-started/extending-existing-resource-views/index.md",tags:[],version:"current",sidebarPosition:8,frontMatter:{title:"Tutorial 7: Extending Existing Resource Views",sidebar_label:"7. Extending Existing Resource Views",sidebar_position:8},sidebar:"mainSidebar",previous:{title:"6. Building List & Detail Pages",permalink:"/docs/latest/tutorials/plugin-development/getting-started/building-list-and-detail-pages/"},next:{title:"8. Adding Plugin Settings",permalink:"/docs/latest/tutorials/plugin-development/getting-started/adding-plugin-settings/"}},a={},d=[{value:"Table of Contents",id:"table-of-contents",level:2},{value:"Introduction",id:"introduction",level:2},{value:"What You'll Build",id:"what-youll-build",level:3},{value:"Prerequisites",id:"prerequisites",level:3},{value:"Adding a Column to the Pods List",id:"adding-a-column-to-the-pods-list",level:2},{value:"Step 1: Import the API and Pod Type",id:"step-1-import-the-api-and-pod-type",level:3},{value:"Step 2: Register the Column Processor",id:"step-2-register-the-column-processor",level:3},{value:"Step 3: Test It",id:"step-3-test-it",level:3},{value:"Understanding registerResourceTableColumnsProcessor",id:"understanding-registerresourcetablecolumnsprocessor",level:2},{value:"Finding table IDs",id:"finding-table-ids",level:3},{value:"Column definition",id:"column-definition",level:3},{value:"Adding a render function for richer cells",id:"adding-a-render-function-for-richer-cells",level:3},{value:"Adding a Custom Action to the Pod Detail View",id:"adding-a-custom-action-to-the-pod-detail-view",level:2},{value:"Step 1: Import the Dependencies",id:"step-1-import-the-dependencies",level:3},{value:"Step 2: Create the Action Component",id:"step-2-create-the-action-component",level:3},{value:"Step 3: Register the Action",id:"step-3-register-the-action",level:3},{value:"Step 4: Test It",id:"step-4-test-it",level:3},{value:"Understanding registerDetailsViewHeaderAction",id:"understanding-registerdetailsviewheaderaction",level:2},{value:"Key points",id:"key-points",level:3},{value:"Registering with a stable ID",id:"registering-with-a-stable-id",level:3},{value:"Adding a Custom Section to the Pod Detail View",id:"adding-a-custom-section-to-the-pod-detail-view",level:2},{value:"Step 1: Import the Section APIs",id:"step-1-import-the-section-apis",level:3},{value:"Step 2: Register the Sections Processor",id:"step-2-register-the-sections-processor",level:3},{value:"Step 3: Test It",id:"step-3-test-it-1",level:3},{value:"Understanding registerDetailsViewSectionsProcessor",id:"understanding-registerdetailsviewsectionsprocessor",level:2},{value:"Sections array structure",id:"sections-array-structure",level:3},{value:"Positioning strategies",id:"positioning-strategies",level:3},{value:"Idempotency guard",id:"idempotency-guard",level:3},{value:"Complete Example",id:"complete-example",level:2},{value:"Troubleshooting",id:"troubleshooting",level:2},{value:"Column not appearing in the Pods list",id:"column-not-appearing-in-the-pods-list",level:3},{value:"Action button not appearing on Pod detail pages",id:"action-button-not-appearing-on-
1pod-detail-pages",level:3},{value:"Section not appearing or appearing multiple times",id:"section-not-appearing-or-appearing-multiple-times",level:3},{value:"What&#39;s Next",id:"whats-next",level:2},{value:"Quick Reference",id:"quick-reference",level:2},{value:"registerResourceTableColumnsProcessor",id:"registerresourcetablecolumnsprocessor",level:3},{value:"registerDetailsViewHeaderAction",id:"registerdetailsviewheaderaction",level:3},{value:"registerDetailsViewSectionsProcessor",id:"registerdetailsviewsectionsprocessor",level:3}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",hr:"hr",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"extending-existing-resource-views",children:"Extending Existing Resource Views"}),"\n",(0,t.jsxs)(n.p,{children:["In ",(0,t.jsx)(n.a,{href:"../building-list-and-detail-pages/",children:"Tutorial 6"})," we covered how to build entirely new list and detail pages for custom resources. In this tutorial we will cover how to extend and customise ",(0,t.jsx)(n.strong,{children:"existing"})," pages and views that are already built into Headlamp, without replacing them. This is one of the most powerful patterns in Headlamp plugin development: you can enrich Headlamp's built-in Pod, Deployment, Node, and other resource views with your own columns, actions, and sections by registering processors that Headlamp calls when it renders those views."]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"table-of-contents",children:"Table of Contents"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#introduction",children:"Introduction"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#adding-a-column-to-the-pods-list",children:"Adding a Column to the Pods List"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#understanding-registerresourcetablecolumnsprocessor",children:"Understanding registerResourceTableColumnsProcessor"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#adding-a-custom-action-to-the-pod-detail-view",children:"Adding a Custom Action to the Pod Detail View"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#understanding-registerdetailsviewheaderaction",children:"Understanding registerDetailsViewHeaderAction"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#adding-a-custom-section-to-the-pod-detail-view",children:"Adding a Custom Section to the Pod Detail View"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#understanding-registerdetailsviewsectionsprocessor",children:"Understanding registerDetailsViewSectionsProcessor"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#complete-example",children:"Complete Example"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#troubleshooting",children:"Troubleshooting"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#whats-next",children:"What's Next"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"#quick-reference",children:"Quick Reference"})}),"\n"]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsxs)(n.p,{children:["In previous tutorials you always built things from scratch \u2014 new routes, new pages, new components. The three APIs covered in this tutorial work differently: instead of creating a new page, they ",(0,t.jsx)(n.strong,{children:"inject"})," your content into existing pages that Headlamp already renders."]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"API"}),(0,t.jsx)(n.th,{children:"What it does"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"registerResourceTableColumnsProcessor"})}),(0,t.jsx)(n.td,{children:"Add, remove, or reorder columns in any resource list table"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"registerDetailsViewHeaderAction"})}),(0,t.jsx)(n.td,{children:"Add action buttons to any resource detail page header"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"registerDetailsViewSectionsProcessor"})}),(0,t.jsx)(n.td,{children:"Add, remove, or reorder sections in any resource detail page"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["All three follow the same mental model: you register a ",(0,t.jsx)(n.strong,{children:"processor function"})," (or component) once at module load time, and Headlamp calls it automatically every time it renders the relevant UI. This means your additions appear in Headlamp's own built-in views, not just in pages your plugin owns."]}),"\n",(0,t.jsx)(n.h3,{id:"what-youll-build",children:"What You'll Build"}),"\n",(0,t.jsx)(n.p,{children:"By the end of this tutorial you'll have extended the built-in Pods views with:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["A ",(0,t.jsx)(n.strong,{children:"Container Images"})," column in Headlamp's Pods list page"]}),"\n",(0,t.jsxs)(n.li,{children:["An ",(0,t.jsx)(n.strong,{children:"Image Details"})," button in every Pod detail header that opens a popup showing each container's image and pull policy"]}),"\n",(0,t.jsxs)(n.li,{children:["A ",(0,t.jsx)(n.strong,{children:"Container Resources"})," section in every Pod detail page showing CPU and memory requests/limits"]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,t.jsx)(n.p,{children:"Before starting, ensure you have:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\u2705 Completed ",(0,t.jsx)(n.a,{href:"../building-list-and-detail-pages/",children:"Tutorial 6: Building List & Detail Pages"})]}),"\n",(0,t.jsxs)(n.li,{children:["\u2705 Your ",(0,t.jsx)(n.code,{children:"hello-headlamp"})," plugin open and running"]}),"\n",(0,t.jsx)(n.li,{children:"\u2705 Headlamp running with a connected cluster"}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Time to complete:"})," ~25 minutes"]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"adding-a-column-to-the-pods-list",children:"Adding a Column to the Pods List"}),"\n",(0,t.jsxs)(n.p,{children:["Headlamp's built-in Pods list already has columns like Name, Namespace, Restarts, Age etc. We'll add a ",(0,t.jsx)(n.strong,{children:"Container Images"})," column to it using ",(0,t.jsx)(n.code,{children:"registerResourceTableColumnsProcessor"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"step-1-import-the-api-and-pod-type",children:"Step 1: Import the API and Pod Type"}),"\n",(0,t.jsxs)(n.p,{children:["Open ",(0,t.jsx)(n.code,{children:"src/index.tsx"})," and add the following imports at the top of the file (alongside your existing imports):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import {\n  registerResourceTableColumnsProcessor,\n} from '@kinvolk/headlamp-plugin/lib';\nimport { ResourceTableColumn } from '@kinvolk/headlamp-plugin/lib/CommonComponents';\nimport Pod from '@kinvolk/headlamp-plugin/lib/K8s/pod';\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-2-register-the-column-processor",children:"Step 2: Register the Column Processor"}),"\n",(0,t.jsxs)(n.p,{children:["Add this code ",(0,t.jsx)(n.strong,{children:"outside"})," any React component, at the module level (for example, at the bottom of ",(0,t.jsx)(n.code,{children:"src/index.tsx"}),"):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"registerResourceTableColumnsProcessor(function addContainerImagesColumn({ id, columns }) {\n  // Only modify the built-in Pods list table.\n  // The table ID follows the convention: headlamp-<plural-resource-name>\n  if (id !== 'headlamp-pods') {\n    return columns;\n  }\n\n  const podColumns = columns as ResourceTableColumn<Pod>[];\n\n  podColumns.push({\n    id: 'hello-headlamp-container-images',\n    label: 'Container Images',\n    // getValue is used for sorting and filtering \u2014 return a plain string\n    getValue: (pod: Pod) =>\n      pod.spec.containers.map(c => c.image).join(', '),\n  });\n\n  return columns;\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-3-test-it",children:"Step 3: Test It"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Save the file"}),"\n",(0,t.jsxs)(n.li,{children:["Navigate to ",(0,t.jsx)(n.strong,{children:"Workloads"})," \u2192 ",(0,t.jsx)(n.strong,{children:"Pods"})," in the Headlamp sidebar"]}),"\n",(0,t.jsxs)(n.li,{children:["You should see a new ",(0,t.jsx)(n.strong,{children:"Container Images"})," column on the right side of the table"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Screenshot of the Pods list view with the custom Container Images column added, highlighted in a red box on the right side of the table",src:i(7267).A+"",width:"3840",height:"2160"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Important:"})," This column appears in Headlamp's ",(0,t.jsx)(n.strong,{children:"own"})," built-in Pods page \u2014 not the custom ",(0,t.jsx)(n.code,{children:"MyPodsPage"})," you built in Tutorial 6. You are injecting into Headlamp's view."]}),"\n"]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"understanding-registerresourcetablecolumnsprocessor",children:"Understanding registerResourceTableColumnsProcessor"}),"\n",(0,t.jsxs)(n.p,{children:["The processor function you pass to ",(0,t.jsx)(n.code,{children:"registerResourceTableColumnsProcessor"})," is called every time Headlamp renders any resource table. It receives an object with:"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Property"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"id"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string"})}),(0,t.jsxs)(n.td,{children:["Unique table identifier, e.g. ",(0,t.jsx)(n.code,{children:"'headlamp-pods'"})]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"columns"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ResourceTableColumn[]"})}),(0,t.jsx)(n.td,{children:"The current column array \u2014 mutate it or return a new one"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["And it must ",(0,t.jsx)(n.strong,{children:"return"})," the (modified) columns array."]}),"\n",(0,t.jsx)(n.h3,{id:"finding-table-ids",children:"Finding table IDs"}),"\n",(0,t.jsxs)(n.p,{children:["Table IDs follow the convention ",(0,t.jsx)(n.code,{children:"headlamp-<plural-resource-name>"}),":"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Resource"}),(0,t.jsx)(n.th,{children:"Table ID"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Pods"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"headlamp-pods"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Deployments"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"headlamp-deployments"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Nodes"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"headlamp-nodes"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Namespaces"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"headlamp-namespaces"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Services"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"headlamp-services"})})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["For tables that don't directly represent a resource list (like the events table on the cluster overview), the ID will reflect the section's role (e.g. ",(0,t.jsx)(n.code,{children:"headlamp-cluster.overview.events"}),"). During development, you can ",(0,t.jsx)(n.code,{children:"console.log(id)"})," inside your processor to discover the exact ID of any table."]}),"\n",(0,t.jsx)(n.h3,{id:"column-definition",children:"Column definition"}),"\n",(0,t.jsxs)(n.p,{children:["Each column follows the same structure as custom columns in ",(0,t.jsx)(n.code,{children:"ResourceListView"})," from Tutorial 6:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"{\n  id: 'my-unique-column-id',  // Prefix with your plugin name to avoid clashes\n  label: 'Column Header',\n  getValue: (item) => 'sortable plain value',   // Used for sorting and filtering\n  render: (item) => <ReactNode />,              // Optional \u2014 falls back to getValue\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-a-render-function-for-richer-cells",children:"Adding a render function for richer cells"}),"\n",(0,t.jsxs)(n.p,{children:["The column above shows images as a plain comma-separated string. You can enrich the display with a ",(0,t.jsx)(n.code,{children:"render"})," function while keeping ",(0,t.jsx)(n.code,{children:"getValue"})," for sorting:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import { Tooltip, Typography } from '@mui/material';\n\npodColumns.push({\n  id: 'hello-headlamp-container-images',\n  label: 'Container Images',\n  getValue: (pod: Pod) =>\n    pod.spec.containers.map(c => c.image).join(', '),\n  render: (pod: Pod) => {\n    const images = pod.spec.containers.map(c => c.image);\n    return (\n      <Tooltip title={images.join('\\n')} placement=\"bottom-start\">\n        <Typography noWrap style={{ maxWidth: 240, fontSize: 'inherit' }}>\n          {images[0]}{images.length > 1 ? ` (+${images.length - 1} more)` : ''}\n        </Typography>\n      </Tooltip>\n    );\n  },\n});\n"})}),"\n",(0,t.jsxs)(n.p,{children:["In the previous screenshot, the Container Images column is added at the end of the table (after Age). If you prefer to keep the Age column always last, you can use JavaScript array methods to insert your column before it. Find the index of the Age column (columns may be string ids or objects with an ",(0,t.jsx)(n.code,{children:"id"})," property) and use ",(0,t.jsx)(n.code,{children:"splice"})," to insert the new column:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import { Tooltip, Typography } from '@mui/material';\n\nconst ageIndex = columns.findIndex(\n  col => col === 'age' || (typeof col === 'object' && col !== null && 'id' in col && col.id === 'age')\n);\nconst containerImagesColumn: ResourceTableColumn<Pod> = {\n  id: 'hello-headlamp-container-images',\n  label: 'Container Images',\n  getValue: (pod: Pod) => pod.spec.containers.map(c => c.image).join(', '),\n  render: (pod: Pod) => {\n    const images = pod.spec.containers.map(c => c.image);\n    return (\n      <Tooltip title={images.join('\\n')} placement=\"bottom-start\">\n        <Typography noWrap style={{ maxWidth: 240, fontSize: 'inherit' }}>\n          {images[0]}{images.length > 1 ? ` (+${images.length - 1} more)` : ''}\n        </Typography>\n      </Tooltip>\n    );\n  },\n};\nconst podColumns = columns as ResourceTableColumn<Pod>[];\nif (ageIndex >= 0) {\n  podColumns.splice(ageIndex, 0, containerImagesColumn);\n} else {\n  podColumns.push(containerImagesColumn);\n}\nreturn columns;\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Screenshot of the Pods list view with the Container Images column showing a tooltip popup on hover, displaying the full list of container images for that pod",src:i(19089).A+"",width:"3840",height:"2160"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"adding-a-custom-action-to-the-pod-detail-view",children:"Adding a Custom Action to the Pod Detail View"}),"\n",(0,t.jsxs)(n.p,{children:["Now let's add an ",(0,t.jsx)(n.strong,{children:"Image Details"})," button to the Pod detail page header. Clicking it will open a popup (MUI Dialog) listing every container's image name and pull policy."]}),"\n",(0,t.jsx)(n.h3,{id:"step-1-import-the-dependencies",children:"Step 1: Import the Dependencies"}),"\n",(0,t.jsx)(n.p,{children:"Add these to your imports:"}),
1"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import {\n  registerResourceTableColumnsProcessor,\n  registerDetailsViewHeaderAction,\n} from '@kinvolk/headlamp-plugin/lib';\nimport { ActionButton, SimpleTable } from '@kinvolk/headlamp-plugin/lib/CommonComponents';\nimport {\n  Dialog,\n  DialogTitle,\n  DialogContent,\n  DialogActions,\n  Button,\n} from '@mui/material';\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-2-create-the-action-component",children:"Step 2: Create the Action Component"}),"\n",(0,t.jsxs)(n.p,{children:["Create the component that will be rendered as the header action button. The component receives the current resource as an ",(0,t.jsx)(n.code,{children:"item"})," prop:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"function ImageDetailsAction({ item }: { item: any }) {\n  const [open, setOpen] = useState(false);\n\n  // Only show this action on Pod detail pages\n  if (!item || item.kind !== 'Pod') {\n    return null;\n  }\n\n  const containers: Array<{\n    name: string;\n    image: string;\n    imagePullPolicy?: string;\n  }> = item.spec?.containers || [];\n\n  return (\n    <>\n      <ActionButton\n        description=\"Image Details\"\n        icon=\"mdi:image-search-outline\"\n        onClick={() => setOpen(true)}\n      />\n      <Dialog open={open} onClose={() => setOpen(false)} maxWidth=\"md\" fullWidth>\n        <DialogTitle>Container Images \u2014 {item.metadata?.name}</DialogTitle>\n        <DialogContent>\n          <SimpleTable\n            data={containers}\n            columns={[\n              { label: 'Container', getter: c => c.name },\n              { label: 'Image', getter: c => c.image },\n              {\n                label: 'Pull Policy',\n                getter: c => c.imagePullPolicy || 'IfNotPresent (default)',\n              },\n            ]}\n          />\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={() => setOpen(false)}>Close</Button>\n        </DialogActions>\n      </Dialog>\n    </>\n  );\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-3-register-the-action",children:"Step 3: Register the Action"}),"\n",(0,t.jsx)(n.p,{children:"Add this at the module level:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"registerDetailsViewHeaderAction(ImageDetailsAction);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-4-test-it",children:"Step 4: Test It"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Save the file"}),"\n",(0,t.jsxs)(n.li,{children:["Navigate to ",(0,t.jsx)(n.strong,{children:"Workloads"})," \u2192 ",(0,t.jsx)(n.strong,{children:"Pods"})," \u2192 click on any pod"]}),"\n",(0,t.jsx)(n.li,{children:"Look for the image search icon in the top-right area of the detail page header (alongside Edit and Delete)"}),"\n",(0,t.jsx)(n.li,{children:"Click it \u2014 a dialog should open showing each container's name, image, and pull policy"}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Screenshot of the Pod detail page header with the custom Image Details action button highlighted in a red box in the top-right action area alongside the existing Edit and Delete buttons",src:i(25362).A+"",width:"3840",height:"2160"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Screenshot of the Image Details dialog open on a Pod detail page, showing a table with columns for Container name, Image, and Pull Policy for each container in the pod",src:i(63373).A+"",width:"3840",height:"2160"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"understanding-registerdetailsviewheaderaction",children:"Understanding registerDetailsViewHeaderAction"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"registerDetailsViewHeaderAction"})," accepts a ",(0,t.jsx)(n.strong,{children:"React component function"}),". Headlamp calls this component for every detail page it renders, passing the current resource as the ",(0,t.jsx)(n.code,{children:"item"})," prop."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:'function MyAction({ item }: { item: any }) {\n  // item is the KubeObject for the resource being viewed\n  // Return null to show nothing for this resource type\n  if (!item || item.kind !== \'Pod\') return null;\n\n  return <ActionButton description="My Action" icon="mdi:icon-name" onClick={...} />;\n}\n\nregisterDetailsViewHeaderAction(MyAction);\n'})}),"\n",(0,t.jsx)(n.h3,{id:"key-points",children:"Key points"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["The component is rendered for ",(0,t.jsx)(n.strong,{children:"every"})," detail view across the entire app, so always check ",(0,t.jsx)(n.code,{children:"item.kind"})," (or any other condition) and return ",(0,t.jsx)(n.code,{children:"null"})," when it shouldn't appear."]}),"\n",(0,t.jsxs)(n.li,{children:["Use ",(0,t.jsx)(n.code,{children:"ActionButton"})," from ",(0,t.jsx)(n.code,{children:"@kinvolk/headlamp-plugin/lib/CommonComponents"})," for consistent styling with Headlamp's existing action buttons."]}),"\n",(0,t.jsxs)(n.li,{children:["Because the component is a regular React component, you can use ",(0,t.jsx)(n.code,{children:"useState"}),", ",(0,t.jsx)(n.code,{children:"useEffect"}),", and any other hooks inside it \u2014 which is how the popup pattern above works."]}),"\n",(0,t.jsx)(n.li,{children:"You can register multiple header actions; they all appear side by side in the header."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"registering-with-a-stable-id",children:"Registering with a stable ID"}),"\n",(0,t.jsxs)(n.p,{children:["If you want to reference or replace your action later (e.g. via ",(0,t.jsx)(n.code,{children:"registerDetailsViewHeaderActionsProcessor"}),"), pass an object with an explicit ",(0,t.jsx)(n.code,{children:"id"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"registerDetailsViewHeaderAction({\n  id: 'hello-headlamp-image-details',\n  action: ImageDetailsAction,\n});\n"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"adding-a-custom-section-to-the-pod-detail-view",children:"Adding a Custom Section to the Pod Detail View"}),"\n",(0,t.jsxs)(n.p,{children:["Now let's add a ",(0,t.jsx)(n.strong,{children:"Container Resources"})," section to the Pod detail page. This section will show each container's CPU and memory requests and limits \u2014 information that is not shown by default in Headlamp's Pod detail view."]}),"\n",(0,t.jsx)(n.h3,{id:"step-1-import-the-section-apis",children:"Step 1: Import the Section APIs"}),"\n",(0,t.jsx)(n.p,{children:"Add these to your imports:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import {\n  registerResourceTableColumnsProcessor,\n  registerDetailsViewHeaderAction,\n  registerDetailsViewSectionsProcessor,\n} from '@kinvolk/headlamp-plugin/lib';\nimport { SectionBox, SimpleTable } from '@kinvolk/headlamp-plugin/lib/CommonComponents';\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-2-register-the-sections-processor",children:"Step 2: Register the Sections Processor"}),"\n",(0,t.jsx)(n.p,{children:"Add this at the module level:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"registerDetailsViewSectionsProcessor(function addContainerResourcesSection(resource, sections) {\n  // Only add the section on Pod detail pages\n  if (!resource || resource.kind !== 'Pod') {\n    return sections;\n  }\n\n  // Guard against being called multiple times \u2014 avoid duplicate sections\n  const sectionId = 'hello-headlamp-container-resources';\n  if (sections.some(s => typeof s === 'object' && s !== null && 'id' in s && s.id === sectionId)) {\n    return sections;\n  }\n\n  // Build the row data from the pod spec\n  const containers: Array<{\n    name: string;\n    cpuRequest: string;\n    cpuLimit: string;\n    memoryRequest: string;\n    memoryLimit: string;\n  }> = (resource.jsonData?.spec?.containers ?? []).map((c: any) => ({\n    name: c.name,\n    cpuRequest: c.resources?.requests?.cpu ?? '\u2014',\n    cpuLimit: c.resources?.limits?.cpu ?? '\u2014',\n    memoryRequest: c.resources?.requests?.memory ?? '\u2014',\n    memoryLimit: c.resources?.limits?.memory ?? '\u2014',\n  }));\n\n  const newSection = {\n    id: sectionId,\n    section: (\n      <SectionBox title=\"Container Resources\">\n        <SimpleTable\n          data={containers}\n          columns={[\n            { label: 'Container', getter: c => c.name ?? '\u2014' },\n            { label: 'CPU Request', getter: c => c.cpuRequest ?? '\u2014' },\n            { label: 'CPU Limit', getter: c => c.cpuLimit ?? '\u2014' },\n            { label: 'Memory Request', getter: c => c.memoryRequest ?? '\u2014' },\n            { label: 'Memory Limit', getter: c => c.memoryLimit ?? '\u2014' },\n          ]}\n        />\n      </SectionBox>\n    ),\n  };\n\n  // Insert our section right after the headlamp.pod-containers section\n  const hasId = (s: (typeof sections)[number]): s is { id: string } =>\n    typeof s === 'object' && s !== null && 'id' in s;\n  const podContainersIdx = sections.findIndex(\n    s => hasId(s) && s.id === 'headlamp.pod-containers'\n  );\n\n  if (podContainersIdx === -1) {\n    // headlamp.pod-containers not found \u2014 append to the end as a fallback\n    return [...sections, newSection];\n  }\n\n  const updated = [...sections];\n  updated.splice(podContainersIdx + 1, 0, newSection);\n  return updated;\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"step-3-test-it-1",children:"Step 3: Test It"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Save the file"}),"\n",(0,t.jsx)(n.li,{children:"Navigate to any Pod's detail page"}),"\n",(0,t.jsxs)(n.li,{children:["Just below the standard metadata header you should see a new ",(0,t.jsx)(n.strong,{children:"Container Resources"})," section with a table showing CPU and memory requests/limits for each container"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Screenshot of a Pod detail page with the custom Container Resources section visible below the Containers section, showing a table of CPU and memory requests and limits for each container",src:i(5232).A+"",width:"3840",height:"2160"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"understanding-registerdetailsviewsectionsprocessor",children:"Understanding registerDetailsViewSectionsProcessor"}),"\n",(0,t.jsxs)(n.p,{children:["The processor function you pass to ",(0,t.jsx)(n.code,{children:"registerDetailsViewSectionsProcessor"})," is called every time Headlamp renders a detail page. It receives:"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Parameter"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"resource"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"KubeObject | null"})}),(0,t.jsx)(n.td,{children:"The resource being viewed (null while loading)"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"sections"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DetailsViewSection[]"})}),(0,t.jsx)(n.td,{children:"Current ordered array of sections"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["And it must ",(0,t.jsx)(n.strong,{children:"return"})," the (modified) sections array."]}),"\n",(0,t.jsx)(n.h3,{id:"sections-array-structure",children:"Sections array structure"}),"\n",(0,t.jsxs)(n.p,{children:["Each entry in the ",(0,t.jsx)(n.code,{children:"sections"})," array is an object with an ",(0,t.jsx)(n.code,{children:"id"})," and ",(0,t.jsx)(n.code,{children:"section"})," (a React node):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"{\n  id: 'my-section-id',\n  section: <SectionBox title=\"...\">...</SectionBox>,\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Headlamp's own built-in sections use IDs from ",(0,t.jsx)(n.code,{children:"DefaultDetailsViewSection"}),":"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Constant"}),(0,t.jsx)(n.th,{children:"What it is"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DefaultDetailsViewSection.MAIN_HEADER"})}),(0,t.jsx)(n.td,{children:"The standard metadata header (name, namespace, labels, etc.)"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DefaultDetailsViewSection.EVENTS"})}),(0,t.jsx)(n.td,{children:"The Kubernetes events table"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DefaultDetailsViewSection.BACK_LINK"})}),(0,t.jsx)(n.td,{children:"The back navigation link"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"You can use these constants to precisely position your sections relative to Headlamp's built-in ones."}),"\n",(0,t.jsx)(n.h3,{id:"positioning-strategies",children:"Positioning strategies"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Append to the end (simplest):"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"return [...sections, newSection];\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Insert after a specific section (e.g. pod containers
1):"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"const hasId = (s: (typeof sections)[number]): s is { id: string } =>\n  typeof s === 'object' && s !== null && 'id' in s;\nconst idx = sections.findIndex(s => hasId(s) && s.id === 'headlamp.pod-containers');\nif (idx === -1) {\n  return [...sections, newSection]; // section not found \u2014 append as fallback\n}\nconst updated = [...sections];\nupdated.splice(idx + 1, 0, newSection);\nreturn updated;\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Insert before a specific section (e.g. events):"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"const hasId = (s: (typeof sections)[number]): s is { id: string } =>\n  typeof s === 'object' && s !== null && 'id' in s;\nconst eventsIdx = sections.findIndex(s => hasId(s) && s.id === DefaultDetailsViewSection.EVENTS);\nif (eventsIdx !== -1) {\n  const updated = [...sections];\n  updated.splice(eventsIdx, 0, newSection);\n  return updated;\n}\nreturn [...sections, newSection];\n"})}),"\n",(0,t.jsx)(n.h3,{id:"idempotency-guard",children:"Idempotency guard"}),"\n",(0,t.jsx)(n.p,{children:"Headlamp may call your processor multiple times as the component re-renders. Always check whether your section is already present before adding it, to avoid duplicates:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"const alreadyAdded = sections.some(\n  s => typeof s === 'object' && s !== null && 'id' in s && s.id === 'my-section-id'\n);\nif (alreadyAdded) return sections;\n"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"complete-example",children:"Complete Example"}),"\n",(0,t.jsxs)(n.p,{children:["Here is the full set of additions to your ",(0,t.jsx)(n.code,{children:"src/index.tsx"})," for this tutorial. These lines can be added ",(0,t.jsx)(n.strong,{children:"after"})," all the existing code from Tutorial 6 (the ",(0,t.jsx)(n.code,{children:"registerRoute"})," and ",(0,t.jsx)(n.code,{children:"registerSidebarEntry"})," calls at the bottom):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import { useState } from 'react';\nimport {\n  registerResourceTableColumnsProcessor,\n  registerDetailsViewHeaderAction,\n  registerDetailsViewSectionsProcessor,\n  DefaultDetailsViewSection,\n} from '@kinvolk/headlamp-plugin/lib';\nimport {\n  ActionButton,\n  ResourceTableColumn,\n  SectionBox,\n  SimpleTable,\n} from '@kinvolk/headlamp-plugin/lib/CommonComponents';\nimport Pod from '@kinvolk/headlamp-plugin/lib/K8s/pod';\nimport {\n  Button,\n  Dialog,\n  DialogActions,\n  DialogContent,\n  DialogTitle,\n  Tooltip,\n  Typography,\n} from '@mui/material';\n\n// \u2500\u2500\u2500 1. Add Container Images column to the built-in Pods list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nregisterResourceTableColumnsProcessor(function addContainerImagesColumn({ id, columns }) {\n  if (id !== 'headlamp-pods') {\n    return columns;\n  }\n\n  const podColumns = columns as ResourceTableColumn<Pod>[];\n\n  podColumns.push({\n    id: 'hello-headlamp-container-images',\n    label: 'Container Images',\n    getValue: (pod: Pod) => pod.spec.containers.map(c => c.image).join(', '),\n    render: (pod: Pod) => {\n      const images = pod.spec.containers.map(c => c.image);\n      return (\n        <Tooltip title={images.join('\\n')} placement=\"bottom-start\">\n          <Typography noWrap style={{ maxWidth: 240, fontSize: 'inherit' }}>\n            {images[0]}\n            {images.length > 1 ? ` (+${images.length - 1} more)` : ''}\n          </Typography>\n        </Tooltip>\n      );\n    },\n  });\n\n  return columns;\n});\n\n// \u2500\u2500\u2500 2. Add Image Details action to the Pod detail header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction ImageDetailsAction({ item }: { item: any }) {\n  const [open, setOpen] = useState(false);\n\n  if (!item || item.kind !== 'Pod') {\n    return null;\n  }\n\n  const containers: Array<{\n    name: string;\n    image: string;\n    imagePullPolicy?: string;\n  }> = item.spec?.containers || [];\n\n  return (\n    <>\n      <ActionButton\n        description=\"Image Details\"\n        icon=\"mdi:image-search-outline\"\n        onClick={() => setOpen(true)}\n      />\n      <Dialog open={open} onClose={() => setOpen(false)} maxWidth=\"md\" fullWidth>\n        <DialogTitle>Container Images \u2014 {item.metadata?.name}</DialogTitle>\n        <DialogContent>\n          <SimpleTable\n            data={containers}\n            columns={[\n              { label: 'Container', getter: (c: any) => c.name },\n              { label: 'Image', getter: (c: any) => c.image },\n              {\n                label: 'Pull Policy',\n                getter: (c: any) => c.imagePullPolicy || 'IfNotPresent (default)',\n              },\n            ]}\n          />\n        </DialogContent>\n        <DialogActions>\n          <Button onClick={() => setOpen(false)}>Close</Button>\n        </DialogActions>\n      </Dialog>\n    </>\n  );\n}\n\nregisterDetailsViewHeaderAction(ImageDetailsAction);\n\n// \u2500\u2500\u2500 3. Add Container Resources section to the Pod detail page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nregisterDetailsViewSectionsProcessor(function addContainerResourcesSection(resource, sections) {\n  if (!resource || resource.kind !== 'Pod') {\n    return sections;\n  }\n\n  const sectionId = 'hello-headlamp-container-resources';\n  if (sections.some(s => typeof s === 'object' && s !== null && 'id' in s && s.id === sectionId)) {\n    return sections;\n  }\n\n  const containers: Array<{\n    name: string;\n    cpuRequest: string;\n    cpuLimit: string;\n    memoryRequest: string;\n    memoryLimit: string;\n  }> = (resource.jsonData?.spec?.containers ?? []).map((c: any) => ({\n    name: c.name,\n    cpuRequest: c.resources?.requests?.cpu ?? '\u2014',\n    cpuLimit: c.resources?.limits?.cpu ?? '\u2014',\n    memoryRequest: c.resources?.requests?.memory ?? '\u2014',\n    memoryLimit: c.resources?.limits?.memory ?? '\u2014',\n  }));\n\n  const newSection = {\n    id: sectionId,\n    section: (\n      <SectionBox title=\"Container Resources\">\n        <SimpleTable\n          data={containers}\n          columns={[\n            { label: 'Container', getter: (c: any) => c.name ?? '\u2014' },\n            { label: 'CPU Request', getter: (c: any) => c.cpuRequest ?? '\u2014' },\n            { label: 'CPU Limit', getter: (c: any) => c.cpuLimit ?? '\u2014' },\n            { label: 'Memory Request', getter: (c: any) => c.memoryRequest ?? '\u2014' },\n            { label: 'Memory Limit', getter: (c: any) => c.memoryLimit ?? '\u2014' },\n          ]}\n        />\n      </SectionBox>\n    ),\n  };\n\n  const hasId = (s: (typeof sections)[number]): s is { id: string } =>\n    typeof s === 'object' && s !== null && 'id' in s;\n  const podContainersIdx = sections.findIndex(\n    s => hasId(s) && s.id === 'headlamp.pod-containers'\n  );\n\n  if (podContainersIdx === -1) {\n    return [...sections, newSection];\n  }\n\n  const updated = [...sections];\n  updated.splice(podContainersIdx + 1, 0, newSection);\n  return updated;\n});\n"})}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,t.jsx)(n.h3,{id:"column-not-appearing-in-the-pods-list",children:"Column not appearing in the Pods list"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Check that you are navigating to ",(0,t.jsx)(n.strong,{children:"Workloads"})," \u2192 ",(0,t.jsx)(n.strong,{children:"Pods"})," (Headlamp's built-in page), not the custom ",(0,t.jsx)(n.code,{children:"MyPodsPage"})," from Tutorial 6."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"console.log(id)"})," inside your processor and check the browser DevTools console to confirm the table ID is ",(0,t.jsx)(n.code,{children:"'headlamp-pods'"}),"."]}),"\n",(0,t.jsx)(n.li,{children:"Make sure the processor call is at module level (
1outside any component or function), so it runs when the plugin loads."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"action-button-not-appearing-on-pod-detail-pages",children:"Action button not appearing on Pod detail pages"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Confirm your component returns ",(0,t.jsx)(n.code,{children:"null"})," for non-Pod resources and a valid React node for Pods."]}),"\n",(0,t.jsxs)(n.li,{children:["Check the browser console for errors inside ",(0,t.jsx)(n.code,{children:"ImageDetailsAction"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["Verify the ",(0,t.jsx)(n.code,{children:"useState"})," import is at the top of the file."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"section-not-appearing-or-appearing-multiple-times",children:"Section not appearing or appearing multiple times"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Check the idempotency guard: the ",(0,t.jsx)(n.code,{children:"sections.some(...)"})," check must match the exact ",(0,t.jsx)(n.code,{children:"id"})," string you use when constructing ",(0,t.jsx)(n.code,{children:"newSection"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["If ",(0,t.jsx)(n.code,{children:"headlamp.pod-containers"})," is not found (",(0,t.jsx)(n.code,{children:"podContainersIdx === -1"}),"), the section falls back to appending at the end \u2014 ",(0,t.jsx)(n.code,{children:"console.log(sections.map(s => s?.id))"})," inside your processor to inspect the current section IDs and confirm the expected anchor section is present."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"console.log(sections)"})," inside your processor to inspect the full sections array."]}),"\n"]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"whats-next",children:"What's Next"}),"\n",(0,t.jsx)(n.p,{children:"You've learned how to extend Headlamp's built-in views without replacing them:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\u2705 Using ",(0,t.jsx)(n.code,{children:"registerResourceTableColumnsProcessor"})," to add columns to existing resource tables"]}),"\n",(0,t.jsxs)(n.li,{children:["\u2705 Using ",(0,t.jsx)(n.code,{children:"registerDetailsViewHeaderAction"})," to add action buttons to existing detail pages"]}),"\n",(0,t.jsxs)(n.li,{children:["\u2705 Using ",(0,t.jsx)(n.code,{children:"registerDetailsViewSectionsProcessor"})," to inject sections into existing detail pages"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Coming up in Tutorial 8: Adding Plugin Settings"})}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Defining a settings schema for your plugin"}),"\n",(0,t.jsx)(n.li,{children:"Reading settings values at runtime"}),"\n",(0,t.jsx)(n.li,{children:"Making plugin behaviour configurable by users"}),"\n"]}),"\n",(0,t.jsx)(n.hr,{}),"\n",(0,t.jsx)(n.h2,{id:"quick-reference",children:"Quick Reference"}),"\n",(0,t.jsx)(n.h3,{id:"registerresourcetablecolumnsprocessor",children:"registerResourceTableColumnsProcessor"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import { registerResourceTableColumnsProcessor } from '@kinvolk/headlamp-plugin/lib';\n\nregisterResourceTableColumnsProcessor(function myProcessor({ id, columns }) {\n  if (id !== 'headlamp-pods') return columns;   // target a specific table\n\n  columns.push({\n    id: 'my-plugin-column-id',      // prefix with plugin name to avoid clashes\n    label: 'Column Header',\n    getValue: item => 'plain value', // for sorting and filtering (required)\n    render: item => <Node />,        // for cell display (optional)\n  });\n\n  return columns;\n});\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Common table IDs:"})," ",(0,t.jsx)(n.code,{children:"headlamp-pods"}),", ",(0,t.jsx)(n.code,{children:"headlamp-deployments"}),", ",(0,t.jsx)(n.code,{children:"headlamp-nodes"}),", ",(0,t.jsx)(n.code,{children:"headlamp-namespaces"}),", ",(0,t.jsx)(n.code,{children:"headlamp-services"})]}),"\n",(0,t.jsx)(n.h3,{id:"registerdetailsviewheaderaction",children:"registerDetailsViewHeaderAction"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import { registerDetailsViewHeaderAction } from '@kinvolk/headlamp-plugin/lib';\nimport { ActionButton } from '@kinvolk/headlamp-plugin/lib/CommonComponents';\n\nfunction MyAction({ item }: { item: any }) {\n  if (!item || item.kind !== 'Pod') return null; // filter by resource kind\n\n  return (\n    <ActionButton\n      description=\"Tooltip text\"\n      icon=\"mdi:icon-name\"\n      onClick={() => { /* ... */ }}\n    />\n  );\n}\n\nregisterDetailsViewHeaderAction(MyAction);\n\n// Or with an explicit ID (for later reference/replacement):\nregisterDetailsViewHeaderAction({ id: 'my-plugin-action', action: MyAction });\n"})}),"\n",(0,t.jsx)(n.h3,{id:"registerdetailsviewsectionsprocessor",children:"registerDetailsViewSectionsProcessor"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"import {\n  registerDetailsViewSectionsProcessor,\n  DefaultDetailsViewSection,\n} from '@kinvolk/headlamp-plugin/lib';\nimport { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';\n\nregisterDetailsViewSectionsProcessor(function myProcessor(resource, sections) {\n  if (!resource || resource.kind !== 'Pod') return sections;  // filter by kind\n\n  const sectionId = 'my-plugin-section';\n  // Idempotency guard\n  if (sections.some(s => typeof s === 'object' && s !== null && 'id' in s && s.id === sectionId)) {\n    return sections;\n  }\n\n  const newSection = {\n    id: sectionId,\n    section: <SectionBox title=\"My Section\">...</SectionBox>,\n  };\n\n  // Insert after the main header\n  const idx = sections.findIndex(\n    s => typeof s === 'object' && s !== null && 'id' in s && s.id === DefaultDetailsViewSection.MAIN_HEADER\n  );\n  const updated = [...sections];\n  updated.splice(idx + 1, 0, newSection);\n  return updated;\n});\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"DefaultDetailsViewSection constants:"})," ",(0,t.jsx)(n.code,{children:"MAIN_HEADER"}),", ",(0,t.jsx)(n.code,{children:"EVENTS"}),", ",(0,t.jsx)(n.code,{children:"BACK_LINK"})]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.