diff --git a/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx b/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx
index 79a1b6a2761..cd042c5398b 100644
--- a/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx
+++ b/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx
@@ -26,6 +26,7 @@ import HPA from '../../../lib/k8s/hpa';
import Pod from '../../../lib/k8s/pod';
import ReplicaSet from '../../../lib/k8s/replicaSet';
import Service from '../../../lib/k8s/service';
+import StatefulSet from '../../../lib/k8s/statefulSet';
import { DateLabel } from '../../common/Label';
import { DeploymentGlance } from './DeploymentGlance';
import { EndpointsGlance } from './EndpointsGlance';
@@ -46,32 +47,30 @@ export const KubeObjectGlance = memo(({ resource }: { resource: KubeObject }) =>
);
}, []);
- const kind = resource.kind;
-
const sections = [];
- if (kind === 'Pod') {
- sections.push();
+ if (Pod.isClassOf(resource)) {
+ sections.push();
}
- if (kind === 'Deployment') {
- sections.push();
+ if (Deployment.isClassOf(resource)) {
+ sections.push();
}
- if (kind === 'Service') {
- sections.push();
+ if (Service.isClassOf(resource)) {
+ sections.push();
}
- if (kind === 'Endpoints') {
- sections.push();
+ if (Endpoints.isClassOf(resource)) {
+ sections.push();
}
- if (kind === 'ReplicaSet' || kind === 'StatefulSet') {
- sections.push();
+ if (ReplicaSet.isClassOf(resource) || StatefulSet.isClassOf(resource)) {
+ sections.push();
}
- if (kind === 'HorizontalPodAutoscaler') {
- sections.push();
+ if (HPA.isClassOf(resource)) {
+ sections.push();
}
if (events.length > 0) {
diff --git a/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx b/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx
index 619c8d9c053..8afc26eae6c 100644
--- a/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx
+++ b/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx
@@ -16,10 +16,11 @@
import { Box } from '@mui/system';
import { useTranslation } from 'react-i18next';
-import ReplicaSet from '../../../lib/k8s/replicaSet';
+import type ReplicaSet from '../../../lib/k8s/replicaSet';
+import type StatefulSet from '../../../lib/k8s/statefulSet';
import { StatusLabel } from '../../common/Label';
-export function ReplicaSetGlance({ set }: { set: ReplicaSet }) {
+export function ReplicaSetGlance({ set }: { set: ReplicaSet | StatefulSet }) {
const { t } = useTranslation();
const ready = set.status?.readyReplicas || 0;
const desired = set.spec?.replicas || 0;
diff --git a/frontend/src/components/resourceMap/graph/graphFiltering.test.ts b/frontend/src/components/resourceMap/graph/graphFiltering.test.ts
index 894fbc6a1f7..fc7c45337f1 100644
--- a/frontend/src/components/resourceMap/graph/graphFiltering.test.ts
+++ b/frontend/src/components/resourceMap/graph/graphFiltering.test.ts
@@ -14,32 +14,41 @@
* limitations under the License.
*/
+import App from '../../../App';
import { KubeMetadata } from '../../../lib/k8s/KubeMetadata';
-import { KubeObject } from '../../../lib/k8s/KubeObject';
+import Pod from '../../../lib/k8s/pod';
import { filterGraph, GraphFilter } from './graphFiltering';
import { GraphEdge, GraphNode } from './graphModel';
+// circular dependency fix
+// eslint-disable-next-line no-unused-vars
+const _dont_delete_me = App;
+
describe('filterGraph', () => {
const nodes: GraphNode[] = [
{
id: '1',
- kubeObject: { metadata: { namespace: 'ns1', name: 'node1' } as KubeMetadata } as KubeObject,
+ kubeObject: new Pod({
+ kind: 'Pod',
+ metadata: { namespace: 'ns1', name: 'node1' },
+ status: {},
+ } as any),
},
{
id: '2',
- kubeObject: {
+ kubeObject: new Pod({
kind: 'Pod',
metadata: { namespace: 'ns2' } as KubeMetadata,
status: { phase: 'Failed' },
- } as any,
+ } as any),
},
{
id: '3',
- kubeObject: { metadata: { namespace: 'ns3' } as KubeMetadata } as KubeObject,
+ kubeObject: new Pod({ kind: 'Pod', metadata: { namespace: 'ns3' }, status: {} } as any),
},
{
id: '4',
- kubeObject: { metadata: { namespace: 'ns3' } as KubeMetadata } as KubeObject,
+ kubeObject: new Pod({ kind: 'Pod', metadata: { namespace: 'ns3' }, status: {} } as any),
},
];
diff --git a/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx b/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx
index 1daf0819650..f721e4367bd 100644
--- a/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx
+++ b/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx
@@ -47,7 +47,7 @@ function getPodStatus(pod: Pod): KubeObjectStatus {
* Not all kinds of resources have a status and/or supported
*/
export function getStatus(w: KubeObject): KubeObjectStatus {
- if (w.kind === 'Pod') return getPodStatus(w as Pod);
+ if (Pod.isClassOf(w)) return getPodStatus(w);
if (['DaemonSet', 'ReplicaSet', 'StatefulSet', 'Deployment'].includes(w.kind)) {
const workload = w as Workload;
diff --git a/frontend/src/components/resourceMap/sources/definitions/graphDefinitionUtils.tsx b/frontend/src/components/resourceMap/sources/definitions/graphDefinitionUtils.tsx
new file mode 100644
index 00000000000..2991d92777f
--- /dev/null
+++ b/frontend/src/components/resourceMap/sources/definitions/graphDefinitionUtils.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { KubeObjectClass } from '../../../../lib/k8s/KubeObject';
+
+/** Create a unique API resource ID consisting of the API group name and kind */
+export function makeKubeSourceId(kubeObjectClass: KubeObjectClass) {
+ const group = kubeObjectClass.apiGroupName;
+
+ return group ? group + '/' + kubeObjectClass.kind : kubeObjectClass.kind;
+}
diff --git a/frontend/src/components/resourceMap/sources/definitions/relations.tsx b/frontend/src/components/resourceMap/sources/definitions/relations.tsx
index 8ef61360136..03c58e84ff9 100644
--- a/frontend/src/components/resourceMap/sources/definitions/relations.tsx
+++ b/frontend/src/components/resourceMap/sources/definitions/relations.tsx
@@ -45,6 +45,7 @@ import StatefulSet from '../../../../lib/k8s/statefulSet';
import ValidatingWebhookConfiguration from '../../../../lib/k8s/validatingWebhookConfiguration';
import { useNamespaces } from '../../../../redux/filterSlice';
import { Relation } from '../../graph/graphModel';
+import { makeKubeSourceId } from './graphDefinitionUtils';
/**
* Check if the given item has matching labels
@@ -62,8 +63,8 @@ const makeRelation = (
to: To,
selector: (a: InstanceType, b: InstanceType) => unknown
): Relation => ({
- fromSource: from.kind,
- toSource: to.kind,
+ fromSource: makeKubeSourceId(from),
+ toSource: makeKubeSourceId(to),
predicate(fromNode, toNode) {
const fromObject = fromNode.kubeObject as InstanceType;
const toObject = toNode.kubeObject as InstanceType;
@@ -73,7 +74,7 @@ const makeRelation = (
});
const makeOwnerRelation = (cl: KubeObjectClass): Relation => ({
- fromSource: cl.kind,
+ fromSource: makeKubeSourceId(cl),
predicate(from, to) {
const obj = from.kubeObject as KubeObject;
@@ -85,7 +86,7 @@ const makeOwnerRelation = (cl: KubeObjectClass): Relation => ({
});
const makeOwnerRelationReversed = (cl: KubeObjectClass): Relation => ({
- fromSource: cl.kind,
+ fromSource: makeKubeSourceId(cl),
predicate(from, to) {
const obj = to.kubeObject as KubeObject;
diff --git a/frontend/src/components/resourceMap/sources/definitions/sources.tsx b/frontend/src/components/resourceMap/sources/definitions/sources.tsx
index 333d091857a..6142a40a9c4 100644
--- a/frontend/src/components/resourceMap/sources/definitions/sources.tsx
+++ b/frontend/src/components/resourceMap/sources/definitions/sources.tsx
@@ -51,12 +51,13 @@ import { useNamespaces } from '../../../../redux/filterSlice';
import { GraphSource } from '../../graph/graphModel';
import { getKindGroupColor, KubeIcon } from '../../kubeIcon/KubeIcon';
import { makeKubeObjectNode } from '../GraphSources';
+import { makeKubeSourceId } from './graphDefinitionUtils';
/**
* Create a GraphSource from KubeObject class definition
*/
const makeKubeSource = (cl: KubeObjectClass): GraphSource => ({
- id: cl.kind,
+ id: makeKubeSourceId(cl),
label: cl.apiName,
icon: ,
useData() {
@@ -72,6 +73,8 @@ const generateCRSources = (crds: CRD[]): GraphSource[] => {
for (const crd of crds) {
const [group] = crd.getMainAPIGroup();
const source = makeKubeSource(crd.makeCRClass());
+ // Add crd prefix to avoid id clashes with resources already defined in other places
+ source.id = 'crd-' + source.id;
if (!groupedSources.has(group)) {
groupedSources.set(group, []);
diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts
index 8b5dcf5b994..0b20e587dc3 100644
--- a/frontend/src/lib/k8s/KubeObject.ts
+++ b/frontend/src/lib/k8s/KubeObject.ts
@@ -122,6 +122,41 @@ export class KubeObject {
return this.kind;
}
+ /**
+ * Get name of the API group of this resource
+ * for example will return batch for CronJob
+ *
+ * For core group, like Pods, it will return undefined
+ *
+ * API group reference https://kubernetes.io/docs/reference/using-api/#api-groups
+ */
+ static get apiGroupName(): string | undefined {
+ // Get any of the versions, group will be the same
+ const apiVersion = typeof this.apiVersion === 'string' ? this.apiVersion : this.apiVersion[0];
+
+ if (!apiVersion.includes('/')) return;
+
+ return apiVersion.split('/')[0];
+ }
+
+ /**
+ * Type guard to check if a KubeObject instance belongs to this class.
+ * Compares API group name and kind to determine if the instance matches.
+ * This works even if class definitions are duplicated and should be used
+ * instead of `instanceof`.
+ *
+ * @param maybeInstance - The KubeObject instance to check.
+ * @returns True if the instance is of this class type, with narrowed type.
+ */
+ static isClassOf(
+ this: K,
+ maybeInstance: KubeObject
+ ): maybeInstance is InstanceType {
+ return (
+ maybeInstance._class().apiGroupName === this.apiGroupName && maybeInstance.kind === this.kind
+ );
+ }
+
static get pluralName(): string {
// This is a naive way to get the plural name of the object by default. It will
// work in most cases, but for exceptions (like Ingress), we must override this.