@skyhook-io/k8s-ui 1.3.2 → 1.4.1

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.
Files changed (78) hide show
  1. package/package.json +2 -2
  2. package/src/components/audit/AuditAlerts.tsx +88 -0
  3. package/src/components/audit/AuditCard.tsx +130 -0
  4. package/src/components/audit/AuditFindingsTable.tsx +526 -0
  5. package/src/components/audit/index.ts +3 -0
  6. package/src/components/dock/DockContext.tsx +5 -2
  7. package/src/components/dock/LocalTerminalTab.tsx +11 -0
  8. package/src/components/resources/ResourcesView.tsx +126 -49
  9. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  10. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  11. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  12. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  13. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  14. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  15. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
  16. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  17. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  18. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  19. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
  20. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  21. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  22. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  23. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  24. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  25. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  26. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  27. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  28. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  29. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  30. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  31. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  32. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
  33. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
  34. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  35. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  36. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  37. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  38. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  39. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  41. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  42. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  43. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  44. package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
  45. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  46. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  47. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  48. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  49. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  50. package/src/components/resources/renderers/index.ts +3 -0
  51. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  52. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  53. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  54. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  55. package/src/components/resources/resource-utils.ts +131 -33
  56. package/src/components/shared/CreateResourceDialog.tsx +236 -0
  57. package/src/components/shared/EditableYamlView.tsx +16 -1
  58. package/src/components/shared/ResourceActionsBar.tsx +9 -7
  59. package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
  60. package/src/components/shared/index.ts +1 -0
  61. package/src/components/timeline/TimelineList.tsx +11 -4
  62. package/src/components/topology/K8sResourceNode.tsx +15 -0
  63. package/src/components/topology/TopologyControls.tsx +21 -1
  64. package/src/components/topology/TopologyGraph.tsx +1 -1
  65. package/src/components/ui/Badge.tsx +8 -0
  66. package/src/components/ui/drawer-components.tsx +68 -5
  67. package/src/components/workload/WorkloadView.tsx +55 -29
  68. package/src/index.ts +3 -0
  69. package/src/theme/variables.css +1 -1
  70. package/src/types/core.ts +2 -1
  71. package/src/types/gitops.ts +8 -0
  72. package/src/utils/badge-colors.ts +7 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/k8s-errors.ts +142 -0
  75. package/src/utils/navigation.test.ts +29 -2
  76. package/src/utils/navigation.ts +45 -24
  77. package/src/utils/resource-icons.ts +3 -0
  78. package/src/utils/skeleton-yaml.ts +302 -0
@@ -1,8 +1,7 @@
1
- import { Database, HardDrive, Activity, Clock, Shield } from 'lucide-react'
1
+ import { Database, HardDrive, Activity, Clock, Shield, KeyRound } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getCNPGClusterInstances,
5
- getCNPGClusterPrimary,
6
5
  getCNPGClusterPhase,
7
6
  getCNPGClusterImage,
8
7
  getCNPGClusterStorage,
@@ -16,6 +15,8 @@ import {
16
15
  getCNPGClusterReplicaSource,
17
16
  getCNPGClusterInstanceNames,
18
17
  getCNPGClusterPostgresParams,
18
+ getCNPGClusterInstancesReportedState,
19
+ getCNPGClusterCertificateExpirations,
19
20
  } from '../resource-utils-cnpg'
20
21
 
