@skyhook-io/k8s-ui 1.16.3 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  3. package/src/components/applications/ApplicationDetail.tsx +17 -15
  4. package/src/components/checks/types.ts +2 -13
  5. package/src/components/cnpg/primitives.tsx +3 -3
  6. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  7. package/src/components/gitops/detail-helpers.ts +1 -1
  8. package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
  9. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
  10. package/src/components/issues/issue-timing.ts +2 -2
  11. package/src/components/issues/issues.test.ts +1 -1
  12. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  13. package/src/components/logs/WorkloadLogsViewer.tsx +5 -0
  14. package/src/components/logs/useLogStream.ts +11 -0
  15. package/src/components/resources/ResourcesView.tsx +7 -8
  16. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  17. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  18. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
  19. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  20. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  21. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  22. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  23. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  24. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
  25. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
  26. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  27. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  28. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  29. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  30. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  31. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
  32. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
  33. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  34. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  35. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  36. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  37. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  38. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  39. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  40. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  41. package/src/components/resources/renderers/HPARenderer.tsx +2 -2
  42. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  43. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  44. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  45. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  46. package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
  47. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  48. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  49. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  50. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  51. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  52. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  53. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  54. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  55. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  56. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  57. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  58. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  59. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  60. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  61. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  62. package/src/components/resources/renderers/PodRenderer.tsx +21 -7
  63. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  64. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  65. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  66. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  67. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  68. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  69. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  70. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
  71. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  72. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  73. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  74. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  75. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  76. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  77. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  78. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  79. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  80. package/src/components/resources/resource-utils-cnpg.ts +0 -4
  81. package/src/components/resources/resource-utils-keda.ts +1 -7
  82. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  83. package/src/components/resources/resource-utils-velero.ts +0 -12
  84. package/src/components/resources/resource-utils.ts +17 -33
  85. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  86. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  87. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  88. package/src/components/shared/ResourceActionsBar.tsx +9 -20
  89. package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
  90. package/src/components/shared/SetImageDialog.test.ts +75 -6
  91. package/src/components/shared/SetImageDialog.tsx +92 -84
  92. package/src/components/shared/index.ts +2 -1
  93. package/src/components/timeline/TimelineList.tsx +2 -15
  94. package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
  95. package/src/components/topology/TopologyGraph.tsx +5 -3
  96. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  97. package/src/components/ui/ScheduleValue.tsx +3 -2
  98. package/src/components/ui/drawer-components.test.tsx +66 -1
  99. package/src/components/ui/drawer-components.tsx +43 -29
  100. package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
  101. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  102. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  103. package/src/components/workload/WorkloadView.tsx +120 -43
  104. package/src/types/capacity.ts +10 -0
  105. package/src/types/core.ts +32 -1
  106. package/src/utils/format.test.ts +40 -2
  107. package/src/utils/format.ts +5 -1
  108. package/src/utils/gateway-references.test.ts +17 -0
  109. package/src/utils/gateway-references.ts +29 -0
  110. package/src/utils/gitops-write-guard.test.ts +401 -0
  111. package/src/utils/gitops-write-guard.ts +509 -0
  112. package/src/utils/index.ts +2 -0
  113. package/src/utils/ingress-class-references.ts +22 -0
  114. package/src/utils/navigation.test.ts +13 -0
  115. package/src/utils/navigation.ts +5 -0
  116. package/src/utils/node-lifecycle.test.ts +36 -0
  117. package/src/utils/node-lifecycle.ts +77 -0
  118. package/src/utils/resource-hierarchy.test.ts +32 -0
  119. package/src/utils/resource-hierarchy.ts +28 -2
  120. package/src/utils/resource-refs.test.ts +23 -0
  121. package/src/utils/resource-refs.ts +30 -0
