@skyhook-io/k8s-ui 1.16.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
- package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
- package/src/components/cnpg/index.ts +14 -0
- package/src/components/cnpg/primitives.tsx +134 -0
- package/src/components/cnpg/relations.test.ts +304 -0
- package/src/components/cnpg/relations.ts +417 -0
- package/src/components/cnpg/workspace.test.ts +285 -0
- package/src/components/cnpg/workspace.ts +700 -0
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/IssuesView.tsx +10 -1
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/issues/types.ts +24 -0
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
- package/src/components/logs/useLogBuffer.test.ts +17 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
- package/src/components/resources/ResourcesSidebar.tsx +183 -11
- package/src/components/resources/ResourcesView.tsx +12 -9
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.ts +3 -7
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/resources/status-overclaim.test.ts +1 -1
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +187 -38
- package/src/components/timeline/TimelineListFold.test.tsx +56 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-filters.ts +7 -0
- package/src/components/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +216 -48
- package/src/components/workload/index.ts +1 -0
- package/src/components/workload/workload-logs-availability.test.ts +6 -0
- package/src/index.ts +3 -0
- package/src/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/log-level.ts +14 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
|
@@ -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,10 +78,21 @@ 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
|
-
export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
|
|
83
|
+
export type WorkloadTabType = 'overview' | 'spec' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
|
|
81
84
|
type TabType = WorkloadTabType
|
|
82
85
|
|
|
86
|
+
/** A host-provided tab. `after` places it behind a built-in tab; `replaces` hides that built-in tab. */
|
|
87
|
+
export interface WorkloadExtraTab {
|
|
88
|
+
id: string
|
|
89
|
+
label: string
|
|
90
|
+
icon?: ReactNode
|
|
91
|
+
after?: WorkloadTabType
|
|
92
|
+
replaces?: WorkloadTabType
|
|
93
|
+
render: () => ReactNode
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
export interface ResourceOwnershipContext {
|
|
84
97
|
application?: {
|
|
85
98
|
key: string
|
|
@@ -271,6 +284,8 @@ interface WorkloadViewProps {
|
|
|
271
284
|
* resource rather than from owner labels on a managed object.
|
|
272
285
|
*/
|
|
273
286
|
onNavigateGitOpsPath?: (path: string) => void
|
|
287
|
+
/** Opens the live traffic view focused on this workload. */
|
|
288
|
+
onOpenLiveTraffic?: () => void
|
|
274
289
|
|
|
275
290
|
// ── Render props for platform-specific content ───────────────────────────
|
|
276
291
|
/** Render the logs tab content */
|
|
@@ -286,6 +301,24 @@ interface WorkloadViewProps {
|
|
|
286
301
|
initialContainer: string | null
|
|
287
302
|
onConsumeInitialContainer: () => void
|
|
288
303
|
}) => ReactNode
|
|
304
|
+
/**
|
|
305
|
+
* A composed summary for kinds that have one. When it returns content, the
|
|
306
|
+
* Overview tab shows the summary and the resource's own renderer moves to a
|
|
307
|
+
* "Spec & status" tab, so the same facts never render twice. Return null to
|
|
308
|
+
* keep the default Overview.
|
|
309
|
+
*/
|
|
310
|
+
renderSummary?: (props: {
|
|
311
|
+
kind: string
|
|
312
|
+
apiKind: string
|
|
313
|
+
namespace: string
|
|
314
|
+
name: string
|
|
315
|
+
group?: string
|
|
316
|
+
resource: any
|
|
317
|
+
context: 'drawer' | 'expanded'
|
|
318
|
+
onNavigate?: NavigateToResource
|
|
319
|
+
}) => ReactNode
|
|
320
|
+
/** Extra tabs for the expanded view (e.g. a domain's own sections). */
|
|
321
|
+
extraTabs?: WorkloadExtraTab[]
|
|
289
322
|
/** Render a full replacement for the expanded Overview tab. */
|
|
290
323
|
renderExpandedOverview?: (props: {
|
|
291
324
|
kind: string
|
|
@@ -434,6 +467,8 @@ export function WorkloadView({
|
|
|
434
467
|
renderDiagnoseTab,
|
|
435
468
|
reachableVia,
|
|
436
469
|
renderExpandedOverview,
|
|
470
|
+
renderSummary,
|
|
471
|
+
extraTabs,
|
|
437
472
|
renderRelatedYaml,
|
|
438
473
|
renderMetricsTab,
|
|
439
474
|
renderCostTab,
|
|
@@ -464,6 +499,7 @@ export function WorkloadView({
|
|
|
464
499
|
helmOwnerSource,
|
|
465
500
|
onOpenHelmRelease,
|
|
466
501
|
onNavigateGitOpsPath,
|
|
502
|
+
onOpenLiveTraffic,
|
|
467
503
|
}: WorkloadViewProps) {
|
|
468
504
|
// The live object preserves exact CRD capitalization (RayJob, TFJob); the URL
|
|
469
505
|
// plural remains the API resource name used by fetches and mutations.
|
|
@@ -480,8 +516,10 @@ export function WorkloadView({
|
|
|
480
516
|
|
|
481
517
|
// Collapsed mode state (YAML toggle for drawer mode)
|
|
482
518
|
const [showYaml, setShowYaml] = useState(initialTab === 'yaml')
|
|
519
|
+
const [drawerSpec, setDrawerSpec] = useState(false)
|
|
483
520
|
useEffect(() => {
|
|
484
521
|
setShowYaml(initialTab === 'yaml')
|
|
522
|
+
setDrawerSpec(false)
|
|
485
523
|
}, [kindProp, namespace, name, initialTab])
|
|
486
524
|
|
|
487
525
|
const switchView = useCallback((yaml: boolean) => {
|
|
@@ -656,6 +694,11 @@ export function WorkloadView({
|
|
|
656
694
|
setTimeout(() => setCopied(null), 2000)
|
|
657
695
|
}, [])
|
|
658
696
|
|
|
697
|
+
// The edit is applied on top of the copy on screen, so the server is asked to
|
|
698
|
+
// reject it if the Secret was deleted or changed since that copy loaded.
|
|
699
|
+
// Otherwise the save would recreate a deleted Secret, or put back the old
|
|
700
|
+
// value of a key someone else changed.
|
|
701
|
+
const resourceVersion: string | undefined = resource?.metadata?.resourceVersion
|
|
659
702
|
const handleSaveSecretValue = useCallback(async (yaml: string) => {
|
|
660
703
|
if (!onUpdateResource) return
|
|
661
704
|
try {
|
|
@@ -665,12 +708,14 @@ export function WorkloadView({
|
|
|
665
708
|
name,
|
|
666
709
|
yaml,
|
|
667
710
|
force: true,
|
|
711
|
+
reviewedResourceVersion: resourceVersion,
|
|
668
712
|
})
|
|
669
713
|
setTimeout(() => refetch(), 1000)
|
|
670
|
-
} catch {
|
|
671
|
-
|
|
714
|
+
} catch (error) {
|
|
715
|
+
refetch()
|
|
716
|
+
throw error
|
|
672
717
|
}
|
|
673
|
-
}, [onUpdateResource, apiKind, namespace, name, refetch])
|
|
718
|
+
}, [onUpdateResource, apiKind, namespace, name, resourceVersion, refetch])
|
|
674
719
|
|
|
675
720
|
const handleSaved = useCallback(() => {
|
|
676
721
|
setSaveSuccess(true)
|
|
@@ -733,7 +778,32 @@ export function WorkloadView({
|
|
|
733
778
|
? getWorkloadDisplayStatus(resource, apiKind, workloadPods)
|
|
734
779
|
: null
|
|
735
780
|
const rolloutActivity = rolloutDisplay?.activity ?? null
|
|
736
|
-
|
|
781
|
+
// A 404 with a cached copy: the object was deleted while this view was
|
|
782
|
+
// open. Everything below that copy describes the past.
|
|
783
|
+
const resourceGone = Boolean(resource) && isFetchError(resourceError) && resourceError.status === 404
|
|
784
|
+
const status = resourceGone
|
|
785
|
+
? { text: 'Deleted', color: 'status-unknown' }
|
|
786
|
+
: rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
|
|
787
|
+
// The host's actions, and the host's renderers with their per-container
|
|
788
|
+
// terminal, logs, scale and env controls, would act on an object that no
|
|
789
|
+
// longer exists. The plain renderers show the last copy without them.
|
|
790
|
+
const actionsBarPropsForState = resourceGone ? undefined : effectiveActionsBarProps
|
|
791
|
+
const rendererOverridesForState = resourceGone ? undefined : rendererOverrides
|
|
792
|
+
// The host's overview panels and Reachability tab diagnose the live object,
|
|
793
|
+
// and the reachability panel polls for it every few seconds.
|
|
794
|
+
const renderOverviewLeadForState = resourceGone ? undefined : renderOverviewLead
|
|
795
|
+
const renderOverviewExtraForState = resourceGone ? undefined : renderOverviewExtra
|
|
796
|
+
const renderDiagnoseTabForState = resourceGone ? undefined : renderDiagnoseTab
|
|
797
|
+
// A remount closes any dialog the bar had open: once its actions are gone,
|
|
798
|
+
// a confirm button left on screen would do nothing.
|
|
799
|
+
const actionsBarKey = resourceGone ? 'gone' : 'live'
|
|
800
|
+
const deletedNotice = resourceGone ? (
|
|
801
|
+
<DeletedResourceNotice
|
|
802
|
+
kindLabel={displayKindName(apiKind, resource?.kind)}
|
|
803
|
+
owner={relationships?.deployment ?? relationships?.owner}
|
|
804
|
+
onNavigateToResource={onNavigateToResource}
|
|
805
|
+
/>
|
|
806
|
+
) : null
|
|
737
807
|
const rolloutMayAutoAdvance = rolloutActivity ? rolloutMayAdvanceAutomatically(rolloutActivity) : false
|
|
738
808
|
useEffect(() => {
|
|
739
809
|
if (!recentImageSave) return
|
|
@@ -746,7 +816,9 @@ export function WorkloadView({
|
|
|
746
816
|
const timeout = window.setTimeout(() => setRecentImageSave(false), 15000)
|
|
747
817
|
return () => window.clearTimeout(timeout)
|
|
748
818
|
}, [recentImageSave, rolloutActivity?.phase])
|
|
749
|
-
|
|
819
|
+
// The rollout state above is the last copy fetched before a 404; a workload
|
|
820
|
+
// the server says is gone has nothing left to roll out.
|
|
821
|
+
useProgressiveRefresh((rolloutMayAutoAdvance || recentImageSave) && !resourceGone, refetchProp)
|
|
750
822
|
const showOwnershipHeading = kind === 'Pod' && Boolean(ownershipContext)
|
|
751
823
|
const headerImage = metadata.find(m => m.label === 'Image')?.value
|
|
752
824
|
|
|
@@ -758,7 +830,7 @@ export function WorkloadView({
|
|
|
758
830
|
const renderDiagnose = actionsBarProps?.renderDiagnose as
|
|
759
831
|
| ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
|
|
760
832
|
| undefined
|
|
761
|
-
const diagnoseAction = renderDiagnose?.({
|
|
833
|
+
const diagnoseAction = resourceGone ? null : renderDiagnose?.({
|
|
762
834
|
kind: resource?.kind ?? knownKindForPluralWithGroup(apiKind, group ?? '') ?? apiKind,
|
|
763
835
|
group,
|
|
764
836
|
namespace,
|
|
@@ -776,8 +848,12 @@ export function WorkloadView({
|
|
|
776
848
|
const podEvidenceLoading = resourceLoading || workloadPodsLoading || eventsLoading
|
|
777
849
|
const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
|
|
778
850
|
const requestedTab: TabType = activeTab
|
|
851
|
+
const summaryContext = { kind, apiKind, namespace, name, group, resource, onNavigate: onNavigateToResource }
|
|
852
|
+
const expandedSummary = expanded && resource ? renderSummary?.({ ...summaryContext, context: 'expanded' }) ?? null : null
|
|
853
|
+
const drawerSummary = !expanded && resource ? renderSummary?.({ ...summaryContext, context: 'drawer' }) ?? null : null
|
|
779
854
|
const tabs: DetailShellTab<TabType>[] = [
|
|
780
855
|
{ id: 'overview', label: 'Overview', icon: <Layers className="w-4 h-4" /> },
|
|
856
|
+
{ id: 'spec', label: 'Spec & status', icon: <FileText className="w-4 h-4" />, hidden: !expandedSummary },
|
|
781
857
|
{ id: 'topology', label: 'Topology', icon: <Network className="w-4 h-4" />, hidden: topologyTabHidden },
|
|
782
858
|
{
|
|
783
859
|
id: 'timeline',
|
|
@@ -791,17 +867,20 @@ export function WorkloadView({
|
|
|
791
867
|
id: 'reachability',
|
|
792
868
|
label: 'Reachability',
|
|
793
869
|
icon: <Stethoscope className="w-4 h-4" />,
|
|
794
|
-
hidden: !(
|
|
870
|
+
hidden: !(renderDiagnoseTabForState && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
|
|
795
871
|
},
|
|
796
872
|
{ id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
|
|
797
873
|
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
798
874
|
]
|
|
799
|
-
const
|
|
875
|
+
const allTabs = mergeExtraTabs(tabs, expanded ? extraTabs : undefined)
|
|
876
|
+
const requestedTabAvailable = allTabs.some((tab) => tab.id === requestedTab && !tab.hidden)
|
|
800
877
|
const effectiveTab: TabType = requestedTabAvailable ? requestedTab : 'overview'
|
|
878
|
+
const activeExtraTab = expanded ? extraTabs?.find((x) => x.id === effectiveTab) : undefined
|
|
801
879
|
const shouldCommitFallback =
|
|
802
880
|
requestedTab !== 'overview' &&
|
|
803
881
|
!requestedTabAvailable &&
|
|
804
882
|
(
|
|
883
|
+
(requestedTab === 'spec' && !!resource && !resourceLoading && !expandedSummary) ||
|
|
805
884
|
(requestedTab === 'topology' && topologyTabHidden) ||
|
|
806
885
|
(requestedTab === 'metrics' && (!renderMetricsTab || (!!resource && !resourceLoading && !showMetricsTab))) ||
|
|
807
886
|
(requestedTab === 'cost' && (!renderCostTab || (!!resource && !resourceLoading && !showCostTab))) ||
|
|
@@ -890,19 +969,20 @@ export function WorkloadView({
|
|
|
890
969
|
</Tooltip>
|
|
891
970
|
</div>
|
|
892
971
|
<p className="text-sm text-theme-text-tertiary">{namespace}</p>
|
|
893
|
-
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
|
|
972
|
+
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath) || onOpenLiveTraffic) && (
|
|
894
973
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
895
974
|
{gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
|
|
896
975
|
{helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
|
|
897
976
|
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
898
977
|
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
899
978
|
)}
|
|
979
|
+
{onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
|
|
900
980
|
</div>
|
|
901
981
|
)}
|
|
902
982
|
</div>
|
|
903
983
|
|
|
904
984
|
{/* Actions bar */}
|
|
905
|
-
<ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...
|
|
985
|
+
<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} />
|
|
906
986
|
</div>
|
|
907
987
|
|
|
908
988
|
{/* Success animation overlay */}
|
|
@@ -917,6 +997,7 @@ export function WorkloadView({
|
|
|
917
997
|
damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
|
|
918
998
|
place. */}
|
|
919
999
|
<div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
|
|
1000
|
+
{deletedNotice && <div className="px-4 pt-4">{deletedNotice}</div>}
|
|
920
1001
|
{!resource ? (
|
|
921
1002
|
// Fill the drawer body so the loading logo centers in it, not in a
|
|
922
1003
|
// 128px box pinned to the top (matches the splash/PaneLoader centering).
|
|
@@ -941,9 +1022,36 @@ export function WorkloadView({
|
|
|
941
1022
|
/>
|
|
942
1023
|
) : (
|
|
943
1024
|
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
|
|
944
|
-
{
|
|
1025
|
+
{drawerSummary && (
|
|
1026
|
+
<div className="px-4 pt-3" role="tablist" aria-label="Detail view">
|
|
1027
|
+
<div className="inline-flex rounded-lg bg-theme-elevated p-0.5 text-xs">
|
|
1028
|
+
{([['overview', 'Overview'], ['spec', 'Spec & status']] as const).map(([id, label]) => {
|
|
1029
|
+
const on = id === 'spec' ? drawerSpec : !drawerSpec
|
|
1030
|
+
return (
|
|
1031
|
+
<button
|
|
1032
|
+
key={id}
|
|
1033
|
+
role="tab"
|
|
1034
|
+
aria-selected={on}
|
|
1035
|
+
onClick={() => setDrawerSpec(id === 'spec')}
|
|
1036
|
+
className={clsx(
|
|
1037
|
+
'rounded-md px-2.5 py-1 font-medium transition-colors',
|
|
1038
|
+
on ? 'bg-theme-surface text-theme-text-primary shadow-theme-sm' : 'text-theme-text-secondary hover:text-theme-text-primary',
|
|
1039
|
+
)}
|
|
1040
|
+
>
|
|
1041
|
+
{label}
|
|
1042
|
+
</button>
|
|
1043
|
+
)
|
|
1044
|
+
})}
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
)}
|
|
1048
|
+
{drawerSummary && !drawerSpec ? (
|
|
1049
|
+
drawerSummary
|
|
1050
|
+
) : (
|
|
1051
|
+
<>
|
|
1052
|
+
{renderOverviewLeadForState && hasOperationalIssues && (
|
|
945
1053
|
<div className="px-4 pt-4">
|
|
946
|
-
{
|
|
1054
|
+
{renderOverviewLeadForState({ kind, namespace, name })}
|
|
947
1055
|
</div>
|
|
948
1056
|
)}
|
|
949
1057
|
<ResourceRendererDispatch
|
|
@@ -957,9 +1065,9 @@ export function WorkloadView({
|
|
|
957
1065
|
onCopy={copyToClipboard}
|
|
958
1066
|
copied={copied}
|
|
959
1067
|
onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
|
|
960
|
-
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
1068
|
+
onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
|
|
961
1069
|
isSavingSecret={isUpdatingResource}
|
|
962
|
-
rendererOverrides={
|
|
1070
|
+
rendererOverrides={rendererOverridesForState}
|
|
963
1071
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
964
1072
|
renderMetrics={renderMetricsTab}
|
|
965
1073
|
events={resourceFocusedK8sEvents}
|
|
@@ -967,8 +1075,10 @@ export function WorkloadView({
|
|
|
967
1075
|
updates={resourceFocusedUpdates}
|
|
968
1076
|
eventsError={resourceFocusedK8sError}
|
|
969
1077
|
updatesError={resourceFocusedUpdatesError}
|
|
970
|
-
mainFooter={
|
|
1078
|
+
mainFooter={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'drawer' })}
|
|
971
1079
|
/>
|
|
1080
|
+
</>
|
|
1081
|
+
)}
|
|
972
1082
|
</OperationalIssuesShownContext.Provider>
|
|
973
1083
|
)}
|
|
974
1084
|
</div>
|
|
@@ -1047,6 +1157,7 @@ export function WorkloadView({
|
|
|
1047
1157
|
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
1048
1158
|
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
1049
1159
|
)}
|
|
1160
|
+
{onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
|
|
1050
1161
|
{relationships?.owner && !showOwnershipHeading && (
|
|
1051
1162
|
<span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
|
|
1052
1163
|
)}
|
|
@@ -1085,31 +1196,35 @@ export function WorkloadView({
|
|
|
1085
1196
|
)}
|
|
1086
1197
|
</>
|
|
1087
1198
|
}
|
|
1088
|
-
tabs={
|
|
1199
|
+
tabs={allTabs}
|
|
1089
1200
|
activeTab={effectiveTab}
|
|
1090
1201
|
onTabChange={handleSetTab}
|
|
1091
1202
|
scopeControls={scopeControls}
|
|
1092
|
-
tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...
|
|
1203
|
+
tabStripEnd={<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} hideLogs {...actionsBarPropsForState} />}
|
|
1093
1204
|
overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
|
|
1094
1205
|
compactHeader={compactHeader}
|
|
1095
1206
|
>
|
|
1096
1207
|
<div className="flex h-full min-h-0 flex-col">
|
|
1097
|
-
{
|
|
1208
|
+
{deletedNotice && <div className="shrink-0 px-4 pt-4">{deletedNotice}</div>}
|
|
1209
|
+
{!resourceGone && effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
|
|
1098
1210
|
<div className="shrink-0 px-4 pt-4">
|
|
1099
1211
|
<WorkloadRolloutNotice activity={rolloutActivity} recentImageSave={recentImageSave} />
|
|
1100
1212
|
</div>
|
|
1101
1213
|
)}
|
|
1102
1214
|
<div className="min-h-0 flex-1">
|
|
1103
|
-
{
|
|
1215
|
+
{activeExtraTab && <div className="h-full min-h-0 overflow-y-auto">{activeExtraTab.render()}</div>}
|
|
1216
|
+
{effectiveTab === 'overview' && expandedSummary ? (
|
|
1217
|
+
<div className="h-full min-h-0 overflow-y-auto">{expandedSummary}</div>
|
|
1218
|
+
) : effectiveTab === 'overview' && expandedOverview ? (
|
|
1104
1219
|
<div className="h-full min-h-0">
|
|
1105
|
-
{hasOperationalIssues &&
|
|
1220
|
+
{hasOperationalIssues && renderOverviewLeadForState && (
|
|
1106
1221
|
<div className="px-4 pt-4">
|
|
1107
|
-
{
|
|
1222
|
+
{renderOverviewLeadForState({ kind, namespace, name })}
|
|
1108
1223
|
</div>
|
|
1109
1224
|
)}
|
|
1110
1225
|
{expandedOverview}
|
|
1111
1226
|
</div>
|
|
1112
|
-
) : effectiveTab === 'overview' && (
|
|
1227
|
+
) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
|
|
1113
1228
|
<InfoTab
|
|
1114
1229
|
resource={resource}
|
|
1115
1230
|
selectedResource={selectedResource}
|
|
@@ -1128,22 +1243,22 @@ export function WorkloadView({
|
|
|
1128
1243
|
onNavigate={onNavigateToResource}
|
|
1129
1244
|
onCopy={copyToClipboard}
|
|
1130
1245
|
copied={copied}
|
|
1131
|
-
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
1246
|
+
onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
|
|
1132
1247
|
isSavingSecret={isUpdatingResource}
|
|
1133
1248
|
onOpenLogs={handleOpenLogs}
|
|
1134
1249
|
onSwitchToTimeline={() => handleSetTab('timeline')}
|
|
1135
1250
|
onSwitchToLogs={logsTabVisible ? () => handleSetTab('logs') : undefined}
|
|
1136
1251
|
onSwitchToTopology={!topologyTabHidden ? () => handleSetTab('topology') : undefined}
|
|
1137
|
-
rendererOverrides={
|
|
1252
|
+
rendererOverrides={rendererOverridesForState}
|
|
1138
1253
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
1139
1254
|
events={overviewEvents}
|
|
1140
1255
|
eventsLoading={overviewEventsLoading}
|
|
1141
1256
|
updates={resourceFocusedUpdates}
|
|
1142
1257
|
eventsError={overviewEventsError}
|
|
1143
1258
|
updatesError={resourceFocusedUpdatesError}
|
|
1144
|
-
extraContent={
|
|
1259
|
+
extraContent={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'expanded' })}
|
|
1145
1260
|
introContent={overviewIntro}
|
|
1146
|
-
leadContent={hasOperationalIssues &&
|
|
1261
|
+
leadContent={hasOperationalIssues && renderOverviewLeadForState ? renderOverviewLeadForState({ kind, namespace, name }) : undefined}
|
|
1147
1262
|
onEvaluateCapacity={onEvaluateCapacity}
|
|
1148
1263
|
recentImageSave={recentImageSave}
|
|
1149
1264
|
/>
|
|
@@ -1202,9 +1317,9 @@ export function WorkloadView({
|
|
|
1202
1317
|
{renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
|
|
1203
1318
|
</div>
|
|
1204
1319
|
)}
|
|
1205
|
-
{effectiveTab === 'reachability' &&
|
|
1320
|
+
{effectiveTab === 'reachability' && renderDiagnoseTabForState && (
|
|
1206
1321
|
<div className="flex h-full min-h-0 flex-col p-3">
|
|
1207
|
-
{
|
|
1322
|
+
{renderDiagnoseTabForState({ kind: resource?.kind || kind, namespace, name })}
|
|
1208
1323
|
</div>
|
|
1209
1324
|
)}
|
|
1210
1325
|
{effectiveTab === 'cost' && renderCostTab && (
|
|
@@ -1335,6 +1450,34 @@ function extractMetadata(kind: string, resource: any): { label: string; value: s
|
|
|
1335
1450
|
// SUB-COMPONENTS
|
|
1336
1451
|
// ============================================================================
|
|
1337
1452
|
|
|
1453
|
+
function DeletedResourceNotice({
|
|
1454
|
+
kindLabel,
|
|
1455
|
+
owner,
|
|
1456
|
+
onNavigateToResource,
|
|
1457
|
+
}: {
|
|
1458
|
+
kindLabel: string
|
|
1459
|
+
owner?: ResourceRef
|
|
1460
|
+
onNavigateToResource?: NavigateToResource
|
|
1461
|
+
}) {
|
|
1462
|
+
return (
|
|
1463
|
+
<AlertBanner
|
|
1464
|
+
variant="warning"
|
|
1465
|
+
title={`This ${kindLabel} no longer exists in the cluster`}
|
|
1466
|
+
message="You're looking at the last version Radar loaded before it was deleted."
|
|
1467
|
+
>
|
|
1468
|
+
{owner && onNavigateToResource && (
|
|
1469
|
+
<button
|
|
1470
|
+
type="button"
|
|
1471
|
+
onClick={() => onNavigateToResource(refToSelectedResource(owner))}
|
|
1472
|
+
className="mt-2 text-xs text-accent-text hover:underline"
|
|
1473
|
+
>
|
|
1474
|
+
Go to its {displayKindName(owner.kind, owner.kind)} {owner.name}
|
|
1475
|
+
</button>
|
|
1476
|
+
)}
|
|
1477
|
+
</AlertBanner>
|
|
1478
|
+
)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1338
1481
|
function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
1339
1482
|
return (
|
|
1340
1483
|
<Tooltip content="Open this resource in the GitOps tab (tree + insights + ops)" delay={150}>
|
|
@@ -1350,6 +1493,21 @@ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
|
1350
1493
|
)
|
|
1351
1494
|
}
|
|
1352
1495
|
|
|
1496
|
+
function LiveTrafficChip({ onClick }: { onClick: () => void }) {
|
|
1497
|
+
return (
|
|
1498
|
+
<Tooltip content="What this workload talks to, from the cluster's traffic source" delay={150}>
|
|
1499
|
+
<button
|
|
1500
|
+
type="button"
|
|
1501
|
+
onClick={onClick}
|
|
1502
|
+
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"
|
|
1503
|
+
>
|
|
1504
|
+
Live traffic
|
|
1505
|
+
<ArrowRight className="h-3 w-3 shrink-0" />
|
|
1506
|
+
</button>
|
|
1507
|
+
</Tooltip>
|
|
1508
|
+
)
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1353
1511
|
function OwnershipHeading({
|
|
1354
1512
|
podName,
|
|
1355
1513
|
context,
|
|
@@ -1846,6 +2004,7 @@ const LOGS_TAB_WITHOUT_PODS_KINDS = new Set([
|
|
|
1846
2004
|
'clusterworkflowtemplates',
|
|
1847
2005
|
'scaledjobs',
|
|
1848
2006
|
'jobsets',
|
|
2007
|
+
'clusters',
|
|
1849
2008
|
])
|
|
1850
2009
|
const RUNTIME_WORKLOAD_OVERVIEW_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs'])
|
|
1851
2010
|
const ROLLOUT_STATUS_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'rollouts'])
|
|
@@ -1862,6 +2021,11 @@ export function supportsLogsWithoutPods(
|
|
|
1862
2021
|
if (normalizedKind === 'jobsets') {
|
|
1863
2022
|
return group === 'jobset.x-k8s.io' && apiVersion === 'jobset.x-k8s.io/v1alpha2'
|
|
1864
2023
|
}
|
|
2024
|
+
// A CloudNativePG Cluster's instance Pods are its children, not related
|
|
2025
|
+
// Pods, and its logs are resolved server-side from the Cluster itself.
|
|
2026
|
+
if (normalizedKind === 'clusters') {
|
|
2027
|
+
return group === 'postgresql.cnpg.io' || !!apiVersion?.startsWith('postgresql.cnpg.io/')
|
|
2028
|
+
}
|
|
1865
2029
|
return true
|
|
1866
2030
|
}
|
|
1867
2031
|
|
|
@@ -2362,8 +2526,8 @@ function buildSummaryMetrics(
|
|
|
2362
2526
|
},
|
|
2363
2527
|
{
|
|
2364
2528
|
label: 'Latest run',
|
|
2365
|
-
value: status.lastScheduleTime ?
|
|
2366
|
-
detail: status.lastSuccessfulTime ? `last success ${
|
|
2529
|
+
value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never',
|
|
2530
|
+
detail: status.lastSuccessfulTime ? `last success ${formatRelativeAgeTime(status.lastSuccessfulTime)}` : 'no successful run recorded',
|
|
2367
2531
|
icon: Activity,
|
|
2368
2532
|
},
|
|
2369
2533
|
{
|
|
@@ -2479,8 +2643,8 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
|
|
|
2479
2643
|
{ label: 'Schedule', value: spec.schedule || '-' },
|
|
2480
2644
|
{ label: 'Active jobs', value: active, tone: active > 0 ? 'warn' : 'default' },
|
|
2481
2645
|
{ label: 'Suspend', value: suspended ? 'Yes' : 'No', tone: suspended ? 'warn' : 'default' },
|
|
2482
|
-
{ label: 'Last schedule', value: status.lastScheduleTime ?
|
|
2483
|
-
{ label: 'Last success', value: status.lastSuccessfulTime ?
|
|
2646
|
+
{ label: 'Last schedule', value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : '-' },
|
|
2647
|
+
{ label: 'Last success', value: status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : '-' },
|
|
2484
2648
|
],
|
|
2485
2649
|
}
|
|
2486
2650
|
}
|
|
@@ -2579,8 +2743,8 @@ function getScalingSummary(
|
|
|
2579
2743
|
label: phase,
|
|
2580
2744
|
detail: '',
|
|
2581
2745
|
stats: [
|
|
2582
|
-
{ label: 'Started', value: status.startTime ?
|
|
2583
|
-
{ label: 'Finished', value: status.completionTime ?
|
|
2746
|
+
{ label: 'Started', value: status.startTime ? formatRelativeAgeTime(status.startTime) : '-' },
|
|
2747
|
+
{ label: 'Finished', value: status.completionTime ? formatRelativeAgeTime(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
|
|
2584
2748
|
{ label: 'Parallelism', value: spec.parallelism ?? 1 },
|
|
2585
2749
|
{ label: 'Completions', value: spec.completions ?? 1 },
|
|
2586
2750
|
{ label: 'Completion mode', value: spec.completionMode ?? 'NonIndexed' },
|
|
@@ -3220,8 +3384,8 @@ function CronJobRunsCard({
|
|
|
3220
3384
|
>
|
|
3221
3385
|
<div className="mb-3 grid gap-x-8 gap-y-3 sm:grid-cols-2">
|
|
3222
3386
|
<OverviewStat label="Active jobs" value={activeJobs.length} tone={activeJobs.length > 0 ? 'warn' : 'default'} />
|
|
3223
|
-
<OverviewStat label="Last schedule" value={status.lastScheduleTime ?
|
|
3224
|
-
<OverviewStat label="Last success" value={status.lastSuccessfulTime ?
|
|
3387
|
+
<OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never'} />
|
|
3388
|
+
<OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : 'Never'} />
|
|
3225
3389
|
<OverviewStat label="Retained history" value={`${spec.successfulJobsHistoryLimit ?? 3} ok / ${spec.failedJobsHistoryLimit ?? 1} failed`} />
|
|
3226
3390
|
</div>
|
|
3227
3391
|
|
|
@@ -3678,7 +3842,7 @@ function ActivityCard({
|
|
|
3678
3842
|
<div className="min-w-0 flex-1">
|
|
3679
3843
|
<div className="flex items-center justify-between gap-2">
|
|
3680
3844
|
<span className="truncate text-sm font-medium text-theme-text-primary">{event.reason || event.eventType || 'update'}</span>
|
|
3681
|
-
<span className="shrink-0 text-xs text-theme-text-tertiary">{
|
|
3845
|
+
<span className="shrink-0 text-xs text-theme-text-tertiary">{formatRelativeAgeTime(event.timestamp)}</span>
|
|
3682
3846
|
</div>
|
|
3683
3847
|
<div className="mt-0.5 line-clamp-2 text-xs text-theme-text-secondary">
|
|
3684
3848
|
{event.message || event.diff?.summary || `${event.kind} ${event.name}`}
|
|
@@ -3801,16 +3965,6 @@ function summarizeServingExposure(
|
|
|
3801
3965
|
return { label: 'No serving path detected', badge: 'None', severity: 'neutral' }
|
|
3802
3966
|
}
|
|
3803
3967
|
|
|
3804
|
-
function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
3805
|
-
const seen = new Set<string>()
|
|
3806
|
-
return refs.filter((ref) => {
|
|
3807
|
-
const key = resourceRefId(ref)
|
|
3808
|
-
if (seen.has(key)) return false
|
|
3809
|
-
seen.add(key)
|
|
3810
|
-
return true
|
|
3811
|
-
})
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
3968
|
function resourceRefId(ref: ResourceRef): string {
|
|
3815
3969
|
return `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
3816
3970
|
}
|
|
@@ -3835,3 +3989,17 @@ function mergeAndRankEvents(events: TimelineEvent[], updates: TimelineEvent[]):
|
|
|
3835
3989
|
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
3836
3990
|
})
|
|
3837
3991
|
}
|
|
3992
|
+
|
|
3993
|
+
function mergeExtraTabs(tabs: DetailShellTab<TabType>[], extra: WorkloadExtraTab[] | undefined): DetailShellTab<TabType>[] {
|
|
3994
|
+
if (!extra || extra.length === 0) return tabs
|
|
3995
|
+
const replaced = new Set(extra.map((x) => x.replaces).filter(Boolean))
|
|
3996
|
+
const out = tabs.map((t) => (replaced.has(t.id) ? { ...t, hidden: true } : t))
|
|
3997
|
+
for (const x of extra) {
|
|
3998
|
+
const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon }
|
|
3999
|
+
const anchor = x.after ?? x.replaces
|
|
4000
|
+
const idx = anchor ? out.findIndex((t) => t.id === anchor) : -1
|
|
4001
|
+
if (idx >= 0) out.splice(idx + 1, 0, tab)
|
|
4002
|
+
else out.push(tab)
|
|
4003
|
+
}
|
|
4004
|
+
return out
|
|
4005
|
+
}
|
|
@@ -17,4 +17,10 @@ describe('supportsLogsWithoutPods', () => {
|
|
|
17
17
|
expect(supportsLogsWithoutPods('jobs', 'Job', 'batch', 'batch/v1')).toBe(true)
|
|
18
18
|
expect(supportsLogsWithoutPods('jobs', 'Job', 'example.io', 'example.io/v1')).toBe(false)
|
|
19
19
|
})
|
|
20
|
+
|
|
21
|
+
it('offers a CloudNativePG Cluster its merged instance logs, but not other Cluster kinds', () => {
|
|
22
|
+
expect(supportsLogsWithoutPods('clusters', 'Cluster', 'postgresql.cnpg.io', 'postgresql.cnpg.io/v1')).toBe(true)
|
|
23
|
+
expect(supportsLogsWithoutPods('clusters', 'Cluster', undefined, 'postgresql.cnpg.io/v1')).toBe(true)
|
|
24
|
+
expect(supportsLogsWithoutPods('clusters', 'Cluster', 'cluster.x-k8s.io', 'cluster.x-k8s.io/v1beta1')).toBe(false)
|
|
25
|
+
})
|
|
20
26
|
})
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,9 @@ export * from './components/checks'
|
|
|
55
55
|
// queue)
|
|
56
56
|
export * from './components/issues'
|
|
57
57
|
|
|
58
|
+
// CloudNativePG workspace model (fleet derivation over /api/cnpg/workspace)
|
|
59
|
+
export * from './components/cnpg'
|
|
60
|
+
|
|
58
61
|
// Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
|
|
59
62
|
export * from './components/cluster-switcher'
|
|
60
63
|
|
package/src/types/capacity.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Issue } from "../components/issues/types";
|
|
2
|
+
import type { NodeLifecycleObservation } from "../utils/node-lifecycle";
|
|
2
3
|
|
|
3
4
|
export const CAPACITY_SCHEMA_VERSION = "v1alpha1" as const;
|
|
4
5
|
|
|
@@ -232,6 +233,14 @@ export interface CapacityClaimLifecycleSummary {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
export interface CapacityNodeLifecycleSummary {
|
|
236
|
+
operational?: {
|
|
237
|
+
total: number;
|
|
238
|
+
ready: number;
|
|
239
|
+
notReady: number;
|
|
240
|
+
cordoned: number;
|
|
241
|
+
removing: number;
|
|
242
|
+
removingUnhealthy: number;
|
|
243
|
+
};
|
|
235
244
|
total: number;
|
|
236
245
|
ready: number;
|
|
237
246
|
notReady: number;
|
|
@@ -325,6 +334,7 @@ export type CapacityClaimStage =
|
|
|
325
334
|
| "unknown";
|
|
326
335
|
|
|
327
336
|
export interface CapacityNodeMember {
|
|
337
|
+
lifecycle?: NodeLifecycleObservation;
|
|
328
338
|
ready?: boolean;
|
|
329
339
|
cordoned: boolean;
|
|
330
340
|
conditions: CapacityCondition[];
|