@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.
- package/package.json +4 -4
- package/src/components/applications/ApplicationDetail.test.tsx +136 -8
- package/src/components/applications/ApplicationDetail.tsx +1215 -487
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +10 -3
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
- package/src/components/gitops/insights/insights-helpers.ts +5 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
- package/src/components/issues/IssuesView.tsx +55 -29
- package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
- package/src/components/issues/ResourceIssuesSection.tsx +21 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/shared/ResourceActionsBar.tsx +2 -3
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +7 -3
- package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyGraph.tsx +76 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +23 -15
- package/src/components/topology/index.ts +2 -0
- package/src/components/ui/Badge.tsx +1 -1
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/drawer-components.tsx +9 -6
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +24 -3
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +1 -0
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/applications.test.ts +34 -0
- package/src/utils/applications.ts +25 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/resource-hierarchy.test.ts +35 -1
- 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 =
|
|
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
|
-
|
|
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 && (
|