@skyhook-io/k8s-ui 1.18.0 → 1.18.2
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 +21 -0
- package/package.json +1 -1
- package/src/components/applications/ApplicationsView.tsx +8 -7
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
- package/src/components/compare/CompareResourcePicker.tsx +18 -4
- package/src/components/compare/ResourceCompareView.tsx +6 -2
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
- package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
- package/src/components/gitops/GitOpsTableView.tsx +46 -12
- package/src/components/gitops/detail-helpers.test.ts +58 -13
- package/src/components/gitops/detail-helpers.ts +37 -6
- package/src/components/gitops/index.ts +1 -0
- package/src/components/issues/IssuesView.tsx +16 -11
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/severity.ts +1 -0
- package/src/components/logs/LogCore.tsx +13 -11
- package/src/components/logs/LogsViewer.test.tsx +104 -59
- package/src/components/logs/LogsViewer.tsx +28 -8
- package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
- package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
- package/src/components/namespace-switcher/NamespacePicker.tsx +44 -8
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
- package/src/components/resources/ResourcesSidebar.tsx +62 -26
- package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
- package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
- package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
- package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
- package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
- package/src/components/resources/ResourcesView.tsx +342 -83
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +15 -0
- package/src/components/resources/printer-columns.test.ts +36 -0
- package/src/components/resources/printer-columns.ts +12 -1
- package/src/components/resources/readiness-gates.test.ts +95 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
- package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
- package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
- package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
- package/src/components/resources/renderers/PodRenderer.tsx +32 -9
- package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
- package/src/components/resources/resource-utils-crossplane.ts +44 -8
- package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
- package/src/components/resources/resource-utils-flagger.ts +76 -0
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
- package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
- package/src/components/resources/resource-utils-trivy.test.ts +87 -0
- package/src/components/resources/resource-utils-trivy.ts +122 -0
- package/src/components/resources/resource-utils.ts +80 -2
- package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
- package/src/components/shared/CreateResourceDialog.tsx +5 -0
- package/src/components/shared/DetailShell.phone.test.tsx +54 -0
- package/src/components/shared/DetailShell.tsx +21 -8
- package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
- package/src/components/shared/EditableYamlView.test.tsx +58 -0
- package/src/components/shared/EditableYamlView.tsx +18 -3
- package/src/components/shared/FilterRailPanel.test.tsx +39 -0
- package/src/components/shared/FilterRailPanel.tsx +62 -0
- package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
- package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
- package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
- package/src/components/shared/ManagedByChip.test.tsx +22 -0
- package/src/components/shared/ManagedByChip.tsx +158 -5
- package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
- package/src/components/shared/ResourceActionsBar.tsx +31 -9
- package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +69 -31
- package/src/components/shared/SetImageDialog.tsx +3 -0
- package/src/components/timeline/TimelineList.tsx +5 -1
- package/src/components/topology/TopologyControls.tsx +1 -1
- package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
- package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
- package/src/components/ui/FetchResult.test.tsx +19 -2
- package/src/components/ui/FetchResult.tsx +13 -1
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
- package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
- package/src/components/ui/MultiSelectPicker.tsx +20 -2
- package/src/components/ui/PageHeader.tsx +7 -4
- package/src/components/ui/SummaryTile.tsx +13 -3
- package/src/components/ui/Toast.tsx +33 -26
- package/src/components/ui/Tooltip.touch.test.tsx +55 -0
- package/src/components/ui/Tooltip.tsx +11 -2
- package/src/components/ui/YamlReview.test.tsx +52 -0
- package/src/components/ui/YamlReview.tsx +7 -0
- package/src/components/ui/drawer-components.test.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +302 -18
- package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +73 -0
- package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
- package/src/components/workload/WorkloadView.tsx +105 -34
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
- package/src/hooks/useKeepInViewport.test.ts +27 -0
- package/src/hooks/useKeepInViewport.ts +87 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/usePhoneLayout.ts +12 -0
- package/src/theme/tailwind-theme.css +5 -0
- package/src/types/core.ts +119 -3
- package/src/types/gitops-flux-inventory.test.ts +24 -0
- package/src/types/gitops.ts +5 -5
- package/src/types/index.ts +3 -0
- package/src/types/read-coverage.ts +65 -0
- package/src/types/resource-coverage.ts +40 -0
- package/src/utils/api-resources.ts +1 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +6 -1
- package/src/utils/gitops-owner.test.ts +42 -1
- package/src/utils/gitops-owner.ts +158 -8
- package/src/utils/gitops-write-guard.test.ts +332 -0
- package/src/utils/gitops-write-guard.ts +175 -11
- package/src/utils/index.ts +3 -0
- package/src/utils/read-actions.test.ts +22 -0
- package/src/utils/read-actions.ts +90 -0
- package/src/utils/resource-coverage.test.ts +121 -0
- package/src/utils/resource-coverage.ts +117 -0
- package/src/utils/touch-intent.test.ts +39 -0
- package/src/utils/touch-intent.ts +35 -0
- package/vitest.config.ts +26 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import { Info, CheckCircle, XCircle, AlertCircle, FileCode, AlertTriangle, Layers } from 'lucide-react'
|
|
2
|
+
import { Info, CheckCircle, XCircle, AlertCircle, FileCode, AlertTriangle, Layers, Minus } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
5
5
|
import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
|
|
6
6
|
import { getGenericResourceStatus } from '../generic-status'
|
|
7
|
+
import { getCrossplanePauseState, getCrossplaneIndependentStatus, getCrossplaneConditionTone } from '../resource-utils-crossplane'
|
|
8
|
+
import { CrossplanePauseNotice } from './CrossplanePauseContext'
|
|
7
9
|
import type { HealthLevel } from '../resource-utils'
|
|
8
10
|
|
|
9
11
|
interface GenericRendererProps {
|
|
@@ -16,6 +18,7 @@ export function GenericRenderer({ data }: GenericRendererProps) {
|
|
|
16
18
|
const conditions = status.conditions
|
|
17
19
|
|
|
18
20
|
// Determine health status from conditions or status fields
|
|
21
|
+
const acknowledgedPause = getCrossplanePauseState(data) === 'acknowledged'
|
|
19
22
|
const healthStatus = getHealthStatus(data)
|
|
20
23
|
|
|
21
24
|
// Extract important fields from spec and status
|
|
@@ -32,6 +35,7 @@ export function GenericRenderer({ data }: GenericRendererProps) {
|
|
|
32
35
|
|
|
33
36
|
return (
|
|
34
37
|
<>
|
|
38
|
+
{acknowledgedPause && <CrossplanePauseNotice data={data} />}
|
|
35
39
|
{/* Health Status Banner (if determinable) */}
|
|
36
40
|
{healthStatus && (
|
|
37
41
|
<div className={clsx(
|
|
@@ -90,7 +94,7 @@ export function GenericRenderer({ data }: GenericRendererProps) {
|
|
|
90
94
|
|
|
91
95
|
{/* Conditions (common in CRDs) */}
|
|
92
96
|
{conditions && Array.isArray(conditions) && conditions.length > 0 && (
|
|
93
|
-
<GenericConditionsSection conditions={conditions} />
|
|
97
|
+
<GenericConditionsSection conditions={conditions} resource={data} />
|
|
94
98
|
)}
|
|
95
99
|
|
|
96
100
|
{/* Nested Status Objects */}
|
|
@@ -154,8 +158,9 @@ const BANNER_TYPE: Record<HealthLevel, HealthType> = {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
function getHealthStatus(resource: any): HealthStatusResult | null {
|
|
157
|
-
const
|
|
158
|
-
|
|
161
|
+
const acknowledgedPause = getCrossplanePauseState(resource) === 'acknowledged'
|
|
162
|
+
const derived = acknowledgedPause ? getCrossplaneIndependentStatus(resource) : getGenericResourceStatus(resource)
|
|
163
|
+
if (!derived || (acknowledgedPause && derived.tone === 'healthy')) return null
|
|
159
164
|
return { type: BANNER_TYPE[derived.tone], label: derived.text, message: derived.reason }
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -323,11 +328,11 @@ function NestedObjectViewer({ name, value, depth = 0 }: NestedObjectViewerProps)
|
|
|
323
328
|
// CONDITIONS SECTION
|
|
324
329
|
// ============================================================================
|
|
325
330
|
|
|
326
|
-
function GenericConditionsSection({ conditions }: { conditions: any[] }) {
|
|
331
|
+
function GenericConditionsSection({ conditions, resource }: { conditions: any[]; resource: any }) {
|
|
327
332
|
// Sort conditions: problematic first, then alphabetically
|
|
328
333
|
const sortedConditions = [...conditions].sort((a, b) => {
|
|
329
|
-
const aProblematic = a.status === 'False' || (a.status === 'True' && ['Degraded', 'Warning'].includes(a.type))
|
|
330
|
-
const bProblematic = b.status === 'False' || (b.status === 'True' && ['Degraded', 'Warning'].includes(b.type))
|
|
334
|
+
const aProblematic = getCrossplaneConditionTone(resource, a) !== 'neutral' && (a.status === 'False' || (a.status === 'True' && ['Degraded', 'Warning'].includes(a.type)))
|
|
335
|
+
const bProblematic = getCrossplaneConditionTone(resource, b) !== 'neutral' && (b.status === 'False' || (b.status === 'True' && ['Degraded', 'Warning'].includes(b.type)))
|
|
331
336
|
if (aProblematic && !bProblematic) return -1
|
|
332
337
|
if (!aProblematic && bProblematic) return 1
|
|
333
338
|
return (a.type || '').localeCompare(b.type || '')
|
|
@@ -335,13 +340,14 @@ function GenericConditionsSection({ conditions }: { conditions: any[] }) {
|
|
|
335
340
|
|
|
336
341
|
// Check for any problems
|
|
337
342
|
const hasProblems = sortedConditions.some(c =>
|
|
338
|
-
c.status === 'False' || (c.status === 'True' && ['Degraded', 'Warning'].includes(c.type))
|
|
343
|
+
getCrossplaneConditionTone(resource, c) !== 'neutral' && (c.status === 'False' || (c.status === 'True' && ['Degraded', 'Warning'].includes(c.type)))
|
|
339
344
|
)
|
|
340
345
|
|
|
341
346
|
return (
|
|
342
347
|
<Section title={`Conditions (${conditions.length})`} defaultExpanded={hasProblems || conditions.length <= 5}>
|
|
343
348
|
<div className="space-y-2">
|
|
344
349
|
{sortedConditions.map((cond: any, index: number) => {
|
|
350
|
+
const isNeutral = getCrossplaneConditionTone(resource, cond) === 'neutral'
|
|
345
351
|
const isTrue = cond.status === 'True'
|
|
346
352
|
const isFalse = cond.status === 'False'
|
|
347
353
|
const isWarningType = ['Degraded', 'Warning', 'ScalingLimited'].includes(cond.type)
|
|
@@ -349,7 +355,8 @@ function GenericConditionsSection({ conditions }: { conditions: any[] }) {
|
|
|
349
355
|
|
|
350
356
|
// Determine icon color based on condition semantics
|
|
351
357
|
let iconColor = 'text-yellow-400'
|
|
352
|
-
if (
|
|
358
|
+
if (isNeutral) iconColor = 'text-theme-text-tertiary'
|
|
359
|
+
else if (isTrue && isPositiveType) iconColor = 'text-green-400'
|
|
353
360
|
else if (isTrue && isWarningType) iconColor = 'text-yellow-400'
|
|
354
361
|
else if (isTrue) iconColor = 'text-green-400'
|
|
355
362
|
else if (isFalse && isPositiveType) iconColor = 'text-red-400'
|
|
@@ -358,7 +365,8 @@ function GenericConditionsSection({ conditions }: { conditions: any[] }) {
|
|
|
358
365
|
return (
|
|
359
366
|
<div key={`${cond.type}-${index}`} className="flex items-start gap-2 text-sm">
|
|
360
367
|
<span className={clsx('w-5 h-5 rounded-full flex items-center justify-center shrink-0 mt-0.5', iconColor)}>
|
|
361
|
-
{
|
|
368
|
+
{isNeutral ? <Minus className="w-4 h-4" /> :
|
|
369
|
+
isTrue && isPositiveType ? <CheckCircle className="w-4 h-4" /> :
|
|
362
370
|
isTrue && isWarningType ? <AlertTriangle className="w-4 h-4" /> :
|
|
363
371
|
isTrue ? <CheckCircle className="w-4 h-4" /> :
|
|
364
372
|
isFalse && isPositiveType ? <XCircle className="w-4 h-4" /> :
|
|
@@ -370,7 +378,7 @@ function GenericConditionsSection({ conditions }: { conditions: any[] }) {
|
|
|
370
378
|
<span className="text-theme-text-primary font-medium">{cond.type}</span>
|
|
371
379
|
<span className={clsx(
|
|
372
380
|
'badge-sm',
|
|
373
|
-
isTrue ? 'bg-theme-elevated text-theme-text-secondary' : 'status-red'
|
|
381
|
+
isTrue || isNeutral ? 'bg-theme-elevated text-theme-text-secondary' : 'status-red'
|
|
374
382
|
)}>
|
|
375
383
|
{cond.status}
|
|
376
384
|
</span>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Settings, Cloud, ScrollText,
|
|
1
|
+
import { Box, Settings, Cloud, ScrollText, Layers } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { CodeViewer } from '../../ui/CodeViewer'
|
|
4
4
|
import {
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
getExternalName,
|
|
9
9
|
getManagementPolicies,
|
|
10
10
|
getDeletionPolicy,
|
|
11
|
-
|
|
11
|
+
getCrossplaneConditionTone,
|
|
12
12
|
getComposingXRRef,
|
|
13
13
|
} from '../resource-utils-crossplane'
|
|
14
|
+
import { CrossplanePauseNotice } from './CrossplanePauseContext'
|
|
14
15
|
import { kindToPluralWithGroup } from '../../../utils/navigation'
|
|
15
16
|
|
|
16
17
|
interface ManagedResourceRendererProps {
|
|
@@ -34,7 +35,6 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
|
|
|
34
35
|
const deletionPolicy = getDeletionPolicy(data)
|
|
35
36
|
const apiGroup = extractApiGroup(data?.apiVersion)
|
|
36
37
|
const namespace = data?.metadata?.namespace || ''
|
|
37
|
-
const paused = isCrossplanePaused(data)
|
|
38
38
|
const composingXR = getComposingXRRef(data)
|
|
39
39
|
|
|
40
40
|
const forProvider = data?.spec?.forProvider
|
|
@@ -45,14 +45,7 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
|
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
47
|
<>
|
|
48
|
-
{
|
|
49
|
-
<AlertBanner
|
|
50
|
-
variant="warning"
|
|
51
|
-
icon={Pause}
|
|
52
|
-
title="Reconciliation paused"
|
|
53
|
-
message="The crossplane.io/paused annotation is set. The provider will not reconcile this resource until the annotation is removed."
|
|
54
|
-
/>
|
|
55
|
-
)}
|
|
48
|
+
<CrossplanePauseNotice data={data} />
|
|
56
49
|
|
|
57
50
|
{alertVariant && statusReason && !operationalIssuesShown && (
|
|
58
51
|
<AlertBanner
|
|
@@ -126,7 +119,7 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
|
|
|
126
119
|
</Section>
|
|
127
120
|
)}
|
|
128
121
|
|
|
129
|
-
<ConditionsSection conditions={data?.status?.conditions} />
|
|
122
|
+
<ConditionsSection conditions={data?.status?.conditions} getConditionTone={condition => getCrossplaneConditionTone(data, condition)} />
|
|
130
123
|
</>
|
|
131
124
|
)
|
|
132
125
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import { Badge } from '../../ui/Badge'
|
|
3
|
+
import { healthToSeverity } from '../../../utils/badge-colors'
|
|
4
|
+
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
5
|
+
import { getCollectorPipelines, getOpenTelemetryCollectorStatus, getOpenTelemetryInstrumentationStatus } from '../resource-utils-opentelemetry'
|
|
6
|
+
|
|
7
|
+
export function OpenTelemetryCollectorRenderer({ data }: { data: any }) {
|
|
8
|
+
const state = getOpenTelemetryCollectorStatus(data)
|
|
9
|
+
const preview = useMemo(() => getCollectorPipelines(data), [data])
|
|
10
|
+
const [showAll, setShowAll] = useState(false)
|
|
11
|
+
const ready = data.status?.conditions?.find((c: any) => c.type === 'Ready')
|
|
12
|
+
const components = (names: string[], separator = ', ') => names.slice(0, 10).map(name => `${name}${preview.connectors.includes(name) ? ' (connector)' : ''}`).join(separator) + (names.length > 10 ? `; ${names.length - 10} more in YAML` : '') || 'None'
|
|
13
|
+
return <>
|
|
14
|
+
<Section title="Collector" defaultExpanded>
|
|
15
|
+
<PropertyList>
|
|
16
|
+
<Property label="Reconciliation" value={<Badge severity={healthToSeverity(state.level)}>{state.text}</Badge>} />
|
|
17
|
+
<Property label="Mode" value={data.spec?.mode} />
|
|
18
|
+
<Property label="Management" value={data.spec?.managementState} />
|
|
19
|
+
<Property label={data.spec?.mode === 'daemonset' ? 'Reported ready / scheduled Pods' : 'Reported ready / observed Pods'} value={data.status?.scale?.statusReplicas} />
|
|
20
|
+
<Property label="Version" value={data.status?.version} />
|
|
21
|
+
{data.status?.image && <Property label="Image" value={<span className="break-all">{data.status.image}</span>} />}
|
|
22
|
+
</PropertyList>
|
|
23
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">Reconciled means the operator applied its configuration. Pod counts are its last reported snapshot. Inspect owned workloads for current readiness; neither proves telemetry delivery.</p>
|
|
24
|
+
{state.text === 'Status stale' && <p className="mt-2 text-sm text-theme-text-secondary">The reported reconciliation describes an earlier configuration generation.</p>}
|
|
25
|
+
{ready?.message && <p className="mt-2 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{ready.message}</p>}
|
|
26
|
+
</Section>
|
|
27
|
+
<Section title={preview.error ? 'Configured Pipelines' : `Configured Pipelines (${preview.pipelines.length})`} defaultExpanded>
|
|
28
|
+
{preview.error ? <p className="text-sm text-theme-text-secondary">{preview.error}</p> : preview.pipelines.length === 0 ? <p className="text-sm text-theme-text-tertiary">No pipelines are declared in this configuration.</p> : <>
|
|
29
|
+
<div className="space-y-3">
|
|
30
|
+
{(showAll ? preview.pipelines : preview.pipelines.slice(0, 5)).map(pipeline => <div key={pipeline.name} className="card-inner min-w-0 break-words">
|
|
31
|
+
<div className="mb-2 text-sm font-medium text-theme-text-primary">{pipeline.name}</div>
|
|
32
|
+
<PropertyList>
|
|
33
|
+
<Property label="Receivers" value={components(pipeline.receivers)} />
|
|
34
|
+
<Property label="Processors" value={components(pipeline.processors, ' → ')} />
|
|
35
|
+
<Property label="Exporters" value={components(pipeline.exporters)} />
|
|
36
|
+
</PropertyList>
|
|
37
|
+
</div>)}
|
|
38
|
+
</div>
|
|
39
|
+
{preview.pipelines.length > 5 && <button type="button" className="mt-2 text-sm text-accent-text hover:underline" onClick={() => setShowAll(!showAll)}>{showAll ? 'Show fewer pipelines' : `Show all ${preview.pipelines.length} pipelines`}</button>}
|
|
40
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">Declared component names, not observed data flow. Connectors pass data between pipelines within the collector.</p>
|
|
41
|
+
</>}
|
|
42
|
+
</Section>
|
|
43
|
+
<ConditionsSection conditions={data.status?.conditions} getConditionTone={condition => ['Status stale', 'Unmanaged'].includes(state.text) ? 'unknown' : condition.type === 'Ready' && condition.status === 'False' ? condition.reason === 'StatusUpdateFailed' ? 'unknown' : 'fail' : 'neutral'} />
|
|
44
|
+
</>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function OpenTelemetryInstrumentationRenderer({ data }: { data: any }) {
|
|
48
|
+
const spec = data.spec ?? {}
|
|
49
|
+
const state = getOpenTelemetryInstrumentationStatus(data)
|
|
50
|
+
const blocked = Object.keys(data.status?.upgradeBlockedVersions ?? {})
|
|
51
|
+
const runtimeImages = ['java', 'nodejs', 'python', 'dotnet', 'go', 'apacheHttpd', 'nginx', 'php', 'ruby'].filter(language => spec[language]?.image)
|
|
52
|
+
return <>
|
|
53
|
+
<Section title="Auto-instrumentation Configuration" defaultExpanded>
|
|
54
|
+
<PropertyList>
|
|
55
|
+
<Property label="Configuration" value={<Badge severity={healthToSeverity(state.level)}>{state.text}</Badge>} />
|
|
56
|
+
{spec.exporter?.endpoint && <Property label="OTLP endpoint" value={<span className="break-all">{spec.exporter.endpoint}</span>} />}
|
|
57
|
+
<Property label="Propagators" value={spec.propagators?.join(', ')} />
|
|
58
|
+
<Property label="Sampler" value={spec.sampler?.type} />
|
|
59
|
+
<Property label="Sampler argument" value={spec.sampler?.argument} />
|
|
60
|
+
{blocked.length > 0 && <Property label="Blocked image upgrades" value={blocked.join(', ')} />}
|
|
61
|
+
</PropertyList>
|
|
62
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">These settings configure injection into annotated workloads. This resource does not report which Pods were injected or whether telemetry reaches the endpoint.</p>
|
|
63
|
+
{blocked.length > 0 && <p className="mt-2 text-sm text-theme-text-secondary">The operator could not automatically upgrade the listed runtime images. Inspect status in YAML and the operator logs before choosing a supported image.</p>}
|
|
64
|
+
</Section>
|
|
65
|
+
{blocked.length > 0 && <Section title="Blocked Image Upgrades" defaultExpanded>
|
|
66
|
+
<PropertyList>{blocked.map(language => <Property key={language} label={language} value={<span className="whitespace-pre-wrap break-words">{String(data.status.upgradeBlockedVersions[language]).slice(0, 1000)}{String(data.status.upgradeBlockedVersions[language]).length > 1000 ? '… Inspect YAML for the full explanation.' : ''}</span>} />)}</PropertyList>
|
|
67
|
+
</Section>}
|
|
68
|
+
{/* Runtime image defaults are secondary to endpoint/sampler and upgrade failures. */}
|
|
69
|
+
{runtimeImages.length > 0 && <Section title="Runtime Images" defaultExpanded={false}><PropertyList>
|
|
70
|
+
{runtimeImages.map(language => <Property key={language} label={language} value={<span className="break-all">{spec[language].image}</span>} />)}
|
|
71
|
+
</PropertyList></Section>}
|
|
72
|
+
</>
|
|
73
|
+
}
|
|
@@ -21,6 +21,70 @@ const pod = {
|
|
|
21
21
|
status: { phase: 'Running' },
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
describe('PodRenderer completed readiness conditions', () => {
|
|
25
|
+
const completionConditions = [
|
|
26
|
+
{ type: 'PodReadyToStartContainers', status: 'False' },
|
|
27
|
+
{ type: 'Initialized', status: 'True', reason: 'PodCompleted' },
|
|
28
|
+
{ type: 'Ready', status: 'False', reason: 'PodCompleted' },
|
|
29
|
+
{ type: 'ContainersReady', status: 'False', reason: 'PodCompleted' },
|
|
30
|
+
{ type: 'PodScheduled', status: 'True' },
|
|
31
|
+
]
|
|
32
|
+
const renderConditions = (phase: string | undefined, conditions = completionConditions) => renderToString(
|
|
33
|
+
<PodRenderer
|
|
34
|
+
data={{ ...pod, status: { phase, conditions } }}
|
|
35
|
+
onCopy={() => undefined}
|
|
36
|
+
copied={null}
|
|
37
|
+
/>,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
it('keeps completed readiness observations visible without counting them as failing', () => {
|
|
41
|
+
const html = renderConditions('Succeeded')
|
|
42
|
+
|
|
43
|
+
expect(html).toContain('Conditions (5)')
|
|
44
|
+
expect(html).not.toContain('failing')
|
|
45
|
+
expect(html).toContain('PodReadyToStartContainers')
|
|
46
|
+
expect(html).toContain('ContainersReady')
|
|
47
|
+
expect(html).toContain('PodCompleted')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('uses successful completion even when readiness reasons are stale or absent', () => {
|
|
51
|
+
const html = renderConditions('Succeeded', [
|
|
52
|
+
{ type: 'PodReadyToStartContainers', status: 'False' },
|
|
53
|
+
{ type: 'Ready', status: 'False', reason: 'ContainersNotReady' },
|
|
54
|
+
{ type: 'ContainersReady', status: 'False' },
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
expect(html).toContain('Conditions (3)')
|
|
58
|
+
expect(html).not.toContain('failing')
|
|
59
|
+
expect(html).toContain('ContainersNotReady')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it.each(['Running', 'Failed', undefined])('retains readiness failures when phase is %s', (phase) => {
|
|
63
|
+
const html = renderConditions(phase, [
|
|
64
|
+
{ type: 'PodReadyToStartContainers', status: 'False' },
|
|
65
|
+
{ type: 'Ready', status: 'False', reason: 'PodFailed' },
|
|
66
|
+
{ type: 'ContainersReady', status: 'False' },
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
expect(html).toContain('Conditions (3) · 3 failing')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('does not let a completion reason hide readiness failures on a failed Pod', () => {
|
|
73
|
+
expect(renderConditions('Failed')).toContain('Conditions (5) · 3 failing')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('retains unrelated false conditions after successful completion', () => {
|
|
77
|
+
const html = renderConditions('Succeeded', [
|
|
78
|
+
...completionConditions,
|
|
79
|
+
{ type: 'example.com/controller-ready', status: 'False', reason: 'ControllerFailed' },
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
expect(html).toContain('Conditions (6) · 1 failing')
|
|
83
|
+
expect(html).toContain('example.com/controller-ready')
|
|
84
|
+
expect(html).toContain('ControllerFailed')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
24
88
|
describe('PodRenderer envFrom expansion', () => {
|
|
25
89
|
it('keeps same-name ConfigMap and Secret values separate', () => {
|
|
26
90
|
const resolvedEnvFrom: ResolvedEnvFrom = {
|
|
@@ -482,3 +546,54 @@ it('shows reported running-but-unready nginx readiness as degraded', () => {
|
|
|
482
546
|
expect(html).toMatch(/bg-amber-100[^>]*>Not Ready</)
|
|
483
547
|
expect(html).toMatch(/>running</)
|
|
484
548
|
})
|
|
549
|
+
|
|
550
|
+
describe('finished Pod resource usage',()=>{
|
|
551
|
+
it.each(['Succeeded','Failed'])('explains ended live usage for %s without cluster repair advice',phase=>{
|
|
552
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase}}} onCopy={()=>{}} copied={null} metricsUnavailable metricsHistory={{containers:[],metricsUnavailableReason:'not found'}} />)
|
|
553
|
+
expect(html).toContain('Live resource usage has ended for this Pod')
|
|
554
|
+
expect(html).toContain('No recorded usage is available.')
|
|
555
|
+
expect(html).not.toContain('Metrics collection error:')
|
|
556
|
+
expect(html).not.toContain('Radar cannot read metrics.k8s.io')
|
|
557
|
+
expect(html).not.toContain('No usage yet: this Pod is not running')
|
|
558
|
+
})
|
|
559
|
+
it('keeps a genuine collector error qualified as current on a finished Pod',()=>{
|
|
560
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase:'Succeeded'}}} onCopy={()=>{}} copied={null} metricsHistory={{containers:[],collectionError:'forbidden: list pods.metrics.k8s.io'}} />)
|
|
561
|
+
expect(html).toContain('Current cluster metrics collection error:')
|
|
562
|
+
expect(html).toContain('forbidden: list pods.metrics.k8s.io')
|
|
563
|
+
expect(html).toContain('Live resource usage has ended for this Pod')
|
|
564
|
+
})
|
|
565
|
+
it('keeps recorded history and hides cached live usage after completion',()=>{
|
|
566
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase:'Succeeded'}}} onCopy={()=>{}} copied={null}
|
|
567
|
+
metrics={{timestamp:'2026-10-25T12:00:00Z',containers:[{name:'cached-live',usage:{cpu:'100m',memory:'1Mi'}}]}}
|
|
568
|
+
metricsHistory={{containers:[{name:'recorded-history',dataPoints:[{timestamp:'2026-10-25T11:59:00Z',cpu:100,memory:1024}]}]}} />)
|
|
569
|
+
expect(html).toContain('recorded-history')
|
|
570
|
+
expect(html).not.toContain('cached-live')
|
|
571
|
+
expect(html).not.toContain('Last updated:')
|
|
572
|
+
})
|
|
573
|
+
it('distinguishes loading from a loaded empty terminal history',()=>{
|
|
574
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase:'Succeeded'}}} onCopy={()=>{}} copied={null} metricsHistoryLoading />)
|
|
575
|
+
expect(html).toContain('Loading recorded usage...')
|
|
576
|
+
expect(html).not.toContain('No recorded usage is available.')
|
|
577
|
+
})
|
|
578
|
+
it('shows a failed terminal history fetch without claiming empty history',()=>{
|
|
579
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase:'Failed'}}} onCopy={()=>{}} copied={null} metricsHistoryError={new Error('Metrics history not available')} />)
|
|
580
|
+
expect(html).toContain('Recorded usage could not be loaded')
|
|
581
|
+
expect(html).toContain('Metrics history not available')
|
|
582
|
+
expect(html).not.toContain('No recorded usage is available.')
|
|
583
|
+
})
|
|
584
|
+
it('retains cached history when its refresh fails',()=>{
|
|
585
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase:'Succeeded'}}} onCopy={()=>{}} copied={null}
|
|
586
|
+
metricsHistory={{containers:[{name:'recorded-history',dataPoints:[{timestamp:'2026-10-25T11:59:00Z',cpu:100,memory:1024}]}]}}
|
|
587
|
+
metricsHistoryError={new Error('Network request failed')} />)
|
|
588
|
+
expect(html).toContain('Recorded usage could not be refreshed')
|
|
589
|
+
expect(html).toContain('recorded-history')
|
|
590
|
+
expect(html).not.toContain('No recorded usage is available.')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('keeps usage absent when the host wires no metrics state',()=>{
|
|
594
|
+
const html=renderToString(<PodRenderer data={{...pod,status:{phase:'Succeeded'}}} onCopy={()=>{}} copied={null} />)
|
|
595
|
+
expect(html).not.toContain('Resource Usage')
|
|
596
|
+
expect(html).not.toContain('Live resource usage has ended')
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
})
|
|
@@ -87,6 +87,8 @@ interface PodRendererProps {
|
|
|
87
87
|
// Metrics data injection
|
|
88
88
|
metrics?: { containers?: any[]; timestamp?: string }
|
|
89
89
|
metricsHistory?: { metricsAPIReachable?: boolean; containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
|
|
90
|
+
metricsHistoryLoading?: boolean
|
|
91
|
+
metricsHistoryError?: Error | null
|
|
90
92
|
metricsUnavailable?: boolean
|
|
91
93
|
hideMetricsServer?: boolean
|
|
92
94
|
// Filesystem browser render props
|
|
@@ -286,6 +288,8 @@ export function PodRenderer({
|
|
|
286
288
|
renderPortAction,
|
|
287
289
|
metrics,
|
|
288
290
|
metricsHistory,
|
|
291
|
+
metricsHistoryLoading,
|
|
292
|
+
metricsHistoryError,
|
|
289
293
|
metricsUnavailable,
|
|
290
294
|
hideMetricsServer,
|
|
291
295
|
renderImageBrowser,
|
|
@@ -320,6 +324,7 @@ export function PodRenderer({
|
|
|
320
324
|
const namespace = data.metadata?.namespace
|
|
321
325
|
const podName = data.metadata?.name
|
|
322
326
|
const isRunning = data.status?.phase === 'Running'
|
|
327
|
+
const isTerminal = data.status?.phase === 'Succeeded' || data.status?.phase === 'Failed'
|
|
323
328
|
|
|
324
329
|
// Check for problems. Suppressed when the detail already shows the dedicated
|
|
325
330
|
// Operational Issues section (the Issues pipeline covers the same pod failures,
|
|
@@ -329,9 +334,10 @@ export function PodRenderer({
|
|
|
329
334
|
const hasProblems = podProblems.length > 0 && !operationalIssuesShown
|
|
330
335
|
const notRunning = data.status?.phase === 'Pending' && ![...containerStatuses, ...initContainerStatuses].some((s: any) => s.state?.running)
|
|
331
336
|
const noUsageYet = notRunning && metricsHistory?.metricsAPIReachable === true && !metricsHistory.collectionError && !metricsHistory.containers?.length && !metrics?.containers?.length
|
|
332
|
-
const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError && !noUsageYet
|
|
337
|
+
const showMetricsUnavailable = !isTerminal && !!metricsUnavailable && !metricsHistory?.collectionError && !noUsageYet
|
|
333
338
|
const hasMetricsHistory = !!metricsHistory?.containers?.length
|
|
334
|
-
const
|
|
339
|
+
const showTerminalUsage = isTerminal && (metricsHistory !== undefined || metricsHistoryLoading || !!metricsHistoryError || !!metrics || metricsUnavailable)
|
|
340
|
+
const currentMetrics = isTerminal || metricsUnavailable ? undefined : metrics
|
|
335
341
|
|
|
336
342
|
// Image filesystem modal state
|
|
337
343
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
|
@@ -872,15 +878,30 @@ export function PodRenderer({
|
|
|
872
878
|
)}
|
|
873
879
|
|
|
874
880
|
{/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
|
|
875
|
-
{!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable || noUsageYet) && (
|
|
881
|
+
{!hideMetricsServer && !!(showTerminalUsage || currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable || noUsageYet) && (
|
|
876
882
|
<Section title="Resource Usage" icon={Activity} defaultExpanded>
|
|
883
|
+
{isTerminal && (
|
|
884
|
+
<div className="mb-3 text-sm text-theme-text-tertiary">
|
|
885
|
+
Live resource usage has ended for this Pod.{metricsHistory && !hasMetricsHistory && !metricsHistoryLoading && !metricsHistoryError && ' No recorded usage is available.'}
|
|
886
|
+
</div>
|
|
887
|
+
)}
|
|
888
|
+
{isTerminal && metricsHistoryLoading && (
|
|
889
|
+
<p className="mb-3 text-sm text-theme-text-tertiary">Loading recorded usage...</p>
|
|
890
|
+
)}
|
|
891
|
+
{isTerminal && metricsHistoryError && (
|
|
892
|
+
<AlertBanner
|
|
893
|
+
variant="warning"
|
|
894
|
+
title={metricsHistory ? 'Recorded usage could not be refreshed' : 'Recorded usage could not be loaded'}
|
|
895
|
+
message={metricsHistoryError.message}
|
|
896
|
+
/>
|
|
897
|
+
)}
|
|
877
898
|
{noUsageYet && <MetricsUnavailableNotice noUsageYet />}
|
|
878
899
|
{showMetricsUnavailable && (
|
|
879
900
|
<MetricsUnavailableNotice rawError={metricsHistory?.metricsUnavailableReason} diagnosis={metricsHistory?.metricsUnavailableDiagnosis} />
|
|
880
901
|
)}
|
|
881
902
|
{metricsHistory?.collectionError && (
|
|
882
903
|
<div className="mb-3 rounded-lg border border-yellow-500/30 bg-yellow-500/5 px-3 py-2 text-xs text-yellow-400">
|
|
883
|
-
<span className="font-medium">Metrics collection error
|
|
904
|
+
<span className="font-medium">{isTerminal ? 'Current cluster metrics collection error:' : 'Metrics collection error:'}</span>{' '}
|
|
884
905
|
<span className="break-all">{metricsHistory.collectionError}</span>
|
|
885
906
|
</div>
|
|
886
907
|
)}
|
|
@@ -970,14 +991,16 @@ export function PodRenderer({
|
|
|
970
991
|
</Section>
|
|
971
992
|
)}
|
|
972
993
|
|
|
973
|
-
{/*
|
|
974
|
-
|
|
975
|
-
so tone it neutral (gray) instead of red. Gated on the PodCompleted reason
|
|
976
|
-
so a genuinely not-ready pod (any other reason) still reads red. */}
|
|
994
|
+
{/* Sandbox teardown and lost serving readiness are expected after success;
|
|
995
|
+
custom conditions retain their own meaning. */}
|
|
977
996
|
<ConditionsSection
|
|
978
997
|
conditions={data.status?.conditions}
|
|
979
998
|
getConditionTone={(cond) =>
|
|
980
|
-
|
|
999
|
+
data.status?.phase === 'Succeeded'
|
|
1000
|
+
&& cond?.status === 'False'
|
|
1001
|
+
&& ['Ready', 'ContainersReady', 'PodReadyToStartContainers'].includes(cond?.type)
|
|
1002
|
+
? 'unknown'
|
|
1003
|
+
: undefined
|
|
981
1004
|
}
|
|
982
1005
|
/>
|
|
983
1006
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
getXRDKind,
|
|
22
22
|
} from '../resource-utils-crossplane'
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
export function CrossplaneStatusCell({ resource, getStatus = getCrossplaneStatus }: { resource: any; getStatus?: (r: any) => { text: string; color: string } }) {
|
|
25
25
|
const status = getStatus(resource)
|
|
26
26
|
const reason = getCrossplaneStatusReason(resource)
|
|
27
27
|
const badge = (
|
|
@@ -35,7 +35,7 @@ function StatusBadge({ resource, getStatus }: { resource: any; getStatus: (r: an
|
|
|
35
35
|
export function ManagedResourceCell({ resource, column }: { resource: any; column: string }) {
|
|
36
36
|
switch (column) {
|
|
37
37
|
case 'status':
|
|
38
|
-
return <
|
|
38
|
+
return <CrossplaneStatusCell resource={resource} getStatus={getCrossplaneStatus} />
|
|
39
39
|
case 'external': {
|
|
40
40
|
const name = getExternalName(resource)
|
|
41
41
|
if (!name) return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
@@ -60,7 +60,7 @@ export function ManagedResourceCell({ resource, column }: { resource: any; colum
|
|
|
60
60
|
export function CompositeResourceCell({ resource, column }: { resource: any; column: string }) {
|
|
61
61
|
switch (column) {
|
|
62
62
|
case 'status':
|
|
63
|
-
return <
|
|
63
|
+
return <CrossplaneStatusCell resource={resource} getStatus={getCrossplaneStatus} />
|
|
64
64
|
case 'composition': {
|
|
65
65
|
const ref = getCompositionRef(resource)
|
|
66
66
|
if (!ref) return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
@@ -82,7 +82,7 @@ export function CompositeResourceCell({ resource, column }: { resource: any; col
|
|
|
82
82
|
export function CrossplaneProviderCell({ resource, column }: { resource: any; column: string }) {
|
|
83
83
|
switch (column) {
|
|
84
84
|
case 'status':
|
|
85
|
-
return <
|
|
85
|
+
return <CrossplaneStatusCell resource={resource} getStatus={getProviderStatus} />
|
|
86
86
|
case 'package': {
|
|
87
87
|
const pkg = getProviderPackage(resource)
|
|
88
88
|
return (
|
|
@@ -108,7 +108,7 @@ export function CrossplaneProviderCell({ resource, column }: { resource: any; co
|
|
|
108
108
|
export function CrossplaneProviderConfigCell({ resource, column }: { resource: any; column: string }) {
|
|
109
109
|
switch (column) {
|
|
110
110
|
case 'status':
|
|
111
|
-
return <
|
|
111
|
+
return <CrossplaneStatusCell resource={resource} getStatus={getProviderConfigStatus} />
|
|
112
112
|
case 'credentials':
|
|
113
113
|
return <span className="text-sm text-theme-text-secondary">{getProviderConfigCredentialsSource(resource)}</span>
|
|
114
114
|
default:
|
|
@@ -192,3 +192,6 @@ export { RayServiceRenderer } from './RayServiceRenderer'
|
|
|
192
192
|
export { RayClusterRenderer } from './RayClusterRenderer'
|
|
193
193
|
|
|
194
194
|
export { RayJobRenderer } from './RayJobRenderer'
|
|
195
|
+
|
|
196
|
+
export { OpenTelemetryCollectorRenderer, OpenTelemetryInstrumentationRenderer } from './OpenTelemetryRenderers'
|
|
197
|
+
export { FlaggerCanaryRenderer, FlaggerMetricTemplateRenderer } from './FlaggerRenderers'
|
|
@@ -675,3 +675,48 @@ describe('isCrossplanePaused', () => {
|
|
|
675
675
|
|
|
676
676
|
// Pluralization moved to the shared kindToPlural in utils/navigation.ts
|
|
677
677
|
// (discovery-aware, handles provider-defined irregular plurals).
|
|
678
|
+
|
|
679
|
+
describe('Crossplane pause and independent status evidence', () => {
|
|
680
|
+
function paused(conditions: any[], annotation = 'true') {
|
|
681
|
+
return {
|
|
682
|
+
metadata: { generation: 9, annotations: { 'crossplane.io/paused': annotation } },
|
|
683
|
+
spec: { providerConfigRef: { name: 'default' } },
|
|
684
|
+
status: { conditions },
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const pause = { type: 'Synced', status: 'False', reason: 'ReconcilePaused', message: 'pause observation', observedGeneration: 1 }
|
|
688
|
+
|
|
689
|
+
it('keeps acknowledged pause neutral with recorded availability despite generation changes', () => {
|
|
690
|
+
const resource = paused([{ type: 'Ready', status: 'True' }, pause])
|
|
691
|
+
expect(getCrossplaneStatus(resource)).toMatchObject({ text: 'Ready (reported)', level: 'neutral' })
|
|
692
|
+
expect(getCrossplaneStatusReason(resource)).toBeNull()
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it.each([undefined, 'Unknown'])('does not invent provisioning or readiness when acknowledged pause has Ready=%s', status => {
|
|
696
|
+
const resource = paused([...(status ? [{ type: 'Ready', status }] : []), pause])
|
|
697
|
+
expect(getCrossplaneStatus(resource)).toMatchObject({ text: 'Readiness unknown', level: 'neutral' })
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('preserves the independent Ready failure and its message over the pause observation', () => {
|
|
701
|
+
const resource = paused([pause, { type: 'Ready', status: 'False', reason: 'Unavailable', message: 'external API access denied' }])
|
|
702
|
+
expect(getCrossplaneStatus(resource)).toMatchObject({ text: 'Unavailable', level: 'unhealthy' })
|
|
703
|
+
expect(getCrossplaneStatusReason(resource)).toBe('external API access denied')
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
it('does not mask a reconcile error with requested pause', () => {
|
|
707
|
+
const resource = paused([{ type: 'Ready', status: 'True' }, { ...pause, reason: 'ReconcileError', message: 'credentials rejected' }])
|
|
708
|
+
expect(getCrossplaneStatus(resource)).toMatchObject({ text: 'Out of sync', level: 'alert' })
|
|
709
|
+
expect(getCrossplaneStatusReason(resource)).toBe('credentials rejected')
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
it('keeps uncorroborated controller pause as an alert without inventing a failed resume', () => {
|
|
713
|
+
const resource = paused([pause], 'false')
|
|
714
|
+
expect(getCrossplaneStatus(resource)).toMatchObject({ text: 'ReconcilePaused', level: 'alert' })
|
|
715
|
+
expect(getCrossplaneStatusReason(resource)).toBe('pause observation')
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it('preserves terminating over pause and recorded readiness', () => {
|
|
719
|
+
const resource = paused([{ type: 'Ready', status: 'True' }, pause])
|
|
720
|
+
expect(getCrossplaneStatus({ ...resource, metadata: { ...resource.metadata, deletionTimestamp: '2026-10-26T08:00:00Z' } })).toMatchObject({ text: 'Terminating', level: 'degraded' })
|
|
721
|
+
})
|
|
722
|
+
})
|