Skip to content

Commit

Permalink
frontend: Map: Allow custom details components for GraphNode
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Dubenko <oldubenko@microsoft.com>
  • Loading branch information
sniok committed Feb 24, 2025
1 parent 3cfd647 commit e4219ec
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 33 deletions.
39 changes: 39 additions & 0 deletions docs/development/plugins/functionality/extending-the-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ const mySource = {
// resource field is required and should contain KubeObject
resource: new KubeObject(myResource),
},
// Optionally provide a custom details component to be shown when node is selected
detailsComponent: ({ node }) => {
return (
<div>
<h2>Custom Details View</h2>
<p>This is a custom details view for: {node.data.resource.metadata.name}</p>
</div>
);
},
},
];

Expand Down Expand Up @@ -121,3 +130,33 @@ registerKindIcon("MyCustomResource", {

<figcaption>Node with a custom Icon</figcaption>
</figure>

## Custom Detail Views

When a node is selected on the map, its details are shown in a side panel. By default, if the node represents a Kubernetes resource (has `kubeObject` property), Headlamp will show the standard resource details view.

You can override this behavior by providing a custom details component:

```tsx
const myNode = {
id: "custom-node",
label: "Node with custom details",
detailsComponent: ({ node }) => {
return (
<div>
<h2>Custom Details</h2>
<p>This is a custom details view for: {node.label}</p>
{/* You can access any node property here */}
<pre>{JSON.stringify(node.data, null, 2)}</pre>
</div>
);
},
};
```

The details component receives the node object as a prop, giving you access to all node properties including any custom data you added to the `data` field.

This is useful when you want to:
- Show custom visualizations for your resources
- Display data from external sources alongside Kubernetes resources
- Create interactive detail views specific to your use case
11 changes: 11 additions & 0 deletions frontend/src/components/resourceMap/GraphView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ const mockNodes: GraphNode[] = [
subtitle: 'Node Subtitle',
icon: <Icon icon="mdi:plus-circle-outline" width="32px" />,
},
{
id: 'custom-node-with-details',
label: 'Node with custom details',
subtitle: 'Click to see custom details',
detailsComponent: ({ node }) => (
<div>
<h3>Custom Details View</h3>
<p>This is a custom details view for node: {node.label}</p>
</div>
),
},
{
id: 'custon-node-2',
label: 'Node with children',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
>
<div
class="react-flow__viewport xyflow__viewport react-flow__container"
style="transform: translate(116px,56px) scale(1);"
style="transform: translate(50px,56px) scale(1);"
>
<div
class="react-flow__edges"
Expand Down Expand Up @@ -634,7 +634,7 @@
class="react-flow__node react-flow__node-object selectable"
data-id="custom-node-with-icon"
data-testid="rf__node-custom-node-with-icon"
style="z-index: 0; transform: translate(24px,342px); pointer-events: all; visibility: visible; width: 220px; height: 70px;"
style="z-index: 0; transform: translate(264px,252px); pointer-events: all; visibility: visible; width: 220px; height: 70px;"
>
<div
class="css-9y15hx"
Expand Down Expand Up @@ -675,6 +675,52 @@
</div>
</div>
</div>
<div
aria-describedby="react-flow__node-desc-1"
class="react-flow__node react-flow__node-object selectable"
data-id="custom-node-with-details"
data-testid="rf__node-custom-node-with-details"
style="z-index: 0; transform: translate(24px,342px); pointer-events: all; visibility: visible; width: 220px; height: 70px;"
>
<div
class="css-9y15hx"
role="button"
tabindex="0"
>
<div
class="react-flow__handle react-flow__handle-top nodrag nopan target connectable connectablestart connectableend connectionindicator"
data-handlepos="top"
data-id="1-custom-node-with-details-null-target"
data-nodeid="custom-node-with-details"
style="opacity: 0;"
/>
<div
class="react-flow__handle react-flow__handle-bottom nodrag nopan source connectable connectablestart connectableend connectionindicator"
data-handlepos="bottom"
data-id="1-custom-node-with-details-null-source"
data-nodeid="custom-node-with-details"
style="opacity: 0;"
/>
<div
class="css-1irety8"
>
<div
class="css-1o5h0m"
>
<div
class="css-x2hlid"
>
Click to see custom details
</div>
<div
class="css-estfx9"
>
Node with custom details
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="react-flow__viewport-portal"
Expand All @@ -693,7 +739,7 @@
patternTransform="translate(-11,-11)"
patternUnits="userSpaceOnUse"
width="20"
x="16"
x="10"
y="16"
>
<circle
Expand Down
32 changes: 5 additions & 27 deletions frontend/src/components/resourceMap/details/GraphNodeDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
import { Box, Card } from '@mui/material';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { ActionButton } from '../../common';
import { GraphNode } from '../graph/graphModel';
import { KubeObjectDetails } from './KubeNodeDetails';

interface GraphNodeDetailsSection {
id: string;
render: (node: GraphNode) => ReactNode;
}

const kubeNodeDetailsSection: GraphNodeDetailsSection = {
id: 'kubeObjectDetails',
render: node =>
node.kubeObject ? (
<KubeObjectDetails key={node.id} resource={(node as GraphNode).kubeObject!} />
) : null,
};

const defaultSections = [kubeNodeDetailsSection];

export interface GraphNodeDetailsProps {
/** Sections to render */
sections?: GraphNodeDetailsSection[];
/** Node to display */
node?: GraphNode;
/** Callback when the panel is closed */
Expand All @@ -32,18 +14,13 @@ export interface GraphNodeDetailsProps {
/**
* Side panel display information about a selected Node
*/
export function GraphNodeDetails({
sections = defaultSections,
node,
close,
}: GraphNodeDetailsProps) {
export function GraphNodeDetails({ node, close }: GraphNodeDetailsProps) {
const { t } = useTranslation();

if (!node) return null;

const renderedSections = sections.map(it => it.render(node)).filter(Boolean);

if (renderedSections.length === 0) return null;
const hasContent = node.detailsComponent || node.kubeObject;
if (!hasContent) return null;

return (
<Card
Expand Down Expand Up @@ -76,7 +53,8 @@ export function GraphNodeDetails({
/>
</Box>

{renderedSections}
{node.detailsComponent && <node.detailsComponent node={node} />}
{node.kubeObject && <KubeObjectDetails resource={node.kubeObject} />}
</Card>
);
}
4 changes: 3 additions & 1 deletion frontend/src/components/resourceMap/graph/graphModel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import { ComponentType, ReactNode } from 'react';
import { KubeObject } from '../../../lib/k8s/KubeObject';

export type GraphNode = {
Expand Down Expand Up @@ -26,6 +26,8 @@ export type GraphNode = {
edges?: GraphEdge[];
/** Whether this Node is collapsed. Only applies to Nodes that have child Nodes. */
collapsed?: boolean;
/** Custom component to render details for this node */
detailsComponent?: ComponentType<{ node: GraphNode }>;
/** Any custom data */
data?: any;
};
Expand Down
9 changes: 7 additions & 2 deletions plugins/examples/customizing-map/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Example Plugin: Customizing Map
# Customizing Map Example

This shows you how to customize Map view with your own Source, Nodes, Edges and Details.
This plugin demonstrates how to customize Headlamp's resource map view:

- Adding custom nodes
- Using custom icons
- Creating custom detail views
- Using custom data

```bash
cd plugins/examples/customizing-map
Expand Down
29 changes: 29 additions & 0 deletions plugins/examples/customizing-map/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@ import { registerMapSource } from '@kinvolk/headlamp-plugin/lib';
import { KubeObject } from '@kinvolk/headlamp-plugin/lib/K8s/cluster';
import { useMemo } from 'react';

// Example custom details component for our resource
function MyResourceDetails({ node }: { node: any }) {
return (
<div style={{ padding: '1rem' }}>
<h2 style={{ marginBottom: '1rem' }}>Custom Resource Details</h2>
<div style={{ display: 'grid', gap: '0.5rem' }}>
<div>
<strong>Name:</strong> {node.kubeObject.metadata.name}
</div>
<div>
<strong>Namespace:</strong> {node.kubeObject.metadata.namespace}
</div>
<div>
<strong>Created:</strong> {node.kubeObject.metadata.creationTimestamp}
</div>
{/* Add any custom visualization or interactive elements here */}
<div style={{ marginTop: '1rem' }}>
<h3>Resource JSON</h3>
<pre style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '4px' }}>
{JSON.stringify(node.kubeObject, null, 2)}
</pre>
</div>
</div>
</div>
);
}

registerMapSource({
id: 'my-source', // ID of the source should be unique
label: 'My Source', // label will be displayed in source picker
Expand Down Expand Up @@ -38,6 +65,8 @@ registerMapSource({
id: myResource.metadata.uid, // ID should be unique
kubeObject: new KubeObject(myResource),
icon: <img src="https://headlamp.dev/img/favicon.png" alt="MyResourceKind icon" />,
// Add custom details component to show our own view when the node is selected
detailsComponent: MyResourceDetails,
},
],
};
Expand Down

0 comments on commit e4219ec

Please sign in to comment.