@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.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +82 -6
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +180 -64
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +29 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- 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 {
|
|
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={`${
|
|
77
|
+
name={`${kindToPluralWithGroup(xrdKind, xrdGroup)}.${xrdGroup}`}
|
|
78
78
|
kind="compositeresourcedefinitions"
|
|
79
79
|
namespace=""
|
|
80
80
|
onNavigate={onNavigate}
|
|
81
|
-
label={<span className="font-mono">{
|
|
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
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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(
|
|
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
|
|
89
|
-
const
|
|
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
|
-
|
|
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: {
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
53
|
-
<ResourceLink
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
service ? (
|
|
34
33
|
<ResourceLink
|
|
35
|
-
name={
|
|
34
|
+
name={service.name}
|
|
36
35
|
kind="services"
|
|
37
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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`} />
|