@skyhook-io/k8s-ui 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. package/src/utils/view-transition.ts +16 -1
@@ -4,7 +4,7 @@ import { Layers, GitBranch, FileText, Boxes, ScrollText } from 'lucide-react'
4
4
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
6
6
  import { CodeViewer } from '../../ui/CodeViewer'
7
- import { kindToPlural } from '../../../utils/navigation'
7
+ import { kindToPluralWithGroup } from '../../../utils/navigation'
8
8
 
9
9
  interface CompositionRendererProps {
10
10
  data: any
@@ -74,11 +74,11 @@ function CompositionBody({ data, onNavigate, revision }: CompositionRendererProp
74
74
  <div className="mt-2 text-xs text-theme-text-secondary">
75
75
  Backed by XRD:{' '}
76
76
  <ResourceLink
77
- name={`${kindToPlural(xrdKind)}.${xrdGroup}`}
77
+ name={`${kindToPluralWithGroup(xrdKind, xrdGroup)}.${xrdGroup}`}
78
78
  kind="compositeresourcedefinitions"
79
79
  namespace=""
80
80
  onNavigate={onNavigate}
81
- label={<span className="font-mono">{kindToPlural(xrdKind)}.{xrdGroup}</span>}
81
+ label={<span className="font-mono">{kindToPluralWithGroup(xrdKind, xrdGroup)}.{xrdGroup}</span>}
82
82
  />
83
83
  </div>
84
84
  )}
@@ -0,0 +1,34 @@
1
+ // @vitest-environment jsdom
2
+ import { act, type ReactNode } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { ContourHTTPProxyRenderer } from './ContourHTTPProxyRenderer'
6
+
7
+ async function rendered(element: ReactNode, check: (container: HTMLDivElement) => Promise<void> | void) {
8
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
9
+ const container = document.createElement('div')
10
+ const root = createRoot(container)
11
+ try { await act(async () => root.render(element)); await check(container) }
12
+ finally { await act(async () => root.unmount()); Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false }) }
13
+ }
14
+ async function click(container: HTMLDivElement, text: string) {
15
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === text)
16
+ expect(button, `missing link ${text}`).toBeDefined()
17
+ await act(async () => button!.click())
18
+ }
19
+
20
+ describe('exact reference navigation', () => {
21
+ it.each([['tls/', 'tls'], ['/', 'app']])('resolves Contour TLS secret prefix %s to namespace %s',async(prefix, namespace)=>{
22
+ const navigate=vi.fn()
23
+ await rendered(<ContourHTTPProxyRenderer data={{metadata:{namespace:'app'},spec:{virtualhost:{fqdn:'example.test',tls:{secretName:`${prefix}server`,clientValidation:{caSecret:`${prefix}ca`,crlSecret:`${prefix}crl`}}}}}} onNavigate={navigate}/>,async container=>{
24
+ for(const name of ['server','ca','crl']) {await click(container,`${prefix}${name}`);expect(navigate).toHaveBeenLastCalledWith({kind:'secrets',group:'',namespace,name})}
25
+ })
26
+ })
27
+ it('links valid long Contour Secret names',async()=>{
28
+ const navigate=vi.fn()
29
+ const name='a'.repeat(70)
30
+ await rendered(<ContourHTTPProxyRenderer data={{metadata:{namespace:'app'},spec:{virtualhost:{tls:{secretName:name}}}}} onNavigate={navigate}/>,async container=>{
31
+ await click(container,name);expect(navigate).toHaveBeenCalledWith({kind:'secrets',group:'',namespace:'app',name})
32
+ })
33
+ })
34
+ })
@@ -1,6 +1,7 @@
1
1
  import { Globe, Route, Layers, Lock } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
+ getContourSecretReference,
4
5
  getHTTPProxyFQDN,
5
6
  getHTTPProxyRouteCount,
6
7
  getHTTPProxyServiceCount,
