@skyhook-io/k8s-ui 1.4.0 → 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.
- package/package.json +2 -2
- package/src/components/resources/ResourcesView.tsx +112 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/ui/drawer-components.tsx +48 -1
- package/src/theme/variables.css +1 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"react": "^19.2.4",
|
|
66
66
|
"react-dom": "^19.2.4",
|
|
67
67
|
"typescript": "^6.0.2",
|
|
68
|
-
"vitest": "^4.
|
|
68
|
+
"vitest": "^4.1.3",
|
|
69
69
|
"yaml": "^2.8.3"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
getPodStatus,
|
|
32
32
|
getPodRestarts,
|
|
33
33
|
getPodProblems,
|
|
34
|
+
getWorkloadProblems,
|
|
35
|
+
workloadMatchesProblemCategory,
|
|
34
36
|
getContainerSquareStates,
|
|
35
37
|
getWorkloadImages,
|
|
36
38
|
getWorkloadConditions,
|
|
@@ -116,6 +118,8 @@ import {
|
|
|
116
118
|
getCellFilterValue,
|
|
117
119
|
parseColumnFilters,
|
|
118
120
|
serializeColumnFilters,
|
|
121
|
+
podMatchesProblemCategory,
|
|
122
|
+
SEVERITY_DOT_COLOR,
|
|
119
123
|
} from './resource-utils'
|
|
120
124
|
import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
121
125
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -140,7 +144,9 @@ import { ResourcesSidebar } from './ResourcesSidebar'
|
|
|
140
144
|
import type { SelectedKindInfo } from './ResourcesSidebar'
|
|
141
145
|
|
|
142
146
|
// Pod problem filter options (special multi-select, not a single column value)
|
|
143
|
-
const POD_PROBLEMS = ['CrashLoopBackOff', 'ImagePullBackOff', 'OOMKilled', 'Unschedulable', 'Not Ready', 'High Restarts'] as const
|
|
147
|
+
const POD_PROBLEMS = ['CrashLoopBackOff', 'ImagePullBackOff', 'OOMKilled', 'Unschedulable', 'Not Ready', 'High Restarts', 'Init Failed', 'Exit Code Error', 'Failed', 'Other'] as const
|
|
148
|
+
const WORKLOAD_PROBLEMS = ['Unavailable', 'Rollout Stuck', 'Rollout In Progress'] as const
|
|
149
|
+
const WORKLOAD_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets'])
|
|
144
150
|
|
|
145
151
|
// Columns to skip for auto-detected filters (high cardinality, text-like, or non-filterable)
|
|
146
152
|
const SKIP_FILTER_COLUMNS = new Set([
|
|
@@ -455,6 +461,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
455
461
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
456
462
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
457
463
|
{ key: 'instanceType', label: 'Instance Type', width: 'w-32' },
|
|
464
|
+
{ key: 'capacityType', label: 'Capacity', width: 'w-24', tooltip: 'Spot or On-Demand' },
|
|
465
|
+
{ key: 'zone', label: 'Zone', width: 'w-28', hideOnMobile: true },
|
|
458
466
|
{ key: 'nodePool', label: 'Node Pool', width: 'w-32' },
|
|
459
467
|
{ key: 'nodeName', label: 'Node', width: 'w-40', hideOnMobile: true },
|
|
460
468
|
{ key: 'age', label: 'Age', width: 'w-20' },
|
|
@@ -2476,27 +2484,8 @@ export function ResourcesView({
|
|
|
2476
2484
|
const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
|
|
2477
2485
|
if (filters.length === 0) return true
|
|
2478
2486
|
const problems = getPodProblems(pod)
|
|
2479
|
-
const problemMessages = problems.map(p => p.message)
|
|
2480
2487
|
const restarts = getPodRestarts(pod)
|
|
2481
|
-
|
|
2482
|
-
return filters.some(filter => {
|
|
2483
|
-
switch (filter) {
|
|
2484
|
-
case 'CrashLoopBackOff':
|
|
2485
|
-
return problemMessages.includes('CrashLoopBackOff')
|
|
2486
|
-
case 'ImagePullBackOff':
|
|
2487
|
-
return problemMessages.some(m => m.includes('ImagePull'))
|
|
2488
|
-
case 'OOMKilled':
|
|
2489
|
-
return problemMessages.includes('OOMKilled')
|
|
2490
|
-
case 'Unschedulable':
|
|
2491
|
-
return problemMessages.includes('Unschedulable')
|
|
2492
|
-
case 'Not Ready':
|
|
2493
|
-
return problemMessages.includes('Not Ready') || problemMessages.some(m => m.includes('Probe'))
|
|
2494
|
-
case 'High Restarts':
|
|
2495
|
-
return restarts > 5
|
|
2496
|
-
default:
|
|
2497
|
-
return false
|
|
2498
|
-
}
|
|
2499
|
-
})
|
|
2488
|
+
return filters.some(filter => podMatchesProblemCategory(problems, restarts, filter))
|
|
2500
2489
|
}, [])
|
|
2501
2490
|
|
|
2502
2491
|
|
|
@@ -2526,9 +2515,17 @@ export function ResourcesView({
|
|
|
2526
2515
|
)
|
|
2527
2516
|
}
|
|
2528
2517
|
|
|
2529
|
-
// Apply problem filters
|
|
2530
|
-
if (problemFilters.length > 0
|
|
2531
|
-
|
|
2518
|
+
// Apply problem filters
|
|
2519
|
+
if (problemFilters.length > 0) {
|
|
2520
|
+
const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
|
|
2521
|
+
if (kindLower === 'pods') {
|
|
2522
|
+
result = result.filter((r: any) => podMatchesProblemFilter(r, problemFilters))
|
|
2523
|
+
} else if (WORKLOAD_KINDS.has(kindLower)) {
|
|
2524
|
+
result = result.filter((r: any) => {
|
|
2525
|
+
const problems = getWorkloadProblems(r, kindLower)
|
|
2526
|
+
return problemFilters.some(f => workloadMatchesProblemCategory(problems, f))
|
|
2527
|
+
})
|
|
2528
|
+
}
|
|
2532
2529
|
}
|
|
2533
2530
|
|
|
2534
2531
|
// Apply inactive ReplicaSet filter (default: hide inactive)
|
|
@@ -2802,15 +2799,12 @@ export function ResourcesView({
|
|
|
2802
2799
|
|
|
2803
2800
|
for (const pod of resources) {
|
|
2804
2801
|
const podProblems = getPodProblems(pod)
|
|
2805
|
-
const msgs = podProblems.map(p => p.message)
|
|
2806
2802
|
const restarts = getPodRestarts(pod)
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
if (msgs.includes('Not Ready') || msgs.some(m => m.includes('Probe'))) problemCounts['Not Ready']++
|
|
2813
|
-
if (restarts > 5) problemCounts['High Restarts']++
|
|
2803
|
+
for (const category of POD_PROBLEMS) {
|
|
2804
|
+
if (podMatchesProblemCategory(podProblems, restarts, category)) {
|
|
2805
|
+
problemCounts[category]++
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2814
2808
|
}
|
|
2815
2809
|
|
|
2816
2810
|
const activeProblems = POD_PROBLEMS
|
|
@@ -2821,6 +2815,28 @@ export function ResourcesView({
|
|
|
2821
2815
|
}
|
|
2822
2816
|
}
|
|
2823
2817
|
|
|
2818
|
+
// Workload-specific: compute problem counts
|
|
2819
|
+
if (WORKLOAD_KINDS.has(kindLower)) {
|
|
2820
|
+
const problemCounts: Record<string, number> = {}
|
|
2821
|
+
WORKLOAD_PROBLEMS.forEach(p => problemCounts[p] = 0)
|
|
2822
|
+
|
|
2823
|
+
for (const resource of resources) {
|
|
2824
|
+
const workloadProblems = getWorkloadProblems(resource, kindLower)
|
|
2825
|
+
for (const category of WORKLOAD_PROBLEMS) {
|
|
2826
|
+
if (workloadMatchesProblemCategory(workloadProblems, category)) {
|
|
2827
|
+
problemCounts[category]++
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
const activeProblems = WORKLOAD_PROBLEMS
|
|
2833
|
+
.map(p => ({ value: p, count: problemCounts[p] }))
|
|
2834
|
+
.filter(p => p.count > 0)
|
|
2835
|
+
if (activeProblems.length > 0) {
|
|
2836
|
+
problems = activeProblems
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2824
2840
|
// Compute available labels for label filtering
|
|
2825
2841
|
const labelCounts: Record<string, number> = {}
|
|
2826
2842
|
for (const r of resources) {
|
|
@@ -4021,12 +4037,21 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
4021
4037
|
const status = getPodStatus(resource)
|
|
4022
4038
|
const problems = getPodProblems(resource)
|
|
4023
4039
|
return (
|
|
4024
|
-
<div className="flex items-center gap-2">
|
|
4025
|
-
<span className={clsx('badge', status.color)}>
|
|
4040
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
4041
|
+
<span className={clsx('badge truncate', status.color)} title={status.text}>
|
|
4026
4042
|
{status.text}
|
|
4027
4043
|
</span>
|
|
4028
|
-
{problems.length >
|
|
4029
|
-
<Tooltip content={
|
|
4044
|
+
{problems.length > 1 && (
|
|
4045
|
+
<Tooltip content={
|
|
4046
|
+
<div className="space-y-0.5">
|
|
4047
|
+
{problems.map((p, i) => (
|
|
4048
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
4049
|
+
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', SEVERITY_DOT_COLOR[p.severity])} />
|
|
4050
|
+
<span>{p.message}</span>
|
|
4051
|
+
</div>
|
|
4052
|
+
))}
|
|
4053
|
+
</div>
|
|
4054
|
+
}>
|
|
4030
4055
|
<span className="text-red-400">
|
|
4031
4056
|
<AlertTriangle className="w-3.5 h-3.5" />
|
|
4032
4057
|
</span>
|
|
@@ -4101,7 +4126,7 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
4101
4126
|
}
|
|
4102
4127
|
}
|
|
4103
4128
|
|
|
4104
|
-
function WorkloadCell({ resource, column }: { resource: any; kind: string; column: string }) {
|
|
4129
|
+
function WorkloadCell({ resource, column, kind }: { resource: any; kind: string; column: string }) {
|
|
4105
4130
|
const status = resource.status || {}
|
|
4106
4131
|
const spec = resource.spec || {}
|
|
4107
4132
|
|
|
@@ -4110,13 +4135,32 @@ function WorkloadCell({ resource, column }: { resource: any; kind: string; colum
|
|
|
4110
4135
|
const desired = spec.replicas ?? 0
|
|
4111
4136
|
const ready = status.readyReplicas || 0
|
|
4112
4137
|
const allReady = ready === desired && desired > 0
|
|
4138
|
+
const problems = getWorkloadProblems(resource, kind)
|
|
4113
4139
|
return (
|
|
4114
|
-
<
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4140
|
+
<div className="flex items-center gap-1.5">
|
|
4141
|
+
<span className={clsx(
|
|
4142
|
+
'text-sm font-medium',
|
|
4143
|
+
desired === 0 ? 'text-theme-text-secondary' : allReady ? 'text-green-400' : ready > 0 ? 'text-yellow-400' : 'text-red-400'
|
|
4144
|
+
)}>
|
|
4145
|
+
{ready}/{desired}
|
|
4146
|
+
</span>
|
|
4147
|
+
{problems.length > 0 && (
|
|
4148
|
+
<Tooltip content={
|
|
4149
|
+
<div className="space-y-0.5">
|
|
4150
|
+
{problems.map((p, i) => (
|
|
4151
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
4152
|
+
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', SEVERITY_DOT_COLOR[p.severity])} />
|
|
4153
|
+
<span>{p.message}</span>
|
|
4154
|
+
</div>
|
|
4155
|
+
))}
|
|
4156
|
+
</div>
|
|
4157
|
+
}>
|
|
4158
|
+
<span className="text-red-400">
|
|
4159
|
+
<AlertTriangle className="w-3.5 h-3.5" />
|
|
4160
|
+
</span>
|
|
4161
|
+
</Tooltip>
|
|
4162
|
+
)}
|
|
4163
|
+
</div>
|
|
4120
4164
|
)
|
|
4121
4165
|
}
|
|
4122
4166
|
case 'upToDate':
|
|
@@ -4167,13 +4211,32 @@ function DaemonSetCell({ resource, column }: { resource: any; column: string })
|
|
|
4167
4211
|
const desired = status.desiredNumberScheduled || 0
|
|
4168
4212
|
const ready = status.numberReady || 0
|
|
4169
4213
|
const allReady = ready === desired && desired > 0
|
|
4214
|
+
const problems = getWorkloadProblems(resource, 'daemonsets')
|
|
4170
4215
|
return (
|
|
4171
|
-
<
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4216
|
+
<div className="flex items-center gap-1.5">
|
|
4217
|
+
<span className={clsx(
|
|
4218
|
+
'text-sm font-medium',
|
|
4219
|
+
allReady ? 'text-green-400' : ready > 0 ? 'text-yellow-400' : 'text-red-400'
|
|
4220
|
+
)}>
|
|
4221
|
+
{ready}
|
|
4222
|
+
</span>
|
|
4223
|
+
{problems.length > 0 && (
|
|
4224
|
+
<Tooltip content={
|
|
4225
|
+
<div className="space-y-0.5">
|
|
4226
|
+
{problems.map((p, i) => (
|
|
4227
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
4228
|
+
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', SEVERITY_DOT_COLOR[p.severity])} />
|
|
4229
|
+
<span>{p.message}</span>
|
|
4230
|
+
</div>
|
|
4231
|
+
))}
|
|
4232
|
+
</div>
|
|
4233
|
+
}>
|
|
4234
|
+
<span className="text-red-400">
|
|
4235
|
+
<AlertTriangle className="w-3.5 h-3.5" />
|
|
4236
|
+
</span>
|
|
4237
|
+
</Tooltip>
|
|
4238
|
+
)}
|
|
4239
|
+
</div>
|
|
4177
4240
|
)
|
|
4178
4241
|
}
|
|
4179
4242
|
case 'upToDate':
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GitBranch, FolderTree, Settings, Target, XCircle } from 'lucide-react'
|
|
1
|
+
import { GitBranch, FolderTree, Settings, Target, XCircle, History, ListChecks } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
|
|
4
4
|
import { formatAge } from '../resource-utils'
|
|
@@ -16,6 +16,62 @@ interface ArgoApplicationRendererProps {
|
|
|
16
16
|
isTerminating?: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function SourceProperties({ source }: { source: any }) {
|
|
20
|
+
if (!source) return null
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<Property label="Repository" value={source.repoURL} />
|
|
24
|
+
{source.path && <Property label="Path" value={source.path} />}
|
|
25
|
+
{source.targetRevision && (
|
|
26
|
+
<Property
|
|
27
|
+
label="Target Revision"
|
|
28
|
+
value={
|
|
29
|
+
<span className="flex items-center gap-1">
|
|
30
|
+
<GitBranch className="w-3.5 h-3.5" />
|
|
31
|
+
{source.targetRevision}
|
|
32
|
+
</span>
|
|
33
|
+
}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
{source.chart && <Property label="Helm Chart" value={source.chart} />}
|
|
37
|
+
{source.helm?.valueFiles && source.helm.valueFiles.length > 0 && (
|
|
38
|
+
<Property label="Value Files" value={source.helm.valueFiles.join(', ')} />
|
|
39
|
+
)}
|
|
40
|
+
{source.helm?.parameters && source.helm.parameters.length > 0 && (
|
|
41
|
+
<Property
|
|
42
|
+
label="Helm Parameters"
|
|
43
|
+
value={
|
|
44
|
+
<div className="flex flex-wrap gap-1">
|
|
45
|
+
{source.helm.parameters.filter(Boolean).map((p: { name: string; value: string }, i: number) => (
|
|
46
|
+
<span key={i} className="badge-sm bg-theme-elevated text-theme-text-secondary font-mono">
|
|
47
|
+
{p.name ?? '?'}={p.value ?? ''}
|
|
48
|
+
</span>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
{source.kustomize?.namePrefix && (
|
|
55
|
+
<Property label="Kustomize Prefix" value={source.kustomize.namePrefix} />
|
|
56
|
+
)}
|
|
57
|
+
</>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSyncResourceFailed(res: any): boolean {
|
|
62
|
+
return res.status === 'SyncFailed' || res.hookPhase === 'Failed' || res.hookPhase === 'Error'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getSyncResourceBadgeClass(status: string, hookPhase?: string): string {
|
|
66
|
+
if (status === 'SyncFailed' || hookPhase === 'Failed' || hookPhase === 'Error') {
|
|
67
|
+
return 'status-unhealthy'
|
|
68
|
+
}
|
|
69
|
+
if (status === 'Synced') return 'status-healthy'
|
|
70
|
+
if (status === 'Pruned') return 'status-neutral'
|
|
71
|
+
if (status === 'PruneSkipped') return 'status-degraded'
|
|
72
|
+
return 'status-unknown'
|
|
73
|
+
}
|
|
74
|
+
|
|
19
75
|
export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: ArgoApplicationRendererProps) {
|
|
20
76
|
const status = (data.status || {}) as ArgoAppStatus & {
|
|
21
77
|
resources?: ArgoResource[]
|
|
@@ -47,8 +103,9 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
47
103
|
problems.push({ color: 'yellow', message: 'Application is out of sync with git' })
|
|
48
104
|
}
|
|
49
105
|
|
|
50
|
-
// Extract source info
|
|
51
|
-
const
|
|
106
|
+
// Extract source info — support both spec.source (single) and spec.sources (multi, ArgoCD 2.6+)
|
|
107
|
+
const sources: any[] = (spec.sources && spec.sources.length > 0) ? spec.sources : (spec.source ? [spec.source] : [])
|
|
108
|
+
const isMultiSource = Array.isArray(spec.sources) && spec.sources.length > 0
|
|
52
109
|
const destination = spec.destination || {}
|
|
53
110
|
const syncPolicy = spec.syncPolicy || {}
|
|
54
111
|
const operationState = status.operationState
|
|
@@ -56,6 +113,12 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
56
113
|
// Check if sync is in progress
|
|
57
114
|
const isSyncing = operationState?.phase === 'Running'
|
|
58
115
|
|
|
116
|
+
// Extract sync result resources for per-resource failure details
|
|
117
|
+
const syncResultResources: any[] = operationState?.syncResult?.resources || []
|
|
118
|
+
const failedSyncResources = syncResultResources.filter(isSyncResourceFailed)
|
|
119
|
+
const otherSyncResources = syncResultResources.filter((r: any) => !isSyncResourceFailed(r))
|
|
120
|
+
const sortedSyncResources = [...failedSyncResources, ...otherSyncResources]
|
|
121
|
+
|
|
59
122
|
return (
|
|
60
123
|
<>
|
|
61
124
|
<ProblemAlerts problems={problems} />
|
|
@@ -90,31 +153,27 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
90
153
|
</div>
|
|
91
154
|
</Section>
|
|
92
155
|
|
|
93
|
-
{/* Source section */}
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<Property label="Kustomize Prefix" value={source.kustomize.namePrefix} />
|
|
115
|
-
)}
|
|
116
|
-
</PropertyList>
|
|
117
|
-
</Section>
|
|
156
|
+
{/* Source section — renders each source for multi-source apps */}
|
|
157
|
+
{isMultiSource ? (
|
|
158
|
+
<Section title={`Sources (${sources.length})`} icon={FolderTree}>
|
|
159
|
+
<div className="space-y-3">
|
|
160
|
+
{sources.map((src: any, idx: number) => (
|
|
161
|
+
<div key={idx} className="card-inner">
|
|
162
|
+
<div className="text-xs font-medium text-theme-text-tertiary mb-1.5">Source {idx + 1}</div>
|
|
163
|
+
<PropertyList>
|
|
164
|
+
<SourceProperties source={src} />
|
|
165
|
+
</PropertyList>
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
</Section>
|
|
170
|
+
) : sources.length > 0 ? (
|
|
171
|
+
<Section title="Source" icon={FolderTree}>
|
|
172
|
+
<PropertyList>
|
|
173
|
+
<SourceProperties source={sources[0]} />
|
|
174
|
+
</PropertyList>
|
|
175
|
+
</Section>
|
|
176
|
+
) : null}
|
|
118
177
|
|
|
119
178
|
{/* Destination section */}
|
|
120
179
|
<Section title="Destination" icon={Target}>
|
|
@@ -192,9 +251,22 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
192
251
|
{operationState.message && (
|
|
193
252
|
<Property label="Message" value={operationState.message} />
|
|
194
253
|
)}
|
|
254
|
+
{operationState.startedAt && (
|
|
255
|
+
<Property label="Started" value={formatAge(operationState.startedAt)} />
|
|
256
|
+
)}
|
|
195
257
|
{operationState.finishedAt && (
|
|
196
258
|
<Property label="Finished" value={formatAge(operationState.finishedAt)} />
|
|
197
259
|
)}
|
|
260
|
+
{operationState.retryCount != null && (
|
|
261
|
+
<Property
|
|
262
|
+
label="Retries"
|
|
263
|
+
value={
|
|
264
|
+
syncPolicy.retry?.limit
|
|
265
|
+
? `${operationState.retryCount}/${syncPolicy.retry.limit}`
|
|
266
|
+
: String(operationState.retryCount)
|
|
267
|
+
}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
198
270
|
{operationState.syncResult?.revision && (
|
|
199
271
|
<Property label="Revision" value={operationState.syncResult.revision} />
|
|
200
272
|
)}
|
|
@@ -202,6 +274,55 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
202
274
|
</Section>
|
|
203
275
|
)}
|
|
204
276
|
|
|
277
|
+
{/* Sync Result Resources — per-resource sync status with failure details */}
|
|
278
|
+
{sortedSyncResources.length > 0 && (
|
|
279
|
+
<Section
|
|
280
|
+
title={`Sync Result Resources (${sortedSyncResources.length})`}
|
|
281
|
+
icon={ListChecks}
|
|
282
|
+
defaultExpanded={failedSyncResources.length > 0}
|
|
283
|
+
>
|
|
284
|
+
<div className="space-y-1.5">
|
|
285
|
+
{sortedSyncResources.map((res: any, idx: number) => {
|
|
286
|
+
const isFailed = isSyncResourceFailed(res)
|
|
287
|
+
return (
|
|
288
|
+
<div
|
|
289
|
+
key={`${res.kind}-${res.namespace}-${res.name}-${idx}`}
|
|
290
|
+
className={clsx(
|
|
291
|
+
'card-inner px-3 py-2',
|
|
292
|
+
isFailed && 'border-l-2 border-red-500'
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
<div className="flex items-center gap-2 text-sm">
|
|
296
|
+
<span className={clsx('badge badge-sm', getSyncResourceBadgeClass(res.status, res.hookPhase))}>
|
|
297
|
+
{res.status || res.hookPhase || 'Unknown'}
|
|
298
|
+
</span>
|
|
299
|
+
<span className="text-theme-text-primary">
|
|
300
|
+
{res.kind}/{res.name}
|
|
301
|
+
</span>
|
|
302
|
+
{res.namespace && (
|
|
303
|
+
<span className="text-theme-text-tertiary text-xs">({res.namespace})</span>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
{res.message && (
|
|
307
|
+
<div className={clsx(
|
|
308
|
+
'text-xs mt-1 break-all',
|
|
309
|
+
isFailed ? 'text-red-400' : 'text-theme-text-secondary'
|
|
310
|
+
)}>
|
|
311
|
+
{res.message}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
{res.hookPhase && res.hookPhase !== res.status && (
|
|
315
|
+
<div className="text-xs text-theme-text-tertiary mt-0.5">
|
|
316
|
+
Hook: {res.hookPhase}
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
})}
|
|
322
|
+
</div>
|
|
323
|
+
</Section>
|
|
324
|
+
)}
|
|
325
|
+
|
|
205
326
|
{/* Managed Resources */}
|
|
206
327
|
{managedResources.length > 0 && (
|
|
207
328
|
<ManagedResourcesList
|
|
@@ -210,6 +331,47 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
210
331
|
/>
|
|
211
332
|
)}
|
|
212
333
|
|
|
334
|
+
{/* Revision History */}
|
|
335
|
+
{status.history && status.history.length > 0 && (
|
|
336
|
+
<Section title={`Revision History (${status.history.length})`} icon={History} defaultExpanded={false}>
|
|
337
|
+
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
338
|
+
{status.history
|
|
339
|
+
.slice(0, 10)
|
|
340
|
+
.map((entry, idx) => (
|
|
341
|
+
<div
|
|
342
|
+
key={entry.id ?? idx}
|
|
343
|
+
className={clsx(
|
|
344
|
+
'p-2 rounded text-sm',
|
|
345
|
+
idx === 0
|
|
346
|
+
? 'bg-green-500/10 border border-green-500/30'
|
|
347
|
+
: 'bg-theme-elevated/30'
|
|
348
|
+
)}
|
|
349
|
+
>
|
|
350
|
+
<div className="flex items-center justify-between">
|
|
351
|
+
<span className="font-medium text-theme-text-primary font-mono text-xs">
|
|
352
|
+
{entry.revision
|
|
353
|
+
? entry.revision.length > 12
|
|
354
|
+
? entry.revision.slice(0, 12)
|
|
355
|
+
: entry.revision
|
|
356
|
+
: '-'}
|
|
357
|
+
</span>
|
|
358
|
+
{entry.deployedAt && (
|
|
359
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
360
|
+
{formatAge(entry.deployedAt)}
|
|
361
|
+
</span>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
{entry.source?.path && (
|
|
365
|
+
<div className="text-xs text-theme-text-secondary mt-0.5 truncate" title={entry.source.path}>
|
|
366
|
+
{entry.source.path}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
371
|
+
</div>
|
|
372
|
+
</Section>
|
|
373
|
+
)}
|
|
374
|
+
|
|
213
375
|
{/* Revision Info */}
|
|
214
376
|
{(status.sync?.revision || status.reconciledAt) && (
|
|
215
377
|
<Section title="Revision Info" defaultExpanded={false}>
|
|
@@ -25,6 +25,13 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
|
|
|
25
25
|
const target = getCNPGBackupTarget(data)
|
|
26
26
|
const clusterName = getCNPGBackupCluster(data)
|
|
27
27
|
|
|
28
|
+
// WAL range fields
|
|
29
|
+
const beginWal = data.status?.beginWal
|
|
30
|
+
const endWal = data.status?.endWal
|
|
31
|
+
const beginLSN = data.status?.beginLSN
|
|
32
|
+
const endLSN = data.status?.endLSN
|
|
33
|
+
const hasWalRange = beginWal || endWal || beginLSN || endLSN
|
|
34
|
+
|
|
28
35
|
return (
|
|
29
36
|
<>
|
|
30
37
|
{/* Problem alerts */}
|
|
@@ -43,6 +50,9 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
|
|
|
43
50
|
<Property label="Method" value={getCNPGBackupMethod(data)} />
|
|
44
51
|
<Property label="Duration" value={getCNPGBackupDuration(data)} />
|
|
45
52
|
<Property label="Backup Name" value={getCNPGBackupName(data)} />
|
|
53
|
+
{data.status?.instanceID?.podName && (
|
|
54
|
+
<Property label="Instance" value={data.status.instanceID.podName} />
|
|
55
|
+
)}
|
|
46
56
|
</PropertyList>
|
|
47
57
|
{data.status?.startedAt && (
|
|
48
58
|
<div className="mt-2 pt-2 border-t border-theme-border">
|
|
@@ -76,6 +86,18 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
|
|
|
76
86
|
</PropertyList>
|
|
77
87
|
</Section>
|
|
78
88
|
|
|
89
|
+
{/* WAL Range */}
|
|
90
|
+
{hasWalRange && (
|
|
91
|
+
<Section title="WAL Range" defaultExpanded>
|
|
92
|
+
<PropertyList>
|
|
93
|
+
{beginWal && <Property label="Begin WAL" value={beginWal} />}
|
|
94
|
+
{endWal && <Property label="End WAL" value={endWal} />}
|
|
95
|
+
{beginLSN && <Property label="Begin LSN" value={beginLSN} />}
|
|
96
|
+
{endLSN && <Property label="End LSN" value={endLSN} />}
|
|
97
|
+
</PropertyList>
|
|
98
|
+
</Section>
|
|
99
|
+
)}
|
|
100
|
+
|
|
79
101
|
{/* Target - for PITR backups */}
|
|
80
102
|
{target !== '-' && (
|
|
81
103
|
<Section title="Target" defaultExpanded>
|