@skyhook-io/k8s-ui 1.0.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 +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { formatAge } from '../resources/resource-utils'
|
|
5
|
+
import { Tooltip } from './Tooltip'
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// UI COMPONENTS
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export function KeyValueBadge({ k, v }: { k: string; v: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<span className="px-2 py-0.5 bg-theme-elevated rounded text-xs text-theme-text-secondary">
|
|
14
|
+
{k}={v}
|
|
15
|
+
</span>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function KeyValueBadgeList({ items }: { items: Record<string, unknown> | undefined | null }) {
|
|
20
|
+
if (!items || Object.keys(items).length === 0) return null
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-wrap gap-1">
|
|
23
|
+
{Object.entries(items).map(([k, v]) => (
|
|
24
|
+
<KeyValueBadge key={k} k={k} v={String(v)} />
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SectionProps {
|
|
31
|
+
title: string
|
|
32
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
33
|
+
children: React.ReactNode
|
|
34
|
+
defaultExpanded?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Section({ title, icon: Icon, children, defaultExpanded = true }: SectionProps) {
|
|
38
|
+
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="border-b-subtle pb-4 last:border-0">
|
|
42
|
+
<button
|
|
43
|
+
onClick={() => setExpanded(!expanded)}
|
|
44
|
+
className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
|
|
45
|
+
>
|
|
46
|
+
<ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform duration-200', expanded && 'rotate-90')} />
|
|
47
|
+
{Icon && <Icon className="w-4 h-4 text-theme-text-secondary" />}
|
|
48
|
+
<span className="text-sm font-medium text-theme-text-secondary">{title}</span>
|
|
49
|
+
</button>
|
|
50
|
+
<div
|
|
51
|
+
className="grid transition-[grid-template-rows] duration-200 ease-out"
|
|
52
|
+
style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
|
|
53
|
+
>
|
|
54
|
+
<div className="overflow-hidden">
|
|
55
|
+
<div className="pl-6">{children}</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ExpandableSectionProps {
|
|
63
|
+
title: string
|
|
64
|
+
children: React.ReactNode
|
|
65
|
+
defaultExpanded?: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ExpandableSection({ title, children, defaultExpanded = true }: ExpandableSectionProps) {
|
|
69
|
+
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => setExpanded(!expanded)}
|
|
75
|
+
className="flex items-center gap-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-1"
|
|
76
|
+
>
|
|
77
|
+
<ChevronRight className={clsx('w-3.5 h-3.5 transition-transform duration-200', expanded && 'rotate-90')} />
|
|
78
|
+
{title}
|
|
79
|
+
</button>
|
|
80
|
+
<div
|
|
81
|
+
className="grid transition-[grid-template-rows] duration-200 ease-out"
|
|
82
|
+
style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
|
|
83
|
+
>
|
|
84
|
+
<div className="overflow-hidden">
|
|
85
|
+
<div className="ml-5">{children}</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function PropertyList({ children }: { children: React.ReactNode }) {
|
|
93
|
+
return <div className="space-y-2">{children}</div>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface PropertyProps {
|
|
97
|
+
label: React.ReactNode
|
|
98
|
+
value: React.ReactNode
|
|
99
|
+
copyable?: boolean
|
|
100
|
+
onCopy?: (text: string, key: string) => void
|
|
101
|
+
copied?: string | null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Helper to check if value is a React element
|
|
105
|
+
function isReactElement(value: unknown): value is React.ReactElement {
|
|
106
|
+
return value !== null && typeof value === 'object' && '$$typeof' in (value as object)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function Property({ label, value, copyable, onCopy, copied }: PropertyProps) {
|
|
110
|
+
if (value === undefined || value === null || value === '') return null
|
|
111
|
+
const labelKey = typeof label === 'string' ? label : 'value'
|
|
112
|
+
|
|
113
|
+
// If value is a React element, render it directly; otherwise convert to string
|
|
114
|
+
const displayValue = isReactElement(value) ? value : String(value)
|
|
115
|
+
const strValue = isReactElement(value) ? '' : String(value)
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex items-start gap-2 text-sm">
|
|
119
|
+
<span className="text-theme-text-tertiary w-28 shrink-0">{label}</span>
|
|
120
|
+
<span className="text-theme-text-primary break-all flex-1">{displayValue}</span>
|
|
121
|
+
{copyable && onCopy && !isReactElement(value) && (
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => onCopy(strValue, labelKey)}
|
|
124
|
+
className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary shrink-0"
|
|
125
|
+
>
|
|
126
|
+
{copied === label ? <Check className="w-3 h-3 text-green-400" /> : <Copy className="w-3 h-3" />}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// COMMON SECTIONS
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
export function ConditionsSection({ conditions }: { conditions?: any[] }) {
|
|
138
|
+
if (!conditions || conditions.length === 0) return null
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Section title={`Conditions (${conditions.length})`} defaultExpanded={conditions.length <= 4}>
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
{conditions.map((cond: any) => (
|
|
144
|
+
<div key={cond.type} className="flex items-start gap-2 text-sm">
|
|
145
|
+
<span className={clsx(
|
|
146
|
+
'w-4 h-4 rounded-full flex items-center justify-center text-xs shrink-0 mt-0.5',
|
|
147
|
+
cond.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
|
148
|
+
)}>
|
|
149
|
+
{cond.status === 'True' ? '✓' : '✗'}
|
|
150
|
+
</span>
|
|
151
|
+
<div>
|
|
152
|
+
<div className="text-theme-text-primary">{cond.type}</div>
|
|
153
|
+
{cond.message && <div className="text-xs text-theme-text-tertiary">{cond.message}</div>}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</Section>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// ALERT BANNER
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
const ALERT_COLORS = {
|
|
167
|
+
error: { bg: 'bg-red-500/10', border: 'border-red-500/30', title: 'text-red-400', message: 'text-red-300/80', list: 'text-red-300', bullet: 'text-red-400/60' },
|
|
168
|
+
warning: { bg: 'bg-yellow-500/10', border: 'border-yellow-500/30', title: 'text-yellow-400', message: 'text-yellow-300/80', list: 'text-yellow-300', bullet: 'text-yellow-400/60' },
|
|
169
|
+
info: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', title: 'text-blue-400', message: 'text-blue-300/80', list: 'text-blue-300', bullet: 'text-blue-400/60' },
|
|
170
|
+
success: { bg: 'bg-green-500/10', border: 'border-green-500/30', title: 'text-green-400', message: 'text-green-300/80', list: 'text-green-300', bullet: 'text-green-400/60' },
|
|
171
|
+
} as const
|
|
172
|
+
|
|
173
|
+
const DEFAULT_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
174
|
+
error: AlertTriangle,
|
|
175
|
+
warning: AlertTriangle,
|
|
176
|
+
info: AlertTriangle,
|
|
177
|
+
success: CheckCircle,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface AlertBannerProps {
|
|
181
|
+
variant: 'error' | 'warning' | 'info' | 'success'
|
|
182
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
183
|
+
title: string
|
|
184
|
+
message?: React.ReactNode
|
|
185
|
+
items?: string[]
|
|
186
|
+
children?: React.ReactNode
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function AlertBanner({ variant, icon, title, message, items, children }: AlertBannerProps) {
|
|
190
|
+
const colors = ALERT_COLORS[variant]
|
|
191
|
+
const Icon = icon || DEFAULT_ICONS[variant]
|
|
192
|
+
const hasBody = message || items || children
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div className={clsx('mb-4 p-3 border rounded-lg', colors.bg, colors.border)}>
|
|
196
|
+
<div className={clsx('flex gap-2', hasBody ? 'items-start' : 'items-center')}>
|
|
197
|
+
<Icon className={clsx('w-4 h-4 shrink-0', colors.title, hasBody && 'mt-0.5')} />
|
|
198
|
+
{hasBody ? (
|
|
199
|
+
<div className="flex-1 min-w-0">
|
|
200
|
+
<div className={clsx('text-sm font-medium', colors.title, items && 'mb-1')}>{title}</div>
|
|
201
|
+
{message && <div className={clsx('text-xs mt-1 break-all', colors.message)}>{message}</div>}
|
|
202
|
+
{items && items.length > 0 && (
|
|
203
|
+
<ul className={clsx('text-xs space-y-1', colors.list)}>
|
|
204
|
+
{items.map((item, i) => (
|
|
205
|
+
<li key={i} className="flex items-start gap-1.5">
|
|
206
|
+
<span className={clsx(colors.bullet, 'mt-0.5')}>•</span>
|
|
207
|
+
<span>{item}</span>
|
|
208
|
+
</li>
|
|
209
|
+
))}
|
|
210
|
+
</ul>
|
|
211
|
+
)}
|
|
212
|
+
{children}
|
|
213
|
+
</div>
|
|
214
|
+
) : (
|
|
215
|
+
<div className={clsx('text-sm font-medium', colors.title)}>{title}</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** KNative "Not Ready" alert banner — shared across all KNative renderers */
|
|
223
|
+
export function KnativeNotReadyBanner({ status, data, resourceType }: { status: { level: string }; data: any; resourceType: string }) {
|
|
224
|
+
if (status.level !== 'unhealthy') return null
|
|
225
|
+
const message = (data.status?.conditions || []).find((c: any) => c.type === 'Ready')?.message
|
|
226
|
+
|| `This ${resourceType} is not in a ready state.`
|
|
227
|
+
return <AlertBanner variant="error" title={`${resourceType} Not Ready`} message={message} />
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Problem type for ProblemAlerts component */
|
|
231
|
+
export interface Problem {
|
|
232
|
+
color: 'red' | 'yellow'
|
|
233
|
+
message: string
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Displays a list of problem alerts (warnings and errors) for GitOps resources */
|
|
237
|
+
export function ProblemAlerts({ problems }: { problems: Problem[] }) {
|
|
238
|
+
if (problems.length === 0) return null
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<>
|
|
242
|
+
{problems.map((problem, i) => (
|
|
243
|
+
<AlertBanner
|
|
244
|
+
key={i}
|
|
245
|
+
variant={problem.color === 'red' ? 'error' : 'warning'}
|
|
246
|
+
title={problem.color === 'red' ? 'Issue Detected' : 'Warning'}
|
|
247
|
+
message={problem.message}
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function LabelsSection({ data }: { data: any }) {
|
|
255
|
+
const labels = data.metadata?.labels
|
|
256
|
+
if (!labels || Object.keys(labels).length === 0) return null
|
|
257
|
+
const count = Object.keys(labels).length
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Section title={`Labels (${count})`} icon={Tag} defaultExpanded={count <= 5}>
|
|
261
|
+
<KeyValueBadgeList items={labels} />
|
|
262
|
+
</Section>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function AnnotationsSection({ data }: { data: any }) {
|
|
267
|
+
const annotations = data.metadata?.annotations
|
|
268
|
+
if (!annotations || Object.keys(annotations).length === 0) return null
|
|
269
|
+
const count = Object.keys(annotations).length
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<Section title={`Annotations (${count})`} defaultExpanded={count <= 3}>
|
|
273
|
+
<div className="space-y-1 max-h-48 overflow-y-auto">
|
|
274
|
+
{Object.entries(annotations).map(([k, v]) => (
|
|
275
|
+
<div key={k} className="text-xs">
|
|
276
|
+
<span className="text-theme-text-tertiary">{k}:</span>
|
|
277
|
+
<span className="text-theme-text-secondary ml-1 break-all">{v as string}</span>
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
</Section>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function MetadataSection({ data }: { data: any }) {
|
|
286
|
+
const meta = data.metadata
|
|
287
|
+
if (!meta) return null
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<Section title="Metadata" defaultExpanded>
|
|
291
|
+
<PropertyList>
|
|
292
|
+
<Property label="UID" value={meta.uid} />
|
|
293
|
+
<Property label="Resource Version" value={meta.resourceVersion} />
|
|
294
|
+
<Property label="Generation" value={meta.generation} />
|
|
295
|
+
<Property label="Created" value={meta.creationTimestamp ? (
|
|
296
|
+
<Tooltip content={new Date(meta.creationTimestamp).toLocaleString()}>
|
|
297
|
+
<span className="border-b border-dotted border-theme-text-tertiary cursor-help">{formatAge(meta.creationTimestamp)}</span>
|
|
298
|
+
</Tooltip>
|
|
299
|
+
) : '-'} />
|
|
300
|
+
</PropertyList>
|
|
301
|
+
</Section>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function PodTemplateSection({ template }: { template: any }) {
|
|
306
|
+
if (!template) return null
|
|
307
|
+
const initContainers = template.spec?.initContainers || []
|
|
308
|
+
const containers = template.spec?.containers || []
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div className="space-y-2">
|
|
312
|
+
{initContainers.length > 0 && (
|
|
313
|
+
<>
|
|
314
|
+
<div className="text-xs text-theme-text-tertiary font-medium uppercase tracking-wide">Init Containers</div>
|
|
315
|
+
{initContainers.map((c: any) => (
|
|
316
|
+
<div key={c.name} className="bg-theme-elevated/30 rounded p-2 text-sm border-l-2 border-yellow-500/40">
|
|
317
|
+
<div className="font-medium text-theme-text-primary">{c.name}</div>
|
|
318
|
+
<div className="text-xs text-theme-text-secondary truncate" title={c.image}>{c.image}</div>
|
|
319
|
+
{(c.command || c.args) && (
|
|
320
|
+
<div className="text-xs text-theme-text-tertiary font-mono mt-1 truncate" title={[...(c.command || []), ...(c.args || [])].join(' ')}>
|
|
321
|
+
$ {[...(c.command || []), ...(c.args || [])].join(' ')}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
<div className="text-xs text-theme-text-tertiary font-medium uppercase tracking-wide mt-3">Containers</div>
|
|
327
|
+
</>
|
|
328
|
+
)}
|
|
329
|
+
{containers.map((c: any) => (
|
|
330
|
+
<div key={c.name} className="bg-theme-elevated/30 rounded p-2 text-sm">
|
|
331
|
+
<div className="font-medium text-theme-text-primary">{c.name}</div>
|
|
332
|
+
<div className="text-xs text-theme-text-secondary truncate" title={c.image}>{c.image}</div>
|
|
333
|
+
{c.ports && (
|
|
334
|
+
<div className="text-xs text-theme-text-tertiary mt-1">
|
|
335
|
+
Ports: {c.ports.map((p: any) => `${p.containerPort}/${p.protocol || 'TCP'}`).join(', ')}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// EXTERNAL LINKS SECTION
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
const A8R_LINK_KEYS: Record<string, string> = {
|
|
349
|
+
'a8r.io/runbook': 'Runbook',
|
|
350
|
+
'a8r.io/documentation': 'Documentation',
|
|
351
|
+
'a8r.io/repository': 'Repository',
|
|
352
|
+
'a8r.io/logs': 'Logs',
|
|
353
|
+
'a8r.io/chat': 'Chat',
|
|
354
|
+
'a8r.io/incidents': 'Incidents',
|
|
355
|
+
'a8r.io/bugs': 'Bugs',
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const A8R_TEXT_KEYS: Record<string, string> = {
|
|
359
|
+
'a8r.io/owner': 'Owner',
|
|
360
|
+
'a8r.io/description': 'Description',
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function ExternalLinksSection({ data }: { data: any }) {
|
|
364
|
+
const annotations = data.metadata?.annotations
|
|
365
|
+
if (!annotations) return null
|
|
366
|
+
|
|
367
|
+
const links: { label: string; url: string }[] = []
|
|
368
|
+
const textProps: { label: string; value: string }[] = []
|
|
369
|
+
|
|
370
|
+
// ArgoCD external link
|
|
371
|
+
const argoLink = annotations['link.argocd.argoproj.io/external-link']
|
|
372
|
+
if (argoLink) links.push({ label: 'External Link', url: argoLink })
|
|
373
|
+
|
|
374
|
+
// a8r.io links
|
|
375
|
+
for (const [key, label] of Object.entries(A8R_LINK_KEYS)) {
|
|
376
|
+
const val = annotations[key]
|
|
377
|
+
if (val) links.push({ label, url: val })
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// a8r.io text properties
|
|
381
|
+
for (const [key, label] of Object.entries(A8R_TEXT_KEYS)) {
|
|
382
|
+
const val = annotations[key]
|
|
383
|
+
if (val) textProps.push({ label, value: val })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (links.length === 0 && textProps.length === 0) return null
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<Section title={`External Info (${links.length + textProps.length})`} icon={ExternalLink} defaultExpanded>
|
|
390
|
+
<div className="space-y-2">
|
|
391
|
+
{textProps.map(({ label, value }) => (
|
|
392
|
+
<div key={label} className="text-sm">
|
|
393
|
+
<span className="text-theme-text-tertiary">{label}: </span>
|
|
394
|
+
<span className="text-theme-text-primary">{value}</span>
|
|
395
|
+
</div>
|
|
396
|
+
))}
|
|
397
|
+
{links.map(({ label, url }) => (
|
|
398
|
+
<div key={label} className="text-sm">
|
|
399
|
+
<a
|
|
400
|
+
href={url}
|
|
401
|
+
target="_blank"
|
|
402
|
+
rel="noopener noreferrer"
|
|
403
|
+
className="text-blue-400 hover:text-blue-300 hover:underline inline-flex items-center gap-1"
|
|
404
|
+
>
|
|
405
|
+
{label}
|
|
406
|
+
<ExternalLink className="w-3 h-3" />
|
|
407
|
+
</a>
|
|
408
|
+
</div>
|
|
409
|
+
))}
|
|
410
|
+
</div>
|
|
411
|
+
</Section>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// APP INFO SECTION (app.kubernetes.io/* labels)
|
|
417
|
+
// ============================================================================
|
|
418
|
+
|
|
419
|
+
const APP_LABELS: Record<string, string> = {
|
|
420
|
+
'app.kubernetes.io/name': 'App Name',
|
|
421
|
+
'app.kubernetes.io/version': 'Version',
|
|
422
|
+
'app.kubernetes.io/component': 'Component',
|
|
423
|
+
'app.kubernetes.io/part-of': 'Part Of',
|
|
424
|
+
'app.kubernetes.io/managed-by': 'Managed By',
|
|
425
|
+
'app.kubernetes.io/instance': 'Instance',
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function AppInfoSection({ data }: { data: any }) {
|
|
429
|
+
const labels = data.metadata?.labels
|
|
430
|
+
if (!labels) return null
|
|
431
|
+
|
|
432
|
+
const entries = Object.entries(APP_LABELS)
|
|
433
|
+
.map(([key, label]) => ({ label, value: labels[key] }))
|
|
434
|
+
.filter(({ value }) => value)
|
|
435
|
+
|
|
436
|
+
if (entries.length === 0) return null
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<Section title="App Info" icon={Layers} defaultExpanded>
|
|
440
|
+
<PropertyList>
|
|
441
|
+
{entries.map(({ label, value }) => (
|
|
442
|
+
<Property key={label} label={label} value={value} />
|
|
443
|
+
))}
|
|
444
|
+
</PropertyList>
|
|
445
|
+
</Section>
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// HELPERS
|
|
451
|
+
// ============================================================================
|
|
452
|
+
|
|
453
|
+
export function getKindColor(kind: string): string {
|
|
454
|
+
const k = kind.toLowerCase()
|
|
455
|
+
// Use darker text for light mode contrast, brighter for dark mode
|
|
456
|
+
if (k.includes('pod')) return 'bg-lime-500/20 text-lime-700 dark:text-lime-300 border-lime-500/30'
|
|
457
|
+
if (k.includes('deployment')) return 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 border-emerald-500/30'
|
|
458
|
+
if (k.includes('service')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30'
|
|
459
|
+
if (k.includes('gateway')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-300 border-violet-500/30'
|
|
460
|
+
if (k.includes('route')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30'
|
|
461
|
+
if (k.includes('ingress')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-300 border-violet-500/30'
|
|
462
|
+
if (k.includes('configmap')) return 'bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/30'
|
|
463
|
+
if (k.endsWith('report')) return 'bg-rose-500/20 text-rose-700 dark:text-rose-300 border-rose-500/30'
|
|
464
|
+
if (k.includes('secret')) return 'bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30'
|
|
465
|
+
if (k.includes('daemonset')) return 'bg-teal-500/20 text-teal-700 dark:text-teal-300 border-teal-500/30'
|
|
466
|
+
if (k.includes('statefulset')) return 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border-cyan-500/30'
|
|
467
|
+
if (k.includes('replicaset')) return 'bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30'
|
|
468
|
+
if (k.includes('hpa') || k.includes('horizontalpodautoscaler')) return 'bg-pink-500/20 text-pink-700 dark:text-pink-300 border-pink-500/30'
|
|
469
|
+
if (k.includes('cronjob')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30'
|
|
470
|
+
if (k.includes('job')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30'
|
|
471
|
+
if (k.includes('node')) return 'bg-gray-500/20 text-gray-700 dark:text-gray-300 border-gray-500/30'
|
|
472
|
+
if (k.includes('namespace')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30'
|
|
473
|
+
if (k.includes('persistentvolume')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30'
|
|
474
|
+
// Default color for CRDs
|
|
475
|
+
return 'bg-fuchsia-500/20 text-fuchsia-700 dark:text-fuchsia-300 border-fuchsia-500/30'
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function formatKindName(kind: string): string {
|
|
479
|
+
const k = kind.toLowerCase()
|
|
480
|
+
const names: Record<string, string> = {
|
|
481
|
+
pods: 'Pod', deployments: 'Deployment', daemonsets: 'DaemonSet', statefulsets: 'StatefulSet',
|
|
482
|
+
replicasets: 'ReplicaSet', services: 'Service', ingresses: 'Ingress',
|
|
483
|
+
gateways: 'Gateway', httproutes: 'HTTPRoute', grpcroutes: 'GRPCRoute',
|
|
484
|
+
tcproutes: 'TCPRoute', tlsroutes: 'TLSRoute', configmaps: 'ConfigMap',
|
|
485
|
+
secrets: 'Secret', jobs: 'Job', cronjobs: 'CronJob', hpas: 'HPA',
|
|
486
|
+
horizontalpodautoscalers: 'HPA', nodes: 'Node', namespaces: 'Namespace',
|
|
487
|
+
persistentvolumeclaims: 'PVC', persistentvolumes: 'PV',
|
|
488
|
+
}
|
|
489
|
+
if (names[k]) return names[k]
|
|
490
|
+
|
|
491
|
+
// For unknown kinds (CRDs), use the original kind name
|
|
492
|
+
// or format it nicely if it's a plural name
|
|
493
|
+
if (kind.endsWith('s') && !kind.endsWith('ss')) {
|
|
494
|
+
// Try to singularize simple plurals
|
|
495
|
+
const singular = kind.slice(0, -1)
|
|
496
|
+
// Capitalize first letter
|
|
497
|
+
return singular.charAt(0).toUpperCase() + singular.slice(1)
|
|
498
|
+
}
|
|
499
|
+
return kind
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Type for copy handler
|
|
503
|
+
export type CopyHandler = (text: string, key: string) => void
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// RELATED RESOURCES SECTION
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
import type { TimelineEvent, Relationships, ResourceRef } from '../../types'
|
|
510
|
+
import { isChangeEvent, isK8sEvent } from '../../types'
|
|
511
|
+
import { Link } from 'lucide-react'
|
|
512
|
+
|
|
513
|
+
interface RelatedResourcesSectionProps {
|
|
514
|
+
relationships: Relationships | undefined
|
|
515
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
519
|
+
const seen = new Set<string>()
|
|
520
|
+
return refs.filter(ref => {
|
|
521
|
+
const key = `${ref.kind}/${ref.namespace}/${ref.name}`
|
|
522
|
+
if (seen.has(key)) return false
|
|
523
|
+
seen.add(key)
|
|
524
|
+
return true
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function RelatedResourcesSection({ relationships, onNavigate }: RelatedResourcesSectionProps) {
|
|
529
|
+
if (!relationships) return null
|
|
530
|
+
|
|
531
|
+
const hasRelationships =
|
|
532
|
+
relationships.owner ||
|
|
533
|
+
relationships.deployment ||
|
|
534
|
+
(relationships.children && relationships.children.length > 0) ||
|
|
535
|
+
(relationships.services && relationships.services.length > 0) ||
|
|
536
|
+
(relationships.ingresses && relationships.ingresses.length > 0) ||
|
|
537
|
+
(relationships.gateways && relationships.gateways.length > 0) ||
|
|
538
|
+
(relationships.routes && relationships.routes.length > 0) ||
|
|
539
|
+
(relationships.pods && relationships.pods.length > 0) ||
|
|
540
|
+
(relationships.configRefs && relationships.configRefs.length > 0) ||
|
|
541
|
+
(relationships.consumers && relationships.consumers.length > 0) ||
|
|
542
|
+
(relationships.scalers && relationships.scalers.length > 0) ||
|
|
543
|
+
(relationships.policies && relationships.policies.length > 0) ||
|
|
544
|
+
relationships.scaleTarget
|
|
545
|
+
|
|
546
|
+
if (!hasRelationships) return null
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
<Section title="Related Resources" icon={Link} defaultExpanded>
|
|
550
|
+
<div className="space-y-3">
|
|
551
|
+
{relationships.owner && (
|
|
552
|
+
<RelationshipGroup label="Owner" refs={[relationships.owner]} onNavigate={onNavigate} />
|
|
553
|
+
)}
|
|
554
|
+
{relationships.deployment && (
|
|
555
|
+
<RelationshipGroup label="Deployment" refs={[relationships.deployment]} onNavigate={onNavigate} />
|
|
556
|
+
)}
|
|
557
|
+
{relationships.children && relationships.children.length > 0 && (
|
|
558
|
+
<RelationshipGroup label="Children" refs={dedupeRefs(relationships.children)} onNavigate={onNavigate} />
|
|
559
|
+
)}
|
|
560
|
+
{relationships.services && relationships.services.length > 0 && (
|
|
561
|
+
<RelationshipGroup label="Services" refs={dedupeRefs(relationships.services)} onNavigate={onNavigate} />
|
|
562
|
+
)}
|
|
563
|
+
{relationships.ingresses && relationships.ingresses.length > 0 && (
|
|
564
|
+
<RelationshipGroup label="Ingresses" refs={dedupeRefs(relationships.ingresses)} onNavigate={onNavigate} />
|
|
565
|
+
)}
|
|
566
|
+
{relationships.gateways && relationships.gateways.length > 0 && (
|
|
567
|
+
<RelationshipGroup label="Gateways" refs={dedupeRefs(relationships.gateways)} onNavigate={onNavigate} />
|
|
568
|
+
)}
|
|
569
|
+
{relationships.routes && relationships.routes.length > 0 && (
|
|
570
|
+
<RelationshipGroup label="Routes" refs={dedupeRefs(relationships.routes)} onNavigate={onNavigate} />
|
|
571
|
+
)}
|
|
572
|
+
{relationships.pods && relationships.pods.length > 0 && (
|
|
573
|
+
<RelationshipGroup label="Pods" refs={dedupeRefs(relationships.pods)} onNavigate={onNavigate} />
|
|
574
|
+
)}
|
|
575
|
+
{relationships.configRefs && relationships.configRefs.length > 0 && (
|
|
576
|
+
<RelationshipGroup label="Configuration" refs={dedupeRefs(relationships.configRefs)} onNavigate={onNavigate} />
|
|
577
|
+
)}
|
|
578
|
+
{relationships.consumers && relationships.consumers.length > 0 && (
|
|
579
|
+
<RelationshipGroup label="Used By" refs={dedupeRefs(relationships.consumers)} onNavigate={onNavigate} />
|
|
580
|
+
)}
|
|
581
|
+
{relationships.scalers && relationships.scalers.length > 0 && (
|
|
582
|
+
<RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
|
|
583
|
+
)}
|
|
584
|
+
{relationships.policies && relationships.policies.length > 0 && (
|
|
585
|
+
<RelationshipGroup label="Disruption Budget" refs={dedupeRefs(relationships.policies)} onNavigate={onNavigate} />
|
|
586
|
+
)}
|
|
587
|
+
{relationships.scaleTarget && (
|
|
588
|
+
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
589
|
+
)}
|
|
590
|
+
</div>
|
|
591
|
+
</Section>
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
interface RelationshipGroupProps {
|
|
596
|
+
label: string
|
|
597
|
+
refs: ResourceRef[]
|
|
598
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const RELATIONSHIP_TRUNCATE_LIMIT = 10
|
|
602
|
+
|
|
603
|
+
function RelationshipGroup({ label, refs, onNavigate }: RelationshipGroupProps) {
|
|
604
|
+
const [showAll, setShowAll] = useState(false)
|
|
605
|
+
if (!refs || refs.length === 0) return null
|
|
606
|
+
|
|
607
|
+
const truncated = !showAll && refs.length > RELATIONSHIP_TRUNCATE_LIMIT
|
|
608
|
+
const visibleRefs = truncated ? refs.slice(0, RELATIONSHIP_TRUNCATE_LIMIT) : refs
|
|
609
|
+
|
|
610
|
+
return (
|
|
611
|
+
<div>
|
|
612
|
+
<div className="text-xs text-theme-text-tertiary mb-1">{label}{refs.length > 1 ? ` (${refs.length})` : ''}</div>
|
|
613
|
+
<div className="flex flex-wrap gap-1">
|
|
614
|
+
{visibleRefs.map((resourceRef, i) => (
|
|
615
|
+
<ResourceRefBadge key={`${resourceRef.kind}-${resourceRef.namespace}-${resourceRef.name}-${i}`} resourceRef={resourceRef} onClick={onNavigate} />
|
|
616
|
+
))}
|
|
617
|
+
{truncated && (
|
|
618
|
+
<button
|
|
619
|
+
onClick={() => setShowAll(true)}
|
|
620
|
+
className="px-2 py-0.5 text-xs rounded border border-theme-border text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated transition-colors"
|
|
621
|
+
>
|
|
622
|
+
Show all {refs.length}
|
|
623
|
+
</button>
|
|
624
|
+
)}
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export interface ResourceRefBadgeProps {
|
|
631
|
+
resourceRef: ResourceRef
|
|
632
|
+
onClick?: (ref: ResourceRef) => void
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Reusable chip/badge for showing a related resource with click-to-navigate */
|
|
636
|
+
export function ResourceRefBadge({ resourceRef, onClick }: ResourceRefBadgeProps) {
|
|
637
|
+
const kindClass = getKindColor(resourceRef.kind)
|
|
638
|
+
const kindName = formatKindForRef(resourceRef.kind)
|
|
639
|
+
|
|
640
|
+
if (onClick) {
|
|
641
|
+
return (
|
|
642
|
+
<button
|
|
643
|
+
onClick={() => onClick(resourceRef)}
|
|
644
|
+
className={clsx(
|
|
645
|
+
'px-2 py-0.5 text-xs rounded border transition-colors hover:brightness-125',
|
|
646
|
+
kindClass
|
|
647
|
+
)}
|
|
648
|
+
title={`${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`}
|
|
649
|
+
>
|
|
650
|
+
<span className="opacity-60">{kindName}/</span>
|
|
651
|
+
{resourceRef.name}
|
|
652
|
+
</button>
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return (
|
|
657
|
+
<span
|
|
658
|
+
className={clsx('px-2 py-0.5 text-xs rounded border', kindClass)}
|
|
659
|
+
title={`${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`}
|
|
660
|
+
>
|
|
661
|
+
<span className="opacity-60">{kindName}/</span>
|
|
662
|
+
{resourceRef.name}
|
|
663
|
+
</span>
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Inline text link for navigating to a resource. Renders as plain text when onNavigate is absent. */
|
|
668
|
+
export function ResourceLink({ name, kind, namespace = '', group, label, onNavigate }: {
|
|
669
|
+
name: string
|
|
670
|
+
kind: string
|
|
671
|
+
namespace?: string
|
|
672
|
+
group?: string
|
|
673
|
+
label?: React.ReactNode
|
|
674
|
+
onNavigate?: ((ref: { kind: string; namespace: string; name: string; group?: string }) => void) | null
|
|
675
|
+
}) {
|
|
676
|
+
if (!onNavigate) return <>{label || name}</>
|
|
677
|
+
return (
|
|
678
|
+
<button
|
|
679
|
+
onClick={() => onNavigate({ kind, namespace, name, group })}
|
|
680
|
+
className="text-blue-400 hover:text-blue-300 hover:underline"
|
|
681
|
+
>
|
|
682
|
+
{label || name}
|
|
683
|
+
</button>
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function formatKindForRef(kind: string): string {
|
|
688
|
+
const k = kind.toLowerCase()
|
|
689
|
+
const shortNames: Record<string, string> = {
|
|
690
|
+
deployment: 'deploy',
|
|
691
|
+
daemonset: 'ds',
|
|
692
|
+
statefulset: 'sts',
|
|
693
|
+
replicaset: 'rs',
|
|
694
|
+
configmap: 'cm',
|
|
695
|
+
service: 'svc',
|
|
696
|
+
ingress: 'ing',
|
|
697
|
+
gateway: 'gw',
|
|
698
|
+
httproute: 'hr',
|
|
699
|
+
grpcroute: 'grpc',
|
|
700
|
+
tcproute: 'tcp',
|
|
701
|
+
tlsroute: 'tls',
|
|
702
|
+
secret: 'secret',
|
|
703
|
+
pod: 'pod',
|
|
704
|
+
job: 'job',
|
|
705
|
+
cronjob: 'cj',
|
|
706
|
+
hpa: 'hpa',
|
|
707
|
+
}
|
|
708
|
+
return shortNames[k] || k
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ============================================================================
|
|
712
|
+
// EVENTS SECTION
|
|
713
|
+
// ============================================================================
|
|
714
|
+
|
|
715
|
+
interface EventsSectionProps {
|
|
716
|
+
events: TimelineEvent[]
|
|
717
|
+
isLoading?: boolean
|
|
718
|
+
/** Optional hint shown below the event list (e.g. "See Timeline tab for related resources") */
|
|
719
|
+
hint?: React.ReactNode
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function EventsSection({ events, isLoading, hint }: EventsSectionProps) {
|
|
723
|
+
if (isLoading) {
|
|
724
|
+
return (
|
|
725
|
+
<Section title="Recent Events" defaultExpanded>
|
|
726
|
+
<div className="text-sm text-theme-text-tertiary">Loading events...</div>
|
|
727
|
+
</Section>
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!events || events.length === 0) {
|
|
732
|
+
return (
|
|
733
|
+
<Section title="Recent Events" defaultExpanded={false}>
|
|
734
|
+
<div className="text-sm text-theme-text-tertiary">No recent events</div>
|
|
735
|
+
{hint && <div className="mt-2">{hint}</div>}
|
|
736
|
+
</Section>
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return (
|
|
741
|
+
<Section title={`Recent Events (${events.length})`} defaultExpanded>
|
|
742
|
+
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
743
|
+
{events.map((event, i) => (
|
|
744
|
+
<div
|
|
745
|
+
key={`${event.id}-${i}`}
|
|
746
|
+
className={clsx(
|
|
747
|
+
'p-2 rounded text-sm border-l-2',
|
|
748
|
+
event.eventType === 'Warning' || (isChangeEvent(event) && event.eventType === 'delete')
|
|
749
|
+
? 'bg-red-500/10 border-red-500'
|
|
750
|
+
: isK8sEvent(event)
|
|
751
|
+
? 'bg-blue-500/10 border-blue-500'
|
|
752
|
+
: 'bg-theme-elevated/30 border-theme-border'
|
|
753
|
+
)}
|
|
754
|
+
>
|
|
755
|
+
<div className="flex items-center justify-between gap-2">
|
|
756
|
+
<span className="font-medium text-theme-text-primary">
|
|
757
|
+
{isK8sEvent(event) ? event.reason : event.eventType}
|
|
758
|
+
</span>
|
|
759
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
760
|
+
{formatEventTime(event.timestamp)}
|
|
761
|
+
</span>
|
|
762
|
+
</div>
|
|
763
|
+
{event.message && (
|
|
764
|
+
<div className="text-xs text-theme-text-secondary mt-1 line-clamp-2">
|
|
765
|
+
{event.message}
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
{isChangeEvent(event) && event.diff?.summary && (
|
|
769
|
+
<div className="text-xs text-theme-text-secondary mt-1">
|
|
770
|
+
{event.diff.summary}
|
|
771
|
+
</div>
|
|
772
|
+
)}
|
|
773
|
+
</div>
|
|
774
|
+
))}
|
|
775
|
+
</div>
|
|
776
|
+
{hint && <div className="mt-2">{hint}</div>}
|
|
777
|
+
</Section>
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function formatEventTime(timestamp: string): string {
|
|
782
|
+
const date = new Date(timestamp)
|
|
783
|
+
const now = new Date()
|
|
784
|
+
const diffMs = now.getTime() - date.getTime()
|
|
785
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
786
|
+
const diffHours = Math.floor(diffMins / 60)
|
|
787
|
+
|
|
788
|
+
if (diffMins < 1) return 'just now'
|
|
789
|
+
if (diffMins < 60) return `${diffMins}m ago`
|
|
790
|
+
if (diffHours < 24) return `${diffHours}h ago`
|
|
791
|
+
return date.toLocaleDateString()
|
|
792
|
+
}
|