Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
import { Badge } from '../../ui/Badge'
import type { ResourceRef } from '../../../types'
import { gatewayBackendResourceRef, gatewayParentResourceRef } from '../../../utils/gateway-references'

interface GRPCRouteRendererProps {
data: any
Expand Down Expand Up @@ -32,22 +33,8 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
? parentStatuses[0].conditions
: undefined

function toGatewayRef(ref: any): ResourceRef {
return {
kind: 'Gateway',
namespace: ref.namespace || routeNs,
name: ref.name,
group: 'gateway.networking.k8s.io',
}
}

function toServiceRef(backend: any): ResourceRef {
return {
kind: 'Service',
namespace: backend.namespace || routeNs,
name: backend.name,
}
}
const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)

return (
<>
Expand All @@ -59,8 +46,8 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
const gwName = p.parentRef?.name || 'unknown'
return cond?.reason
? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
: `Gateway "${gwName}" has not accepted this route.`
? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
: `Parent "${gwName}" has not accepted this route.`
}).join('; ')}
/>
)}
Expand Down Expand Up @@ -90,12 +77,12 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
}
/>
<Property
label="Parent Gateways"
label="Parents"
value={
parentRefs.length > 0 ? (
<div className="flex flex-wrap gap-1">
{parentRefs.map((ref: any, i: number) => (
<ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
<ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
))}
</div>
) : 'None'
Expand Down Expand Up @@ -129,7 +116,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
<div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
<span className="italic text-theme-text-tertiary">all</span>
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
{backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
</div>
)}
Expand All @@ -141,7 +128,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
return (
<div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
{b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
{pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
</div>
Expand Down Expand Up @@ -176,7 +163,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
{matchIdx === 0 && backendRefs.length === 1 && (
<>
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
{backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
</>
)}
Expand All @@ -189,7 +176,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
return (
<div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
{b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
{pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @vitest-environment jsdom
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { describe, expect, it, vi } from 'vitest'
import { HTTPRouteRenderer } from './HTTPRouteRenderer'
import { GRPCRouteRenderer } from './GRPCRouteRenderer'
import { SimpleRouteRenderer } from './SimpleRouteRenderer'

const backend = { name: 'custom-backend', kind: 'Widget', group: 'relationships.radar.test', namespace: 'backends' }
const parent = { name: 'mesh-parent', kind: 'Service', group: '', namespace: 'mesh' }

function routeData(kind: string) {
return {
metadata: { name: 'route', namespace: 'app' },
status: { parents: [{ parentRef: parent, conditions: [{ type: 'Accepted', status: 'False', reason: 'Rejected' }] }] },
spec: {
parentRefs: [parent],
rules: [{
backendRefs: [backend, { name: 'default-service', port: 8080 }],
...(kind === 'HTTPRoute' ? { filters: [{ type: 'RequestMirror', requestMirror: { backendRef: { ...backend, name: 'mirror' } } }] } : {}),
}],
},
}
}

describe('Gateway route reference navigation', () => {
it.each(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute'] as const)('preserves backend and parent identities in %s', async kind => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
const container = document.createElement('div')
const root = createRoot(container)
const onNavigate = vi.fn()
const data = routeData(kind)
const click = async (name: string) => {
const button = [...container.querySelectorAll('button')].find(button => button.textContent?.includes(name))
expect(button, `button ${name}`).toBeDefined()
await act(async () => button!.click())
}
try {
await act(async () => root.render(
kind === 'HTTPRoute' ? <HTTPRouteRenderer data={data} onNavigate={onNavigate} />
: kind === 'GRPCRoute' ? <GRPCRouteRenderer data={data} onNavigate={onNavigate} />
: <SimpleRouteRenderer kind={kind} data={data} onNavigate={onNavigate} />,
))
expect(container.textContent).toContain('Parents')
expect(container.textContent).toContain('Parent "mesh-parent"')
expect(container.textContent).not.toContain('Gateway "mesh-parent"')

await click('custom-backend')
expect(onNavigate).toHaveBeenLastCalledWith(backend)
await click('default-service')
expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Service', group: '', namespace: 'app', name: 'default-service' })
await click('mesh-parent')
expect(onNavigate).toHaveBeenLastCalledWith(parent)
if (kind === 'HTTPRoute') {
await click('mirror')
expect(onNavigate).toHaveBeenLastCalledWith({ ...backend, name: 'mirror' })
}
} finally {
await act(async () => root.unmount())
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
import { Badge } from '../../ui/Badge'
import type { ResourceRef } from '../../../types'
import { gatewayBackendResourceRef, gatewayParentResourceRef } from '../../../utils/gateway-references'

interface HTTPRouteRendererProps {
data: any
Expand Down Expand Up @@ -32,22 +33,8 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
? parentStatuses[0].conditions
: undefined

function toGatewayRef(ref: any): ResourceRef {
return {
kind: 'Gateway',
namespace: ref.namespace || routeNs,
name: ref.name,
group: 'gateway.networking.k8s.io',
}
}

function toServiceRef(backend: any): ResourceRef {
return {
kind: 'Service',
namespace: backend.namespace || routeNs,
name: backend.name,
}
}
const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)

return (
<>
Expand All @@ -59,8 +46,8 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
const gwName = p.parentRef?.name || 'unknown'
return cond?.reason
? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
: `Gateway "${gwName}" has not accepted this route.`
? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
: `Parent "${gwName}" has not accepted this route.`
}).join('; ')}
/>
)}
Expand Down Expand Up @@ -90,12 +77,12 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
}
/>
<Property
label="Parent Gateways"
label="Parents"
value={
parentRefs.length > 0 ? (
<div className="flex flex-wrap gap-1">
{parentRefs.map((ref: any, i: number) => (
<ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
<ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
))}
</div>
) : 'None'
Expand Down Expand Up @@ -129,7 +116,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
<div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
<span className="italic text-theme-text-tertiary">all</span>
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
{backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
</div>
)}
Expand All @@ -141,7 +128,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
return (
<div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
{b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
{pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
</div>
Expand Down Expand Up @@ -176,7 +163,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
{matchIdx === 0 && backendRefs.length === 1 && (
<>
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
{backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
</>
)}
Expand All @@ -189,7 +176,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
return (
<div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
<ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
<ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
{b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
{pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
</div>
Expand Down Expand Up @@ -234,7 +221,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
)}
{filter.type === 'RequestMirror' && filter.requestMirror && (
<span className="text-theme-text-tertiary">
{filter.requestMirror.backendRef?.name || 'unknown'}
{filter.requestMirror.backendRef && <ResourceRefBadge resourceRef={toBackendRef(filter.requestMirror.backendRef)} onClick={onNavigate} />}
{filter.requestMirror.backendRef?.port ? `:${filter.requestMirror.backendRef.port}` : ''}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Globe, ArrowRight, Network } from 'lucide-react'
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
import { Badge } from '../../ui/Badge'
import type { ResourceRef } from '../../../types'
import { gatewayBackendResourceRef, gatewayParentResourceRef } from '../../../utils/gateway-references'

interface SimpleRouteRendererProps {
data: any
Expand Down Expand Up @@ -35,22 +36,8 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
? parentStatuses[0].conditions
: undefined

function toGatewayRef(ref: any): ResourceRef {
return {
kind: 'Gateway',
namespace: ref.namespace || routeNs,
name: ref.name,
group: 'gateway.networking.k8s.io',
}
}

function toServiceRef(backend: any): ResourceRef {
return {
kind: 'Service',
namespace: backend.namespace || routeNs,
name: backend.name,
}
}
const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)

return (
<>
Expand All @@ -62,8 +49,8 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
const gwName = p.parentRef?.name || 'unknown'
return cond?.reason
? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
: `Gateway "${gwName}" has not accepted this route.`
? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
: `Parent "${gwName}" has not accepted this route.`
}).join('; ')}
/>
)}
Expand Down Expand Up @@ -95,12 +82,12 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
/>
)}
<Property
label="Parent Gateways"
label="Parents"
value={
parentRefs.length > 0 ? (
<div className="flex flex-wrap gap-1">
{parentRefs.map((ref: any, i: number) => (
<ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
<ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
))}
</div>
) : 'None'
Expand Down Expand Up @@ -131,7 +118,7 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
const pct = hasWeights ? Math.round(((b.weight ?? 1) / totalWeight) * 100) : null
return (
<span key={bi} className="flex items-center gap-1">
<ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
<ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
{b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
{pct !== null && (
<span className="text-theme-text-tertiary text-[10px]">{pct}%</span>
Expand Down
17 changes: 17 additions & 0 deletions packages/k8s-ui/src/utils/gateway-references.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { gatewayBackendResourceRef, gatewayParentResourceRef } from './gateway-references'

describe('Gateway API references', () => {
it('applies the distinct backend and parent defaults', () => {
expect(gatewayBackendResourceRef({ name: 'api' }, 'app')).toEqual({ kind: 'Service', group: '', namespace: 'app', name: 'api' })
expect(gatewayParentResourceRef({ name: 'edge' }, 'app')).toEqual({ kind: 'Gateway', group: 'gateway.networking.k8s.io', namespace: 'app', name: 'edge' })
})

it('preserves custom kinds, exact groups and explicit namespaces', () => {
expect(gatewayBackendResourceRef({ name: 'api', kind: 'Widget', group: 'custom.example.io', namespace: 'backends' }, 'app'))
.toEqual({ kind: 'Widget', group: 'custom.example.io', namespace: 'backends', name: 'api' })
expect(gatewayBackendResourceRef({ name: 'api', kind: 'Service', group: 'serving.knative.dev' }, 'app').group).toBe('serving.knative.dev')
expect(gatewayParentResourceRef({ name: 'mesh', kind: 'Service', group: '', namespace: 'front' }, 'app'))
.toEqual({ kind: 'Service', group: '', namespace: 'front', name: 'mesh' })
})
})
Loading
Loading