Skip to content

Commit 08439fa

Browse files
committed
frontend: resourceMap: Support apiGroup + kind for registerKindIcon
- Extended registerKindIcon to accept an optional apiGroup. - Updated icon lookup logic to prioritize apiGroup/kind before falling back to kind. - Preserved backward compatibility for existing plugins using kind only registration. - Updated relevant types and selectors to support the new behavior.
1 parent 074548f commit 08439fa

File tree

5 files changed

+95
-38
lines changed

5 files changed

+95
-38
lines changed

frontend/src/components/resourceMap/graphViewSlice.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,13 @@ export const graphViewSlice = createSlice({
7575
}
7676
state.graphSources.push(action.payload);
7777
},
78-
addKindIcon(state, action: PayloadAction<{ kind: string; definition: IconDefinition }>) {
79-
state.kindIcons[action.payload.kind] = action.payload.definition;
78+
addKindIcon(
79+
state,
80+
action: PayloadAction<{ kind: string; definition: IconDefinition; apiGroup?: string }>
81+
) {
82+
const { kind, definition, apiGroup } = action.payload;
83+
const key = apiGroup ? `${apiGroup}/${kind}` : kind;
84+
state.kindIcons[key] = definition;
8085
},
8186
setGlance(state, action: PayloadAction<Glance>) {
8287
state.glances[action.payload.id] = action.payload;

frontend/src/components/resourceMap/kubeIcon/KubeIcon.tsx

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,39 +48,54 @@ import SvcIcon from './img/svc.svg?react';
4848
import UserIcon from './img/user.svg?react';
4949
import VolIcon from './img/vol.svg?react';
5050

51-
const kindToIcon = {
52-
ClusterRole: CRoleIcon,
53-
ClusterRoleBinding: CrbIcon,
54-
CronJob: CronjobIcon,
55-
DaemonSet: DsIcon,
56-
Group: GroupIcon,
57-
Ingress: IngIcon,
58-
LimitRange: LimitsIcon,
51+
const kindToIcon: Record<string, React.FC<any>> = {
52+
// core group
5953
Namespace: NsIcon,
60-
PodSecurityPolicy: PspIcon,
61-
PersistentVolumeClaim: PvcIcon,
62-
RoleBinding: RbIcon,
63-
ReplicaSet: RsIcon,
64-
StorageClass: ScIcon,
65-
StatefulSet: StsIcon,
66-
User: UserIcon,
67-
ConfigMap: CmIcon,
68-
CustomResourceDefinition: CrdIcon,
69-
Deployment: DeployIcon,
70-
Endpoint: EpIcon,
54+
Pod: PodIcon,
55+
Service: SvcIcon,
7156
Endpoints: EpIcon,
57+
Endpoint: EpIcon,
7258
EndpointSlice: EpIcon,
73-
HorizontalPodAutoscaler: HpaIcon,
74-
Job: JobIcon,
75-
NetworkPolicy: NetpolIcon,
76-
Pod: PodIcon,
59+
ConfigMap: CmIcon,
60+
Secret: SecretIcon,
7761
PersistentVolume: PvIcon,
78-
ResourceQuota: QuotaIcon,
79-
Role: RoleIcon,
62+
PodSecurityPolicy: PspIcon,
63+
PersistentVolumeClaim: PvcIcon,
8064
ServiceAccount: SaIcon,
81-
Secret: SecretIcon,
82-
Service: SvcIcon,
65+
ResourceQuota: QuotaIcon,
66+
LimitRange: LimitsIcon,
8367
Volume: VolIcon,
68+
User: UserIcon,
69+
Group: GroupIcon,
70+
71+
// apps
72+
'apps/Deployment': DeployIcon,
73+
'apps/ReplicaSet': RsIcon,
74+
'apps/StatefulSet': StsIcon,
75+
'apps/DaemonSet': DsIcon,
76+
77+
// batch
78+
'batch/Job': JobIcon,
79+
'batch/CronJob': CronjobIcon,
80+
81+
// rbac
82+
'rbac.authorization.k8s.io/Role': RoleIcon,
83+
'rbac.authorization.k8s.io/RoleBinding': RbIcon,
84+
'rbac.authorization.k8s.io/ClusterRole': CRoleIcon,
85+
'rbac.authorization.k8s.io/ClusterRoleBinding': CrbIcon,
86+
87+
// networking
88+
'networking.k8s.io/Ingress': IngIcon,
89+
'networking.k8s.io/NetworkPolicy': NetpolIcon,
90+
91+
// autoscaling
92+
'autoscaling/HorizontalPodAutoscaler': HpaIcon,
93+
94+
// storage
95+
'storage.k8s.io/StorageClass': ScIcon,
96+
97+
// apiextensions
98+
'apiextensions.k8s.io/CustomResourceDefinition': CrdIcon,
8499
} as const;
85100

86101
const kindGroups = {
@@ -141,26 +156,37 @@ export const getKindGroupColor = (group: keyof typeof kindGroupColors) =>
141156
* https://github.com/kubernetes/community/tree/master/icons
142157
*
143158
* @param params.kind - Resource kind
159+
* @param params.apiGroup - Resource API group
144160
* @param params.width - width in css units
145161
* @param params.height - width in css units
146162
* @returns
147163
*/
148164
export function KubeIcon({
149165
kind,
166+
apiGroup,
150167
width,
151168
height,
152169
}: {
153-
kind: keyof typeof kindToIcon;
170+
kind: string;
171+
apiGroup?: string;
154172
width?: string;
155173
height?: string;
156174
}) {
157175
const pluginDefinedIcons = useTypedSelector(state => state.graphView.kindIcons);
158176

159-
const IconComponent = kindToIcon[kind] ?? kindToIcon['Pod'];
160-
const icon = pluginDefinedIcons[kind]?.icon ?? (
177+
const apiGroupKey = apiGroup ? `${apiGroup}/${kind}` : null;
178+
179+
const pluginIcon = (apiGroupKey && pluginDefinedIcons[apiGroupKey]) || pluginDefinedIcons[kind];
180+
181+
const IconComponent =
182+
(apiGroupKey && kindToIcon[apiGroupKey as keyof typeof kindToIcon]) ||
183+
kindToIcon[kind as keyof typeof kindToIcon] ||
184+
kindToIcon['Pod'];
185+
186+
const icon = pluginIcon?.icon ?? (
161187
<IconComponent style={{ scale: '1.1', width: '100%', height: '100%' }} />
162188
);
163-
const color = pluginDefinedIcons[kind]?.color ?? getKindColor(kind);
189+
const color = pluginIcon?.color ?? getKindColor(kind);
164190

165191
return (
166192
<Box

frontend/src/components/resourceMap/nodes/GroupNode.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export const GroupNodeComponent = memo(({ id }: { id: string }) => {
5050
const graph = useGraphView();
5151
const node = useNode(id);
5252

53+
const kubeObject = node?.kubeObject;
54+
55+
const apiGroup =
56+
kubeObject?.jsonData?.apiVersion && kubeObject.jsonData.apiVersion.includes('/')
57+
? kubeObject.jsonData.apiVersion.split('/')[0]
58+
: 'core';
59+
5360
const handleSelect = () => {
5461
graph.setNodeSelection(id);
5562
};
@@ -68,7 +75,7 @@ export const GroupNodeComponent = memo(({ id }: { id: string }) => {
6875
{(node?.label || node?.subtitle) && (
6976
<Label title={node?.label}>
7077
{node?.kubeObject ? (
71-
<KubeIcon kind={node.kubeObject.kind} width="24px" height="24px" />
78+
<KubeIcon kind={node.kubeObject.kind} apiGroup={apiGroup} width="24px" height="24px" />
7279
) : (
7380
node?.icon ?? null
7481
)}

frontend/src/components/resourceMap/nodes/KubeObjectNode.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ export const KubeObjectNodeComponent = memo(({ id }: NodeProps) => {
151151
const mainNode = node?.nodes ? getMainNode(node.nodes) : undefined;
152152
const kubeObject = node?.kubeObject ?? mainNode?.kubeObject;
153153

154+
const apiGroup =
155+
kubeObject?.jsonData?.apiVersion && kubeObject.jsonData.apiVersion.includes('/')
156+
? kubeObject.jsonData.apiVersion.split('/')[0]
157+
: 'core';
158+
154159
const isSelected = id === graph.nodeSelection;
155160
const isCollapsed = node?.nodes?.length ? node?.collapsed : true;
156161

@@ -186,7 +191,7 @@ export const KubeObjectNodeComponent = memo(({ id }: NodeProps) => {
186191
}, [isHovered]);
187192

188193
const icon = kubeObject ? (
189-
<KubeIcon width="42px" height="42px" kind={kubeObject.kind} />
194+
<KubeIcon width="42px" height="42px" kind={kubeObject.kind} apiGroup={apiGroup} />
190195
) : (
191196
node?.icon ?? null
192197
);
@@ -207,7 +212,7 @@ export const KubeObjectNodeComponent = memo(({ id }: NodeProps) => {
207212
cluster: node.kubeObject?.cluster,
208213
hideTitleInHeader: true,
209214
icon: node.kubeObject ? (
210-
<KubeIcon kind={node.kubeObject.kind} width="100%" height="100%" />
215+
<KubeIcon kind={node.kubeObject.kind} apiGroup={apiGroup} width="100%" height="100%" />
211216
) : null,
212217
title: node.label ?? node.kubeObject?.metadata?.name,
213218
content: <GraphNodeDetails node={node} />,

frontend/src/plugin/registry.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -828,19 +828,33 @@ export function registerMapSource(source: GraphSource) {
828828
/**
829829
* Register Icon for a resource kind
830830
*
831+
* By default, icons are matched only by `kind`.
832+
* Optionally, `apiGroup` can be provided to differentiate resources that share the same kind across different API groups.
833+
*
834+
* When `apiGroup` is provided, Headlamp will:
835+
* 1. First try to match `${apiGroup}/${kind}`.
836+
* 2. Fall back to `kind` if no match is found.
837+
*
831838
* @param kind - Resource kind
832839
* @param {IconDefinition} definition - icon definition
833840
* @param definition.icon - React Element of the icon
834841
* @param definition.color - Color for the icon, optional
842+
* @param apiGroup - Kubernetes API group, optional
835843
*
836844
* @example
837845
*
846+
* Kind only Matching
838847
* ```tsx
839848
* registerKindIcon("MyCustomResource", { icon: <MyIcon />, color: "#FF0000" })
840849
* ```
850+
*
851+
* Match only networking service
852+
* ```tsx
853+
* registerKindIcon("Service", { icon: <NetworkingServiceIcon /> }, "networking.k8s.io");
854+
* ```
841855
*/
842-
export function registerKindIcon(kind: string, definition: IconDefinition) {
843-
store.dispatch(graphViewSlice.actions.addKindIcon({ kind, definition }));
856+
export function registerKindIcon(kind: string, definition: IconDefinition, apiGroup?: string) {
857+
store.dispatch(graphViewSlice.actions.addKindIcon({ kind, definition, apiGroup }));
844858
}
845859

846860
/**

0 commit comments

Comments
 (0)