21
22
  interface CNPGClusterRendererProps {
@@ -35,15 +36,49 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
35
36
  const postgresParams = getCNPGClusterPostgresParams(data)
36
37
  const instanceNames = getCNPGClusterInstanceNames(data)
37
38
  const bootstrapMethod = getCNPGClusterBootstrapMethod(data)
39
+ const reportedState = getCNPGClusterInstancesReportedState(data)
40
+ const certExpirations = getCNPGClusterCertificateExpirations(data)
41
+
42
+ // Primary mismatch detection
43
+ const targetPrimary = data.status?.targetPrimary
44
+ const currentPrimary = data.status?.currentPrimary
45
+ const primaryMismatch = targetPrimary && currentPrimary && targetPrimary !== currentPrimary
46
+
47
+ // Split-brain detection: multiple instances report isPrimary
48
+ const primariesReported = reportedState.filter(i => i.isPrimary)
49
+ const hasSplitBrain = primariesReported.length > 1
50
+
51
+ // Certificate expiry warnings
52
+ const expiredCerts = certExpirations.filter(c => c.daysUntilExpiry < 0)
53
+ const criticalCerts = certExpirations.filter(c => c.daysUntilExpiry >= 0 && c.daysUntilExpiry <= 7)
54
+ const warningCerts = certExpirations.filter(c => c.daysUntilExpiry > 7 && c.daysUntilExpiry <= 30)
55
+
56
+ // Last failed backup
57
+ const lastFailedBackup = data.status?.lastFailedBackup
38
58
 
39
59
  // Problem detection
40
- const isDegraded = instances > 0 && readyInstances < instances
60
+ const isDown = instances > 0 && readyInstances === 0
61
+ const isDegraded = instances > 0 && readyInstances < instances && readyInstances > 0
41
62
  const isFailover = phase.toLowerCase().includes('failing over')
42
63
  const isSwitchover = phase.toLowerCase().includes('switchover')
43
64
 
44
65
  return (
45
66
  <>
46
67
  {/* Problem alerts */}
68
+ {hasSplitBrain && (
69
+ <AlertBanner
70
+ variant="error"
71
+ title="Potential Split-Brain Detected"
72
+ message={`Multiple instances report as primary: ${primariesReported.map(i => i.podName).join(', ')}. Immediate investigation required.`}
73
+ />
74
+ )}
75
+ {isDown && (
76
+ <AlertBanner
77
+ variant="error"
78
+ title="Cluster Down"
79
+ message={`All ${instances} instances are not ready.`}
80
+ />
81
+ )}
47
82
  {isDegraded && (
48
83
  <AlertBanner
49
84
  variant="warning"
@@ -65,15 +100,76 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
65
100
  message={`Cluster is performing a switchover. Current phase: ${phase}`}
66
101
  />
67
102
  )}
103
+ {primaryMismatch && (
104
+ <AlertBanner
105
+ variant="warning"
106
+ title="Switchover Pending"
107
+ message={`Target primary is ${targetPrimary} but current primary is ${currentPrimary}.`}
108
+ />
109
+ )}
110
+ {lastFailedBackup && (
111
+ <AlertBanner
112
+ variant="error"
113
+ title="Last Backup Failed"
114
+ message={`Last backup failed at ${lastFailedBackup}. WAL archiving may be impacted and RPO is growing.`}
115
+ />
116
+ )}
117
+ {expiredCerts.length > 0 && (
118
+ <AlertBanner
119
+ variant="error"
120
+ title="Certificate Expired"
121
+ items={expiredCerts.map(c => `${c.secretName} expired ${Math.abs(c.daysUntilExpiry)} day${Math.abs(c.daysUntilExpiry) === 1 ? '' : 's'} ago (${c.expiryDate})`)}
122
+ />
123
+ )}
124
+ {criticalCerts.length > 0 && (
125
+ <AlertBanner
126
+ variant="error"
127
+ title="Certificate Expiring Soon"
128
+ items={criticalCerts.map(c => `${c.secretName} expires in ${c.daysUntilExpiry} day${c.daysUntilExpiry === 1 ? '' : 's'} (${c.expiryDate})`)}
129
+ />
130
+ )}
131
+ {warningCerts.length > 0 && (
132
+ <AlertBanner
133
+ variant="warning"
134
+ title="Certificate Expiry Warning"
135
+ items={warningCerts.map(c => `${c.secretName} expires in ${c.daysUntilExpiry} days (${c.expiryDate})`)}
136
+ />
137
+ )}
68
138
 
69
139
  {/* Cluster Overview */}
70
140
  <Section title="Cluster Overview" icon={Database} defaultExpanded>
71
141
  <PropertyList>
72
142
  <Property label="Phase" value={phase} />
73
143
  <Property label="Instances" value={getCNPGClusterInstances(data)} />
74
- <Property label="Current Primary" value={getCNPGClusterPrimary(data)} />
144
+ <Property label="Current Primary" value={currentPrimary || '-'} />
145
+ {targetPrimary && targetPrimary !== currentPrimary && (
146
+ <Property label="Target Primary" value={targetPrimary} />
147
+ )}
75
148
  <Property label="Image" value={getCNPGClusterImage(data)} />
76
149
  <Property label="Update Strategy" value={getCNPGClusterUpdateStrategy(data)} />
150
+ {data.status?.writeService && (
151
+ <Property label="Write Service" value={
152
+ <ResourceLink name={data.status.writeService} kind="Service" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} />
153
+ } />
154
+ )}
155
+ {data.status?.readService && (
156
+ <Property label="Read Service" value={
157
+ <ResourceLink name={data.status.readService} kind="Service" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} />
158
+ } />
159
+ )}
160
+ {data.spec?.enableSuperuserAccess !== undefined && (
161
+ <Property label="Superuser Access" value={
162
+ <span className={`badge-sm ${data.spec.enableSuperuserAccess ? 'bg-green-500/20 text-green-400' : 'bg-theme-hover text-theme-text-secondary'}`}>
163
+ {data.spec.enableSuperuserAccess ? 'Enabled' : 'Disabled'}
164
+ </span>
165
+ } />
166
+ )}
167
+ {(data.spec?.minSyncReplicas !== undefined || data.spec?.maxSyncReplicas !== undefined) && (
168
+ <Property
169
+ label="Sync Replicas"
170
+ value={`Min: ${data.spec?.minSyncReplicas ?? '-'} / Max: ${data.spec?.maxSyncReplicas ?? '-'}`}
171
+ />
172
+ )}
77
173
  </PropertyList>
