@skyhook-io/k8s-ui 1.16.3 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +14 -3
- package/src/components/issues/issues.test.ts +50 -2
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +28 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +309 -117
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- 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/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- 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 +62 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
- 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 +57 -17
- 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.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- 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 +15 -13
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- 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/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- 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.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +62 -36
- 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 +9 -22
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- 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-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -24
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-knative.ts +18 -0
- 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 +99 -39
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +20 -24
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +4 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +8 -74
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +175 -1
- package/src/components/ui/drawer-components.tsx +103 -36
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +297 -104
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +21 -2
- package/src/types/core.ts +60 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +49 -2
- package/src/utils/format.ts +9 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +70 -0
- package/src/utils/navigation.ts +91 -8
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +41 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dedupeResourceRefs } from '../../utils/resource-refs'
|
|
1
2
|
import { PodRow, PodListFrame, PodListToggle, workloadPodDetail } from './PodList'
|
|
2
3
|
export { workloadPodDetail, podStatusLabel, podStatusClass, podDotClass } from './PodList'
|
|
3
4
|
import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
} from 'lucide-react'
|
|
38
39
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
|
|
39
40
|
import type { GitOpsStatus } from '../../types/gitops'
|
|
41
|
+
import { isFetchError } from '../../types/fetch-error'
|
|
40
42
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
41
43
|
import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
|
|
42
44
|
import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
|
|
@@ -76,6 +78,7 @@ import { ServicePortCards, type ServicePortRenderProps } from '../resources/rend
|
|
|
76
78
|
import { rolloutMayAdvanceAutomatically, type WorkloadRolloutActivity } from '../../utils/workload-rollout'
|
|
77
79
|
import { WorkloadRolloutNotice } from './WorkloadRolloutNotice'
|
|
78
80
|
import { isCoreBatchJob } from '../../utils/api-resources'
|
|
81
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
79
82
|
|
|
80
83
|
export type WorkloadTabType = 'overview' | 'spec' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
|
|
81
84
|
type TabType = WorkloadTabType
|
|
@@ -87,6 +90,8 @@ export interface WorkloadExtraTab {
|
|
|
87
90
|
icon?: ReactNode
|
|
88
91
|
after?: WorkloadTabType
|
|
89
92
|
replaces?: WorkloadTabType
|
|
93
|
+
/** Trailing adornment after the label, e.g. a status mark. */
|
|
94
|
+
badge?: ReactNode
|
|
90
95
|
render: () => ReactNode
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -142,6 +147,23 @@ interface WorkloadViewProps {
|
|
|
142
147
|
* the Escape shortcut.
|
|
143
148
|
*/
|
|
144
149
|
breadcrumb?: ReactNode
|
|
150
|
+
/** Where the object lives, shown on the title line before its name (e.g. "Workspace / View /"). */
|
|
151
|
+
titlePrefix?: ReactNode
|
|
152
|
+
/**
|
|
153
|
+
* Kind, status and namespace on the title line, wrapping below the name
|
|
154
|
+
* only when there is no room; other chips (image, owners) keep a second
|
|
155
|
+
* row. The header actions then wrap below the title as a group rather
|
|
156
|
+
* than squeezing it.
|
|
157
|
+
*/
|
|
158
|
+
inlineBadges?: boolean
|
|
159
|
+
/** A note right after the namespace, e.g. that it is outside the namespace filter. */
|
|
160
|
+
namespaceNote?: ReactNode
|
|
161
|
+
/** Replaces the derived status badge on the title line, e.g. with the controller's own reported phase. */
|
|
162
|
+
renderStatusBadge?: (resource: any) => ReactNode
|
|
163
|
+
/** A short status right after the controller status on the title line, e.g. a derived health verdict. */
|
|
164
|
+
statusNote?: ReactNode
|
|
165
|
+
/** Leave out the kind badge, for a host whose title prefix already names the kind. */
|
|
166
|
+
hideKindBadge?: boolean
|
|
145
167
|
/** Suppress the standalone back arrow — for embeddings where "back" has no
|
|
146
168
|
* meaningful target (a single-workload app has no app graph to return to). */
|
|
147
169
|
hideBackButton?: boolean
|
|
@@ -281,6 +303,8 @@ interface WorkloadViewProps {
|
|
|
281
303
|
* resource rather than from owner labels on a managed object.
|
|
282
304
|
*/
|
|
283
305
|
onNavigateGitOpsPath?: (path: string) => void
|
|
306
|
+
/** Opens the live traffic view focused on this workload. */
|
|
307
|
+
onOpenLiveTraffic?: () => void
|
|
284
308
|
|
|
285
309
|
// ── Render props for platform-specific content ───────────────────────────
|
|
286
310
|
/** Render the logs tab content */
|
|
@@ -314,6 +338,14 @@ interface WorkloadViewProps {
|
|
|
314
338
|
}) => ReactNode
|
|
315
339
|
/** Extra tabs for the expanded view (e.g. a domain's own sections). */
|
|
316
340
|
extraTabs?: WorkloadExtraTab[]
|
|
341
|
+
/** Expanded view: a line under the title, above the tabs, shown on every tab. */
|
|
342
|
+
subheader?: ReactNode
|
|
343
|
+
/** Expanded view: tab ids in display order; tabs not listed follow in their usual order. */
|
|
344
|
+
tabOrder?: string[]
|
|
345
|
+
/** Expanded view: relabel, lead, or replace the whole "Spec & status" body. */
|
|
346
|
+
specTab?: { label?: string; icon?: ReactNode; lead?: ReactNode; render?: () => ReactNode }
|
|
347
|
+
/** Domain actions rendered in the header (drawer and expanded). */
|
|
348
|
+
renderHeaderActions?: (props: { resource: any; context: 'drawer' | 'expanded'; onNavigate?: NavigateToResource }) => ReactNode
|
|
317
349
|
/** Render a full replacement for the expanded Overview tab. */
|
|
318
350
|
renderExpandedOverview?: (props: {
|
|
319
351
|
kind: string
|
|
@@ -404,6 +436,12 @@ export function WorkloadView({
|
|
|
404
436
|
initialTab,
|
|
405
437
|
group,
|
|
406
438
|
breadcrumb,
|
|
439
|
+
titlePrefix,
|
|
440
|
+
inlineBadges = false,
|
|
441
|
+
namespaceNote,
|
|
442
|
+
renderStatusBadge,
|
|
443
|
+
statusNote,
|
|
444
|
+
hideKindBadge = false,
|
|
407
445
|
hideBackButton,
|
|
408
446
|
scopeControls,
|
|
409
447
|
compactHeader,
|
|
@@ -464,6 +502,10 @@ export function WorkloadView({
|
|
|
464
502
|
renderExpandedOverview,
|
|
465
503
|
renderSummary,
|
|
466
504
|
extraTabs,
|
|
505
|
+
tabOrder,
|
|
506
|
+
specTab,
|
|
507
|
+
subheader,
|
|
508
|
+
renderHeaderActions,
|
|
467
509
|
renderRelatedYaml,
|
|
468
510
|
renderMetricsTab,
|
|
469
511
|
renderCostTab,
|
|
@@ -494,6 +536,7 @@ export function WorkloadView({
|
|
|
494
536
|
helmOwnerSource,
|
|
495
537
|
onOpenHelmRelease,
|
|
496
538
|
onNavigateGitOpsPath,
|
|
539
|
+
onOpenLiveTraffic,
|
|
497
540
|
}: WorkloadViewProps) {
|
|
498
541
|
// The live object preserves exact CRD capitalization (RayJob, TFJob); the URL
|
|
499
542
|
// plural remains the API resource name used by fetches and mutations.
|
|
@@ -688,6 +731,11 @@ export function WorkloadView({
|
|
|
688
731
|
setTimeout(() => setCopied(null), 2000)
|
|
689
732
|
}, [])
|
|
690
733
|
|
|
734
|
+
// The edit is applied on top of the copy on screen, so the server is asked to
|
|
735
|
+
// reject it if the Secret was deleted or changed since that copy loaded.
|
|
736
|
+
// Otherwise the save would recreate a deleted Secret, or put back the old
|
|
737
|
+
// value of a key someone else changed.
|
|
738
|
+
const resourceVersion: string | undefined = resource?.metadata?.resourceVersion
|
|
691
739
|
const handleSaveSecretValue = useCallback(async (yaml: string) => {
|
|
692
740
|
if (!onUpdateResource) return
|
|
693
741
|
try {
|
|
@@ -697,12 +745,14 @@ export function WorkloadView({
|
|
|
697
745
|
name,
|
|
698
746
|
yaml,
|
|
699
747
|
force: true,
|
|
748
|
+
reviewedResourceVersion: resourceVersion,
|
|
700
749
|
})
|
|
701
750
|
setTimeout(() => refetch(), 1000)
|
|
702
|
-
} catch {
|
|
703
|
-
|
|
751
|
+
} catch (error) {
|
|
752
|
+
refetch()
|
|
753
|
+
throw error
|
|
704
754
|
}
|
|
705
|
-
}, [onUpdateResource, apiKind, namespace, name, refetch])
|
|
755
|
+
}, [onUpdateResource, apiKind, namespace, name, resourceVersion, refetch])
|
|
706
756
|
|
|
707
757
|
const handleSaved = useCallback(() => {
|
|
708
758
|
setSaveSuccess(true)
|
|
@@ -765,7 +815,35 @@ export function WorkloadView({
|
|
|
765
815
|
? getWorkloadDisplayStatus(resource, apiKind, workloadPods)
|
|
766
816
|
: null
|
|
767
817
|
const rolloutActivity = rolloutDisplay?.activity ?? null
|
|
768
|
-
|
|
818
|
+
// A 404 with a cached copy: the object was deleted while this view was
|
|
819
|
+
// open. Everything below that copy describes the past.
|
|
820
|
+
const resourceGone = Boolean(resource) && isFetchError(resourceError) && resourceError.status === 404
|
|
821
|
+
const status = resourceGone
|
|
822
|
+
? { text: 'Deleted', color: 'status-unknown' }
|
|
823
|
+
: rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
|
|
824
|
+
// The host's actions, and the host's renderers with their per-container
|
|
825
|
+
// terminal, logs, scale and env controls, would act on an object that no
|
|
826
|
+
// longer exists. The plain renderers show the last copy without them.
|
|
827
|
+
const actionsBarPropsForState = resourceGone ? undefined : effectiveActionsBarProps
|
|
828
|
+
const rendererOverridesForState = resourceGone ? undefined : rendererOverrides
|
|
829
|
+
// The host's overview panels and Reachability tab diagnose the live object,
|
|
830
|
+
// and the reachability panel polls for it every few seconds.
|
|
831
|
+
const renderOverviewLeadForState = resourceGone ? undefined : renderOverviewLead
|
|
832
|
+
const renderOverviewExtraForState = resourceGone ? undefined : renderOverviewExtra
|
|
833
|
+
const renderDiagnoseTabForState = resourceGone ? undefined : renderDiagnoseTab
|
|
834
|
+
// A host's summary, tabs and subheader can hold actions on the live object.
|
|
835
|
+
const renderSummaryForState = resourceGone ? undefined : renderSummary
|
|
836
|
+
const extraTabsForState = resourceGone ? undefined : extraTabs
|
|
837
|
+
// A remount closes any dialog the bar had open: once its actions are gone,
|
|
838
|
+
// a confirm button left on screen would do nothing.
|
|
839
|
+
const actionsBarKey = resourceGone ? 'gone' : 'live'
|
|
840
|
+
const deletedNotice = resourceGone ? (
|
|
841
|
+
<DeletedResourceNotice
|
|
842
|
+
kindLabel={displayKindName(apiKind, resource?.kind)}
|
|
843
|
+
owner={relationships?.deployment ?? relationships?.owner}
|
|
844
|
+
onNavigateToResource={onNavigateToResource}
|
|
845
|
+
/>
|
|
846
|
+
) : null
|
|
769
847
|
const rolloutMayAutoAdvance = rolloutActivity ? rolloutMayAdvanceAutomatically(rolloutActivity) : false
|
|
770
848
|
useEffect(() => {
|
|
771
849
|
if (!recentImageSave) return
|
|
@@ -778,7 +856,9 @@ export function WorkloadView({
|
|
|
778
856
|
const timeout = window.setTimeout(() => setRecentImageSave(false), 15000)
|
|
779
857
|
return () => window.clearTimeout(timeout)
|
|
780
858
|
}, [recentImageSave, rolloutActivity?.phase])
|
|
781
|
-
|
|
859
|
+
// The rollout state above is the last copy fetched before a 404; a workload
|
|
860
|
+
// the server says is gone has nothing left to roll out.
|
|
861
|
+
useProgressiveRefresh((rolloutMayAutoAdvance || recentImageSave) && !resourceGone, refetchProp)
|
|
782
862
|
const showOwnershipHeading = kind === 'Pod' && Boolean(ownershipContext)
|
|
783
863
|
const headerImage = metadata.find(m => m.label === 'Image')?.value
|
|
784
864
|
|
|
@@ -790,7 +870,8 @@ export function WorkloadView({
|
|
|
790
870
|
const renderDiagnose = actionsBarProps?.renderDiagnose as
|
|
791
871
|
| ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
|
|
792
872
|
| undefined
|
|
793
|
-
const
|
|
873
|
+
const headerActions = resource && !resourceGone && renderHeaderActions?.({ resource, context: expanded ? 'expanded' : 'drawer', onNavigate: onNavigateToResource })
|
|
874
|
+
const diagnoseAction = resourceGone ? null : renderDiagnose?.({
|
|
794
875
|
kind: resource?.kind ?? knownKindForPluralWithGroup(apiKind, group ?? '') ?? apiKind,
|
|
795
876
|
group,
|
|
796
877
|
namespace,
|
|
@@ -809,11 +890,11 @@ export function WorkloadView({
|
|
|
809
890
|
const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
|
|
810
891
|
const requestedTab: TabType = activeTab
|
|
811
892
|
const summaryContext = { kind, apiKind, namespace, name, group, resource, onNavigate: onNavigateToResource }
|
|
812
|
-
const expandedSummary = expanded && resource ?
|
|
813
|
-
const drawerSummary = !expanded && resource ?
|
|
893
|
+
const expandedSummary = expanded && resource ? renderSummaryForState?.({ ...summaryContext, context: 'expanded' }) ?? null : null
|
|
894
|
+
const drawerSummary = !expanded && resource ? renderSummaryForState?.({ ...summaryContext, context: 'drawer' }) ?? null : null
|
|
814
895
|
const tabs: DetailShellTab<TabType>[] = [
|
|
815
896
|
{ id: 'overview', label: 'Overview', icon: <Layers className="w-4 h-4" /> },
|
|
816
|
-
{ id: 'spec', label: 'Spec & status', icon: <FileText className="w-4 h-4" />, hidden: !expandedSummary },
|
|
897
|
+
{ id: 'spec', label: specTab?.label ?? 'Spec & status', icon: specTab?.icon ?? <FileText className="w-4 h-4" />, hidden: !expandedSummary },
|
|
817
898
|
{ id: 'topology', label: 'Topology', icon: <Network className="w-4 h-4" />, hidden: topologyTabHidden },
|
|
818
899
|
{
|
|
819
900
|
id: 'timeline',
|
|
@@ -827,15 +908,15 @@ export function WorkloadView({
|
|
|
827
908
|
id: 'reachability',
|
|
828
909
|
label: 'Reachability',
|
|
829
910
|
icon: <Stethoscope className="w-4 h-4" />,
|
|
830
|
-
hidden: !(
|
|
911
|
+
hidden: !(renderDiagnoseTabForState && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
|
|
831
912
|
},
|
|
832
913
|
{ id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
|
|
833
914
|
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
834
915
|
]
|
|
835
|
-
const allTabs = mergeExtraTabs(tabs, expanded ?
|
|
916
|
+
const allTabs = orderTabs(mergeExtraTabs(tabs, expanded ? extraTabsForState : undefined), expanded ? tabOrder : undefined)
|
|
836
917
|
const requestedTabAvailable = allTabs.some((tab) => tab.id === requestedTab && !tab.hidden)
|
|
837
918
|
const effectiveTab: TabType = requestedTabAvailable ? requestedTab : 'overview'
|
|
838
|
-
const activeExtraTab = expanded ?
|
|
919
|
+
const activeExtraTab = expanded ? extraTabsForState?.find((x) => x.id === effectiveTab) : undefined
|
|
839
920
|
const shouldCommitFallback =
|
|
840
921
|
requestedTab !== 'overview' &&
|
|
841
922
|
!requestedTabAvailable &&
|
|
@@ -864,13 +945,13 @@ export function WorkloadView({
|
|
|
864
945
|
<span className={clsx('badge', getKindColorOutline(apiKind))}>
|
|
865
946
|
{displayKindName(apiKind, resource?.kind)}
|
|
866
947
|
</span>
|
|
867
|
-
{status && (
|
|
948
|
+
{renderStatusBadge && resource && !resourceGone ? renderStatusBadge(resource) : status && (
|
|
868
949
|
<span className={clsx('badge', status.color)}>
|
|
869
950
|
{status.text}
|
|
870
951
|
</span>
|
|
871
952
|
)}
|
|
872
953
|
</div>
|
|
873
|
-
<div className="flex items-center gap-1.5">
|
|
954
|
+
<div className="flex shrink-0 flex-nowrap items-center justify-end gap-1.5">
|
|
874
955
|
{diagnoseAction}
|
|
875
956
|
{onExpand && (
|
|
876
957
|
<Tooltip content="Open full view" delay={150} position="bottom">
|
|
@@ -929,19 +1010,20 @@ export function WorkloadView({
|
|
|
929
1010
|
</Tooltip>
|
|
930
1011
|
</div>
|
|
931
1012
|
<p className="text-sm text-theme-text-tertiary">{namespace}</p>
|
|
932
|
-
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
|
|
1013
|
+
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath) || onOpenLiveTraffic) && (
|
|
933
1014
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
934
1015
|
{gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
|
|
935
1016
|
{helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
|
|
936
1017
|
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
937
1018
|
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
938
1019
|
)}
|
|
1020
|
+
{onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
|
|
939
1021
|
</div>
|
|
940
1022
|
)}
|
|
941
1023
|
</div>
|
|
942
1024
|
|
|
943
1025
|
{/* Actions bar */}
|
|
944
|
-
<ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...
|
|
1026
|
+
<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} leadingActions={headerActions} />
|
|
945
1027
|
</div>
|
|
946
1028
|
|
|
947
1029
|
{/* Success animation overlay */}
|
|
@@ -956,6 +1038,7 @@ export function WorkloadView({
|
|
|
956
1038
|
damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
|
|
957
1039
|
place. */}
|
|
958
1040
|
<div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
|
|
1041
|
+
{deletedNotice && <div className="px-4 pt-4">{deletedNotice}</div>}
|
|
959
1042
|
{!resource ? (
|
|
960
1043
|
// Fill the drawer body so the loading logo centers in it, not in a
|
|
961
1044
|
// 128px box pinned to the top (matches the splash/PaneLoader centering).
|
|
@@ -1007,9 +1090,9 @@ export function WorkloadView({
|
|
|
1007
1090
|
drawerSummary
|
|
1008
1091
|
) : (
|
|
1009
1092
|
<>
|
|
1010
|
-
{
|
|
1093
|
+
{renderOverviewLeadForState && hasOperationalIssues && (
|
|
1011
1094
|
<div className="px-4 pt-4">
|
|
1012
|
-
{
|
|
1095
|
+
{renderOverviewLeadForState({ kind, namespace, name })}
|
|
1013
1096
|
</div>
|
|
1014
1097
|
)}
|
|
1015
1098
|
<ResourceRendererDispatch
|
|
@@ -1023,9 +1106,9 @@ export function WorkloadView({
|
|
|
1023
1106
|
onCopy={copyToClipboard}
|
|
1024
1107
|
copied={copied}
|
|
1025
1108
|
onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
|
|
1026
|
-
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
1109
|
+
onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
|
|
1027
1110
|
isSavingSecret={isUpdatingResource}
|
|
1028
|
-
rendererOverrides={
|
|
1111
|
+
rendererOverrides={rendererOverridesForState}
|
|
1029
1112
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
1030
1113
|
renderMetrics={renderMetricsTab}
|
|
1031
1114
|
events={resourceFocusedK8sEvents}
|
|
@@ -1033,7 +1116,7 @@ export function WorkloadView({
|
|
|
1033
1116
|
updates={resourceFocusedUpdates}
|
|
1034
1117
|
eventsError={resourceFocusedK8sError}
|
|
1035
1118
|
updatesError={resourceFocusedUpdatesError}
|
|
1036
|
-
mainFooter={
|
|
1119
|
+
mainFooter={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'drawer' })}
|
|
1037
1120
|
/>
|
|
1038
1121
|
</>
|
|
1039
1122
|
)}
|
|
@@ -1049,6 +1132,8 @@ export function WorkloadView({
|
|
|
1049
1132
|
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
|
|
1050
1133
|
<DetailShell
|
|
1051
1134
|
breadcrumb={breadcrumb}
|
|
1135
|
+
wrapHeader={inlineBadges}
|
|
1136
|
+
subheader={resourceGone ? undefined : subheader}
|
|
1052
1137
|
nav={
|
|
1053
1138
|
breadcrumb || hideBackButton ? undefined : (
|
|
1054
1139
|
<Tooltip content="Go back (Esc)" delay={150} position="bottom">
|
|
@@ -1062,67 +1147,111 @@ export function WorkloadView({
|
|
|
1062
1147
|
</Tooltip>
|
|
1063
1148
|
)
|
|
1064
1149
|
}
|
|
1065
|
-
identity={
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
<
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
)}
|
|
1090
|
-
<div className="flex items-center gap-3 text-sm text-theme-text-secondary">
|
|
1091
|
-
<span className={clsx('badge', getKindColorOutline(apiKind))}>
|
|
1092
|
-
{displayKindName(apiKind, resource?.kind)}
|
|
1093
|
-
</span>
|
|
1094
|
-
{status && (
|
|
1095
|
-
<span className={clsx('badge', status.color)}>
|
|
1150
|
+
identity={(() => {
|
|
1151
|
+
const nameAndCopy = (
|
|
1152
|
+
<>
|
|
1153
|
+
<h1 className="text-lg font-semibold text-theme-text-primary truncate">{name}</h1>
|
|
1154
|
+
<Tooltip content="Copy name" delay={150}>
|
|
1155
|
+
<button
|
|
1156
|
+
onClick={() => copyToClipboard(name, 'name')}
|
|
1157
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
|
|
1158
|
+
aria-label="Copy name"
|
|
1159
|
+
>
|
|
1160
|
+
{copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
1161
|
+
</button>
|
|
1162
|
+
</Tooltip>
|
|
1163
|
+
</>
|
|
1164
|
+
)
|
|
1165
|
+
const kindAndStatus = (
|
|
1166
|
+
<>
|
|
1167
|
+
{!hideKindBadge && (
|
|
1168
|
+
<span className={clsx('badge whitespace-nowrap', getKindColorOutline(apiKind))}>
|
|
1169
|
+
{displayKindName(apiKind, resource?.kind)}
|
|
1170
|
+
</span>
|
|
1171
|
+
)}
|
|
1172
|
+
{renderStatusBadge && resource && !resourceGone ? renderStatusBadge(resource) : status && (
|
|
1173
|
+
<span className={clsx('badge whitespace-nowrap', status.color)}>
|
|
1096
1174
|
{status.text}
|
|
1097
1175
|
</span>
|
|
1098
1176
|
)}
|
|
1177
|
+
{!resourceGone && statusNote}
|
|
1178
|
+
</>
|
|
1179
|
+
)
|
|
1180
|
+
const coreBadges = (
|
|
1181
|
+
<>
|
|
1182
|
+
{/* On the title line the kind and status wrap together, never apart. */}
|
|
1183
|
+
{inlineBadges ? <span className="inline-flex items-center gap-x-3">{kindAndStatus}</span> : kindAndStatus}
|
|
1099
1184
|
{namespace && namespace !== '_' && (
|
|
1100
|
-
<span>Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
|
|
1185
|
+
<span className="whitespace-nowrap">Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
|
|
1101
1186
|
)}
|
|
1102
|
-
{
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1187
|
+
{namespace && namespace !== '_' && namespaceNote}
|
|
1188
|
+
</>
|
|
1189
|
+
)
|
|
1190
|
+
const extraChips = [
|
|
1191
|
+
headerImage && (
|
|
1192
|
+
<Tooltip key="image" content={headerImage} delay={300} wrapperClassName="min-w-0 max-w-md">
|
|
1193
|
+
<span className="truncate font-mono text-xs">
|
|
1194
|
+
{midTruncate(headerImage, 72)}
|
|
1195
|
+
</span>
|
|
1196
|
+
</Tooltip>
|
|
1197
|
+
),
|
|
1198
|
+
gitopsOwner && (
|
|
1199
|
+
<ManagedByChip key="gitops" owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
|
|
1200
|
+
),
|
|
1201
|
+
helmOwner && (
|
|
1202
|
+
<HelmManagedByChip key="helm" owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
|
|
1203
|
+
),
|
|
1204
|
+
gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
1205
|
+
<OpenInGitOpsChip key="open-gitops" onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
1206
|
+
),
|
|
1207
|
+
onOpenLiveTraffic && <LiveTrafficChip key="live-traffic" onClick={onOpenLiveTraffic} />,
|
|
1208
|
+
relationships?.owner && !showOwnershipHeading && (
|
|
1209
|
+
<span key="owner">Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
|
|
1210
|
+
),
|
|
1211
|
+
].filter(Boolean)
|
|
1212
|
+
if (inlineBadges && !(showOwnershipHeading && ownershipContext)) {
|
|
1213
|
+
// The name and its copy button stay together and truncate only when
|
|
1214
|
+
// the line holds nothing else; the badges wrap below before that.
|
|
1215
|
+
return (
|
|
1216
|
+
<>
|
|
1217
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">
|
|
1218
|
+
{titlePrefix}
|
|
1219
|
+
<div className="flex min-w-0 max-w-full items-center gap-2">{nameAndCopy}</div>
|
|
1220
|
+
{coreBadges}
|
|
1221
|
+
</div>
|
|
1222
|
+
{extraChips.length > 0 && (
|
|
1223
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">{extraChips}</div>
|
|
1224
|
+
)}
|
|
1225
|
+
</>
|
|
1226
|
+
)
|
|
1227
|
+
}
|
|
1228
|
+
return (
|
|
1229
|
+
<>
|
|
1230
|
+
{showOwnershipHeading && ownershipContext ? (
|
|
1231
|
+
<OwnershipHeading
|
|
1232
|
+
podName={name}
|
|
1233
|
+
context={ownershipContext}
|
|
1234
|
+
copied={copied === 'name'}
|
|
1235
|
+
onCopy={() => copyToClipboard(name, 'name')}
|
|
1236
|
+
onNavigateToResource={onNavigateToResource}
|
|
1237
|
+
onOpenApplication={onOpenApplication}
|
|
1238
|
+
/>
|
|
1239
|
+
) : (
|
|
1240
|
+
<div className="flex items-center gap-3 mb-1">
|
|
1241
|
+
{titlePrefix}
|
|
1242
|
+
{nameAndCopy}
|
|
1243
|
+
</div>
|
|
1120
1244
|
)}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1245
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">
|
|
1246
|
+
{coreBadges}
|
|
1247
|
+
{extraChips}
|
|
1248
|
+
</div>
|
|
1249
|
+
</>
|
|
1250
|
+
)
|
|
1251
|
+
})()}
|
|
1124
1252
|
headerActions={
|
|
1125
1253
|
<>
|
|
1254
|
+
{headerActions}
|
|
1126
1255
|
{diagnoseAction}
|
|
1127
1256
|
<Tooltip content="Refresh" delay={150} position="bottom">
|
|
1128
1257
|
<button
|
|
@@ -1157,12 +1286,13 @@ export function WorkloadView({
|
|
|
1157
1286
|
activeTab={effectiveTab}
|
|
1158
1287
|
onTabChange={handleSetTab}
|
|
1159
1288
|
scopeControls={scopeControls}
|
|
1160
|
-
tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...
|
|
1289
|
+
tabStripEnd={<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} hideLogs {...actionsBarPropsForState} />}
|
|
1161
1290
|
overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
|
|
1162
1291
|
compactHeader={compactHeader}
|
|
1163
1292
|
>
|
|
1164
1293
|
<div className="flex h-full min-h-0 flex-col">
|
|
1165
|
-
{
|
|
1294
|
+
{deletedNotice && <div className="shrink-0 px-4 pt-4">{deletedNotice}</div>}
|
|
1295
|
+
{!resourceGone && effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
|
|
1166
1296
|
<div className="shrink-0 px-4 pt-4">
|
|
1167
1297
|
<WorkloadRolloutNotice activity={rolloutActivity} recentImageSave={recentImageSave} />
|
|
1168
1298
|
</div>
|
|
@@ -1173,13 +1303,15 @@ export function WorkloadView({
|
|
|
1173
1303
|
<div className="h-full min-h-0 overflow-y-auto">{expandedSummary}</div>
|
|
1174
1304
|
) : effectiveTab === 'overview' && expandedOverview ? (
|
|
1175
1305
|
<div className="h-full min-h-0">
|
|
1176
|
-
{hasOperationalIssues &&
|
|
1306
|
+
{hasOperationalIssues && renderOverviewLeadForState && (
|
|
1177
1307
|
<div className="px-4 pt-4">
|
|
1178
|
-
{
|
|
1308
|
+
{renderOverviewLeadForState({ kind, namespace, name })}
|
|
1179
1309
|
</div>
|
|
1180
1310
|
)}
|
|
1181
1311
|
{expandedOverview}
|
|
1182
1312
|
</div>
|
|
1313
|
+
) : effectiveTab === 'spec' && specTab?.render && !resourceGone ? (
|
|
1314
|
+
<div className="h-full min-h-0 overflow-y-auto">{specTab.render()}</div>
|
|
1183
1315
|
) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
|
|
1184
1316
|
<InfoTab
|
|
1185
1317
|
resource={resource}
|
|
@@ -1199,22 +1331,29 @@ export function WorkloadView({
|
|
|
1199
1331
|
onNavigate={onNavigateToResource}
|
|
1200
1332
|
onCopy={copyToClipboard}
|
|
1201
1333
|
copied={copied}
|
|
1202
|
-
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
1334
|
+
onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
|
|
1203
1335
|
isSavingSecret={isUpdatingResource}
|
|
1204
1336
|
onOpenLogs={handleOpenLogs}
|
|
1205
1337
|
onSwitchToTimeline={() => handleSetTab('timeline')}
|
|
1206
1338
|
onSwitchToLogs={logsTabVisible ? () => handleSetTab('logs') : undefined}
|
|
1207
1339
|
onSwitchToTopology={!topologyTabHidden ? () => handleSetTab('topology') : undefined}
|
|
1208
|
-
rendererOverrides={
|
|
1340
|
+
rendererOverrides={rendererOverridesForState}
|
|
1209
1341
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
1210
1342
|
events={overviewEvents}
|
|
1211
1343
|
eventsLoading={overviewEventsLoading}
|
|
1212
1344
|
updates={resourceFocusedUpdates}
|
|
1213
1345
|
eventsError={overviewEventsError}
|
|
1214
1346
|
updatesError={resourceFocusedUpdatesError}
|
|
1215
|
-
extraContent={
|
|
1347
|
+
extraContent={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'expanded' })}
|
|
1216
1348
|
introContent={overviewIntro}
|
|
1217
|
-
leadContent={
|
|
1349
|
+
leadContent={
|
|
1350
|
+
// A host's spec lead replaces the operational issues there: its Overview already leads with them.
|
|
1351
|
+
effectiveTab === 'spec' && specTab?.lead
|
|
1352
|
+
? specTab.lead
|
|
1353
|
+
: hasOperationalIssues && renderOverviewLeadForState
|
|
1354
|
+
? renderOverviewLeadForState({ kind, namespace, name })
|
|
1355
|
+
: undefined
|
|
1356
|
+
}
|
|
1218
1357
|
onEvaluateCapacity={onEvaluateCapacity}
|
|
1219
1358
|
recentImageSave={recentImageSave}
|
|
1220
1359
|
/>
|
|
@@ -1273,9 +1412,9 @@ export function WorkloadView({
|
|
|
1273
1412
|
{renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
|
|
1274
1413
|
</div>
|
|
1275
1414
|
)}
|
|
1276
|
-
{effectiveTab === 'reachability' &&
|
|
1415
|
+
{effectiveTab === 'reachability' && renderDiagnoseTabForState && (
|
|
1277
1416
|
<div className="flex h-full min-h-0 flex-col p-3">
|
|
1278
|
-
{
|
|
1417
|
+
{renderDiagnoseTabForState({ kind: resource?.kind || kind, namespace, name })}
|
|
1279
1418
|
</div>
|
|
1280
1419
|
)}
|
|
1281
1420
|
{effectiveTab === 'cost' && renderCostTab && (
|
|
@@ -1406,6 +1545,34 @@ function extractMetadata(kind: string, resource: any): { label: string; value: s
|
|
|
1406
1545
|
// SUB-COMPONENTS
|
|
1407
1546
|
// ============================================================================
|
|
1408
1547
|
|
|
1548
|
+
function DeletedResourceNotice({
|
|
1549
|
+
kindLabel,
|
|
1550
|
+
owner,
|
|
1551
|
+
onNavigateToResource,
|
|
1552
|
+
}: {
|
|
1553
|
+
kindLabel: string
|
|
1554
|
+
owner?: ResourceRef
|
|
1555
|
+
onNavigateToResource?: NavigateToResource
|
|
1556
|
+
}) {
|
|
1557
|
+
return (
|
|
1558
|
+
<AlertBanner
|
|
1559
|
+
variant="warning"
|
|
1560
|
+
title={`This ${kindLabel} no longer exists in the cluster`}
|
|
1561
|
+
message="You're looking at the last version Radar loaded before it was deleted."
|
|
1562
|
+
>
|
|
1563
|
+
{owner && onNavigateToResource && (
|
|
1564
|
+
<button
|
|
1565
|
+
type="button"
|
|
1566
|
+
onClick={() => onNavigateToResource(refToSelectedResource(owner))}
|
|
1567
|
+
className="mt-2 text-xs text-accent-text hover:underline"
|
|
1568
|
+
>
|
|
1569
|
+
Go to its {displayKindName(owner.kind, owner.kind)} {owner.name}
|
|
1570
|
+
</button>
|
|
1571
|
+
)}
|
|
1572
|
+
</AlertBanner>
|
|
1573
|
+
)
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1409
1576
|
function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
1410
1577
|
return (
|
|
1411
1578
|
<Tooltip content="Open this resource in the GitOps tab (tree + insights + ops)" delay={150}>
|
|
@@ -1421,6 +1588,21 @@ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
|
1421
1588
|
)
|
|
1422
1589
|
}
|
|
1423
1590
|
|
|
1591
|
+
function LiveTrafficChip({ onClick }: { onClick: () => void }) {
|
|
1592
|
+
return (
|
|
1593
|
+
<Tooltip content="What this workload talks to, from the cluster's traffic source" delay={150}>
|
|
1594
|
+
<button
|
|
1595
|
+
type="button"
|
|
1596
|
+
onClick={onClick}
|
|
1597
|
+
className="inline-flex items-center gap-1 rounded border border-theme-border bg-theme-elevated px-1.5 py-0.5 text-[11px] font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
|
|
1598
|
+
>
|
|
1599
|
+
Live traffic
|
|
1600
|
+
<ArrowRight className="h-3 w-3 shrink-0" />
|
|
1601
|
+
</button>
|
|
1602
|
+
</Tooltip>
|
|
1603
|
+
)
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1424
1606
|
function OwnershipHeading({
|
|
1425
1607
|
podName,
|
|
1426
1608
|
context,
|
|
@@ -2023,6 +2205,7 @@ function WorkloadOverviewTab({
|
|
|
2023
2205
|
const relationshipGroups = buildRelationshipGroups(relationships)
|
|
2024
2206
|
const servingRelationshipGroups = buildServingRelationshipGroups(relationships)
|
|
2025
2207
|
const dependencyRelationshipGroups = buildDependencyRelationshipGroups(relationships)
|
|
2208
|
+
const observationGroups = dependencyRelationshipGroups.filter((group) => group.label === 'Monitored By' || group.label === 'Staged Policies')
|
|
2026
2209
|
const showServingPath = servingRelationshipGroups.length > 0 && overviewShape !== 'job' && overviewShape !== 'cronjob'
|
|
2027
2210
|
const secondaryRelationshipGroups = showServingPath ? dependencyRelationshipGroups : relationshipGroups
|
|
2028
2211
|
const relationshipCardTitle =
|
|
@@ -2101,6 +2284,14 @@ function WorkloadOverviewTab({
|
|
|
2101
2284
|
onNavigate={onNavigate}
|
|
2102
2285
|
onSwitchToTopology={onSwitchToTopology}
|
|
2103
2286
|
/>
|
|
2287
|
+
{observationGroups.length > 0 && (
|
|
2288
|
+
<ServingDependenciesCard
|
|
2289
|
+
title="Related resources"
|
|
2290
|
+
groups={observationGroups}
|
|
2291
|
+
onNavigate={onNavigate}
|
|
2292
|
+
onSwitchToTopology={onSwitchToTopology}
|
|
2293
|
+
/>
|
|
2294
|
+
)}
|
|
2104
2295
|
{extraContent}
|
|
2105
2296
|
</div>
|
|
2106
2297
|
</div>
|
|
@@ -2439,8 +2630,8 @@ function buildSummaryMetrics(
|
|
|
2439
2630
|
},
|
|
2440
2631
|
{
|
|
2441
2632
|
label: 'Latest run',
|
|
2442
|
-
value: status.lastScheduleTime ?
|
|
2443
|
-
detail: status.lastSuccessfulTime ? `last success ${
|
|
2633
|
+
value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never',
|
|
2634
|
+
detail: status.lastSuccessfulTime ? `last success ${formatRelativeAgeTime(status.lastSuccessfulTime)}` : 'no successful run recorded',
|
|
2444
2635
|
icon: Activity,
|
|
2445
2636
|
},
|
|
2446
2637
|
{
|
|
@@ -2556,8 +2747,8 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
|
|
|
2556
2747
|
{ label: 'Schedule', value: spec.schedule || '-' },
|
|
2557
2748
|
{ label: 'Active jobs', value: active, tone: active > 0 ? 'warn' : 'default' },
|
|
2558
2749
|
{ label: 'Suspend', value: suspended ? 'Yes' : 'No', tone: suspended ? 'warn' : 'default' },
|
|
2559
|
-
{ label: 'Last schedule', value: status.lastScheduleTime ?
|
|
2560
|
-
{ label: 'Last success', value: status.lastSuccessfulTime ?
|
|
2750
|
+
{ label: 'Last schedule', value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : '-' },
|
|
2751
|
+
{ label: 'Last success', value: status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : '-' },
|
|
2561
2752
|
],
|
|
2562
2753
|
}
|
|
2563
2754
|
}
|
|
@@ -2656,8 +2847,8 @@ function getScalingSummary(
|
|
|
2656
2847
|
label: phase,
|
|
2657
2848
|
detail: '',
|
|
2658
2849
|
stats: [
|
|
2659
|
-
{ label: 'Started', value: status.startTime ?
|
|
2660
|
-
{ label: 'Finished', value: status.completionTime ?
|
|
2850
|
+
{ label: 'Started', value: status.startTime ? formatRelativeAgeTime(status.startTime) : '-' },
|
|
2851
|
+
{ label: 'Finished', value: status.completionTime ? formatRelativeAgeTime(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
|
|
2661
2852
|
{ label: 'Parallelism', value: spec.parallelism ?? 1 },
|
|
2662
2853
|
{ label: 'Completions', value: spec.completions ?? 1 },
|
|
2663
2854
|
{ label: 'Completion mode', value: spec.completionMode ?? 'NonIndexed' },
|
|
@@ -3297,8 +3488,8 @@ function CronJobRunsCard({
|
|
|
3297
3488
|
>
|
|
3298
3489
|
<div className="mb-3 grid gap-x-8 gap-y-3 sm:grid-cols-2">
|
|
3299
3490
|
<OverviewStat label="Active jobs" value={activeJobs.length} tone={activeJobs.length > 0 ? 'warn' : 'default'} />
|
|
3300
|
-
<OverviewStat label="Last schedule" value={status.lastScheduleTime ?
|
|
3301
|
-
<OverviewStat label="Last success" value={status.lastSuccessfulTime ?
|
|
3491
|
+
<OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never'} />
|
|
3492
|
+
<OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : 'Never'} />
|
|
3302
3493
|
<OverviewStat label="Retained history" value={`${spec.successfulJobsHistoryLimit ?? 3} ok / ${spec.failedJobsHistoryLimit ?? 1} failed`} />
|
|
3303
3494
|
</div>
|
|
3304
3495
|
|
|
@@ -3755,7 +3946,7 @@ function ActivityCard({
|
|
|
3755
3946
|
<div className="min-w-0 flex-1">
|
|
3756
3947
|
<div className="flex items-center justify-between gap-2">
|
|
3757
3948
|
<span className="truncate text-sm font-medium text-theme-text-primary">{event.reason || event.eventType || 'update'}</span>
|
|
3758
|
-
<span className="shrink-0 text-xs text-theme-text-tertiary">{
|
|
3949
|
+
<span className="shrink-0 text-xs text-theme-text-tertiary">{formatRelativeAgeTime(event.timestamp)}</span>
|
|
3759
3950
|
</div>
|
|
3760
3951
|
<div className="mt-0.5 line-clamp-2 text-xs text-theme-text-secondary">
|
|
3761
3952
|
{event.message || event.diff?.summary || `${event.kind} ${event.name}`}
|
|
@@ -3790,6 +3981,7 @@ function buildServingRelationshipGroups(relationships: Relationships | undefined
|
|
|
3790
3981
|
...(relationships.ingresses ?? []),
|
|
3791
3982
|
...(relationships.gateways ?? []),
|
|
3792
3983
|
...(relationships.routes ?? []),
|
|
3984
|
+
...(relationships.routedFrom ?? []),
|
|
3793
3985
|
])
|
|
3794
3986
|
return [
|
|
3795
3987
|
{ label: 'Services', refs: dedupeResourceRefs(relationships.services ?? []) },
|
|
@@ -3804,6 +3996,8 @@ function buildDependencyRelationshipGroups(relationships: Relationships | undefi
|
|
|
3804
3996
|
{ label: 'Autoscalers', refs: dedupeResourceRefs(relationships.scalers ?? []) },
|
|
3805
3997
|
{ label: 'Disruption budgets', refs: dedupeResourceRefs(relationships.pdbs ?? []) },
|
|
3806
3998
|
{ label: 'Network policies', refs: dedupeResourceRefs(relationships.networkPolicies ?? []) },
|
|
3999
|
+
{ label: 'Staged Policies', refs: dedupeResourceRefs(relationships.stagedPolicies ?? []) },
|
|
4000
|
+
{ label: 'Monitored By', refs: dedupeResourceRefs(relationships.monitors ?? []) },
|
|
3807
4001
|
{ label: 'Storage claims', refs: dedupeResourceRefs(relationships.resourceClaims ?? []) },
|
|
3808
4002
|
].filter((group) => group.refs.length > 0)
|
|
3809
4003
|
}
|
|
@@ -3878,16 +4072,6 @@ function summarizeServingExposure(
|
|
|
3878
4072
|
return { label: 'No serving path detected', badge: 'None', severity: 'neutral' }
|
|
3879
4073
|
}
|
|
3880
4074
|
|
|
3881
|
-
function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
3882
|
-
const seen = new Set<string>()
|
|
3883
|
-
return refs.filter((ref) => {
|
|
3884
|
-
const key = resourceRefId(ref)
|
|
3885
|
-
if (seen.has(key)) return false
|
|
3886
|
-
seen.add(key)
|
|
3887
|
-
return true
|
|
3888
|
-
})
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
4075
|
function resourceRefId(ref: ResourceRef): string {
|
|
3892
4076
|
return `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
3893
4077
|
}
|
|
@@ -3913,12 +4097,21 @@ function mergeAndRankEvents(events: TimelineEvent[], updates: TimelineEvent[]):
|
|
|
3913
4097
|
})
|
|
3914
4098
|
}
|
|
3915
4099
|
|
|
4100
|
+
function orderTabs(tabs: DetailShellTab<TabType>[], order: string[] | undefined): DetailShellTab<TabType>[] {
|
|
4101
|
+
if (!order || order.length === 0) return tabs
|
|
4102
|
+
const rank = (id: string) => {
|
|
4103
|
+
const i = order.indexOf(id)
|
|
4104
|
+
return i < 0 ? order.length : i
|
|
4105
|
+
}
|
|
4106
|
+
return tabs.map((t, i) => ({ t, i })).sort((a, b) => rank(a.t.id) - rank(b.t.id) || a.i - b.i).map((x) => x.t)
|
|
4107
|
+
}
|
|
4108
|
+
|
|
3916
4109
|
function mergeExtraTabs(tabs: DetailShellTab<TabType>[], extra: WorkloadExtraTab[] | undefined): DetailShellTab<TabType>[] {
|
|
3917
4110
|
if (!extra || extra.length === 0) return tabs
|
|
3918
4111
|
const replaced = new Set(extra.map((x) => x.replaces).filter(Boolean))
|
|
3919
4112
|
const out = tabs.map((t) => (replaced.has(t.id) ? { ...t, hidden: true } : t))
|
|
3920
4113
|
for (const x of extra) {
|
|
3921
|
-
const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon }
|
|
4114
|
+
const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon, badge: x.badge }
|
|
3922
4115
|
const anchor = x.after ?? x.replaces
|
|
3923
4116
|
const idx = anchor ? out.findIndex((t) => t.id === anchor) : -1
|
|
3924
4117
|
if (idx >= 0) out.splice(idx + 1, 0, tab)
|