@skyhook-io/k8s-ui 1.8.10 → 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 +7 -6
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +697 -0
- package/src/components/applications/ApplicationDetail.tsx +3269 -474
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +13 -6
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
- package/src/components/gitops/insights/insights-helpers.ts +74 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
- package/src/components/issues/IssuesView.tsx +91 -52
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
- package/src/components/issues/ResourceIssuesSection.tsx +46 -144
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +3 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- 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/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- 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/JobRenderer.tsx +3 -1
- 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/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +142 -111
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +375 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +149 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +25 -17
- package/src/components/topology/index.ts +2 -0
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/drawer-components.tsx +48 -32
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2706 -475
- package/src/components/workload/index.ts +1 -1
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +2 -1
- package/src/types/core.ts +57 -4
- 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/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +201 -1
- package/src/utils/applications.ts +1182 -393
- package/src/utils/index.ts +3 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +1024 -1
- package/src/utils/resource-hierarchy.ts +925 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
|
@@ -2,7 +2,7 @@ import { clsx } from 'clsx'
|
|
|
2
2
|
import { type ReactNode } from 'react'
|
|
3
3
|
import { Tooltip } from './Tooltip'
|
|
4
4
|
|
|
5
|
-
type ColorScheme = 'utilization' | 'count'
|
|
5
|
+
type ColorScheme = 'utilization' | 'count' | 'quiet'
|
|
6
6
|
|
|
7
7
|
interface ResourceBarProps {
|
|
8
8
|
/** Formatted usage string (e.g., "847m", "2.1Gi", "17") */
|
|
@@ -11,50 +11,93 @@ interface ResourceBarProps {
|
|
|
11
11
|
total: string
|
|
12
12
|
/** Usage percentage (0–100) */
|
|
13
13
|
percent: number
|
|
14
|
-
/** Color scheme:
|
|
14
|
+
/** Color scheme:
|
|
15
|
+
* - "utilization": green/yellow/red — node/workload detail surfaces
|
|
16
|
+
* - "count": blue/yellow
|
|
17
|
+
* - "quiet": dashboard-calibrated — brand fill, amber only at genuine
|
|
18
|
+
* scheduling-pressure levels (≥90%), never red. High utilization is a
|
|
19
|
+
* bin-packing goal, not an incident; alarming it trains amber-blindness. */
|
|
15
20
|
colorScheme?: ColorScheme
|
|
16
|
-
/** Optional marker line position (0–100), e.g.,
|
|
21
|
+
/** Optional marker line position (0–100), e.g., requests as % of allocatable.
|
|
22
|
+
* Under "quiet" the marker itself turns amber at ≥95% — a cluster whose
|
|
23
|
+
* requests are at capacity is full to the scheduler even when usage is low. */
|
|
17
24
|
markerPercent?: number
|
|
18
25
|
/** Optional tooltip content shown on hover */
|
|
19
26
|
tooltip?: ReactNode
|
|
27
|
+
/** Layout: "stacked" (default — numbers row above the track) or "inline"
|
|
28
|
+
* (one row: label · track · percent) for dense cards. Inline requires
|
|
29
|
+
* `label` and omits the used/total numbers row — put them in `tooltip`. */
|
|
30
|
+
layout?: 'stacked' | 'inline'
|
|
31
|
+
/** Short leading label for the inline layout (e.g. "CPU", "MEM"). */
|
|
32
|
+
label?: string
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
function getBarColor(percent: number, scheme: ColorScheme): string {
|
|
23
36
|
if (scheme === 'count') {
|
|
24
37
|
return percent > 90 ? 'bg-yellow-500' : 'bg-blue-500'
|
|
25
38
|
}
|
|
39
|
+
if (scheme === 'quiet') {
|
|
40
|
+
return percent >= 90 ? 'bg-amber-500' : 'bg-emerald-400'
|
|
41
|
+
}
|
|
26
42
|
if (percent > 85) return 'bg-red-500'
|
|
27
43
|
if (percent > 60) return 'bg-yellow-500'
|
|
28
44
|
return 'bg-green-500'
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
function getMarkerColor(markerPercent: number, scheme: ColorScheme): string {
|
|
48
|
+
if (scheme === 'quiet' && markerPercent >= 95) return 'bg-amber-500'
|
|
49
|
+
return 'bg-theme-text-primary/70'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ResourceBar({
|
|
53
|
+
used,
|
|
54
|
+
total,
|
|
55
|
+
percent,
|
|
56
|
+
colorScheme = 'utilization',
|
|
57
|
+
markerPercent,
|
|
58
|
+
tooltip,
|
|
59
|
+
layout = 'stacked',
|
|
60
|
+
label,
|
|
61
|
+
}: ResourceBarProps) {
|
|
62
|
+
const track = (
|
|
63
|
+
<div className={clsx('relative', layout === 'inline' && 'min-w-0 flex-1')}>
|
|
64
|
+
<div className={clsx('rounded-full border border-theme-border bg-theme-elevated overflow-hidden', layout === 'inline' ? 'h-1' : 'h-1.5')}>
|
|
65
|
+
<div
|
|
66
|
+
className={clsx('h-full rounded-full transition-[width] duration-300 ease-out', getBarColor(percent, colorScheme))}
|
|
67
|
+
style={{ width: `${Math.min(percent, 100)}%` }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
{markerPercent != null && markerPercent > 0 && markerPercent <= 100 && (
|
|
71
|
+
<div
|
|
72
|
+
className={clsx('absolute -top-[1px] h-[calc(100%+2px)] w-[2px]', getMarkerColor(markerPercent, colorScheme))}
|
|
73
|
+
style={{ left: `${markerPercent}%` }}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const bar =
|
|
80
|
+
layout === 'inline' ? (
|
|
81
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
82
|
+
<span className="w-7 shrink-0 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">{label}</span>
|
|
83
|
+
{track}
|
|
84
|
+
<span className="w-8 shrink-0 text-right text-[10.5px] font-mono tabular-nums text-theme-text-secondary">
|
|
39
85
|
{Math.round(percent)}%
|
|
40
86
|
</span>
|
|
41
87
|
</div>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
88
|
+
) : (
|
|
89
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
90
|
+
<div className="flex items-baseline justify-between gap-1">
|
|
91
|
+
<span className="text-xs font-mono text-theme-text-secondary truncate">
|
|
92
|
+
{used} / {total}
|
|
93
|
+
</span>
|
|
94
|
+
<span className="text-[10px] font-mono text-theme-text-tertiary shrink-0">
|
|
95
|
+
{Math.round(percent)}%
|
|
96
|
+
</span>
|
|
48
97
|
</div>
|
|
49
|
-
{
|
|
50
|
-
<div
|
|
51
|
-
className="absolute -top-[1px] h-[calc(100%+2px)] w-[2px] bg-theme-text-primary/70"
|
|
52
|
-
style={{ left: `${markerPercent}%` }}
|
|
53
|
-
/>
|
|
54
|
-
)}
|
|
98
|
+
{track}
|
|
55
99
|
</div>
|
|
56
|
-
|
|
57
|
-
)
|
|
100
|
+
)
|
|
58
101
|
|
|
59
102
|
if (tooltip) {
|
|
60
103
|
return (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useRef } from 'react'
|
|
1
|
+
import { useRef, type RefObject } from 'react'
|
|
2
2
|
import { Search, X } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
+
import { Input } from './Input'
|
|
4
5
|
import { useRegisterShortcut, type ShortcutScope } from '../../hooks/useKeyboardShortcuts'
|
|
5
6
|
|
|
6
7
|
/** The standard list-view search box: themed input with a `/`-to-focus
|
|
@@ -17,6 +18,9 @@ export function SearchBox({
|
|
|
17
18
|
className,
|
|
18
19
|
onEnter,
|
|
19
20
|
onArrowDown,
|
|
21
|
+
inputRef: externalRef,
|
|
22
|
+
shortcutEnabled = true,
|
|
23
|
+
onBlur,
|
|
20
24
|
}: {
|
|
21
25
|
value: string
|
|
22
26
|
onChange: (value: string) => void
|
|
@@ -31,8 +35,17 @@ export function SearchBox({
|
|
|
31
35
|
onEnter?: () => void
|
|
32
36
|
/** ArrowDown in the box — hand focus off to list keyboard navigation. */
|
|
33
37
|
onArrowDown?: () => void
|
|
38
|
+
/** Externally-owned input ref — lets a collapsible wrapper focus the box
|
|
39
|
+
* after it mounts. Falls back to an internal ref when omitted. */
|
|
40
|
+
inputRef?: RefObject<HTMLInputElement | null>
|
|
41
|
+
/** Set false when a wrapper owns the `/` shortcut, so it isn't registered
|
|
42
|
+
* twice under the same id. */
|
|
43
|
+
shortcutEnabled?: boolean
|
|
44
|
+
/** Blur handler — a collapsible wrapper uses it to fold back when empty. */
|
|
45
|
+
onBlur?: () => void
|
|
34
46
|
}) {
|
|
35
|
-
const
|
|
47
|
+
const internalRef = useRef<HTMLInputElement>(null)
|
|
48
|
+
const inputRef = externalRef ?? internalRef
|
|
36
49
|
|
|
37
50
|
useRegisterShortcut({
|
|
38
51
|
id: shortcutId,
|
|
@@ -40,18 +53,19 @@ export function SearchBox({
|
|
|
40
53
|
description: 'Focus search',
|
|
41
54
|
category: 'Search',
|
|
42
55
|
scope,
|
|
56
|
+
enabled: shortcutEnabled,
|
|
43
57
|
handler: () => inputRef.current?.focus(),
|
|
44
58
|
})
|
|
45
59
|
|
|
46
60
|
return (
|
|
47
61
|
<div className={clsx('relative', className)}>
|
|
48
62
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-tertiary" />
|
|
49
|
-
<
|
|
63
|
+
<Input
|
|
50
64
|
ref={inputRef}
|
|
51
|
-
type="text"
|
|
52
65
|
value={value}
|
|
53
66
|
placeholder={placeholder}
|
|
54
67
|
onChange={(e) => onChange(e.target.value)}
|
|
68
|
+
onBlur={onBlur}
|
|
55
69
|
onKeyDown={(e) => {
|
|
56
70
|
if (e.key === 'Escape') {
|
|
57
71
|
inputRef.current?.blur()
|
|
@@ -2,6 +2,7 @@ import { useRef, useState, useMemo, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { X } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
|
+
import { Input } from './Input'
|
|
5
6
|
|
|
6
7
|
// A structured search input: free-text plus removable `key:value` modifier
|
|
7
8
|
// PILLS (ns:, kind:, label:, …). Typing a complete modifier token and pressing
|
|
@@ -236,9 +237,8 @@ export function SearchPillInput({
|
|
|
236
237
|
less
|
|
237
238
|
</button>
|
|
238
239
|
)}
|
|
239
|
-
<
|
|
240
|
+
<Input
|
|
240
241
|
ref={inputRef}
|
|
241
|
-
type="text"
|
|
242
242
|
value={text}
|
|
243
243
|
onChange={(e) => { setDismissed(false); onChange({ text: e.target.value, pills }) }}
|
|
244
244
|
onKeyDown={handleKeyDown}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createContext, useContext, useState } from 'react'
|
|
2
2
|
import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { formatAge, formatDuration, formatResources } from '../resources/resource-utils'
|
|
@@ -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
|
|
@@ -532,9 +535,11 @@ function ContainerResourcesLine({ resources }: { resources: any }) {
|
|
|
532
535
|
const effective = getEffectiveResources(resources)
|
|
533
536
|
if (Object.keys(effective).length === 0) return null
|
|
534
537
|
return (
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
+
<Tooltip content="Effective requests (requests, falling back to limits)" delay={150}>
|
|
539
|
+
<div className="text-xs text-theme-text-tertiary mt-1">
|
|
540
|
+
Resources: {formatResources(effective)}
|
|
541
|
+
</div>
|
|
542
|
+
</Tooltip>
|
|
538
543
|
)
|
|
539
544
|
}
|
|
540
545
|
|
|
@@ -551,11 +556,15 @@ export function PodTemplateSection({ template }: { template: any }) {
|
|
|
551
556
|
{initContainers.map((c: any) => (
|
|
552
557
|
<div key={c.name} className="card-inner text-sm border-l-2 border-yellow-500/40">
|
|
553
558
|
<div className="font-medium text-theme-text-primary">{c.name}</div>
|
|
554
|
-
<
|
|
559
|
+
<Tooltip content={c.image} delay={300} wrapperClassName="block min-w-0">
|
|
560
|
+
<div className="text-xs text-theme-text-secondary truncate">{c.image}</div>
|
|
561
|
+
</Tooltip>
|
|
555
562
|
{(c.command || c.args) && (
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
|
|
563
|
+
<Tooltip content={[...(c.command || []), ...(c.args || [])].join(' ')} delay={300} wrapperClassName="block min-w-0">
|
|
564
|
+
<div className="text-xs text-theme-text-tertiary font-mono mt-1 truncate">
|
|
565
|
+
$ {[...(c.command || []), ...(c.args || [])].join(' ')}
|
|
566
|
+
</div>
|
|
567
|
+
</Tooltip>
|
|
559
568
|
)}
|
|
560
569
|
<ContainerResourcesLine resources={c.resources} />
|
|
561
570
|
</div>
|
|
@@ -566,7 +575,9 @@ export function PodTemplateSection({ template }: { template: any }) {
|
|
|
566
575
|
{containers.map((c: any) => (
|
|
567
576
|
<div key={c.name} className="card-inner text-sm">
|
|
568
577
|
<div className="font-medium text-theme-text-primary">{c.name}</div>
|
|
569
|
-
<
|
|
578
|
+
<Tooltip content={c.image} delay={300} wrapperClassName="block min-w-0">
|
|
579
|
+
<div className="text-xs text-theme-text-secondary truncate">{c.image}</div>
|
|
580
|
+
</Tooltip>
|
|
570
581
|
{c.ports && (
|
|
571
582
|
<div className="text-xs text-theme-text-tertiary mt-1">
|
|
572
583
|
Ports: {c.ports.map((p: any) => `${p.name ? `${p.name}: ` : ''}${p.containerPort}/${p.protocol || 'TCP'}`).join(', ')}
|
|
@@ -786,6 +797,7 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
786
797
|
(relationships.configRefs && relationships.configRefs.length > 0) ||
|
|
787
798
|
(relationships.consumers && relationships.consumers.length > 0) ||
|
|
788
799
|
(relationships.scalers && relationships.scalers.length > 0) ||
|
|
800
|
+
(relationships.storageRefs && relationships.storageRefs.length > 0) ||
|
|
789
801
|
(relationships.pdbs && relationships.pdbs.length > 0) ||
|
|
790
802
|
(relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
|
|
791
803
|
(relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
|
|
@@ -829,6 +841,9 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
829
841
|
{relationships.scalers && relationships.scalers.length > 0 && (
|
|
830
842
|
<RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
|
|
831
843
|
)}
|
|
844
|
+
{relationships.storageRefs && relationships.storageRefs.length > 0 && (
|
|
845
|
+
<RelationshipGroup label="Storage" refs={dedupeRefs(relationships.storageRefs)} onNavigate={onNavigate} />
|
|
846
|
+
)}
|
|
832
847
|
{relationships.pdbs && relationships.pdbs.length > 0 && (
|
|
833
848
|
<RelationshipGroup label="Disruption Budget" refs={dedupeRefs(relationships.pdbs)} onNavigate={onNavigate} />
|
|
834
849
|
)}
|
|
@@ -905,30 +920,31 @@ export function ResourceRefBadge({ resourceRef, onClick, wrapAtSeparator }: Reso
|
|
|
905
920
|
const layoutClass = wrapAtSeparator
|
|
906
921
|
? 'badge max-w-full min-w-0 flex-wrap items-center whitespace-normal text-left leading-tight'
|
|
907
922
|
: 'badge'
|
|
923
|
+
const tooltip = `${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`
|
|
908
924
|
|
|
909
925
|
if (onClick) {
|
|
910
926
|
return (
|
|
911
|
-
<
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
927
|
+
<Tooltip content={tooltip} delay={150}>
|
|
928
|
+
<button
|
|
929
|
+
onClick={() => onClick(resourceRef)}
|
|
930
|
+
className={clsx(
|
|
931
|
+
layoutClass,
|
|
932
|
+
'hover:brightness-[0.92] dark:hover:brightness-125 transition-[filter]',
|
|
933
|
+
kindClass
|
|
934
|
+
)}
|
|
935
|
+
>
|
|
936
|
+
{content}
|
|
937
|
+
</button>
|
|
938
|
+
</Tooltip>
|
|
922
939
|
)
|
|
923
940
|
}
|
|
924
941
|
|
|
925
942
|
return (
|
|
926
|
-
<
|
|
927
|
-
className={clsx(layoutClass, kindClass)}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
</span>
|
|
943
|
+
<Tooltip content={tooltip} delay={300}>
|
|
944
|
+
<span className={clsx(layoutClass, kindClass)}>
|
|
945
|
+
{content}
|
|
946
|
+
</span>
|
|
947
|
+
</Tooltip>
|
|
932
948
|
)
|
|
933
949
|
}
|
|
934
950
|
|
|
@@ -10,6 +10,9 @@ 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'
|
|
15
|
+
export { Input } from './Input'
|
|
13
16
|
export { FilterPill } from './FilterPill'
|
|
14
17
|
export type { FilterPillTone } from './FilterPill'
|
|
15
18
|
export { StatusDot, mapHealthToTone } from './status-tone'
|
|
@@ -19,6 +22,7 @@ export { ConfirmDialog } from './ConfirmDialog'
|
|
|
19
22
|
export { HealthRing } from './HealthRing'
|
|
20
23
|
export { MetricsChart, MetricsSparkline } from './MetricsChart'
|
|
21
24
|
export * from './drawer-components'
|
|
25
|
+
export { Collapse, CollapseChevron } from './Collapse'
|
|
22
26
|
export { ResourceBar } from './ResourceBar'
|
|
23
27
|
export { ForceDeleteConfirmDialog } from './ForceDeleteConfirmDialog'
|
|
24
28
|
export { ToastProvider, useToast, showApiError, showApiSuccess } from './Toast'
|
|
@@ -40,3 +44,5 @@ export { SearchPillInput } from './SearchPillInput'
|
|
|
40
44
|
export type { SearchModifier, SearchPillInputProps } from './SearchPillInput'
|
|
41
45
|
export { CardSection, CardBody, TerminalBlock, renderProse, NEUTRAL_CHIP_CLASS, KIND_CHIP_CLASS } from './CardSection'
|
|
42
46
|
export type { CardSectionTone } from './CardSection'
|
|
47
|
+
export { MultiSelectPicker } from './MultiSelectPicker'
|
|
48
|
+
export type { MultiSelectPickerProps } from './MultiSelectPicker'
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
// Literal class strings (no template interpolation) so Tailwind's scanner sees
|
|
8
8
|
// every utility.
|
|
9
9
|
|
|
10
|
-
export type SeverityTone = 'red' | 'orange' | 'amber' | 'slate'
|
|
10
|
+
export type SeverityTone = 'red' | 'orange' | 'amber' | 'yellow' | 'slate'
|
|
11
11
|
|
|
12
|
-
// Text color — leading severity glyph + emphasis text.
|
|
12
|
+
// Text color — leading severity glyph + emphasis text. Yellow takes the darker
|
|
13
|
+
// -700 step in light mode: its low luminance is illegible at -600 on a light
|
|
14
|
+
// surface, where red/orange/amber still clear.
|
|
13
15
|
export const TONE_TEXT_CLASS: Record<SeverityTone, string> = {
|
|
14
16
|
red: 'text-red-600 dark:text-red-400',
|
|
15
17
|
orange: 'text-orange-600 dark:text-orange-400',
|
|
16
18
|
amber: 'text-amber-600 dark:text-amber-400',
|
|
19
|
+
yellow: 'text-yellow-700 dark:text-yellow-400',
|
|
17
20
|
slate: 'text-slate-500 dark:text-slate-400',
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -22,6 +25,7 @@ export const TONE_FILL_CLASS: Record<SeverityTone, string> = {
|
|
|
22
25
|
red: 'bg-red-500',
|
|
23
26
|
orange: 'bg-orange-500',
|
|
24
27
|
amber: 'bg-amber-500',
|
|
28
|
+
yellow: 'bg-yellow-500',
|
|
25
29
|
slate: 'bg-slate-400',
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -31,17 +35,19 @@ export const TONE_RAIL_CLASS: Record<SeverityTone, string> = {
|
|
|
31
35
|
red: 'border-l-red-500 hover:bg-red-50/40 dark:hover:bg-red-950/20',
|
|
32
36
|
orange: 'border-l-orange-500 hover:bg-orange-50/40 dark:hover:bg-orange-950/20',
|
|
33
37
|
amber: 'border-l-amber-500 hover:bg-amber-50/30 dark:hover:bg-amber-950/15',
|
|
38
|
+
yellow: 'border-l-yellow-500 hover:bg-yellow-50/30 dark:hover:bg-yellow-950/15',
|
|
34
39
|
slate: 'border-l-slate-300 dark:border-l-slate-600 hover:bg-theme-hover/40',
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
// Solid severity pill — the loud focus signal on an expanded card's header band
|
|
38
43
|
// (collapsed cards keep the soft BADGE pill so a long queue stays calm).
|
|
39
|
-
// The warm mid-tones (orange/amber) take dark text: white on
|
|
40
|
-
//
|
|
44
|
+
// The warm mid-tones (orange/amber/yellow) take dark text: white on them sits
|
|
45
|
+
// below WCAG for the 12px pill text.
|
|
41
46
|
export const TONE_SOLID_CLASS: Record<SeverityTone, string> = {
|
|
42
47
|
red: 'bg-red-600 text-white',
|
|
43
48
|
orange: 'bg-orange-500 text-orange-950',
|
|
44
49
|
amber: 'bg-amber-400 text-amber-950',
|
|
50
|
+
yellow: 'bg-yellow-400 text-yellow-950',
|
|
45
51
|
slate: 'bg-slate-500 text-white dark:bg-slate-600',
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -52,5 +58,6 @@ export const TONE_HEADER_BAND_CLASS: Record<SeverityTone, string> = {
|
|
|
52
58
|
red: 'border-l-red-500 bg-red-50 dark:bg-red-950/40',
|
|
53
59
|
orange: 'border-l-orange-500 bg-orange-50 dark:bg-orange-950/30',
|
|
54
60
|
amber: 'border-l-amber-500 bg-amber-50 dark:bg-amber-950/30',
|
|
61
|
+
yellow: 'border-l-yellow-500 bg-yellow-50 dark:bg-yellow-950/30',
|
|
55
62
|
slate: 'border-l-slate-300 dark:border-l-slate-600 bg-slate-50 dark:bg-slate-900/40',
|
|
56
63
|
}
|
|
@@ -32,6 +32,9 @@ interface ResourceDetailDrawerProps {
|
|
|
32
32
|
headerHeight?: number
|
|
33
33
|
/** Left offset to exclude (e.g. sidebar width) so expanded mode doesn't cover the sidebar (default: 0) */
|
|
34
34
|
leftOffset?: number
|
|
35
|
+
/** Right inset in px to leave clear (e.g. a docked side panel) so the drawer sits
|
|
36
|
+
* beside it instead of under it. Shared "right-side surface" contract (default: 0). */
|
|
37
|
+
rightInset?: number
|
|
35
38
|
/** Render the content inside the drawer */
|
|
36
39
|
children: (props: {
|
|
37
40
|
resource: SelectedResource
|
|
@@ -81,7 +84,7 @@ function usePrefersReducedMotion(): boolean {
|
|
|
81
84
|
return reduced
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab, isOpen = true, expanded, onCollapse, onExpand, canCollapseToDrawer = true, onNavigateToResource, headerHeight: headerHeightProp, leftOffset = 0, children }: ResourceDetailDrawerProps) {
|
|
87
|
+
export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab, isOpen = true, expanded, onCollapse, onExpand, canCollapseToDrawer = true, onNavigateToResource, headerHeight: headerHeightProp, leftOffset = 0, rightInset = 0, children }: ResourceDetailDrawerProps) {
|
|
85
88
|
const [drawerWidth, setDrawerWidth] = useState(() => getDefaultWidth(resource.kind))
|
|
86
89
|
const [isResizing, setIsResizing] = useState(false)
|
|
87
90
|
const resizeStartX = useRef(0)
|
|
@@ -225,7 +228,7 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
|
|
|
225
228
|
// During the pre-mount frames (transitioning but not yet armed) hold the frame
|
|
226
229
|
// at its START width so the width transition fires only once the incoming
|
|
227
230
|
// layer has painted; otherwise it snaps to the target.
|
|
228
|
-
const fullW = `calc(100% - ${leftOffset}px)`
|
|
231
|
+
const fullW = `calc(100% - ${leftOffset}px - ${rightInset}px)`
|
|
229
232
|
const startWidth = settledExpanded.current ? fullW : drawerWidth
|
|
230
233
|
const targetWidth = expanded ? fullW : drawerWidth
|
|
231
234
|
const containerWidth = transitioning && !crossfadeArmed ? startWidth : targetWidth
|
|
@@ -274,6 +277,7 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
|
|
|
274
277
|
style={{
|
|
275
278
|
width: containerWidth,
|
|
276
279
|
top: headerHeight,
|
|
280
|
+
right: rightInset, // leave room for a docked side panel (AI), else flush right
|
|
277
281
|
height: `calc(100% - ${headerHeight}px - ${dockInset}px)`,
|
|
278
282
|
// Inline so the morph duration/easing is controlled precisely (and stays
|
|
279
283
|
// in lockstep with the crossfade + JS window). Width only animates when
|