@skyhook-io/k8s-ui 1.17.0 → 1.18.0
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 +6 -6
- 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 +7 -3
- 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 +142 -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 +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- 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 +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -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 +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- 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/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -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 +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- 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/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- 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.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- 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 +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -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/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- 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/Toast.tsx +15 -4
- 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 +4 -2
- 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 +193 -71
- 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 +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { PingSourceRenderer } from './KnativeSourceRenderer'
|
|
6
|
+
import { DomainMappingRenderer } from './KnativeNetworkingRenderer'
|
|
7
|
+
import { initNavigationMap, resetNavigationMap } from '../../../utils/navigation'
|
|
8
|
+
|
|
9
|
+
describe('Knative reference links', () => {
|
|
10
|
+
it('keep the sink reference API group instead of resolving to the core Kind', async () => {
|
|
11
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
12
|
+
initNavigationMap([
|
|
13
|
+
{ group: '', version: 'v1', kind: 'Service', name: 'services', namespaced: true, isCrd: false, verbs: ['get'] },
|
|
14
|
+
{ group: 'serving.knative.dev', version: 'v1', kind: 'Service', name: 'services', namespaced: true, isCrd: true, verbs: ['get'] },
|
|
15
|
+
{ group: 'relationships.radar.test', version: 'v1', kind: 'Service', name: 'virtualservices', namespaced: true, isCrd: true, verbs: ['get'] },
|
|
16
|
+
])
|
|
17
|
+
const container = document.createElement('div')
|
|
18
|
+
const root = createRoot(container)
|
|
19
|
+
const onNavigate = vi.fn()
|
|
20
|
+
const clickSink = async (sinkRef: Record<string, string>) => {
|
|
21
|
+
await act(async () => root.render(<PingSourceRenderer onNavigate={onNavigate} data={{
|
|
22
|
+
metadata: { namespace: 'team', name: 'ping' },
|
|
23
|
+
spec: { schedule: '* * * * *', sink: { ref: sinkRef } },
|
|
24
|
+
}} />))
|
|
25
|
+
const link = [...container.querySelectorAll('button')].find(button => button.textContent === sinkRef.name)
|
|
26
|
+
expect(link).toBeDefined()
|
|
27
|
+
await act(async () => link!.click())
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await clickSink({ apiVersion: 'serving.knative.dev/v1', kind: 'Service', name: 'knative-backend' })
|
|
31
|
+
expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', group: 'serving.knative.dev', namespace: 'team', name: 'knative-backend' }))
|
|
32
|
+
await clickSink({ group: 'serving.knative.dev', kind: 'Service', name: 'group-only-backend' })
|
|
33
|
+
expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', group: 'serving.knative.dev', name: 'group-only-backend' }))
|
|
34
|
+
await clickSink({ apiVersion: 'relationships.radar.test/v1', kind: 'Service', name: 'custom-backend' })
|
|
35
|
+
expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'virtualservices', group: 'relationships.radar.test', name: 'custom-backend' }))
|
|
36
|
+
await clickSink({ apiVersion: 'v1', kind: 'Service', name: 'core-backend' })
|
|
37
|
+
expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', name: 'core-backend' }))
|
|
38
|
+
expect(onNavigate.mock.lastCall?.[0].group ?? '').toBe('')
|
|
39
|
+
|
|
40
|
+
await act(async () => root.render(<PingSourceRenderer onNavigate={onNavigate} data={{
|
|
41
|
+
metadata: { namespace: 'team' }, spec: { sink: { ref: { kind: 'Service', name: 'unknown-group' } } },
|
|
42
|
+
}} />))
|
|
43
|
+
expect(container.textContent).toContain('unknown-group')
|
|
44
|
+
expect([...container.querySelectorAll('button')].some(button => button.textContent === 'unknown-group')).toBe(false)
|
|
45
|
+
|
|
46
|
+
await act(async () => root.render(<DomainMappingRenderer onNavigate={onNavigate} data={{
|
|
47
|
+
metadata: { namespace: 'team', name: 'example.com' },
|
|
48
|
+
spec: { ref: { apiVersion: 'serving.knative.dev/v1', kind: 'Service', name: 'mapped-backend' } },
|
|
49
|
+
}} />))
|
|
50
|
+
const target = [...container.querySelectorAll('button')].find(button => button.textContent === 'mapped-backend')
|
|
51
|
+
expect(target).toBeDefined()
|
|
52
|
+
await act(async () => target!.click())
|
|
53
|
+
expect(onNavigate).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'services', group: 'serving.knative.dev', name: 'mapped-backend' }))
|
|
54
|
+
} finally {
|
|
55
|
+
await act(async () => root.unmount())
|
|
56
|
+
resetNavigationMap()
|
|
57
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { KnativeReferenceLink } from './KnativeReferenceLink'
|
|
1
2
|
import { Clock, Server, Container, Link2 } from 'lucide-react'
|
|
2
3
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner
|
|
4
|
-
import { kindToPlural } from '../../../utils/navigation'
|
|
4
|
+
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner } from '../../ui/drawer-components'
|
|
5
5
|
import { getKnativeConditionStatus } from '../resource-utils-knative'
|
|
6
6
|
|
|
7
7
|
interface RendererProps {
|
|
8
8
|
data: any
|
|
9
|
-
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
9
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function SinkProperty({ sink, ns, onNavigate }: { sink: any; ns: string; onNavigate?: RendererProps['onNavigate'] }) {
|
|
@@ -15,10 +15,9 @@ function SinkProperty({ sink, ns, onNavigate }: { sink: any; ns: string; onNavig
|
|
|
15
15
|
if (sink.ref) {
|
|
16
16
|
return (
|
|
17
17
|
<Property label="Sink" value={
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
namespace={sink.ref.namespace || ns}
|
|
18
|
+
<KnativeReferenceLink
|
|
19
|
+
reference={sink.ref}
|
|
20
|
+
namespace={ns}
|
|
22
21
|
onNavigate={onNavigate}
|
|
23
22
|
/>
|
|
24
23
|
} />
|
|
@@ -194,10 +193,9 @@ export function SinkBindingRenderer({ data, onNavigate }: RendererProps) {
|
|
|
194
193
|
<Property label="Subject API" value={subjectRef.apiVersion} />
|
|
195
194
|
{subjectRef.name && (
|
|
196
195
|
<Property label="Subject" value={
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
namespace={subjectRef.namespace || ns}
|
|
196
|
+
<KnativeReferenceLink
|
|
197
|
+
reference={subjectRef}
|
|
198
|
+
namespace={ns}
|
|
201
199
|
onNavigate={onNavigate}
|
|
202
200
|
/>
|
|
203
201
|
} />
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
isCrossplanePaused,
|
|
12
12
|
getComposingXRRef,
|
|
13
13
|
} from '../resource-utils-crossplane'
|
|
14
|
-
import {
|
|
14
|
+
import { kindToPluralWithGroup } from '../../../utils/navigation'
|
|
15
15
|
|
|
16
16
|
interface ManagedResourceRendererProps {
|
|
17
17
|
data: any
|
|
@@ -82,7 +82,7 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
|
|
|
82
82
|
value={
|
|
83
83
|
<ResourceLink
|
|
84
84
|
name={composingXR.name}
|
|
85
|
-
kind={
|
|
85
|
+
kind={kindToPluralWithGroup(composingXR.kind, extractApiGroup(composingXR.apiVersion))}
|
|
86
86
|
namespace={namespace}
|
|
87
87
|
group={extractApiGroup(composingXR.apiVersion) || undefined}
|
|
88
88
|
onNavigate={onNavigate}
|
|
@@ -2,16 +2,17 @@ import { Info } from 'lucide-react'
|
|
|
2
2
|
import { Tooltip } from '../../ui/Tooltip'
|
|
3
3
|
|
|
4
4
|
interface MetricsUnavailableNoticeProps {
|
|
5
|
+
noUsageYet?: boolean
|
|
5
6
|
rawError?: string
|
|
6
7
|
diagnosis?: string
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function MetricsUnavailableNotice({ rawError, diagnosis }: MetricsUnavailableNoticeProps) {
|
|
10
|
+
export function MetricsUnavailableNotice({ rawError, diagnosis, noUsageYet }: MetricsUnavailableNoticeProps) {
|
|
10
11
|
return (
|
|
11
12
|
<div className="card-inner-lg text-xs text-theme-text-tertiary">
|
|
12
13
|
<div className="flex items-center gap-1.5">
|
|
13
14
|
<span className="min-w-0 leading-5">
|
|
14
|
-
Metrics unavailable. Radar cannot read metrics.k8s.io.
|
|
15
|
+
{noUsageYet ? 'No usage yet: this Pod is not running' : 'Metrics unavailable. Radar cannot read metrics.k8s.io.'}
|
|
15
16
|
</span>
|
|
16
17
|
{rawError && (
|
|
17
18
|
<Tooltip
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { PodRenderer } from './PodRenderer'
|
|
6
|
+
import { getExecContainerNames } from '../resource-utils'
|
|
7
|
+
|
|
8
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
9
|
+
|
|
10
|
+
const running = { running: { startedAt: '2026-10-16T10:00:00Z' } }
|
|
11
|
+
|
|
12
|
+
const pod = {
|
|
13
|
+
metadata: { name: 'app-with-sidecar', namespace: 'shop' },
|
|
14
|
+
spec: {
|
|
15
|
+
initContainers: [
|
|
16
|
+
{ name: 'migrate', image: 'busybox' },
|
|
17
|
+
{ name: 'log-shipper', image: 'fluent-bit:3', restartPolicy: 'Always' },
|
|
18
|
+
{ name: 'stopped-sidecar', image: 'busybox', restartPolicy: 'Always' },
|
|
19
|
+
],
|
|
20
|
+
containers: [{ name: 'app', image: 'shop/app:1' }],
|
|
21
|
+
},
|
|
22
|
+
status: {
|
|
23
|
+
phase: 'Running',
|
|
24
|
+
initContainerStatuses: [
|
|
25
|
+
{ name: 'migrate', state: running, restartCount: 0 },
|
|
26
|
+
{ name: 'log-shipper', state: running, restartCount: 0, ready: true },
|
|
27
|
+
{ name: 'stopped-sidecar', state: { waiting: { reason: 'CrashLoopBackOff' } }, restartCount: 3 },
|
|
28
|
+
],
|
|
29
|
+
containerStatuses: [{ name: 'app', state: running, restartCount: 0, ready: true }],
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let container: HTMLDivElement
|
|
34
|
+
let root: Root
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
container = document.createElement('div')
|
|
38
|
+
document.body.appendChild(container)
|
|
39
|
+
root = createRoot(container)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
act(() => root.unmount())
|
|
44
|
+
container.remove()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function initCard(name: string): HTMLElement {
|
|
48
|
+
const label = Array.from(container.querySelectorAll('span')).find((s) => s.textContent === name)
|
|
49
|
+
if (!label) throw new Error(`no init container card for ${name}`)
|
|
50
|
+
return label.closest('.rounded-lg') as HTMLElement
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const terminalButton = (card: HTMLElement) => card.querySelector('svg.lucide-terminal')?.closest('button') ?? null
|
|
54
|
+
|
|
55
|
+
describe('PodRenderer native sidecar terminal', () => {
|
|
56
|
+
it('offers Terminal on a running native sidecar only, and lists sidecars in the terminal', () => {
|
|
57
|
+
const onOpenTerminal = vi.fn()
|
|
58
|
+
act(() => {
|
|
59
|
+
root.render(
|
|
60
|
+
<PodRenderer data={pod} onCopy={() => undefined} copied={null} canExec canViewLogs onOpenTerminal={onOpenTerminal} />,
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(terminalButton(initCard('migrate'))).toBeNull()
|
|
65
|
+
expect(terminalButton(initCard('stopped-sidecar'))).toBeNull()
|
|
66
|
+
|
|
67
|
+
const sidecarTerminal = terminalButton(initCard('log-shipper'))
|
|
68
|
+
expect(sidecarTerminal).not.toBeNull()
|
|
69
|
+
act(() => sidecarTerminal!.click())
|
|
70
|
+
|
|
71
|
+
expect(onOpenTerminal).toHaveBeenCalledWith({
|
|
72
|
+
namespace: 'shop',
|
|
73
|
+
podName: 'app-with-sidecar',
|
|
74
|
+
containerName: 'log-shipper',
|
|
75
|
+
containers: ['app', 'log-shipper', 'stopped-sidecar'],
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('hides the sidecar Terminal without exec permission', () => {
|
|
80
|
+
act(() => {
|
|
81
|
+
root.render(
|
|
82
|
+
<PodRenderer data={pod} onCopy={() => undefined} copied={null} canViewLogs onOpenTerminal={vi.fn()} />,
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
expect(terminalButton(initCard('log-shipper'))).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('PodRenderer sidecar files and image', () => {
|
|
90
|
+
it('switches between a sidecar\'s files and its image, not the first container\'s', () => {
|
|
91
|
+
let browser: { containers: string[]; initialContainer: string; onSwitchToImageFiles: (c?: string) => void } | undefined
|
|
92
|
+
let imageBrowser: { image: string; onSwitchToPodFiles?: () => void } | undefined
|
|
93
|
+
act(() => {
|
|
94
|
+
root.render(
|
|
95
|
+
<PodRenderer
|
|
96
|
+
data={pod}
|
|
97
|
+
onCopy={() => undefined}
|
|
98
|
+
copied={null}
|
|
99
|
+
canExec
|
|
100
|
+
renderPodBrowser={(props) => { browser = props; return null }}
|
|
101
|
+
renderImageBrowser={(props) => { imageBrowser = props; return null }}
|
|
102
|
+
/>,
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const card = initCard('log-shipper')
|
|
107
|
+
act(() => (card.querySelector('svg.lucide-folder-open')!.closest('button') as HTMLButtonElement).click())
|
|
108
|
+
expect(browser?.initialContainer).toBe('log-shipper')
|
|
109
|
+
expect(browser?.containers).toEqual(['app', 'log-shipper', 'stopped-sidecar'])
|
|
110
|
+
|
|
111
|
+
// The user switched the dialog to app before asking for image files.
|
|
112
|
+
act(() => browser!.onSwitchToImageFiles('app'))
|
|
113
|
+
expect(imageBrowser?.image).toBe('shop/app:1')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('opens a sidecar\'s files from its image', () => {
|
|
117
|
+
let browser: { initialContainer: string; onSwitchToImageFiles: (c?: string) => void } | undefined
|
|
118
|
+
let imageBrowser: { image: string; onSwitchToPodFiles?: () => void } | undefined
|
|
119
|
+
act(() => {
|
|
120
|
+
root.render(
|
|
121
|
+
<PodRenderer
|
|
122
|
+
data={pod}
|
|
123
|
+
onCopy={() => undefined}
|
|
124
|
+
copied={null}
|
|
125
|
+
canExec
|
|
126
|
+
renderPodBrowser={(props) => { browser = props; return null }}
|
|
127
|
+
renderImageBrowser={(props) => { imageBrowser = props; return null }}
|
|
128
|
+
/>,
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
const card = initCard('log-shipper')
|
|
132
|
+
act(() => (card.querySelector('svg.lucide-folder-open')!.closest('button') as HTMLButtonElement).click())
|
|
133
|
+
act(() => browser!.onSwitchToImageFiles())
|
|
134
|
+
expect(imageBrowser?.image).toBe('fluent-bit:3')
|
|
135
|
+
act(() => imageBrowser!.onSwitchToPodFiles!())
|
|
136
|
+
expect(browser?.initialContainer).toBe('log-shipper')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('PodRenderer image to live files', () => {
|
|
141
|
+
function renderWith(data: typeof pod) {
|
|
142
|
+
let browser: { initialContainer: string } | undefined
|
|
143
|
+
let imageBrowser: { image: string; onSwitchToPodFiles?: () => void } | undefined
|
|
144
|
+
act(() => {
|
|
145
|
+
root.render(
|
|
146
|
+
<PodRenderer
|
|
147
|
+
data={data}
|
|
148
|
+
onCopy={() => undefined}
|
|
149
|
+
copied={null}
|
|
150
|
+
canExec
|
|
151
|
+
renderPodBrowser={(props) => { browser = props; return null }}
|
|
152
|
+
renderImageBrowser={(props) => { imageBrowser = props; return null }}
|
|
153
|
+
/>,
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
return { getBrowser: () => browser, getImageBrowser: () => imageBrowser }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const openImage = (name: string) => {
|
|
160
|
+
const card = initCard(name)
|
|
161
|
+
const link = Array.from(card.querySelectorAll('button')).find((el) => el.textContent?.startsWith('Image:'))
|
|
162
|
+
if (!link) throw new Error(`no image link on ${name}`)
|
|
163
|
+
act(() => (link as HTMLElement).click())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
it('does not send a stopped sidecar\'s image to its live files', () => {
|
|
167
|
+
const { getBrowser, getImageBrowser } = renderWith(pod)
|
|
168
|
+
// stopped-sidecar and migrate share the busybox image; neither is a running exec target.
|
|
169
|
+
openImage('stopped-sidecar')
|
|
170
|
+
expect(getImageBrowser()?.image).toBe('busybox')
|
|
171
|
+
act(() => getImageBrowser()!.onSwitchToPodFiles!())
|
|
172
|
+
expect(getBrowser()?.initialContainer).toBe('app')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('keeps the live-files link for a running sidecar while the pod is still Pending', () => {
|
|
176
|
+
const pending = { ...pod, status: { ...pod.status, phase: 'Pending' } }
|
|
177
|
+
const { getBrowser, getImageBrowser } = renderWith(pending)
|
|
178
|
+
openImage('log-shipper')
|
|
179
|
+
expect(getImageBrowser()?.onSwitchToPodFiles).toBeDefined()
|
|
180
|
+
act(() => getImageBrowser()!.onSwitchToPodFiles!())
|
|
181
|
+
expect(getBrowser()?.initialContainer).toBe('log-shipper')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('offers no live-files link in a Pending pod when nothing using the image runs', () => {
|
|
185
|
+
const pending = { ...pod, status: { ...pod.status, phase: 'Pending' } }
|
|
186
|
+
const { getImageBrowser } = renderWith(pending)
|
|
187
|
+
openImage('stopped-sidecar')
|
|
188
|
+
expect(getImageBrowser()?.onSwitchToPodFiles).toBeUndefined()
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('getExecContainerNames', () => {
|
|
193
|
+
it('lists regular containers first, then native sidecars, never one-shot init containers', () => {
|
|
194
|
+
expect(getExecContainerNames(pod)).toEqual(['app', 'log-shipper', 'stopped-sidecar'])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('handles a pod without init containers', () => {
|
|
198
|
+
expect(getExecContainerNames({ spec: { containers: [{ name: 'app' }] } })).toEqual(['app'])
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -450,3 +450,35 @@ describe('PodRenderer on a Radar without variable sources', () => {
|
|
|
450
450
|
expect(html).not.toContain('could not be loaded')
|
|
451
451
|
})
|
|
452
452
|
})
|
|
453
|
+
|
|
454
|
+
describe('PodRenderer pending Pods, including non-CNPG workloads', () => {
|
|
455
|
+
const pending = { apiVersion: 'v1', kind: 'Pod', metadata: { name: 'nginx-join', namespace: 'default' }, spec: { containers: [{ name: 'nginx', image: 'nginx' }], initContainers: [{ name: 'setup', image: 'busybox' }] }, status: { phase: 'Pending' } }
|
|
456
|
+
const render = (data: any, metricsHistory: any = { containers: [], metricsAPIReachable: true }) => renderToString(<PodRenderer data={data} onCopy={() => {}} copied={null} metricsUnavailable metricsHistory={metricsHistory} />)
|
|
457
|
+
it('shows no usage yet only with a successful metrics API collection', () => {
|
|
458
|
+
expect(render(pending)).toContain('No usage yet: this Pod is not running')
|
|
459
|
+
expect(render(pending)).not.toContain('Radar cannot read metrics.k8s.io')
|
|
460
|
+
expect(render(pending, { containers: [], metricsAPIReachable: false })).toContain('Radar cannot read metrics.k8s.io')
|
|
461
|
+
expect(render(pending, { containers: [], collectionError: 'forbidden' })).toContain('forbidden')
|
|
462
|
+
expect(render({ ...pending, status: { phase: 'Running' } })).toContain('Radar cannot read metrics.k8s.io')
|
|
463
|
+
expect(render({ ...pending, status: { phase: 'Pending', initContainerStatuses: [{ name: 'setup', state: { running: {} } }] } })).not.toContain('No usage yet')
|
|
464
|
+
})
|
|
465
|
+
it('shows unstarted regular and init containers neutrally', () => {
|
|
466
|
+
const html = render(pending)
|
|
467
|
+
expect(html.match(/Not started/g)).toHaveLength(2)
|
|
468
|
+
expect(html).not.toContain('Not Ready')
|
|
469
|
+
expect(html).not.toMatch(/>unknown</)
|
|
470
|
+
})
|
|
471
|
+
it.each(['ContainerCreating', 'PodInitializing', 'ImagePullBackOff', 'CrashLoopBackOff'])('tones reported waiting reason %s appropriately', (reason) => {
|
|
472
|
+
const html = render({ ...pending, status: { phase: 'Pending', containerStatuses: [{ name: 'nginx', state: { waiting: { reason } } }] } })
|
|
473
|
+
const bad = ['ImagePullBackOff', 'CrashLoopBackOff'].includes(reason)
|
|
474
|
+
expect(html).toContain(bad ? 'bg-red-100' : 'bg-theme-hover/50')
|
|
475
|
+
if (bad) expect(html).toContain(reason)
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('shows reported running-but-unready nginx readiness as degraded', () => {
|
|
480
|
+
const data = { ...pod, status: { phase: 'Running', containerStatuses: [{ name: pod.spec.containers[0].name, ready: false, state: { running: {} } }] } }
|
|
481
|
+
const html = renderToString(<PodRenderer data={data} onCopy={() => {}} copied={null} />)
|
|
482
|
+
expect(html).toMatch(/bg-amber-100[^>]*>Not Ready</)
|
|
483
|
+
expect(html).toMatch(/>running</)
|
|
484
|
+
})
|
|
@@ -4,8 +4,8 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { PolicySection } from './PolicySection'
|
|
5
5
|
import type { PolicyResourceResponse } from '../../../types/policy'
|
|
6
6
|
import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
|
|
7
|
-
import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
|
|
8
|
-
import { getResourceStatusColor,
|
|
7
|
+
import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName, getExecContainerNames } from '../resource-utils'
|
|
8
|
+
import { getResourceStatusColor, SEVERITY_BORDER } from '../../../utils/badge-colors'
|
|
9
9
|
import {
|
|
10
10
|
rbacVerbBadgeClass,
|
|
11
11
|
rbacResourceBadgeClass,
|
|
@@ -26,6 +26,7 @@ import { Tooltip } from '../../ui/Tooltip'
|
|
|
26
26
|
import { RadarUpgradeNote } from '../../ui/RadarUpgradeNote'
|
|
27
27
|
import { getRadarUpgradeRequirement, type RadarUpgradeRequirement } from '../../../types/fetch-error'
|
|
28
28
|
import { MetricsChart } from '../../ui/MetricsChart'
|
|
29
|
+
import { Badge } from '../../ui/Badge'
|
|
29
30
|
import { MetricsUnavailableNotice } from './MetricsUnavailableNotice'
|
|
30
31
|
import { ContainerEnvironmentSection } from './ContainerEnvironmentSection'
|
|
31
32
|
import { formatRelativeAgeTime } from '../../../utils/format'
|
|
@@ -85,12 +86,12 @@ interface PodRendererProps {
|
|
|
85
86
|
renderPortAction?: (props: { namespace: string; podName: string; port: number; protocol: string; disabled?: boolean }) => ReactNode
|
|
86
87
|
// Metrics data injection
|
|
87
88
|
metrics?: { containers?: any[]; timestamp?: string }
|
|
88
|
-
metricsHistory?: { containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
|
|
89
|
+
metricsHistory?: { metricsAPIReachable?: boolean; containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
|
|
89
90
|
metricsUnavailable?: boolean
|
|
90
91
|
hideMetricsServer?: boolean
|
|
91
92
|
// Filesystem browser render props
|
|
92
93
|
renderImageBrowser?: (props: { image: string; namespace: string; podName: string; pullSecrets: string[]; onClose: () => void; onSwitchToPodFiles?: () => void }) => ReactNode
|
|
93
|
-
renderPodBrowser?: (props: { namespace: string; podName: string; containers: string[]; initialContainer: string; onClose: () => void; onSwitchToImageFiles: () => void }) => ReactNode
|
|
94
|
+
renderPodBrowser?: (props: { namespace: string; podName: string; containers: string[]; initialContainer: string; onClose: () => void; onSwitchToImageFiles: (containerName?: string) => void }) => ReactNode
|
|
94
95
|
/**
|
|
95
96
|
* Resolved content for envFrom references.
|
|
96
97
|
* When provided, expands ConfigMap/Secret keys inline instead of showing "(all keys)".
|
|
@@ -305,6 +306,7 @@ export function PodRenderer({
|
|
|
305
306
|
const containerStatuses = data.status?.containerStatuses || []
|
|
306
307
|
const containers = data.spec?.containers || []
|
|
307
308
|
const initContainers = data.spec?.initContainers || []
|
|
309
|
+
const podResources = data.spec?.resources
|
|
308
310
|
const initContainerStatuses = data.status?.initContainerStatuses || []
|
|
309
311
|
const hasEnvironmentDeclarations = [...initContainers, ...containers].some(
|
|
310
312
|
(container: any) => container.env?.length > 0 || container.envFrom?.length > 0,
|
|
@@ -325,7 +327,9 @@ export function PodRenderer({
|
|
|
325
327
|
const operationalIssuesShown = useOperationalIssuesShown()
|
|
326
328
|
const podProblems = getPodProblems(data)
|
|
327
329
|
const hasProblems = podProblems.length > 0 && !operationalIssuesShown
|
|
328
|
-
const
|
|
330
|
+
const notRunning = data.status?.phase === 'Pending' && ![...containerStatuses, ...initContainerStatuses].some((s: any) => s.state?.running)
|
|
331
|
+
const noUsageYet = notRunning && metricsHistory?.metricsAPIReachable === true && !metricsHistory.collectionError && !metricsHistory.containers?.length && !metrics?.containers?.length
|
|
332
|
+
const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError && !noUsageYet
|
|
329
333
|
const hasMetricsHistory = !!metricsHistory?.containers?.length
|
|
330
334
|
const currentMetrics = metricsUnavailable ? undefined : metrics
|
|
331
335
|
|
|
@@ -336,6 +340,16 @@ export function PodRenderer({
|
|
|
336
340
|
// Pod filesystem modal state
|
|
337
341
|
const [podFilesContainer, setPodFilesContainer] = useState<string | null>(null)
|
|
338
342
|
|
|
343
|
+
// A sidecar can be running while the pod is still Pending, and a stopped
|
|
344
|
+
// container has no live files, so match the image against running containers.
|
|
345
|
+
const runningImageContainer = selectedImage
|
|
346
|
+
? getExecContainerNames(data).find((name) => {
|
|
347
|
+
const spec = [...containers, ...initContainers].find((c: any) => c.name === name)
|
|
348
|
+
const status = [...containerStatuses, ...initContainerStatuses].find((st: any) => st.name === name)
|
|
349
|
+
return spec?.image === selectedImage && !!status?.state?.running
|
|
350
|
+
})
|
|
351
|
+
: undefined
|
|
352
|
+
|
|
339
353
|
const handleOpenTerminal = (containerName?: string) => {
|
|
340
354
|
const container = containerName || getDefaultContainerName(data)
|
|
341
355
|
if (namespace && podName && container) {
|
|
@@ -343,7 +357,7 @@ export function PodRenderer({
|
|
|
343
357
|
namespace,
|
|
344
358
|
podName,
|
|
345
359
|
containerName: container,
|
|
346
|
-
containers:
|
|
360
|
+
containers: getExecContainerNames(data),
|
|
347
361
|
})
|
|
348
362
|
}
|
|
349
363
|
}
|
|
@@ -508,6 +522,7 @@ export function PodRenderer({
|
|
|
508
522
|
const isFailed = stateKey === 'terminated' && exitCode != null && exitCode !== 0
|
|
509
523
|
const isWaiting = stateKey === 'waiting'
|
|
510
524
|
const isInitRunning = stateKey === 'running'
|
|
525
|
+
const isSidecar = container.restartPolicy === 'Always'
|
|
511
526
|
|
|
512
527
|
// Status label and color
|
|
513
528
|
let statusLabel: string
|
|
@@ -520,10 +535,11 @@ export function PodRenderer({
|
|
|
520
535
|
} else if (isWaiting) {
|
|
521
536
|
statusLabel = state?.waiting?.reason || 'Waiting'
|
|
522
537
|
} else {
|
|
523
|
-
statusLabel = '
|
|
538
|
+
statusLabel = 'Not started'
|
|
524
539
|
}
|
|
540
|
+
const reportedFailure = isFailed || !!(isWaiting && state?.waiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(state.waiting.reason))
|
|
525
541
|
const statusColor = getResourceStatusColor(
|
|
526
|
-
isCompleted ? 'succeeded' :
|
|
542
|
+
isCompleted ? 'succeeded' : reportedFailure ? 'failed' : isInitRunning ? 'running' : 'unknown'
|
|
527
543
|
)
|
|
528
544
|
|
|
529
545
|
// Build command string
|
|
@@ -534,10 +550,11 @@ export function PodRenderer({
|
|
|
534
550
|
return (
|
|
535
551
|
<div key={container.name} className={clsx(
|
|
536
552
|
'rounded-lg p-3 border-l-2',
|
|
537
|
-
isCompleted ? 'bg-theme-elevated/20
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
553
|
+
isCompleted ? 'bg-theme-elevated/20' : 'bg-theme-elevated/30',
|
|
554
|
+
isCompleted ? SEVERITY_BORDER.success :
|
|
555
|
+
reportedFailure ? 'border-l-semantic-error' :
|
|
556
|
+
isInitRunning ? SEVERITY_BORDER.info :
|
|
557
|
+
SEVERITY_BORDER.neutral
|
|
541
558
|
)}>
|
|
542
559
|
<div className="flex items-center justify-between mb-2">
|
|
543
560
|
<div className="flex items-center gap-2">
|
|
@@ -547,6 +564,26 @@ export function PodRenderer({
|
|
|
547
564
|
<span className="text-sm font-medium text-theme-text-primary">{container.name}</span>
|
|
548
565
|
</div>
|
|
549
566
|
<div className="flex items-center gap-2">
|
|
567
|
+
{isSidecar && isInitRunning && canExec && onOpenTerminal && (
|
|
568
|
+
<Tooltip content="Terminal" delay={150}>
|
|
569
|
+
<button
|
|
570
|
+
onClick={() => handleOpenTerminal(container.name)}
|
|
571
|
+
className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
|
|
572
|
+
>
|
|
573
|
+
<TerminalIcon className="w-4 h-4" />
|
|
574
|
+
</button>
|
|
575
|
+
</Tooltip>
|
|
576
|
+
)}
|
|
577
|
+
{isSidecar && isInitRunning && canExec && renderPodBrowser && (
|
|
578
|
+
<Tooltip content="Browse files" delay={150}>
|
|
579
|
+
<button
|
|
580
|
+
onClick={() => setPodFilesContainer(container.name)}
|
|
581
|
+
className="p-1 text-slate-400 hover:text-blue-400 hover:bg-slate-600/50 rounded transition-colors"
|
|
582
|
+
>
|
|
583
|
+
<FolderOpen className="w-4 h-4" />
|
|
584
|
+
</button>
|
|
585
|
+
</Tooltip>
|
|
586
|
+
)}
|
|
550
587
|
{canViewLogs && (
|
|
551
588
|
<Tooltip content="Logs" delay={150}>
|
|
552
589
|
<button
|
|
@@ -575,7 +612,7 @@ export function PodRenderer({
|
|
|
575
612
|
</div>
|
|
576
613
|
)}
|
|
577
614
|
{/* Waiting reason detail */}
|
|
578
|
-
{isWaiting && state?.waiting?.reason && state.waiting.reason
|
|
615
|
+
{isWaiting && state?.waiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(state.waiting.reason) && (
|
|
579
616
|
<div className="text-red-400 flex items-center gap-1">
|
|
580
617
|
<span className="font-medium">{state.waiting.reason}</span>
|
|
581
618
|
{state.waiting.message && (
|
|
@@ -623,6 +660,20 @@ export function PodRenderer({
|
|
|
623
660
|
|
|
624
661
|
<Section title="Containers" icon={HardDrive} defaultExpanded>
|
|
625
662
|
<div className="space-y-3">
|
|
663
|
+
{/* Pod-level budget (spec.resources) is shared by all containers, so it
|
|
664
|
+
stays its own row rather than being merged into the container rows. */}
|
|
665
|
+
{(podResources?.requests || podResources?.limits) && (
|
|
666
|
+
<div className="rounded-[10px] border border-dashed border-theme-border p-3">
|
|
667
|
+
<div className="flex items-baseline justify-between gap-2 mb-2">
|
|
668
|
+
<span className="text-sm font-medium text-theme-text-primary">Pod (aggregate)</span>
|
|
669
|
+
<span className="text-xs text-theme-text-tertiary">Shared by all containers</span>
|
|
670
|
+
</div>
|
|
671
|
+
<div className="text-xs text-theme-text-secondary space-y-1">
|
|
672
|
+
{podResources.requests && <div>Requests: {formatResources(podResources.requests)}</div>}
|
|
673
|
+
{podResources.limits && <div>Limits: {formatResources(podResources.limits)}</div>}
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
626
677
|
{containers.map((container: any) => {
|
|
627
678
|
const status = containerStatuses.find((s: any) => s.name === container.name)
|
|
628
679
|
const state = status?.state
|
|
@@ -638,6 +689,7 @@ export function PodRenderer({
|
|
|
638
689
|
// failure — tone its badges/text sky, not red, so the drawer agrees
|
|
639
690
|
// with the calm "Completed" table badge instead of screaming red.
|
|
640
691
|
const terminatedOk = currentTerminated?.exitCode === 0
|
|
692
|
+
const reportedFailure = !!((currentTerminated?.exitCode != null && currentTerminated.exitCode !== 0) || (currentWaiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(currentWaiting.reason)))
|
|
641
693
|
|
|
642
694
|
return (
|
|
643
695
|
<div key={container.name} className="card-inner-lg">
|
|
@@ -674,23 +726,12 @@ export function PodRenderer({
|
|
|
674
726
|
</button>
|
|
675
727
|
</Tooltip>
|
|
676
728
|
)}
|
|
677
|
-
<
|
|
678
|
-
'
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
SEVERITY_BADGE_BORDERED.error
|
|
682
|
-
)}>
|
|
683
|
-
{isReady ? 'Ready' : terminatedOk ? 'Completed' : 'Not Ready'}
|
|
684
|
-
</span>
|
|
685
|
-
<span className={clsx(
|
|
686
|
-
'badge',
|
|
687
|
-
stateKey === 'running' ? SEVERITY_BADGE_BORDERED.success :
|
|
688
|
-
stateKey === 'waiting' ? SEVERITY_BADGE_BORDERED.warning :
|
|
689
|
-
terminatedOk ? SEVERITY_BADGE_BORDERED.info :
|
|
690
|
-
SEVERITY_BADGE_BORDERED.error
|
|
691
|
-
)}>
|
|
729
|
+
<Badge severity={isReady ? 'success' : terminatedOk ? 'info' : reportedFailure ? 'error' : stateKey === 'running' && isReady === false ? 'warning' : 'neutral'}>
|
|
730
|
+
{isReady ? 'Ready' : terminatedOk ? 'Completed' : !stateKey || stateKey === 'unknown' ? 'Not started' : 'Not Ready'}
|
|
731
|
+
</Badge>
|
|
732
|
+
{stateKey !== 'unknown' && <Badge severity={stateKey === 'running' ? 'success' : terminatedOk ? 'info' : reportedFailure ? 'error' : 'neutral'}>
|
|
692
733
|
{stateKey}
|
|
693
|
-
</
|
|
734
|
+
</Badge>}
|
|
694
735
|
</div>
|
|
695
736
|
</div>
|
|
696
737
|
<div className="text-xs text-theme-text-secondary space-y-1">
|
|
@@ -704,7 +745,7 @@ export function PodRenderer({
|
|
|
704
745
|
)
|
|
705
746
|
})()}
|
|
706
747
|
{/* Show current waiting reason (e.g., CrashLoopBackOff) */}
|
|
707
|
-
{currentWaiting?.reason && currentWaiting.reason
|
|
748
|
+
{currentWaiting?.reason && !['ContainerCreating', 'PodInitializing'].includes(currentWaiting.reason) && (
|
|
708
749
|
<div className="text-red-400 flex items-center gap-1">
|
|
709
750
|
<span className="font-medium">{currentWaiting.reason}</span>
|
|
710
751
|
{currentWaiting.message && (
|
|
@@ -831,8 +872,9 @@ export function PodRenderer({
|
|
|
831
872
|
)}
|
|
832
873
|
|
|
833
874
|
{/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
|
|
834
|
-
{!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable) && (
|
|
875
|
+
{!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable || noUsageYet) && (
|
|
835
876
|
<Section title="Resource Usage" icon={Activity} defaultExpanded>
|
|
877
|
+
{noUsageYet && <MetricsUnavailableNotice noUsageYet />}
|
|
836
878
|
{showMetricsUnavailable && (
|
|
837
879
|
<MetricsUnavailableNotice rawError={metricsHistory?.metricsUnavailableReason} diagnosis={metricsHistory?.metricsUnavailableDiagnosis} />
|
|
838
880
|
)}
|
|
@@ -852,7 +894,8 @@ export function PodRenderer({
|
|
|
852
894
|
// fall through to them or their chart shows no limit line.
|
|
853
895
|
const containerSpec = containers.find((c: any) => c.name === historyContainer.name)
|
|
854
896
|
|| initContainers.find((c: any) => c.name === historyContainer.name && c.restartPolicy === 'Always')
|
|
855
|
-
|
|
897
|
+
// A container without its own limit is still bounded by the pod-level one.
|
|
898
|
+
const limits = { ...podResources?.limits, ...containerSpec?.resources?.limits }
|
|
856
899
|
const requests = containerSpec?.resources?.requests
|
|
857
900
|
|
|
858
901
|
// Get historical data points (from history or empty)
|
|
@@ -961,10 +1004,8 @@ export function PodRenderer({
|
|
|
961
1004
|
podName: podName || '',
|
|
962
1005
|
pullSecrets: imagePullSecrets,
|
|
963
1006
|
onClose: () => setSelectedImage(null),
|
|
964
|
-
onSwitchToPodFiles:
|
|
965
|
-
|
|
966
|
-
const match = containers.find((c: any) => c.image === selectedImage)
|
|
967
|
-
setPodFilesContainer(match?.name || containers[0]?.name)
|
|
1007
|
+
onSwitchToPodFiles: canExec && renderPodBrowser && (isRunning || runningImageContainer) ? () => {
|
|
1008
|
+
setPodFilesContainer(runningImageContainer || containers[0]?.name)
|
|
968
1009
|
} : undefined,
|
|
969
1010
|
})}
|
|
970
1011
|
|
|
@@ -972,12 +1013,12 @@ export function PodRenderer({
|
|
|
972
1013
|
{podFilesContainer && renderPodBrowser && renderPodBrowser({
|
|
973
1014
|
namespace: namespace || '',
|
|
974
1015
|
podName: podName || '',
|
|
975
|
-
containers:
|
|
1016
|
+
containers: getExecContainerNames(data),
|
|
976
1017
|
initialContainer: podFilesContainer,
|
|
977
1018
|
onClose: () => setPodFilesContainer(null),
|
|
978
|
-
onSwitchToImageFiles: () => {
|
|
979
|
-
|
|
980
|
-
const match = containers.find((c: any) => c.name ===
|
|
1019
|
+
onSwitchToImageFiles: (containerName?: string) => {
|
|
1020
|
+
const selected = containerName || podFilesContainer
|
|
1021
|
+
const match = [...containers, ...initContainers].find((c: any) => c.name === selected)
|
|
981
1022
|
if (match?.image) setSelectedImage(match.image)
|
|
982
1023
|
},
|
|
983
1024
|
})}
|