78
174
  {instanceNames.length > 0 && (
79
175
  <div className="mt-2 pt-2 border-t border-theme-border">
@@ -92,6 +188,29 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
92
188
  )}
93
189
  </Section>
94
190
 
191
+ {/* Replication State */}
192
+ {reportedState.length > 0 && (
193
+ <Section title="Replication" icon={Activity} defaultExpanded>
194
+ <div className="space-y-1.5">
195
+ {reportedState.map((instance) => (
196
+ <div key={instance.podName} className="flex items-center gap-2 text-sm">
197
+ <span className="text-theme-text-primary font-mono flex-1 break-all">{instance.podName}</span>
198
+ <span className={
199
+ instance.isPrimary
200
+ ? 'badge-sm bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'
201
+ : 'badge-sm bg-theme-elevated text-theme-text-secondary'
202
+ }>
203
+ {instance.isPrimary ? 'Primary' : 'Replica'}
204
+ </span>
205
+ {instance.timelineID != null && (
206
+ <span className="text-xs text-theme-text-tertiary">TL {instance.timelineID}</span>
207
+ )}
208
+ </div>
209
+ ))}
210
+ </div>
211
+ </Section>
212
+ )}
213
+
95
214
  {/* Storage */}
96
215
  <Section title="Storage" icon={HardDrive} defaultExpanded>
97
216
  <PropertyList>
@@ -131,6 +250,9 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
131
250
  {backupConfig.lastSuccessfulBackup && (
132
251
  <Property label="Last Successful" value={backupConfig.lastSuccessfulBackup} />
133
252
  )}
253
+ {lastFailedBackup && (
254
+ <Property label="Last Failed" value={lastFailedBackup} />
255
+ )}
134
256
  {backupConfig.firstRecoverabilityPoint && (
135
257
  <Property label="First Recoverability" value={backupConfig.firstRecoverabilityPoint} />
136
258
  )}
@@ -138,6 +260,35 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
138
260
  </Section>
139
261
  )}
140
262
 