@@ -174,16 +175,16 @@ export function ContourHTTPProxyRenderer({ data, onNavigate }: ContourHTTPProxyR
174
175
  {tls && (
175
176
  <Section title="TLS" icon={Lock} defaultExpanded>
176
177
  <PropertyList>
177
- {tls.secretName && (
178
- <Property label="Secret" value={
179
- <ResourceLink
180
- name={tls.secretName}
181
- kind="secrets"
182
- namespace={ns}
183
- onNavigate={onNavigate}
184
- />
185
- } />
186
- )}
178
+ {[
179
+ { label: 'Secret', name: tls.secretName },
180
+ { label: 'Client CA Secret', name: tls.clientValidation?.caSecret },
181
+ { label: 'Client CRL Secret', name: tls.clientValidation?.crlSecret },
182
+ ].filter(item => item.name).map(item => {
183
+ const ref = getContourSecretReference(item.name, ns)
184
+ return <Property key={item.label} label={item.label} value={ref
185
+ ? <ResourceLink {...ref} kind="secrets" group="" label={item.name} onNavigate={onNavigate} />
186
+ : item.name} />
187
+ })}
187
188
  {tls.minimumProtocolVersion && (
188
189
  <Property label="Min Protocol" value={tls.minimumProtocolVersion} />
189
190
  )}
@@ -1,8 +1,9 @@
1
1
  // @vitest-environment jsdom
2
2
  import { act } from 'react'
3
3
  import { createRoot } from 'react-dom/client'
4
- import { expect, it, vi } from 'vitest'
4
+ import { describe, expect, it, vi } from 'vitest'
5
5
  import { EndpointSliceRenderer } from './EndpointSliceRenderer'
6
+ import { initNavigationMap, resetNavigationMap } from '../../../utils/navigation'
6
7
 
7
8
  vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
8
9
 
@@ -22,3 +23,40 @@ it('navigates to the labeled Service as a core-group resource', async () => {
22
23
  expect(onNavigate).toHaveBeenCalledWith({ kind: 'Service', group: '', namespace: 'team', name: 'web' })
23
24
  await act(async () => root.unmount())
24
25
  })
26
+
27
+ describe('EndpointSliceRenderer target navigation', () => {
28
+ it('navigates exact target identity, controller-written Pod targets and Node placement', async () => {
29
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
30
+ initNavigationMap([{ group: 'custom.example.io', version: 'v1', kind: 'Node', name: 'nodes', namespaced: false, isCrd: true, verbs: ['get'] }])
31
+ const container = document.createElement('div')
32
+ const root = createRoot(container)
33
+ const onNavigate = vi.fn()
34
+ const click = async (label: string) => {
35
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === label)
36
+ expect(button).toBeDefined()
37
+ await act(async () => button!.click())
38
+ }
39
+ try {
40
+ await act(async () => root.render(<EndpointSliceRenderer onNavigate={onNavigate} data={{ metadata: { namespace: 'slice-team' }, endpoints: [
41
+ { addresses: ['10.0.0.1'], nodeName: 'worker', targetRef: { apiVersion: 'custom.example.io/v1', kind: 'Node', namespace: 'irrelevant', name: 'custom-target' } },
42
+ { addresses: ['10.0.0.2'], targetRef: { apiVersion: 'v1', kind: 'Pod', namespace: 'target-team', name: 'backend' } },
43
+ { addresses: ['10.0.0.3'], targetRef: { kind: 'Pod', namespace: 'slice-team', name: 'controller-written', uid: 'pod-uid' } },
44
+ { addresses: ['10.0.0.4'], targetRef: { kind: 'Widget', namespace: 'slice-team', name: 'unknown-api' } },
45
+ ] }} />))
46
+ await click('Node custom-target')
47
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Node', group: 'custom.example.io', namespace: '', name: 'custom-target' })
48
+ await click('Pod target-team/backend')
49
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Pod', group: '', namespace: 'target-team', name: 'backend' })
50
+ await click('Pod slice-team/controller-written')
51
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Pod', group: '', namespace: 'slice-team', name: 'controller-written' })
52
+ await click('worker')
53
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Node', group: '', namespace: '', name: 'worker' })
54
+ expect([...container.querySelectorAll('button')].some(button => button.textContent?.includes('unknown-api'))).toBe(false)
55
+ expect(container.textContent).toContain('unknown-api')
56
+ } finally {
57
+ await act(async () => root.unmount())
58
+ resetNavigationMap()
59
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
60
+ }
61
+ })
62
+ })
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
4
4
  import { Badge } from '../../ui/Badge'
5
5
  import type { ResourceRef } from '../../../types'
6
+ import { objectReferenceToResourceRef } from '../../../utils/navigation'
6
7
 
7
8
  interface EndpointSliceRendererProps {
8
9
  data: any
@@ -17,8 +18,7 @@ function endpointAddressCount(endpoints: any[]): number {
17
18
  return endpoints.reduce((total, endpoint) => total + (endpoint.addresses?.length || 0), 0)
18
19
  }
19
20
 
20
- function endpointTargetLabel(endpoint: any): string | null {
21
- const target = endpoint.targetRef
21
+ function endpointTargetLabel(target: any): string | null {
22
22
  if (!target?.kind || !target?.name) return null
23
23
  return target.namespace ? `${target.kind} ${target.namespace}/${target.name}` : `${target.kind} ${target.name}`
24
24
  }
@@ -85,8 +85,8 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
85
85
  <div className="space-y-2">
86
86
  {endpoints.map((endpoint: any, index: number) => {
87
87
  const ready = isEndpointReady(endpoint)
88
- const targetLabel = endpointTargetLabel(endpoint)
89
- const target = endpoint.targetRef
88
+ const targetRef = objectReferenceToResourceRef(endpoint.targetRef)
89
+ const targetLabel = endpointTargetLabel(targetRef || endpoint.targetRef)
90
90
  return (
91
91
  <div key={`${endpoint.addresses?.join(',') || 'endpoint'}-${index}`} className="card-inner text-sm space-y-3">
92
92
  <div className="flex items-start justify-between gap-3">
@@ -98,20 +98,15 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
98
98
  </Badge>
99
99
  ))}
100
100
  </div>
101
- {targetLabel && (
101
+ {targetLabel && (onNavigate && targetRef ? (
102
102
  <button
103
103
  type="button"
104
104
  className="text-xs text-theme-text-secondary hover:text-accent-text"
105
- disabled={!onNavigate}
106
- onClick={() => onNavigate?.({
107
- kind: target.kind,
108
- namespace: target.namespace || metadata.namespace,
109
- name: target.name,
110
- })}
105
+ onClick={() => onNavigate(targetRef)}
111
106
  >
112
107
  {targetLabel}
113
108
  </button>
114
- )}
109
+ ) : <span className="text-xs text-theme-text-secondary">{targetLabel}</span>)}
115
110
  </div>
116
111
  <div className="flex flex-wrap justify-end gap-1.5 shrink-0">
117
112
  <span className={clsx('badge-sm', ready ? 'status-healthy' : 'status-unhealthy')}>
@@ -127,7 +122,13 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
127
122
  </div>
128
123
  {(endpoint.nodeName || endpoint.zone) && (
129
124
  <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-theme-text-tertiary">
130
- {endpoint.nodeName && <span>Node: {endpoint.nodeName}</span>}
125
+ {endpoint.nodeName && <span>Node: {onNavigate ? (
126
+ <button
127
+ type="button"
128
+ className="hover:text-accent-text hover:underline"
129
+ onClick={() => onNavigate({ kind: 'Node', group: '', namespace: '', name: endpoint.nodeName })}
130
+ >{endpoint.nodeName}</button>
131
+ ) : endpoint.nodeName}</span>}
131
132
  {endpoint.zone && <span>Zone: {endpoint.zone}</span>}
132
133
  </div>
133
134
  )}
@@ -1,16 +1,17 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import { Activity, Cpu } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { Section, PropertyList, Property, ConditionsSection, ResourceLink, type ConditionTone } from '../../ui/drawer-components'
4
+ import { Section, PropertyList, Property, ConditionsSection, ResourceLink, LabelSelectorDisplay, type ConditionTone } from '../../ui/drawer-components'
5
5
  import { Badge } from '../../ui/Badge'
6
- import { kindToPlural } from '../../../utils/navigation'
6
+ import { objectReferenceToResourceRef, apiVersionToGroup } from '../../../utils/navigation'
7
+ import type { ResourceRef } from '../../../types'
8
+ import { TimeValue } from '../../ui/ScheduleValue'
7
9
  import { HPADiagnosisSummary } from '../HPADiagnosisSummary'
8
10
  import type { HPADiagnosis } from '../../../types'
9
- import { TimeValue } from '../../ui/ScheduleValue'
10
11
 
11
12
  interface HPARendererProps {
12
13
  data: any
13
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
14
+ onNavigate?: (ref: ResourceRef) => void
14
15
  hpaDiagnosis?: HPADiagnosis
15
16
  /** Optional host-provided section rendered after Conditions — used to inject Prometheus-backed charts. */
16
17
  extraSections?: ReactNode
@@ -37,6 +38,11 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
37
38
  const status = data.status || {}
38
39
  const spec = data.spec || {}
39
40
  const metrics = status.currentMetrics || []
41
+ const namespace = data.metadata?.namespace
42
+ const targetRef = spec.scaleTargetRef || {}
43
+ const target = objectReferenceToResourceRef({ ...targetRef, namespace })
44
+ const hasDiagnosisMetrics = !!hpaDiagnosis?.metrics?.length
45
+ const objectMetrics = (spec.metrics || []).filter((metric: any) => metric.type === 'Object' && metric.object)
40
46
 
41
47
  return (
42
48
  <>
@@ -49,15 +55,9 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
49
55
  <Section title="Scaling" icon={Cpu}>
50
56
  <PropertyList>
51
57
  <Property label="Target" value={
52
- spec.scaleTargetRef?.name ? (
53
- <ResourceLink
54
- name={spec.scaleTargetRef.name}
55
- kind={kindToPlural(spec.scaleTargetRef.kind || 'Deployment')}
56
- namespace={data.metadata?.namespace || ''}
57
- label={`${spec.scaleTargetRef.kind}/${spec.scaleTargetRef.name}`}
58
- onNavigate={onNavigate}
59
- />
60
- ) : undefined
58
+ targetRef.name ? (target ? (
59
+ <ResourceLink {...target} label={`${targetRef.kind}/${targetRef.name}`} onNavigate={onNavigate} />
60
+ ) : `${targetRef.kind || 'Unknown kind'}/${targetRef.name}`) : undefined
61
61
  } />
62
62
  <Property label="Current" value={status.currentReplicas} />
63
63
  <Property label="Desired" value={status.desiredReplicas} />
@@ -70,11 +70,11 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
70
70
  {hpaDiagnosis?.metrics && hpaDiagnosis.metrics.length > 0 && (
71
71
  <Section title="Metrics" defaultExpanded>
72
72
  <div className="space-y-2">
73
- {hpaDiagnosis.metrics.map((metric) => (
74
- <div key={`${metric.type}-${metric.name}`} className="card-inner">
73
+ {hpaDiagnosis.metrics.map((metric, index) => (
74
+ <div key={`${metric.type}-${metric.name}-${index}`} className="card-inner">
75
75
  <div className="flex flex-wrap items-center justify-between gap-2 text-sm">
76
76
  <div>
77
- <div className="font-medium text-theme-text-primary">{metric.name}</div>
77
+ <div className="font-medium text-theme-text-primary break-words">{spec.metrics?.[index]?.type === 'Object' ? spec.metrics[index].object.metric.name : metric.name}</div>
78
78
  <div className="text-xs text-theme-text-tertiary">{metric.type}</div>
79
79
  </div>
80
80
  <div className="flex flex-wrap items-center gap-2 text-xs text-theme-text-secondary">
@@ -85,6 +85,11 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
85
85
  </Badge>
86
86
  </div>
87
87
  </div>
88
+ {spec.metrics?.[index]?.type === 'Object' && (
89
+ <div className="mt-2">
90
+ <HPAObjectMetricSource object={spec.metrics[index].object} namespace={namespace} onNavigate={onNavigate} />
91
+ </div>
92
+ )}
88
93
  </div>
89
94
  ))}
90
95
  </div>
@@ -121,9 +126,44 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
121
126
  </Section>
122
127
  )}
123
128
 
129
+ {!hasDiagnosisMetrics && objectMetrics.length > 0 && (
130
+ <Section title="Object metric sources" defaultExpanded>
131
+ <div className="space-y-3">
132
+ {objectMetrics.map((metric: any, i: number) => (
133
+ <div key={`${metric.object.metric.name}-${i}`} className="card-inner">
134
+ <HPAObjectMetricSource object={metric.object} namespace={namespace} onNavigate={onNavigate} showMetric />
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </Section>
139
+ )}
140
+
124
141
  <ConditionsSection conditions={status.conditions} getConditionTone={hpaConditionTone} />
125
142
 
126
143
  {extraSections}
127
144
  </>
128
145
  )
129
146
  }
147
+
148
+ function HPAObjectMetricSource({ object, namespace, onNavigate, showMetric = false }: {
149
+ object: any
150
+ namespace?: string
151
+ onNavigate?: (ref: ResourceRef) => void
152
+ showMetric?: boolean
153
+ }) {
154
+ const described = object.describedObject || {}
155
+ const namespaceMetric = described.kind === 'Namespace' && apiVersionToGroup(described.apiVersion) === ''
156
+ const name = namespaceMetric ? namespace : described.name
157
+ const source = objectReferenceToResourceRef({ ...described, name, namespace })
158
+ return (
159
+ <PropertyList>
160
+ {showMetric && <Property label="Metric" value={object.metric.name} />}
161
+ <Property label="Object" value={source ? (
162
+ <ResourceLink {...source} label={`${described.kind}/${name}`} onNavigate={onNavigate} />
163
+ ) : described.name ? `${described.kind || 'Unknown kind'}/${described.name}` : undefined} />
164
+ {source?.group && <Property label="API Group" value={source.group} />}
165
+ {object.metric.selector && <Property label="Selector" value={<LabelSelectorDisplay selector={object.metric.selector} />} />}
166
+ {namespaceMetric && <Property label="Scope" value="This HPA's namespace" />}
167
+ </PropertyList>
168
+ )
169
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { IngressRenderer } from './IngressRenderer'
4
+
5
+ describe('IngressRenderer default backend', () => {
6
+ it('shows the default backend and no "not routed" warning', () => {
7
+ const html = renderToString(
8
+ <IngressRenderer data={{
9
+ metadata: { name: 'catch-all', namespace: 'team' },
10
+ spec: { ingressClassName: 'nginx', defaultBackend: { service: { name: 'fallback', port: { number: 80 } } } },
11
+ }} />,
12
+ )
13
+ expect(html).toContain('Default Backend')
14
+ expect(html.replace(/<!-- -->/g, '')).toContain('fallback:80')
15
+ expect(html).not.toContain('Traffic will not be routed')
16
+ })
17
+
18
+ it('still warns when an Ingress routes nowhere', () => {
19
+ const html = renderToString(<IngressRenderer data={{ metadata: { name: 'empty', namespace: 'team' }, spec: { ingressClassName: 'nginx' } }} />)
20
+ expect(html).toContain('Traffic will not be routed')
21
+ })
22
+
23
+ it('shows a resource default backend', () => {
24
+ const html = renderToString(
25
+ <IngressRenderer data={{
26
+ metadata: { name: 'static', namespace: 'team' },
27
+ spec: { ingressClassName: 'nginx', defaultBackend: { resource: { apiGroup: 'k8s.example.com', kind: 'StorageBucket', name: 'static-assets' } } },
28
+ }} />,
29
+ )
30
+ expect(html).toContain('Default Backend')
31
+ expect(html).toContain('StorageBucket/static-assets')
32
+ })
33
+ })
@@ -1,6 +1,7 @@
1
1
  import { Globe, Shield, Clock } from 'lucide-react'
2
2
  import type { ResourceRef } from '../../../types'
3
3
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPluralWithGroup } from '../../../utils/navigation'
4
5
 
5
6
  interface IngressRendererProps {
6
7
  data: any
@@ -16,7 +17,10 @@ export function IngressRenderer({ data, onNavigate }: IngressRendererProps) {
16
17
  // Check for issues
17
18
  const hasNoAddress = lbIngress.length === 0
18
19
  const hasNoClass = !spec.ingressClassName && !data.metadata?.annotations?.['kubernetes.io/ingress.class']
19
- const hasNoRules = rules.length === 0
20
+ const defaultService = spec.defaultBackend?.service
21
+ const defaultResource = spec.defaultBackend?.resource
22
+ // An Ingress with only a default backend still routes everything to it.
23
+ const hasNoRules = rules.length === 0 && !spec.defaultBackend
20
24
 
21
25
  return (
22
26
  <>
@@ -50,11 +54,34 @@ export function IngressRenderer({ data, onNavigate }: IngressRendererProps) {
50
54
  {lbIngress.length > 0 && (
51
55
  <Property label="Address" value={lbIngress[0].ip || lbIngress[0].hostname} />
52
56
  )}
57
+ {defaultService?.name && (
58
+ <Property label="Default Backend" value={
59
+ <ResourceLink
60
+ name={defaultService.name}
61
+ kind="services"
62
+ namespace={data.metadata?.namespace || ''}
63
+ label={<span className="text-blue-400">{defaultService.name}:{defaultService.port?.number || defaultService.port?.name}</span>}
64
+ onNavigate={onNavigate}
65
+ />
66
+ } />
67
+ )}
68
+ {defaultResource?.name && (
69
+ <Property label="Default Backend" value={
70
+ <ResourceLink
71
+ name={defaultResource.name}
72
+ kind={kindToPluralWithGroup(defaultResource.kind, defaultResource.apiGroup || '')}
73
+ group={defaultResource.apiGroup || ''}
74
+ namespace={data.metadata?.namespace || ''}
75
+ label={<span className="text-blue-400">{`${defaultResource.kind}/${defaultResource.name}`}</span>}
76
+ onNavigate={onNavigate}
77
+ />
78
+ } />
79
+ )}
53
80
  <Property label="TLS" value={tls.length > 0 ? `${tls.length} certificate(s)` : 'None'} />
54
81
  </PropertyList>
55
82
  </Section>
56
83
 
57
- <Section title="Rules" defaultExpanded>
84
+ {rules.length > 0 && <Section title="Rules" defaultExpanded>
58
85
  <div className="space-y-3">
59
86
  {rules.map((rule: any, i: number) => (
60
87
  <div key={i} className="card-inner-lg">
@@ -87,7 +114,7 @@ export function IngressRenderer({ data, onNavigate }: IngressRendererProps) {
87
114
  </div>
88
115
  ))}
89
116
  </div>
90
- </Section>
117
+ </Section>}
91
118
 
92
119
  {tls.length > 0 && (
93
120
  <Section title="TLS" icon={Shield}>
@@ -2,6 +2,7 @@ import { Network } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, ResourceLink, KeyValueBadgeList } from '../../ui/drawer-components'
3
3
  import {
4
4
  getDestinationRuleHost,
5
+ resolveIstioServiceHost,
5
6
  getDestinationRuleSubsets,
6
7
  getDestinationRuleTrafficPolicy,
7
8
  getDestinationRuleLoadBalancer,
@@ -9,20 +10,18 @@ import {
9
10
 
10
11
  interface IstioDestinationRuleRendererProps {
11
12
  data: any
13
+ serviceDomain?: string
12
14
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
13
15
  }
14
16
 
15
- export function IstioDestinationRuleRenderer({ data, onNavigate }: IstioDestinationRuleRendererProps) {
17
+ export function IstioDestinationRuleRenderer({ data, onNavigate, serviceDomain }: IstioDestinationRuleRendererProps) {
16
18
  const host = getDestinationRuleHost(data)
17
19
  const subsets = getDestinationRuleSubsets(data)
18
20
  const trafficPolicy = getDestinationRuleTrafficPolicy(data)
19
21
  const loadBalancer = getDestinationRuleLoadBalancer(data)
20
22
  const ns = data.metadata?.namespace || ''
21
23
 
22
- // Parse host to create service link
23
- const hostParts = host.split('.')
24
- const svcName = hostParts[0]
25
- const svcNs = hostParts.length >= 2 ? hostParts[1] : ns
24
+ const service = resolveIstioServiceHost(host, ns, serviceDomain)
26
25
 
27
26
  return (
28
27
  <>
@@ -30,15 +29,16 @@ export function IstioDestinationRuleRenderer({ data, onNavigate }: IstioDestinat
30
29
  <Section title="Destination" icon={Network} defaultExpanded>
31
30
  <PropertyList>
32
31
  <Property label="Host" value={
33
- host !== '-' ? (
32
+ service ? (
34
33
  <ResourceLink
35
- name={svcName}
34
+ name={service.name}
36
35
  kind="services"
37
- namespace={svcNs}
36
+ group=""
37
+ namespace={service.namespace}
38
38
  label={host}
39
39
  onNavigate={onNavigate}
40
40
  />
41
- ) : '-'
41
+ ) : host
42
42
  } />
43
43
  <Property label="Load Balancer" value={loadBalancer} />
44
44
  </PropertyList>
@@ -0,0 +1,34 @@
1
+ // @vitest-environment jsdom
2
+ import { act, type ReactNode } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { IstioDestinationRuleRenderer } from './IstioDestinationRuleRenderer'
6
+
7
+ async function rendered(element: ReactNode, check: (container: HTMLDivElement) => Promise<void> | void) {
8
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
9
+ const container = document.createElement('div')
10
+ const root = createRoot(container)
11
+ try { await act(async () => root.render(element)); await check(container) }
12
+ finally { await act(async () => root.unmount()); Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false }) }
13
+ }
14
+ async function click(container: HTMLDivElement, text: string) {
15
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent === text)
16
+ expect(button, `missing link ${text}`).toBeDefined()
17
+ await act(async () => button!.click())
18
+ }
19
+
20
+ describe('exact reference navigation', () => {
21
+ it.each([
22
+ {host:'api.prod.svc.mesh.internal',domain:'mesh.internal',linked:true},
23
+ {host:'api.prod.svc.cluster.local',domain:undefined,linked:false},
24
+ {host:'api.prod.svc.external.test',domain:'mesh.internal',linked:false},
25
+ {host:'api.prod',domain:'mesh.internal',linked:false},
26
+ ])('links only observed Istio service domains: $host',async ({host,domain,linked})=>{
27
+ const navigate=vi.fn()
28
+ await rendered(<IstioDestinationRuleRenderer data={{metadata:{namespace:'app'},spec:{host}}} serviceDomain={domain} onNavigate={navigate}/>,async container=>{
29
+ const link=[...container.querySelectorAll('button')].find(button=>button.textContent===host)
30
+ expect(Boolean(link)).toBe(linked)
31
+ if(link) {await click(container,host);expect(navigate).toHaveBeenCalledWith({kind:'services',group:'',namespace:'prod',name:'api'})}
32
+ })
33
+ })
34
+ })
@@ -7,6 +7,7 @@ import {
7
7
  getVirtualServiceHostsList,
8
8
  getVirtualServiceGatewaysList,
9
9
  getVirtualServiceHttpRoutes,
10
+ resolveIstioServiceHost,
10
11
  getVirtualServiceTcpRoutes,
11
12
  getVirtualServiceTlsRoutes,
12
13
  getVirtualServiceRouteCount,
@@ -14,10 +15,11 @@ import {
14
15
 
15
16
  interface IstioVirtualServiceRendererProps {
16
17
  data: any
18
+ serviceDomain?: string
17
19
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
18
20
  }
19
21
 
20
- export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualServiceRendererProps) {
22
+ export function IstioVirtualServiceRenderer({ data, onNavigate, serviceDomain }: IstioVirtualServiceRendererProps) {
21
23
  const status = getVirtualServiceStatus(data)
22
24
  const hosts = getVirtualServiceHostsList(data)
23
25
  const gateways = getVirtualServiceGatewaysList(data)
@@ -148,6 +150,7 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
148
150
  {route.route.map((dest: any, di: number) => {
149
151
  const weight = dest.weight ?? (route.route.length === 1 ? 100 : undefined)
150
152
  const destHost = dest.destination?.host || ''
153
+ const service = resolveIstioServiceHost(destHost, ns, serviceDomain)
151
154
  const destPort = dest.destination?.port?.number
152
155
  const destSubset = dest.destination?.subset
153
156
 
@@ -164,13 +167,7 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
164
167
  <span className="text-theme-text-secondary font-medium">{weight}%</span>
165
168
  </div>
166
169
  )}
167
- <ResourceLink
168
- name={destHost.split('.')[0]}
169
- kind="services"
170
- namespace={destHost.includes('.') ? destHost.split('.')[1] : ns}
171
- label={destHost}
172
- onNavigate={onNavigate}
173
- />
170
+ {service ? <ResourceLink name={service.name} kind="services" group="" namespace={service.namespace} label={destHost} onNavigate={onNavigate} /> : <span className="text-theme-text-primary break-all">{destHost}</span>}
174
171
  {destPort && (
175
172
  <span className="text-theme-text-tertiary">:{destPort}</span>
176
173
  )}
@@ -1,6 +1,7 @@
1
1
  import { Cpu, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
- import { kindToPlural } from '../../../utils/navigation'
3
+ import { objectReferenceToResourceRef } from '../../../utils/navigation'
4
+ import type { ResourceRef } from '../../../types'
4
5
  import { TimeValue } from '../../ui/ScheduleValue'
5
6
  import {
6
7
  getScaledObjectStatus,
@@ -24,7 +25,7 @@ function summarizeScalingPolicies(policies: any[] | undefined, periodSeconds?: n
24
25
 
25
26
  interface KedaScaledObjectRendererProps {
26
27
  data: any
27
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
28
+ onNavigate?: (ref: ResourceRef) => void
28
29
  }
29
30
 
30
31
  export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectRendererProps) {
@@ -75,18 +76,19 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
75
76
  <Property label="Target" value={(() => {
76
77
  const target = data.spec?.scaleTargetRef
77
78
  if (target?.name) {
78
- return (
79
- <ResourceLink
80
- name={target.name}
81
- kind={kindToPlural(target.kind || 'Deployment')}
82
- namespace={data.metadata?.namespace || ''}
83
- label={getScaledObjectTarget(data)}
84
- onNavigate={onNavigate}
85
- />
86
- )
79
+ // KEDA documents these defaults for omitted fields; an explicit
80
+ // target API group still belongs to the declared custom resource.
81
+ const ref = objectReferenceToResourceRef({
82
+ ...target,
83
+ apiVersion: target.apiVersion || 'apps/v1',
84
+ kind: target.kind || 'Deployment',
85
+ namespace: data.metadata?.namespace,
86
+ })
87
+ return ref ? <ResourceLink {...ref} label={getScaledObjectTarget(data)} onNavigate={onNavigate} /> : getScaledObjectTarget(data)
87
88
  }
88
89
  return getScaledObjectTarget(data)
89
90
  })()} />
91
+ <Property label="Target API" value={data.spec?.scaleTargetRef?.apiVersion || 'apps/v1'} />
90
92
  <Property label="Replicas" value={getScaledObjectReplicas(data)} />
91
93
  <Property label="Polling Interval" value={`${getScaledObjectPollingInterval(data)}s`} />
92
94
  <Property label="Cooldown Period" value={`${getScaledObjectCooldownPeriod(data)}s`} />