@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Package, ChevronDown, ChevronRight } from 'lucide-react'
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { Package, ChevronDown, ChevronRight, Search } from 'lucide-react'
|
|
3
3
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
4
|
import { formatAge } from '../resource-utils'
|
|
5
5
|
import { formatTrivyImage } from './trivy-shared'
|
|
@@ -13,6 +13,7 @@ const INITIAL_SHOW_COUNT = 100
|
|
|
13
13
|
export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
14
14
|
const [showAll, setShowAll] = useState(false)
|
|
15
15
|
const [expanded, setExpanded] = useState(true)
|
|
16
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
16
17
|
|
|
17
18
|
const report = data.report || {}
|
|
18
19
|
const scanner = report.scanner || {}
|
|
@@ -29,7 +30,18 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
29
30
|
const bomFormat = components.bomFormat || '-'
|
|
30
31
|
const specVersion = components.specVersion || '-'
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const filteredBom = useMemo(() => {
|
|
34
|
+
if (!searchTerm) return bom
|
|
35
|
+
const term = searchTerm.toLowerCase()
|
|
36
|
+
return bom.filter((comp: any) =>
|
|
37
|
+
(comp.name || '').toLowerCase().includes(term) ||
|
|
38
|
+
(comp.purl || '').toLowerCase().includes(term) ||
|
|
39
|
+
(comp.type || '').toLowerCase().includes(term)
|
|
40
|
+
)
|
|
41
|
+
}, [bom, searchTerm])
|
|
42
|
+
|
|
43
|
+
const hasActiveFilter = searchTerm !== ''
|
|
44
|
+
const displayedComponents = showAll ? filteredBom : filteredBom.slice(0, INITIAL_SHOW_COUNT)
|
|
33
45
|
|
|
34
46
|
return (
|
|
35
47
|
<>
|
|
@@ -58,6 +70,30 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
58
70
|
</button>
|
|
59
71
|
{expanded && (
|
|
60
72
|
<div className="overflow-x-auto -mx-1">
|
|
73
|
+
{/* Search */}
|
|
74
|
+
<div className="flex items-center gap-2 mb-2 px-1">
|
|
75
|
+
<div className="relative flex-1">
|
|
76
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
placeholder="Filter by name, type, or purl..."
|
|
80
|
+
value={searchTerm}
|
|
81
|
+
onChange={(e) => { setSearchTerm(e.target.value); setShowAll(false) }}
|
|
82
|
+
className="w-full pl-6 pr-2 py-1 text-xs bg-theme-bg border border-theme-border rounded text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
{hasActiveFilter && (
|
|
87
|
+
<div className="flex items-center gap-1 mb-2 px-1 text-xs text-theme-text-tertiary">
|
|
88
|
+
Showing {filteredBom.length} of {bom.length}
|
|
89
|
+
<button
|
|
90
|
+
onClick={() => { setSearchTerm(''); setShowAll(false) }}
|
|
91
|
+
className="ml-1 text-blue-400 hover:text-blue-300 hover:underline"
|
|
92
|
+
>
|
|
93
|
+
Clear
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
61
97
|
<table className="w-full text-xs">
|
|
62
98
|
<thead>
|
|
63
99
|
<tr className="border-b border-theme-border text-theme-text-tertiary">
|
|
@@ -81,12 +117,17 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
81
117
|
})}
|
|
82
118
|
</tbody>
|
|
83
119
|
</table>
|
|
84
|
-
{
|
|
120
|
+
{displayedComponents.length === 0 && (
|
|
121
|
+
<div className="py-4 text-center text-xs text-theme-text-tertiary">
|
|
122
|
+
No components match the current filter.
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
{!showAll && filteredBom.length > INITIAL_SHOW_COUNT && (
|
|
85
126
|
<button
|
|
86
127
|
onClick={() => setShowAll(true)}
|
|
87
128
|
className="mt-2 text-xs text-blue-400 hover:text-blue-300 hover:underline"
|
|
88
129
|
>
|
|
89
|
-
Show all {
|
|
130
|
+
Show all {filteredBom.length} components
|
|
90
131
|
</button>
|
|
91
132
|
)}
|
|
92
133
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Lock, Key, FileText } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
|
|
5
5
|
interface SealedSecretRendererProps {
|
|
6
6
|
data: any
|
|
7
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function getScope(annotations: Record<string, string> | undefined): string {
|
|
@@ -13,7 +14,7 @@ function getScope(annotations: Record<string, string> | undefined): string {
|
|
|
13
14
|
return 'strict'
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function SealedSecretRenderer({ data }: SealedSecretRendererProps) {
|
|
17
|
+
export function SealedSecretRenderer({ data, onNavigate }: SealedSecretRendererProps) {
|
|
17
18
|
const spec = data.spec || {}
|
|
18
19
|
const status = data.status || {}
|
|
19
20
|
const conditions = status.conditions || []
|
|
@@ -65,6 +66,14 @@ export function SealedSecretRenderer({ data }: SealedSecretRendererProps) {
|
|
|
65
66
|
</span>
|
|
66
67
|
}
|
|
67
68
|
/>
|
|
69
|
+
<Property label="Target Secret" value={
|
|
70
|
+
<ResourceLink
|
|
71
|
+
name={data.spec?.template?.metadata?.name || data.metadata?.name || ''}
|
|
72
|
+
kind="secrets"
|
|
73
|
+
namespace={data.metadata?.namespace || ''}
|
|
74
|
+
onNavigate={onNavigate}
|
|
75
|
+
/>
|
|
76
|
+
} />
|
|
68
77
|
<Property label="Secret Type" value={secretType} />
|
|
69
78
|
<Property label="Scope" value={scope} />
|
|
70
79
|
<Property label="Observed Gen" value={status.observedGeneration} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Radio } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection,
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, LabelSelectorDisplay } from '../../ui/drawer-components'
|
|
3
3
|
import {
|
|
4
4
|
getServiceMonitorEndpoints,
|
|
5
5
|
getServiceMonitorJobLabel,
|
|
@@ -12,7 +12,7 @@ interface ServiceMonitorRendererProps {
|
|
|
12
12
|
|
|
13
13
|
export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
|
|
14
14
|
const endpoints = getServiceMonitorEndpoints(data)
|
|
15
|
-
const selector = data.spec?.selector
|
|
15
|
+
const selector = data.spec?.selector
|
|
16
16
|
const conditions = data.status?.conditions
|
|
17
17
|
|
|
18
18
|
return (
|
|
@@ -53,11 +53,9 @@ export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
|
|
|
53
53
|
</Section>
|
|
54
54
|
)}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
</Section>
|
|
60
|
-
)}
|
|
56
|
+
<Section title="Selector">
|
|
57
|
+
<LabelSelectorDisplay selector={selector} emptyText="No selector" />
|
|
58
|
+
</Section>
|
|
61
59
|
|
|
62
60
|
<ConditionsSection conditions={conditions} />
|
|
63
61
|
</>
|
|
@@ -16,11 +16,10 @@ export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: Serv
|
|
|
16
16
|
const namespace = data.metadata?.namespace
|
|
17
17
|
const serviceName = data.metadata?.name
|
|
18
18
|
|
|
19
|
-
// Check for issues
|
|
20
19
|
const isLoadBalancer = spec.type === 'LoadBalancer'
|
|
20
|
+
const isExternalName = spec.type === 'ExternalName'
|
|
21
21
|
const lbPending = isLoadBalancer && lbIngress.length === 0
|
|
22
22
|
const hasNoSelector = !spec.selector || Object.keys(spec.selector).length === 0
|
|
23
|
-
const isExternalName = spec.type === 'ExternalName'
|
|
24
23
|
|
|
25
24
|
return (
|
|
26
25
|
<>
|
|
@@ -46,47 +45,60 @@ export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: Serv
|
|
|
46
45
|
<Section title="Service" icon={Globe}>
|
|
47
46
|
<PropertyList>
|
|
48
47
|
<Property label="Type" value={spec.type || 'ClusterIP'} />
|
|
49
|
-
|
|
48
|
+
{isExternalName ? (
|
|
49
|
+
<Property label="External Name" value={spec.externalName} copyable onCopy={onCopy} copied={copied} />
|
|
50
|
+
) : (
|
|
51
|
+
<Property label="Cluster IP" value={spec.clusterIP} copyable onCopy={onCopy} copied={copied} />
|
|
52
|
+
)}
|
|
50
53
|
{spec.externalIPs?.length > 0 && (
|
|
51
54
|
<Property label="External IPs" value={spec.externalIPs.join(', ')} copyable onCopy={onCopy} copied={copied} />
|
|
52
55
|
)}
|
|
53
|
-
{lbIngress.
|
|
56
|
+
{lbIngress.map((ing: any, i: number) => (
|
|
54
57
|
<Property
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
key={i}
|
|
59
|
+
label={lbIngress.length > 1 ? `Load Balancer ${i + 1}` : 'Load Balancer'}
|
|
60
|
+
value={ing.ip || ing.hostname}
|
|
57
61
|
copyable
|
|
58
62
|
onCopy={onCopy}
|
|
59
63
|
copied={copied}
|
|
60
64
|
/>
|
|
61
|
-
)}
|
|
65
|
+
))}
|
|
62
66
|
<Property label="Session Affinity" value={spec.sessionAffinity} />
|
|
63
67
|
<Property label="External Traffic" value={spec.externalTrafficPolicy} />
|
|
68
|
+
<Property label="Internal Traffic" value={spec.internalTrafficPolicy} />
|
|
69
|
+
{spec.ipFamilyPolicy && <Property label="IP Family Policy" value={spec.ipFamilyPolicy} />}
|
|
70
|
+
{spec.ipFamilies?.length > 0 && <Property label="IP Families" value={spec.ipFamilies.join(', ')} />}
|
|
64
71
|
</PropertyList>
|
|
65
72
|
</Section>
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<div className="
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
{ports.length > 0 && (
|
|
75
|
+
<Section title="Ports" defaultExpanded>
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
{ports.map((port: any, i: number) => (
|
|
78
|
+
<div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
|
|
79
|
+
<div className="flex items-center justify-between">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
|
|
82
|
+
<span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
{renderPortAction?.({
|
|
86
|
+
namespace,
|
|
87
|
+
serviceName,
|
|
88
|
+
port: port.port,
|
|
89
|
+
protocol: port.protocol || 'TCP',
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="text-xs text-theme-text-secondary mt-1">
|
|
94
|
+
{port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
|
|
95
|
+
{port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
|
|
80
96
|
</div>
|
|
81
97
|
</div>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</div>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
</Section>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</Section>
|
|
101
|
+
)}
|
|
90
102
|
|
|
91
103
|
{spec.selector && (
|
|
92
104
|
<Section title="Selector">
|
|
@@ -56,7 +56,7 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
56
56
|
<AlertBanner
|
|
57
57
|
variant="error"
|
|
58
58
|
title={isFailed ? 'Backup Failed' : 'Backup Partially Failed'}
|
|
59
|
-
message={`${errors} error(s) occurred during backup.`}
|
|
59
|
+
message={status.failureReason || `${errors} error(s) occurred during backup.`}
|
|
60
60
|
items={validationErrors.length > 0 ? validationErrors : undefined}
|
|
61
61
|
/>
|
|
62
62
|
)}
|
|
@@ -84,8 +84,16 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
|
|
|
84
84
|
)}
|
|
85
85
|
<Property label="Duration" value={getBackupDuration(data)} />
|
|
86
86
|
<Property label="Expiration" value={getBackupExpiry(data)} />
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
<Property label="Errors" value={
|
|
88
|
+
errors > 0
|
|
89
|
+
? <span className="text-red-500 dark:text-red-400 font-medium">{errors}</span>
|
|
90
|
+
: '0'
|
|
91
|
+
} />
|
|
92
|
+
<Property label="Warnings" value={
|
|
93
|
+
warnings > 0
|
|
94
|
+
? <span className="text-amber-500 dark:text-amber-400 font-medium">{warnings}</span>
|
|
95
|
+
: '0'
|
|
96
|
+
} />
|
|
89
97
|
</PropertyList>
|
|
90
98
|
</Section>
|
|
91
99
|
|
|
@@ -49,7 +49,7 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
49
49
|
<AlertBanner
|
|
50
50
|
variant="error"
|
|
51
51
|
title={isFailed ? 'Restore Failed' : 'Restore Partially Failed'}
|
|
52
|
-
message={`${errors} error(s) occurred during restore.`}
|
|
52
|
+
message={status.failureReason || `${errors} error(s) occurred during restore.`}
|
|
53
53
|
/>
|
|
54
54
|
)}
|
|
55
55
|
{warnings > 0 && !isFailed && (
|
|
@@ -76,8 +76,16 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
|
|
|
76
76
|
<Property label="Completed" value={formatAge(status.completionTimestamp) + ' ago'} />
|
|
77
77
|
)}
|
|
78
78
|
<Property label="Duration" value={getRestoreDuration(data)} />
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
<Property label="Errors" value={
|
|
80
|
+
errors > 0
|
|
81
|
+
? <span className="text-red-500 dark:text-red-400 font-medium">{errors}</span>
|
|
82
|
+
: '0'
|
|
83
|
+
} />
|
|
84
|
+
<Property label="Warnings" value={
|
|
85
|
+
warnings > 0
|
|
86
|
+
? <span className="text-amber-500 dark:text-amber-400 font-medium">{warnings}</span>
|
|
87
|
+
: '0'
|
|
88
|
+
} />
|
|
81
89
|
</PropertyList>
|
|
82
90
|
</Section>
|
|
83
91
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Shield } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, AlertBanner, LabelSelectorDisplay } from '../../ui/drawer-components'
|
|
4
4
|
|
|
5
5
|
interface WebhookConfigRendererProps {
|
|
6
6
|
data: any
|
|
@@ -65,8 +65,8 @@ export function WebhookConfigRenderer({ data, isMutating }: WebhookConfigRendere
|
|
|
65
65
|
? `URL: ${url}`
|
|
66
66
|
: 'No target'
|
|
67
67
|
|
|
68
|
-
const
|
|
69
|
-
const
|
|
68
|
+
const nsSelector = wh.namespaceSelector
|
|
69
|
+
const objSelector = wh.objectSelector
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
72
|
<div key={i} className="card-inner-lg">
|
|
@@ -119,26 +119,18 @@ export function WebhookConfigRenderer({ data, isMutating }: WebhookConfigRendere
|
|
|
119
119
|
)}
|
|
120
120
|
|
|
121
121
|
{/* Selectors */}
|
|
122
|
-
{(
|
|
122
|
+
{(nsSelector || objSelector) && (
|
|
123
123
|
<div className="mt-2 space-y-1">
|
|
124
|
-
{
|
|
124
|
+
{nsSelector && (
|
|
125
125
|
<div>
|
|
126
126
|
<div className="text-xs text-theme-text-tertiary mb-1">Namespace Selector</div>
|
|
127
|
-
<
|
|
128
|
-
{Object.entries(nsLabels).map(([k, v]) => (
|
|
129
|
-
<span key={k} className="badge-sm bg-theme-elevated text-theme-text-secondary">{k}={String(v)}</span>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
127
|
+
<LabelSelectorDisplay selector={nsSelector} />
|
|
132
128
|
</div>
|
|
133
129
|
)}
|
|
134
|
-
{
|
|
130
|
+
{objSelector && (
|
|
135
131
|
<div>
|
|
136
132
|
<div className="text-xs text-theme-text-tertiary mb-1">Object Selector</div>
|
|
137
|
-
<
|
|
138
|
-
{Object.entries(objLabels).map(([k, v]) => (
|
|
139
|
-
<span key={k} className="badge-sm bg-theme-elevated text-theme-text-secondary">{k}={String(v)}</span>
|
|
140
|
-
))}
|
|
141
|
-
</div>
|
|
133
|
+
<LabelSelectorDisplay selector={objSelector} />
|
|
142
134
|
</div>
|
|
143
135
|
)}
|
|
144
136
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Play, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
|
1
|
+
import { Play, Clock, CheckCircle, XCircle, Loader2, SkipForward, PauseCircle } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
4
|
import { formatAge, formatDuration } from '../resource-utils'
|
|
@@ -13,6 +13,8 @@ interface WorkflowStep {
|
|
|
13
13
|
phase: string
|
|
14
14
|
startedAt: string | null
|
|
15
15
|
finishedAt: string | null
|
|
16
|
+
message: string | null
|
|
17
|
+
nodeType: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function getStepDuration(step: WorkflowStep): string | null {
|
|
@@ -22,7 +24,13 @@ function getStepDuration(step: WorkflowStep): string | null {
|
|
|
22
24
|
return formatDuration(end.getTime() - start.getTime(), true)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
function StepStatusIcon({ phase }: { phase: string }) {
|
|
27
|
+
function StepStatusIcon({ phase, nodeType }: { phase: string; nodeType?: string }) {
|
|
28
|
+
if (nodeType === 'Skipped' || phase === 'Skipped') {
|
|
29
|
+
return <SkipForward className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
30
|
+
}
|
|
31
|
+
if (nodeType === 'Suspend' && phase !== 'Succeeded') {
|
|
32
|
+
return <PauseCircle className="w-4 h-4 text-yellow-400 shrink-0" />
|
|
33
|
+
}
|
|
26
34
|
switch (phase) {
|
|
27
35
|
case 'Succeeded':
|
|
28
36
|
return <CheckCircle className="w-4 h-4 text-green-400 shrink-0" />
|
|
@@ -46,6 +54,9 @@ function getPhaseBadgeClass(phase: string): string {
|
|
|
46
54
|
return 'status-unhealthy'
|
|
47
55
|
case 'Pending':
|
|
48
56
|
return 'status-degraded'
|
|
57
|
+
case 'Skipped':
|
|
58
|
+
case 'Omitted':
|
|
59
|
+
return 'status-unknown'
|
|
49
60
|
default:
|
|
50
61
|
return 'status-unknown'
|
|
51
62
|
}
|
|
@@ -74,29 +85,39 @@ function getWorkflowProblems(data: any): string[] {
|
|
|
74
85
|
problems.push(status.message || 'Workflow error')
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
// Check for failed nodes
|
|
88
|
+
// Check for failed nodes — include error messages when available
|
|
78
89
|
const nodes = status.nodes || {}
|
|
79
|
-
const
|
|
80
|
-
.filter((node: any) => node.type === 'Pod' && node.phase === 'Failed')
|
|
81
|
-
.map((node: any) => node.displayName)
|
|
90
|
+
const failedNodes = Object.values(nodes)
|
|
91
|
+
.filter((node: any) => (node.type === 'Pod' || node.type === 'Suspend' || node.type === 'Skipped') && (node.phase === 'Failed' || node.phase === 'Error'))
|
|
82
92
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
93
|
+
if (failedNodes.length > 0) {
|
|
94
|
+
const withMessages = failedNodes.filter((node: any) => node.message)
|
|
95
|
+
if (withMessages.length > 0) {
|
|
96
|
+
for (const n of withMessages as any[]) {
|
|
97
|
+
problems.push(`${n.displayName}: ${n.message}`)
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
problems.push(`Failed steps: ${failedNodes.map((n: any) => n.displayName).join(', ')}`)
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
return problems
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
const VISIBLE_NODE_TYPES = new Set(['Pod', 'Skipped', 'Suspend'])
|
|
108
|
+
|
|
90
109
|
function extractSteps(data: any): WorkflowStep[] {
|
|
91
110
|
const nodes = data.status?.nodes || {}
|
|
92
111
|
const steps: WorkflowStep[] = Object.entries(nodes)
|
|
93
|
-
.filter(([, node]: [string, any]) => node.type
|
|
112
|
+
.filter(([, node]: [string, any]) => VISIBLE_NODE_TYPES.has(node.type))
|
|
94
113
|
.map(([id, node]: [string, any]) => ({
|
|
95
114
|
id,
|
|
96
115
|
displayName: node.displayName || id,
|
|
97
|
-
phase: node.phase || 'Pending',
|
|
116
|
+
phase: node.phase || (node.type === 'Skipped' ? 'Skipped' : 'Pending'),
|
|
98
117
|
startedAt: node.startedAt || null,
|
|
99
118
|
finishedAt: node.finishedAt || null,
|
|
119
|
+
message: node.message || null,
|
|
120
|
+
nodeType: node.type,
|
|
100
121
|
}))
|
|
101
122
|
|
|
102
123
|
steps.sort((a, b) => {
|
|
@@ -163,9 +184,22 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
163
184
|
{status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
|
|
164
185
|
<Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
|
|
165
186
|
{templateName && <Property label="Template" value={templateName} />}
|
|
187
|
+
{status.progress && (
|
|
188
|
+
<Property label="Progress" value={status.progress} />
|
|
189
|
+
)}
|
|
166
190
|
{estimatedDuration != null && (
|
|
167
191
|
<Property label="Estimated Duration" value={formatEstimatedDuration(estimatedDuration)} />
|
|
168
192
|
)}
|
|
193
|
+
{status.resourcesDuration && (
|
|
194
|
+
<Property
|
|
195
|
+
label="Resource Usage"
|
|
196
|
+
value={
|
|
197
|
+
Object.entries(status.resourcesDuration as Record<string, number>)
|
|
198
|
+
.map(([resource, seconds]) => `${resource}: ${seconds}s`)
|
|
199
|
+
.join(', ')
|
|
200
|
+
}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
169
203
|
</PropertyList>
|
|
170
204
|
</Section>
|
|
171
205
|
|
|
@@ -173,13 +207,33 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
173
207
|
{steps.length > 0 && (
|
|
174
208
|
<Section title={`Steps (${steps.length})`} defaultExpanded>
|
|
175
209
|
<div className="space-y-1.5">
|
|
176
|
-
{steps.map(step =>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
210
|
+
{steps.map(step => {
|
|
211
|
+
const isFailed = step.phase === 'Failed' || step.phase === 'Error'
|
|
212
|
+
const isSkipped = step.nodeType === 'Skipped' || step.phase === 'Skipped'
|
|
213
|
+
const isSuspend = step.nodeType === 'Suspend'
|
|
214
|
+
return (
|
|
215
|
+
<div key={step.id} className={clsx(
|
|
216
|
+
'text-sm card-inner px-3 py-2',
|
|
217
|
+
isFailed && 'border-l-2 border-red-500'
|
|
218
|
+
)}>
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<StepStatusIcon phase={step.phase} nodeType={step.nodeType} />
|
|
221
|
+
<span className={clsx(
|
|
222
|
+
'flex-1',
|
|
223
|
+
isSkipped ? 'text-theme-text-tertiary' : isSuspend ? 'text-yellow-400' : 'text-theme-text-primary'
|
|
224
|
+
)}>
|
|
225
|
+
{step.displayName}
|
|
226
|
+
{isSkipped && <span className="ml-1 text-xs text-theme-text-tertiary">(skipped)</span>}
|
|
227
|
+
{isSuspend && <span className="ml-1 text-xs text-yellow-400/70">(suspend)</span>}
|
|
228
|
+
</span>
|
|
229
|
+
<span className="text-xs text-theme-text-secondary">{getStepDuration(step) || '-'}</span>
|
|
230
|
+
</div>
|
|
231
|
+
{isFailed && step.message && (
|
|
232
|
+
<div className="text-xs text-red-400 mt-1 ml-6 break-all">{step.message}</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
})}
|
|
183
237
|
</div>
|
|
184
238
|
</Section>
|
|
185
239
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Play, FileText, Key, Lock } from 'lucide-react'
|
|
1
|
+
import { Play, FileText, Key, Lock, File, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
3
3
|
|
|
4
4
|
interface WorkflowTemplateRendererProps {
|
|
@@ -44,6 +44,11 @@ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps
|
|
|
44
44
|
{templates.map((template: any) => {
|
|
45
45
|
const type = getTemplateType(template)
|
|
46
46
|
const image = getTemplateImage(template)
|
|
47
|
+
const inputParams: Array<{ name: string }> = template.inputs?.parameters || []
|
|
48
|
+
const outputParams: Array<{ name: string }> = template.outputs?.parameters || []
|
|
49
|
+
const outputArtifacts: Array<{ name: string }> = template.outputs?.artifacts || []
|
|
50
|
+
const inputArtifacts: Array<{ name: string }> = template.inputs?.artifacts || []
|
|
51
|
+
const hasIO = inputParams.length > 0 || outputParams.length > 0 || outputArtifacts.length > 0 || inputArtifacts.length > 0
|
|
47
52
|
return (
|
|
48
53
|
<div key={template.name} className="card-inner px-3 py-2 text-sm">
|
|
49
54
|
<div className="font-medium text-theme-text-primary">{template.name}</div>
|
|
@@ -53,6 +58,46 @@ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps
|
|
|
53
58
|
{image}
|
|
54
59
|
</div>
|
|
55
60
|
)}
|
|
61
|
+
{hasIO && (
|
|
62
|
+
<div className="mt-2 space-y-1.5">
|
|
63
|
+
{(inputParams.length > 0 || inputArtifacts.length > 0) && (
|
|
64
|
+
<div className="flex items-start gap-1.5">
|
|
65
|
+
<ArrowDownToLine className="w-3 h-3 text-theme-text-tertiary mt-0.5 shrink-0" />
|
|
66
|
+
<div className="flex flex-wrap gap-1">
|
|
67
|
+
{inputParams.map((p) => (
|
|
68
|
+
<span key={p.name} className="badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
69
|
+
{p.name}
|
|
70
|
+
</span>
|
|
71
|
+
))}
|
|
72
|
+
{inputArtifacts.map((a) => (
|
|
73
|
+
<span key={a.name} className="badge-sm bg-theme-elevated text-theme-text-secondary flex items-center gap-0.5">
|
|
74
|
+
<File className="w-2.5 h-2.5" />
|
|
75
|
+
{a.name}
|
|
76
|
+
</span>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{(outputParams.length > 0 || outputArtifacts.length > 0) && (
|
|
82
|
+
<div className="flex items-start gap-1.5">
|
|
83
|
+
<ArrowUpFromLine className="w-3 h-3 text-theme-text-tertiary mt-0.5 shrink-0" />
|
|
84
|
+
<div className="flex flex-wrap gap-1">
|
|
85
|
+
{outputParams.map((p) => (
|
|
86
|
+
<span key={p.name} className="badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
87
|
+
{p.name}
|
|
88
|
+
</span>
|
|
89
|
+
))}
|
|
90
|
+
{outputArtifacts.map((a) => (
|
|
91
|
+
<span key={a.name} className="badge-sm bg-theme-elevated text-theme-text-secondary flex items-center gap-0.5">
|
|
92
|
+
<File className="w-2.5 h-2.5" />
|
|
93
|
+
{a.name}
|
|
94
|
+
</span>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
56
101
|
</div>
|
|
57
102
|
)
|
|
58
103
|
})}
|
|
@@ -62,6 +62,9 @@ export * from './kyverno-cells'
|
|
|
62
62
|
export * from './KyvernoPolicyReportRenderer'
|
|
63
63
|
export * from './LeaseRenderer'
|
|
64
64
|
export * from './NetworkPolicyRenderer'
|
|
65
|
+
export * from './NetworkPolicyDiagram'
|
|
66
|
+
export * from './CiliumNetworkPolicyRenderer'
|
|
67
|
+
export * from './ClusterNetworkPolicyRenderer'
|
|
65
68
|
export * from './NodeRenderer'
|
|
66
69
|
export * from './OCIRepositoryRenderer'
|
|
67
70
|
export * from './OrderRenderer'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Tooltip } from '../../ui/Tooltip'
|
|
5
5
|
import {
|
|
6
|
+
CAPACITY_TYPE_BADGE,
|
|
6
7
|
getNodePoolStatus,
|
|
7
8
|
getNodePoolNodeClassRef,
|
|
8
9
|
getNodePoolLimits,
|
|
@@ -22,9 +23,11 @@ export function NodePoolCell({ resource, column }: { resource: any; column: stri
|
|
|
22
23
|
case 'status': {
|
|
23
24
|
const status = getNodePoolStatus(resource)
|
|
24
25
|
return (
|
|
25
|
-
<
|
|
26
|
-
{status.
|
|
27
|
-
|
|
26
|
+
<Tooltip content={status.text}>
|
|
27
|
+
<span className={clsx('badge truncate max-w-[140px]', status.color)}>
|
|
28
|
+
{status.text}
|
|
29
|
+
</span>
|
|
30
|
+
</Tooltip>
|
|
28
31
|
)
|
|
29
32
|
}
|
|
30
33
|
case 'nodeClass': {
|
|
@@ -70,6 +73,18 @@ export function NodeClaimCell({ resource, column }: { resource: any; column: str
|
|
|
70
73
|
const pool = getNodeClaimNodePoolRef(resource)
|
|
71
74
|
return <span className="text-sm text-theme-text-secondary">{pool}</span>
|
|
72
75
|
}
|
|
76
|
+
case 'capacityType': {
|
|
77
|
+
const ct = resource.metadata?.labels?.['karpenter.sh/capacity-type'] || '-'
|
|
78
|
+
return (
|
|
79
|
+
<span className={clsx('badge-sm', CAPACITY_TYPE_BADGE[ct] || '')}>
|
|
80
|
+
{ct}
|
|
81
|
+
</span>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
case 'zone': {
|
|
85
|
+
const zone = resource.metadata?.labels?.['topology.kubernetes.io/zone'] || '-'
|
|
86
|
+
return <span className="text-sm text-theme-text-secondary">{zone}</span>
|
|
87
|
+
}
|
|
73
88
|
default:
|
|
74
89
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
75
90
|
}
|