@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
|
@@ -30,6 +30,9 @@ import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
|
30
30
|
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
31
31
|
import { displayKindName } from '../ui/drawer-components'
|
|
32
32
|
import { getDefaultContainerName, getExecContainerNames } from '../resources/resource-utils'
|
|
33
|
+
import { argoApplicationSyncOptions, argoComparedRevisionCurrent, isArgoAutoSyncEnabled, isArgoAutoSyncSwitchedOff, isArgoMultiSource, isArgoOperationInProgress } from '../resources/resource-utils-argo'
|
|
34
|
+
import { SyncOptionsDialog, type ArgoSyncOpts } from '../gitops/SyncOptionsDialog'
|
|
35
|
+
import { radarUpgradeNoteText } from '../ui/RadarUpgradeNote'
|
|
33
36
|
import { SetImageDialog, type SetImageOwnership } from './SetImageDialog'
|
|
34
37
|
import { DrainPlanDialog, DEFAULT_DRAIN_DIALOG_OPTIONS, type DrainDialogOptions, type DrainPlan } from './DrainPlanDialog'
|
|
35
38
|
import type { WorkloadImageInventory, WorkloadImageUpdate } from '../../types/core'
|
|
@@ -129,14 +132,25 @@ interface ResourceActionsBarProps {
|
|
|
129
132
|
isFluxResuming?: boolean
|
|
130
133
|
|
|
131
134
|
// Argo actions
|
|
132
|
-
|
|
135
|
+
// May return a promise that settles with the request: the options dialog
|
|
136
|
+
// then stays open, with its choices, when it rejects.
|
|
137
|
+
onArgoSync?: (params: { namespace: string; name: string; applicationSyncOptions?: string[] } & Partial<ArgoSyncOpts>) => void | Promise<unknown>
|
|
133
138
|
isArgoSyncing?: boolean
|
|
139
|
+
// Set when onArgoSync forwards the options in its params: Sync then opens
|
|
140
|
+
// the options dialog. Without it Sync syncs at once with the Application's
|
|
141
|
+
// own options. pinsComparedRevision: the Radar pins an omitted revision
|
|
142
|
+
// (argoSyncInheritance).
|
|
143
|
+
argoSyncDialog?: { pinsComparedRevision: boolean }
|
|
134
144
|
onArgoRefresh?: (params: { namespace: string; name: string; hard: boolean }) => void
|
|
135
145
|
isArgoRefreshing?: boolean
|
|
136
146
|
onArgoSuspend?: (params: { namespace: string; name: string }) => void
|
|
137
147
|
isArgoSuspending?: boolean
|
|
138
148
|
onArgoResume?: (params: { namespace: string; name: string }) => void
|
|
139
149
|
isArgoResuming?: boolean
|
|
150
|
+
// Set when the connected Radar is too old to turn auto-sync back on for an
|
|
151
|
+
// Application with automated.enabled: false; Enable Auto-Sync renders disabled.
|
|
152
|
+
argoResumeUpgrade?: RadarUpgradeRequirement | null
|
|
153
|
+
argoResumeBlockedReason?: string
|
|
140
154
|
|
|
141
155
|
// Node debug shell
|
|
142
156
|
onOpenNodeTerminal?: (params: { nodeName: string }) => void
|
|
@@ -183,10 +197,10 @@ export function ResourceActionsBar({
|
|
|
183
197
|
onFluxSyncWithSource, isFluxSyncing,
|
|
184
198
|
onFluxSuspend, isFluxSuspending,
|
|
185
199
|
onFluxResume, isFluxResuming,
|
|
186
|
-
onArgoSync, isArgoSyncing,
|
|
200
|
+
onArgoSync, isArgoSyncing, argoSyncDialog,
|
|
187
201
|
onArgoRefresh, isArgoRefreshing,
|
|
188
202
|
onArgoSuspend, isArgoSuspending,
|
|
189
|
-
onArgoResume, isArgoResuming,
|
|
203
|
+
onArgoResume, isArgoResuming, argoResumeUpgrade, argoResumeBlockedReason,
|
|
190
204
|
onOpenNodeTerminal,
|
|
191
205
|
canNodeWrite,
|
|
192
206
|
onCordonNode, isCordoningNode,
|
|
@@ -556,12 +570,15 @@ export function ResourceActionsBar({
|
|
|
556
570
|
data={data}
|
|
557
571
|
onSync={onArgoSync}
|
|
558
572
|
isSyncing={isArgoSyncing}
|
|
573
|
+
syncDialog={argoSyncDialog}
|
|
559
574
|
onRefresh={onArgoRefresh}
|
|
560
575
|
isRefreshing={isArgoRefreshing}
|
|
561
576
|
onSuspend={onArgoSuspend}
|
|
562
577
|
isSuspending={isArgoSuspending}
|
|
563
578
|
onResume={onArgoResume}
|
|
564
579
|
isResuming={isArgoResuming}
|
|
580
|
+
resumeUpgrade={argoResumeUpgrade}
|
|
581
|
+
resumeBlockedReason={argoResumeBlockedReason}
|
|
565
582
|
/>
|
|
566
583
|
)}
|
|
567
584
|
|
|
@@ -881,32 +898,71 @@ function FluxActions({ resource, data, onReconcile, isReconciling, onSyncWithSou
|
|
|
881
898
|
// ARGO ACTIONS
|
|
882
899
|
// ============================================================================
|
|
883
900
|
|
|
884
|
-
function ArgoActions({ resource, data, onSync, isSyncing, onRefresh, isRefreshing, onSuspend, isSuspending, onResume, isResuming }: {
|
|
901
|
+
function ArgoActions({ resource, data, onSync, isSyncing, syncDialog, onRefresh, isRefreshing, onSuspend, isSuspending, onResume, isResuming, resumeUpgrade, resumeBlockedReason }: {
|
|
885
902
|
resource: SelectedResource; data: any
|
|
886
|
-
onSync?: (params: { namespace: string; name: string }) => void
|
|
903
|
+
onSync?: (params: { namespace: string; name: string; applicationSyncOptions?: string[] } & Partial<ArgoSyncOpts>) => void | Promise<unknown>; isSyncing?: boolean
|
|
904
|
+
syncDialog?: { pinsComparedRevision: boolean }
|
|
887
905
|
onRefresh?: (params: { namespace: string; name: string; hard: boolean }) => void; isRefreshing?: boolean
|
|
888
906
|
onSuspend?: (params: { namespace: string; name: string }) => void; isSuspending?: boolean
|
|
889
907
|
onResume?: (params: { namespace: string; name: string }) => void; isResuming?: boolean
|
|
908
|
+
resumeUpgrade?: RadarUpgradeRequirement | null
|
|
909
|
+
resumeBlockedReason?: string
|
|
890
910
|
}) {
|
|
891
|
-
const
|
|
911
|
+
const autoSyncEnabled = isArgoAutoSyncEnabled(data)
|
|
912
|
+
const resumeBlocked = isArgoAutoSyncSwitchedOff(data)
|
|
913
|
+
? resumeBlockedReason || (resumeUpgrade ? radarUpgradeNoteText(resumeUpgrade) : undefined)
|
|
914
|
+
: undefined
|
|
915
|
+
|
|
916
|
+
// The drawer can switch Applications under an open dialog; its choices
|
|
917
|
+
// belong to the one it was opened for, so it closes on any other.
|
|
918
|
+
const appKey = `${resource.namespace}/${resource.name}`
|
|
919
|
+
const [syncDialogFor, setSyncDialogFor] = useState<string | null>(null)
|
|
920
|
+
if (syncDialogFor !== null && syncDialogFor !== appKey) setSyncDialogFor(null)
|
|
921
|
+
const syncDialogOpen = syncDialogFor === appKey
|
|
922
|
+
const [syncError, setSyncError] = useState<string | null>(null)
|
|
923
|
+
const operationRunning = isArgoOperationInProgress(data)
|
|
924
|
+
const applicationSyncOptions = argoApplicationSyncOptions(data)
|
|
925
|
+
// A failed request is reported by the host; it only keeps the dialog open.
|
|
926
|
+
const sync = (opts?: ArgoSyncOpts) => Promise.resolve(onSync?.({ namespace: resource.namespace, name: resource.name, applicationSyncOptions, ...opts }))
|
|
892
927
|
|
|
893
928
|
return (
|
|
894
929
|
<>
|
|
895
930
|
{onSync && (
|
|
896
|
-
<Tooltip content="Sync
|
|
931
|
+
<Tooltip content={operationRunning ? 'Wait for the current Argo operation to finish.' : syncDialog ? 'Apply manifests from Git to the cluster. Opens an options dialog (prune, dry-run, revision).' : "Syncs with the Application's own sync options, without pruning. For prune, dry run or a revision, use Sync… on its GitOps page."} delay={150}>
|
|
897
932
|
<button
|
|
898
|
-
onClick={() =>
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
933
|
+
onClick={() => {
|
|
934
|
+
if (!syncDialog) return void sync().catch(() => {})
|
|
935
|
+
setSyncError(null)
|
|
936
|
+
setSyncDialogFor(appKey)
|
|
937
|
+
}}
|
|
938
|
+
disabled={isSyncing || operationRunning}
|
|
903
939
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand rounded-lg"
|
|
904
940
|
>
|
|
905
941
|
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
|
|
906
|
-
{isSyncing ? 'Syncing...' : 'Sync'}
|
|
942
|
+
{isSyncing ? 'Syncing...' : syncDialog ? 'Sync…' : 'Sync'}
|
|
907
943
|
</button>
|
|
908
944
|
</Tooltip>
|
|
909
945
|
)}
|
|
946
|
+
{onSync && syncDialog && (
|
|
947
|
+
<SyncOptionsDialog
|
|
948
|
+
key={appKey}
|
|
949
|
+
open={syncDialogOpen}
|
|
950
|
+
appLabel={`${resource.namespace}/${resource.name}`}
|
|
951
|
+
multiSource={isArgoMultiSource(data)}
|
|
952
|
+
comparedRevision={data?.status?.sync?.revision}
|
|
953
|
+
pinsComparedRevision={syncDialog.pinsComparedRevision && argoComparedRevisionCurrent(data)}
|
|
954
|
+
applicationSyncOptions={applicationSyncOptions}
|
|
955
|
+
pending={isSyncing}
|
|
956
|
+
autoSyncEnabled={isArgoAutoSyncEnabled(data)}
|
|
957
|
+
operationInProgress={operationRunning}
|
|
958
|
+
syncError={syncError}
|
|
959
|
+
onCancel={() => setSyncDialogFor(null)}
|
|
960
|
+
onConfirm={(opts) => {
|
|
961
|
+
setSyncError(null)
|
|
962
|
+
sync(opts).then(() => setSyncDialogFor(null), (error: unknown) => setSyncError(error instanceof Error ? error.message : 'the request was refused'))
|
|
963
|
+
}}
|
|
964
|
+
/>
|
|
965
|
+
)}
|
|
910
966
|
|
|
911
967
|
{onRefresh && (
|
|
912
968
|
<Tooltip content="Refresh (re-read from git)" delay={150}>
|
|
@@ -925,7 +981,7 @@ function ArgoActions({ resource, data, onSync, isSyncing, onRefresh, isRefreshin
|
|
|
925
981
|
</Tooltip>
|
|
926
982
|
)}
|
|
927
983
|
|
|
928
|
-
{
|
|
984
|
+
{autoSyncEnabled ? (
|
|
929
985
|
onSuspend && (
|
|
930
986
|
<button
|
|
931
987
|
onClick={() => onSuspend({
|
|
@@ -941,17 +997,19 @@ function ArgoActions({ resource, data, onSync, isSyncing, onRefresh, isRefreshin
|
|
|
941
997
|
)
|
|
942
998
|
) : (
|
|
943
999
|
onResume && (
|
|
944
|
-
<
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1000
|
+
<Tooltip content={resumeBlocked} disabled={!resumeBlocked} delay={150}>
|
|
1001
|
+
<button
|
|
1002
|
+
onClick={() => !resumeBlocked && onResume({
|
|
1003
|
+
namespace: resource.namespace,
|
|
1004
|
+
name: resource.name,
|
|
1005
|
+
})}
|
|
1006
|
+
disabled={isResuming || !!resumeBlocked}
|
|
1007
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1008
|
+
>
|
|
1009
|
+
<Play className="w-3.5 h-3.5" />
|
|
1010
|
+
{isResuming ? 'Enabling...' : 'Enable Auto-Sync'}
|
|
1011
|
+
</button>
|
|
1012
|
+
</Tooltip>
|
|
955
1013
|
)
|
|
956
1014
|
)}
|
|
957
1015
|
</>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { HEALTH_BADGE_COLORS } from '../../utils/badge-colors'
|
|
2
|
+
import { getGenericResourceStatus } from '../resources/generic-status'
|
|
1
3
|
import { describe, expect, it } from 'vitest'
|
|
2
4
|
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from './ResourceRendererDispatch'
|
|
5
|
+
import { ResourceRendererDispatch, diagnoseHealthHint, getResourceStatus, type RendererOverrides } from './ResourceRendererDispatch'
|
|
4
6
|
import { getCellFilterValue } from '../resources/resource-utils'
|
|
5
7
|
import type { ResourceRef } from '../../types'
|
|
6
8
|
|
|
@@ -1027,11 +1029,11 @@ describe('Kueue Workload detail dispatch', () => {
|
|
|
1027
1029
|
describe('Kueue queue detail dispatch', () => {
|
|
1028
1030
|
it.each(['localqueues', 'clusterqueues'])('curates supported %s and preserves foreign and future versions', (kind) => {
|
|
1029
1031
|
for (const version of ['v1beta1', 'v1beta2']) {
|
|
1030
|
-
expect(renderKind(kind, { apiVersion: `kueue.x-k8s.io/${version}`, spec: {} })).toContain('
|
|
1032
|
+
expect(renderKind(kind, { apiVersion: `kueue.x-k8s.io/${version}`, spec: {} })).toContain('Reported by controller')
|
|
1031
1033
|
}
|
|
1032
1034
|
for (const apiVersion of ['example.com/v1', 'fake.kueue.x-k8s.io/v1', 'kueue.x-k8s.io/v99']) {
|
|
1033
1035
|
const html = renderKind(kind, { apiVersion, spec: { foreignField: 'preserved-value' } })
|
|
1034
|
-
expect(html).not.toContain('
|
|
1036
|
+
expect(html).not.toContain('Reported by controller')
|
|
1035
1037
|
expect(html).toContain('preserved-value')
|
|
1036
1038
|
}
|
|
1037
1039
|
})
|
|
@@ -1136,3 +1138,35 @@ describe('Flux kind collisions', () => {
|
|
|
1136
1138
|
}
|
|
1137
1139
|
})
|
|
1138
1140
|
})
|
|
1141
|
+
|
|
1142
|
+
describe('Strimzi KafkaConnector exact group', () => {
|
|
1143
|
+
const data = {apiVersion: 'kafka.strimzi.io/v1', kind: 'KafkaConnector', metadata: {name:'connector',generation:1}, spec: {class:'Example'}, status:{observedGeneration:1,conditions:[{type:'Ready',status:'True'}],connectorStatus:{connector:{state:'RUNNING'},tasks:[{id:0,state:'FAILED'}]}}}
|
|
1144
|
+
it('shows failed tasks despite Ready=True', () => {
|
|
1145
|
+
expect(getResourceStatus('kafkaconnectors', data)?.text).toBe('Tasks failed')
|
|
1146
|
+
expect(renderKind('kafkaconnectors', data)).toContain('Reported Runtime')
|
|
1147
|
+
})
|
|
1148
|
+
it('leaves unrelated and lookalike groups generic', () => {
|
|
1149
|
+
for (const apiVersion of ['other.io/v1', 'fake.kafka.strimzi.io/v1']) {
|
|
1150
|
+
const other = {...data,apiVersion}
|
|
1151
|
+
expect(getResourceStatus('kafkaconnectors',other)?.text).not.toBe('Tasks failed')
|
|
1152
|
+
expect(renderKind('kafkaconnectors',other)).not.toContain('Reported Runtime')
|
|
1153
|
+
}
|
|
1154
|
+
})
|
|
1155
|
+
})
|
|
1156
|
+
|
|
1157
|
+
it('keeps foreign customresourcedefinitions on generic renderer and status', () => {
|
|
1158
|
+
const data = { apiVersion: 'foreign.io/v1', kind: 'CustomResourceDefinition', metadata: { name: 'foreign' }, spec: { example: 'generic-sentinel' }, status: { conditions: [{ type: 'Established', status: 'True' }] } }
|
|
1159
|
+
const html = renderToString(<ResourceRendererDispatch resource={{ kind: 'customresourcedefinitions', name: 'foreign', namespace: '' }} data={data} onCopy={() => {}} copied={null} showCommonSections={false} />)
|
|
1160
|
+
expect(html).toContain('generic-sentinel')
|
|
1161
|
+
expect(html).not.toContain('Names & conversion')
|
|
1162
|
+
expect(getGenericResourceStatus(data)).toEqual({ text: 'Established', tone: 'unknown' })
|
|
1163
|
+
expect(getResourceStatus('customresourcedefinitions', data)).toEqual({ text: 'Established', color: HEALTH_BADGE_COLORS.unknown })
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
it('gives Diagnose the CRD health the header badge shows', () => {
|
|
1167
|
+
const crd = (conditions: any[], extra: any = {}) => ({ apiVersion: 'apiextensions.k8s.io/v1', kind: 'CustomResourceDefinition', metadata: { name: 'widgets.example.io', ...extra }, status: { conditions } })
|
|
1168
|
+
expect(diagnoseHealthHint('customresourcedefinitions', crd([{ type: 'Established', status: 'True' }]))).toBe('healthy')
|
|
1169
|
+
expect(diagnoseHealthHint('customresourcedefinitions', crd([{ type: 'NamesAccepted', status: 'False', reason: 'ListKindConflict' }]))).toBe('problem')
|
|
1170
|
+
expect(diagnoseHealthHint('customresourcedefinitions', crd([{ type: 'Established', status: 'True' }], { deletionTimestamp: '2026-01-01T00:00:00Z' }))).toBe('problem')
|
|
1171
|
+
expect(diagnoseHealthHint('customresourcedefinitions', crd([]))).toBe('unknown')
|
|
1172
|
+
})
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { getCRDStatus } from '../resources/resource-utils-crd'
|
|
2
|
+
import { getStrimziConnectorStatus } from '../resources/resource-utils-strimzi'
|
|
3
|
+
import { StrimziConnectorRenderer } from '../resources/renderers/StrimziConnectorRenderer'
|
|
1
4
|
import { getOpenTelemetryCollectorStatus, getOpenTelemetryInstrumentationStatus } from '../resources/resource-utils-opentelemetry'
|
|
2
5
|
import { OpenTelemetryCollectorRenderer, OpenTelemetryInstrumentationRenderer } from '../resources/renderers/OpenTelemetryRenderers'
|
|
3
6
|
import { FlaggerCanaryRenderer, FlaggerMetricTemplateRenderer } from '../resources/renderers/FlaggerRenderers'
|
|
@@ -132,6 +135,7 @@ import {
|
|
|
132
135
|
CronJobRenderer,
|
|
133
136
|
CronWorkflowRenderer,
|
|
134
137
|
HPARenderer,
|
|
138
|
+
CustomResourceDefinitionRenderer,
|
|
135
139
|
NodeRenderer,
|
|
136
140
|
PVCRenderer,
|
|
137
141
|
RolloutRenderer,
|
|
@@ -321,13 +325,21 @@ import type { ScalerDiagnosis } from '../resources/renderers/WorkloadRenderer'
|
|
|
321
325
|
* When an override is not provided, the base (shared) renderer is used.
|
|
322
326
|
*/
|
|
323
327
|
export interface RendererOverrides {
|
|
328
|
+
CustomResourceDefinitionRenderer?: React.ComponentType<{
|
|
329
|
+
data: any
|
|
330
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
331
|
+
}>
|
|
332
|
+
|
|
333
|
+
OpenTelemetryCollectorRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void; active?: boolean }>
|
|
324
334
|
FluxHelmReleaseRenderer?: React.ComponentType<React.ComponentProps<typeof FluxHelmReleaseRenderer>>
|
|
325
335
|
JobRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
326
336
|
JobSetRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
327
337
|
RayJobRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
328
338
|
RayClusterRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
329
339
|
RayServiceRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
330
|
-
KueueWorkloadRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
340
|
+
KueueWorkloadRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void; relationships?: Relationships }>
|
|
341
|
+
LocalQueueRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
342
|
+
ClusterQueueRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
331
343
|
PodRenderer?: React.ComponentType<{
|
|
332
344
|
data: any; onCopy: CopyHandler; copied: string | null
|
|
333
345
|
onNavigate?: (ref: ResourceRef) => void
|
|
@@ -455,6 +467,8 @@ export interface RendererOverrides {
|
|
|
455
467
|
|
|
456
468
|
// Known resource types with specific renderers (module-level to avoid re-allocation)
|
|
457
469
|
const KNOWN_KINDS = new Set([
|
|
470
|
+
'customresourcedefinitions',
|
|
471
|
+
'kafkaconnectors',
|
|
458
472
|
'opentelemetrycollectors', 'instrumentations',
|
|
459
473
|
'canaries', 'metrictemplates',
|
|
460
474
|
'pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets',
|
|
@@ -558,6 +572,8 @@ function isCAPITopologyGroup(apiVersion?: string): boolean {
|
|
|
558
572
|
// ============================================================================
|
|
559
573
|
|
|
560
574
|
interface ResourceRendererDispatchProps {
|
|
575
|
+
/** Whether the containing resource view is active; hosts can pause their reads. */
|
|
576
|
+
active?: boolean
|
|
561
577
|
resource: SelectedResource
|
|
562
578
|
data: any
|
|
563
579
|
relationships?: Relationships
|
|
@@ -606,6 +622,7 @@ interface ResourceRendererDispatchProps {
|
|
|
606
622
|
}
|
|
607
623
|
|
|
608
624
|
export function ResourceRendererDispatch({
|
|
625
|
+
active = true,
|
|
609
626
|
resource,
|
|
610
627
|
data,
|
|
611
628
|
relationships,
|
|
@@ -716,6 +733,8 @@ export function ResourceRendererDispatch({
|
|
|
716
733
|
const isFluxKind = ['gitrepositories', 'ocirepositories', 'helmrepositories', 'kustomizations', 'helmreleases', 'alerts'].includes(kind)
|
|
717
734
|
const isFluxResource = getGitOpsTool(kind, apiVersionToGroup(data?.apiVersion)) === 'flux'
|
|
718
735
|
const isGroupGatedKind =
|
|
736
|
+
kind === 'customresourcedefinitions' ||
|
|
737
|
+
kind === 'kafkaconnectors' ||
|
|
719
738
|
['opentelemetrycollectors', 'instrumentations'].includes(kind) ||
|
|
720
739
|
['canaries', 'metrictemplates'].includes(kind) ||
|
|
721
740
|
isFluxKind || kind === 'applications' || kind === 'services' || kind === 'ingresses' || kind === 'certificates' || kind === 'gateways'
|
|
@@ -727,6 +746,8 @@ export function ResourceRendererDispatch({
|
|
|
727
746
|
|| kind === 'machines' || kind === 'machinesets' || kind === 'workloads' || kind === 'localqueues' || kind === 'clusterqueues'
|
|
728
747
|
const isCNPGApiVersion = isApiGroup(data?.apiVersion, CNPG_GROUP)
|
|
729
748
|
const groupGatedMatched =
|
|
749
|
+
(kind === 'customresourcedefinitions' && isApiGroup(data?.apiVersion, 'apiextensions.k8s.io')) ||
|
|
750
|
+
(kind === 'kafkaconnectors' && isApiGroup(data?.apiVersion, 'kafka.strimzi.io')) ||
|
|
730
751
|
(['opentelemetrycollectors', 'instrumentations'].includes(kind) && isApiGroup(data?.apiVersion, 'opentelemetry.io')) ||
|
|
731
752
|
(['canaries', 'metrictemplates'].includes(kind) && isApiGroup(data?.apiVersion, 'flagger.app')) ||
|
|
732
753
|
(isFluxKind && isFluxResource)
|
|
@@ -785,8 +806,11 @@ export function ResourceRendererDispatch({
|
|
|
785
806
|
const JobSetComp = rendererOverrides?.JobSetRenderer ?? JobSetRenderer
|
|
786
807
|
const RayJobComp = rendererOverrides?.RayJobRenderer ?? RayJobRenderer
|
|
787
808
|
const RayClusterComp = rendererOverrides?.RayClusterRenderer ?? RayClusterRenderer
|
|
809
|
+
const CollectorComp = rendererOverrides?.OpenTelemetryCollectorRenderer ?? OpenTelemetryCollectorRenderer
|
|
788
810
|
const RayServiceComp = rendererOverrides?.RayServiceRenderer ?? RayServiceRenderer
|
|
789
811
|
const KueueWorkloadComp = rendererOverrides?.KueueWorkloadRenderer ?? KueueWorkloadRenderer
|
|
812
|
+
const LocalQueueComp = rendererOverrides?.LocalQueueRenderer ?? LocalQueueRenderer
|
|
813
|
+
const ClusterQueueComp = rendererOverrides?.ClusterQueueRenderer ?? ClusterQueueRenderer
|
|
790
814
|
const PodComp = rendererOverrides?.PodRenderer ?? PodRenderer
|
|
791
815
|
const KarpenterNodePoolComp = rendererOverrides?.KarpenterNodePoolRenderer ?? KarpenterNodePoolRenderer
|
|
792
816
|
const WorkloadComp = rendererOverrides?.WorkloadRenderer ?? WorkloadRenderer
|
|
@@ -813,6 +837,7 @@ export function ResourceRendererDispatch({
|
|
|
813
837
|
const RoleBindingComp = rendererOverrides?.RoleBindingRenderer ?? RoleBindingRenderer
|
|
814
838
|
const NamespaceComp = rendererOverrides?.NamespaceRenderer ?? NamespaceRenderer
|
|
815
839
|
const CAPIClusterComp = rendererOverrides?.CAPIClusterRenderer ?? CAPIClusterRenderer
|
|
840
|
+
const CRDComp = rendererOverrides?.CustomResourceDefinitionRenderer ?? CustomResourceDefinitionRenderer
|
|
816
841
|
const HPAComp = rendererOverrides?.HPARenderer ?? HPARenderer
|
|
817
842
|
const PVCComp = rendererOverrides?.PVCRenderer ?? PVCRenderer
|
|
818
843
|
const RolloutComp = rendererOverrides?.RolloutRenderer ?? RolloutRenderer
|
|
@@ -858,11 +883,11 @@ export function ResourceRendererDispatch({
|
|
|
858
883
|
{kind === 'rayjobs' && data?.apiVersion === 'ray.io/v1' && <RayJobComp data={data} onNavigate={onNavigate} />}
|
|
859
884
|
{kind === 'rayclusters' && data?.apiVersion === 'ray.io/v1' && <RayClusterComp data={data} onNavigate={onNavigate} />}
|
|
860
885
|
{kind === 'rayservices' && data?.apiVersion === 'ray.io/v1' && <RayServiceComp data={data} onNavigate={onNavigate} />}
|
|
861
|
-
{kind === 'workloads' && isApiGroup(data?.apiVersion, 'kueue.x-k8s.io') && <KueueWorkloadComp data={data} onNavigate={onNavigate} />}
|
|
886
|
+
{kind === 'workloads' && isApiGroup(data?.apiVersion, 'kueue.x-k8s.io') && <KueueWorkloadComp data={data} onNavigate={onNavigate} relationships={relationships} />}
|
|
862
887
|
{kind === 'admissionchecks' && isKueueQueueResource(data) && <AdmissionCheckRenderer data={data} onNavigate={onNavigate} />}
|
|
863
888
|
{kind === 'provisioningrequests' && ['autoscaling.x-k8s.io/v1', 'autoscaling.x-k8s.io/v1beta1'].includes(data?.apiVersion) && <ProvisioningRequestRenderer data={data} onNavigate={onNavigate} />}
|
|
864
|
-
{kind === 'localqueues' && isKueueQueueResource(data) && <
|
|
865
|
-
{kind === 'clusterqueues' && isKueueQueueResource(data) && <
|
|
889
|
+
{kind === 'localqueues' && isKueueQueueResource(data) && <LocalQueueComp data={data} onNavigate={onNavigate} />}
|
|
890
|
+
{kind === 'clusterqueues' && isKueueQueueResource(data) && <ClusterQueueComp data={data} onNavigate={onNavigate} />}
|
|
866
891
|
{kind === 'jobsets' && isJobSetV1Alpha2(data) && <JobSetComp data={data} onNavigate={onNavigate} />}
|
|
867
892
|
{kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
|
|
868
893
|
{kind === 'cronworkflows' && <CronWorkflowRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -1006,9 +1031,10 @@ export function ResourceRendererDispatch({
|
|
|
1006
1031
|
{kind === 'serviceentries' && <IstioServiceEntryRenderer data={data} />}
|
|
1007
1032
|
{kind === 'peerauthentications' && <IstioPeerAuthenticationRenderer data={data} />}
|
|
1008
1033
|
{kind === 'authorizationpolicies' && <IstioAuthorizationPolicyRenderer data={data} />}
|
|
1009
|
-
{kind === 'mutatingwebhookconfigurations' && <WebhookConfigRenderer data={data} isMutating />}
|
|
1010
|
-
{kind === 'validatingwebhookconfigurations' && <WebhookConfigRenderer data={data} />}
|
|
1034
|
+
{kind === 'mutatingwebhookconfigurations' && <WebhookConfigRenderer data={data} isMutating onNavigate={onNavigate} />}
|
|
1035
|
+
{kind === 'validatingwebhookconfigurations' && <WebhookConfigRenderer data={data} onNavigate={onNavigate} />}
|
|
1011
1036
|
{kind === 'ingressclasses' && <IngressClassRenderer data={data} onNavigate={onNavigate} />}
|
|
1037
|
+
{kind === 'customresourcedefinitions' && isApiGroup(data?.apiVersion, 'apiextensions.k8s.io') && <CRDComp data={data} onNavigate={onNavigate} />}
|
|
1012
1038
|
{kind === 'priorityclasses' && <PriorityClassRenderer data={data} />}
|
|
1013
1039
|
{kind === 'runtimeclasses' && <RuntimeClassRenderer data={data} />}
|
|
1014
1040
|
{kind === 'leases' && <LeaseRenderer data={data} />}
|
|
@@ -1063,7 +1089,8 @@ export function ResourceRendererDispatch({
|
|
|
1063
1089
|
{isCrossplaneMR && <ManagedResourceRenderer data={data} onNavigate={onNavigate} />}
|
|
1064
1090
|
{(isCrossplaneXR || isCrossplaneClaim) && <CompositeComp data={data} onNavigate={onNavigate} />}
|
|
1065
1091
|
|
|
1066
|
-
{kind === 'opentelemetrycollectors' && isApiGroup(data?.apiVersion, 'opentelemetry.io') && <
|
|
1092
|
+
{kind === 'opentelemetrycollectors' && isApiGroup(data?.apiVersion, 'opentelemetry.io') && <CollectorComp data={data} onNavigate={onNavigate} active={active} />}
|
|
1093
|
+
{kind === 'kafkaconnectors' && isApiGroup(data?.apiVersion, 'kafka.strimzi.io') && <StrimziConnectorRenderer data={data} onNavigate={onNavigate} />}
|
|
1067
1094
|
{kind === 'instrumentations' && isApiGroup(data?.apiVersion, 'opentelemetry.io') && <OpenTelemetryInstrumentationRenderer data={data} />}
|
|
1068
1095
|
{kind === 'canaries' && isApiGroup(data?.apiVersion, 'flagger.app') && <FlaggerCanaryRenderer data={data} onNavigate={onNavigate} />}
|
|
1069
1096
|
{kind === 'metrictemplates' && isApiGroup(data?.apiVersion, 'flagger.app') && <FlaggerMetricTemplateRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -1124,6 +1151,12 @@ export function diagnoseHealthHint(kind: string, data: any): DiagnoseHealthHint
|
|
|
1124
1151
|
export function getResourceStatus(kind: string, data: any): { text: string; color: string } | null {
|
|
1125
1152
|
if (!data) return null
|
|
1126
1153
|
const k = kind.toLowerCase()
|
|
1154
|
+
if (k === 'customresourcedefinitions' && isApiGroup(data.apiVersion, 'apiextensions.k8s.io')) {
|
|
1155
|
+
const status = getCRDStatus(data)
|
|
1156
|
+
const badge = { text: status.text, color: HEALTH_BADGE_COLORS[status.tone], level: status.tone }
|
|
1157
|
+
return badge
|
|
1158
|
+
}
|
|
1159
|
+
if (k === 'kafkaconnectors' && isApiGroup(data.apiVersion, 'kafka.strimzi.io')) return getStrimziConnectorStatus(data)
|
|
1127
1160
|
|
|
1128
1161
|
if (isApiGroup(data.apiVersion, 'opentelemetry.io')) {
|
|
1129
1162
|
if (k === 'opentelemetrycollectors') return getOpenTelemetryCollectorStatus(data)
|
|
@@ -2,6 +2,10 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { baseSubtitle, knativeDomainsTooltipLines } from './K8sResourceNode'
|
|
3
3
|
|
|
4
4
|
describe('baseSubtitle', () => {
|
|
5
|
+
it('explains earlier Job runs on grouped and loose Pods', () => {
|
|
6
|
+
expect(baseSubtitle('PodGroup', { earlierJobRun: 'train-workers-0', terminating: 6 })).toBe('Earlier run • terminating')
|
|
7
|
+
expect(baseSubtitle('Pod', { earlierJobRun: 'hook', terminating: 0 })).toBe('Earlier run')
|
|
8
|
+
})
|
|
5
9
|
it('describes workflow templates without dropping newer topology kinds', () => {
|
|
6
10
|
expect(baseSubtitle('WorkflowTemplate', { entrypoint: 'main', templateCount: 3 })).toBe('main • 3 templates')
|
|
7
11
|
expect(baseSubtitle('ClusterWorkflowTemplate', { templateCount: 2 })).toBe('2 templates')
|
|
@@ -9,6 +13,16 @@ describe('baseSubtitle', () => {
|
|
|
9
13
|
expect(baseSubtitle('ServiceMonitor', { endpointCount: 1 })).toBe('1 scrape endpoint')
|
|
10
14
|
})
|
|
11
15
|
|
|
16
|
+
it('describes a JobSet by its lifecycle and the Jobs it declares, not Jobs it has created', () => {
|
|
17
|
+
const apiVersion = 'jobset.x-k8s.io/v1alpha2'
|
|
18
|
+
expect(baseSubtitle('JobSet', { apiVersion, phase: 'Active', declaredJobs: 8 })).toBe('Active • 8 Jobs declared')
|
|
19
|
+
expect(baseSubtitle('JobSet', { apiVersion, phase: 'Pending', declaredJobs: 1 })).toBe('Pending • 1 Job declared')
|
|
20
|
+
expect(baseSubtitle('JobSet', { apiVersion, phase: 'Unknown' })).toBe('Unknown')
|
|
21
|
+
expect(baseSubtitle('JobSet', { apiVersion })).toBe('')
|
|
22
|
+
// Another group's JobSet has no lifecycle fields to describe.
|
|
23
|
+
expect(baseSubtitle('JobSet', { apiVersion: 'jobset.example.com/v1' })).toBe('')
|
|
24
|
+
})
|
|
25
|
+
|
|
12
26
|
it('describes a Service by its first port, capping additional ports to a count', () => {
|
|
13
27
|
expect(baseSubtitle('Service', { type: 'ClusterIP', ports: [] })).toBe('ClusterIP')
|
|
14
28
|
expect(baseSubtitle('Service', { type: 'ClusterIP', ports: [{ port: 80 }] })).toBe('ClusterIP :80')
|
|
@@ -267,6 +267,7 @@ export const NODE_DIMENSIONS: Record<
|
|
|
267
267
|
HorizontalPodAutoscaler: { width: 280, height: 84 },
|
|
268
268
|
Job: { width: 300, height: 84 },
|
|
269
269
|
CronJob: { width: 200, height: 84 },
|
|
270
|
+
JobSet: { width: 260, height: 84 },
|
|
270
271
|
Workflow: { width: 400, height: 84 },
|
|
271
272
|
CronWorkflow: { width: 240, height: 84 },
|
|
272
273
|
WorkflowTemplate: { width: 240, height: 84 },
|
|
@@ -391,6 +392,10 @@ export interface ServicePortEntry {
|
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
export function baseSubtitle(kind: NodeKind, nodeData: Record<string, unknown>): string {
|
|
395
|
+
if (typeof nodeData.earlierJobRun === 'string') {
|
|
396
|
+
const state = Number(nodeData.terminating) > 0 ? ' • terminating' : ''
|
|
397
|
+
return `Earlier run${state}`
|
|
398
|
+
}
|
|
394
399
|
if (nodeData.deploymentMembership === 'source-only') {
|
|
395
400
|
return `Declared by ${nodeData.deploymentSourceLabel ?? 'deployment source'}`
|
|
396
401
|
}
|
|
@@ -535,6 +540,18 @@ export function baseSubtitle(kind: NodeKind, nodeData: Record<string, unknown>):
|
|
|
535
540
|
const instanceType = nodeData.instanceType as string;
|
|
536
541
|
return instanceType || "";
|
|
537
542
|
}
|
|
543
|
+
case "JobSet": {
|
|
544
|
+
// Another API group's JobSet comes from the generic pass, without these fields.
|
|
545
|
+
if (!String(nodeData.apiVersion ?? "").startsWith("jobset.x-k8s.io/")) return "";
|
|
546
|
+
// Declared, not observed: a JobSet holding roles behind dependencies
|
|
547
|
+
// has created fewer Jobs than its spec asks for.
|
|
548
|
+
// An older Radar draws an owned JobSet without a phase; its status
|
|
549
|
+
// color then comes from generic conditions, so don't claim "Unknown".
|
|
550
|
+
const phase = (nodeData.phase as string) || "";
|
|
551
|
+
if (!phase) return "";
|
|
552
|
+
const declared = nodeData.declaredJobs as number | undefined;
|
|
553
|
+
return typeof declared === "number" ? `${phase} • ${declared} ${declared === 1 ? "Job" : "Jobs"} declared` : phase;
|
|
554
|
+
}
|
|
538
555
|
case "AnalysisRun": {
|
|
539
556
|
const phase = (nodeData.phase as string) || "Unknown";
|
|
540
557
|
const trigger = nodeData.trigger as string;
|
|
@@ -839,7 +856,7 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
839
856
|
</div>
|
|
840
857
|
</Tooltip>
|
|
841
858
|
) : (
|
|
842
|
-
<div className="text-xs text-theme-text-secondary truncate mt-0.5">
|
|
859
|
+
<div className="text-xs text-theme-text-secondary truncate mt-0.5" title={typeof nodeData.earlierJobRun === 'string' ? `Earlier run of Job ${nodeData.earlierJobRun}` : undefined}>
|
|
843
860
|
{subtitle}
|
|
844
861
|
</div>
|
|
845
862
|
)
|
|
@@ -51,6 +51,7 @@ const RESOURCE_KINDS: {
|
|
|
51
51
|
{ kind: 'PodGroup', label: 'Pod Group', icon: getTopologyIcon('PodGroup'), color: 'text-lime-400', category: 'workloads' },
|
|
52
52
|
{ kind: 'Job', label: 'Job', icon: getTopologyIcon('Job'), color: 'text-orange-400', category: 'workloads' },
|
|
53
53
|
{ kind: 'CronJob', label: 'CronJob', icon: getTopologyIcon('CronJob'), color: 'text-orange-300', category: 'workloads' },
|
|
54
|
+
{ kind: 'JobSet', label: 'JobSet', icon: getTopologyIcon('JobSet'), color: 'text-orange-400', category: 'workloads' },
|
|
54
55
|
{ kind: 'Workflow', label: 'Workflow', icon: getTopologyIcon('Workflow'), color: 'text-purple-400', category: 'workloads' },
|
|
55
56
|
{ kind: 'CronWorkflow', label: 'CronWorkflow', icon: getTopologyIcon('CronWorkflow'), color: 'text-purple-300', category: 'workloads' },
|
|
56
57
|
|