@skyhook-io/k8s-ui 1.8.11 → 1.8.12

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 (80) hide show
  1. package/package.json +4 -4
  2. package/src/components/applications/ApplicationDetail.test.tsx +136 -8
  3. package/src/components/applications/ApplicationDetail.tsx +1215 -487
  4. package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
  5. package/src/components/gitops/GitOpsTableView.tsx +10 -3
  6. package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
  7. package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
  8. package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
  9. package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
  10. package/src/components/gitops/insights/index.ts +1 -0
  11. package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
  12. package/src/components/gitops/insights/insights-helpers.ts +5 -0
  13. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
  14. package/src/components/issues/IssuesView.tsx +55 -29
  15. package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
  16. package/src/components/issues/ResourceIssuesSection.tsx +21 -1
  17. package/src/components/issues/severity.ts +1 -0
  18. package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
  19. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
  20. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
  21. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
  22. package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
  23. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
  24. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
  25. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  26. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
  27. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
  28. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
  29. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
  30. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
  31. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
  32. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
  33. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
  34. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
  35. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
  36. package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
  37. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
  38. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
  39. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
  40. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
  41. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
  42. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
  43. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
  44. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
  45. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
  46. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
  47. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
  48. package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
  49. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
  50. package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
  51. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
  52. package/src/components/resources/resource-utils-argo.test.ts +18 -1
  53. package/src/components/resources/resource-utils-argo.ts +8 -0
  54. package/src/components/shared/ResourceActionsBar.tsx +2 -3
  55. package/src/components/shared/UnifiedDiff.tsx +51 -0
  56. package/src/components/shared/index.ts +1 -0
  57. package/src/components/timeline/TimelineList.tsx +7 -3
  58. package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
  59. package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
  60. package/src/components/topology/TopologyControls.tsx +3 -1
  61. package/src/components/topology/TopologyGraph.tsx +76 -90
  62. package/src/components/topology/TopologyOverlayBar.tsx +30 -0
  63. package/src/components/topology/TopologySearch.tsx +23 -15
  64. package/src/components/topology/index.ts +2 -0
  65. package/src/components/ui/Badge.tsx +1 -1
  66. package/src/components/ui/SelectMenu.tsx +81 -0
  67. package/src/components/ui/drawer-components.tsx +9 -6
  68. package/src/components/ui/index.ts +2 -0
  69. package/src/components/workload/WorkloadView.tsx +24 -3
  70. package/src/theme/tailwind-theme.css +1 -0
  71. package/src/theme/variables.css +1 -0
  72. package/src/types/gitops-insights.ts +69 -1
  73. package/src/types/gitops.ts +3 -3
  74. package/src/utils/application-history.test.ts +538 -0
  75. package/src/utils/application-history.ts +257 -0
  76. package/src/utils/applications.test.ts +34 -0
  77. package/src/utils/applications.ts +25 -0
  78. package/src/utils/index.ts +1 -0
  79. package/src/utils/resource-hierarchy.test.ts +35 -1
  80. package/src/utils/resource-hierarchy.ts +20 -0
@@ -1,5 +1,5 @@
1
1
  import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge } from '../../ui/drawer-components'
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
5
 
@@ -23,6 +23,10 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
23
23
  const unresolvedRefsParents = parentStatuses.filter((p: any) =>
24
24
  (p.conditions || []).some((c: any) => c.type === 'ResolvedRefs' && c.status === 'False')
25
25
  )
26
+ // The Operational Issues section (when the host shows it) already reports these
27
+ // Accepted/ResolvedRefs failures with richer cause + next-step context, so drop
28
+ // the renderer's own banners to avoid showing the same failure twice.
29
+ const operationalIssuesShown = useOperationalIssuesShown()
26
30
 
27
31
  const firstParentConditions = parentStatuses.length > 0
28
32
  ? parentStatuses[0].conditions
@@ -47,7 +51,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
47
51
 
