@skyhook-io/k8s-ui 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -2
- package/src/components/dock/BottomDock.tsx +14 -15
- package/src/components/dock/DockContext.tsx +40 -1
- package/src/components/dock/LocalTerminalTab.tsx +241 -0
- package/src/components/dock/NodeTerminalTab.tsx +129 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
- package/src/components/logs/LogCore.tsx +30 -4
- package/src/components/logs/StructuredLogLine.tsx +168 -0
- package/src/components/logs/index.ts +1 -1
- package/src/components/logs/useLogBuffer.ts +16 -10
- package/src/components/resources/ResourcesView.tsx +186 -67
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
- package/src/components/resources/renderers/HPARenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.tsx +164 -8
- package/src/components/resources/renderers/VPARenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
- package/src/components/resources/renderers/contour-cells.tsx +48 -0
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/renderers/trivy-shared.tsx +9 -25
- package/src/components/resources/resource-utils-contour.ts +34 -0
- package/src/components/resources/resource-utils.ts +91 -0
- package/src/components/shared/ResourceActionsBar.tsx +276 -144
- package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
- package/src/components/shared/index.ts +1 -1
- package/src/components/timeline/shared.tsx +8 -10
- package/src/components/topology/K8sResourceNode.tsx +1 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
- package/src/components/topology/TopologyGraph.tsx +2 -17
- package/src/components/topology/layout.ts +1 -0
- package/src/components/topology/topology.css +1 -0
- package/src/components/ui/CodeViewer.tsx +1 -2
- package/src/components/ui/ConfirmDialog.tsx +85 -130
- package/src/components/ui/DialogPortal.tsx +82 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
- package/src/components/ui/Toast.tsx +5 -5
- package/src/components/ui/drawer-components.tsx +28 -22
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +30 -2
- package/src/theme/index.ts +3 -0
- package/src/theme/tailwind-theme.css +61 -0
- package/src/theme/variables.css +121 -0
- package/src/types/core.ts +13 -2
- package/src/utils/animation.ts +1 -1
- package/src/utils/badge-colors.ts +158 -123
- package/src/utils/log-format.ts +127 -35
- package/src/utils/navigation.test.ts +142 -0
- package/src/utils/navigation.ts +52 -47
- package/src/utils/resource-hierarchy.ts +5 -0
- package/src/utils/resource-icons.ts +3 -0
|
@@ -32,9 +32,9 @@ import type { NavigateToResource } from '../../utils/navigation'
|
|
|
32
32
|
import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
|
|
33
33
|
import {
|
|
34
34
|
getPodStatus,
|
|
35
|
-
getPodReadiness,
|
|
36
35
|
getPodRestarts,
|
|
37
36
|
getPodProblems,
|
|
37
|
+
getContainerSquareStates,
|
|
38
38
|
getWorkloadImages,
|
|
39
39
|
getWorkloadConditions,
|
|
40
40
|
getReplicaSetOwner,
|
|
@@ -114,6 +114,7 @@ import {
|
|
|
114
114
|
getServiceAccountSecretCount,
|
|
115
115
|
getRoleRuleCount,
|
|
116
116
|
formatAge,
|
|
117
|
+
formatDuration,
|
|
117
118
|
truncate,
|
|
118
119
|
getCellFilterValue,
|
|
119
120
|
parseColumnFilters,
|
|
@@ -136,6 +137,7 @@ import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCel
|
|
|
136
137
|
import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
|
|
137
138
|
import { KnativeServiceCell, ConfigurationCell as KnativeConfigurationCell, RevisionCell as KnativeRevisionCell, RouteCell as KnativeRouteCell, BrokerCell, TriggerCell, EventTypeCell, PingSourceCell, ApiServerSourceCell, ContainerSourceCell, SinkBindingCell, ChannelCell, InMemoryChannelCell, SubscriptionCell, SequenceCell, ParallelCell, DomainMappingCell, ServerlessServiceCell, KnativeIngressCell, KnativeCertificateCell } from './renderers/knative-cells'
|
|
138
139
|
import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
|
|
140
|
+
import { HTTPProxyCell } from './renderers/contour-cells'
|
|
139
141
|
import { useRegisterShortcut, useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
140
142
|
|
|
141
143
|
// Pod problem filter options (special multi-select, not a single column value)
|
|
@@ -242,7 +244,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
242
244
|
pods: [
|
|
243
245
|
{ key: 'name', label: 'Name' },
|
|
244
246
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
245
|
-
{ key: '
|
|
247
|
+
{ key: 'containers', label: 'Containers', width: 'w-28' },
|
|
246
248
|
{ key: 'status', label: 'Status', width: 'w-40' },
|
|
247
249
|
{ key: 'cpu', label: 'CPU', width: 'w-40', tooltip: 'CPU usage / limit (marker = request)' },
|
|
248
250
|
{ key: 'memory', label: 'Memory', width: 'w-40', tooltip: 'Memory usage / limit (marker = request)' },
|
|
@@ -1318,6 +1320,17 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1318
1320
|
{ key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
|
|
1319
1321
|
{ key: 'age', label: 'Age', width: 'w-16 shrink-0' },
|
|
1320
1322
|
],
|
|
1323
|
+
// Contour
|
|
1324
|
+
httpproxies: [
|
|
1325
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1326
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
|
|
1327
|
+
{ key: 'fqdn', label: 'FQDN', width: 'min-w-44' },
|
|
1328
|
+
{ key: 'routes', label: 'Routes', width: 'w-20 shrink-0' },
|
|
1329
|
+
{ key: 'includes', label: 'Includes', width: 'w-24 shrink-0' },
|
|
1330
|
+
{ key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
|
|
1331
|
+
{ key: 'status', label: 'Status', width: 'w-24 shrink-0' },
|
|
1332
|
+
{ key: 'age', label: 'Age', width: 'w-16 shrink-0' },
|
|
1333
|
+
],
|
|
1321
1334
|
}
|
|
1322
1335
|
|
|
1323
1336
|
// Map (plural, group) → KNOWN_COLUMNS key for kinds that collide with core K8s
|
|
@@ -1344,6 +1357,8 @@ function normalizeKindToPlural(kind: string, group?: string): string {
|
|
|
1344
1357
|
if (!lower.endsWith('s') && KNOWN_COLUMNS[lower + 's']) return lower + 's'
|
|
1345
1358
|
// Try 'es' for kinds ending in s/sh/ch/x/z (e.g., "ingress" → "ingresses")
|
|
1346
1359
|
if (KNOWN_COLUMNS[lower + 'es']) return lower + 'es'
|
|
1360
|
+
// Try 'ies' for kinds ending in y (e.g., "httpproxy" → "httpproxies")
|
|
1361
|
+
if (lower.endsWith('y') && KNOWN_COLUMNS[lower.slice(0, -1) + 'ies']) return lower.slice(0, -1) + 'ies'
|
|
1347
1362
|
return lower
|
|
1348
1363
|
}
|
|
1349
1364
|
|
|
@@ -1598,8 +1613,6 @@ export function ResourcesView({
|
|
|
1598
1613
|
// Pinned kinds (favorites) — provided via props
|
|
1599
1614
|
const [favoritesExpanded, setFavoritesExpanded] = useState(() => pinned.length > 0)
|
|
1600
1615
|
|
|
1601
|
-
console.debug('[filters] ResourcesView render:', { kind: selectedKind.name, columnFilters, searchTerm, url: location.search })
|
|
1602
|
-
|
|
1603
1616
|
useEffect(() => { persistedExpandedCategories = expandedCategories }, [expandedCategories])
|
|
1604
1617
|
// Track if this is the initial mount to avoid re-syncing on first render
|
|
1605
1618
|
const isInitialMount = useRef(true)
|
|
@@ -2198,6 +2211,14 @@ export function ResourcesView({
|
|
|
2198
2211
|
}
|
|
2199
2212
|
// Signal that initial resource param has been processed — URL update effect can now run
|
|
2200
2213
|
hasProcessedInitialResource.current = true
|
|
2214
|
+
|
|
2215
|
+
// If the URL has no kind segment (e.g., /resources), update to include the default kind
|
|
2216
|
+
const base = basePath.replace(/\/$/, '')
|
|
2217
|
+
const path = window.location.pathname
|
|
2218
|
+
if (path === base || path === base + '/') {
|
|
2219
|
+
const search = window.location.search
|
|
2220
|
+
window.history.replaceState({}, '', `${base}/${selectedKind.name}${search}`)
|
|
2221
|
+
}
|
|
2201
2222
|
}, []) // Only on mount
|
|
2202
2223
|
|
|
2203
2224
|
// API resources for dynamic sidebar — provided via props
|
|
@@ -2435,13 +2456,15 @@ export function ResourcesView({
|
|
|
2435
2456
|
return meta.creationTimestamp ? new Date(meta.creationTimestamp).getTime() : 0
|
|
2436
2457
|
case 'status':
|
|
2437
2458
|
return status.phase || ''
|
|
2438
|
-
case '
|
|
2439
|
-
//
|
|
2459
|
+
case 'containers':
|
|
2460
|
+
// Pod containers column — sort by readiness ratio
|
|
2440
2461
|
if (status.containerStatuses) {
|
|
2441
2462
|
const ready = status.containerStatuses.filter((c: any) => c.ready).length
|
|
2442
2463
|
const total = status.containerStatuses.length
|
|
2443
2464
|
return total > 0 ? ready / total : 0
|
|
2444
2465
|
}
|
|
2466
|
+
return 0
|
|
2467
|
+
case 'ready':
|
|
2445
2468
|
// For DaemonSets, use numberReady/desiredNumberScheduled
|
|
2446
2469
|
if (kindLower === 'daemonsets') {
|
|
2447
2470
|
const desired = status.desiredNumberScheduled ?? 0
|
|
@@ -2623,13 +2646,13 @@ export function ResourcesView({
|
|
|
2623
2646
|
const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
|
|
2624
2647
|
|
|
2625
2648
|
if (kindLower === 'pods') {
|
|
2626
|
-
// Completed pods at bottom
|
|
2649
|
+
// Completed pods at bottom, then sort by name for stability across refreshes
|
|
2627
2650
|
result = [...result].sort((a: any, b: any) => {
|
|
2628
2651
|
const aCompleted = a.status?.phase === 'Succeeded'
|
|
2629
2652
|
const bCompleted = b.status?.phase === 'Succeeded'
|
|
2630
2653
|
if (aCompleted && !bCompleted) return 1
|
|
2631
2654
|
if (!aCompleted && bCompleted) return -1
|
|
2632
|
-
return
|
|
2655
|
+
return (a.metadata?.name || '').localeCompare(b.metadata?.name || '')
|
|
2633
2656
|
})
|
|
2634
2657
|
} else if (kindLower === 'daemonsets') {
|
|
2635
2658
|
// DaemonSets with 0 desired (empty/inactive) at bottom, then sort by ready desc
|
|
@@ -2653,11 +2676,12 @@ export function ResourcesView({
|
|
|
2653
2676
|
return (a.metadata?.name || '').localeCompare(b.metadata?.name || '')
|
|
2654
2677
|
})
|
|
2655
2678
|
} else if (kindLower === 'events') {
|
|
2656
|
-
// Events: most recently seen first
|
|
2679
|
+
// Events: most recently seen first, name tiebreaker for same-timestamp stability
|
|
2657
2680
|
result = [...result].sort((a: any, b: any) => {
|
|
2658
2681
|
const aTime = new Date(a.lastTimestamp || a.metadata?.creationTimestamp || 0).getTime()
|
|
2659
2682
|
const bTime = new Date(b.lastTimestamp || b.metadata?.creationTimestamp || 0).getTime()
|
|
2660
|
-
return bTime - aTime
|
|
2683
|
+
if (bTime !== aTime) return bTime - aTime
|
|
2684
|
+
return (a.metadata?.name || '').localeCompare(b.metadata?.name || '')
|
|
2661
2685
|
})
|
|
2662
2686
|
} else if (['deployments', 'statefulsets', 'replicasets'].includes(kindLower)) {
|
|
2663
2687
|
// Workloads: unhealthy first, scaled-to-zero at bottom
|
|
@@ -2680,6 +2704,11 @@ export function ResourcesView({
|
|
|
2680
2704
|
// Finally sort by name
|
|
2681
2705
|
return (a.metadata?.name || '').localeCompare(b.metadata?.name || '')
|
|
2682
2706
|
})
|
|
2707
|
+
} else {
|
|
2708
|
+
// All other kinds: sort by name for stability across refreshes
|
|
2709
|
+
result = [...result].sort((a: any, b: any) =>
|
|
2710
|
+
(a.metadata?.name || '').localeCompare(b.metadata?.name || '')
|
|
2711
|
+
)
|
|
2683
2712
|
}
|
|
2684
2713
|
}
|
|
2685
2714
|
|
|
@@ -3041,18 +3070,16 @@ export function ResourcesView({
|
|
|
3041
3070
|
<ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
|
|
3042
3071
|
<div className="flex h-full w-full">
|
|
3043
3072
|
{/* Sidebar - Resource Types */}
|
|
3044
|
-
<div className="w-72 bg-theme-surface border-r border-theme-border overflow-y-auto overflow-x-hidden shrink-0">
|
|
3045
|
-
<div className="
|
|
3046
|
-
<
|
|
3047
|
-
Resources
|
|
3048
|
-
</h2>
|
|
3049
|
-
<div className="flex-1 relative">
|
|
3073
|
+
<div className="w-56 2xl:w-72 bg-theme-surface border-r border-theme-border overflow-y-auto overflow-x-hidden shrink-0">
|
|
3074
|
+
<div className="px-2 py-2 border-b border-theme-border">
|
|
3075
|
+
<div className="relative">
|
|
3050
3076
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
3051
3077
|
<input
|
|
3052
3078
|
type="text"
|
|
3053
|
-
placeholder="Filter
|
|
3079
|
+
placeholder="Filter resources..."
|
|
3054
3080
|
value={kindFilter}
|
|
3055
3081
|
onChange={(e) => setKindFilter(e.target.value)}
|
|
3082
|
+
onKeyDown={(e) => { if (e.key === 'Escape') { setKindFilter(''); (e.target as HTMLInputElement).blur() } }}
|
|
3056
3083
|
className="w-full pl-7 pr-7 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
3057
3084
|
/>
|
|
3058
3085
|
{kindFilter && (
|
|
@@ -3079,7 +3106,7 @@ export function ResourcesView({
|
|
|
3079
3106
|
)}
|
|
3080
3107
|
<span className="flex-1 text-left">Favorites</span>
|
|
3081
3108
|
{!favoritesExpanded && pinned.length > 0 && (
|
|
3082
|
-
<span className=
|
|
3109
|
+
<span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center', pinned.length < 1000 ? 'w-8' : 'w-9')}>
|
|
3083
3110
|
{pinned.length}
|
|
3084
3111
|
</span>
|
|
3085
3112
|
)}
|
|
@@ -3125,7 +3152,7 @@ export function ResourcesView({
|
|
|
3125
3152
|
<div key={category.name} className="mb-2">
|
|
3126
3153
|
<button
|
|
3127
3154
|
onClick={() => toggleCategory(category.name)}
|
|
3128
|
-
className="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-
|
|
3155
|
+
className="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-semibold text-theme-text-tertiary hover:text-theme-text-secondary uppercase tracking-wide"
|
|
3129
3156
|
>
|
|
3130
3157
|
{isExpanded ? (
|
|
3131
3158
|
<ChevronDown className="w-3 h-3" />
|
|
@@ -3134,7 +3161,7 @@ export function ResourcesView({
|
|
|
3134
3161
|
)}
|
|
3135
3162
|
<span className="flex-1 text-left">{category.name}</span>
|
|
3136
3163
|
{!isExpanded && (
|
|
3137
|
-
<span className=
|
|
3164
|
+
<span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center', category.total < 1000 ? 'w-8' : 'w-9')}>
|
|
3138
3165
|
{category.total}
|
|
3139
3166
|
</span>
|
|
3140
3167
|
)}
|
|
@@ -3781,7 +3808,7 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
|
|
|
3781
3808
|
ref={ref}
|
|
3782
3809
|
onClick={onClick}
|
|
3783
3810
|
className={clsx(
|
|
3784
|
-
'w-full flex items-center gap-
|
|
3811
|
+
'w-full flex items-center gap-2 px-2 xl:px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
|
|
3785
3812
|
isSelected
|
|
3786
3813
|
? 'bg-blue-500/20 text-blue-700 dark:text-blue-300'
|
|
3787
3814
|
: forbidden
|
|
@@ -3795,36 +3822,39 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
|
|
|
3795
3822
|
{resource.kind}
|
|
3796
3823
|
</span>
|
|
3797
3824
|
</Tooltip>
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
<
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3825
|
+
<div className="ml-auto flex items-center gap-1 shrink-0">
|
|
3826
|
+
{onTogglePin && (
|
|
3827
|
+
<span
|
|
3828
|
+
role="button"
|
|
3829
|
+
onClick={(e) => {
|
|
3830
|
+
e.stopPropagation()
|
|
3831
|
+
onTogglePin()
|
|
3832
|
+
}}
|
|
3833
|
+
className={clsx(
|
|
3834
|
+
'p-0.5 rounded transition-all hover:bg-theme-hover',
|
|
3835
|
+
isPinned
|
|
3836
|
+
? 'text-theme-text-secondary'
|
|
3837
|
+
: 'opacity-0 group-hover/kind:opacity-100 text-theme-text-disabled'
|
|
3838
|
+
)}
|
|
3839
|
+
title={isPinned ? 'Unpin from favorites' : 'Pin to favorites'}
|
|
3840
|
+
>
|
|
3841
|
+
<Pin className={clsx('w-3 h-3', isPinned && 'fill-current')} />
|
|
3842
|
+
</span>
|
|
3843
|
+
)}
|
|
3844
|
+
{forbidden ? (
|
|
3845
|
+
<Tooltip content="Insufficient permissions" position="left">
|
|
3846
|
+
<Shield className="w-3.5 h-3.5 text-amber-400/60" />
|
|
3847
|
+
</Tooltip>
|
|
3848
|
+
) : (
|
|
3849
|
+
<span className={clsx(
|
|
3850
|
+
'text-xs py-0.5 rounded text-center',
|
|
3851
|
+
isSelected ? 'bg-blue-500/30 text-blue-700 dark:text-blue-300' : 'bg-theme-elevated',
|
|
3852
|
+
count < 1000 ? 'w-8' : 'w-9'
|
|
3853
|
+
)}>
|
|
3854
|
+
{count}
|
|
3855
|
+
</span>
|
|
3856
|
+
)}
|
|
3857
|
+
</div>
|
|
3828
3858
|
</button>
|
|
3829
3859
|
)
|
|
3830
3860
|
}
|
|
@@ -4202,6 +4232,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion }
|
|
|
4202
4232
|
return <ServersTransportCell resource={resource} column={column} />
|
|
4203
4233
|
case 'tlsoptions':
|
|
4204
4234
|
return <TLSOptionCell resource={resource} column={column} />
|
|
4235
|
+
// Contour
|
|
4236
|
+
case 'httpproxies':
|
|
4237
|
+
return <HTTPProxyCell resource={resource} column={column} />
|
|
4205
4238
|
default:
|
|
4206
4239
|
// Generic cell for CRDs and unknown resources
|
|
4207
4240
|
return <GenericCell resource={resource} column={column} />
|
|
@@ -4270,27 +4303,113 @@ function GenericCell({ resource, column }: { resource: any; column: string }) {
|
|
|
4270
4303
|
// ============================================================================
|
|
4271
4304
|
|
|
4272
4305
|
function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
4273
|
-
const phase = resource.status?.phase
|
|
4274
|
-
const isCompleted = phase === 'Succeeded'
|
|
4275
4306
|
const metrics = useContext(MetricsContext)
|
|
4276
4307
|
const { onNavigate: navigate } = useContext(ResourcesViewDataContext)
|
|
4277
4308
|
|
|
4278
4309
|
switch (column) {
|
|
4279
|
-
case '
|
|
4280
|
-
const
|
|
4281
|
-
const
|
|
4282
|
-
// Completed pods (Succeeded) show neutral color, not red
|
|
4283
|
-
const color = isCompleted
|
|
4284
|
-
? 'text-theme-text-secondary'
|
|
4285
|
-
: allReady
|
|
4286
|
-
? 'text-green-400'
|
|
4287
|
-
: ready > 0
|
|
4288
|
-
? 'text-yellow-400'
|
|
4289
|
-
: 'text-red-400'
|
|
4310
|
+
case 'containers': {
|
|
4311
|
+
const squares = getContainerSquareStates(resource)
|
|
4312
|
+
const hasInit = squares.some(s => s.isInit)
|
|
4290
4313
|
return (
|
|
4291
|
-
<
|
|
4292
|
-
{
|
|
4293
|
-
|
|
4314
|
+
<div className="flex items-center gap-1">
|
|
4315
|
+
{squares.map((sq, i) => {
|
|
4316
|
+
const showSeparator = hasInit && i > 0 && sq.isInit !== squares[i - 1].isInit
|
|
4317
|
+
const bgClass =
|
|
4318
|
+
sq.status === 'ready' ? 'bg-green-500' :
|
|
4319
|
+
sq.status === 'completed' ? 'bg-theme-text-tertiary/30 border border-theme-text-tertiary' :
|
|
4320
|
+
sq.status === 'running' ? 'bg-yellow-500' :
|
|
4321
|
+
sq.status === 'waiting' ? 'bg-red-500' :
|
|
4322
|
+
sq.status === 'terminated' ? 'bg-red-500' :
|
|
4323
|
+
'bg-theme-text-tertiary/30 border border-dashed border-theme-text-tertiary'
|
|
4324
|
+
const ringClass = sq.restarts > 0 ? 'ring-2 ring-orange-400' : ''
|
|
4325
|
+
const dotColor =
|
|
4326
|
+
sq.status === 'ready' ? 'bg-green-500' :
|
|
4327
|
+
sq.status === 'completed' ? 'bg-theme-text-tertiary' :
|
|
4328
|
+
sq.status === 'running' ? 'bg-yellow-500' :
|
|
4329
|
+
sq.status === 'waiting' || sq.status === 'terminated' ? 'bg-red-500' :
|
|
4330
|
+
'bg-theme-text-tertiary'
|
|
4331
|
+
// Relative time helper
|
|
4332
|
+
const timeAgo = (dateStr?: string) => {
|
|
4333
|
+
if (!dateStr) return null
|
|
4334
|
+
const ms = Date.now() - new Date(dateStr).getTime()
|
|
4335
|
+
if (isNaN(ms) || ms < 0) return null
|
|
4336
|
+
if (ms < 60000) return 'just now'
|
|
4337
|
+
return formatDuration(ms) + ' ago'
|
|
4338
|
+
}
|
|
4339
|
+
const runDuration = (start?: string, end?: string) => {
|
|
4340
|
+
if (!start || !end) return null
|
|
4341
|
+
const ms = new Date(end).getTime() - new Date(start).getTime()
|
|
4342
|
+
return ms > 0 ? formatDuration(ms, true) : null
|
|
4343
|
+
}
|
|
4344
|
+
const stateLabel =
|
|
4345
|
+
sq.status === 'ready' ? 'Running' :
|
|
4346
|
+
sq.status === 'completed' ? 'Completed' :
|
|
4347
|
+
sq.status === 'running' ? 'Running (not ready)' :
|
|
4348
|
+
sq.status === 'waiting' ? 'Waiting' :
|
|
4349
|
+
sq.status === 'terminated' ? 'Terminated' : 'Unknown'
|
|
4350
|
+
const uptime = sq.status === 'ready' || sq.status === 'running' ? timeAgo(sq.startedAt) : null
|
|
4351
|
+
const duration = (sq.status === 'completed' || sq.status === 'terminated') ? runDuration(sq.startedAt, sq.finishedAt) : null
|
|
4352
|
+
const lt = sq.lastTermination
|
|
4353
|
+
const restartRecencyMs = lt?.finishedAt ? Date.now() - new Date(lt.finishedAt).getTime() : null
|
|
4354
|
+
const restartColor = restartRecencyMs !== null && restartRecencyMs < 600000 ? 'text-red-400' : restartRecencyMs !== null && restartRecencyMs < 3600000 ? 'text-yellow-400' : 'text-orange-400'
|
|
4355
|
+
const tooltipContent = (
|
|
4356
|
+
<div className="whitespace-normal space-y-1">
|
|
4357
|
+
{/* Header: dot + name + state */}
|
|
4358
|
+
<div className="flex items-center gap-1.5">
|
|
4359
|
+
<div className={clsx('w-2 h-2 rounded-full shrink-0', dotColor)} />
|
|
4360
|
+
<span className="font-medium">{sq.isInit ? <span className="text-theme-text-tertiary font-normal">init · </span> : ''}{sq.name}</span>
|
|
4361
|
+
<span className="text-theme-text-tertiary">·</span>
|
|
4362
|
+
<span className="text-theme-text-secondary">{stateLabel}</span>
|
|
4363
|
+
</div>
|
|
4364
|
+
{/* Reason (when different from state label) */}
|
|
4365
|
+
{sq.reason && sq.reason !== stateLabel && (
|
|
4366
|
+
<div className={clsx(
|
|
4367
|
+
'font-medium',
|
|
4368
|
+
(sq.status === 'waiting' || sq.status === 'terminated') ? 'text-red-400' : 'text-theme-text-secondary'
|
|
4369
|
+
)}>{sq.reason}</div>
|
|
4370
|
+
)}
|
|
4371
|
+
{/* Message — truncated for tooltip */}
|
|
4372
|
+
{sq.message && (
|
|
4373
|
+
<div className="text-theme-text-secondary text-[11px] leading-tight">
|
|
4374
|
+
{sq.message.length > 120 ? sq.message.slice(0, 120) + '...' : sq.message}
|
|
4375
|
+
</div>
|
|
4376
|
+
)}
|
|
4377
|
+
{/* Exit code + uptime/duration on same line */}
|
|
4378
|
+
{(sq.exitCode !== undefined && sq.status !== 'ready' && sq.status !== 'running') || uptime || duration ? (
|
|
4379
|
+
<div className="text-theme-text-tertiary flex items-center gap-1.5">
|
|
4380
|
+
{sq.exitCode !== undefined && sq.status !== 'ready' && sq.status !== 'running' && (
|
|
4381
|
+
<span className={sq.exitCode !== 0 ? 'text-red-400' : ''}>exit {sq.exitCode}</span>
|
|
4382
|
+
)}
|
|
4383
|
+
{uptime && <span>up {uptime.replace(' ago', '')}</span>}
|
|
4384
|
+
{duration && <span>ran {duration}</span>}
|
|
4385
|
+
</div>
|
|
4386
|
+
) : null}
|
|
4387
|
+
{/* Restarts + last crash info */}
|
|
4388
|
+
{sq.restarts > 0 && (
|
|
4389
|
+
<div className={clsx('border-t border-theme-border/50 pt-1 space-y-0.5', restartColor)}>
|
|
4390
|
+
<div className="flex items-center gap-1.5">
|
|
4391
|
+
<span>{sq.restarts} restart{sq.restarts !== 1 ? 's' : ''}</span>
|
|
4392
|
+
{lt?.finishedAt && <span className="text-theme-text-tertiary">· last {timeAgo(lt.finishedAt)}</span>}
|
|
4393
|
+
</div>
|
|
4394
|
+
{lt?.reason && (
|
|
4395
|
+
<div className="text-theme-text-tertiary">
|
|
4396
|
+
{lt.reason}{lt.exitCode !== undefined && lt.exitCode !== 0 ? ` (exit ${lt.exitCode})` : ''}
|
|
4397
|
+
</div>
|
|
4398
|
+
)}
|
|
4399
|
+
</div>
|
|
4400
|
+
)}
|
|
4401
|
+
</div>
|
|
4402
|
+
)
|
|
4403
|
+
return (
|
|
4404
|
+
<React.Fragment key={i}>
|
|
4405
|
+
{showSeparator && <div className="w-px h-3 bg-theme-text-tertiary/40 mx-0.5" />}
|
|
4406
|
+
<Tooltip content={tooltipContent} className="whitespace-normal max-w-xs">
|
|
4407
|
+
<div className={clsx('w-2.5 h-2.5 rounded-sm', bgClass, ringClass)} />
|
|
4408
|
+
</Tooltip>
|
|
4409
|
+
</React.Fragment>
|
|
4410
|
+
)
|
|
4411
|
+
})}
|
|
4412
|
+
</div>
|
|
4294
4413
|
)
|
|
4295
4414
|
}
|
|
4296
4415
|
case 'status': {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Globe, Route, Layers, Lock } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, AlertBanner, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import {
|
|
4
|
+
getHTTPProxyFQDN,
|
|
5
|
+
getHTTPProxyRouteCount,
|
|
6
|
+
getHTTPProxyServiceCount,
|
|
7
|
+
getHTTPProxyStatus,
|
|
8
|
+
hasHTTPProxyTLS,
|
|
9
|
+
} from '../resource-utils-contour'
|
|
10
|
+
|
|
11
|
+
interface ContourHTTPProxyRendererProps {
|
|
12
|
+
data: any
|
|
13
|
+
onNavigate?: (ref: { kind: string; name: string; namespace: string }) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ContourHTTPProxyRenderer({ data, onNavigate }: ContourHTTPProxyRendererProps) {
|
|
17
|
+
const spec = data.spec || {}
|
|
18
|
+
const status = data.status || {}
|
|
19
|
+
const routes = spec.routes || []
|
|
20
|
+
const includes = spec.includes || []
|
|
21
|
+
const tls = spec.virtualhost?.tls
|
|
22
|
+
const tcpproxy = spec.tcpproxy
|
|
23
|
+
const ns = data.metadata?.namespace || ''
|
|
24
|
+
const conditions = status.conditions
|
|
25
|
+
|
|
26
|
+
const { label: statusLabel } = getHTTPProxyStatus(data)
|
|
27
|
+
const currentStatus = status.currentStatus?.toLowerCase()
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{currentStatus === 'invalid' && (
|
|
32
|
+
<AlertBanner
|
|
33
|
+
variant="error"
|
|
34
|
+
title="Invalid HTTPProxy"
|
|
35
|
+
message={status.description || 'This HTTPProxy has an invalid configuration.'}
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
{currentStatus === 'orphaned' && (
|
|
40
|
+
<AlertBanner
|
|
41
|
+
variant="warning"
|
|
42
|
+
title="Orphaned HTTPProxy"
|
|
43
|
+
message={status.description || 'This HTTPProxy is orphaned — it is not part of any valid delegation chain.'}
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<Section title="HTTPProxy" icon={Globe} defaultExpanded>
|
|
48
|
+
<PropertyList>
|
|
49
|
+
<Property label="FQDN" value={getHTTPProxyFQDN(data)} />
|
|
50
|
+
<Property label="TLS" value={hasHTTPProxyTLS(data) ? 'Enabled' : 'None'} />
|
|
51
|
+
<Property label="Status" value={statusLabel} />
|
|
52
|
+
<Property label="Routes" value={`${getHTTPProxyRouteCount(data)}`} />
|
|
53
|
+
<Property label="Services" value={`${getHTTPProxyServiceCount(data)}`} />
|
|
54
|
+
</PropertyList>
|
|
55
|
+
</Section>
|
|
56
|
+
|
|
57
|
+
<Section title={`Routes (${routes.length})`} icon={Route} defaultExpanded>
|
|
58
|
+
<div className="space-y-3">
|
|
59
|
+
{routes.map((route: any, i: number) => {
|
|
60
|
+
const services = route.services || []
|
|
61
|
+
const routeConditions = route.conditions || []
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div key={i} className="bg-theme-elevated/30 rounded p-3">
|
|
65
|
+
{/* Conditions (prefix match, header match, etc.) */}
|
|
66
|
+
{routeConditions.length > 0 && (
|
|
67
|
+
<div className="flex items-start gap-2 mb-2">
|
|
68
|
+
<span className="text-sm font-medium text-theme-text-primary break-all">
|
|
69
|
+
{routeConditions.map((c: any) => {
|
|
70
|
+
if (c.prefix) return `prefix: ${c.prefix}`
|
|
71
|
+
if (c.header) return `header: ${c.header.name} ${c.header.contains || c.header.exact || c.header.present ? 'match' : ''}`
|
|
72
|
+
return JSON.stringify(c)
|
|
73
|
+
}).join(', ')}
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{routeConditions.length === 0 && (
|
|
79
|
+
<div className="flex items-start gap-2 mb-2">
|
|
80
|
+
<span className="text-sm font-medium text-theme-text-primary">
|
|
81
|
+
(no conditions)
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Services */}
|
|
87
|
+
{services.length > 0 && (
|
|
88
|
+
<div>
|
|
89
|
+
<div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-1">Services</div>
|
|
90
|
+
<div className="space-y-1">
|
|
91
|
+
{services.map((svc: any, si: number) => {
|
|
92
|
+
const svcNs = svc.namespace || ns
|
|
93
|
+
const port = svc.port ? `:${svc.port}` : ''
|
|
94
|
+
const weight = svc.weight !== undefined ? ` (${svc.weight}%)` : ''
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div key={si} className="flex items-center gap-2 text-xs">
|
|
98
|
+
<ResourceLink
|
|
99
|
+
name={svc.name}
|
|
100
|
+
kind="services"
|
|
101
|
+
namespace={svcNs}
|
|
102
|
+
label={<span className="text-blue-400">{svc.name}{port}{weight}</span>}
|
|
103
|
+
onNavigate={onNavigate}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
})}
|
|
114
|
+
|
|
115
|
+
{routes.length === 0 && (
|
|
116
|
+
<div className="text-sm text-theme-text-tertiary">No routes configured</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</Section>
|
|
120
|
+
|
|
121
|
+
{includes.length > 0 && (
|
|
122
|
+
<Section title={`Includes (${includes.length})`} icon={Layers} defaultExpanded>
|
|
123
|
+
<div className="space-y-2">
|
|
124
|
+
{includes.map((inc: any, i: number) => {
|
|
125
|
+
const incNs = inc.namespace || ns
|
|
126
|
+
const incConditions = inc.conditions || []
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
130
|
+
<ResourceLink
|
|
131
|
+
name={inc.name}
|
|
132
|
+
kind="httpproxies"
|
|
133
|
+
namespace={incNs}
|
|
134
|
+
label={
|
|
135
|
+
<span className="text-blue-400">
|
|
136
|
+
{incNs !== ns ? `${incNs}/` : ''}{inc.name}
|
|
137
|
+
</span>
|
|
138
|
+
}
|
|
139
|
+
onNavigate={onNavigate}
|
|
140
|
+
/>
|
|
141
|
+
{incConditions.length > 0 && (
|
|
142
|
+
<span className="text-theme-text-tertiary">
|
|
143
|
+
({incConditions.map((c: any) => c.prefix ? `prefix: ${c.prefix}` : JSON.stringify(c)).join(', ')})
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
</Section>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{tcpproxy && (
|
|
154
|
+
<Section title="TCP Proxy" icon={Route} defaultExpanded>
|
|
155
|
+
<div className="space-y-1">
|
|
156
|
+
{(tcpproxy.services || []).map((svc: any, i: number) => {
|
|
157
|
+
const port = svc.port ? `:${svc.port}` : ''
|
|
158
|
+
return (
|
|
159
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
160
|
+
<ResourceLink
|
|
161
|
+
name={svc.name}
|
|
162
|
+
kind="services"
|
|
163
|
+
namespace={svc.namespace || ns}
|
|
164
|
+
label={<span className="text-blue-400">{svc.name}{port}</span>}
|
|
165
|
+
onNavigate={onNavigate}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
</Section>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{tls && (
|
|
175
|
+
<Section title="TLS" icon={Lock} defaultExpanded>
|
|
176
|
+
<PropertyList>
|
|
177
|
+
{tls.secretName && (
|
|
178
|
+
<Property label="Secret" value={
|
|
179
|
+
<ResourceLink
|
|
180
|
+
name={tls.secretName}
|
|
181
|
+
kind="secrets"
|
|
182
|
+
namespace={ns}
|
|
183
|
+
onNavigate={onNavigate}
|
|
184
|
+
/>
|
|
185
|
+
} />
|
|
186
|
+
)}
|
|
187
|
+
{tls.minimumProtocolVersion && (
|
|
188
|
+
<Property label="Min Protocol" value={tls.minimumProtocolVersion} />
|
|
189
|
+
)}
|
|
190
|
+
{tls.passthrough !== undefined && (
|
|
191
|
+
<Property label="Passthrough" value={tls.passthrough ? 'Yes' : 'No'} />
|
|
192
|
+
)}
|
|
193
|
+
{!tls.secretName && !tls.passthrough && (
|
|
194
|
+
<Property label="Mode" value="TLS termination (no explicit secret)" />
|
|
195
|
+
)}
|
|
196
|
+
</PropertyList>
|
|
197
|
+
</Section>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{conditions && conditions.length > 0 && (
|
|
201
|
+
<Section title="Status" defaultExpanded>
|
|
202
|
+
<ConditionsSection conditions={conditions} />
|
|
203
|
+
</Section>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cpu, AlertTriangle } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
import { formatAge } from '../resource-utils'
|
|
5
6
|
|
|
6
7
|
interface HPARendererProps {
|
|
@@ -113,7 +114,7 @@ export function HPARenderer({ data, onNavigate }: HPARendererProps) {
|
|
|
113
114
|
spec.scaleTargetRef?.name ? (
|
|
114
115
|
<ResourceLink
|
|
115
116
|
name={spec.scaleTargetRef.name}
|
|
116
|
-
kind={(spec.scaleTargetRef.kind || 'Deployment')
|
|
117
|
+
kind={kindToPlural(spec.scaleTargetRef.kind || 'Deployment')}
|
|
117
118
|
namespace={data.metadata?.namespace || ''}
|
|
118
119
|
label={`${spec.scaleTargetRef.kind}/${spec.scaleTargetRef.name}`}
|
|
119
120
|
onNavigate={onNavigate}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Server, Cpu, Settings } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { kindToPlural } from '../../../utils/navigation'
|
|
4
5
|
import {
|
|
5
6
|
getNodeClaimStatus,
|
|
6
7
|
getNodeClaimInstanceType,
|
|
@@ -73,7 +74,7 @@ export function KarpenterNodeClaimRenderer({ data, onNavigate }: KarpenterNodeCl
|
|
|
73
74
|
value={nodeClassRef.name ? (
|
|
74
75
|
<ResourceLink
|
|
75
76
|
name={nodeClassRef.name}
|
|
76
|
-
kind={(nodeClassRef.kind || 'EC2NodeClass')
|
|
77
|
+
kind={kindToPlural(nodeClassRef.kind || 'EC2NodeClass')}
|
|
77
78
|
namespace=""
|
|
78
79
|
group={nodeClassRef.group}
|
|
79
80
|
label={`${nodeClassRef.kind || 'EC2NodeClass'}/${nodeClassRef.name}`}
|