diff --git a/packages/k8s-ui/src/components/resources/renderers/IngressClassReferences.test.tsx b/packages/k8s-ui/src/components/resources/renderers/IngressClassReferences.test.tsx
new file mode 100644
index 0000000000..d573be5cca
--- /dev/null
+++ b/packages/k8s-ui/src/components/resources/renderers/IngressClassReferences.test.tsx
@@ -0,0 +1,52 @@
+// @vitest-environment jsdom
+import { act } from 'react'
+import { createRoot } from 'react-dom/client'
+import { describe, expect, it, vi } from 'vitest'
+import { IngressRenderer } from './IngressRenderer'
+import { IngressClassRenderer } from './IngressClassRenderer'
+import { ingressClassParametersResourceRef } from '../../../utils/ingress-class-references'
+
+describe('ingressClassParametersResourceRef', () => {
+ it('defaults to the core group and cluster scope, and requires a namespace for namespaced parameters', () => {
+ expect(ingressClassParametersResourceRef({ kind: 'IngressClassParams', apiGroup: 'elbv2.k8s.aws', name: 'shared' }))
+ .toEqual({ kind: 'IngressClassParams', group: 'elbv2.k8s.aws', namespace: '', name: 'shared' })
+ expect(ingressClassParametersResourceRef({ kind: 'ConfigMap', scope: 'Namespace', namespace: 'controller', name: 'settings' }))
+ .toEqual({ kind: 'ConfigMap', group: '', namespace: 'controller', name: 'settings' })
+ expect(ingressClassParametersResourceRef({ kind: 'ConfigMap', scope: 'Namespace', name: 'settings' })).toBeNull()
+ })
+})
+
+describe('Ingress class navigation', () => {
+ it('links the Ingress class and both parameter scopes, but leaves the legacy annotation as text', async () => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
+ const container = document.createElement('div')
+ const root = createRoot(container)
+ const onNavigate = vi.fn()
+ const click = async (name: string) => {
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === name)
+ expect(button).toBeDefined()
+ await act(async () => button!.click())
+ }
+ try {
+ await act(async () => root.render())
+ await click('public')
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'ingressclasses', group: 'networking.k8s.io', namespace: '', name: 'public' })
+
+ await act(async () => root.render())
+ expect(container.textContent).toContain('old-controller')
+ expect([...container.querySelectorAll('button')].some(button => button.textContent === 'old-controller')).toBe(false)
+
+ for (const parameters of [
+ { name: 'shared', kind: 'IngressClassParams', apiGroup: 'elbv2.k8s.aws' },
+ { name: 'settings', kind: 'ConfigMap', scope: 'Namespace' as const, namespace: 'controller' },
+ ]) {
+ await act(async () => root.render())
+ await click(parameters.name)
+ expect(onNavigate).toHaveBeenLastCalledWith(ingressClassParametersResourceRef(parameters))
+ }
+ } finally {
+ await act(async () => root.unmount())
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
+ }
+ })
+})
diff --git a/packages/k8s-ui/src/components/resources/renderers/IngressClassRenderer.tsx b/packages/k8s-ui/src/components/resources/renderers/IngressClassRenderer.tsx
index cdd8706790..f447bfa98e 100644
--- a/packages/k8s-ui/src/components/resources/renderers/IngressClassRenderer.tsx
+++ b/packages/k8s-ui/src/components/resources/renderers/IngressClassRenderer.tsx
@@ -1,14 +1,18 @@
import { Globe } from 'lucide-react'
import { clsx } from 'clsx'
-import { Section, PropertyList, Property } from '../../ui/drawer-components'
+import { Section, PropertyList, Property, ResourceLink } from '../../ui/drawer-components'
+import type { ResourceRef } from '../../../types'
+import { ingressClassParametersResourceRef } from '../../../utils/ingress-class-references'
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
interface IngressClassRendererProps {
data: any
+ onNavigate?: (ref: ResourceRef) => void
}
-export function IngressClassRenderer({ data }: IngressClassRendererProps) {
+export function IngressClassRenderer({ data, onNavigate }: IngressClassRendererProps) {
const spec = data.spec || {}
+ const parametersRef = spec.parameters ? ingressClassParametersResourceRef(spec.parameters) : null
const annotations = data.metadata?.annotations || {}
const isDefault = annotations['ingressclass.kubernetes.io/is-default-class'] === 'true'
@@ -39,7 +43,10 @@ export function IngressClassRenderer({ data }: IngressClassRendererProps) {
{spec.parameters.apiGroup && }
-
+
+ : spec.parameters.name
+ } />
{spec.parameters.namespace && }
{spec.parameters.scope && }
diff --git a/packages/k8s-ui/src/components/resources/renderers/IngressRenderer.tsx b/packages/k8s-ui/src/components/resources/renderers/IngressRenderer.tsx
index 3ef9c12f34..d0e3740094 100644
--- a/packages/k8s-ui/src/components/resources/renderers/IngressRenderer.tsx
+++ b/packages/k8s-ui/src/components/resources/renderers/IngressRenderer.tsx
@@ -1,9 +1,10 @@
import { Globe, Shield, Clock } from 'lucide-react'
+import type { ResourceRef } from '../../../types'
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
interface IngressRendererProps {
data: any
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
+ onNavigate?: (ref: ResourceRef) => void
}
export function IngressRenderer({ data, onNavigate }: IngressRendererProps) {
@@ -42,7 +43,10 @@ export function IngressRenderer({ data, onNavigate }: IngressRendererProps) {
-
+
+ : data.metadata?.annotations?.['kubernetes.io/ingress.class']
+ } />
{lbIngress.length > 0 && (
)}
diff --git a/packages/k8s-ui/src/components/shared/ResourceRendererDispatch.tsx b/packages/k8s-ui/src/components/shared/ResourceRendererDispatch.tsx
index 68969915d1..6f5d0be65f 100644
--- a/packages/k8s-ui/src/components/shared/ResourceRendererDispatch.tsx
+++ b/packages/k8s-ui/src/components/shared/ResourceRendererDispatch.tsx
@@ -978,7 +978,7 @@ export function ResourceRendererDispatch({
{kind === 'authorizationpolicies' && }
{kind === 'mutatingwebhookconfigurations' && }
{kind === 'validatingwebhookconfigurations' && }
- {kind === 'ingressclasses' && }
+ {kind === 'ingressclasses' && }
{kind === 'priorityclasses' && }
{kind === 'runtimeclasses' && }
{kind === 'leases' && }
diff --git a/packages/k8s-ui/src/utils/ingress-class-references.ts b/packages/k8s-ui/src/utils/ingress-class-references.ts
new file mode 100644
index 0000000000..ba42340765
--- /dev/null
+++ b/packages/k8s-ui/src/utils/ingress-class-references.ts
@@ -0,0 +1,22 @@
+import type { ResourceRef } from '../types'
+
+interface IngressClassParametersReference {
+ name: string
+ kind: string
+ apiGroup?: string
+ scope?: 'Cluster' | 'Namespace'
+ namespace?: string
+}
+
+/** IngressClass parameters default to cluster scope and the core API group.
+ * A namespaced reference requires its own namespace; the class has none.
+ */
+export function ingressClassParametersResourceRef(ref: IngressClassParametersReference): ResourceRef | null {
+ if (ref.scope === 'Namespace' && !ref.namespace) return null
+ return {
+ kind: ref.kind,
+ group: ref.apiGroup ?? '',
+ namespace: ref.scope === 'Namespace' ? ref.namespace! : '',
+ name: ref.name,
+ }
+}
diff --git a/packages/k8s-ui/src/utils/navigation.test.ts b/packages/k8s-ui/src/utils/navigation.test.ts
index b1b413edb6..5fb6ba2b97 100644
--- a/packages/k8s-ui/src/utils/navigation.test.ts
+++ b/packages/k8s-ui/src/utils/navigation.test.ts
@@ -305,6 +305,19 @@ describe('refToSelectedResource', () => {
})
})
+ test('resolves AWS IngressClassParams without discovery', () => {
+ expect(refToSelectedResource({ kind: 'IngressClassParams', name: 'alb', group: 'elbv2.k8s.aws' })).toEqual({
+ kind: 'ingressclassparams',
+ name: 'alb',
+ namespace: '',
+ group: 'elbv2.k8s.aws',
+ })
+ expect(knownKindForPluralWithGroup('ingressclassparams', 'elbv2.k8s.aws')).toBe('IngressClassParams')
+ // Hosts without discovery fold the plural back to a Kind and re-pluralize it.
+ expect(pluralToKind('ingressclassparams')).toBe('IngressClassParams')
+ expect(kindToPlural(pluralToKind('ingressclassparams'))).toBe('ingressclassparams')
+ })
+
test('normalizes an omitted namespace for cluster-scoped references', () => {
expect(refToSelectedResource({ kind: 'NodePool', name: 'spot' })).toEqual({
kind: 'nodepools',
diff --git a/packages/k8s-ui/src/utils/navigation.ts b/packages/k8s-ui/src/utils/navigation.ts
index b355930142..6140770517 100644
--- a/packages/k8s-ui/src/utils/navigation.ts
+++ b/packages/k8s-ui/src/utils/navigation.ts
@@ -41,6 +41,10 @@ const BUILTIN_PLURAL_TO_KIND: Record = {
clusterrolebindings: 'ClusterRoleBinding',
serviceaccounts: 'ServiceAccount',
networkpolicies: 'NetworkPolicy',
+ // AWS Load Balancer Controller's IngressClass parameters. The Kind is
+ // singular and plural at once, so English rules cannot recover it in hosts
+ // that never load discovery.
+ ingressclassparams: 'IngressClassParams',
}
const BUILTIN_GROUP_KIND_TO_PLURAL: Record = {
@@ -54,6 +58,7 @@ const BUILTIN_GROUP_PLURAL_TO_KIND: Record = {
'metrics.k8s.io/pods': 'PodMetrics',
'metrics.k8s.io/nodes': 'NodeMetrics',
'scheduling.k8s.io/podgroups': 'PodGroup',
+ 'elbv2.k8s.aws/ingressclassparams': 'IngressClassParams',
}
// Dynamic map built from API discovery — populated by initNavigationMap().