48
52
  return (
49
53
  <>
50
- {notAcceptedParents.length > 0 && (
54
+ {notAcceptedParents.length > 0 && !operationalIssuesShown && (
51
55
  <AlertBanner
52
56
  variant="error"
53
57
  title="Route Not Accepted"
@@ -61,7 +65,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
61
65
  />
62
66
  )}
63
67
 
64
- {unresolvedRefsParents.length > 0 && (
68
+ {unresolvedRefsParents.length > 0 && !operationalIssuesShown && (
65
69
  <AlertBanner
66
70
  variant="warning"
67
71
  title="Unresolved References"
@@ -1,5 +1,5 @@
1
1
  import { Cpu } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import { Badge } from '../../ui/Badge'
4
4
 
5
5
  interface GatewayClassRendererProps {
@@ -14,11 +14,12 @@ export function GatewayClassRenderer({ data }: GatewayClassRendererProps) {
14
14
  const acceptedCond = conditions.find((c: any) => c.type === 'Accepted')
15
15
  const isAccepted = acceptedCond?.status === 'True'
16
16
  const isNotAccepted = acceptedCond?.status === 'False'
17
+ const operationalIssuesShown = useOperationalIssuesShown()
17
18
 
18
19
  return (
19
20
  <>
20
21
  {/* Problem detection alert */}
21
- {isNotAccepted && (
22
+ {isNotAccepted && !operationalIssuesShown && (
22
23
  <AlertBanner
23
24
  variant="error"
24
25
  title="GatewayClass Not Accepted"
@@ -1,5 +1,5 @@
1
1
  import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge } from '../../ui/drawer-components'
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
5
 
@@ -23,6 +23,10 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
23
23
  const unresolvedRefsParents = parentStatuses.filter((p: any) =>
24
24
  (p.conditions || []).some((c: any) => c.type === 'ResolvedRefs' && c.status === 'False')
25
25
  )
26
+ // The Operational Issues section (when the host shows it) already reports these
27
+ // Accepted/ResolvedRefs failures with richer cause + next-step context, so drop
28
+ // the renderer's own banners to avoid showing the same failure twice.
29
+ const operationalIssuesShown = useOperationalIssuesShown()
26
30
 
27
31
  const firstParentConditions = parentStatuses.length > 0
28
32
  ? parentStatuses[0].conditions
@@ -47,7 +51,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
47
51
 
48
52
  return (
49
53
  <>
50
- {notAcceptedParents.length > 0 && (
54
+ {notAcceptedParents.length > 0 && !operationalIssuesShown && (
51
55
  <AlertBanner
52
56
  variant="error"
53
57
  title="Route Not Accepted"
@@ -61,7 +65,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
61
65
  />
62
66
  )}
63
67
 
64
- {unresolvedRefsParents.length > 0 && (
68
+ {unresolvedRefsParents.length > 0 && !operationalIssuesShown && (
65
69
  <AlertBanner
66
70
  variant="warning"
67
71
  title="Unresolved References"
@@ -1,5 +1,5 @@
1
1
  import { Server, HardDrive, Shield, Network, Tag, Image } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, KeyValueBadgeList } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, KeyValueBadgeList, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import { getEC2NodeClassStatus } from '../resource-utils-karpenter'
4
4
 
5
5
  interface KarpenterEC2NodeClassRendererProps {
@@ -13,6 +13,7 @@ export function KarpenterEC2NodeClassRenderer({ data }: KarpenterEC2NodeClassRen
13
13
 
14
14
  const nodeClassStatus = getEC2NodeClassStatus(data)
15
15
  const isNotReady = nodeClassStatus.level === 'unhealthy'
16
+ const operationalIssuesShown = useOperationalIssuesShown()
16
17
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
17
18
 
18
19
  const amiTerms = spec.amiSelectorTerms || []
@@ -28,7 +29,7 @@ export function KarpenterEC2NodeClassRenderer({ data }: KarpenterEC2NodeClassRen
28
29
 
29
30
  return (
30
31
  <>
31
- {isNotReady && (
32
+ {isNotReady && !operationalIssuesShown && (
32
33
  <AlertBanner
33
34
  variant="error"
34
35
  title="EC2NodeClass Not Ready"
@@ -1,6 +1,6 @@
1
1
  import { Server, Cpu, Settings } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
4
4
  import { kindToPlural } from '../../../utils/navigation'
5
5
  import { CAPACITY_TYPE_BADGE, BADGE_INACTIVE } from '../../../utils/badge-colors'
6
6
  import {
@@ -40,6 +40,7 @@ export function KarpenterNodeClaimRenderer({ data, onNavigate }: KarpenterNodeCl
40
40
 
41
41
  const claimStatus = getNodeClaimStatus(data)
42
42
  const isNotReady = claimStatus.level === 'unhealthy'
43
+ const operationalIssuesShown = useOperationalIssuesShown()
43
44
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
44
45
  const capacity = getNodeClaimCapacity(data)
45
46
  const requirements = getNodeClaimRequirements(data)
@@ -61,7 +62,7 @@ export function KarpenterNodeClaimRenderer({ data, onNavigate }: KarpenterNodeCl
61
62
  return (
62
63
  <>
63
64
  {/* Problem alert */}
64
- {isNotReady && (
65
+ {isNotReady && !operationalIssuesShown && (
65
66
  <AlertBanner
66
67
  variant="error"
67
68
  title="NodeClaim Not Ready"
@@ -1,5 +1,5 @@
1
1
  import { Server, Settings, Shield, Cpu, Tag, BarChart3 } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
4
  import {
5
5
  getNodePoolStatus,
@@ -31,6 +31,7 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
31
31
 
32
32
  const poolStatus = getNodePoolStatus(data)
33
33
  const isNotReady = poolStatus.level === 'unhealthy'
34
+ const operationalIssuesShown = useOperationalIssuesShown()
34
35
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
35
36
  const requirements = getNodePoolRequirements(data)
36
37
  const weight = getNodePoolWeight(data)
@@ -45,7 +46,7 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
45
46
  return (
46
47
  <>
47
48
  {/* Problem alert */}
48
- {isNotReady && (
49
+ {isNotReady && !operationalIssuesShown && (
49
50
  <AlertBanner
50
51
  variant="error"
51
52
  title="NodePool Not Ready"
@@ -1,5 +1,5 @@
1
1
  import { Cpu } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import {
4
4
  getScaledJobStatus,
5
5
  getScaledJobTarget,
@@ -18,13 +18,14 @@ export function KedaScaledJobRenderer({ data }: KedaScaledJobRendererProps) {
18
18
 
19
19
  const jobStatus = getScaledJobStatus(data)
20
20
  const isNotReady = jobStatus.level === 'unhealthy'
21
+ const operationalIssuesShown = useOperationalIssuesShown()
21
22
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
22
23
  const triggers = getScaledJobTriggers(data)
23
24
 
24
25
  return (
25
26
  <>
26
27
  {/* Problem alert */}
27
- {isNotReady && (
28
+ {isNotReady && !operationalIssuesShown && (
28
29
  <AlertBanner
29
30
  variant="error"
30
31
  title="ScaledJob Not Ready"
@@ -1,5 +1,5 @@
1
1
  import { Cpu, Settings } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { kindToPlural } from '../../../utils/navigation'
4
4
  import {
5
5
  getScaledObjectStatus,
@@ -39,6 +39,7 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
39
39
  const isPaused = soStatus.text === 'Paused'
40
40
  const isFallback = soStatus.text === 'Fallback'
41
41
  const isNotReady = soStatus.level === 'unhealthy'
42
+ const operationalIssuesShown = useOperationalIssuesShown()
42
43
 
43
44
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
44
45
  const fallbackCond = conditions.find((c: any) => c.type === 'Fallback')
@@ -53,7 +54,7 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
53
54
  message={fallbackCond?.message || 'KEDA is using fallback replicas because triggers are failing.'}
54
55
  />
55
56
  )}
56
- {isNotReady && !isFallback && (
57
+ {isNotReady && !isFallback && !operationalIssuesShown && (
57
58
  <AlertBanner
58
59
  variant="error"
59
60
  title="ScaledObject Not Ready"
@@ -1,5 +1,5 @@
1
1
  import { Box, Settings, Cloud, ScrollText, Pause, Layers } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
3
3
  import { CodeViewer } from '../../ui/CodeViewer'
4
4
  import {
5
5
  getCrossplaneStatus,
@@ -41,6 +41,7 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
41
41
  const atProvider = data?.status?.atProvider
42
42
 
43
43
  const alertVariant = status.level === 'unhealthy' || status.level === 'alert' ? status.level : null
44
+ const operationalIssuesShown = useOperationalIssuesShown()
44
45
 
45
46
  return (
46
47
  <>
@@ -53,7 +54,7 @@ export function ManagedResourceRenderer({ data, onNavigate }: ManagedResourceRen
53
54
  />
54
55
  )}
55
56
 
56
- {alertVariant && statusReason && (
57
+ {alertVariant && statusReason && !operationalIssuesShown && (
57
58
  <AlertBanner
58
59
  variant={alertVariant === 'unhealthy' ? 'error' : 'warning'}
59
60
  title={status.text}
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import { HardDrive } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
5
5
 
6
6
  interface PVCRendererProps {
7
7
  data: any
@@ -38,11 +38,12 @@ export function PVCRenderer({ data, onNavigate, extraSections }: PVCRendererProp
38
38
  const selectedNode = annotations['volume.kubernetes.io/selected-node']
39
39
  const bindCompleted = annotations['pv.kubernetes.io/bind-completed']
40
40
  const hasProvisionerInfo = provisioner || selectedNode || bindCompleted
41
+ const operationalIssuesShown = useOperationalIssuesShown()
41
42
 
42
43
  return (
43
44
  <>
44
45
  {/* Problem alerts */}
45
- {isLost && (
46
+ {isLost && !operationalIssuesShown && (
46
47
  <AlertBanner
47
48
  variant="error"
48
49
  title="Issues Detected"
@@ -1,6 +1,6 @@
1
1
  import { Lock, Key, FileText } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
4
4
 
5
5
  interface SealedSecretRendererProps {
6
6
  data: any
@@ -32,6 +32,7 @@ export function SealedSecretRenderer({ data, onNavigate }: SealedSecretRendererP
32
32
  const syncedCond = conditions.find((c: any) => c.type === 'Synced')
33
33
  const isSynced = syncedCond?.status === 'True'
34
34
  const isNotSynced = syncedCond?.status === 'False'
35
+ const operationalIssuesShown = useOperationalIssuesShown()
35
36
 
36
37
  const hasTemplateMetadata =
37
38
  (templateLabels && Object.keys(templateLabels).length > 0) ||
@@ -40,7 +41,7 @@ export function SealedSecretRenderer({ data, onNavigate }: SealedSecretRendererP
40
41
  return (
41
42
  <>
42
43
  {/* Problem detection alert */}
43
- {isNotSynced && (
44
+ {isNotSynced && !operationalIssuesShown && (
44
45
  <AlertBanner
45
46
  variant="error"
46
47
  title="Secret is not synced"
@@ -1,5 +1,5 @@
1
1
  import { ShieldCheck } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown } from '../../ui/drawer-components'
3
3
  import {
4
4
  getSecretStoreStatus,
5
5
  getSecretStoreProviderType,
@@ -41,11 +41,12 @@ export function SecretStoreRenderer({ data }: SecretStoreRendererProps) {
41
41
  // Problem detection
42
42
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
43
43
  const isNotReady = readyCond?.status === 'False'
44
+ const operationalIssuesShown = useOperationalIssuesShown()
44
45
 
45
46
  return (
46
47
  <>
47
48
  {/* Alert if not ready */}
48
- {isNotReady && (
49
+ {isNotReady && !operationalIssuesShown && (
49
50
  <AlertBanner
50
51
  variant="error"
51
52
  title="Store Not Ready"
@@ -1,5 +1,5 @@
1
1
  import { Globe, ArrowRight, Network } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge } from '../../ui/drawer-components'
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
5
 
@@ -26,6 +26,10 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
26
26
  const unresolvedRefsParents = parentStatuses.filter((p: any) =>
27
27
  (p.conditions || []).some((c: any) => c.type === 'ResolvedRefs' && c.status === 'False')
28
28
  )
29
+ // The Operational Issues section (when the host shows it) already reports these
30
+ // Accepted/ResolvedRefs failures with richer cause + next-step context, so drop
31
+ // the renderer's own banners to avoid showing the same failure twice.
32
+ const operationalIssuesShown = useOperationalIssuesShown()
29
33
 
30
34
  const firstParentConditions = parentStatuses.length > 0
31
35
  ? parentStatuses[0].conditions
@@ -50,7 +54,7 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
50
54
 
51
55
  return (
52
56
  <>
53
- {notAcceptedParents.length > 0 && (
57
+ {notAcceptedParents.length > 0 && !operationalIssuesShown && (
54
58
  <AlertBanner
55
59
  variant="error"
56
60
  title="Route Not Accepted"
@@ -64,7 +68,7 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
64
68
  />
65
69
  )}
66
70
 
67
- {unresolvedRefsParents.length > 0 && (
71
+ {unresolvedRefsParents.length > 0 && !operationalIssuesShown && (
68
72
  <AlertBanner
69
73
  variant="warning"
70
74
  title="Unresolved References"
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { getArgoApplicationStatus } from './resource-utils-argo'
2
+ import { getArgoApplicationStatus, isArgoOperationInProgress } from './resource-utils-argo'
3
3
 
4
4
  describe('getArgoApplicationStatus', () => {
5
5
  // A Suspended Argo app is intentionally paused — neutral (sky), matching the
@@ -20,4 +20,21 @@ describe('getArgoApplicationStatus', () => {
20
20
  const badge = getArgoApplicationStatus({ status: { health: { status: 'Degraded' }, sync: { status: 'Synced' } } })
21
21
  expect(badge.level).toBe('unhealthy')
22
22
  })
23
+
24
+ it('surfaces an operation being terminated', () => {
25
+ const badge = getArgoApplicationStatus({ status: { operationState: { phase: 'Terminating' } } })
26
+ expect(badge.level).toBe('degraded')
27
+ expect(badge.text).toBe('Terminating')
28
+ })
29
+ })
30
+
31
+ describe('isArgoOperationInProgress', () => {
32
+ it.each([
33
+ [{ status: { operationState: { phase: 'Running' } } }, true],
34
+ [{ status: { operationState: { phase: 'Terminating' } } }, true],
35
+ [{ operation: { sync: {} }, status: { operationState: { phase: 'Succeeded' } } }, true],
36
+ [{ status: { operationState: { phase: 'Succeeded' } } }, false],
37
+ ])('maps %o to %s', (app, expected) => {
38
+ expect(isArgoOperationInProgress(app)).toBe(expected)
39
+ })
23
40
  })
@@ -33,6 +33,9 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
33
33
  if (opPhase === 'Running') {
34
34
  return { text: 'Syncing', color: healthColors.degraded, level: 'degraded' }
35
35
  }
36
+ if (opPhase === 'Terminating') {
37
+ return { text: 'Terminating', color: healthColors.degraded, level: 'degraded' }
38
+ }
36
39
 
37
40
  // Failed operation
38
41
  if (opPhase === 'Failed' || opPhase === 'Error') {
@@ -61,6 +64,11 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
61
64
  return { text: health || sync || 'Unknown', color: healthColors.unknown, level: 'unknown' }
62
65
  }
63
66
 
67
+ export function isArgoOperationInProgress(app: any): boolean {
68
+ const phase = app?.status?.operationState?.phase
69
+ return app?.operation != null || phase === 'Running' || phase === 'Terminating'
70
+ }
71
+
64
72
  // Radar suspends an Argo Application by clearing spec.syncPolicy.automated and
65
73
  // recording the prior prune/selfHeal flags in annotations so Resume can restore
66
74
  // them. The *presence* of any of these annotations marks the app as suspended —
@@ -18,6 +18,7 @@ import {
18
18
  } from 'lucide-react'
19
19
  import { createTwoFilesPatch } from 'diff'
20
20
  import { clsx } from 'clsx'
21
+ import { classifyDiffLine } from './UnifiedDiff'
21
22
  import { Tooltip } from '../ui/Tooltip'
22
23
  import { ForceDeleteConfirmDialog, type CascadeDependent } from '../ui/ForceDeleteConfirmDialog'
23
24
  import { ConfirmDialog } from '../ui/ConfirmDialog'
@@ -1124,9 +1125,7 @@ function RevisionDiffView({ currentTemplate, selectedTemplate, currentRevision,
1124
1125
  {hasChanges ? (
1125
1126
  <pre className="text-xs font-mono p-0 m-0">
1126
1127
  {diffLines.map((line, index) => {
1127
- const isAddition = line.startsWith('+') && !line.startsWith('+++')
1128
- const isDeletion = line.startsWith('-') && !line.startsWith('---')
1129
- const isHeader = line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')
1128
+ const { isAddition, isRemoval: isDeletion, isHeader } = classifyDiffLine(line)
1130
1129
 
1131
1130
  return (
1132
1131
  <div
@@ -0,0 +1,51 @@
1
+ import { clsx } from 'clsx'
2
+
3
+ // Shared unified-diff rendering primitives. Every diff surface (Helm manifest
4
+ // diff, Helm values preview, revision history, Argo resource diff) classifies
5
+ // lines by the same leading marker; keeping that logic here stops the four
6
+ // renderers from silently drifting apart.
7
+
8
+ // classifyDiffLine tags a unified-diff line as an addition, a removal, or a hunk/
9
+ // file header. `+++`/`---` file headers are NOT additions/removals.
10
+ export function classifyDiffLine(line: string): {
11
+ isAddition: boolean
12
+ isRemoval: boolean
13
+ isHeader: boolean
14
+ } {
15
+ return {
16
+ isAddition: line.startsWith('+') && !line.startsWith('+++'),
17
+ isRemoval: line.startsWith('-') && !line.startsWith('---'),
18
+ isHeader: line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++'),
19
+ }
20
+ }
21
+
22
+ // hasDiffBodyChange is true when a unified-diff string has any added/removed body
23
+ // line (ignoring the patch's own file/hunk headers) — the "any real change?"
24
+ // short-circuit shared by the manifest and Argo diff surfaces.
25
+ export function hasDiffBodyChange(diff: string): boolean {
26
+ return diff.split('\n').some((line) => {
27
+ const { isAddition, isRemoval } = classifyDiffLine(line)
28
+ return isAddition || isRemoval
29
+ })
30
+ }
31
+
32
+ // DiffLine renders one unified-diff line colored by marker — the gutter-less
33
+ // "manifest" style shared by the Helm manifest diff and the Argo resource diff.
34
+ // (The line-numbered gutter variants build their own row but share
35
+ // classifyDiffLine.)
36
+ export function DiffLine({ line }: { line: string }) {
37
+ const { isAddition, isRemoval, isHeader } = classifyDiffLine(line)
38
+ return (
39
+ <div
40
+ className={clsx(
41
+ 'whitespace-pre',
42
+ isAddition && 'bg-green-500/10 text-green-700 dark:text-green-400',
43
+ isRemoval && 'bg-red-500/10 text-red-700 dark:text-red-400',
44
+ isHeader && 'text-theme-text-tertiary font-bold',
45
+ !isAddition && !isRemoval && !isHeader && 'text-theme-text-secondary'
46
+ )}
47
+ >
48
+ {line || ' '}
49
+ </div>
50
+ )
51
+ }
@@ -4,3 +4,4 @@ export { ResourceActionsBar, RevisionHistoryDialog } from './ResourceActionsBar'
4
4
  export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
5
5
  export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
6
6
  export { DetailShell, type DetailShellProps, type DetailShellTab } from './DetailShell'
7
+ export { classifyDiffLine, hasDiffBodyChange, DiffLine } from './UnifiedDiff'
@@ -98,6 +98,10 @@ export interface TimelineListProps {
98
98
  // returned set reaches it, the list surfaces an end-of-list note so the drop of
99
99
  // older events isn't silent. Omit when the source isn't capped (e.g. compact).
100
100
  truncatedAt?: number
101
+ // Hosts that filter a capped source after fetching must report truncation
102
+ // explicitly because the visible row count no longer reflects the source cap.
103
+ isTruncated?: boolean
104
+ truncationMessage?: string
101
105
  }
102
106
 
103
107
  const TIME_RANGES: { value: TimeRange; label: string }[] = [
@@ -109,7 +113,7 @@ const TIME_RANGES: { value: TimeRange; label: string }[] = [
109
113
  { value: 'all', label: 'All' },
110
114
  ]
111
115
 
112
- export function TimelineList({ events, isLoading, onRefresh, onQueryChange, hasLimitedAccess, namespaces, onViewChange, currentView = 'list', onResourceClick, initialFilter, initialTimeRange, rangeOptions = TIME_RANGES, hideRangeSelector = false, showDeleted: showDeletedProp, onShowDeletedChange, search: searchProp, onSearchChange, activityFilter: activityFilterProp, onActivityFilterChange, kindFilter: kindFilterProp, onKindFilterChange, onVisibleWindowChange, scrollToMs, compact = false, selectedEventId, onSelectEvent, truncatedAt }: TimelineListProps) {
116
+ export function TimelineList({ events, isLoading, onRefresh, onQueryChange, hasLimitedAccess, namespaces, onViewChange, currentView = 'list', onResourceClick, initialFilter, initialTimeRange, rangeOptions = TIME_RANGES, hideRangeSelector = false, showDeleted: showDeletedProp, onShowDeletedChange, search: searchProp, onSearchChange, activityFilter: activityFilterProp, onActivityFilterChange, kindFilter: kindFilterProp, onKindFilterChange, onVisibleWindowChange, scrollToMs, compact = false, selectedEventId, onSelectEvent, truncatedAt, isTruncated, truncationMessage }: TimelineListProps) {
113
117
  const [searchInternal, setSearchInternal] = useState('')
114
118
  const searchTerm = searchProp ?? searchInternal
115
119
  const setSearchTerm = onSearchChange ?? setSearchInternal
@@ -510,10 +514,10 @@ export function TimelineList({ events, isLoading, onRefresh, onQueryChange, hasL
510
514
  </div>
511
515
  </div>
512
516
  ))}
513
- {truncatedAt != null && events.length >= truncatedAt && (
517
+ {truncatedAt != null && (isTruncated ?? events.length >= truncatedAt) && (
514
518
  <div className="flex items-center justify-center gap-2 py-4 text-sm text-theme-text-tertiary">
515
519
  <Clock className="h-4 w-4 opacity-50" />
516
- <span>Showing the newest {truncatedAt.toLocaleString()} events in this range — narrow the query to see older ones</span>
520
+ <span>{truncationMessage ?? `Showing the newest ${truncatedAt.toLocaleString()} events in this range — narrow the query to see older ones`}</span>
517
521
  </div>
518
522
  )}
519
523
  </div>
@@ -35,6 +35,30 @@ describe('TimelineList controlled vs fallback', () => {
35
35
  expect(html).toContain('K8s Events')
36
36
  expect(html).toContain('Problems')
37
37
  })
38
+
39
+ it('shows an explicit truncation message after host-side filtering', () => {
40
+ const html = renderToString(
41
+ <TimelineList
42
+ events={EVENTS}
43
+ isLoading={false}
44
+ truncatedAt={10_000}
45
+ isTruncated
46
+ truncationMessage="Only the newest source window was searched."
47
+ />,
48
+ )
49
+ expect(html).toContain('Only the newest source window was searched.')
50
+ })
51
+
52
+ it('does not infer truncation when the host explicitly reports a complete source', () => {
53
+ const events = Array.from({ length: 2 }, (_, index) => ({
54
+ ...EVENTS[0],
55
+ id: String(index),
56
+ }))
57
+ const html = renderToString(
58
+ <TimelineList events={events} isLoading={false} truncatedAt={2} isTruncated={false} />,
59
+ )
60
+ expect(html).not.toContain('Showing the newest 2 events')
61
+ })
38
62
  })
39
63
 
40
64
  describe('TimelineSwimlanes controlled vs fallback', () => {
@@ -0,0 +1,30 @@
1
+ interface TopologyBreadcrumbProps {
2
+ /** The single active namespace to show. */
3
+ namespace: string
4
+ /** When set, renders a clickable "All Namespaces" crumb that clears the filter. */
5
+ onClear?: () => void
6
+ }
7
+
8
+ /**
9
+ * Namespace-context crumb for the topology overlay bar, shown when the graph is
10
+ * scoped to a single namespace. Place it in the host's overlay-bar composition
11
+ * (e.g. above the search) so it reads as page context.
12
+ */
13
+ export function TopologyBreadcrumb({ namespace, onClear }: TopologyBreadcrumbProps) {
14
+ return (
15
+ <div className="flex items-center gap-1.5 pointer-events-auto">
16
+ {onClear && (
17
+ <button
18
+ onClick={onClear}
19
+ className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
20
+ >
21
+ All Namespaces
22
+ </button>
23
+ )}
24
+ {onClear && <span className="text-xs text-theme-text-tertiary">/</span>}
25
+ <span className="text-xs font-medium text-theme-text-secondary bg-theme-surface/80 backdrop-blur-sm border border-theme-border/50 rounded-md px-2 py-0.5">
26
+ {namespace}
27
+ </span>
28
+ </div>
29
+ )
30
+ }
@@ -80,7 +80,9 @@ export function TopologyControls({
80
80
  }
81
81
 
82
82
  return (
83
- <div className="absolute top-4 right-4 z-10 flex items-center gap-2">
83
+ // Layout-neutral (positioned by the host's overlay bar): `relative` anchors
84
+ // its own dropdowns; `pointer-events-auto` inside the pointer-events-none bar.
85
+ <div className="relative flex items-center gap-2 pointer-events-auto">
84
86
  {/* Freshness/liveness status — backed for legibility over the canvas but
85
87
  borderless + divided off, so it reads as a status, not another control. */}
86
88
  {leadingSlot && (