@skyhook-io/k8s-ui 1.16.2 → 1.17.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/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
- package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
- package/src/components/cnpg/index.ts +14 -0
- package/src/components/cnpg/primitives.tsx +134 -0
- package/src/components/cnpg/relations.test.ts +304 -0
- package/src/components/cnpg/relations.ts +417 -0
- package/src/components/cnpg/workspace.test.ts +285 -0
- package/src/components/cnpg/workspace.ts +700 -0
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/IssuesView.tsx +10 -1
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/issues/types.ts +24 -0
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
- package/src/components/logs/useLogBuffer.test.ts +17 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
- package/src/components/resources/ResourcesSidebar.tsx +183 -11
- package/src/components/resources/ResourcesView.tsx +12 -9
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.ts +3 -7
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/resources/status-overclaim.test.ts +1 -1
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +187 -38
- package/src/components/timeline/TimelineListFold.test.tsx +56 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-filters.ts +7 -0
- package/src/components/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +216 -48
- package/src/components/workload/index.ts +1 -0
- package/src/components/workload/workload-logs-availability.test.ts +6 -0
- package/src/index.ts +3 -0
- package/src/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/log-level.ts +14 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
} from './shared'
|
|
63
63
|
import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
|
|
64
64
|
import { clampLensToSelection, formatLensDuration, MIN_WINDOW_MS, type ScrubberRange } from './scrubber-math'
|
|
65
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
65
66
|
|
|
66
67
|
// Predefined zoom levels (window widths in hours): 15m, 30m, 1h, 2h, 4h, 8h,
|
|
67
68
|
// 12h, 1d, 2d, 3d, 7d. Hoisted so the controlled-window adapter can size the
|
|
@@ -2884,15 +2885,6 @@ function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarker
|
|
|
2884
2885
|
const markerClasses = getMarkerStyle()
|
|
2885
2886
|
|
|
2886
2887
|
// Build tooltip text - focus on what happened, explain the color meaning
|
|
2887
|
-
const getRelativeTime = (timestamp: string) => {
|
|
2888
|
-
const diff = Date.now() - new Date(timestamp).getTime()
|
|
2889
|
-
const mins = Math.floor(diff / 60000)
|
|
2890
|
-
if (mins < 1) return 'just now'
|
|
2891
|
-
if (mins < 60) return `${mins}m ago`
|
|
2892
|
-
const hours = Math.floor(mins / 60)
|
|
2893
|
-
if (hours < 24) return `${hours}h ago`
|
|
2894
|
-
return `${Math.floor(hours / 24)}d ago`
|
|
2895
|
-
}
|
|
2896
2888
|
|
|
2897
2889
|
// Get human-readable operation label with color indicator
|
|
2898
2890
|
const getOperationLabel = () => {
|
|
@@ -2922,7 +2914,7 @@ function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarker
|
|
|
2922
2914
|
const msg = event.message.length > 110 ? event.message.slice(0, 110) + '…' : event.message
|
|
2923
2915
|
tooltipLines.push(msg)
|
|
2924
2916
|
}
|
|
2925
|
-
tooltipLines.push(
|
|
2917
|
+
tooltipLines.push(formatRelativeAgeTime(event.timestamp))
|
|
2926
2918
|
if (isHistoricalEvent(event)) tooltipLines.push('(from metadata)')
|
|
2927
2919
|
|
|
2928
2920
|
const tooltipText = tooltipLines.join(' · ')
|
|
@@ -86,3 +86,101 @@ describe('TimelineSwimlanes controlled vs fallback', () => {
|
|
|
86
86
|
expect(html).toContain('>View<')
|
|
87
87
|
})
|
|
88
88
|
})
|
|
89
|
+
|
|
90
|
+
describe('TimelineList routine activity toggle', () => {
|
|
91
|
+
const at = (id: string, over: Partial<TimelineEvent>): TimelineEvent => ({
|
|
92
|
+
...EVENTS[0], id, timestamp: new Date(NOW).toISOString(), ...over,
|
|
93
|
+
})
|
|
94
|
+
const ROUTINE: TimelineEvent[] = [
|
|
95
|
+
at('pod-started', { kind: 'Pod', name: 'web-1', source: 'k8s_event', eventType: 'Normal', reason: 'Started' }),
|
|
96
|
+
at('pod-pulled', { kind: 'Pod', name: 'web-2', source: 'k8s_event', eventType: 'Normal', reason: 'Pulled' }),
|
|
97
|
+
at('rs-scaled', { kind: 'ReplicaSet', name: 'web-5f86', eventType: 'update' }),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
it('shows the checkbox with the hidden count and the (i) explanation', () => {
|
|
101
|
+
const html = renderToString(
|
|
102
|
+
<TimelineList events={EVENTS} isLoading={false} routineEvents={ROUTINE} onShowRoutineChange={() => {}} />,
|
|
103
|
+
)
|
|
104
|
+
expect(html).toContain('Show routine activity')
|
|
105
|
+
expect(html).toContain('type="checkbox"')
|
|
106
|
+
expect(html).toContain('· <!-- -->3 hidden')
|
|
107
|
+
expect(html).toContain('aria-label="About routine activity"')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('counts only hidden rows the active filters would show', () => {
|
|
111
|
+
const html = renderToString(
|
|
112
|
+
<TimelineList
|
|
113
|
+
events={EVENTS}
|
|
114
|
+
isLoading={false}
|
|
115
|
+
kindFilter={['Pod']}
|
|
116
|
+
onKindFilterChange={() => {}}
|
|
117
|
+
routineEvents={ROUTINE}
|
|
118
|
+
onShowRoutineChange={() => {}}
|
|
119
|
+
/>,
|
|
120
|
+
)
|
|
121
|
+
expect(html).toContain('2 routine events<!-- --> <!-- -->match')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('says none hidden when nothing matching is routine', () => {
|
|
125
|
+
const html = renderToString(
|
|
126
|
+
<TimelineList events={EVENTS} isLoading={false} search="web" onSearchChange={() => {}} routineEvents={[]} onShowRoutineChange={() => {}} />,
|
|
127
|
+
)
|
|
128
|
+
expect(html).toContain('· <!-- -->none hidden')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('reads shown, with the count, once the user opts in', () => {
|
|
132
|
+
const html = renderToString(
|
|
133
|
+
<TimelineList events={[...EVENTS, ...ROUTINE]} isLoading={false} showRoutine routineEvents={ROUTINE} onShowRoutineChange={() => {}} />,
|
|
134
|
+
)
|
|
135
|
+
expect(html).toContain('checked=""')
|
|
136
|
+
expect(html).toContain('· <!-- -->3 shown')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('explains an empty list whose matching rows are all routine', () => {
|
|
140
|
+
const html = renderToString(
|
|
141
|
+
<TimelineList events={[]} isLoading={false} routineEvents={ROUTINE} onShowRoutineChange={() => {}} />,
|
|
142
|
+
)
|
|
143
|
+
expect(html).toContain('All matching activity is routine')
|
|
144
|
+
expect(html).toContain('3 routine events<!-- --> <!-- -->match<!-- -->. Warnings and failures would show here.')
|
|
145
|
+
expect(html).toContain('Show routine activity')
|
|
146
|
+
expect(html).not.toContain('No activity found')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('has no toggle when the host does not offer it', () => {
|
|
150
|
+
const html = renderToString(<TimelineList events={EVENTS} isLoading={false} />)
|
|
151
|
+
expect(html).not.toContain('routine activity')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('folds a resource\'s rows under its newest card, describing the rest', () => {
|
|
155
|
+
const crash = (id: string, minutesAgo: number, over: Partial<TimelineEvent>) =>
|
|
156
|
+
at(id, { kind: 'Pod', name: 'api-1', timestamp: new Date(NOW - minutesAgo * 60_000).toISOString(), ...over })
|
|
157
|
+
const rows = [
|
|
158
|
+
crash('newest', 0, { eventType: 'update', healthState: 'unhealthy', owner: { kind: 'ReplicaSet', name: 'api-5f86' } }),
|
|
159
|
+
crash('b1', 1, { source: 'k8s_event', eventType: 'Warning', reason: 'BackOff' }),
|
|
160
|
+
crash('b2', 2, { source: 'k8s_event', eventType: 'Warning', reason: 'BackOff' }),
|
|
161
|
+
crash('u1', 3, { eventType: 'update', healthState: 'degraded' }),
|
|
162
|
+
]
|
|
163
|
+
const folded = renderToString(<TimelineList events={rows} isLoading={false} foldPerResource={(e) => e.kind === 'Pod'} />)
|
|
164
|
+
expect(folded).toContain('3<!-- --> more on this <!-- -->Pod<!-- -->: <!-- -->BackOff ×2, 1 status change')
|
|
165
|
+
expect(folded).toContain('deploy<!-- -->/</span>api')
|
|
166
|
+
expect(folded).toContain('data-event-id="newest"')
|
|
167
|
+
const plain = renderToString(<TimelineList events={rows} isLoading={false} />)
|
|
168
|
+
expect(plain).not.toContain('more on this')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('counts each event\'s occurrences in the fold summary', () => {
|
|
172
|
+
const crash = (id: string, minutesAgo: number, over: Partial<TimelineEvent>) =>
|
|
173
|
+
at(id, { kind: 'Pod', name: 'api-1', timestamp: new Date(NOW - minutesAgo * 60_000).toISOString(), ...over })
|
|
174
|
+
const rows = [
|
|
175
|
+
crash('newest', 0, { eventType: 'update', healthState: 'unhealthy' }),
|
|
176
|
+
crash('backoff', 1, { source: 'k8s_event', eventType: 'Warning', reason: 'BackOff', count: 43 }),
|
|
177
|
+
]
|
|
178
|
+
const html = renderToString(<TimelineList events={rows} isLoading={false} foldPerResource={() => true} />)
|
|
179
|
+
expect(html).toContain('BackOff ×43')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('keeps the (i) explanation reachable by keyboard', () => {
|
|
183
|
+
const html = renderToString(<TimelineList events={EVENTS} isLoading={false} routineEvents={[]} onShowRoutineChange={() => {}} />)
|
|
184
|
+
expect(html).toContain('<button type="button" aria-label="About routine activity"')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './shared'
|
|
2
2
|
export * from './DiffViewer'
|
|
3
3
|
export type { ActivityTypeFilter, ActivityFilterKey } from './timeline-filters'
|
|
4
|
+
export { isTimelineProblem } from './timeline-filters'
|
|
4
5
|
export type { TimelineSort } from './timeline-lane-sort'
|
|
5
6
|
export * from './TimelineList'
|
|
6
7
|
export * from './TimelineSwimlanes'
|
|
@@ -69,6 +69,13 @@ export function selectionToActivityKeys(sel: ActivitySelection): ActivityFilterK
|
|
|
69
69
|
return []
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// A row the Problems chip counts. Hosts that hide routine activity must never
|
|
73
|
+
// hide these, so the Problems count and the list stay truthful.
|
|
74
|
+
const PROBLEM_KEYS = selectionToActivityKeys({ source: 'all', problemsOnly: true })
|
|
75
|
+
export function isTimelineProblem(event: TimelineEvent): boolean {
|
|
76
|
+
return matchesActivityFilter(event, PROBLEM_KEYS)
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
// Inverse of selectionToActivityKeys for the six canonical states. Legacy
|
|
73
80
|
// multi-select key sets (pre-two-axis URLs) fall back to the widest reading —
|
|
74
81
|
// showing more than a stale link intended beats silently hiding activity.
|
|
@@ -46,7 +46,8 @@ const EDGE_COLORS = {
|
|
|
46
46
|
'exposes': '#3b82f6', // Blue for service exposure
|
|
47
47
|
'manages': '#64748b', // Gray for management relationships
|
|
48
48
|
'configures': '#f59e0b', // Amber for config
|
|
49
|
-
'uses': '#ec4899', // Pink
|
|
49
|
+
'uses': '#ec4899', // Pink: autoscalers, mounted claims, dependencies
|
|
50
|
+
'protects': '#8b5cf6', // Violet for policies and disruption budgets
|
|
50
51
|
} as const
|
|
51
52
|
|
|
52
53
|
function getEdgeColor(type: string, isTrafficView: boolean): string {
|
|
@@ -59,10 +60,11 @@ function getEdgeColor(type: string, isTrafficView: boolean): string {
|
|
|
59
60
|
|
|
60
61
|
// Human-readable edge legend for the resources view (traffic view is all-green).
|
|
61
62
|
const EDGE_LEGEND: { label: string; color: string }[] = [
|
|
62
|
-
{ label: '
|
|
63
|
+
{ label: 'manages', color: EDGE_COLORS['manages'] },
|
|
63
64
|
{ label: 'exposes', color: EDGE_COLORS['exposes'] },
|
|
64
65
|
{ label: 'configures', color: EDGE_COLORS['configures'] },
|
|
65
|
-
{ label: '
|
|
66
|
+
{ label: 'uses', color: EDGE_COLORS['uses'] },
|
|
67
|
+
{ label: 'protects', color: EDGE_COLORS['protects'] },
|
|
66
68
|
{ label: 'routes to', color: EDGE_COLORS['routes-to'] },
|
|
67
69
|
]
|
|
68
70
|
|
|
@@ -74,6 +74,11 @@ describe('TimeValue', () => {
|
|
|
74
74
|
expect(html).not.toMatch(/\dT\d\d:\d\d:\d\dZ/)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
+
// A bare "6h" under "Started" or "Finished" reads as a duration.
|
|
78
|
+
it('says the moment was in the past', () => {
|
|
79
|
+
expect(render(<TimeValue timestamp={new Date(Date.now() - 6 * 3600_000).toISOString()} />)).toContain('6h ago')
|
|
80
|
+
})
|
|
81
|
+
|
|
77
82
|
it('says so when the thing never happened', () => {
|
|
78
83
|
expect(render(<TimeValue timestamp={undefined} fallback="Never" />)).toContain('Never')
|
|
79
84
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tooltip } from './Tooltip'
|
|
2
|
-
import { cronToHuman
|
|
2
|
+
import { cronToHuman } from '../resources/resource-utils'
|
|
3
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Hover affordance used across the drawer: without it a tooltip is a secret.
|
|
@@ -79,7 +80,7 @@ export function TimeValue({ timestamp, fallback = '-' }: TimeValueProps) {
|
|
|
79
80
|
if (!timestamp) return <>{fallback}</>
|
|
80
81
|
return (
|
|
81
82
|
<Tooltip content={new Date(timestamp).toLocaleString()} position="right">
|
|
82
|
-
<span className={HOVERABLE}>{
|
|
83
|
+
<span className={HOVERABLE}>{formatRelativeAgeTime(timestamp)}</span>
|
|
83
84
|
</Tooltip>
|
|
84
85
|
)
|
|
85
86
|
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { EventsSection, ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection } from './drawer-components'
|
|
3
|
+
import { EventsSection, ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection, defaultConditionTone } from './drawer-components'
|
|
4
4
|
|
|
5
5
|
const problems = [
|
|
6
6
|
{ color: 'red' as const, message: 'Application is Degraded' },
|
|
7
7
|
{ color: 'yellow' as const, message: 'Application is OutOfSync' },
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
+
describe('GKE node condition polarity', () => {
|
|
11
|
+
it.each(['StoragePressureRootFileSystem', 'DPv2MigrationUnsupportedCNI', 'UnsupportedEBPFPrograms'])('%s is a problem only when True', type => {
|
|
12
|
+
expect(defaultConditionTone({ type, status: 'False' })).toBe('ok')
|
|
13
|
+
expect(defaultConditionTone({ type, status: 'True' })).toBe('fail')
|
|
14
|
+
expect(defaultConditionTone({ type, status: 'Unknown' })).toBe('unknown')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
10
18
|
describe('ProblemAlerts', () => {
|
|
11
19
|
it('renders every problem', () => {
|
|
12
20
|
const html = renderToString(<ProblemAlerts problems={problems} />)
|
|
@@ -62,3 +70,60 @@ describe('Recent Events layout', () => {
|
|
|
62
70
|
expect(fullscreen).toContain('Reason23')
|
|
63
71
|
})
|
|
64
72
|
})
|
|
73
|
+
|
|
74
|
+
describe('dependency relationships', () => {
|
|
75
|
+
it('renders a dependency-only response with the existing navigation control', () => {
|
|
76
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ dependencies: [{ kind: 'Issuer', group: 'cert-manager.io', namespace: 'team', name: 'ca' }] }} onNavigate={() => {}} />)
|
|
77
|
+
expect(html).toContain('Depends On')
|
|
78
|
+
expect(html).toContain('ca')
|
|
79
|
+
expect(html).toContain('<button')
|
|
80
|
+
expect(html).not.toContain('Scale Target')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('renders the reverse dependency without calling it an autoscaler', () => {
|
|
84
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ dependents: [{ kind: 'Certificate', group: 'cert-manager.io', namespace: 'team', name: 'tls' }] }} />)
|
|
85
|
+
expect(html).toContain('Required By')
|
|
86
|
+
expect(html).toContain('tls')
|
|
87
|
+
expect(html).not.toContain('Autoscaler')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('versioned dependency projections', () => {
|
|
92
|
+
const issuer = { kind: 'Issuer', group: 'cert-manager.io', namespace: 'team', name: 'ca' }
|
|
93
|
+
const certificate = { kind: 'Certificate', group: 'cert-manager.io', namespace: 'team', name: 'tls' }
|
|
94
|
+
|
|
95
|
+
it('presents mirrored configuration dependencies once under the specific labels', () => {
|
|
96
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ dependencies: [issuer], configRefs: [issuer], dependents: [certificate], consumers: [certificate] }} onNavigate={() => {}} />)
|
|
97
|
+
expect(html).toContain('Depends On')
|
|
98
|
+
expect(html).toContain('Required By')
|
|
99
|
+
expect(html).not.toContain('Configuration')
|
|
100
|
+
expect(html).not.toContain('Used By')
|
|
101
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('preserves configuration and consumer groups when dependency fields are absent', () => {
|
|
105
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ configRefs: [issuer], consumers: [certificate] }} />)
|
|
106
|
+
expect(html).toContain('Configuration')
|
|
107
|
+
expect(html).toContain('Used By')
|
|
108
|
+
expect(html).not.toContain('Depends On')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('retains navigation controls for identically named resources from distinct API groups', () => {
|
|
112
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ dependencies: [issuer, { ...issuer, group: 'other.example.com' }, issuer] }} onNavigate={() => {}} />)
|
|
113
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('namespace deletion condition polarity', () => {
|
|
118
|
+
it.each([
|
|
119
|
+
'NamespaceDeletionDiscoveryFailure',
|
|
120
|
+
'NamespaceDeletionGroupVersionParsingFailure',
|
|
121
|
+
'NamespaceDeletionContentFailure',
|
|
122
|
+
'NamespaceContentRemaining',
|
|
123
|
+
'NamespaceFinalizersRemaining',
|
|
124
|
+
])('%s treats True as a blocker and False as cleared', (type) => {
|
|
125
|
+
expect(defaultConditionTone({ type, status: 'True' })).toBe('fail')
|
|
126
|
+
expect(defaultConditionTone({ type, status: 'False' })).toBe('ok')
|
|
127
|
+
expect(defaultConditionTone({ type, status: 'Unknown' })).toBe('unknown')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { dedupeResourceRefs, omitResourceRefs } from '../../utils/resource-refs'
|
|
1
2
|
import { Disclosure } from './Disclosure'
|
|
2
3
|
import { createContext, useContext, useState } from 'react'
|
|
3
4
|
import { Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
|
|
4
5
|
import { clsx } from 'clsx'
|
|
5
|
-
import {
|
|
6
|
+
import { formatDuration, formatResources } from '../resources/resource-utils'
|
|
6
7
|
import { getEffectiveResources } from '../../utils/extended-resources'
|
|
7
8
|
import { Tooltip } from './Tooltip'
|
|
8
9
|
import { getKindColorClass } from '../ui/Badge'
|
|
@@ -206,6 +207,12 @@ const NEGATIVE_POLARITY_TYPES = new Set([
|
|
|
206
207
|
'NetworkUnavailable',
|
|
207
208
|
// Core K8s — Pod
|
|
208
209
|
'DisruptionTarget',
|
|
210
|
+
// Core K8s — Namespace
|
|
211
|
+
'NamespaceDeletionDiscoveryFailure',
|
|
212
|
+
'NamespaceDeletionGroupVersionParsingFailure',
|
|
213
|
+
'NamespaceDeletionContentFailure',
|
|
214
|
+
'NamespaceContentRemaining',
|
|
215
|
+
'NamespaceFinalizersRemaining',
|
|
209
216
|
// Core K8s — workloads
|
|
210
217
|
'ReplicaFailure',
|
|
211
218
|
// NPD — upstream default configs
|
|
@@ -220,6 +227,9 @@ const NEGATIVE_POLARITY_TYPES = new Set([
|
|
|
220
227
|
'ReadOnlyRootFileSystem',
|
|
221
228
|
'ResourceExhausted',
|
|
222
229
|
'Swap',
|
|
230
|
+
'StoragePressureRootFileSystem',
|
|
231
|
+
'DPv2MigrationUnsupportedCNI',
|
|
232
|
+
'UnsupportedEBPFPrograms',
|
|
223
233
|
// NPD — AKS extensions (learn.microsoft.com/azure/aks/node-problem-detector)
|
|
224
234
|
'FilesystemCorruptionProblem',
|
|
225
235
|
'KubeletProblem',
|
|
@@ -402,15 +412,17 @@ interface AlertBannerProps {
|
|
|
402
412
|
message?: React.ReactNode
|
|
403
413
|
items?: string[]
|
|
404
414
|
children?: React.ReactNode
|
|
415
|
+
/** Replaces the default bottom margin when the banner sits in a tighter layout. */
|
|
416
|
+
className?: string
|
|
405
417
|
}
|
|
406
418
|
|
|
407
|
-
export function AlertBanner({ variant, icon, title, message, items, children }: AlertBannerProps) {
|
|
419
|
+
export function AlertBanner({ variant, icon, title, message, items, children, className }: AlertBannerProps) {
|
|
408
420
|
const colors = ALERT_COLORS[variant]
|
|
409
421
|
const Icon = icon || DEFAULT_ICONS[variant]
|
|
410
422
|
const hasBody = message || items || children
|
|
411
423
|
|
|
412
424
|
return (
|
|
413
|
-
<div className={clsx('
|
|
425
|
+
<div className={clsx('p-3 border rounded-lg', className ?? 'mb-4', colors.bg, colors.border)}>
|
|
414
426
|
<div className={clsx('flex gap-2', hasBody ? 'items-start' : 'items-center')}>
|
|
415
427
|
<Icon className={clsx('w-4 h-4 shrink-0', colors.title, hasBody && 'mt-0.5')} />
|
|
416
428
|
{hasBody ? (
|
|
@@ -527,7 +539,7 @@ export function MetadataSection({ data }: { data: any }) {
|
|
|
527
539
|
<Property label="Generation" value={meta.generation} />
|
|
528
540
|
<Property label="Created" value={meta.creationTimestamp ? (
|
|
529
541
|
<Tooltip content={new Date(meta.creationTimestamp).toLocaleString()}>
|
|
530
|
-
<span className="border-b border-dotted border-theme-text-tertiary cursor-help">{
|
|
542
|
+
<span className="border-b border-dotted border-theme-text-tertiary cursor-help">{formatRelativeAgeTime(meta.creationTimestamp)}</span>
|
|
531
543
|
</Tooltip>
|
|
532
544
|
) : '-'} />
|
|
533
545
|
</PropertyList>
|
|
@@ -784,25 +796,19 @@ export type CopyHandler = (text: string, key: string) => void
|
|
|
784
796
|
import type { TimelineEvent, Relationships, ResourceRef } from '../../types'
|
|
785
797
|
import { isChangeEvent, isK8sEvent } from '../../types'
|
|
786
798
|
import { Link } from 'lucide-react'
|
|
799
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
787
800
|
|
|
788
801
|
interface RelatedResourcesSectionProps {
|
|
789
802
|
relationships: Relationships | undefined
|
|
790
803
|
onNavigate?: (ref: ResourceRef) => void
|
|
791
804
|
}
|
|
792
805
|
|
|
793
|
-
function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
794
|
-
const seen = new Set<string>()
|
|
795
|
-
return refs.filter(ref => {
|
|
796
|
-
const key = `${ref.kind}/${ref.namespace}/${ref.name}`
|
|
797
|
-
if (seen.has(key)) return false
|
|
798
|
-
seen.add(key)
|
|
799
|
-
return true
|
|
800
|
-
})
|
|
801
|
-
}
|
|
802
|
-
|
|
803
806
|
export function RelatedResourcesSection({ relationships, onNavigate }: RelatedResourcesSectionProps) {
|
|
804
807
|
if (!relationships) return null
|
|
805
808
|
|
|
809
|
+
const configRefs = omitResourceRefs(relationships.configRefs ?? [], relationships.dependencies ?? [])
|
|
810
|
+
const consumers = omitResourceRefs(relationships.consumers ?? [], relationships.dependents ?? [])
|
|
811
|
+
|
|
806
812
|
const hasRelationships =
|
|
807
813
|
relationships.owner ||
|
|
808
814
|
relationships.deployment ||
|
|
@@ -820,6 +826,8 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
820
826
|
(relationships.pdbs && relationships.pdbs.length > 0) ||
|
|
821
827
|
(relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
|
|
822
828
|
(relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
|
|
829
|
+
(relationships.dependencies && relationships.dependencies.length > 0) ||
|
|
830
|
+
(relationships.dependents && relationships.dependents.length > 0) ||
|
|
823
831
|
relationships.scaleTarget
|
|
824
832
|
|
|
825
833
|
if (!hasRelationships) return null
|
|
@@ -837,43 +845,49 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
837
845
|
<RelationshipGroup label="Node" refs={[relationships.node]} onNavigate={onNavigate} />
|
|
838
846
|
)}
|
|
839
847
|
{relationships.children && relationships.children.length > 0 && (
|
|
840
|
-
<RelationshipGroup label="Children" refs={
|
|
848
|
+
<RelationshipGroup label="Children" refs={dedupeResourceRefs(relationships.children)} onNavigate={onNavigate} />
|
|
841
849
|
)}
|
|
842
850
|
{relationships.services && relationships.services.length > 0 && (
|
|
843
|
-
<RelationshipGroup label="Services" refs={
|
|
851
|
+
<RelationshipGroup label="Services" refs={dedupeResourceRefs(relationships.services)} onNavigate={onNavigate} />
|
|
844
852
|
)}
|
|
845
853
|
{relationships.ingresses && relationships.ingresses.length > 0 && (
|
|
846
|
-
<RelationshipGroup label="Ingresses" refs={
|
|
854
|
+
<RelationshipGroup label="Ingresses" refs={dedupeResourceRefs(relationships.ingresses)} onNavigate={onNavigate} />
|
|
847
855
|
)}
|
|
848
856
|
{relationships.gateways && relationships.gateways.length > 0 && (
|
|
849
|
-
<RelationshipGroup label="Gateways" refs={
|
|
857
|
+
<RelationshipGroup label="Gateways" refs={dedupeResourceRefs(relationships.gateways)} onNavigate={onNavigate} />
|
|
850
858
|
)}
|
|
851
859
|
{relationships.routes && relationships.routes.length > 0 && (
|
|
852
|
-
<RelationshipGroup label="Routes" refs={
|
|
860
|
+
<RelationshipGroup label="Routes" refs={dedupeResourceRefs(relationships.routes)} onNavigate={onNavigate} />
|
|
853
861
|
)}
|
|
854
862
|
{relationships.pods && relationships.pods.length > 0 && (
|
|
855
|
-
<RelationshipGroup label="Pods" refs={
|
|
863
|
+
<RelationshipGroup label="Pods" refs={dedupeResourceRefs(relationships.pods)} onNavigate={onNavigate} />
|
|
856
864
|
)}
|
|
857
|
-
{
|
|
858
|
-
<RelationshipGroup label="Configuration" refs={
|
|
865
|
+
{configRefs.length > 0 && (
|
|
866
|
+
<RelationshipGroup label="Configuration" refs={dedupeResourceRefs(configRefs)} onNavigate={onNavigate} />
|
|
859
867
|
)}
|
|
860
|
-
{
|
|
861
|
-
<RelationshipGroup label="Used By" refs={
|
|
868
|
+
{consumers.length > 0 && (
|
|
869
|
+
<RelationshipGroup label="Used By" refs={dedupeResourceRefs(consumers)} onNavigate={onNavigate} />
|
|
862
870
|
)}
|
|
863
871
|
{relationships.scalers && relationships.scalers.length > 0 && (
|
|
864
|
-
<RelationshipGroup label="Autoscaler" refs={
|
|
872
|
+
<RelationshipGroup label="Autoscaler" refs={dedupeResourceRefs(relationships.scalers)} onNavigate={onNavigate} />
|
|
865
873
|
)}
|
|
866
874
|
{relationships.storageRefs && relationships.storageRefs.length > 0 && (
|
|
867
|
-
<RelationshipGroup label="Storage" refs={
|
|
875
|
+
<RelationshipGroup label="Storage" refs={dedupeResourceRefs(relationships.storageRefs)} onNavigate={onNavigate} />
|
|
868
876
|
)}
|
|
869
877
|
{relationships.pdbs && relationships.pdbs.length > 0 && (
|
|
870
|
-
<RelationshipGroup label="Disruption Budget" refs={
|
|
878
|
+
<RelationshipGroup label="Disruption Budget" refs={dedupeResourceRefs(relationships.pdbs)} onNavigate={onNavigate} />
|
|
871
879
|
)}
|
|
872
880
|
{relationships.networkPolicies && relationships.networkPolicies.length > 0 && (
|
|
873
|
-
<RelationshipGroup label="Network Policies" refs={
|
|
881
|
+
<RelationshipGroup label="Network Policies" refs={dedupeResourceRefs(relationships.networkPolicies)} onNavigate={onNavigate} />
|
|
874
882
|
)}
|
|
875
883
|
{relationships.resourceClaims && relationships.resourceClaims.length > 0 && (
|
|
876
|
-
<RelationshipGroup label="Resource Claims" refs={
|
|
884
|
+
<RelationshipGroup label="Resource Claims" refs={dedupeResourceRefs(relationships.resourceClaims)} onNavigate={onNavigate} />
|
|
885
|
+
)}
|
|
886
|
+
{relationships.dependencies && relationships.dependencies.length > 0 && (
|
|
887
|
+
<RelationshipGroup label="Depends On" refs={dedupeResourceRefs(relationships.dependencies)} onNavigate={onNavigate} />
|
|
888
|
+
)}
|
|
889
|
+
{relationships.dependents && relationships.dependents.length > 0 && (
|
|
890
|
+
<RelationshipGroup label="Required By" refs={dedupeResourceRefs(relationships.dependents)} onNavigate={onNavigate} />
|
|
877
891
|
)}
|
|
878
892
|
{relationships.scaleTarget && (
|
|
879
893
|
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { WorkloadView } from './WorkloadView'
|
|
5
|
+
|
|
6
|
+
const pod = {
|
|
7
|
+
apiVersion: 'v1',
|
|
8
|
+
kind: 'Pod',
|
|
9
|
+
metadata: { name: 'web-1', namespace: 'shop' },
|
|
10
|
+
spec: { containers: [{ name: 'app', image: 'nginx' }] },
|
|
11
|
+
status: { phase: 'Running', containerStatuses: [{ name: 'app', ready: true, state: { running: {} } }] },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function render(resourceError: unknown) {
|
|
15
|
+
return renderToStaticMarkup(
|
|
16
|
+
<WorkloadView
|
|
17
|
+
kind="pods"
|
|
18
|
+
namespace="shop"
|
|
19
|
+
name="web-1"
|
|
20
|
+
onBack={vi.fn()}
|
|
21
|
+
resource={pod}
|
|
22
|
+
resourceError={resourceError}
|
|
23
|
+
relationships={{ deployment: { kind: 'Deployment', namespace: 'shop', name: 'web' } }}
|
|
24
|
+
onNavigateToResource={vi.fn()}
|
|
25
|
+
actionsBarProps={{ canExec: true, canViewLogs: true, onOpenTerminal: vi.fn(), onOpenLogs: vi.fn(), onDelete: vi.fn(), renderDiagnose: () => 'Diagnose' }}
|
|
26
|
+
hasOperationalIssues
|
|
27
|
+
renderOverviewLead={() => 'Host issues panel'}
|
|
28
|
+
renderOverviewExtra={() => 'Host reachability panel'}
|
|
29
|
+
renderDiagnoseTab={() => 'Host reachability tab'}
|
|
30
|
+
reachableVia={[{ kind: 'Service', namespace: 'shop', name: 'web' }]}
|
|
31
|
+
/>,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('WorkloadView for an object deleted while open', () => {
|
|
36
|
+
it('says it is gone, names its owner, and offers nothing that acts on it', () => {
|
|
37
|
+
const html = render(Object.assign(new Error('pods "web-1" not found'), { status: 404 }))
|
|
38
|
+
expect(html).toContain('This Pod no longer exists in the cluster')
|
|
39
|
+
expect(html).toContain('Go to its Deployment web')
|
|
40
|
+
expect(html).toContain('>Deleted<')
|
|
41
|
+
expect(html).not.toContain('Terminal')
|
|
42
|
+
expect(html).not.toContain('Diagnose')
|
|
43
|
+
expect(html).not.toContain('Host issues panel')
|
|
44
|
+
expect(html).not.toContain('Host reachability panel')
|
|
45
|
+
expect(html).not.toContain('Reachability')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('shows the live view when the fetch failed for another reason', () => {
|
|
49
|
+
const html = render(Object.assign(new Error('boom'), { status: 500 }))
|
|
50
|
+
expect(html).not.toContain('no longer exists')
|
|
51
|
+
expect(html).not.toContain('>Deleted<')
|
|
52
|
+
expect(html).toContain('Terminal')
|
|
53
|
+
expect(html).toContain('Diagnose')
|
|
54
|
+
expect(html).toContain('Host issues panel')
|
|
55
|
+
expect(html).toContain('Host reachability panel')
|
|
56
|
+
expect(html).toContain('Reachability')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { WorkloadView } from './WorkloadView'
|
|
7
|
+
|
|
8
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
9
|
+
|
|
10
|
+
const deployment = {
|
|
11
|
+
apiVersion: 'apps/v1',
|
|
12
|
+
kind: 'Deployment',
|
|
13
|
+
metadata: { name: 'web', namespace: 'shop' },
|
|
14
|
+
spec: { replicas: 1, template: { spec: { containers: [{ name: 'app', image: 'nginx' }] } } },
|
|
15
|
+
status: { replicas: 1, readyReplicas: 1, availableReplicas: 1 },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it('closes an open Delete dialog when the object is deleted elsewhere', async () => {
|
|
19
|
+
const element = document.createElement('div')
|
|
20
|
+
document.body.append(element)
|
|
21
|
+
const root = createRoot(element)
|
|
22
|
+
const render = (resourceError: unknown) =>
|
|
23
|
+
root.render(
|
|
24
|
+
<WorkloadView
|
|
25
|
+
kind="deployments"
|
|
26
|
+
namespace="shop"
|
|
27
|
+
name="web"
|
|
28
|
+
onBack={vi.fn()}
|
|
29
|
+
resource={deployment}
|
|
30
|
+
resourceError={resourceError}
|
|
31
|
+
actionsBarProps={{ onDelete: vi.fn() }}
|
|
32
|
+
/>,
|
|
33
|
+
)
|
|
34
|
+
const dialogOpen = () => document.body.textContent?.includes('Are you sure you want to delete') ?? false
|
|
35
|
+
|
|
36
|
+
await act(async () => render(null))
|
|
37
|
+
await act(async () => { element.querySelector<HTMLButtonElement>('button:has(svg.lucide-trash-2)')!.click() })
|
|
38
|
+
expect(dialogOpen()).toBe(true)
|
|
39
|
+
|
|
40
|
+
await act(async () => render(Object.assign(new Error('deployments.apps "web" not found'), { status: 404 })))
|
|
41
|
+
expect(dialogOpen()).toBe(false)
|
|
42
|
+
|
|
43
|
+
await act(async () => root.unmount())
|
|
44
|
+
element.remove()
|
|
45
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { WorkloadView } from './WorkloadView'
|
|
7
|
+
|
|
8
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
9
|
+
|
|
10
|
+
const secret = {
|
|
11
|
+
apiVersion: 'v1',
|
|
12
|
+
kind: 'Secret',
|
|
13
|
+
metadata: { name: 'db', namespace: 'shop', resourceVersion: '7' },
|
|
14
|
+
type: 'Opaque',
|
|
15
|
+
data: { password: btoa('old') },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function button(label: string): HTMLButtonElement | undefined {
|
|
19
|
+
return [...document.querySelectorAll('button')].find(b => b.textContent?.trim() === label || b.title === label)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('saves a Secret value against the version on screen, and offers no edit once the Secret is gone', async () => {
|
|
23
|
+
const element = document.createElement('div')
|
|
24
|
+
document.body.append(element)
|
|
25
|
+
const root = createRoot(element)
|
|
26
|
+
const onUpdateResource = vi.fn().mockResolvedValue(undefined)
|
|
27
|
+
const render = (resourceError: unknown) =>
|
|
28
|
+
root.render(
|
|
29
|
+
<WorkloadView
|
|
30
|
+
kind="secrets"
|
|
31
|
+
namespace="shop"
|
|
32
|
+
name="db"
|
|
33
|
+
onBack={vi.fn()}
|
|
34
|
+
resource={secret}
|
|
35
|
+
resourceError={resourceError}
|
|
36
|
+
canUpdateSecrets
|
|
37
|
+
onUpdateResource={onUpdateResource}
|
|
38
|
+
/>,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
await act(async () => render(null))
|
|
42
|
+
await act(async () => button('Reveal')!.click())
|
|
43
|
+
await act(async () => button('Edit value')!.click())
|
|
44
|
+
await act(async () => button('Save')!.click())
|
|
45
|
+
await act(async () => button('Update')!.click())
|
|
46
|
+
expect(onUpdateResource).toHaveBeenCalledWith(expect.objectContaining({
|
|
47
|
+
kind: 'secrets',
|
|
48
|
+
namespace: 'shop',
|
|
49
|
+
name: 'db',
|
|
50
|
+
reviewedResourceVersion: '7',
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
await act(async () => button('Edit value')!.click())
|
|
54
|
+
expect(element.querySelector('textarea')).not.toBeNull()
|
|
55
|
+
await act(async () => render(Object.assign(new Error('secrets "db" not found'), { status: 404 })))
|
|
56
|
+
expect(element.textContent).toContain('This Secret no longer exists in the cluster')
|
|
57
|
+
expect(element.querySelector('textarea')).toBeNull()
|
|
58
|
+
expect(button('Edit value')).toBeUndefined()
|
|
59
|
+
|
|
60
|
+
await act(async () => root.unmount())
|
|
61
|
+
element.remove()
|
|
62
|
+
})
|