@skyhook-io/k8s-ui 1.16.3 → 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 +1 -1
- 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/primitives.tsx +3 -3
- 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/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +5 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesView.tsx +7 -8
- 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/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.ts +0 -4
- 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/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 +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- 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 +120 -43
- 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/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
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState, useId } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
AlertTriangle,
|
|
4
|
-
ExternalLink,
|
|
5
|
-
Loader2,
|
|
6
|
-
RefreshCw,
|
|
7
|
-
X,
|
|
8
|
-
} from 'lucide-react'
|
|
2
|
+
import { Loader2, RefreshCw, X } from 'lucide-react'
|
|
9
3
|
import { clsx } from 'clsx'
|
|
10
4
|
|
|
11
5
|
import type {
|
|
@@ -16,11 +10,33 @@ import type {
|
|
|
16
10
|
} from '../../types/core'
|
|
17
11
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
18
12
|
import { Badge } from '../ui/Badge'
|
|
13
|
+
import {
|
|
14
|
+
canConfirmGitOpsWrite,
|
|
15
|
+
gitOpsWriteGuardKey,
|
|
16
|
+
type GitOpsWrite,
|
|
17
|
+
type GitOpsWriteGuard,
|
|
18
|
+
} from '../../utils/gitops-write-guard'
|
|
19
|
+
import { GitOpsWriteWarning } from './GitOpsWriteWarning'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/** The GitOps write guard for the object whose Pod template the dialog
|
|
22
|
+
* writes, evaluated by the host: for {@link SET_IMAGE_WRITES} until the
|
|
23
|
+
* containers are known, then for {@link setImageWrites} of the containers
|
|
24
|
+
* {@link onSelectionChange} reports. */
|
|
25
|
+
export interface SetImageOwnership {
|
|
26
|
+
guard: GitOpsWriteGuard
|
|
27
|
+
onOpenOwner?: () => void
|
|
28
|
+
/** The containers the dialog lists and the ones being changed, so the
|
|
29
|
+
* guard can judge exactly those (null until the containers load). */
|
|
30
|
+
onSelectionChange?: (selection: SetImageSelection | null) => void
|
|
31
|
+
/** {@link setImageSelectionKey} of the selection `guard` was evaluated
|
|
32
|
+
* for. When set, Update waits until it matches the dialog's current
|
|
33
|
+
* selection. */
|
|
34
|
+
selectionKey?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SetImageSelection {
|
|
38
|
+
containers: Pick<WorkloadContainerImage, 'type' | 'name'>[]
|
|
39
|
+
changed: Pick<WorkloadContainerImage, 'type' | 'name'>[]
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
export interface SetImageDialogProps {
|
|
@@ -28,7 +44,8 @@ export interface SetImageDialogProps {
|
|
|
28
44
|
workloadLabel: string
|
|
29
45
|
workloadName: string
|
|
30
46
|
workloadResource: string
|
|
31
|
-
|
|
47
|
+
/** Undefined while the host can't verify ownership; the dialog then blocks. */
|
|
48
|
+
ownership?: SetImageOwnership
|
|
32
49
|
pending?: boolean
|
|
33
50
|
onClose: () => void
|
|
34
51
|
onLoad: () => Promise<WorkloadImageInventory>
|
|
@@ -83,31 +100,49 @@ export function reconcileRefreshedImageDrafts(
|
|
|
83
100
|
return { drafts: nextDrafts, changedCurrentKeys }
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
export function setImageSelectionKey(selection: SetImageSelection | null): string {
|
|
104
|
+
if (!selection) return ''
|
|
105
|
+
const rows = (refs: SetImageSelection['containers']) => refs.map((c) => [c.type, c.name])
|
|
106
|
+
return JSON.stringify([rows(selection.containers), rows(selection.changed)])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One write per container, by name: the guard's verdict for exactly the
|
|
110
|
+
* images being changed. */
|
|
111
|
+
export function setImageWrites(containers: Pick<WorkloadContainerImage, 'type' | 'name'>[]): GitOpsWrite[] {
|
|
112
|
+
return containers.map((container) => ({
|
|
113
|
+
scope: 'spec',
|
|
114
|
+
paths: [`spec.template.spec.${container.type === 'initContainer' ? 'initContainers' : 'containers'}[name=${container.name}].image`],
|
|
115
|
+
description: container.name,
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Before the containers load, the guard covers every container image.
|
|
120
|
+
export const SET_IMAGE_WRITES: GitOpsWrite[] = [
|
|
121
|
+
{
|
|
122
|
+
scope: 'spec',
|
|
123
|
+
paths: [
|
|
124
|
+
'spec.template.spec.containers[*].image',
|
|
125
|
+
'spec.template.spec.initContainers[*].image',
|
|
126
|
+
],
|
|
127
|
+
description: 'Container images',
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
86
131
|
export function canSubmitImageUpdates({
|
|
87
132
|
updateCount,
|
|
88
133
|
hasEmptyImage,
|
|
89
|
-
|
|
90
|
-
ownershipResolved,
|
|
91
|
-
acknowledged,
|
|
134
|
+
ownershipConfirmable,
|
|
92
135
|
busy,
|
|
93
136
|
loadFailed = false,
|
|
94
137
|
}: {
|
|
95
138
|
updateCount: number
|
|
96
139
|
hasEmptyImage: boolean
|
|
97
|
-
managed
|
|
98
|
-
|
|
99
|
-
acknowledged: boolean
|
|
140
|
+
/** Ownership resolved and, when it's managed, acknowledged. */
|
|
141
|
+
ownershipConfirmable: boolean
|
|
100
142
|
busy: boolean
|
|
101
143
|
loadFailed?: boolean
|
|
102
144
|
}): boolean {
|
|
103
|
-
return
|
|
104
|
-
updateCount > 0 &&
|
|
105
|
-
!hasEmptyImage &&
|
|
106
|
-
ownershipResolved &&
|
|
107
|
-
(!managed || acknowledged) &&
|
|
108
|
-
!busy &&
|
|
109
|
-
!loadFailed
|
|
110
|
-
)
|
|
145
|
+
return updateCount > 0 && !hasEmptyImage && ownershipConfirmable && !busy && !loadFailed
|
|
111
146
|
}
|
|
112
147
|
|
|
113
148
|
export function describeImageUpdateBehavior(
|
|
@@ -140,7 +175,7 @@ export function SetImageDialog({
|
|
|
140
175
|
workloadLabel,
|
|
141
176
|
workloadName,
|
|
142
177
|
workloadResource,
|
|
143
|
-
|
|
178
|
+
ownership,
|
|
144
179
|
pending = false,
|
|
145
180
|
onClose,
|
|
146
181
|
onLoad,
|
|
@@ -155,7 +190,9 @@ export function SetImageDialog({
|
|
|
155
190
|
const [submitting, setSubmitting] = useState(false)
|
|
156
191
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
157
192
|
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
158
|
-
|
|
193
|
+
// The guard key the acknowledgment was given for: a different verdict
|
|
194
|
+
// (fresh evidence, other containers, a changed policy) asks again.
|
|
195
|
+
const [ackedKey, setAckedKey] = useState('')
|
|
159
196
|
const [changedCurrentKeys, setChangedCurrentKeys] = useState<string[]>([])
|
|
160
197
|
const onLoadRef = useRef(onLoad)
|
|
161
198
|
const loadRequestRef = useRef(0)
|
|
@@ -216,7 +253,7 @@ export function SetImageDialog({
|
|
|
216
253
|
setDrafts({})
|
|
217
254
|
setChangedCurrentKeys([])
|
|
218
255
|
setSubmitError(null)
|
|
219
|
-
|
|
256
|
+
setAckedKey('')
|
|
220
257
|
void loadInventory()
|
|
221
258
|
}, [open, workloadLabel, workloadName, workloadResource])
|
|
222
259
|
|
|
@@ -229,13 +266,27 @@ export function SetImageDialog({
|
|
|
229
266
|
(container) => !(drafts[containerKey(container)] ?? '').trim(),
|
|
230
267
|
) ?? false
|
|
231
268
|
const busy = pending || submitting
|
|
232
|
-
const ownershipResolved =
|
|
269
|
+
const ownershipResolved = ownership !== undefined
|
|
270
|
+
const onSelectionChange = ownership?.onSelectionChange
|
|
271
|
+
const selectionKey = setImageSelectionKey(
|
|
272
|
+
inventory ? { containers: inventory.containers, changed: updates } : null,
|
|
273
|
+
)
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!onSelectionChange) return
|
|
276
|
+
const [containers, changed] = selectionKey ? (JSON.parse(selectionKey) as [string[][], string[][]]) : [null, null]
|
|
277
|
+
const toRefs = (rows: string[][]) => rows.map(([type, name]) => ({ type: type as WorkloadContainerImage['type'], name }))
|
|
278
|
+
onSelectionChange(containers && changed ? { containers: toRefs(containers), changed: toRefs(changed) } : null)
|
|
279
|
+
}, [onSelectionChange, selectionKey])
|
|
280
|
+
const guard = ownership?.guard
|
|
281
|
+
// The host evaluates the guard after it hears of an edit; until then the
|
|
282
|
+
// verdict on screen is for other containers.
|
|
283
|
+
const guardCurrent = ownership?.selectionKey === undefined || ownership.selectionKey === selectionKey
|
|
284
|
+
const guardKey = gitOpsWriteGuardKey(guard)
|
|
285
|
+
const acknowledged = Boolean(guardKey) && ackedKey === guardKey
|
|
233
286
|
const canSubmit = canSubmitImageUpdates({
|
|
234
287
|
updateCount: updates.length,
|
|
235
288
|
hasEmptyImage,
|
|
236
|
-
|
|
237
|
-
ownershipResolved,
|
|
238
|
-
acknowledged,
|
|
289
|
+
ownershipConfirmable: guardCurrent && canConfirmGitOpsWrite(guard, acknowledged),
|
|
239
290
|
busy,
|
|
240
291
|
loadFailed: Boolean(loadError),
|
|
241
292
|
})
|
|
@@ -378,57 +429,14 @@ export function SetImageDialog({
|
|
|
378
429
|
{describeImageUpdateBehavior(inventory.behavior)}
|
|
379
430
|
</div>
|
|
380
431
|
|
|
381
|
-
{
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<p className="mt-1 text-xs text-theme-text-secondary">
|
|
390
|
-
Managed by{' '}
|
|
391
|
-
{managedSources
|
|
392
|
-
.map((source) => `${source.type} ${source.label}`)
|
|
393
|
-
.join(' and ')}
|
|
394
|
-
. Update the declared source for a durable change; use
|
|
395
|
-
this action only as a break-glass operation.
|
|
396
|
-
</p>
|
|
397
|
-
<div className="mt-2 flex flex-wrap gap-2">
|
|
398
|
-
{managedSources
|
|
399
|
-
.filter((source) => source.onOpen)
|
|
400
|
-
.map((source) => (
|
|
401
|
-
<button
|
|
402
|
-
key={`${source.type}-${source.label}`}
|
|
403
|
-
type="button"
|
|
404
|
-
onClick={source.onOpen}
|
|
405
|
-
className="inline-flex items-center gap-1 text-xs font-medium text-skyhook-600 hover:underline dark:text-skyhook-400"
|
|
406
|
-
>
|
|
407
|
-
{source.type === 'GitOps'
|
|
408
|
-
? 'View GitOps owner'
|
|
409
|
-
: 'View Helm release'}
|
|
410
|
-
<ExternalLink className="h-3 w-3" />
|
|
411
|
-
</button>
|
|
412
|
-
))}
|
|
413
|
-
</div>
|
|
414
|
-
<label className="mt-3 flex cursor-pointer items-start gap-2 text-xs text-theme-text-primary">
|
|
415
|
-
<input
|
|
416
|
-
type="checkbox"
|
|
417
|
-
checked={acknowledged}
|
|
418
|
-
onChange={(event) =>
|
|
419
|
-
setAcknowledged(event.target.checked)
|
|
420
|
-
}
|
|
421
|
-
disabled={busy}
|
|
422
|
-
className="mt-0.5 h-3.5 w-3.5 accent-skyhook-500"
|
|
423
|
-
/>
|
|
424
|
-
<span>
|
|
425
|
-
I understand the declared source may overwrite this live
|
|
426
|
-
change.
|
|
427
|
-
</span>
|
|
428
|
-
</label>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
432
|
+
{guard && (
|
|
433
|
+
<GitOpsWriteWarning
|
|
434
|
+
guard={guard}
|
|
435
|
+
acknowledged={acknowledged}
|
|
436
|
+
onAcknowledgedChange={(value) => setAckedKey(value ? guardKey : '')}
|
|
437
|
+
onOpenOwner={ownership?.onOpenOwner}
|
|
438
|
+
disabled={busy}
|
|
439
|
+
/>
|
|
432
440
|
)}
|
|
433
441
|
|
|
434
442
|
<ContainerImageGroup
|
|
@@ -5,7 +5,8 @@ export {
|
|
|
5
5
|
DrainPlanDialog, DrainPlanContent, canConfirmDrain, planMatches, emptyDirPodsAtRisk, DEFAULT_DRAIN_DIALOG_OPTIONS,
|
|
6
6
|
type DrainPlan, type DrainPlanPod, type DrainOutcome, type DrainDialogOptions,
|
|
7
7
|
} from './DrainPlanDialog'
|
|
8
|
-
export { SetImageDialog, type
|
|
8
|
+
export { SetImageDialog, SET_IMAGE_WRITES, setImageWrites, setImageSelectionKey, type SetImageOwnership, type SetImageSelection, type SetImageDialogProps } from './SetImageDialog'
|
|
9
|
+
export { GitOpsWriteWarning, type GitOpsWriteWarningProps } from './GitOpsWriteWarning'
|
|
9
10
|
export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
|
|
10
11
|
export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
|
|
11
12
|
export { DetailShell, type DetailShellProps, type DetailShellTab } from './DetailShell'
|
|
@@ -35,20 +35,7 @@ import type { NavigateToResource } from '../../utils/navigation'
|
|
|
35
35
|
import { builtinGroupForKind } from '../../utils/api-resources'
|
|
36
36
|
import { kindToPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '../../utils/navigation'
|
|
37
37
|
import { pluralize } from '../../utils/pluralize'
|
|
38
|
-
|
|
39
|
-
/** Format resource age (e.g., "3d", "5h", "10m") */
|
|
40
|
-
function formatResourceAge(createdAt: string): string {
|
|
41
|
-
const diff = Date.now() - new Date(createdAt).getTime()
|
|
42
|
-
const mins = Math.floor(diff / 60000)
|
|
43
|
-
if (mins < 1) return '<1m'
|
|
44
|
-
if (mins < 60) return `${mins}m`
|
|
45
|
-
const hours = Math.floor(mins / 60)
|
|
46
|
-
if (hours < 24) return `${hours}h`
|
|
47
|
-
const days = Math.floor(hours / 24)
|
|
48
|
-
if (days < 30) return `${days}d`
|
|
49
|
-
const months = Math.floor(days / 30)
|
|
50
|
-
return `${months}mo`
|
|
51
|
-
}
|
|
38
|
+
import { formatCompactAge } from '../../utils/format'
|
|
52
39
|
|
|
53
40
|
export type { ActivityTypeFilter, ActivityFilterKey }
|
|
54
41
|
|
|
@@ -736,7 +723,7 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick, compact, sele
|
|
|
736
723
|
{item.createdAt && (
|
|
737
724
|
<Tooltip content={`Created: ${new Date(item.createdAt).toLocaleString()}`}>
|
|
738
725
|
<span className="text-xs text-theme-text-quaternary">
|
|
739
|
-
• {
|
|
726
|
+
• {formatCompactAge(item.createdAt)} old
|
|
740
727
|
</span>
|
|
741
728
|
</Tooltip>
|
|
742
729
|
)}
|
|
@@ -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(' · ')
|
|
@@ -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} />
|