@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,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
ReactFlow,
|
|
4
4
|
ReactFlowProvider,
|
|
@@ -23,6 +23,7 @@ import { toCanvas } from 'html-to-image'
|
|
|
23
23
|
|
|
24
24
|
import { AlertTriangle, ChevronsDownUp, ChevronsUpDown, Download, Info, Layers, LayoutGrid, Loader2, Maximize, Minus, Pause, Play, Plus, RotateCw, Shield } from 'lucide-react'
|
|
25
25
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
26
|
+
import { TopologyOverlayBar } from './TopologyOverlayBar'
|
|
26
27
|
import { Input } from '../ui/Input'
|
|
27
28
|
import { Tooltip } from '../ui/Tooltip'
|
|
28
29
|
import { useToast } from '../ui/Toast'
|
|
@@ -186,10 +187,6 @@ interface TopologyGraphProps {
|
|
|
186
187
|
onTogglePause?: () => void
|
|
187
188
|
/** Called when user clicks "maximize" on a namespace group — sets namespace filter to just that namespace */
|
|
188
189
|
onMaximizeNamespace?: (namespace: string) => void
|
|
189
|
-
/** Shown as a breadcrumb label when viewing a single namespace */
|
|
190
|
-
namespaceBreadcrumb?: string
|
|
191
|
-
/** Called when breadcrumb "back" is clicked to return to all-namespace view */
|
|
192
|
-
onClearNamespace?: () => void
|
|
193
190
|
/** Serialized namespace filter — when this changes, reset groupLevels for fresh smart default */
|
|
194
191
|
namespacesKey?: string
|
|
195
192
|
/** Node to pan/zoom the canvas to. Bump focusNonce to re-trigger for the same id. */
|
|
@@ -207,6 +204,9 @@ interface TopologyGraphProps {
|
|
|
207
204
|
onToggleReplicaSets?: (ownerID: string) => void
|
|
208
205
|
/** Padding reserved around fit-to-view operations for overlaid UI. */
|
|
209
206
|
fitViewPadding?: FitViewOptions['padding']
|
|
207
|
+
/** Host overlay content (e.g. a search + controls row), stacked above the
|
|
208
|
+
* topology's own status banners in the overlay bar. */
|
|
209
|
+
children?: ReactNode
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
export function TopologyGraph({
|
|
@@ -220,8 +220,6 @@ export function TopologyGraph({
|
|
|
220
220
|
paused = false,
|
|
221
221
|
onTogglePause,
|
|
222
222
|
onMaximizeNamespace,
|
|
223
|
-
namespaceBreadcrumb,
|
|
224
|
-
onClearNamespace,
|
|
225
223
|
namespacesKey = '',
|
|
226
224
|
focusNodeId,
|
|
227
225
|
focusNonce,
|
|
@@ -229,6 +227,7 @@ export function TopologyGraph({
|
|
|
229
227
|
onNodeHover,
|
|
230
228
|
onToggleReplicaSets,
|
|
231
229
|
fitViewPadding = 0.15,
|
|
230
|
+
children,
|
|
232
231
|
}: TopologyGraphProps) {
|
|
233
232
|
const isTrafficView = viewMode === 'traffic'
|
|
234
233
|
const [nodes, setNodes, onNodesChangeBase] = useNodesState([] as Node[])
|
|
@@ -933,96 +932,83 @@ export function TopologyGraph({
|
|
|
933
932
|
|
|
934
933
|
return (
|
|
935
934
|
<ReactFlowProvider>
|
|
936
|
-
{/*
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
<
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
935
|
+
{/* Overlay bar: status banners lead (a partial-data/RBAC warning must read
|
|
936
|
+
before the user trusts the graph), host children stack below. Flex
|
|
937
|
+
column so nothing overlaps. Banners de-absoluted — the bar owns position/z. */}
|
|
938
|
+
<TopologyOverlayBar>
|
|
939
|
+
{/* Warning banner for partial topology data */}
|
|
940
|
+
{topology?.warnings && topology.warnings.length > 0 && (() => {
|
|
941
|
+
const rbacWarnings = topology.warnings.filter(w => w.includes('RBAC not granted'))
|
|
942
|
+
const viewWarnings = topology.warnings.filter(w => w.startsWith('Topology view:'))
|
|
943
|
+
const otherWarnings = topology.warnings.filter(w => !w.includes('RBAC not granted') && !w.startsWith('Topology view:'))
|
|
944
|
+
const isAllRbac = otherWarnings.length === 0 && viewWarnings.length === 0
|
|
945
|
+
const isViewLimited = otherWarnings.length === 0 && rbacWarnings.length === 0 && viewWarnings.length > 0
|
|
946
|
+
return (
|
|
947
|
+
<div className={`pointer-events-auto ${isViewLimited ? 'max-w-xl' : 'w-full'} ${isAllRbac ? 'bg-amber-500/10 border-amber-500/20' : 'bg-amber-500/10 border-amber-500/30'} border rounded-lg p-2 backdrop-blur-sm`}>
|
|
948
|
+
<div className="flex items-start gap-2">
|
|
949
|
+
{isAllRbac ? (
|
|
950
|
+
<Shield className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
|
951
|
+
) : (
|
|
952
|
+
<AlertTriangle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
|
953
|
+
)}
|
|
954
|
+
<div className="text-sm">
|
|
955
|
+
<span className="font-medium text-amber-400">
|
|
956
|
+
{isAllRbac ? 'Limited Access:' : isViewLimited ? 'View limited:' : 'Warning:'}
|
|
957
|
+
</span>
|
|
958
|
+
<span className="text-theme-text-secondary ml-1">
|
|
959
|
+
{isAllRbac
|
|
960
|
+
? `${pluralize(rbacWarnings.length, 'resource type')} not accessible due to RBAC restrictions.`
|
|
961
|
+
: isViewLimited
|
|
962
|
+
? 'Large retained run history is summarized to keep the graph readable.'
|
|
963
|
+
: 'Some resources failed to load. Data may be incomplete.'}
|
|
964
|
+
</span>
|
|
965
|
+
<details className="mt-1">
|
|
966
|
+
<summary className="text-xs text-amber-400/80 hover:text-amber-400">
|
|
967
|
+
Show details ({topology.warnings.length})
|
|
968
|
+
</summary>
|
|
969
|
+
<ul className="mt-1 text-xs text-theme-text-tertiary space-y-0.5">
|
|
970
|
+
{rbacWarnings.length > 0 && otherWarnings.length > 0 && (
|
|
971
|
+
<li className="text-amber-400/60 font-medium mt-1">RBAC restrictions:</li>
|
|
972
|
+
)}
|
|
973
|
+
{rbacWarnings.map((w, i) => (
|
|
974
|
+
<li key={`rbac-${i}`} className="font-mono">{w}</li>
|
|
975
|
+
))}
|
|
976
|
+
{viewWarnings.length > 0 && (rbacWarnings.length > 0 || otherWarnings.length > 0) && (
|
|
977
|
+
<li className="text-amber-400/60 font-medium mt-1">View limits:</li>
|
|
978
|
+
)}
|
|
979
|
+
{viewWarnings.map((w, i) => (
|
|
980
|
+
<li key={`view-${i}`} className="font-mono">{w}</li>
|
|
981
|
+
))}
|
|
982
|
+
{otherWarnings.length > 0 && (rbacWarnings.length > 0 || viewWarnings.length > 0) && (
|
|
983
|
+
<li className="text-amber-400/60 font-medium mt-1">Other warnings:</li>
|
|
984
|
+
)}
|
|
985
|
+
{otherWarnings.map((w, i) => (
|
|
986
|
+
<li key={`other-${i}`} className="font-mono">{w}</li>
|
|
987
|
+
))}
|
|
988
|
+
</ul>
|
|
989
|
+
</details>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
)
|
|
994
|
+
})()}
|
|
995
|
+
{/* Layout error banner - shown even when stale nodes exist */}
|
|
996
|
+
{layoutError && nodes.length > 0 && (
|
|
997
|
+
<div className="w-full pointer-events-auto bg-red-500/10 border border-red-500/30 rounded-lg p-2 backdrop-blur-sm">
|
|
964
998
|
<div className="flex items-start gap-2">
|
|
965
|
-
|
|
966
|
-
<Shield className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
|
967
|
-
) : (
|
|
968
|
-
<AlertTriangle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
|
969
|
-
)}
|
|
999
|
+
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
|
|
970
1000
|
<div className="text-sm">
|
|
971
|
-
<span className="font-medium text-
|
|
972
|
-
{isAllRbac ? 'Limited Access:' : isViewLimited ? 'View limited:' : 'Warning:'}
|
|
973
|
-
</span>
|
|
1001
|
+
<span className="font-medium text-red-400">Layout Error:</span>
|
|
974
1002
|
<span className="text-theme-text-secondary ml-1">
|
|
975
|
-
|
|
976
|
-
? `${pluralize(rbacWarnings.length, 'resource type')} not accessible due to RBAC restrictions.`
|
|
977
|
-
: isViewLimited
|
|
978
|
-
? 'Large retained run history is summarized to keep the graph readable.'
|
|
979
|
-
: 'Some resources failed to load. Data may be incomplete.'}
|
|
1003
|
+
Failed to update layout. Showing previous view.
|
|
980
1004
|
</span>
|
|
981
|
-
<
|
|
982
|
-
<summary className="text-xs text-amber-400/80 hover:text-amber-400">
|
|
983
|
-
Show details ({topology.warnings.length})
|
|
984
|
-
</summary>
|
|
985
|
-
<ul className="mt-1 text-xs text-theme-text-tertiary space-y-0.5">
|
|
986
|
-
{rbacWarnings.length > 0 && otherWarnings.length > 0 && (
|
|
987
|
-
<li className="text-amber-400/60 font-medium mt-1">RBAC restrictions:</li>
|
|
988
|
-
)}
|
|
989
|
-
{rbacWarnings.map((w, i) => (
|
|
990
|
-
<li key={`rbac-${i}`} className="font-mono">{w}</li>
|
|
991
|
-
))}
|
|
992
|
-
{viewWarnings.length > 0 && (rbacWarnings.length > 0 || otherWarnings.length > 0) && (
|
|
993
|
-
<li className="text-amber-400/60 font-medium mt-1">View limits:</li>
|
|
994
|
-
)}
|
|
995
|
-
{viewWarnings.map((w, i) => (
|
|
996
|
-
<li key={`view-${i}`} className="font-mono">{w}</li>
|
|
997
|
-
))}
|
|
998
|
-
{otherWarnings.length > 0 && (rbacWarnings.length > 0 || viewWarnings.length > 0) && (
|
|
999
|
-
<li className="text-amber-400/60 font-medium mt-1">Other warnings:</li>
|
|
1000
|
-
)}
|
|
1001
|
-
{otherWarnings.map((w, i) => (
|
|
1002
|
-
<li key={`other-${i}`} className="font-mono">{w}</li>
|
|
1003
|
-
))}
|
|
1004
|
-
</ul>
|
|
1005
|
-
</details>
|
|
1005
|
+
<p className="mt-1 text-xs text-theme-text-tertiary font-mono">{layoutError}</p>
|
|
1006
1006
|
</div>
|
|
1007
1007
|
</div>
|
|
1008
1008
|
</div>
|
|
1009
|
-
)
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
{layoutError && nodes.length > 0 && (
|
|
1013
|
-
<div className="absolute top-2 left-2 right-2 z-10 bg-red-500/10 border border-red-500/30 rounded-lg p-2 backdrop-blur-sm">
|
|
1014
|
-
<div className="flex items-start gap-2">
|
|
1015
|
-
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
|
|
1016
|
-
<div className="text-sm">
|
|
1017
|
-
<span className="font-medium text-red-400">Layout Error:</span>
|
|
1018
|
-
<span className="text-theme-text-secondary ml-1">
|
|
1019
|
-
Failed to update layout. Showing previous view.
|
|
1020
|
-
</span>
|
|
1021
|
-
<p className="mt-1 text-xs text-theme-text-tertiary font-mono">{layoutError}</p>
|
|
1022
|
-
</div>
|
|
1023
|
-
</div>
|
|
1024
|
-
</div>
|
|
1025
|
-
)}
|
|
1009
|
+
)}
|
|
1010
|
+
{children}
|
|
1011
|
+
</TopologyOverlayBar>
|
|
1026
1012
|
{/* Summary-mode pill — pod tier collapsed to per-workload/service counts */}
|
|
1027
1013
|
{topology?.summaryMode && (
|
|
1028
1014
|
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/30 rounded-full px-3 py-1 backdrop-blur-sm">
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
|
|
4
|
+
interface TopologyOverlayBarProps {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
/** Extra classes merged onto the bar container. */
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Top-anchored overlay bar for the topology canvas. Stacks children top-to-
|
|
12
|
+
* bottom (flex column, source order); wrap items in your own `flex` div to put
|
|
13
|
+
* them on one line. Everything stacks, so overlays never collide.
|
|
14
|
+
*
|
|
15
|
+
* `pointer-events-none` so the canvas stays pannable in the gaps — interactive
|
|
16
|
+
* children opt back in with `pointer-events-auto`. Children are `items-start`
|
|
17
|
+
* (content width); use `w-full` for a full-width row.
|
|
18
|
+
*/
|
|
19
|
+
export function TopologyOverlayBar({ children, className }: TopologyOverlayBarProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={clsx(
|
|
23
|
+
'pointer-events-none absolute inset-x-2 top-2 z-10 flex flex-col items-start gap-2',
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { Search, X, ChevronRight } from 'lucide-react'
|
|
3
4
|
import { Input } from '../ui/Input'
|
|
4
5
|
import { getTopologyIcon } from '../../utils/resource-icons'
|
|
@@ -24,12 +25,12 @@ interface TopologySearchProps {
|
|
|
24
25
|
* empty-state hint above. Defaults to "current view".
|
|
25
26
|
*/
|
|
26
27
|
viewModeLabel?: string
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* (
|
|
31
|
-
*/
|
|
32
|
-
|
|
28
|
+
/** Element the search overlay portals into — typically the topology pane. Set
|
|
29
|
+
* it when the trigger lives inside a `pointer-events-none` overlay bar: the
|
|
30
|
+
* overlay then renders OUTSIDE that bar (so its backdrop is clickable) and is
|
|
31
|
+
* `absolute inset-0` scoped to this container (dims only the pane). Omit to
|
|
32
|
+
* fall back to a viewport-level `fixed` overlay on document.body. */
|
|
33
|
+
overlayContainer?: HTMLElement | null
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Icon mapping for different resource kinds
|
|
@@ -71,7 +72,7 @@ function getKindColor(kind: string): string {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, viewModeLabel,
|
|
75
|
+
export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, viewModeLabel, overlayContainer }: TopologySearchProps) {
|
|
75
76
|
const [isOpen, setIsOpen] = useState(false)
|
|
76
77
|
const [query, setQuery] = useState('')
|
|
77
78
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
@@ -195,15 +196,20 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
195
196
|
setTimeout(() => inputRef.current?.focus(), 0)
|
|
196
197
|
}, [])
|
|
197
198
|
|
|
199
|
+
// Portal the overlay OUT of the (pointer-events-none) overlay bar the trigger
|
|
200
|
+
// sits in. With a container → `absolute inset-0` scoped to it (the pane);
|
|
201
|
+
// without → a viewport `fixed` overlay on document.body.
|
|
202
|
+
const portalTarget = overlayContainer ?? (typeof document !== 'undefined' ? document.body : null)
|
|
203
|
+
const overlayPositionClass = overlayContainer ? 'absolute' : 'fixed'
|
|
204
|
+
|
|
198
205
|
return (
|
|
199
206
|
<>
|
|
200
207
|
{/* Search trigger button */}
|
|
201
208
|
<button
|
|
202
209
|
onClick={handleOpen}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)}
|
|
210
|
+
// Layout-neutral (positioned by the host's overlay bar); pointer-events-
|
|
211
|
+
// auto inside the pointer-events-none bar.
|
|
212
|
+
className="pointer-events-auto flex items-center gap-2 px-3 py-2 bg-theme-surface/90 backdrop-blur border border-theme-border rounded-lg text-theme-text-secondary hover:text-theme-text-primary hover:border-theme-border-light transition-colors"
|
|
207
213
|
>
|
|
208
214
|
<Search className="w-4 h-4" />
|
|
209
215
|
<span className="text-sm">Search</span>
|
|
@@ -212,9 +218,10 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
212
218
|
</kbd>
|
|
213
219
|
</button>
|
|
214
220
|
|
|
215
|
-
{/*
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
{/* Portaled out of the overlay bar (see portalTarget above) so the
|
|
222
|
+
backdrop is clickable and the dim covers only the pane, not the app. */}
|
|
223
|
+
{isOpen && portalTarget && createPortal(
|
|
224
|
+
<div className={`${overlayPositionClass} inset-0 z-50 flex items-start justify-center pt-[10vh]`}>
|
|
218
225
|
{/* Backdrop */}
|
|
219
226
|
<div
|
|
220
227
|
className="absolute inset-0 bg-theme-base/60 backdrop-blur-sm"
|
|
@@ -339,7 +346,8 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
339
346
|
</div>
|
|
340
347
|
)}
|
|
341
348
|
</div>
|
|
342
|
-
</div
|
|
349
|
+
</div>,
|
|
350
|
+
portalTarget,
|
|
343
351
|
)}
|
|
344
352
|
</>
|
|
345
353
|
)
|
|
@@ -4,4 +4,6 @@ export { GroupNode } from './GroupNode'
|
|
|
4
4
|
export { TopologySearch } from './TopologySearch'
|
|
5
5
|
export { TopologyFilterSidebar } from './TopologyFilterSidebar'
|
|
6
6
|
export { TopologyControls } from './TopologyControls'
|
|
7
|
+
export { TopologyOverlayBar } from './TopologyOverlayBar'
|
|
8
|
+
export { TopologyBreadcrumb } from './TopologyBreadcrumb'
|
|
7
9
|
export { buildHierarchicalElkGraph, applyHierarchicalLayout, getGroupKey, setLayoutEngine } from './layout'
|
|
@@ -52,7 +52,7 @@ interface BadgeProps {
|
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
const SEVERITY: Record<BadgeSeverity, string> = {
|
|
54
54
|
success: 'bg-emerald-100 text-emerald-700 border-emerald-300 dark:bg-emerald-950/50 dark:text-emerald-400 dark:border-emerald-700/40',
|
|
55
|
-
warning: 'bg-amber-100 text-
|
|
55
|
+
warning: 'bg-amber-100 text-warning-text border-amber-300 dark:bg-amber-950/50 dark:border-amber-700/40',
|
|
56
56
|
alert: 'bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-950/50 dark:text-orange-400 dark:border-orange-700/40',
|
|
57
57
|
error: 'bg-red-100 text-red-700 border-red-300 dark:bg-red-950/50 dark:text-red-400 dark:border-red-700/40',
|
|
58
58
|
info: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40',
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Check, ChevronDown } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
|
|
5
|
+
export interface SelectMenuOption {
|
|
6
|
+
value: string
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SelectMenu({
|
|
11
|
+
value,
|
|
12
|
+
options,
|
|
13
|
+
onChange,
|
|
14
|
+
ariaLabel,
|
|
15
|
+
className,
|
|
16
|
+
}: {
|
|
17
|
+
value: string
|
|
18
|
+
options: SelectMenuOption[]
|
|
19
|
+
onChange: (value: string) => void
|
|
20
|
+
ariaLabel: string
|
|
21
|
+
className?: string
|
|
22
|
+
}) {
|
|
23
|
+
const [open, setOpen] = useState(false)
|
|
24
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
25
|
+
const selected = options.find((option) => option.value === value) ?? options[0]
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!open) return
|
|
29
|
+
const onPointerDown = (event: MouseEvent) => {
|
|
30
|
+
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
|
|
31
|
+
}
|
|
32
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
33
|
+
if (event.key === 'Escape') setOpen(false)
|
|
34
|
+
}
|
|
35
|
+
document.addEventListener('mousedown', onPointerDown)
|
|
36
|
+
document.addEventListener('keydown', onKeyDown)
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener('mousedown', onPointerDown)
|
|
39
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
40
|
+
}
|
|
41
|
+
}, [open])
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div ref={rootRef} className={clsx('relative', className)}>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
aria-label={ariaLabel}
|
|
48
|
+
aria-haspopup="listbox"
|
|
49
|
+
aria-expanded={open}
|
|
50
|
+
onClick={() => setOpen((current) => !current)}
|
|
51
|
+
className="flex h-8 w-full items-center justify-between gap-2 rounded-md border border-theme-border bg-theme-elevated px-2.5 text-xs text-theme-text-primary transition-colors hover:bg-theme-hover"
|
|
52
|
+
>
|
|
53
|
+
<span className="truncate">{selected?.label}</span>
|
|
54
|
+
<ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', open && 'rotate-180')} />
|
|
55
|
+
</button>
|
|
56
|
+
{open && (
|
|
57
|
+
<div role="listbox" className="absolute right-0 top-full z-50 mt-1 min-w-full overflow-hidden rounded-md border border-theme-border bg-theme-surface py-1 shadow-theme-lg">
|
|
58
|
+
{options.map((option) => {
|
|
59
|
+
const active = option.value === value
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={option.value}
|
|
63
|
+
type="button"
|
|
64
|
+
role="option"
|
|
65
|
+
aria-selected={active}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
onChange(option.value)
|
|
68
|
+
setOpen(false)
|
|
69
|
+
}}
|
|
70
|
+
className="flex w-full items-center gap-2 whitespace-nowrap px-2.5 py-1.5 text-left text-xs text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary"
|
|
71
|
+
>
|
|
72
|
+
<Check className={clsx('h-3.5 w-3.5 shrink-0 text-accent', !active && 'opacity-0')} />
|
|
73
|
+
{option.label}
|
|
74
|
+
</button>
|
|
75
|
+
)
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -410,7 +410,7 @@ export function AlertBanner({ variant, icon, title, message, items, children }:
|
|
|
410
410
|
{hasBody ? (
|
|
411
411
|
<div className="flex-1 min-w-0">
|
|
412
412
|
<div className={clsx('text-sm font-medium', colors.title, items && 'mb-1')}>{title}</div>
|
|
413
|
-
{message && <div className={clsx('text-xs mt-1 break-
|
|
413
|
+
{message && <div className={clsx('text-xs mt-1 break-words', colors.message)}>{message}</div>}
|
|
414
414
|
{items && items.length > 0 && (
|
|
415
415
|
<ul className={clsx('text-xs space-y-1', colors.list)}>
|
|
416
416
|
{items.map((item, i) => (
|
|
@@ -446,11 +446,14 @@ export interface Problem {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
/** Displays a list of problem alerts (warnings and errors) */
|
|
449
|
-
// True when the resource detail is
|
|
450
|
-
// "Operational Issues" section (the Issues pipeline — richer
|
|
451
|
-
//
|
|
452
|
-
// their own
|
|
453
|
-
//
|
|
449
|
+
// True when the resource detail is rendering (or still fetching) a dedicated,
|
|
450
|
+
// authoritative "Operational Issues" section (the Issues pipeline — richer
|
|
451
|
+
// cause/action). Renderers whose problems the pipeline COMPREHENSIVELY covers read
|
|
452
|
+
// this and drop their own condition banner so the same failure isn't shown twice.
|
|
453
|
+
// It stays true WHILE the live-issues fetch is pending: a banner the pipeline will
|
|
454
|
+
// cover would otherwise flash on first paint before the issues arrive and hide it;
|
|
455
|
+
// once the fetch settles with no issues (e.g. a CRD the pipeline doesn't watch),
|
|
456
|
+
// it flips false and the renderer's banner appears cleanly. It is deliberately
|
|
454
457
|
// NOT wired into ProblemAlerts: that component is used only by GitOps renderers
|
|
455
458
|
// (Argo/Flux), whose Degraded/OutOfSync/revision-mismatch banners the pipeline
|
|
456
459
|
// does not fully emit (e.g. OutOfSync only for automated Argo apps) — suppressing
|
|
@@ -10,6 +10,8 @@ export type { EmptyStateTone, EmptyStateVariant } from './EmptyState'
|
|
|
10
10
|
export { RestrictedState } from './RestrictedState'
|
|
11
11
|
export { FetchResult } from './FetchResult'
|
|
12
12
|
export { SearchBox } from './SearchBox'
|
|
13
|
+
export { SelectMenu } from './SelectMenu'
|
|
14
|
+
export type { SelectMenuOption } from './SelectMenu'
|
|
13
15
|
export { Input } from './Input'
|
|
14
16
|
export { FilterPill } from './FilterPill'
|
|
15
17
|
export type { FilterPillTone } from './FilterPill'
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
X,
|
|
30
30
|
BarChart3,
|
|
31
31
|
Network,
|
|
32
|
+
DollarSign,
|
|
32
33
|
} from 'lucide-react'
|
|
33
34
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
|
|
34
35
|
import type { GitOpsStatus } from '../../types/gitops'
|
|
@@ -64,7 +65,7 @@ import {
|
|
|
64
65
|
} from '../resources/resource-utils'
|
|
65
66
|
import { ServicePortCards, type ServicePortRenderProps } from '../resources/renderers/ServiceRenderer'
|
|
66
67
|
|
|
67
|
-
export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'yaml'
|
|
68
|
+
export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'cost' | 'yaml'
|
|
68
69
|
type TabType = WorkloadTabType
|
|
69
70
|
|
|
70
71
|
export interface ResourceOwnershipContext {
|
|
@@ -257,6 +258,8 @@ interface WorkloadViewProps {
|
|
|
257
258
|
}) => ReactNode
|
|
258
259
|
/** Render the metrics tab content */
|
|
259
260
|
renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
261
|
+
/** Render the cost tab content */
|
|
262
|
+
renderCostTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
260
263
|
/** Render a read-only YAML view for a related object from the workload's
|
|
261
264
|
* neighborhood. Providing this turns the YAML tab into an object explorer
|
|
262
265
|
* (rail of the workload + its Services/config/policies/pods); omitting it
|
|
@@ -265,6 +268,8 @@ interface WorkloadViewProps {
|
|
|
265
268
|
renderRelatedYaml?: (ref: { kind: string; namespace: string; name: string; group?: string }) => ReactNode
|
|
266
269
|
/** Whether metrics are available for this resource kind */
|
|
267
270
|
isMetricsAvailable?: (kind: string, resource: any) => boolean
|
|
271
|
+
/** Whether cost is available for this resource kind */
|
|
272
|
+
isCostAvailable?: (kind: string, resource: any) => boolean
|
|
268
273
|
/** Render extra content at the bottom of the overview tab (e.g. audit findings) */
|
|
269
274
|
renderOverviewExtra?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
270
275
|
/** Render lightweight overview intro content before the default renderer. */
|
|
@@ -280,6 +285,10 @@ interface WorkloadViewProps {
|
|
|
280
285
|
* live issues for this resource). Avoids showing the same failure twice.
|
|
281
286
|
* Also gates the `renderOverviewLead` wrapper (see above). */
|
|
282
287
|
hasOperationalIssues?: boolean
|
|
288
|
+
/** True while the live-issues fetch is still pending. Renderers suppress their
|
|
289
|
+
* banners during this window too — a banner whose failure the pipeline covers
|
|
290
|
+
* would otherwise flash on first paint before the issues arrive and hide it. */
|
|
291
|
+
operationalIssuesPending?: boolean
|
|
283
292
|
|
|
284
293
|
// ── Duplicate ────────────────────────────────────────────────────────────
|
|
285
294
|
/** Duplicate handler — opens create dialog with this resource's YAML */
|
|
@@ -359,7 +368,9 @@ export function WorkloadView({
|
|
|
359
368
|
renderExpandedOverview,
|
|
360
369
|
renderRelatedYaml,
|
|
361
370
|
renderMetricsTab,
|
|
371
|
+
renderCostTab,
|
|
362
372
|
isMetricsAvailable,
|
|
373
|
+
isCostAvailable,
|
|
363
374
|
// Duplicate
|
|
364
375
|
onDuplicate,
|
|
365
376
|
onDownload,
|
|
@@ -367,6 +378,7 @@ export function WorkloadView({
|
|
|
367
378
|
renderOverviewIntro,
|
|
368
379
|
renderOverviewLead,
|
|
369
380
|
hasOperationalIssues,
|
|
381
|
+
operationalIssuesPending,
|
|
370
382
|
// Actions bar
|
|
371
383
|
actionsBarProps,
|
|
372
384
|
// Renderer overrides
|
|
@@ -644,8 +656,10 @@ export function WorkloadView({
|
|
|
644
656
|
})
|
|
645
657
|
|
|
646
658
|
const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
|
|
659
|
+
const showCostTab = isCostAvailable ? isCostAvailable(kind, resource) : false
|
|
647
660
|
const logsTabVisible = Boolean(renderLogsTab) && (allPods.length > 0 || LOGS_TAB_WITHOUT_PODS_KINDS.has(kindToPlural(kind).toLowerCase()))
|
|
648
661
|
const metricsTabVisible = Boolean(showMetricsTab && renderMetricsTab)
|
|
662
|
+
const costTabVisible = Boolean(showCostTab && renderCostTab)
|
|
649
663
|
const podEvidenceLoading = resourceLoading || workloadPodsLoading || eventsLoading
|
|
650
664
|
const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
|
|
651
665
|
const requestedTab: TabType = activeTab
|
|
@@ -660,6 +674,7 @@ export function WorkloadView({
|
|
|
660
674
|
},
|
|
661
675
|
{ id: 'logs', label: 'Logs', icon: <Terminal className="w-4 h-4" />, hidden: !logsTabVisible },
|
|
662
676
|
{ id: 'metrics', label: 'Metrics', icon: <BarChart3 className="w-4 h-4" />, hidden: !metricsTabVisible },
|
|
677
|
+
{ id: 'cost', label: 'Cost', icon: <DollarSign className="w-4 h-4" />, hidden: !costTabVisible },
|
|
663
678
|
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
664
679
|
]
|
|
665
680
|
const requestedTabAvailable = tabs.some((tab) => tab.id === requestedTab && !tab.hidden)
|
|
@@ -670,6 +685,7 @@ export function WorkloadView({
|
|
|
670
685
|
(
|
|
671
686
|
(requestedTab === 'topology' && topologyTabHidden) ||
|
|
672
687
|
(requestedTab === 'metrics' && (!renderMetricsTab || (!!resource && !resourceLoading && !showMetricsTab))) ||
|
|
688
|
+
(requestedTab === 'cost' && (!renderCostTab || (!!resource && !resourceLoading && !showCostTab))) ||
|
|
673
689
|
(requestedTab === 'logs' && logsFallbackReady)
|
|
674
690
|
)
|
|
675
691
|
useEffect(() => {
|
|
@@ -794,7 +810,7 @@ export function WorkloadView({
|
|
|
794
810
|
onDownload={onDownload}
|
|
795
811
|
/>
|
|
796
812
|
) : (
|
|
797
|
-
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues}>
|
|
813
|
+
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
|
|
798
814
|
{renderOverviewLead && hasOperationalIssues && (
|
|
799
815
|
<div className="px-4 pt-4">
|
|
800
816
|
{renderOverviewLead({ kind, namespace, name })}
|
|
@@ -831,7 +847,7 @@ export function WorkloadView({
|
|
|
831
847
|
|
|
832
848
|
// ── Expanded (full) mode ─────────────────────────────────────────────────
|
|
833
849
|
return (
|
|
834
|
-
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues}>
|
|
850
|
+
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
|
|
835
851
|
<DetailShell
|
|
836
852
|
breadcrumb={breadcrumb}
|
|
837
853
|
nav={
|
|
@@ -1038,6 +1054,11 @@ export function WorkloadView({
|
|
|
1038
1054
|
{renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
|
|
1039
1055
|
</div>
|
|
1040
1056
|
)}
|
|
1057
|
+
{effectiveTab === 'cost' && renderCostTab && (
|
|
1058
|
+
<div className="h-full overflow-auto p-4">
|
|
1059
|
+
{renderCostTab({ kind: resource?.kind || kind, namespace, name })}
|
|
1060
|
+
</div>
|
|
1061
|
+
)}
|
|
1041
1062
|
{effectiveTab === 'yaml' && (
|
|
1042
1063
|
<div className="flex h-full min-h-0">
|
|
1043
1064
|
{renderRelatedYaml && yamlObjects.length > 1 && (
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
--color-theme-text-secondary: var(--text-secondary);
|
|
21
21
|
--color-theme-text-tertiary: var(--text-tertiary);
|
|
22
22
|
--color-theme-text-disabled: var(--text-disabled);
|
|
23
|
+
--color-warning-text: var(--warning-text);
|
|
23
24
|
|
|
24
25
|
/* Borders */
|
|
25
26
|
--color-theme-border: var(--border-default);
|
package/src/theme/variables.css
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
--text-secondary: light-dark(#555860, #D8DAE0);
|
|
50
50
|
--text-tertiary: light-dark(#6B7080, #B0B5C0);
|
|
51
51
|
--text-disabled: light-dark(#A0A4AC, #6A7080);
|
|
52
|
+
--warning-text: light-dark(#92400E, #FBBF24);
|
|
52
53
|
|
|
53
54
|
/* ── Borders (lightly blue-tinted in light mode) ── */
|
|
54
55
|
--border-default: light-dark(#D8E0EE, #222840);
|