@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
|
@@ -603,10 +603,17 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick }: ActivityCar
|
|
|
603
603
|
{item.owner && (
|
|
604
604
|
<span className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
605
605
|
<span className="text-xs text-theme-text-quaternary">←</span>
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
606
|
+
{item.owner.kind === 'ReplicaSet' ? (
|
|
607
|
+
<ResourceRefBadge
|
|
608
|
+
resourceRef={{ kind: 'Deployment', namespace: item.namespace, name: item.owner.name.replace(/-[a-z0-9]+$/, '') }}
|
|
609
|
+
onClick={(ref) => onResourceClick?.(refToSelectedResource(ref))}
|
|
610
|
+
/>
|
|
611
|
+
) : (
|
|
612
|
+
<ResourceRefBadge
|
|
613
|
+
resourceRef={{ kind: item.owner.kind, namespace: item.namespace, name: item.owner.name }}
|
|
614
|
+
onClick={(ref) => onResourceClick?.(refToSelectedResource(ref))}
|
|
615
|
+
/>
|
|
616
|
+
)}
|
|
610
617
|
</span>
|
|
611
618
|
)}
|
|
612
619
|
{item.createdAt && (
|
|
@@ -28,6 +28,53 @@ export function KeyValueBadgeList({ items }: { items: Record<string, unknown> |
|
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Renders a Kubernetes label selector (matchLabels + matchExpressions).
|
|
33
|
+
* Accepts either a full selector object `{ matchLabels?, matchExpressions? }`
|
|
34
|
+
* or a flat `Record<string, string>` (e.g. Service.spec.selector).
|
|
35
|
+
* Pass `inline` for flow-inline rendering (e.g. within a sentence).
|
|
36
|
+
*/
|
|
37
|
+
export function LabelSelectorDisplay({
|
|
38
|
+
selector,
|
|
39
|
+
emptyText = 'All',
|
|
40
|
+
inline = false,
|
|
41
|
+
}: {
|
|
42
|
+
selector: any
|
|
43
|
+
emptyText?: string
|
|
44
|
+
inline?: boolean
|
|
45
|
+
}) {
|
|
46
|
+
if (!selector) {
|
|
47
|
+
return <span className="text-xs text-theme-text-tertiary">{emptyText}</span>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Detect flat selector (e.g. Service.spec.selector has no matchLabels wrapper)
|
|
51
|
+
const isFlat = typeof selector === 'object' && !Array.isArray(selector) && !selector.matchLabels && !selector.matchExpressions
|
|
52
|
+
const matchLabels: Record<string, unknown> = isFlat ? selector : (selector.matchLabels || {})
|
|
53
|
+
const matchExpressions: Array<{ key: string; operator: string; values?: string[] }> = selector.matchExpressions || []
|
|
54
|
+
|
|
55
|
+
const hasLabels = Object.keys(matchLabels).length > 0
|
|
56
|
+
const hasExpressions = matchExpressions.length > 0
|
|
57
|
+
|
|
58
|
+
if (!hasLabels && !hasExpressions) {
|
|
59
|
+
return <span className="text-xs text-theme-text-tertiary">{emptyText}</span>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const Wrapper = inline ? 'span' : 'div'
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Wrapper className={inline ? 'inline-flex flex-wrap gap-1 align-middle' : 'flex flex-wrap gap-1'}>
|
|
66
|
+
{Object.entries(matchLabels).map(([k, v]) => (
|
|
67
|
+
<KeyValueBadge key={`l-${k}`} k={k} v={String(v)} />
|
|
68
|
+
))}
|
|
69
|
+
{matchExpressions.map((expr, i) => (
|
|
70
|
+
<span key={`e-${i}`} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
71
|
+
{expr.key} {expr.operator}{expr.values && expr.values.length > 0 ? ` ${expr.values.join(', ')}` : ''}
|
|
72
|
+
</span>
|
|
73
|
+
))}
|
|
74
|
+
</Wrapper>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
31
78
|
interface SectionProps {
|
|
32
79
|
title: string
|
|
33
80
|
icon?: React.ComponentType<{ className?: string }>
|
|
@@ -234,7 +281,7 @@ export interface Problem {
|
|
|
234
281
|
message: string
|
|
235
282
|
}
|
|
236
283
|
|
|
237
|
-
/** Displays a list of problem alerts (warnings and errors)
|
|
284
|
+
/** Displays a list of problem alerts (warnings and errors) */
|
|
238
285
|
export function ProblemAlerts({ problems }: { problems: Problem[] }) {
|
|
239
286
|
if (problems.length === 0) return null
|
|
240
287
|
|
package/src/theme/variables.css
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
/* ── Text (cool neutral) ── */
|
|
44
44
|
--text-primary: light-dark(#1A1C20, #F5F6F8);
|
|
45
45
|
--text-secondary: light-dark(#555860, #D8DAE0);
|
|
46
|
-
--text-tertiary: light-dark(#
|
|
46
|
+
--text-tertiary: light-dark(#6B7080, #B0B5C0);
|
|
47
47
|
--text-disabled: light-dark(#A0A4AC, #6A7080);
|
|
48
48
|
|
|
49
49
|
/* ── Borders (lightly blue-tinted in light mode) ── */
|
package/src/types/gitops.ts
CHANGED
|
@@ -230,7 +230,9 @@ export interface ArgoAppStatus {
|
|
|
230
230
|
operationState?: {
|
|
231
231
|
phase?: ArgoOperationPhase | string // Known types + allow unknown
|
|
232
232
|
message?: string
|
|
233
|
+
startedAt?: string
|
|
233
234
|
finishedAt?: string
|
|
235
|
+
retryCount?: number
|
|
234
236
|
syncResult?: {
|
|
235
237
|
revision?: string
|
|
236
238
|
source?: ArgoSyncResultSource
|
|
@@ -238,6 +240,12 @@ export interface ArgoAppStatus {
|
|
|
238
240
|
}
|
|
239
241
|
}
|
|
240
242
|
reconciledAt?: string
|
|
243
|
+
history?: Array<{
|
|
244
|
+
revision?: string
|
|
245
|
+
deployedAt?: string
|
|
246
|
+
id?: number
|
|
247
|
+
source?: { repoURL?: string; path?: string }
|
|
248
|
+
}>
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
/**
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { describe, test, expect } from 'vitest'
|
|
2
|
-
import { kindToPlural, pluralToKind, refToSelectedResource } from './navigation'
|
|
1
|
+
import { describe, test, expect, afterEach } from 'vitest'
|
|
2
|
+
import { kindToPlural, pluralToKind, refToSelectedResource, initNavigationMap, resetNavigationMap } from './navigation'
|
|
3
|
+
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
resetNavigationMap()
|
|
6
|
+
})
|
|
3
7
|
|
|
4
8
|
describe('kindToPlural', () => {
|
|
5
9
|
test('singular PascalCase to plural lowercase', () => {
|
|
@@ -110,6 +114,29 @@ describe('pluralToKind', () => {
|
|
|
110
114
|
})
|
|
111
115
|
})
|
|
112
116
|
|
|
117
|
+
describe('initNavigationMap', () => {
|
|
118
|
+
test('discovered API resources override heuristic pluralization', () => {
|
|
119
|
+
// Before init, an unknown CRD would hit the heuristic fallback
|
|
120
|
+
// After init, it uses the discovered plural name
|
|
121
|
+
initNavigationMap([
|
|
122
|
+
{ group: 'external-secrets.io', version: 'v1', kind: 'SecretStore', name: 'secretstores', namespaced: true, isCrd: true, verbs: ['get'] },
|
|
123
|
+
{ group: 'external-secrets.io', version: 'v1', kind: 'ClusterSecretStore', name: 'clustersecretstores', namespaced: false, isCrd: true, verbs: ['get'] },
|
|
124
|
+
])
|
|
125
|
+
expect(kindToPlural('SecretStore')).toBe('secretstores')
|
|
126
|
+
expect(kindToPlural('ClusterSecretStore')).toBe('clustersecretstores')
|
|
127
|
+
expect(pluralToKind('secretstores')).toBe('SecretStore')
|
|
128
|
+
expect(pluralToKind('clustersecretstores')).toBe('ClusterSecretStore')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('prevents double-pluralization of discovered plurals', () => {
|
|
132
|
+
initNavigationMap([
|
|
133
|
+
{ group: 'external-secrets.io', version: 'v1', kind: 'SecretStore', name: 'secretstores', namespaced: true, isCrd: true, verbs: ['get'] },
|
|
134
|
+
])
|
|
135
|
+
// Passing an already-plural kind should be idempotent
|
|
136
|
+
expect(kindToPlural('secretstores')).toBe('secretstores')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
113
140
|
describe('refToSelectedResource', () => {
|
|
114
141
|
test('converts singular kind to plural for navigation', () => {
|
|
115
142
|
const result = refToSelectedResource({
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SelectedResource, ResourceRef } from '../types/core'
|
|
1
|
+
import type { SelectedResource, ResourceRef, APIResource } from '../types/core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Canonical callback type for navigating to a resource.
|
|
@@ -6,9 +6,8 @@ import type { SelectedResource, ResourceRef } from '../types/core'
|
|
|
6
6
|
*/
|
|
7
7
|
export type NavigateToResource = (resource: SelectedResource) => void
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
const PLURAL_TO_KIND: Record<string, string> = {
|
|
9
|
+
// Fallback map for core K8s resources — used before API discovery completes.
|
|
10
|
+
const BUILTIN_PLURAL_TO_KIND: Record<string, string> = {
|
|
12
11
|
pods: 'Pod',
|
|
13
12
|
services: 'Service',
|
|
14
13
|
deployments: 'Deployment',
|
|
@@ -16,11 +15,6 @@ const PLURAL_TO_KIND: Record<string, string> = {
|
|
|
16
15
|
statefulsets: 'StatefulSet',
|
|
17
16
|
replicasets: 'ReplicaSet',
|
|
18
17
|
ingresses: 'Ingress',
|
|
19
|
-
gateways: 'Gateway',
|
|
20
|
-
httproutes: 'HTTPRoute',
|
|
21
|
-
grpcroutes: 'GRPCRoute',
|
|
22
|
-
tcproutes: 'TCPRoute',
|
|
23
|
-
tlsroutes: 'TLSRoute',
|
|
24
18
|
configmaps: 'ConfigMap',
|
|
25
19
|
secrets: 'Secret',
|
|
26
20
|
namespaces: 'Namespace',
|
|
@@ -33,24 +27,44 @@ const PLURAL_TO_KIND: Record<string, string> = {
|
|
|
33
27
|
persistentvolumes: 'PersistentVolume',
|
|
34
28
|
storageclasses: 'StorageClass',
|
|
35
29
|
poddisruptionbudgets: 'PodDisruptionBudget',
|
|
36
|
-
rollouts: 'Rollout',
|
|
37
|
-
applications: 'Application',
|
|
38
|
-
kustomizations: 'Kustomization',
|
|
39
|
-
helmreleases: 'HelmRelease',
|
|
40
|
-
gitrepositories: 'GitRepository',
|
|
41
|
-
certificates: 'Certificate',
|
|
42
30
|
roles: 'Role',
|
|
43
31
|
clusterroles: 'ClusterRole',
|
|
44
32
|
rolebindings: 'RoleBinding',
|
|
45
33
|
clusterrolebindings: 'ClusterRoleBinding',
|
|
46
34
|
serviceaccounts: 'ServiceAccount',
|
|
47
35
|
networkpolicies: 'NetworkPolicy',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Dynamic map built from API discovery — populated by initNavigationMap().
|
|
39
|
+
// Once populated, this is the source of truth for all kind↔plural lookups.
|
|
40
|
+
let discoveredPluralToKind: Record<string, string> | null = null
|
|
41
|
+
let discoveredKindToPlural: Record<string, string> | null = null
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize navigation maps from discovered API resources.
|
|
45
|
+
* Call once when API resources are fetched. After this, kindToPlural/pluralToKind
|
|
46
|
+
* use the real cluster data instead of heuristics.
|
|
47
|
+
*/
|
|
48
|
+
export function initNavigationMap(resources: APIResource[]) {
|
|
49
|
+
const p2k: Record<string, string> = { ...BUILTIN_PLURAL_TO_KIND }
|
|
50
|
+
const k2p: Record<string, string> = {}
|
|
51
|
+
for (const r of resources) {
|
|
52
|
+
const plural = r.name.toLowerCase()
|
|
53
|
+
p2k[plural] = r.kind
|
|
54
|
+
k2p[r.kind.toLowerCase()] = plural
|
|
55
|
+
}
|
|
56
|
+
discoveredPluralToKind = p2k
|
|
57
|
+
discoveredKindToPlural = k2p
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reset navigation maps to builtin-only state. For testing. */
|
|
61
|
+
export function resetNavigationMap() {
|
|
62
|
+
discoveredPluralToKind = null
|
|
63
|
+
discoveredKindToPlural = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPluralToKind(): Record<string, string> {
|
|
67
|
+
return discoveredPluralToKind || BUILTIN_PLURAL_TO_KIND
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
/**
|
|
@@ -61,9 +75,15 @@ const PLURAL_TO_KIND: Record<string, string> = {
|
|
|
61
75
|
*/
|
|
62
76
|
export function kindToPlural(kind: string): string {
|
|
63
77
|
const kindLower = kind.toLowerCase()
|
|
78
|
+
const pluralToKindMap = getPluralToKind()
|
|
64
79
|
|
|
65
80
|
// Already a known plural — return as-is to prevent double-pluralization
|
|
66
|
-
if (kindLower in
|
|
81
|
+
if (kindLower in pluralToKindMap) return kindLower
|
|
82
|
+
|
|
83
|
+
// Lookup from discovered API resources (singular kind → plural name)
|
|
84
|
+
if (discoveredKindToPlural && kindLower in discoveredKindToPlural) {
|
|
85
|
+
return discoveredKindToPlural[kindLower]
|
|
86
|
+
}
|
|
67
87
|
|
|
68
88
|
// Aliases: abbreviations or mappings to a different resource name
|
|
69
89
|
const aliases: Record<string, string> = {
|
|
@@ -73,7 +93,7 @@ export function kindToPlural(kind: string): string {
|
|
|
73
93
|
}
|
|
74
94
|
if (aliases[kindLower]) return aliases[kindLower]
|
|
75
95
|
|
|
76
|
-
// English pluralization rules (covers *Class→*classes, *Policy→*policies, *Repository→*repositories, etc.)
|
|
96
|
+
// Fallback: English pluralization rules (covers *Class→*classes, *Policy→*policies, *Repository→*repositories, etc.)
|
|
77
97
|
if (kindLower.endsWith('s') || kindLower.endsWith('x') || kindLower.endsWith('ch') || kindLower.endsWith('sh')) {
|
|
78
98
|
return kindLower + 'es'
|
|
79
99
|
}
|
|
@@ -90,8 +110,9 @@ export function kindToPlural(kind: string): string {
|
|
|
90
110
|
*/
|
|
91
111
|
export function pluralToKind(plural: string): string {
|
|
92
112
|
const lower = plural.toLowerCase()
|
|
113
|
+
const pluralToKindMap = getPluralToKind()
|
|
93
114
|
|
|
94
|
-
if (
|
|
115
|
+
if (pluralToKindMap[lower]) return pluralToKindMap[lower]
|
|
95
116
|
|
|
96
117
|
// If it already looks like a singular PascalCase kind (starts with uppercase), return as-is
|
|
97
118
|
if (plural[0] === plural[0].toUpperCase() && plural[0] !== plural[0].toLowerCase()) {
|