@@ -1,8 +1,8 @@
1
1
  import { Server, Shield, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
- import { formatAge } from '../resource-utils'
5
4
  import { getKCPStatus, getKCPVersion, getKCPInitialized, getMachineClusterName } from '../resource-utils-capi'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface Props {
8
8
  data: any
@@ -50,7 +50,7 @@ export function CAPIKubeadmControlPlaneRenderer({ data, onNavigate }: Props) {
50
50
  <Property label="Initialized" value={initialized ? 'Yes' : 'No'} />
51
51
  {updateStrategy && <Property label="Update Strategy" value={updateStrategy} />}
52
52
  {readyCond?.lastTransitionTime && (
53
- <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
53
+ <Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
54
54
  )}
55
55
  </PropertyList>
56
56
  </Section>
@@ -1,8 +1,8 @@
1
1
  import { Server, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
- import { formatAge } from '../resource-utils'
5
4
  import { getMachineDeploymentStatus, getMachineDeploymentVersion, getMachineClusterName } from '../resource-utils-capi'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface Props {
8
8
  data: any
@@ -56,7 +56,7 @@ export function CAPIMachineDeploymentRenderer({ data, onNavigate }: Props) {
56
56
  <Property label="Cluster" value={clusterName} />
57
57
  <Property label="Version" value={version} />
58
58
  {readyCond?.lastTransitionTime && (
59
- <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
59
+ <Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
60
60
  )}
61
61
  </PropertyList>
62
62
  </Section>
@@ -1,8 +1,8 @@
1
1
  import { Server, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
- import { formatAge } from '../resource-utils'
5
4
  import { getMachinePoolStatus } from '../resource-utils-capi'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface Props {
8
8
  data: any
@@ -44,7 +44,7 @@ export function CAPIMachinePoolRenderer({ data, onNavigate }: Props) {
44
44
  <Property label="Min Ready Seconds" value={String(spec.minReadySeconds)} />
45
45
  )}
46
46
  {readyCond?.lastTransitionTime && (
47
- <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
47
+ <Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
48
48
  )}
49
49
  </PropertyList>
50
50
  </Section>
@@ -1,8 +1,8 @@
1
1
  import { Cpu, Server, Network, Cloud } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
- import { formatAge } from '../resource-utils'
5
4
  import { getMachineStatus, getMachineRole, getMachineClusterName, getMachineNodeRef, getMachineVersion, getMachineProviderID, parseProviderID, getProviderFromInfraKind, parseCAPIConditionMessage } from '../resource-utils-capi'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface Props {
8
8
  data: any
@@ -59,7 +59,7 @@ export function CAPIMachineRenderer({ data, onNavigate }: Props) {
59
59
  {infraProvider && <Property label="Provider" value={infraProvider} />}
60
60
  {spec.failureDomain && <Property label="Failure Domain" value={spec.failureDomain} />}
61
61
  {readyCond?.lastTransitionTime && (
62
- <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
62
+ <Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
63
63
  )}
64
64
  </PropertyList>
65
65
  </Section>
@@ -1,8 +1,8 @@
1
1
  import { Server, Settings } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
- import { formatAge } from '../resource-utils'
5
4
  import { getMachineSetStatus, getMachineClusterName } from '../resource-utils-capi'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface Props {
8
8
  data: any
@@ -46,7 +46,7 @@ export function CAPIMachineSetRenderer({ data, onNavigate }: Props) {
46
46
  <Property label="Min Ready Seconds" value={String(spec.minReadySeconds)} />
47
47
  )}
48
48
  {readyCond?.lastTransitionTime && (
49
- <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
49
+ <Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
50
50
  )}
51
51
  </PropertyList>
52
52
  </Section>
@@ -28,6 +28,7 @@ import {
28
28
  } from '../resource-utils-cnpg'
29
29
  import { formatAge } from '../resource-utils'
30
30
  import { RecoveryTime } from './CNPGShared'
31
+ import { formatRelativeAgeTime } from '../../../utils/format'
31
32
 
32
33
  interface CNPGClusterRendererProps {
33
34
  /** Filled by the host with the Databases, Publications and Subscriptions
@@ -195,7 +196,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
195
196
  variant="warning"
196
197
  title="Last backup failed"
197
198
  message={
198
- `The most recent backup attempt failed${lastBackupFailure.lastTransitionTime ? ` ${formatAge(lastBackupFailure.lastTransitionTime)} ago` : ''}.` +
199
+ `The most recent backup attempt failed${lastBackupFailure.lastTransitionTime ? ` ${formatRelativeAgeTime(lastBackupFailure.lastTransitionTime)}` : ''}.` +
199
200
  (lastBackupFailure.message ? ` Operator reported: ${lastBackupFailure.message}` : '')
200
201
  }
201
202
  />
@@ -1,11 +1,11 @@
1
1
  import { Clock, Database } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import { CronValue } from '../../ui/ScheduleValue'
4
+ import { TimeValue } from '../../ui/ScheduleValue'
4
5
  import {
5
6
  getCNPGScheduledBackupCluster,
6
7
  getCNPGScheduleCron,
7
8
  getCNPGScheduledBackupMethod,
8
- getCNPGScheduledBackupLastSchedule,
9
9
  getCNPGScheduledBackupNextSchedule,
10
10
  getCNPGScheduledBackupIsSuspended,
11
11
  getCNPGScheduledBackupIsImmediate,
@@ -39,7 +39,7 @@ export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledB
39
39
  <Section title="Schedule" icon={Clock} defaultExpanded>
40
40
  <PropertyList>
41
41
  <Property label="Cron Expression" value={<CronValue cron={getCNPGScheduleCron(data)} dialect="seconds" />} />
42
- <Property label="Last Schedule" value={getCNPGScheduledBackupLastSchedule(data)} />
42
+ <Property label="Last Schedule" value={<TimeValue timestamp={data.status?.lastScheduleTime} />} />
43
43
  <Property label="Next Schedule" value={getCNPGScheduledBackupNextSchedule(data)} />
44
44
  <Property label="Suspended" value={isSuspended ? 'Yes' : 'No'} />
45
45
  <Property label="Immediate" value={getCNPGScheduledBackupIsImmediate(data) ? 'Yes' : 'No'} />
@@ -1,4 +1,4 @@
1
- import { formatAge } from '../resource-utils'
1
+ import { formatRelativeAgeTime } from '../../../utils/format'
2
2
 
3
3
  /**
4
4
  * A recovery point, stated absolutely with the interval beside it.
@@ -21,7 +21,7 @@ export function RecoveryTime({ at }: { at: string }) {
21
21
  <span>{d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC')}</span>
22
22
  {/* One interpolation, so the age and its unit stay a single text node
23
23
  rather than being split by a renderer comment marker. */}
24
- <span className="text-xs text-theme-text-tertiary shrink-0">{`${formatAge(at)} ago`}</span>
24
+ <span className="text-xs text-theme-text-tertiary shrink-0">{formatRelativeAgeTime(at)}</span>
25
25
  </span>
26
26
  )
27
27
  }
@@ -4,10 +4,10 @@ import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../../ui/Collapse'
6
6
  import { Input } from '../../ui/Input'
7
- import { formatAge } from '../resource-utils'
8
7
  import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER } from './trivy-shared'
9
8
  import { pluralize } from '../../../utils/pluralize'
10
9
  import { BADGE_INACTIVE } from '../../../utils/badge-colors'
10
+ import { TimeValue } from '../../ui/ScheduleValue'
11
11
 
12
12
  interface ClusterComplianceReportRendererProps {
13
13
  data: any
@@ -97,7 +97,7 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
97
97
  {compliance.description && <Property label="Description" value={compliance.description} />}
98
98
  {compliance.version && <Property label="Version" value={compliance.version} />}
99
99
  {compliance.platform && <Property label="Platform" value={compliance.platform} />}
100
- <Property label="Last Updated" value={status.updateTimestamp ? formatAge(status.updateTimestamp) + ' ago' : '-'} />
100
+ <Property label="Last Updated" value={<TimeValue timestamp={status.updateTimestamp} />} />
101
101
  </PropertyList>
102
102
  </Section>
103
103
 
@@ -3,9 +3,9 @@ import { ShieldCheck, CheckCircle2, XCircle } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../../ui/Collapse'
6
- import { formatAge } from '../resource-utils'
7
6
  import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER, TrivyAlertBanner } from './trivy-shared'
8
7
  import { BADGE_INACTIVE } from '../../../utils/badge-colors'
8
+ import { TimeValue } from '../../ui/ScheduleValue'
9
9
 
10
10
  interface ConfigAuditReportRendererProps {
11
11
  data: any
@@ -115,7 +115,7 @@ export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererPro
115
115
  <Section title="Report Overview" icon={ShieldCheck}>
116
116
  <PropertyList>
117
117
  <Property label="Scanner" value={scanner.name ? `${scanner.name} ${scanner.version || ''}`.trim() : '-'} />
118
- <Property label="Scanned" value={report.updateTimestamp ? formatAge(report.updateTimestamp) + ' ago' : '-'} />
118
+ <Property label="Scanned" value={<TimeValue timestamp={report.updateTimestamp} />} />
119
119
  </PropertyList>
120
120
  </Section>
121
121
 
@@ -1,6 +1,8 @@
1
1
  import { Clock, Pause } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
- import { formatAge, cronToHuman } from '../resource-utils'
3
+ import { cronToHuman } from '../resource-utils'
4
+ import { TimeValue } from '../../ui/ScheduleValue'
5
+ import { formatRelativeAgeTime } from '../../../utils/format'
4
6
 
5
7
  interface CronJobRendererProps {
6
8
  data: any
@@ -53,7 +55,7 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
53
55
  <AlertBanner
54
56
  variant="error"
55
57
  title="Recent Jobs Failing"
56
- message={<>Jobs have been scheduled but haven't succeeded recently. Last success: {formatAge(status.lastSuccessfulTime)}. Check job history and pod logs.</>}
58
+ message={<>Jobs have been scheduled but haven't succeeded recently. Last success: {formatRelativeAgeTime(status.lastSuccessfulTime, 'never')}. Check job history and pod logs.</>}
57
59
  />
58
60
  )}
59
61
 
@@ -62,8 +64,8 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
62
64
  <Property label="Schedule" value={spec.schedule} />
63
65
  <Property label="Human" value={cronToHuman(spec.schedule)} />
64
66
  <Property label="Suspend" value={spec.suspend ? 'Yes' : 'No'} />
65
- <Property label="Last Schedule" value={status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never'} />
66
- <Property label="Last Success" value={status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : 'Never'} />
67
+ <Property label="Last Schedule" value={<TimeValue timestamp={status.lastScheduleTime} fallback="Never" />} />
68
+ <Property label="Last Success" value={<TimeValue timestamp={status.lastSuccessfulTime} fallback="Never" />} />
67
69
  <Property label="Active Jobs" value={status.active?.length || 0} />
68
70
  </PropertyList>
69
71
  </Section>
@@ -1,6 +1,7 @@
1
1
  import { Clock, Pause } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink, ConditionsSection } from '../../ui/drawer-components'
3
- import { formatAge, cronToHuman } from '../resource-utils'
3
+ import { cronToHuman } from '../resource-utils'
4
+ import { TimeValue } from '../../ui/ScheduleValue'
4
5
 
5
6
  interface CronWorkflowRendererProps {
6
7
  data: any
@@ -45,7 +46,7 @@ export function CronWorkflowRenderer({ data, onNavigate }: CronWorkflowRendererP
45
46
  <Property label="Timezone" value={spec.timezone || 'Cluster default'} />
46
47
  <Property label="Suspend" value={isSuspended ? 'Yes' : 'No'} />
47
48
  <Property label="Concurrency" value={spec.concurrencyPolicy || 'Allow'} />
48
- <Property label="Last Scheduled" value={status.lastScheduledTime ? formatAge(status.lastScheduledTime) : 'Never'} />
49
+ <Property label="Last Scheduled" value={<TimeValue timestamp={status.lastScheduledTime} fallback="Never" />} />
49
50
  <Property label="Active Workflows" value={active.length} />
50
51
  </PropertyList>
51
52
  </Section>
@@ -0,0 +1,24 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { expect, it, vi } from 'vitest'
5
+ import { EndpointSliceRenderer } from './EndpointSliceRenderer'
6
+
7
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
8
+
9
+ it('navigates to the labeled Service as a core-group resource', async () => {
10
+ const onNavigate = vi.fn()
11
+ const element = document.createElement('div')
12
+ const root = createRoot(element)
13
+ const slice = {
14
+ apiVersion: 'discovery.k8s.io/v1',
15
+ kind: 'EndpointSlice',
16
+ metadata: { name: 'web-abc', namespace: 'team', labels: { 'kubernetes.io/service-name': 'web' } },
17
+ }
18
+ await act(async () => { root.render(<EndpointSliceRenderer data={slice} onNavigate={onNavigate} />) })
19
+ const button = [...element.querySelectorAll('button')].find(b => b.textContent === 'web')
20
+ expect(button).toBeDefined()
21
+ await act(async () => { button!.click() })
22
+ expect(onNavigate).toHaveBeenCalledWith({ kind: 'Service', group: '', namespace: 'team', name: 'web' })
23
+ await act(async () => root.unmount())
24
+ })
@@ -47,7 +47,7 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
47
47
  <button
48
48
  type="button"
49
49
  className="text-sm text-accent-text hover:underline font-medium"
50
- onClick={() => onNavigate({ kind: 'Service', namespace: metadata.namespace, name: serviceName })}
50
+ onClick={() => onNavigate({ kind: 'Service', group: '', namespace: metadata.namespace, name: serviceName })}
51
51
  >
52
52
  {serviceName}
53
53
  </button>
@@ -2,9 +2,9 @@ import { AlertTriangle, Info, Clock, Target, Server, Hash } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Badge } from '../../ui/Badge'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
- import { formatAge } from '../resource-utils'
6
5
  import type { NavigateToResource } from '../../../utils/navigation'
7
6
  import { apiVersionToGroup, kindToPluralWithGroup } from '../../../utils/navigation'
7
+ import { TimeValue } from '../../ui/ScheduleValue'
8
8
 
9
9
  interface EventRendererProps {
10
10
  data: any
@@ -118,10 +118,10 @@ export function EventRenderer({ data, onNavigate }: EventRendererProps) {
118
118
  <PropertyList>
119
119
  {count > 1 && <Property label="Count" value={count} />}
120
120
  {firstTimestamp && (
121
- <Property label="First Seen" value={formatAge(firstTimestamp)} />
121
+ <Property label="First Seen" value={<TimeValue timestamp={firstTimestamp} />} />
122
122
  )}
123
123
  {lastTimestamp && (
124
- <Property label="Last Seen" value={formatAge(lastTimestamp)} />
124
+ <Property label="Last Seen" value={<TimeValue timestamp={lastTimestamp} />} />
125
125
  )}
126
126
  {firstTimestamp && lastTimestamp && firstTimestamp !== lastTimestamp && (
127
127
  <Property label="Duration" value={calculateDuration(firstTimestamp, lastTimestamp)} />
@@ -1,8 +1,8 @@
1
1
  import { Activity, Boxes, Server } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, AlertBanner, ConditionsSection } from '../../ui/drawer-components'
4
- import { formatAge } from '../resource-utils'
5
4
  import { canaryStepTemplateRefs } from './RolloutRenderer'
5
+ import { TimeValue } from '../../ui/ScheduleValue'
6
6
 
7
7
  interface ExperimentRendererProps {
8
8
  data: any
@@ -72,7 +72,7 @@ export function ExperimentRenderer({ data, onNavigate }: ExperimentRendererProps
72
72
  <Property label="Phase" value={<span className={clsx('badge', phaseClass(phase))}>{phase}</span>} />
73
73
  <Property label="Duration" value={spec.duration} />
74
74
  <Property label="Message" value={status.message} />
75
- <Property label="Available At" value={status.availableAt ? formatAge(status.availableAt) : undefined} />
75
+ <Property label="Available At" value={status.availableAt ? <TimeValue timestamp={status.availableAt} /> : undefined} />
76
76
  {ownerRollout && (
77
77
  <Property
78
78
  label="Rollout"
@@ -3,10 +3,10 @@ import { KeyRound } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
5
  import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
6
- import { formatAge } from '../resource-utils'
7
6
  import { SEVERITY_BADGE_COLORS, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
8
7
  import { pluralize } from '../../../utils/pluralize'
9
8
  import { BADGE_INACTIVE } from '../../../utils/badge-colors'
9
+ import { TimeValue } from '../../ui/ScheduleValue'
10
10
 
11
11
  interface ExposedSecretReportRendererProps {
12
12
  data: any
@@ -45,7 +45,7 @@ export function ExposedSecretReportRenderer({ data }: ExposedSecretReportRendere
45
45
  <Property label="Container" value={containerName} />
46
46
  <Property label="Image" value={image} />
47
47
  <Property label="Scanner" value={scanner.name ? `${scanner.name} ${scanner.version || ''}`.trim() : '-'} />
48
- <Property label="Scanned" value={report.updateTimestamp ? formatAge(report.updateTimestamp) + ' ago' : '-'} />
48
+ <Property label="Scanned" value={<TimeValue timestamp={report.updateTimestamp} />} />
49
49
  </PropertyList>
50
50
  </Section>
51
51
 
@@ -1,10 +1,10 @@
1
1
  import { KeyRound, RefreshCw, CloudCog } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
3
+ import { TimeValue } from '../../ui/ScheduleValue'
3
4
  import {
4
5
  getExternalSecretStatus,
5
6
  getExternalSecretStore,
6
7
  getExternalSecretRefreshInterval,
7
- getExternalSecretLastSync,
8
8
  getExternalSecretTargetName,
9
9
  getExternalSecretTargetCreationPolicy,
10
10
  getExternalSecretTargetDeletionPolicy,
@@ -47,7 +47,7 @@ export function ExternalSecretRenderer({ data, onNavigate }: ExternalSecretRende
47
47
  <Section title="Sync Status" icon={RefreshCw} defaultExpanded>
48
48
  <PropertyList>
49
49
  <Property label="Status" value={esStatus.text} />
50
- <Property label="Last Sync" value={getExternalSecretLastSync(data)} />
50
+ <Property label="Last Sync" value={<TimeValue timestamp={data.status?.refreshTime} />} />
51
51
  <Property label="Refresh Interval" value={getExternalSecretRefreshInterval(data)} />
52
52
  <Property label="Target Secret" value={(() => {
53
53
  const targetName = getExternalSecretTargetName(data)
@@ -1,9 +1,9 @@
1
1
  import { Package, Settings, CheckCircle2, History, FileCode2, GitBranch } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts, ResourceLink } from '../../ui/drawer-components'
4
- import { formatAge } from '../resource-utils'
5
4
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
6
5
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
6
+ import { TimeValue } from '../../ui/ScheduleValue'
7
7
 
8
8
  interface FluxHelmReleaseRendererProps {
9
9
  data: any
@@ -249,13 +249,13 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
249
249
  {lastRelease.firstDeployed && (
250
250
  <Property
251
251
  label="First Deployed"
252
- value={formatAge(lastRelease.firstDeployed)}
252
+ value={<TimeValue timestamp={lastRelease.firstDeployed} />}
253
253
  />
254
254
  )}
255
255
  {lastRelease.lastDeployed && (
256
256
  <Property
257
257
  label="Last Deployed"
258
- value={formatAge(lastRelease.lastDeployed)}
258
+ value={<TimeValue timestamp={lastRelease.lastDeployed} />}
259
259
  />
260
260
  )}
261
261
  </PropertyList>
@@ -319,7 +319,7 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
319
319
  {status.lastHandledReconcileAt && (
320
320
  <Property
321
321
  label="Last Reconciled"
322
- value={formatAge(status.lastHandledReconcileAt)}
322
+ value={<TimeValue timestamp={status.lastHandledReconcileAt} />}
323
323
  />
324
324
  )}
325
325
  </PropertyList>
@@ -2,6 +2,7 @@ import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import { Badge } from '../../ui/Badge'
4
4
  import type { ResourceRef } from '../../../types'
5
+ import { gatewayBackendResourceRef, gatewayParentResourceRef } from '../../../utils/gateway-references'
5
6
 
6
7
  interface GRPCRouteRendererProps {
7
8
  data: any
@@ -32,22 +33,8 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
32
33
  ? parentStatuses[0].conditions
33
34
  : undefined
34
35
 
35
- function toGatewayRef(ref: any): ResourceRef {
36
- return {
37
- kind: 'Gateway',
38
- namespace: ref.namespace || routeNs,
39
- name: ref.name,
40
- group: 'gateway.networking.k8s.io',
41
- }
42
- }
43
-
44
- function toServiceRef(backend: any): ResourceRef {
45
- return {
46
- kind: 'Service',
47
- namespace: backend.namespace || routeNs,
48
- name: backend.name,
49
- }
50
- }
36
+ const toParentRef = (ref: any) => gatewayParentResourceRef(ref, routeNs)
37
+ const toBackendRef = (ref: any) => gatewayBackendResourceRef(ref, routeNs)
51
38
 
52
39
  return (
53
40
  <>
@@ -59,8 +46,8 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
59
46
  const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
60
47
  const gwName = p.parentRef?.name || 'unknown'
61
48
  return cond?.reason
62
- ? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
63
- : `Gateway "${gwName}" has not accepted this route.`
49
+ ? `Parent "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
50
+ : `Parent "${gwName}" has not accepted this route.`
64
51
  }).join('; ')}
65
52
  />
66
53
  )}
@@ -90,12 +77,12 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
90
77
  }
91
78
  />
92
79
  <Property
93
- label="Parent Gateways"
80
+ label="Parents"
94
81
  value={
95
82
  parentRefs.length > 0 ? (
96
83
  <div className="flex flex-wrap gap-1">
97
84
  {parentRefs.map((ref: any, i: number) => (
98
- <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
85
+ <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toParentRef(ref)} onClick={onNavigate} />
99
86
  ))}
100
87
  </div>
101
88
  ) : 'None'
@@ -129,7 +116,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
129
116
  <div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
130
117
  <span className="italic text-theme-text-tertiary">all</span>
131
118
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
132
- <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
119
+ <ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
133
120
  {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
134
121
  </div>
135
122
  )}
@@ -141,7 +128,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
141
128
  return (
142
129
  <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
143
130
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
144
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
131
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
145
132
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
146
133
  {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
147
134
  </div>
@@ -176,7 +163,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
176
163
  {matchIdx === 0 && backendRefs.length === 1 && (
177
164
  <>
178
165
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
179
- <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
166
+ <ResourceRefBadge resourceRef={toBackendRef(backendRefs[0])} onClick={onNavigate} />
180
167
  {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
181
168
  </>
182
169
  )}
@@ -189,7 +176,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
189
176
  return (
190
177
  <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
191
178
  <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
192
- <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
179
+ <ResourceRefBadge resourceRef={toBackendRef(b)} onClick={onNavigate} />
193
180
  {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
194
181
  {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
195
182
  </div>
@@ -0,0 +1,63 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+ import { HTTPRouteRenderer } from './HTTPRouteRenderer'
6
+ import { GRPCRouteRenderer } from './GRPCRouteRenderer'
7
+ import { SimpleRouteRenderer } from './SimpleRouteRenderer'
8
+
9
+ const backend = { name: 'custom-backend', kind: 'Widget', group: 'relationships.radar.test', namespace: 'backends' }
10
+ const parent = { name: 'mesh-parent', kind: 'Service', group: '', namespace: 'mesh' }
11
+
12
+ function routeData(kind: string) {
13
+ return {
14
+ metadata: { name: 'route', namespace: 'app' },
15
+ status: { parents: [{ parentRef: parent, conditions: [{ type: 'Accepted', status: 'False', reason: 'Rejected' }] }] },
16
+ spec: {
17
+ parentRefs: [parent],
18
+ rules: [{
19
+ backendRefs: [backend, { name: 'default-service', port: 8080 }],
20
+ ...(kind === 'HTTPRoute' ? { filters: [{ type: 'RequestMirror', requestMirror: { backendRef: { ...backend, name: 'mirror' } } }] } : {}),
21
+ }],
22
+ },
23
+ }
24
+ }
25
+
26
+ describe('Gateway route reference navigation', () => {
27
+ it.each(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute'] as const)('preserves backend and parent identities in %s', async kind => {
28
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
29
+ const container = document.createElement('div')
30
+ const root = createRoot(container)
31
+ const onNavigate = vi.fn()
32
+ const data = routeData(kind)
33
+ const click = async (name: string) => {
34
+ const button = [...container.querySelectorAll('button')].find(button => button.textContent?.includes(name))
35
+ expect(button, `button ${name}`).toBeDefined()
36
+ await act(async () => button!.click())
37
+ }
38
+ try {
39
+ await act(async () => root.render(
40
+ kind === 'HTTPRoute' ? <HTTPRouteRenderer data={data} onNavigate={onNavigate} />
41
+ : kind === 'GRPCRoute' ? <GRPCRouteRenderer data={data} onNavigate={onNavigate} />
42
+ : <SimpleRouteRenderer kind={kind} data={data} onNavigate={onNavigate} />,
43
+ ))
44
+ expect(container.textContent).toContain('Parents')
45
+ expect(container.textContent).toContain('Parent "mesh-parent"')
46
+ expect(container.textContent).not.toContain('Gateway "mesh-parent"')
47
+
48
+ await click('custom-backend')
49
+ expect(onNavigate).toHaveBeenLastCalledWith(backend)
50
+ await click('default-service')
51
+ expect(onNavigate).toHaveBeenLastCalledWith({ kind: 'Service', group: '', namespace: 'app', name: 'default-service' })
52
+ await click('mesh-parent')
53
+ expect(onNavigate).toHaveBeenLastCalledWith(parent)
54
+ if (kind === 'HTTPRoute') {
55
+ await click('mirror')
56
+ expect(onNavigate).toHaveBeenLastCalledWith({ ...backend, name: 'mirror' })
57
+ }
58
+ } finally {
59
+ await act(async () => root.unmount())
60
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
61
+ }
62
+ })
63
+ })
@@ -1,9 +1,9 @@
1
1
  import { GitBranch, FolderGit, CheckCircle2 } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
3
- import { formatAge } from '../resource-utils'
4
3
  import { formatBytes } from '../../../utils/format'
5
4
  import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
6
5
  import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
6
+ import { TimeValue } from '../../ui/ScheduleValue'
7
7
 
8
8
  interface GitRepositoryRendererProps {
9
9
  data: any
@@ -87,7 +87,7 @@ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
87
87
  <Property label="Digest" value={artifact.digest} />
88
88
  <Property
89
89
  label="Last Updated"
90
- value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
90
+ value={<TimeValue timestamp={artifact.lastUpdateTime} />}
91
91
  />
92
92
  {artifact.size && (
93
93
  <Property label="Size" value={formatBytes(artifact.size)} />
@@ -104,7 +104,7 @@ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
104
104
  {status.lastHandledReconcileAt && (
105
105
  <Property
106
106
  label="Last Reconciled"
107
- value={formatAge(status.lastHandledReconcileAt)}
107
+ value={<TimeValue timestamp={status.lastHandledReconcileAt} />}
108
108
  />
109
109
  )}
110
110
  </PropertyList>
@@ -4,9 +4,9 @@ import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property, ConditionsSection, ResourceLink, type ConditionTone } from '../../ui/drawer-components'
5
5
  import { Badge } from '../../ui/Badge'
6
6
  import { kindToPlural } from '../../../utils/navigation'
7
- import { formatAge } from '../resource-utils'
8
7
  import { HPADiagnosisSummary } from '../HPADiagnosisSummary'
9
8
  import type { HPADiagnosis } from '../../../types'
9
+ import { TimeValue } from '../../ui/ScheduleValue'
10
10
 
11
11
  interface HPARendererProps {
12
12
  data: any
@@ -63,7 +63,7 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
63
63
  <Property label="Desired" value={status.desiredReplicas} />
64
64
  <Property label="Min" value={spec.minReplicas ?? 1} />
65
65
  <Property label="Max" value={spec.maxReplicas} />
66
- {status.lastScaleTime && <Property label="Last Scale" value={formatAge(status.lastScaleTime)} />}
66
+ {status.lastScaleTime && <Property label="Last Scale" value={<TimeValue timestamp={status.lastScaleTime} />} />}
67
67
  </PropertyList>
68
68
  </Section>
69
69