263
+ {/* Certificates */}
264
+ {certExpirations.length > 0 && (
265
+ <Section title="Certificates" icon={KeyRound} defaultExpanded>
266
+ <div className="space-y-1.5">
267
+ {certExpirations.map((cert) => (
268
+ <div key={cert.secretName} className="flex items-center gap-2 text-sm">
269
+ <span className="text-theme-text-primary font-mono flex-1 break-all">{cert.secretName}</span>
270
+ <span className="text-xs text-theme-text-tertiary">{cert.expiryDate}</span>
271
+ {cert.daysUntilExpiry <= 7 && (
272
+ <span className="badge-sm bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
273
+ {cert.daysUntilExpiry}d
274
+ </span>
275
+ )}
276
+ {cert.daysUntilExpiry > 7 && cert.daysUntilExpiry <= 30 && (
277
+ <span className="badge-sm bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
278
+ {cert.daysUntilExpiry}d
279
+ </span>
280
+ )}
281
+ {cert.daysUntilExpiry > 30 && (
282
+ <span className="badge-sm bg-theme-elevated text-theme-text-secondary">
283
+ {cert.daysUntilExpiry}d
284
+ </span>
285
+ )}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ </Section>
290
+ )}
291
+
141
292
  {/* Monitoring */}
142
293
  {(monitoring.podMonitorEnabled || (monitoring.customQueriesConfigMap && monitoring.customQueriesConfigMap.length > 0)) && (
143
294
  <Section title="Monitoring" icon={Activity} defaultExpanded>
@@ -165,7 +316,7 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
165
316
 
166
317
  {/* Replication - only if this is a replica cluster */}
167
318
  {isReplica && (
168
- <Section title="Replication" icon={Shield} defaultExpanded>
319
+ <Section title="Replica Cluster" icon={Shield} defaultExpanded>
169
320
  <PropertyList>
170
321
  <Property label="Role" value="Replica" />
171
322
  <Property label="Source" value={getCNPGClusterReplicaSource(data)} />
@@ -1,4 +1,4 @@
1
- import { Users, Database } from 'lucide-react'
1
+ import { Users, Database, KeyRound } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getCNPGPoolerCluster,
@@ -6,6 +6,8 @@ import {
6
6
  getCNPGPoolerMode,
7
7
  getCNPGPoolerInstances,
8
8
  getCNPGPoolerParameters,
9
+ getCNPGPoolerAuthQuery,
10
+ getCNPGPoolerAuthQuerySecret,
9
11
  } from '../resource-utils-cnpg'
10
12
 
11
13
  interface CNPGPoolerRendererProps {
@@ -19,6 +21,8 @@ export function CNPGPoolerRenderer({ data, onNavigate }: CNPGPoolerRendererProps
19
21
  const isDegraded = desired > 0 && ready < desired
20
22
  const clusterName = getCNPGPoolerCluster(data)
21
23
  const parameters = getCNPGPoolerParameters(data)
24
+ const authQuery = getCNPGPoolerAuthQuery(data)
25
+ const authQuerySecret = getCNPGPoolerAuthQuerySecret(data)
22
26
 
23
27
  return (
24
28
  <>
@@ -40,6 +44,29 @@ export function CNPGPoolerRenderer({ data, onNavigate }: CNPGPoolerRendererProps
40
44
  </PropertyList>
41
45
  </Section>
42
46
 
47
+ {/* Authentication */}
48
+ {(authQuery || authQuerySecret) && (
49
+ <Section title="Authentication" icon={KeyRound} defaultExpanded>
50
+ <PropertyList>
51
+ {authQuery && (
52
+ <Property label="Auth Query" value={
53
+ <code className="text-xs font-mono bg-theme-elevated px-1.5 py-0.5 rounded break-all">{authQuery}</code>
54
+ } />
55
+ )}
56
+ {authQuerySecret && (
57
+ <Property label="Auth Query Secret" value={
58
+ <ResourceLink
59
+ name={authQuerySecret.name}
60
+ kind="secrets"
61
+ namespace={data.metadata?.namespace || ''}
62
+ onNavigate={onNavigate}
63
+ />
64
+ } />
65
+ )}
66
+ </PropertyList>
67
+ </Section>
68
+ )}
69
+
43
70
  {/* Cluster Reference */}
44
71
  <Section title="Cluster" icon={Database} defaultExpanded>
45
72
  <PropertyList>
@@ -1,4 +1,4 @@
1
- import { Shield, Clock, Globe } from 'lucide-react'
1
+ import { Shield, Clock, Globe, Key } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
4
4
 
@@ -26,6 +26,10 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
26
26
  const issuerRef = spec.issuerRef || {}
27
27
  const usages = spec.usages || []
28
28
 
29
+ const privateKey = spec.privateKey
30
+ const failedIssuanceAttempts = status.failedIssuanceAttempts
31
+ const lastFailureTime = status.lastFailureTime
32
+
29
33
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
30
34
  const isReady = readyCond?.status === 'True'
31
35
  const isNotReady = readyCond?.status === 'False'
@@ -96,30 +100,53 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
96
100
  />
97
101
  )}
98
102
 
103
+ {failedIssuanceAttempts > 0 && (
104
+ <AlertBanner
105
+ variant="warning"
106
+ title={`${failedIssuanceAttempts} failed issuance attempt${failedIssuanceAttempts !== 1 ? 's' : ''}`}
107
+ message={lastFailureTime ? `Last failure: ${formatDate(lastFailureTime)}` : undefined}
108
+ />
109
+ )}
110
+
99
111
  {/* Certificate Info */}
100
112
  <Section title="Certificate Info" icon={Shield}>
101
113
  <PropertyList>
102
114
  <Property
103
115
  label="Status"
104
116
  value={
105
- <span className={clsx(
106
- 'badge',
107
- isReady
108
- ? 'bg-green-500/20 text-green-400'
109
- : 'bg-red-500/20 text-red-400'
110
- )}>
111
- {isReady ? 'Ready' : 'Not Ready'}
117
+ <span className="flex items-center gap-2">
118
+ <span className={clsx(
119
+ 'badge',
120
+ isReady
121
+ ? 'bg-green-500/20 text-green-400'
122
+ : 'bg-red-500/20 text-red-400'
123
+ )}>
124
+ {isReady ? 'Ready' : 'Not Ready'}
125
+ </span>
126
+ {spec.isCA && (
127
+ <span className="badge bg-purple-500/20 text-purple-400">
128
+ CA Certificate
129
+ </span>
130
+ )}
112
131
  </span>
113
132
  }
114
133
  />
115
134
  <Property label="Secret Name" value={spec.secretName} />
116
135
  <Property label="Revision" value={status.revision} />
136
+ {failedIssuanceAttempts > 0 && (
137
+ <Property label="Failed Issuance Attempts" value={String(failedIssuanceAttempts)} />
138
+ )}
139
+ {lastFailureTime && (
140
+ <Property label="Last Failure Time" value={formatDate(lastFailureTime)} />
141
+ )}
117
142
  </PropertyList>
118
143
  </Section>
119
144
 
120
145
  {/* Validity */}
121
146
  <Section title="Validity" icon={Clock}>
122
147
  <PropertyList>
148
+ <Property label="Duration" value={spec.duration} />
149
+ <Property label="Renew Before" value={spec.renewBefore} />
123
150
  <Property label="Not Before" value={notBefore ? formatDate(notBefore) : '-'} />
124
151
  <Property
125
152
  label="Not After"
@@ -161,6 +188,18 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
161
188
  )}
162
189
  </Section>
163
190
 
191
+ {/* Private Key */}
192
+ {privateKey && (
193
+ <Section title="Private Key" icon={Key}>
194
+ <PropertyList>
195
+ <Property label="Algorithm" value={privateKey.algorithm} />
196
+ <Property label="Size" value={privateKey.size != null ? String(privateKey.size) : undefined} />
197
+ <Property label="Encoding" value={privateKey.encoding} />
198
+ <Property label="Rotation Policy" value={privateKey.rotationPolicy} />
199
+ </PropertyList>
200
+ </Section>
201
+ )}
202
+
164
203
  {/* Domains */}
165
204
  {dnsNames.length > 0 && (
166
205
  <Section title="Domains" icon={Globe}>
@@ -1,4 +1,4 @@
1
- import { Shield, AlertTriangle, Globe, Key } from 'lucide-react'
1
+ import { Shield, AlertTriangle, Globe, Key, FileText } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
4
4
  import { getSolverType } from './ClusterIssuerRenderer'
@@ -68,11 +68,19 @@ export function ChallengeRenderer({ data }: { data: any }) {
68
68
  </span>
69
69
  }
70
70
  />
71
+ {status.reason && !isError && <Property label="Reason" value={status.reason} />}
71
72
  <Property
72
73
  label="Type"
73
74
  value={
74
- <span className="badge bg-theme-elevated text-theme-text-secondary">
75
- {challengeType}
75
+ <span className="flex items-center gap-2">
76
+ <span className="badge bg-theme-elevated text-theme-text-secondary">
77
+ {challengeType}
78
+ </span>
79
+ {spec.wildcard && (
80
+ <span className="badge bg-purple-500/20 text-purple-400">
81
+ Wildcard
82
+ </span>
83
+ )}
76
84
  </span>
77
85
  }
78
86
  />
@@ -84,6 +92,17 @@ export function ChallengeRenderer({ data }: { data: any }) {
84
92
  </PropertyList>
85
93
  </Section>
86
94
 
95
+ {/* Issuer */}
96
+ {spec.issuerRef && (
97
+ <Section title="Issuer" icon={FileText}>
98
+ <PropertyList>
99
+ <Property label="Name" value={spec.issuerRef.name} />
100
+ <Property label="Kind" value={spec.issuerRef.kind} />
101
+ <Property label="Group" value={spec.issuerRef.group} />
102
+ </PropertyList>
103
+ </Section>
104
+ )}
105
+
87
106
  {/* ACME */}
88
107
  {(spec.url || spec.token) && (
89
108
  <Section title="ACME" icon={Globe}>