@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.
- package/package.json +2 -2
- package/src/components/audit/AuditAlerts.tsx +88 -0
- package/src/components/audit/AuditCard.tsx +130 -0
- package/src/components/audit/AuditFindingsTable.tsx +526 -0
- package/src/components/audit/index.ts +3 -0
- package/src/components/dock/DockContext.tsx +5 -2
- package/src/components/dock/LocalTerminalTab.tsx +11 -0
- package/src/components/resources/ResourcesView.tsx +126 -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 +445 -0
- 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 +160 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- 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/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
- 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/ServiceRenderer.tsx +40 -28
- 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/index.ts +3 -0
- 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/CreateResourceDialog.tsx +236 -0
- package/src/components/shared/EditableYamlView.tsx +16 -1
- package/src/components/shared/ResourceActionsBar.tsx +9 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/topology/K8sResourceNode.tsx +15 -0
- package/src/components/topology/TopologyControls.tsx +21 -1
- package/src/components/topology/TopologyGraph.tsx +1 -1
- package/src/components/ui/Badge.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +68 -5
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +2 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/badge-colors.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/k8s-errors.ts +142 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
- package/src/utils/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
Tag,
|
|
21
21
|
Copy,
|
|
22
22
|
Check,
|
|
23
|
+
Plus,
|
|
23
24
|
} from 'lucide-react'
|
|
24
25
|
import { clsx } from 'clsx'
|
|
25
26
|
import { ResourceBar } from '../ui/ResourceBar'
|
|
@@ -30,6 +31,8 @@ import {
|
|
|
30
31
|
getPodStatus,
|
|
31
32
|
getPodRestarts,
|
|
32
33
|
getPodProblems,
|
|
34
|
+
getWorkloadProblems,
|
|
35
|
+
workloadMatchesProblemCategory,
|
|
33
36
|
getContainerSquareStates,
|
|
34
37
|
getWorkloadImages,
|
|
35
38
|
getWorkloadConditions,
|
|
@@ -115,6 +118,8 @@ import {
|
|
|
115
118
|
getCellFilterValue,
|
|
116
119
|
parseColumnFilters,
|
|
117
120
|
serializeColumnFilters,
|
|
121
|
+
podMatchesProblemCategory,
|
|
122
|
+
SEVERITY_DOT_COLOR,
|
|
118
123
|
} from './resource-utils'
|
|
119
124
|
import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
120
125
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -139,7 +144,9 @@ import { ResourcesSidebar } from './ResourcesSidebar'
|
|
|
139
144
|
import type { SelectedKindInfo } from './ResourcesSidebar'
|
|
140
145
|
|
|
141
146
|
// Pod problem filter options (special multi-select, not a single column value)
|
|
142
|
-
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'])
|
|
143
150
|
|
|
144
151
|
// Columns to skip for auto-detected filters (high cardinality, text-like, or non-filterable)
|
|
145
152
|
const SKIP_FILTER_COLUMNS = new Set([
|
|
@@ -454,6 +461,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
454
461
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
455
462
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
456
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 },
|
|
457
466
|
{ key: 'nodePool', label: 'Node Pool', width: 'w-32' },
|
|
458
467
|
{ key: 'nodeName', label: 'Node', width: 'w-40', hideOnMobile: true },
|
|
459
468
|
{ key: 'age', label: 'Age', width: 'w-20' },
|
|
@@ -1421,6 +1430,8 @@ interface ResourcesViewProps {
|
|
|
1421
1430
|
onSelectedKindChange?: (kind: { name: string; kind: string; group: string }) => void
|
|
1422
1431
|
/** When true, the sidebar is not rendered. Useful when a standalone ResourcesSidebar is used externally. */
|
|
1423
1432
|
hideSidebar?: boolean
|
|
1433
|
+
/** Callback when the [+] create button is clicked. Receives the currently selected kind info. */
|
|
1434
|
+
onCreateResource?: (kind: { name: string; kind: string; group: string } | null) => void
|
|
1424
1435
|
}
|
|
1425
1436
|
|
|
1426
1437
|
// Default selected kind
|
|
@@ -1492,6 +1503,7 @@ export function ResourcesView({
|
|
|
1492
1503
|
onOpenWorkloadLogs,
|
|
1493
1504
|
onSelectedKindChange,
|
|
1494
1505
|
hideSidebar = false,
|
|
1506
|
+
onCreateResource,
|
|
1495
1507
|
}: ResourcesViewProps) {
|
|
1496
1508
|
const location = useMemo(() => ({ search: locationSearch, pathname: locationPathname }), [locationSearch, locationPathname])
|
|
1497
1509
|
const initialFilters = getInitialFiltersFromURL()
|
|
@@ -2472,27 +2484,8 @@ export function ResourcesView({
|
|
|
2472
2484
|
const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
|
|
2473
2485
|
if (filters.length === 0) return true
|
|
2474
2486
|
const problems = getPodProblems(pod)
|
|
2475
|
-
const problemMessages = problems.map(p => p.message)
|
|
2476
2487
|
const restarts = getPodRestarts(pod)
|
|
2477
|
-
|
|
2478
|
-
return filters.some(filter => {
|
|
2479
|
-
switch (filter) {
|
|
2480
|
-
case 'CrashLoopBackOff':
|
|
2481
|
-
return problemMessages.includes('CrashLoopBackOff')
|
|
2482
|
-
case 'ImagePullBackOff':
|
|
2483
|
-
return problemMessages.some(m => m.includes('ImagePull'))
|
|
2484
|
-
case 'OOMKilled':
|
|
2485
|
-
return problemMessages.includes('OOMKilled')
|
|
2486
|
-
case 'Unschedulable':
|
|
2487
|
-
return problemMessages.includes('Unschedulable')
|
|
2488
|
-
case 'Not Ready':
|
|
2489
|
-
return problemMessages.includes('Not Ready') || problemMessages.some(m => m.includes('Probe'))
|
|
2490
|
-
case 'High Restarts':
|
|
2491
|
-
return restarts > 5
|
|
2492
|
-
default:
|
|
2493
|
-
return false
|
|
2494
|
-
}
|
|
2495
|
-
})
|
|
2488
|
+
return filters.some(filter => podMatchesProblemCategory(problems, restarts, filter))
|
|
2496
2489
|
}, [])
|
|
2497
2490
|
|
|
2498
2491
|
|
|
@@ -2522,9 +2515,17 @@ export function ResourcesView({
|
|
|
2522
2515
|
)
|
|
2523
2516
|
}
|
|
2524
2517
|
|
|
2525
|
-
// Apply problem filters
|
|
2526
|
-
if (problemFilters.length > 0
|
|
2527
|
-
|
|
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
|
+
}
|
|
2528
2529
|
}
|
|
2529
2530
|
|
|
2530
2531
|
// Apply inactive ReplicaSet filter (default: hide inactive)
|
|
@@ -2798,15 +2799,12 @@ export function ResourcesView({
|
|
|
2798
2799
|
|
|
2799
2800
|
for (const pod of resources) {
|
|
2800
2801
|
const podProblems = getPodProblems(pod)
|
|
2801
|
-
const msgs = podProblems.map(p => p.message)
|
|
2802
2802
|
const restarts = getPodRestarts(pod)
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
if (msgs.includes('Not Ready') || msgs.some(m => m.includes('Probe'))) problemCounts['Not Ready']++
|
|
2809
|
-
if (restarts > 5) problemCounts['High Restarts']++
|
|
2803
|
+
for (const category of POD_PROBLEMS) {
|
|
2804
|
+
if (podMatchesProblemCategory(podProblems, restarts, category)) {
|
|
2805
|
+
problemCounts[category]++
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2810
2808
|
}
|
|
2811
2809
|
|
|
2812
2810
|
const activeProblems = POD_PROBLEMS
|
|
@@ -2817,6 +2815,28 @@ export function ResourcesView({
|
|
|
2817
2815
|
}
|
|
2818
2816
|
}
|
|
2819
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
|
+
|
|
2820
2840
|
// Compute available labels for label filtering
|
|
2821
2841
|
const labelCounts: Record<string, number> = {}
|
|
2822
2842
|
for (const r of resources) {
|
|
@@ -3157,6 +3177,16 @@ export function ResourcesView({
|
|
|
3157
3177
|
: <RefreshCw className={clsx('w-4 h-4', refreshPhase === 'spinning' && 'animate-spin')} />
|
|
3158
3178
|
}
|
|
3159
3179
|
</button>
|
|
3180
|
+
{onCreateResource && (
|
|
3181
|
+
<Tooltip content={`Create ${selectedKind.kind || 'resource'}`}>
|
|
3182
|
+
<button
|
|
3183
|
+
onClick={() => onCreateResource(selectedKind)}
|
|
3184
|
+
className="p-2 hover:bg-theme-elevated rounded-lg text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
3185
|
+
>
|
|
3186
|
+
<Plus className="w-4 h-4" />
|
|
3187
|
+
</button>
|
|
3188
|
+
</Tooltip>
|
|
3189
|
+
)}
|
|
3160
3190
|
</div>
|
|
3161
3191
|
|
|
3162
3192
|
{/* Table */}
|
|
@@ -4007,12 +4037,21 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
4007
4037
|
const status = getPodStatus(resource)
|
|
4008
4038
|
const problems = getPodProblems(resource)
|
|
4009
4039
|
return (
|
|
4010
|
-
<div className="flex items-center gap-2">
|
|
4011
|
-
<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}>
|
|
4012
4042
|
{status.text}
|
|
4013
4043
|
</span>
|
|
4014
|
-
{problems.length >
|
|
4015
|
-
<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
|
+
}>
|
|
4016
4055
|
<span className="text-red-400">
|
|
4017
4056
|
<AlertTriangle className="w-3.5 h-3.5" />
|
|
4018
4057
|
</span>
|
|
@@ -4087,7 +4126,7 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
4087
4126
|
}
|
|
4088
4127
|
}
|
|
4089
4128
|
|
|
4090
|
-
function WorkloadCell({ resource, column }: { resource: any; kind: string; column: string }) {
|
|
4129
|
+
function WorkloadCell({ resource, column, kind }: { resource: any; kind: string; column: string }) {
|
|
4091
4130
|
const status = resource.status || {}
|
|
4092
4131
|
const spec = resource.spec || {}
|
|
4093
4132
|
|
|
@@ -4096,13 +4135,32 @@ function WorkloadCell({ resource, column }: { resource: any; kind: string; colum
|
|
|
4096
4135
|
const desired = spec.replicas ?? 0
|
|
4097
4136
|
const ready = status.readyReplicas || 0
|
|
4098
4137
|
const allReady = ready === desired && desired > 0
|
|
4138
|
+
const problems = getWorkloadProblems(resource, kind)
|
|
4099
4139
|
return (
|
|
4100
|
-
<
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
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>
|
|
4106
4164
|
)
|
|
4107
4165
|
}
|
|
4108
4166
|
case 'upToDate':
|
|
@@ -4153,13 +4211,32 @@ function DaemonSetCell({ resource, column }: { resource: any; column: string })
|
|
|
4153
4211
|
const desired = status.desiredNumberScheduled || 0
|
|
4154
4212
|
const ready = status.numberReady || 0
|
|
4155
4213
|
const allReady = ready === desired && desired > 0
|
|
4214
|
+
const problems = getWorkloadProblems(resource, 'daemonsets')
|
|
4156
4215
|
return (
|
|
4157
|
-
<
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
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>
|
|
4163
4240
|
)
|
|
4164
4241
|
}
|
|
4165
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>
|