@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,321 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import { AlertTriangle, Copy, Check, Shield, Pencil, Save, XCircle, RefreshCw } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { stringify as yamlStringify } from 'yaml'
|
|
5
|
+
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
6
|
+
import { ConfirmDialog } from '../../ui/ConfirmDialog'
|
|
7
|
+
import type { SecretCertificateInfo, CertificateInfo } from '../../../types'
|
|
8
|
+
|
|
9
|
+
interface SecretRendererProps {
|
|
10
|
+
data: any
|
|
11
|
+
certificateInfo?: SecretCertificateInfo
|
|
12
|
+
resourceData?: any
|
|
13
|
+
onSaveSecretValue?: (yaml: string) => Promise<void>
|
|
14
|
+
isSaving?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDate(dateStr: string): string {
|
|
18
|
+
if (!dateStr) return '-'
|
|
19
|
+
const d = new Date(dateStr)
|
|
20
|
+
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecretValue, isSaving }: SecretRendererProps) {
|
|
24
|
+
const [revealed, setRevealed] = useState<Set<string>>(new Set())
|
|
25
|
+
const [copied, setCopied] = useState<string | null>(null)
|
|
26
|
+
const [editingKey, setEditingKey] = useState<string | null>(null)
|
|
27
|
+
const [editValue, setEditValue] = useState('')
|
|
28
|
+
const [showSaveConfirm, setShowSaveConfirm] = useState(false)
|
|
29
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
30
|
+
const dataKeys = Object.keys(data.data || {})
|
|
31
|
+
const isImmutable = data.immutable === true
|
|
32
|
+
|
|
33
|
+
function toggleReveal(key: string) {
|
|
34
|
+
setRevealed(prev => {
|
|
35
|
+
const next = new Set(prev)
|
|
36
|
+
next.has(key) ? next.delete(key) : next.add(key)
|
|
37
|
+
return next
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decodeBase64(value: string): string {
|
|
42
|
+
try {
|
|
43
|
+
return decodeURIComponent(escape(atob(value)))
|
|
44
|
+
} catch {
|
|
45
|
+
return '[binary data]'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function copyValue(key: string, decodedValue: string): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
await navigator.clipboard.writeText(decodedValue)
|
|
52
|
+
setCopied(key)
|
|
53
|
+
setTimeout(() => setCopied(null), 2000)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('Failed to copy:', err)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function startEdit(key: string, decoded: string) {
|
|
60
|
+
setEditingKey(key)
|
|
61
|
+
setEditValue(decoded)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cancelEdit() {
|
|
65
|
+
setEditingKey(null)
|
|
66
|
+
setEditValue('')
|
|
67
|
+
setShowSaveConfirm(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleSave = useCallback(async (key: string, newValue: string) => {
|
|
71
|
+
if (!onSaveSecretValue || !resourceData) return
|
|
72
|
+
const cleaned = structuredClone(resourceData)
|
|
73
|
+
delete cleaned.status
|
|
74
|
+
if (cleaned.metadata) {
|
|
75
|
+
delete cleaned.metadata.managedFields
|
|
76
|
+
delete cleaned.metadata.resourceVersion
|
|
77
|
+
delete cleaned.metadata.uid
|
|
78
|
+
delete cleaned.metadata.creationTimestamp
|
|
79
|
+
delete cleaned.metadata.generation
|
|
80
|
+
}
|
|
81
|
+
// Encode with UTF-8 support
|
|
82
|
+
cleaned.data[key] = btoa(unescape(encodeURIComponent(newValue)))
|
|
83
|
+
const yaml = yamlStringify(cleaned, { lineWidth: 0, indent: 2 })
|
|
84
|
+
try {
|
|
85
|
+
await onSaveSecretValue(yaml)
|
|
86
|
+
setEditingKey(null)
|
|
87
|
+
setEditValue('')
|
|
88
|
+
setShowSaveConfirm(false)
|
|
89
|
+
} catch {
|
|
90
|
+
// Error is handled by the mutation (toast)
|
|
91
|
+
setShowSaveConfirm(false)
|
|
92
|
+
}
|
|
93
|
+
}, [onSaveSecretValue, resourceData])
|
|
94
|
+
|
|
95
|
+
// Auto-resize textarea
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (textareaRef.current) {
|
|
98
|
+
textareaRef.current.style.height = 'auto'
|
|
99
|
+
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
|
100
|
+
}
|
|
101
|
+
}, [editValue])
|
|
102
|
+
|
|
103
|
+
const certs = certificateInfo?.certificates
|
|
104
|
+
const leafCert = certs?.[0]
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<Section title="Secret">
|
|
109
|
+
<PropertyList>
|
|
110
|
+
<Property label="Type" value={data.type || 'Opaque'} />
|
|
111
|
+
<Property label="Keys" value={String(dataKeys.length)} />
|
|
112
|
+
{data.immutable && <Property label="Immutable" value="Yes" />}
|
|
113
|
+
</PropertyList>
|
|
114
|
+
</Section>
|
|
115
|
+
|
|
116
|
+
{/* Certificate expiry alerts */}
|
|
117
|
+
{leafCert && leafCert.expired && (
|
|
118
|
+
<AlertBanner
|
|
119
|
+
variant="error"
|
|
120
|
+
title="Certificate has expired"
|
|
121
|
+
message={`Expired ${formatDate(leafCert.notAfter)}.${leafCert.daysLeft !== 0 ? ` ${Math.abs(leafCert.daysLeft)}d ago.` : ''}`}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{leafCert && !leafCert.expired && leafCert.daysLeft <= 7 && (
|
|
126
|
+
<AlertBanner
|
|
127
|
+
variant="error"
|
|
128
|
+
title={`Certificate expires in ${leafCert.daysLeft} day${leafCert.daysLeft !== 1 ? 's' : ''}`}
|
|
129
|
+
message="Check that cert-manager or your CA is renewing this certificate."
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{leafCert && !leafCert.expired && leafCert.daysLeft > 7 && leafCert.daysLeft <= 30 && (
|
|
134
|
+
<AlertBanner
|
|
135
|
+
variant="warning"
|
|
136
|
+
title={`Certificate expires in ${leafCert.daysLeft} day${leafCert.daysLeft !== 1 ? 's' : ''}`}
|
|
137
|
+
message="Renewal should happen automatically before expiry."
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Certificate info section */}
|
|
142
|
+
{certs && certs.length > 0 && (
|
|
143
|
+
<>
|
|
144
|
+
{certs.map((cert, i) => (
|
|
145
|
+
<CertificateInfoSection
|
|
146
|
+
key={cert.serialNumber}
|
|
147
|
+
cert={cert}
|
|
148
|
+
index={i}
|
|
149
|
+
total={certs.length}
|
|
150
|
+
/>
|
|
151
|
+
))}
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<Section title="Data" defaultExpanded>
|
|
156
|
+
<div className="space-y-2">
|
|
157
|
+
{dataKeys.map((key) => {
|
|
158
|
+
const decoded = decodeBase64(data.data[key])
|
|
159
|
+
const isBinary = decoded === '[binary data]'
|
|
160
|
+
const isEditing = editingKey === key
|
|
161
|
+
const canEdit = onSaveSecretValue && !isImmutable && !isBinary
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div key={key} className="bg-theme-elevated/30 rounded p-2">
|
|
165
|
+
<div className="flex items-center justify-between gap-2">
|
|
166
|
+
<span className="text-sm text-theme-text-primary truncate">{key}</span>
|
|
167
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
168
|
+
{revealed.has(key) && !isBinary && !isEditing && canEdit && (
|
|
169
|
+
<button
|
|
170
|
+
onClick={() => startEdit(key, decoded)}
|
|
171
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 transition-colors"
|
|
172
|
+
title="Edit value"
|
|
173
|
+
>
|
|
174
|
+
<Pencil className="w-3.5 h-3.5" />
|
|
175
|
+
</button>
|
|
176
|
+
)}
|
|
177
|
+
{revealed.has(key) && !isBinary && !isEditing && (
|
|
178
|
+
<button
|
|
179
|
+
onClick={() => copyValue(key, decoded)}
|
|
180
|
+
className="p-1 text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
|
|
181
|
+
title="Copy value"
|
|
182
|
+
>
|
|
183
|
+
{copied === key ? (
|
|
184
|
+
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
185
|
+
) : (
|
|
186
|
+
<Copy className="w-3.5 h-3.5" />
|
|
187
|
+
)}
|
|
188
|
+
</button>
|
|
189
|
+
)}
|
|
190
|
+
{!isEditing && (
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => toggleReveal(key)}
|
|
193
|
+
className="text-xs text-theme-text-secondary hover:text-theme-text-primary px-1.5 py-0.5 rounded hover:bg-theme-elevated transition-colors"
|
|
194
|
+
>
|
|
195
|
+
{revealed.has(key) ? 'Hide' : 'Reveal'}
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
{isEditing ? (
|
|
201
|
+
<div className="mt-2">
|
|
202
|
+
<textarea
|
|
203
|
+
ref={textareaRef}
|
|
204
|
+
value={editValue}
|
|
205
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
206
|
+
className="w-full bg-theme-base rounded p-2 text-xs text-theme-text-secondary font-mono border border-blue-500/50 focus:border-blue-500 focus:outline-none resize-none overflow-hidden whitespace-pre-wrap"
|
|
207
|
+
style={{ minHeight: '60px' }}
|
|
208
|
+
disabled={isSaving}
|
|
209
|
+
/>
|
|
210
|
+
<div className="flex items-center gap-2 mt-2">
|
|
211
|
+
<button
|
|
212
|
+
onClick={() => setShowSaveConfirm(true)}
|
|
213
|
+
disabled={isSaving}
|
|
214
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors disabled:opacity-50"
|
|
215
|
+
>
|
|
216
|
+
{isSaving ? (
|
|
217
|
+
<RefreshCw className="w-3 h-3 animate-spin" />
|
|
218
|
+
) : (
|
|
219
|
+
<Save className="w-3 h-3" />
|
|
220
|
+
)}
|
|
221
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
222
|
+
</button>
|
|
223
|
+
<button
|
|
224
|
+
onClick={cancelEdit}
|
|
225
|
+
disabled={isSaving}
|
|
226
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary rounded hover:bg-theme-elevated transition-colors disabled:opacity-50"
|
|
227
|
+
>
|
|
228
|
+
<XCircle className="w-3 h-3" />
|
|
229
|
+
Cancel
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
) : revealed.has(key) ? (
|
|
234
|
+
<pre className="mt-2 bg-theme-base rounded p-2 text-xs text-theme-text-secondary overflow-x-auto max-h-40 whitespace-pre-wrap">
|
|
235
|
+
{decoded}
|
|
236
|
+
</pre>
|
|
237
|
+
) : null}
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
})}
|
|
241
|
+
{dataKeys.length === 0 && (
|
|
242
|
+
<div className="text-sm text-theme-text-tertiary">No data</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
</Section>
|
|
246
|
+
|
|
247
|
+
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm">
|
|
248
|
+
<AlertTriangle className="w-4 h-4" />
|
|
249
|
+
Secret values are sensitive. Be careful when revealing.
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{editingKey && (
|
|
253
|
+
<ConfirmDialog
|
|
254
|
+
open={showSaveConfirm}
|
|
255
|
+
onClose={() => setShowSaveConfirm(false)}
|
|
256
|
+
onConfirm={() => handleSave(editingKey, editValue)}
|
|
257
|
+
title="Update Secret"
|
|
258
|
+
message={`Update key "${editingKey}" in secret "${data.metadata?.name || 'unknown'}"?`}
|
|
259
|
+
details="This will modify the secret value in the cluster immediately."
|
|
260
|
+
confirmLabel="Update"
|
|
261
|
+
variant="warning"
|
|
262
|
+
isLoading={isSaving}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function CertificateInfoSection({ cert, index, total }: { cert: CertificateInfo; index: number; total: number }) {
|
|
270
|
+
const expiryTextColor = cert.expired || cert.daysLeft <= 7
|
|
271
|
+
? 'text-red-400'
|
|
272
|
+
: cert.daysLeft <= 30
|
|
273
|
+
? 'text-yellow-400'
|
|
274
|
+
: 'text-green-400'
|
|
275
|
+
|
|
276
|
+
const title = total > 1
|
|
277
|
+
? `Certificate ${index + 1} of ${total}`
|
|
278
|
+
: 'Certificate Info'
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<Section title={title} icon={Shield} defaultExpanded={index === 0}>
|
|
282
|
+
<PropertyList>
|
|
283
|
+
<Property label="Subject (CN)" value={cert.subject} />
|
|
284
|
+
{cert.sans && cert.sans.length > 0 && (
|
|
285
|
+
<Property label="SANs" value={
|
|
286
|
+
<div className="flex flex-wrap gap-1">
|
|
287
|
+
{cert.sans.map(san => (
|
|
288
|
+
<span key={san} className="px-2 py-0.5 bg-theme-elevated rounded text-xs text-theme-text-secondary">
|
|
289
|
+
{san}
|
|
290
|
+
</span>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
} />
|
|
294
|
+
)}
|
|
295
|
+
<Property label="Issuer" value={
|
|
296
|
+
<span>
|
|
297
|
+
{cert.issuer}
|
|
298
|
+
{cert.selfSigned && (
|
|
299
|
+
<span className="ml-2 text-[10px] px-1 py-0.5 bg-yellow-500/10 text-yellow-400 rounded">self-signed</span>
|
|
300
|
+
)}
|
|
301
|
+
</span>
|
|
302
|
+
} />
|
|
303
|
+
<Property label="Key Type" value={cert.keyType} />
|
|
304
|
+
<Property label="Serial" value={
|
|
305
|
+
<span className="font-mono text-xs">{cert.serialNumber}</span>
|
|
306
|
+
} />
|
|
307
|
+
<Property label="Not Before" value={formatDate(cert.notBefore)} />
|
|
308
|
+
<Property label="Expires" value={
|
|
309
|
+
<span>
|
|
310
|
+
{formatDate(cert.notAfter)}
|
|
311
|
+
<span className={clsx('ml-2 text-xs', expiryTextColor)}>
|
|
312
|
+
{cert.expired
|
|
313
|
+
? `(expired ${Math.abs(cert.daysLeft)}d ago)`
|
|
314
|
+
: `(${cert.daysLeft}d remaining)`}
|
|
315
|
+
</span>
|
|
316
|
+
</span>
|
|
317
|
+
} />
|
|
318
|
+
</PropertyList>
|
|
319
|
+
</Section>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ShieldCheck } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import {
|
|
4
|
+
getSecretStoreStatus,
|
|
5
|
+
getSecretStoreProviderType,
|
|
6
|
+
getSecretStoreProviderKey,
|
|
7
|
+
getSecretStoreProviderDetails,
|
|
8
|
+
getSecretStoreRetrySettings,
|
|
9
|
+
getSecretStoreController,
|
|
10
|
+
} from '../resource-utils-eso'
|
|
11
|
+
|
|
12
|
+
// Provider-specific color for visual distinction
|
|
13
|
+
function getProviderColor(providerKey: string): string {
|
|
14
|
+
switch (providerKey) {
|
|
15
|
+
case 'aws': return 'bg-orange-500/15 text-orange-400 border-orange-500/30'
|
|
16
|
+
case 'azurekv': return 'bg-blue-500/15 text-blue-400 border-blue-500/30'
|
|
17
|
+
case 'gcpsm': return 'bg-blue-500/15 text-blue-400 border-blue-500/30'
|
|
18
|
+
case 'vault': return 'bg-purple-500/15 text-purple-400 border-purple-500/30'
|
|
19
|
+
case 'kubernetes': return 'bg-cyan-500/15 text-cyan-400 border-cyan-500/30'
|
|
20
|
+
case 'doppler': return 'bg-green-500/15 text-green-400 border-green-500/30'
|
|
21
|
+
case 'onepassword': return 'bg-blue-500/15 text-blue-400 border-blue-500/30'
|
|
22
|
+
case 'akeyless': return 'bg-indigo-500/15 text-indigo-400 border-indigo-500/30'
|
|
23
|
+
default: return 'bg-theme-elevated text-theme-text-secondary border-theme-border'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SecretStoreRendererProps {
|
|
28
|
+
data: any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SecretStoreRenderer({ data }: SecretStoreRendererProps) {
|
|
32
|
+
const conditions = data.status?.conditions || []
|
|
33
|
+
|
|
34
|
+
const storeStatus = getSecretStoreStatus(data)
|
|
35
|
+
const providerType = getSecretStoreProviderType(data)
|
|
36
|
+
const providerKey = getSecretStoreProviderKey(data)
|
|
37
|
+
const providerDetails = getSecretStoreProviderDetails(data)
|
|
38
|
+
const retrySettings = getSecretStoreRetrySettings(data)
|
|
39
|
+
const controller = getSecretStoreController(data)
|
|
40
|
+
|
|
41
|
+
// Problem detection
|
|
42
|
+
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
43
|
+
const isNotReady = readyCond?.status === 'False'
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{/* Alert if not ready */}
|
|
48
|
+
{isNotReady && (
|
|
49
|
+
<AlertBanner
|
|
50
|
+
variant="error"
|
|
51
|
+
title="Store Not Ready"
|
|
52
|
+
message={readyCond?.message || 'The SecretStore connection is not ready.'}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Provider section */}
|
|
57
|
+
<Section title="Provider" icon={ShieldCheck} defaultExpanded>
|
|
58
|
+
<div className="mb-3">
|
|
59
|
+
<span className={`inline-flex items-center px-2.5 py-1 rounded border text-sm font-medium ${getProviderColor(providerKey)}`}>
|
|
60
|
+
{providerType}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{providerDetails.length > 0 && (
|
|
65
|
+
<PropertyList>
|
|
66
|
+
{providerDetails.map((detail, i) => (
|
|
67
|
+
<Property key={i} label={detail.label} value={detail.value} />
|
|
68
|
+
))}
|
|
69
|
+
</PropertyList>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{controller && (
|
|
73
|
+
<div className="mt-2 pt-2 border-t border-theme-border">
|
|
74
|
+
<PropertyList>
|
|
75
|
+
<Property label="Controller" value={controller} />
|
|
76
|
+
</PropertyList>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</Section>
|
|
80
|
+
|
|
81
|
+
{/* Connection Status */}
|
|
82
|
+
<Section title="Connection" defaultExpanded>
|
|
83
|
+
<PropertyList>
|
|
84
|
+
<Property label="Status" value={storeStatus.text} />
|
|
85
|
+
{readyCond?.reason && readyCond.reason !== storeStatus.text && (
|
|
86
|
+
<Property label="Reason" value={readyCond.reason} />
|
|
87
|
+
)}
|
|
88
|
+
{readyCond?.lastTransitionTime && (
|
|
89
|
+
<Property label="Last Transition" value={new Date(readyCond.lastTransitionTime).toLocaleString()} />
|
|
90
|
+
)}
|
|
91
|
+
</PropertyList>
|
|
92
|
+
</Section>
|
|
93
|
+
|
|
94
|
+
{/* Retry Settings */}
|
|
95
|
+
{retrySettings && (
|
|
96
|
+
<Section title="Retry Settings" defaultExpanded={false}>
|
|
97
|
+
<PropertyList>
|
|
98
|
+
{retrySettings.maxRetries !== undefined && (
|
|
99
|
+
<Property label="Max Retries" value={String(retrySettings.maxRetries)} />
|
|
100
|
+
)}
|
|
101
|
+
{retrySettings.retryInterval && (
|
|
102
|
+
<Property label="Retry Interval" value={retrySettings.retryInterval} />
|
|
103
|
+
)}
|
|
104
|
+
</PropertyList>
|
|
105
|
+
</Section>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<ConditionsSection conditions={conditions} />
|
|
109
|
+
</>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UserCog, Key, Cloud } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
3
|
+
|
|
4
|
+
interface ServiceAccountRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ServiceAccountRenderer({ data }: ServiceAccountRendererProps) {
|
|
9
|
+
const metadata = data.metadata || {}
|
|
10
|
+
const annotations = metadata.annotations || {}
|
|
11
|
+
const secrets = data.secrets || []
|
|
12
|
+
const imagePullSecrets = data.imagePullSecrets || []
|
|
13
|
+
|
|
14
|
+
// automountServiceAccountToken defaults to true if not set
|
|
15
|
+
const automountToken = data.automountServiceAccountToken !== false
|
|
16
|
+
|
|
17
|
+
// Workload identity annotations
|
|
18
|
+
const gcpServiceAccount = annotations['iam.gke.io/gcp-service-account']
|
|
19
|
+
const awsRoleArn = annotations['eks.amazonaws.com/role-arn']
|
|
20
|
+
const azureClientId = annotations['azure.workload.identity/client-id']
|
|
21
|
+
const hasWorkloadIdentity = gcpServiceAccount || awsRoleArn || azureClientId
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{/* Configuration */}
|
|
26
|
+
<Section title="Configuration" icon={UserCog}>
|
|
27
|
+
<PropertyList>
|
|
28
|
+
<Property
|
|
29
|
+
label="Automount Token"
|
|
30
|
+
value={
|
|
31
|
+
<span
|
|
32
|
+
className={automountToken ? 'text-yellow-400' : 'text-green-400'}
|
|
33
|
+
title={automountToken ? 'Token is automatically mounted in pods' : undefined}
|
|
34
|
+
>
|
|
35
|
+
{automountToken ? 'Yes' : 'No'}
|
|
36
|
+
</span>
|
|
37
|
+
}
|
|
38
|
+
/>
|
|
39
|
+
</PropertyList>
|
|
40
|
+
</Section>
|
|
41
|
+
|
|
42
|
+
{/* Workload Identity */}
|
|
43
|
+
{hasWorkloadIdentity && (
|
|
44
|
+
<Section title="Workload Identity" icon={Cloud}>
|
|
45
|
+
<PropertyList>
|
|
46
|
+
<Property label="GCP Service Account" value={gcpServiceAccount} />
|
|
47
|
+
<Property label="AWS Role ARN" value={awsRoleArn} />
|
|
48
|
+
<Property label="Azure Client ID" value={azureClientId} />
|
|
49
|
+
</PropertyList>
|
|
50
|
+
</Section>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{/* Secrets */}
|
|
54
|
+
{secrets.length > 0 && (
|
|
55
|
+
<Section title={`Secrets (${secrets.length})`} icon={Key}>
|
|
56
|
+
<PropertyList>
|
|
57
|
+
{secrets.map((secret: any) => (
|
|
58
|
+
<Property key={secret.name} label="Secret" value={secret.name} />
|
|
59
|
+
))}
|
|
60
|
+
</PropertyList>
|
|
61
|
+
</Section>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* Image Pull Secrets */}
|
|
65
|
+
{imagePullSecrets.length > 0 && (
|
|
66
|
+
<Section title={`Image Pull Secrets (${imagePullSecrets.length})`}>
|
|
67
|
+
<div className="flex flex-wrap gap-1">
|
|
68
|
+
{imagePullSecrets.map((secret: any) => (
|
|
69
|
+
<span
|
|
70
|
+
key={secret.name}
|
|
71
|
+
className="px-2 py-0.5 bg-theme-elevated rounded text-xs text-theme-text-secondary"
|
|
72
|
+
>
|
|
73
|
+
{secret.name}
|
|
74
|
+
</span>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</Section>
|
|
78
|
+
)}
|
|
79
|
+
</>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Radio } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList } from '../../ui/drawer-components'
|
|
3
|
+
import {
|
|
4
|
+
getServiceMonitorEndpoints,
|
|
5
|
+
getServiceMonitorJobLabel,
|
|
6
|
+
getServiceMonitorNamespaceSelector,
|
|
7
|
+
} from '../resource-utils-prometheus'
|
|
8
|
+
|
|
9
|
+
interface ServiceMonitorRendererProps {
|
|
10
|
+
data: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
|
|
14
|
+
const endpoints = getServiceMonitorEndpoints(data)
|
|
15
|
+
const selector = data.spec?.selector?.matchLabels
|
|
16
|
+
const conditions = data.status?.conditions
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<Section title="ServiceMonitor" icon={Radio}>
|
|
21
|
+
<PropertyList>
|
|
22
|
+
<Property label="Job Label" value={getServiceMonitorJobLabel(data)} />
|
|
23
|
+
<Property label="Endpoints" value={String(endpoints.length)} />
|
|
24
|
+
<Property label="Namespace Scope" value={getServiceMonitorNamespaceSelector(data)} />
|
|
25
|
+
<Property label="Sample Limit" value={data.spec?.sampleLimit ? String(data.spec.sampleLimit) : undefined} />
|
|
26
|
+
</PropertyList>
|
|
27
|
+
</Section>
|
|
28
|
+
|
|
29
|
+
{endpoints.length > 0 && (
|
|
30
|
+
<Section title={`Scrape Endpoints (${endpoints.length})`} defaultExpanded>
|
|
31
|
+
<div className="space-y-2">
|
|
32
|
+
{endpoints.map((ep, i) => (
|
|
33
|
+
<div key={i} className="bg-theme-elevated/30 rounded p-2 text-sm">
|
|
34
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
35
|
+
{ep.port && (
|
|
36
|
+
<span className="text-theme-text-primary font-medium">{ep.port}</span>
|
|
37
|
+
)}
|
|
38
|
+
<span className="text-theme-text-secondary">{ep.path}</span>
|
|
39
|
+
{ep.interval && (
|
|
40
|
+
<span className="px-1.5 py-0.5 bg-theme-hover rounded text-xs text-theme-text-secondary">
|
|
41
|
+
every {ep.interval}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
{ep.scheme && ep.scheme !== 'http' && (
|
|
45
|
+
<span className="px-1.5 py-0.5 bg-theme-hover rounded text-xs text-theme-text-secondary">
|
|
46
|
+
{ep.scheme}
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</Section>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{selector && Object.keys(selector).length > 0 && (
|
|
57
|
+
<Section title="Selector">
|
|
58
|
+
<KeyValueBadgeList items={selector} />
|
|
59
|
+
</Section>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<ConditionsSection conditions={conditions} />
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { Globe, Clock } from 'lucide-react'
|
|
3
|
+
import { Section, PropertyList, Property, KeyValueBadgeList, CopyHandler, AlertBanner } from '../../ui/drawer-components'
|
|
4
|
+
|
|
5
|
+
interface ServiceRendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
onCopy: CopyHandler
|
|
8
|
+
copied: string | null
|
|
9
|
+
renderPortAction?: (props: { namespace: string; serviceName: string; port: number; protocol: string }) => ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: ServiceRendererProps) {
|
|
13
|
+
const spec = data.spec || {}
|
|
14
|
+
const ports = spec.ports || []
|
|
15
|
+
const lbIngress = data.status?.loadBalancer?.ingress || []
|
|
16
|
+
const namespace = data.metadata?.namespace
|
|
17
|
+
const serviceName = data.metadata?.name
|
|
18
|
+
|
|
19
|
+
// Check for issues
|
|
20
|
+
const isLoadBalancer = spec.type === 'LoadBalancer'
|
|
21
|
+
const lbPending = isLoadBalancer && lbIngress.length === 0
|
|
22
|
+
const hasNoSelector = !spec.selector || Object.keys(spec.selector).length === 0
|
|
23
|
+
const isExternalName = spec.type === 'ExternalName'
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{/* LoadBalancer pending warning */}
|
|
28
|
+
{lbPending && (
|
|
29
|
+
<AlertBanner
|
|
30
|
+
variant="warning"
|
|
31
|
+
icon={Clock}
|
|
32
|
+
title="Load Balancer Pending"
|
|
33
|
+
message="External IP/hostname has not been assigned yet. This may take a few minutes. Check Events below if provisioning is stuck."
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
{/* No selector warning (manual endpoints) */}
|
|
38
|
+
{hasNoSelector && !isExternalName && (
|
|
39
|
+
<AlertBanner
|
|
40
|
+
variant="info"
|
|
41
|
+
title="No Pod Selector"
|
|
42
|
+
message="This service has no selector — endpoints must be managed manually or by an external controller."
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<Section title="Service" icon={Globe}>
|
|
47
|
+
<PropertyList>
|
|
48
|
+
<Property label="Type" value={spec.type || 'ClusterIP'} />
|
|
49
|
+
<Property label="Cluster IP" value={spec.clusterIP} copyable onCopy={onCopy} copied={copied} />
|
|
50
|
+
{spec.externalIPs?.length > 0 && (
|
|
51
|
+
<Property label="External IPs" value={spec.externalIPs.join(', ')} copyable onCopy={onCopy} copied={copied} />
|
|
52
|
+
)}
|
|
53
|
+
{lbIngress.length > 0 && (
|
|
54
|
+
<Property
|
|
55
|
+
label="Load Balancer"
|
|
56
|
+
value={lbIngress[0].ip || lbIngress[0].hostname}
|
|
57
|
+
copyable
|
|
58
|
+
onCopy={onCopy}
|
|
59
|
+
copied={copied}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<Property label="Session Affinity" value={spec.sessionAffinity} />
|
|
63
|
+
<Property label="External Traffic" value={spec.externalTrafficPolicy} />
|
|
64
|
+
</PropertyList>
|
|
65
|
+
</Section>
|
|
66
|
+
|
|
67
|
+
<Section title="Ports" defaultExpanded>
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
{ports.map((port: any, i: number) => (
|
|
70
|
+
<div key={`${port.port}-${port.protocol || 'TCP'}`} className="bg-theme-elevated/30 rounded p-2 text-sm">
|
|
71
|
+
<div className="flex items-center justify-between">
|
|
72
|
+
<span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
{renderPortAction?.({
|
|
75
|
+
namespace,
|
|
76
|
+
serviceName,
|
|
77
|
+
port: port.port,
|
|
78
|
+
protocol: port.protocol || 'TCP',
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="text-xs text-theme-text-secondary mt-1">
|
|
83
|
+
{port.port} {port.targetPort !== port.port && `→ ${port.targetPort}`}
|
|
84
|
+
{port.nodePort && ` (NodePort: ${port.nodePort})`}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</Section>
|
|
90
|
+
|
|
91
|
+
{spec.selector && (
|
|
92
|
+
<Section title="Selector">
|
|
93
|
+
<KeyValueBadgeList items={spec.selector} />
|
|
94
|
+
</Section>
|
|
95
|
+
)}
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
}
|