@skyhook-io/k8s-ui 1.18.2 → 1.20.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 +9 -2
- package/src/components/applications/ApplicationDetail.tsx +8 -2
- package/src/components/charts/AreaChart.test.tsx +39 -1
- package/src/components/charts/AreaChart.tsx +32 -13
- package/src/components/charts/axis.test.ts +17 -1
- package/src/components/charts/axis.ts +13 -0
- package/src/components/charts/format.test.ts +159 -2
- package/src/components/charts/format.ts +118 -0
- package/src/components/cluster-switcher/ClusterSwitcher.test.tsx +54 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +7 -3
- package/src/components/compare/ResourceCompareView.test.tsx +54 -0
- package/src/components/compare/ResourceCompareView.tsx +35 -12
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +16 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +54 -7
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +1 -1
- package/src/components/gitops/GitOpsTableView.tsx +26 -7
- package/src/components/gitops/RollbackDialog.test.tsx +80 -0
- package/src/components/gitops/RollbackDialog.tsx +25 -6
- package/src/components/gitops/SyncOptionsDialog.render.test.tsx +374 -0
- package/src/components/gitops/SyncOptionsDialog.test.ts +54 -1
- package/src/components/gitops/SyncOptionsDialog.tsx +179 -34
- package/src/components/gitops/applicationset-row.test.ts +12 -0
- package/src/components/gitops/argo-radar-upgrade.test.tsx +369 -0
- package/src/components/gitops/detail-helpers.test.ts +8 -2
- package/src/components/gitops/detail-helpers.ts +9 -11
- package/src/components/gitops/index.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +32 -0
- package/src/components/gitops/insights/GitOpsActivityInsightView.test.tsx +55 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +94 -46
- package/src/components/gitops/insights/GitOpsIssuesBand.test.tsx +108 -0
- package/src/components/gitops/insights/GitOpsRollbackInsightView.test.tsx +73 -0
- package/src/components/gitops/insights/GitOpsSyncWindowInsightView.test.tsx +29 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +65 -0
- package/src/components/gitops/insights/insights-helpers.ts +17 -3
- package/src/components/issues/IssueGuidance.test.tsx +190 -0
- package/src/components/issues/IssueGuidance.tsx +48 -0
- package/src/components/issues/IssuesView.tsx +15 -3
- package/src/components/issues/ResourceIssuesSection.tsx +3 -1
- package/src/components/issues/diagnostic.ts +14 -1
- package/src/components/issues/index.ts +1 -0
- package/src/components/issues/severity.ts +15 -0
- package/src/components/issues/types.ts +10 -0
- package/src/components/kueue/AdmissionPath.tsx +223 -0
- package/src/components/kueue/QueueEvidence.tsx +148 -0
- package/src/components/kueue/constants.ts +1 -0
- package/src/components/kueue/eligibility.ts +43 -0
- package/src/components/kueue/flavors-tried.ts +79 -0
- package/src/components/kueue/helpers.test.ts +105 -0
- package/src/components/kueue/policy.ts +100 -0
- package/src/components/kueue/queue-detail.test.tsx +317 -0
- package/src/components/kueue/queue-status.ts +42 -0
- package/src/components/kueue/related-read.ts +48 -0
- package/src/components/kueue/testdata/kueue-v0.18-policy-enums.json +89 -0
- package/src/components/logs/StructuredLogLine.test.tsx +22 -0
- package/src/components/logs/StructuredLogLine.tsx +8 -3
- package/src/components/resources/KueueAdmissionSection.test.tsx +117 -0
- package/src/components/resources/KueueAdmissionSection.tsx +51 -12
- package/src/components/resources/KueuePendingPosition.test.tsx +97 -0
- package/src/components/resources/KueuePendingPosition.tsx +91 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +8 -0
- package/src/components/resources/ResourcesSidebar.tsx +3 -2
- package/src/components/resources/ResourcesView.crd.test.tsx +28 -0
- package/src/components/resources/ResourcesView.node-pods.test.tsx +25 -0
- package/src/components/resources/ResourcesView.strimzi.test.tsx +37 -0
- package/src/components/resources/ResourcesView.tsx +50 -3
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +7 -0
- package/src/components/resources/get-pod-phase-display.test.ts +70 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +54 -1
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +7 -8
- package/src/components/resources/renderers/CRDSchemaBrowser.test.tsx +105 -0
- package/src/components/resources/renderers/CRDSchemaBrowser.tsx +134 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.test.tsx +106 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.tsx +152 -0
- package/src/components/resources/renderers/JobRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/JobRenderer.tsx +7 -2
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +5 -5
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +250 -170
- package/src/components/resources/renderers/KueueQueueWorkloads.test.tsx +189 -0
- package/src/components/resources/renderers/KueueQueueWorkloads.tsx +460 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +23 -52
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +11 -6
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +23 -5
- package/src/components/resources/renderers/ServiceAccountRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +15 -10
- package/src/components/resources/renderers/StrimziConnectorRenderer.tsx +61 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -3
- package/src/components/resources/renderers/crd-cells.test.tsx +30 -0
- package/src/components/resources/renderers/crd-cells.tsx +55 -0
- package/src/components/resources/renderers/crd-schema-parts.test.tsx +59 -0
- package/src/components/resources/renderers/crd-schema-parts.tsx +121 -0
- package/src/components/resources/renderers/index.ts +6 -0
- package/src/components/resources/renderers/kueue-cells.tsx +5 -0
- package/src/components/resources/resource-utils-argo.test.ts +94 -1
- package/src/components/resources/resource-utils-argo.ts +84 -5
- package/src/components/resources/resource-utils-crd.test.ts +32 -0
- package/src/components/resources/resource-utils-crd.ts +26 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +12 -0
- package/src/components/resources/resource-utils-kueue-admission.test.ts +26 -0
- package/src/components/resources/resource-utils-kueue.test.ts +15 -0
- package/src/components/resources/resource-utils-kueue.ts +137 -4
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +9 -1
- package/src/components/resources/resource-utils-opentelemetry.ts +1 -1
- package/src/components/resources/resource-utils-ray.test.ts +42 -1
- package/src/components/resources/resource-utils-ray.ts +38 -0
- package/src/components/resources/resource-utils-strimzi.test.tsx +118 -0
- package/src/components/resources/resource-utils-strimzi.ts +35 -0
- package/src/components/resources/resource-utils.ts +45 -1
- package/src/components/shared/ResourceActionsBar.tsx +83 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +37 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +40 -7
- package/src/components/topology/K8sResourceNode.test.ts +14 -0
- package/src/components/topology/K8sResourceNode.tsx +18 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +96 -94
- package/src/components/topology/layout.ts +3 -1
- package/src/components/topology/pod-group-expansion.test.ts +38 -0
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/RadarUpgradeNote.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/PodList.tsx +35 -9
- package/src/components/workload/WorkloadView.spec.test.tsx +41 -1
- package/src/components/workload/WorkloadView.tsx +61 -8
- package/src/components/workload/workload-logs-availability.test.ts +7 -0
- package/src/types/core.ts +41 -0
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +2 -2
- package/src/types/kueue-queues.ts +84 -0
- package/src/types/read-coverage.ts +2 -1
- package/src/types/scheduling.ts +72 -0
- package/src/utils/animation.test.ts +38 -0
- package/src/utils/animation.ts +34 -0
- package/src/utils/api-resources-crd.test.ts +11 -0
- package/src/utils/api-resources.ts +5 -1
- package/src/utils/applications.test.ts +15 -1
- package/src/utils/applications.ts +26 -1
- package/src/utils/crd-schema.test.ts +107 -0
- package/src/utils/crd-schema.ts +201 -0
- package/src/utils/format.test.ts +37 -1
- package/src/utils/format.ts +28 -80
- package/src/utils/gitops-owner.test.ts +28 -2
- package/src/utils/gitops-owner.ts +19 -0
- package/src/utils/kueue-queue-workloads.ts +19 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +90 -1
- package/src/utils/topology-neighborhood.ts +55 -3
- package/src/utils/workload-history-notes.test.ts +76 -0
- package/src/utils/workload-history-notes.ts +93 -0
|
@@ -307,6 +307,101 @@ export function phaseOnlyHealth(phase: string | undefined): HealthStatus {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/** Replaces a PodGroup with its pods. A pod connects to its own owners
|
|
311
|
+
* (`ownerIds`) or, when the server sent none, to every owner of the group;
|
|
312
|
+
* an empty `ownerIds` connects it to none. */
|
|
313
|
+
export function expandPodGroup(
|
|
314
|
+
topoNodes: TopologyNode[],
|
|
315
|
+
topoEdges: TopologyEdge[],
|
|
316
|
+
podGroupId: string,
|
|
317
|
+
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
|
|
318
|
+
const podGroupNode = topoNodes.find(n => n.id === podGroupId && n.kind === 'PodGroup')
|
|
319
|
+
if (!podGroupNode || !podGroupNode.data.pods) {
|
|
320
|
+
return { nodes: topoNodes, edges: topoEdges }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const pods = podGroupNode.data.pods as Array<{
|
|
324
|
+
name: string
|
|
325
|
+
namespace: string
|
|
326
|
+
phase: string
|
|
327
|
+
restarts: number
|
|
328
|
+
containers: number
|
|
329
|
+
status?: HealthStatus
|
|
330
|
+
terminating?: boolean
|
|
331
|
+
// Present only when the group spans more than one owner (e.g. a
|
|
332
|
+
// Rollout's canary + stable ReplicaSets) — the specific edge
|
|
333
|
+
// source(s) that actually own this pod, from the backend's own
|
|
334
|
+
// per-pod owner resolution. See pkg/topology/builder.go's
|
|
335
|
+
// ownerKeyToSourceIDs.
|
|
336
|
+
ownerIds?: string[]
|
|
337
|
+
// The group's own trafficRole (podGroupNode.data.trafficRole) is only
|
|
338
|
+
// set when every pod agrees — this is each pod's OWN role, so a mixed
|
|
339
|
+
// canary/stable group still badges correctly once expanded.
|
|
340
|
+
trafficRole?: string
|
|
341
|
+
}>
|
|
342
|
+
|
|
343
|
+
const edgesToGroup = topoEdges.filter(e => e.target === podGroupId)
|
|
344
|
+
const sourceIds = edgesToGroup.map(e => e.source)
|
|
345
|
+
// Homogeneous per build — resources-view feeds `manages` ownership
|
|
346
|
+
// edges, traffic-view feeds `routes-to` Service edges — so any surviving
|
|
347
|
+
// edge's type applies to the whole group.
|
|
348
|
+
const edgeType = edgesToGroup[0]?.type ?? 'routes-to'
|
|
349
|
+
|
|
350
|
+
const newNodes = topoNodes.filter(n => n.id !== podGroupId)
|
|
351
|
+
const newEdges = topoEdges.filter(e => e.target !== podGroupId)
|
|
352
|
+
|
|
353
|
+
for (const pod of pods) {
|
|
354
|
+
const podId = `pod/${pod.namespace}/${pod.name}`
|
|
355
|
+
newNodes.push({
|
|
356
|
+
id: podId,
|
|
357
|
+
kind: 'Pod',
|
|
358
|
+
name: pod.name,
|
|
359
|
+
// The server computes pod health; pkg/health tracks a crash loop across
|
|
360
|
+
// the kubelet's Waiting->Running oscillation, which the phase alone
|
|
361
|
+
// hides — a crash-looping pod sits at Phase=Running. Deriving it here
|
|
362
|
+
// again would rebuild that logic in a second place and get it wrong.
|
|
363
|
+
status: pod.status ?? phaseOnlyHealth(pod.phase),
|
|
364
|
+
data: {
|
|
365
|
+
...podGroupNode.data,
|
|
366
|
+
namespace: pod.namespace,
|
|
367
|
+
phase: pod.phase,
|
|
368
|
+
restarts: pod.restarts,
|
|
369
|
+
containers: pod.containers,
|
|
370
|
+
trafficRole: pod.trafficRole,
|
|
371
|
+
...(podGroupNode.data.earlierJobRun ? { terminating: pod.terminating ? 1 : 0 } : {}),
|
|
372
|
+
expandedFromGroup: podGroupId, // Track which group this came from
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// A group with a single owner has every source apply to every pod —
|
|
377
|
+
// the common case. A mixed-owner group (pod.ownerIds present) instead
|
|
378
|
+
// connects each pod only to the source(s) that are actually its own
|
|
379
|
+
// owner, so e.g. a canary pod doesn't end up drawn as owned by the
|
|
380
|
+
// stable ReplicaSet too.
|
|
381
|
+
const podSourceIds = pod.ownerIds?.filter(id => sourceIds.includes(id)) ?? sourceIds
|
|
382
|
+
// A mixed-owner group's shared sources (e.g. a Rollout, reached via
|
|
383
|
+
// both a canary and a stable edge) can't be told apart by source id
|
|
384
|
+
// alone once collapsed — both edges point at the same node id, just
|
|
385
|
+
// with different labels. The pod's own trafficRole is unambiguous, so
|
|
386
|
+
// an ownership edge derives its label from that directly rather than
|
|
387
|
+
// trying to match back to one specific original edge.
|
|
388
|
+
const label = podGroupNode.data.earlierJobRun ? edgesToGroup[0]?.label : edgeType === 'manages' && pod.trafficRole
|
|
389
|
+
? pod.trafficRole[0].toUpperCase() + pod.trafficRole.slice(1)
|
|
390
|
+
: undefined
|
|
391
|
+
for (const sourceId of podSourceIds) {
|
|
392
|
+
newEdges.push({
|
|
393
|
+
id: `${sourceId}-to-${podId}`,
|
|
394
|
+
source: sourceId,
|
|
395
|
+
target: podId,
|
|
396
|
+
type: edgeType,
|
|
397
|
+
...(label ? { label } : {}),
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { nodes: newNodes, edges: newEdges }
|
|
403
|
+
}
|
|
404
|
+
|
|
310
405
|
export function TopologyGraph({
|
|
311
406
|
topology,
|
|
312
407
|
viewMode,
|
|
@@ -455,99 +550,6 @@ export function TopologyGraph({
|
|
|
455
550
|
fitAllAfterLayoutRef.current = true
|
|
456
551
|
}, [])
|
|
457
552
|
|
|
458
|
-
// Expand PodGroup to individual pods
|
|
459
|
-
const expandPodGroup = useCallback((
|
|
460
|
-
topoNodes: TopologyNode[],
|
|
461
|
-
topoEdges: TopologyEdge[],
|
|
462
|
-
podGroupId: string
|
|
463
|
-
): { nodes: TopologyNode[]; edges: TopologyEdge[] } => {
|
|
464
|
-
const podGroupNode = topoNodes.find(n => n.id === podGroupId && n.kind === 'PodGroup')
|
|
465
|
-
if (!podGroupNode || !podGroupNode.data.pods) {
|
|
466
|
-
return { nodes: topoNodes, edges: topoEdges }
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const pods = podGroupNode.data.pods as Array<{
|
|
470
|
-
name: string
|
|
471
|
-
namespace: string
|
|
472
|
-
phase: string
|
|
473
|
-
restarts: number
|
|
474
|
-
containers: number
|
|
475
|
-
status?: HealthStatus
|
|
476
|
-
// Present only when the group spans more than one owner (e.g. a
|
|
477
|
-
// Rollout's canary + stable ReplicaSets) — the specific edge
|
|
478
|
-
// source(s) that actually own this pod, from the backend's own
|
|
479
|
-
// per-pod owner resolution. See pkg/topology/builder.go's
|
|
480
|
-
// ownerKeyToSourceIDs.
|
|
481
|
-
ownerIds?: string[]
|
|
482
|
-
// The group's own trafficRole (podGroupNode.data.trafficRole) is only
|
|
483
|
-
// set when every pod agrees — this is each pod's OWN role, so a mixed
|
|
484
|
-
// canary/stable group still badges correctly once expanded.
|
|
485
|
-
trafficRole?: string
|
|
486
|
-
}>
|
|
487
|
-
|
|
488
|
-
// Find edges pointing to this pod group
|
|
489
|
-
const edgesToGroup = topoEdges.filter(e => e.target === podGroupId)
|
|
490
|
-
const sourceIds = edgesToGroup.map(e => e.source)
|
|
491
|
-
// Homogeneous per build — resources-view feeds `manages` ownership
|
|
492
|
-
// edges, traffic-view feeds `routes-to` Service edges — so any surviving
|
|
493
|
-
// edge's type applies to the whole group.
|
|
494
|
-
const edgeType = edgesToGroup[0]?.type ?? 'routes-to'
|
|
495
|
-
|
|
496
|
-
// Remove the PodGroup node and its edges
|
|
497
|
-
const newNodes = topoNodes.filter(n => n.id !== podGroupId)
|
|
498
|
-
const newEdges = topoEdges.filter(e => e.target !== podGroupId)
|
|
499
|
-
|
|
500
|
-
// Add individual pod nodes
|
|
501
|
-
for (const pod of pods) {
|
|
502
|
-
const podId = `pod/${pod.namespace}/${pod.name}`
|
|
503
|
-
newNodes.push({
|
|
504
|
-
id: podId,
|
|
505
|
-
kind: 'Pod',
|
|
506
|
-
name: pod.name,
|
|
507
|
-
// The server computes pod health; pkg/health tracks a crash loop across
|
|
508
|
-
// the kubelet's Waiting->Running oscillation, which the phase alone
|
|
509
|
-
// hides — a crash-looping pod sits at Phase=Running. Deriving it here
|
|
510
|
-
// again would rebuild that logic in a second place and get it wrong.
|
|
511
|
-
status: pod.status ?? phaseOnlyHealth(pod.phase),
|
|
512
|
-
data: {
|
|
513
|
-
...podGroupNode.data,
|
|
514
|
-
namespace: pod.namespace,
|
|
515
|
-
phase: pod.phase,
|
|
516
|
-
restarts: pod.restarts,
|
|
517
|
-
containers: pod.containers,
|
|
518
|
-
trafficRole: pod.trafficRole,
|
|
519
|
-
expandedFromGroup: podGroupId, // Track which group this came from
|
|
520
|
-
},
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
// A group with a single owner has every source apply to every pod —
|
|
524
|
-
// the common case. A mixed-owner group (pod.ownerIds present) instead
|
|
525
|
-
// connects each pod only to the source(s) that are actually its own
|
|
526
|
-
// owner, so e.g. a canary pod doesn't end up drawn as owned by the
|
|
527
|
-
// stable ReplicaSet too.
|
|
528
|
-
const podSourceIds = pod.ownerIds?.filter(id => sourceIds.includes(id)) ?? sourceIds
|
|
529
|
-
// A mixed-owner group's shared sources (e.g. a Rollout, reached via
|
|
530
|
-
// both a canary and a stable edge) can't be told apart by source id
|
|
531
|
-
// alone once collapsed — both edges point at the same node id, just
|
|
532
|
-
// with different labels. The pod's own trafficRole is unambiguous, so
|
|
533
|
-
// an ownership edge derives its label from that directly rather than
|
|
534
|
-
// trying to match back to one specific original edge.
|
|
535
|
-
const label = edgeType === 'manages' && pod.trafficRole
|
|
536
|
-
? pod.trafficRole[0].toUpperCase() + pod.trafficRole.slice(1)
|
|
537
|
-
: undefined
|
|
538
|
-
for (const sourceId of podSourceIds) {
|
|
539
|
-
newEdges.push({
|
|
540
|
-
id: `${sourceId}-to-${podId}`,
|
|
541
|
-
source: sourceId,
|
|
542
|
-
target: podId,
|
|
543
|
-
type: edgeType,
|
|
544
|
-
...(label ? { label } : {}),
|
|
545
|
-
})
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return { nodes: newNodes, edges: newEdges }
|
|
550
|
-
}, [])
|
|
551
553
|
|
|
552
554
|
|
|
553
555
|
// Prepare topology data with expanded pod groups
|
|
@@ -574,7 +576,7 @@ export function TopologyGraph({
|
|
|
574
576
|
}
|
|
575
577
|
|
|
576
578
|
return { workingNodes: nodes, workingEdges: edges }
|
|
577
|
-
}, [topology, expandedPodGroups,
|
|
579
|
+
}, [topology, expandedPodGroups, isTrafficView, groupingMode])
|
|
578
580
|
|
|
579
581
|
// Handle card click in card-grid view — find the topology node and open drawer
|
|
580
582
|
const handleCardClick = useCallback((nodeId: string) => {
|
|
@@ -829,6 +829,8 @@ function computeWorkloadCards(
|
|
|
829
829
|
subtitle = (d.schedule as string) || ''
|
|
830
830
|
} else if (primary.kind === 'CronWorkflow') {
|
|
831
831
|
subtitle = (d.schedule as string) || ''
|
|
832
|
+
} else if (primary.kind === 'JobSet') {
|
|
833
|
+
subtitle = (d.phase as string) || ''
|
|
832
834
|
} else if (primary.kind === 'Job') {
|
|
833
835
|
subtitle = (d.phase as string) || ''
|
|
834
836
|
} else if (primary.kind === 'Workflow') {
|
|
@@ -863,7 +865,7 @@ function computeWorkloadCards(
|
|
|
863
865
|
// Shared kind priority map — used by both pickGroupName and computeWorkloadCards
|
|
864
866
|
const KIND_PRIORITY: Record<string, number> = {
|
|
865
867
|
'Deployment': 1, 'Rollout': 1, 'StatefulSet': 2, 'DaemonSet': 3,
|
|
866
|
-
'CronJob': 4, 'CronWorkflow': 4, 'Job': 5, 'Workflow': 5, 'Service': 6, 'Gateway': 7,
|
|
868
|
+
'CronJob': 4, 'CronWorkflow': 4, 'JobSet': 4, 'Job': 5, 'Workflow': 5, 'Service': 6, 'Gateway': 7,
|
|
867
869
|
'HTTPRoute': 6, 'GRPCRoute': 6, 'TCPRoute': 6, 'TLSRoute': 6, 'UDPRoute': 6, 'Ingress': 7,
|
|
868
870
|
'CalicoNetworkPolicy': 10, 'CalicoGlobalNetworkPolicy': 10,
|
|
869
871
|
'CalicoStagedNetworkPolicy': 10, 'CalicoStagedGlobalNetworkPolicy': 10,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { expandPodGroup } from './TopologyGraph'
|
|
3
|
+
import type { TopologyEdge, TopologyNode } from '../../types'
|
|
4
|
+
|
|
5
|
+
function group(pods: Record<string, unknown>[]): TopologyNode {
|
|
6
|
+
return { id: 'podgroup/ml/train-workers-0', kind: 'PodGroup', name: 'train-workers-0', status: 'healthy', data: { namespace: 'ml', pods } }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const jobEdge: TopologyEdge = { id: 'job-to-group', source: 'job/ml/train-workers-0', target: 'podgroup/ml/train-workers-0', type: 'manages' }
|
|
10
|
+
const pod = (name: string, extra: Record<string, unknown> = {}) => ({ name, namespace: 'ml', phase: 'Running', restarts: 0, containers: 1, ...extra })
|
|
11
|
+
|
|
12
|
+
describe('expandPodGroup', () => {
|
|
13
|
+
it('preserves earlier-run context and each Pod termination state when expanded', () => {
|
|
14
|
+
const node = group([pod('old', { status: 'neutral', terminating: true }), pod('failed', { status: 'unhealthy', terminating: false })])
|
|
15
|
+
node.data.earlierJobRun = 'train-workers-0'
|
|
16
|
+
node.data.terminating = 1
|
|
17
|
+
const edge: TopologyEdge = { ...jobEdge, source: 'jobset/ml/train/jobset.x-k8s.io', type: 'uses', label: 'Earlier Job run (same name)' }
|
|
18
|
+
const out = expandPodGroup([node], [edge], node.id)
|
|
19
|
+
expect(out.nodes.map(n => [n.status, n.data.earlierJobRun, n.data.terminating])).toEqual([
|
|
20
|
+
['neutral', 'train-workers-0', 1], ['unhealthy', 'train-workers-0', 0],
|
|
21
|
+
])
|
|
22
|
+
expect(out.edges.every(e => e.type === 'uses' && e.label === edge.label)).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
it('connects a pod without ownerIds to every owner of the group', () => {
|
|
25
|
+
const out = expandPodGroup([group([pod('a')])], [jobEdge], 'podgroup/ml/train-workers-0')
|
|
26
|
+
expect(out.edges.map((e) => [e.source, e.target])).toEqual([['job/ml/train-workers-0', 'pod/ml/a']])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('connects a pod with empty ownerIds to none of the group\'s owners', () => {
|
|
30
|
+
const out = expandPodGroup(
|
|
31
|
+
[group([pod('current', { ownerIds: ['job/ml/train-workers-0'] }), pod('earlier', { ownerIds: [] })])],
|
|
32
|
+
[jobEdge],
|
|
33
|
+
'podgroup/ml/train-workers-0',
|
|
34
|
+
)
|
|
35
|
+
expect(out.nodes.map((n) => n.id).sort()).toEqual(['pod/ml/current', 'pod/ml/earlier'])
|
|
36
|
+
expect(out.edges.map((e) => [e.source, e.target])).toEqual([['job/ml/train-workers-0', 'pod/ml/current']])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -95,6 +95,7 @@ const KIND: Record<string, string> = {
|
|
|
95
95
|
// Jobs
|
|
96
96
|
Job: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
97
97
|
CronJob: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
98
|
+
JobSet: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
98
99
|
Workflow: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
99
100
|
CronWorkflow: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
100
101
|
WorkflowTemplate: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
@@ -81,6 +81,14 @@ function radarUpgradeNeed(requirement: RadarUpgradeRequirement): string {
|
|
|
81
81
|
return current ? `${need} It runs ${current}.` : need
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The inline note's sentence as plain text, for the tooltip of a control the
|
|
86
|
+
* feature backs: "Argo CD rollbacks need Radar v1.10 or newer on this cluster."
|
|
87
|
+
*/
|
|
88
|
+
export function radarUpgradeNoteText(requirement: RadarUpgradeRequirement): string {
|
|
89
|
+
return `${requirement.feature} ${radarUpgradeNeed(requirement)}`
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
/** The upgrade call to action, or nothing when the host offers no way to upgrade. */
|
|
85
93
|
export function RadarUpgradeAction({ requirement }: { requirement: RadarUpgradeRequirement }) {
|
|
86
94
|
const { onRequestUpgrade, upgradeHref } = useContext(RadarUpgradeContext)
|
|
@@ -1016,7 +1016,7 @@ function ownershipUnanswered(coverage: OwnershipCoverage | undefined): string |
|
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
1018
|
// ownerNote says why no owner is named, when there is an owner reference.
|
|
1019
|
-
function ownerNote(coverage: OwnershipCoverage | undefined): RelationshipNote | undefined {
|
|
1019
|
+
export function ownerNote(coverage: OwnershipCoverage | undefined): RelationshipNote | undefined {
|
|
1020
1020
|
const ref = coverage?.ownerRef
|
|
1021
1021
|
const named = ref?.kind && ref.name ? `${ref.kind} ${ref.name}` : 'the owner it names'
|
|
1022
1022
|
switch (coverage?.primaryOwnerState) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import clsx from 'clsx'
|
|
3
|
+
import { Terminal } from 'lucide-react'
|
|
3
4
|
import { Collapse } from '../ui/Collapse'
|
|
4
5
|
import type { WorkloadPodInfo } from '../../types'
|
|
5
6
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
@@ -12,6 +13,8 @@ export function PodRow({
|
|
|
12
13
|
healthLevel,
|
|
13
14
|
detail,
|
|
14
15
|
onNavigate,
|
|
16
|
+
onOpenLogs,
|
|
17
|
+
logsUnavailable,
|
|
15
18
|
}: {
|
|
16
19
|
name: string
|
|
17
20
|
namespace: string
|
|
@@ -19,6 +22,10 @@ export function PodRow({
|
|
|
19
22
|
healthLevel?: WorkloadPodInfo['healthLevel']
|
|
20
23
|
detail: string
|
|
21
24
|
onNavigate?: NavigateToResource
|
|
25
|
+
/** Adds a Logs button beside the row. */
|
|
26
|
+
onOpenLogs?: () => void
|
|
27
|
+
/** With `onOpenLogs`, shows this reason in place of the Logs button. */
|
|
28
|
+
logsUnavailable?: string
|
|
22
29
|
}) {
|
|
23
30
|
const statusLabel = podStatusLabel(healthLevel, ready)
|
|
24
31
|
const statusClass = podStatusClass(healthLevel, ready)
|
|
@@ -35,15 +42,11 @@ export function PodRow({
|
|
|
35
42
|
</>
|
|
36
43
|
)
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return (
|
|
45
|
+
const row = !onNavigate ? (
|
|
46
|
+
<div className="flex w-full min-w-0 items-center gap-3 rounded-md border border-theme-border bg-theme-base px-3 py-2 text-left">
|
|
47
|
+
{content}
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
47
50
|
<button
|
|
48
51
|
type="button"
|
|
49
52
|
onClick={() => onNavigate({ kind: 'pods', namespace, name })}
|
|
@@ -52,6 +55,29 @@ export function PodRow({
|
|
|
52
55
|
{content}
|
|
53
56
|
</button>
|
|
54
57
|
)
|
|
58
|
+
if (!onOpenLogs) return row
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex min-w-0 items-stretch gap-2">
|
|
62
|
+
<div className="min-w-0 flex-1">{row}</div>
|
|
63
|
+
{logsUnavailable ? (
|
|
64
|
+
<span className="flex w-24 shrink-0 items-center justify-center px-2 text-center text-xs text-theme-text-tertiary">
|
|
65
|
+
<span className="sr-only">Logs for {name}: </span>
|
|
66
|
+
{logsUnavailable}
|
|
67
|
+
</span>
|
|
68
|
+
) : (
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={onOpenLogs}
|
|
72
|
+
aria-label={`Open logs for ${name}`}
|
|
73
|
+
className="btn-secondary inline-flex min-w-24 shrink-0 items-center justify-center gap-1.5 rounded-md px-3 text-xs font-medium"
|
|
74
|
+
>
|
|
75
|
+
<Terminal className="h-3.5 w-3.5" />
|
|
76
|
+
Logs
|
|
77
|
+
</button>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
export function PodListFrame({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { act } from 'react'
|
|
2
|
+
import { act, useEffect } from 'react'
|
|
3
3
|
import { createRoot } from 'react-dom/client'
|
|
4
4
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
5
5
|
import type { ReactNode } from 'react'
|
|
@@ -147,3 +147,43 @@ it.each(['Deployment', 'Pod', 'Pooler', 'ScheduledBackup'])('groups %s drawer ac
|
|
|
147
147
|
if (kind === 'ScheduledBackup') { expect(object.querySelectorAll('button')).toHaveLength(3); expect(object.textContent).toContain('Run blocked: no backup destination') }
|
|
148
148
|
act(() => root.unmount())
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
it('does not carry a picked Pod to the next resource the view shows', () => {
|
|
152
|
+
const pods = (name: string) => ({ pods: [{ kind: 'Pod', namespace: 'db', name }] })
|
|
153
|
+
const seen: (string | null)[] = []
|
|
154
|
+
const Probe = ({ selectedPod, onSelectPod }: { selectedPod: string | null; onSelectPod: (pod: string | null) => void }) => {
|
|
155
|
+
useEffect(() => { seen.push(selectedPod) })
|
|
156
|
+
return <button data-pick onClick={() => onSelectPod('pg-1')}>pick</button>
|
|
157
|
+
}
|
|
158
|
+
const renderLogsTab = (tab: { selectedPod: string | null; onSelectPod: (pod: string | null) => void }) => <Probe {...tab} />
|
|
159
|
+
const host = document.createElement('div')
|
|
160
|
+
const root = createRoot(host)
|
|
161
|
+
act(() => root.render(<WorkloadView {...props} activeTab="logs" relationships={pods('pg-1')} renderLogsTab={renderLogsTab} />))
|
|
162
|
+
act(() => host.querySelector<HTMLButtonElement>('[data-pick]')!.click())
|
|
163
|
+
expect(seen.at(-1)).toBe('pg-1')
|
|
164
|
+
const before = seen.length
|
|
165
|
+
act(() => root.render(<WorkloadView {...props} name="other" activeTab="logs" relationships={pods('other-1')} renderLogsTab={renderLogsTab} />))
|
|
166
|
+
expect(seen.slice(before)).not.toContain('pg-1')
|
|
167
|
+
act(() => root.unmount())
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('does not carry a picked Pod to a recreated resource with the same name', () => {
|
|
171
|
+
const relationships = { pods: [{ kind: 'Pod', namespace: 'db', name: 'pg-1' }] }
|
|
172
|
+
const seen: (string | null)[] = []
|
|
173
|
+
const Probe = ({ selectedPod, onSelectPod }: { selectedPod: string | null; onSelectPod: (pod: string | null) => void }) => {
|
|
174
|
+
useEffect(() => { seen.push(selectedPod) })
|
|
175
|
+
return <button data-pick onClick={() => onSelectPod('pg-1')}>pick</button>
|
|
176
|
+
}
|
|
177
|
+
const renderLogsTab = (tab: { selectedPod: string | null; onSelectPod: (pod: string | null) => void }) => <Probe {...tab} />
|
|
178
|
+
const host = document.createElement('div')
|
|
179
|
+
const root = createRoot(host)
|
|
180
|
+
const withUID = (uid: string) => ({ ...resource, metadata: { ...resource.metadata, uid } })
|
|
181
|
+
act(() => root.render(<WorkloadView {...props} resource={withUID('a')} activeTab="logs" relationships={relationships} renderLogsTab={renderLogsTab} />))
|
|
182
|
+
act(() => host.querySelector<HTMLButtonElement>('[data-pick]')!.click())
|
|
183
|
+
act(() => root.render(<WorkloadView {...props} resource={withUID('a')} activeTab="logs" relationships={relationships} renderLogsTab={renderLogsTab} />))
|
|
184
|
+
expect(seen.at(-1)).toBe('pg-1')
|
|
185
|
+
const before = seen.length
|
|
186
|
+
act(() => root.render(<WorkloadView {...props} resource={withUID('b')} activeTab="logs" relationships={relationships} renderLogsTab={renderLogsTab} />))
|
|
187
|
+
expect(seen.slice(before)).not.toContain('pg-1')
|
|
188
|
+
act(() => root.unmount())
|
|
189
|
+
})
|