@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,476 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Info, CheckCircle, XCircle, AlertCircle, ChevronRight, ChevronDown, FileCode, AlertTriangle, Layers } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
5
|
+
|
|
6
|
+
interface GenericRendererProps {
|
|
7
|
+
data: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function GenericRenderer({ data }: GenericRendererProps) {
|
|
11
|
+
const spec = data.spec || {}
|
|
12
|
+
const status = data.status || {}
|
|
13
|
+
const conditions = status.conditions
|
|
14
|
+
|
|
15
|
+
// Determine health status from conditions or status fields
|
|
16
|
+
const healthStatus = getHealthStatus(status, conditions)
|
|
17
|
+
|
|
18
|
+
// Extract important fields from spec and status
|
|
19
|
+
const statusFields = getImportantFields(status, 'status')
|
|
20
|
+
const specFields = getImportantFields(spec, 'spec')
|
|
21
|
+
|
|
22
|
+
// Get nested objects for separate display
|
|
23
|
+
const nestedSpecObjects = getNestedObjects(spec)
|
|
24
|
+
const nestedStatusObjects = getNestedObjects(status)
|
|
25
|
+
|
|
26
|
+
// Check if resource has meaningful content
|
|
27
|
+
const hasStatusContent = Object.keys(statusFields).length > 0 || (conditions && conditions.length > 0)
|
|
28
|
+
const hasSpecContent = Object.keys(specFields).length > 0 || nestedSpecObjects.length > 0
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{/* Health Status Banner (if determinable) */}
|
|
33
|
+
{healthStatus && (
|
|
34
|
+
<div className={clsx(
|
|
35
|
+
'mb-4 p-3 rounded-lg border flex items-center gap-3',
|
|
36
|
+
healthStatus.type === 'healthy' && 'bg-green-500/10 border-green-500/30',
|
|
37
|
+
healthStatus.type === 'degraded' && 'bg-yellow-500/10 border-yellow-500/30',
|
|
38
|
+
healthStatus.type === 'unhealthy' && 'bg-red-500/10 border-red-500/30',
|
|
39
|
+
healthStatus.type === 'unknown' && 'bg-slate-500/10 border-slate-500/30'
|
|
40
|
+
)}>
|
|
41
|
+
{healthStatus.type === 'healthy' && <CheckCircle className="w-5 h-5 text-green-400 shrink-0" />}
|
|
42
|
+
{healthStatus.type === 'degraded' && <AlertCircle className="w-5 h-5 text-yellow-400 shrink-0" />}
|
|
43
|
+
{healthStatus.type === 'unhealthy' && <XCircle className="w-5 h-5 text-red-400 shrink-0" />}
|
|
44
|
+
{healthStatus.type === 'unknown' && <Info className="w-5 h-5 text-slate-400 shrink-0" />}
|
|
45
|
+
<div>
|
|
46
|
+
<div className={clsx(
|
|
47
|
+
'text-sm font-medium',
|
|
48
|
+
healthStatus.type === 'healthy' && 'text-green-400',
|
|
49
|
+
healthStatus.type === 'degraded' && 'text-yellow-400',
|
|
50
|
+
healthStatus.type === 'unhealthy' && 'text-red-400',
|
|
51
|
+
healthStatus.type === 'unknown' && 'text-slate-400'
|
|
52
|
+
)}>
|
|
53
|
+
{healthStatus.label}
|
|
54
|
+
</div>
|
|
55
|
+
{healthStatus.message && (
|
|
56
|
+
<div className="text-xs text-theme-text-secondary mt-0.5">{healthStatus.message}</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Empty state hint */}
|
|
63
|
+
{!hasStatusContent && !hasSpecContent && (
|
|
64
|
+
<div className="mb-4 p-4 bg-slate-500/10 border border-slate-500/20 rounded-lg">
|
|
65
|
+
<div className="flex items-start gap-3">
|
|
66
|
+
<FileCode className="w-5 h-5 text-slate-400 mt-0.5 shrink-0" />
|
|
67
|
+
<div>
|
|
68
|
+
<div className="text-sm font-medium text-theme-text-secondary">Custom Resource</div>
|
|
69
|
+
<div className="text-xs text-theme-text-tertiary mt-1">
|
|
70
|
+
This resource type doesn't have a dedicated view. Use the YAML view (code icon in header) to see the full resource definition.
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{/* Status Section */}
|
|
78
|
+
{Object.keys(statusFields).length > 0 && (
|
|
79
|
+
<Section title="Status" icon={Info} defaultExpanded>
|
|
80
|
+
<PropertyList>
|
|
81
|
+
{Object.entries(statusFields).map(([key, value]) => (
|
|
82
|
+
<Property key={key} label={formatFieldName(key)} value={formatValue(value)} />
|
|
83
|
+
))}
|
|
84
|
+
</PropertyList>
|
|
85
|
+
</Section>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* Conditions (common in CRDs) */}
|
|
89
|
+
{conditions && Array.isArray(conditions) && conditions.length > 0 && (
|
|
90
|
+
<GenericConditionsSection conditions={conditions} />
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Nested Status Objects */}
|
|
94
|
+
{nestedStatusObjects.length > 0 && (
|
|
95
|
+
<Section title="Status Details" icon={Layers} defaultExpanded={false}>
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
{nestedStatusObjects.map(({ key, value }) => (
|
|
98
|
+
<NestedObjectViewer key={key} name={formatFieldName(key)} value={value} />
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</Section>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Spec Section */}
|
|
105
|
+
{Object.keys(specFields).length > 0 && (
|
|
106
|
+
<Section title="Specification" defaultExpanded={Object.keys(specFields).length <= 8}>
|
|
107
|
+
<PropertyList>
|
|
108
|
+
{Object.entries(specFields).map(([key, value]) => (
|
|
109
|
+
<Property key={key} label={formatFieldName(key)} value={formatValue(value)} />
|
|
110
|
+
))}
|
|
111
|
+
</PropertyList>
|
|
112
|
+
</Section>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Nested Spec Objects */}
|
|
116
|
+
{nestedSpecObjects.length > 0 && (
|
|
117
|
+
<Section title="Specification Details" icon={Layers} defaultExpanded={false}>
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
{nestedSpecObjects.map(({ key, value }) => (
|
|
120
|
+
<NestedObjectViewer key={key} name={formatFieldName(key)} value={value} />
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
</Section>
|
|
124
|
+
)}
|
|
125
|
+
</>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// HEALTH STATUS DETECTION
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
type HealthType = 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
134
|
+
|
|
135
|
+
interface HealthStatusResult {
|
|
136
|
+
type: HealthType
|
|
137
|
+
label: string
|
|
138
|
+
message?: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getHealthStatus(status: any, conditions?: any[]): HealthStatusResult | null {
|
|
142
|
+
if (!status) return null
|
|
143
|
+
|
|
144
|
+
// Check phase first (common pattern)
|
|
145
|
+
if (status.phase) {
|
|
146
|
+
const phase = String(status.phase)
|
|
147
|
+
const healthyPhases = ['Running', 'Active', 'Succeeded', 'Ready', 'Healthy', 'Available', 'Bound', 'Complete']
|
|
148
|
+
const degradedPhases = ['Pending', 'Progressing', 'Unknown', 'Terminating', 'Waiting']
|
|
149
|
+
const unhealthyPhases = ['Failed', 'Error', 'CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull']
|
|
150
|
+
|
|
151
|
+
if (healthyPhases.includes(phase)) {
|
|
152
|
+
return { type: 'healthy', label: phase }
|
|
153
|
+
}
|
|
154
|
+
if (degradedPhases.includes(phase)) {
|
|
155
|
+
return { type: 'degraded', label: phase, message: status.message || status.reason }
|
|
156
|
+
}
|
|
157
|
+
if (unhealthyPhases.includes(phase)) {
|
|
158
|
+
return { type: 'unhealthy', label: phase, message: status.message || status.reason }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check conditions
|
|
163
|
+
if (conditions && Array.isArray(conditions) && conditions.length > 0) {
|
|
164
|
+
// Look for Ready or Available condition
|
|
165
|
+
const readyCondition = conditions.find((c: any) =>
|
|
166
|
+
c.type === 'Ready' || c.type === 'Available' || c.type === 'Healthy'
|
|
167
|
+
)
|
|
168
|
+
if (readyCondition) {
|
|
169
|
+
if (readyCondition.status === 'True') {
|
|
170
|
+
return { type: 'healthy', label: 'Ready' }
|
|
171
|
+
}
|
|
172
|
+
if (readyCondition.status === 'False') {
|
|
173
|
+
return {
|
|
174
|
+
type: 'unhealthy',
|
|
175
|
+
label: 'Not Ready',
|
|
176
|
+
message: readyCondition.message || readyCondition.reason
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check for any False conditions that indicate problems
|
|
182
|
+
const problemConditions = conditions.filter((c: any) =>
|
|
183
|
+
c.status === 'False' && ['Ready', 'Available', 'Healthy', 'Initialized'].includes(c.type)
|
|
184
|
+
)
|
|
185
|
+
if (problemConditions.length > 0) {
|
|
186
|
+
const problem = problemConditions[0]
|
|
187
|
+
return {
|
|
188
|
+
type: 'unhealthy',
|
|
189
|
+
label: `${problem.type}: False`,
|
|
190
|
+
message: problem.message || problem.reason
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check for warning conditions
|
|
195
|
+
const warningConditions = conditions.filter((c: any) =>
|
|
196
|
+
c.status === 'True' && ['Degraded', 'Warning', 'ScalingLimited'].includes(c.type)
|
|
197
|
+
)
|
|
198
|
+
if (warningConditions.length > 0) {
|
|
199
|
+
const warning = warningConditions[0]
|
|
200
|
+
return {
|
|
201
|
+
type: 'degraded',
|
|
202
|
+
label: warning.type,
|
|
203
|
+
message: warning.message || warning.reason
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check replica-based status
|
|
209
|
+
if (status.replicas !== undefined) {
|
|
210
|
+
const desired = status.replicas
|
|
211
|
+
const ready = status.readyReplicas || status.availableReplicas || 0
|
|
212
|
+
if (desired > 0 && ready >= desired) {
|
|
213
|
+
return { type: 'healthy', label: `${ready}/${desired} Ready` }
|
|
214
|
+
}
|
|
215
|
+
if (desired > 0 && ready > 0) {
|
|
216
|
+
return { type: 'degraded', label: `${ready}/${desired} Ready` }
|
|
217
|
+
}
|
|
218
|
+
if (desired > 0 && ready === 0) {
|
|
219
|
+
return { type: 'unhealthy', label: `0/${desired} Ready` }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// FIELD EXTRACTION
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
function getImportantFields(obj: any, context: string): Record<string, any> {
|
|
231
|
+
const result: Record<string, any> = {}
|
|
232
|
+
if (!obj || typeof obj !== 'object') return result
|
|
233
|
+
|
|
234
|
+
// Priority fields to show first
|
|
235
|
+
const priorityFields = context === 'status'
|
|
236
|
+
? ['phase', 'state', 'ready', 'available', 'replicas', 'currentReplicas', 'readyReplicas',
|
|
237
|
+
'availableReplicas', 'updatedReplicas', 'observedGeneration', 'message', 'reason']
|
|
238
|
+
: ['replicas', 'selector', 'schedule', 'suspend', 'concurrencyPolicy', 'parallelism',
|
|
239
|
+
'completions', 'backoffLimit', 'image', 'command', 'args', 'ports', 'resources']
|
|
240
|
+
|
|
241
|
+
// First add priority fields
|
|
242
|
+
for (const field of priorityFields) {
|
|
243
|
+
if (obj[field] !== undefined && !isComplexObject(obj[field])) {
|
|
244
|
+
result[field] = obj[field]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Then add other simple fields
|
|
249
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
250
|
+
if (result[key] !== undefined) continue
|
|
251
|
+
if (key === 'conditions') continue
|
|
252
|
+
if (key === 'managedFields') continue
|
|
253
|
+
if (isComplexObject(value)) continue
|
|
254
|
+
|
|
255
|
+
result[key] = value
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getNestedObjects(obj: any): Array<{ key: string; value: any }> {
|
|
262
|
+
const result: Array<{ key: string; value: any }> = []
|
|
263
|
+
if (!obj || typeof obj !== 'object') return result
|
|
264
|
+
|
|
265
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
266
|
+
if (key === 'conditions') continue
|
|
267
|
+
if (key === 'managedFields') continue
|
|
268
|
+
if (isComplexObject(value)) {
|
|
269
|
+
result.push({ key, value })
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isComplexObject(value: any): boolean {
|
|
277
|
+
if (value === null || value === undefined) return false
|
|
278
|
+
if (typeof value !== 'object') return false
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
return value.some(item => typeof item === 'object' && item !== null)
|
|
281
|
+
}
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatFieldName(name: string): string {
|
|
286
|
+
return name
|
|
287
|
+
.replace(/([A-Z])/g, ' $1')
|
|
288
|
+
.replace(/^./, str => str.toUpperCase())
|
|
289
|
+
.trim()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function formatValue(value: any): string {
|
|
293
|
+
if (value === null || value === undefined) return '-'
|
|
294
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
|
295
|
+
if (Array.isArray(value)) return value.join(', ')
|
|
296
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
297
|
+
return String(value)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// NESTED OBJECT VIEWER
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
interface NestedObjectViewerProps {
|
|
305
|
+
name: string
|
|
306
|
+
value: any
|
|
307
|
+
depth?: number
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function NestedObjectViewer({ name, value, depth = 0 }: NestedObjectViewerProps) {
|
|
311
|
+
const [expanded, setExpanded] = useState(depth < 1)
|
|
312
|
+
|
|
313
|
+
if (Array.isArray(value)) {
|
|
314
|
+
return (
|
|
315
|
+
<div className="text-sm">
|
|
316
|
+
<button
|
|
317
|
+
onClick={() => setExpanded(!expanded)}
|
|
318
|
+
className="flex items-center gap-1 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
319
|
+
>
|
|
320
|
+
{expanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
|
|
321
|
+
<span className="font-medium">{name}</span>
|
|
322
|
+
<span className="text-xs text-theme-text-tertiary">({value.length} items)</span>
|
|
323
|
+
</button>
|
|
324
|
+
{expanded && (
|
|
325
|
+
<div className="ml-5 mt-1 space-y-1">
|
|
326
|
+
{value.map((item, i) => {
|
|
327
|
+
if (typeof item === 'object' && item !== null) {
|
|
328
|
+
return <NestedObjectViewer key={i} name={`[${i}]`} value={item} depth={depth + 1} />
|
|
329
|
+
}
|
|
330
|
+
return (
|
|
331
|
+
<div key={i} className="text-xs text-theme-text-secondary">
|
|
332
|
+
[{i}]: {String(item)}
|
|
333
|
+
</div>
|
|
334
|
+
)
|
|
335
|
+
})}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (typeof value === 'object' && value !== null) {
|
|
343
|
+
const entries = Object.entries(value)
|
|
344
|
+
const simpleEntries = entries.filter(([, v]) => !isComplexObject(v))
|
|
345
|
+
const complexEntries = entries.filter(([, v]) => isComplexObject(v))
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div className="text-sm">
|
|
349
|
+
<button
|
|
350
|
+
onClick={() => setExpanded(!expanded)}
|
|
351
|
+
className="flex items-center gap-1 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
352
|
+
>
|
|
353
|
+
{expanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
|
|
354
|
+
<span className="font-medium">{name}</span>
|
|
355
|
+
<span className="text-xs text-theme-text-tertiary">({entries.length} fields)</span>
|
|
356
|
+
</button>
|
|
357
|
+
{expanded && (
|
|
358
|
+
<div className="ml-5 mt-1 space-y-1">
|
|
359
|
+
{simpleEntries.map(([k, v]) => (
|
|
360
|
+
<div key={k} className="flex items-start gap-2 text-xs">
|
|
361
|
+
<span className="text-theme-text-tertiary w-28 shrink-0">{formatFieldName(k)}</span>
|
|
362
|
+
<span className="text-theme-text-secondary break-all">{formatValue(v)}</span>
|
|
363
|
+
</div>
|
|
364
|
+
))}
|
|
365
|
+
{complexEntries.map(([k, v]) => (
|
|
366
|
+
<NestedObjectViewer key={k} name={formatFieldName(k)} value={v} depth={depth + 1} />
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<div className="text-xs text-theme-text-secondary">
|
|
376
|
+
{name}: {String(value)}
|
|
377
|
+
</div>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// CONDITIONS SECTION
|
|
383
|
+
// ============================================================================
|
|
384
|
+
|
|
385
|
+
function GenericConditionsSection({ conditions }: { conditions: any[] }) {
|
|
386
|
+
// Sort conditions: problematic first, then alphabetically
|
|
387
|
+
const sortedConditions = [...conditions].sort((a, b) => {
|
|
388
|
+
const aProblematic = a.status === 'False' || (a.status === 'True' && ['Degraded', 'Warning'].includes(a.type))
|
|
389
|
+
const bProblematic = b.status === 'False' || (b.status === 'True' && ['Degraded', 'Warning'].includes(b.type))
|
|
390
|
+
if (aProblematic && !bProblematic) return -1
|
|
391
|
+
if (!aProblematic && bProblematic) return 1
|
|
392
|
+
return (a.type || '').localeCompare(b.type || '')
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Check for any problems
|
|
396
|
+
const hasProblems = sortedConditions.some(c =>
|
|
397
|
+
c.status === 'False' || (c.status === 'True' && ['Degraded', 'Warning'].includes(c.type))
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<Section title={`Conditions (${conditions.length})`} defaultExpanded={hasProblems || conditions.length <= 5}>
|
|
402
|
+
<div className="space-y-2">
|
|
403
|
+
{sortedConditions.map((cond: any, index: number) => {
|
|
404
|
+
const isTrue = cond.status === 'True'
|
|
405
|
+
const isFalse = cond.status === 'False'
|
|
406
|
+
const isWarningType = ['Degraded', 'Warning', 'ScalingLimited'].includes(cond.type)
|
|
407
|
+
const isPositiveType = ['Ready', 'Available', 'Healthy', 'Initialized', 'Complete'].includes(cond.type)
|
|
408
|
+
|
|
409
|
+
// Determine icon color based on condition semantics
|
|
410
|
+
let iconColor = 'text-yellow-400'
|
|
411
|
+
if (isTrue && isPositiveType) iconColor = 'text-green-400'
|
|
412
|
+
else if (isTrue && isWarningType) iconColor = 'text-yellow-400'
|
|
413
|
+
else if (isTrue) iconColor = 'text-green-400'
|
|
414
|
+
else if (isFalse && isPositiveType) iconColor = 'text-red-400'
|
|
415
|
+
else if (isFalse) iconColor = 'text-slate-400'
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div key={`${cond.type}-${index}`} className="flex items-start gap-2 text-sm">
|
|
419
|
+
<span className={clsx('w-5 h-5 rounded-full flex items-center justify-center shrink-0 mt-0.5', iconColor)}>
|
|
420
|
+
{isTrue && isPositiveType ? <CheckCircle className="w-4 h-4" /> :
|
|
421
|
+
isTrue && isWarningType ? <AlertTriangle className="w-4 h-4" /> :
|
|
422
|
+
isTrue ? <CheckCircle className="w-4 h-4" /> :
|
|
423
|
+
isFalse && isPositiveType ? <XCircle className="w-4 h-4" /> :
|
|
424
|
+
isFalse ? <AlertCircle className="w-4 h-4" /> :
|
|
425
|
+
<AlertCircle className="w-4 h-4" />}
|
|
426
|
+
</span>
|
|
427
|
+
<div className="flex-1 min-w-0">
|
|
428
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
429
|
+
<span className="text-theme-text-primary font-medium">{cond.type}</span>
|
|
430
|
+
<span className={clsx(
|
|
431
|
+
'px-1.5 py-0.5 rounded text-xs font-medium',
|
|
432
|
+
isTrue ? 'bg-theme-elevated text-theme-text-secondary' : 'bg-red-500/20 text-red-400'
|
|
433
|
+
)}>
|
|
434
|
+
{cond.status}
|
|
435
|
+
</span>
|
|
436
|
+
{cond.reason && (
|
|
437
|
+
<span className="text-xs text-theme-text-tertiary">({cond.reason})</span>
|
|
438
|
+
)}
|
|
439
|
+
</div>
|
|
440
|
+
{cond.message && (
|
|
441
|
+
<div className="text-xs text-theme-text-secondary mt-0.5 break-words">
|
|
442
|
+
{cond.message}
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
{cond.lastTransitionTime && (
|
|
446
|
+
<div className="text-xs text-theme-text-tertiary mt-0.5">
|
|
447
|
+
Last transition: {formatTimestamp(cond.lastTransitionTime)}
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
)
|
|
453
|
+
})}
|
|
454
|
+
</div>
|
|
455
|
+
</Section>
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function formatTimestamp(timestamp: string): string {
|
|
460
|
+
try {
|
|
461
|
+
const date = new Date(timestamp)
|
|
462
|
+
const now = new Date()
|
|
463
|
+
const diffMs = now.getTime() - date.getTime()
|
|
464
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
465
|
+
const diffHours = Math.floor(diffMins / 60)
|
|
466
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
467
|
+
|
|
468
|
+
if (diffMins < 1) return 'just now'
|
|
469
|
+
if (diffMins < 60) return `${diffMins}m ago`
|
|
470
|
+
if (diffHours < 24) return `${diffHours}h ago`
|
|
471
|
+
if (diffDays < 7) return `${diffDays}d ago`
|
|
472
|
+
return date.toLocaleDateString()
|
|
473
|
+
} catch {
|
|
474
|
+
return timestamp
|
|
475
|
+
}
|
|
476
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { GitBranch, AlertTriangle, FolderGit, CheckCircle2 } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
4
|
+
import { formatAge } from '../resource-utils'
|
|
5
|
+
import { formatBytes } from '../../../utils/format'
|
|
6
|
+
import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
|
|
7
|
+
import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
|
|
8
|
+
|
|
9
|
+
interface GitRepositoryRendererProps {
|
|
10
|
+
data: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
|
|
14
|
+
const status = data.status || {}
|
|
15
|
+
const spec = data.spec || {}
|
|
16
|
+
const conditions = (status.conditions || []) as FluxCondition[]
|
|
17
|
+
const artifact = status.artifact || {}
|
|
18
|
+
|
|
19
|
+
// Convert to unified GitOps status
|
|
20
|
+
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true)
|
|
21
|
+
|
|
22
|
+
// Problem detection
|
|
23
|
+
const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
|
|
24
|
+
|
|
25
|
+
if (gitOpsStatus.suspended) {
|
|
26
|
+
problems.push({ color: 'yellow', message: 'GitRepository is suspended' })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (gitOpsStatus.health === 'Degraded' && gitOpsStatus.message) {
|
|
30
|
+
problems.push({ color: 'red', message: gitOpsStatus.message })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract repository info
|
|
34
|
+
const url = spec.url || ''
|
|
35
|
+
const ref = spec.ref || {}
|
|
36
|
+
const branch = ref.branch || ref.tag || ref.semver || ref.commit || 'default'
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
{/* Problem alerts */}
|
|
41
|
+
{problems.map((problem, i) => (
|
|
42
|
+
<div
|
|
43
|
+
key={i}
|
|
44
|
+
className={clsx(
|
|
45
|
+
'mb-4 p-3 border rounded-lg',
|
|
46
|
+
problem.color === 'red'
|
|
47
|
+
? 'bg-red-500/10 border-red-500/30'
|
|
48
|
+
: 'bg-yellow-500/10 border-yellow-500/30'
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<div className="flex items-start gap-2">
|
|
52
|
+
<AlertTriangle
|
|
53
|
+
className={clsx(
|
|
54
|
+
'w-4 h-4 mt-0.5 shrink-0',
|
|
55
|
+
problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
<div className="flex-1 min-w-0">
|
|
59
|
+
<div
|
|
60
|
+
className={clsx(
|
|
61
|
+
'text-sm font-medium',
|
|
62
|
+
problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{problem.color === 'red' ? 'Issue Detected' : 'Warning'}
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
className={clsx(
|
|
69
|
+
'text-xs mt-1',
|
|
70
|
+
problem.color === 'red' ? 'text-red-300/80' : 'text-yellow-300/80'
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{problem.message}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
|
|
80
|
+
{/* Status section */}
|
|
81
|
+
<Section title="Status">
|
|
82
|
+
<div className="space-y-3">
|
|
83
|
+
<GitOpsStatusBadge status={gitOpsStatus} showHealth={false} />
|
|
84
|
+
{spec.interval && (
|
|
85
|
+
<SyncCountdown
|
|
86
|
+
interval={spec.interval}
|
|
87
|
+
lastSyncTime={status.lastHandledReconcileAt}
|
|
88
|
+
suspended={gitOpsStatus.suspended}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</Section>
|
|
93
|
+
|
|
94
|
+
{/* Source section */}
|
|
95
|
+
<Section title="Source" icon={FolderGit}>
|
|
96
|
+
<PropertyList>
|
|
97
|
+
<Property label="URL" value={url} />
|
|
98
|
+
<Property
|
|
99
|
+
label="Reference"
|
|
100
|
+
value={
|
|
101
|
+
<span className="flex items-center gap-1">
|
|
102
|
+
<GitBranch className="w-3.5 h-3.5" />
|
|
103
|
+
{branch}
|
|
104
|
+
</span>
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
{ref.branch && <Property label="Branch" value={ref.branch} />}
|
|
108
|
+
{ref.tag && <Property label="Tag" value={ref.tag} />}
|
|
109
|
+
{ref.semver && <Property label="Semver" value={ref.semver} />}
|
|
110
|
+
{ref.commit && <Property label="Commit" value={ref.commit} />}
|
|
111
|
+
{spec.secretRef?.name && (
|
|
112
|
+
<Property label="Secret" value={spec.secretRef.name} />
|
|
113
|
+
)}
|
|
114
|
+
{spec.ignore && <Property label="Ignore" value={spec.ignore} />}
|
|
115
|
+
</PropertyList>
|
|
116
|
+
</Section>
|
|
117
|
+
|
|
118
|
+
{/* Artifact section (last fetched source) */}
|
|
119
|
+
{artifact.revision && (
|
|
120
|
+
<Section title="Latest Artifact" icon={CheckCircle2}>
|
|
121
|
+
<PropertyList>
|
|
122
|
+
<Property label="Revision" value={artifact.revision} />
|
|
123
|
+
<Property label="Digest" value={artifact.digest} />
|
|
124
|
+
<Property
|
|
125
|
+
label="Last Updated"
|
|
126
|
+
value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
|
|
127
|
+
/>
|
|
128
|
+
{artifact.size && (
|
|
129
|
+
<Property label="Size" value={formatBytes(artifact.size)} />
|
|
130
|
+
)}
|
|
131
|
+
</PropertyList>
|
|
132
|
+
</Section>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Additional Status Info */}
|
|
136
|
+
{status.observedGeneration !== undefined && (
|
|
137
|
+
<Section title="Additional Info" defaultExpanded={false}>
|
|
138
|
+
<PropertyList>
|
|
139
|
+
<Property label="Observed Generation" value={status.observedGeneration} />
|
|
140
|
+
{status.lastHandledReconcileAt && (
|
|
141
|
+
<Property
|
|
142
|
+
label="Last Reconciled"
|
|
143
|
+
value={formatAge(status.lastHandledReconcileAt)}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</PropertyList>
|
|
147
|
+
</Section>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Conditions section */}
|
|
151
|
+
<ConditionsSection conditions={conditions} />
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
}
|