@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
|
@@ -97,6 +97,8 @@ import {
|
|
|
97
97
|
SealedSecretRenderer,
|
|
98
98
|
WorkflowTemplateRenderer,
|
|
99
99
|
NetworkPolicyRenderer,
|
|
100
|
+
CiliumNetworkPolicyRenderer,
|
|
101
|
+
ClusterNetworkPolicyRenderer,
|
|
100
102
|
PodDisruptionBudgetRenderer,
|
|
101
103
|
ServiceAccountRenderer,
|
|
102
104
|
RoleRenderer,
|
|
@@ -212,7 +214,10 @@ const KNOWN_KINDS = new Set([
|
|
|
212
214
|
'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
|
|
213
215
|
'orders', 'challenges',
|
|
214
216
|
'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates',
|
|
215
|
-
'networkpolicies', '
|
|
217
|
+
'networkpolicies', 'networkpolicy',
|
|
218
|
+
'ciliumnetworkpolicies', 'ciliumnetworkpolicy', 'ciliumclusterwidenetworkpolicies', 'ciliumclusterwidenetworkpolicy',
|
|
219
|
+
'clusternetworkpolicies', 'clusternetworkpolicy',
|
|
220
|
+
'poddisruptionbudgets', 'serviceaccounts',
|
|
216
221
|
'roles', 'clusterroles', 'rolebindings', 'clusterrolebindings',
|
|
217
222
|
'events', 'gitrepositories', 'ocirepositories', 'helmrepositories',
|
|
218
223
|
'kustomizations', 'helmreleases', 'alerts', 'applications',
|
|
@@ -348,9 +353,11 @@ export function ResourceRendererDispatch({
|
|
|
348
353
|
{kind === 'grpcroutes' && <GRPCRouteRenderer data={data} onNavigate={onNavigate} />}
|
|
349
354
|
{kind === 'tcproutes' && <SimpleRouteRenderer data={data} kind="TCPRoute" onNavigate={onNavigate} />}
|
|
350
355
|
{kind === 'tlsroutes' && <SimpleRouteRenderer data={data} kind="TLSRoute" onNavigate={onNavigate} />}
|
|
351
|
-
{kind === 'sealedsecrets' && <SealedSecretRenderer data={data} />}
|
|
356
|
+
{kind === 'sealedsecrets' && <SealedSecretRenderer data={data} onNavigate={onNavigate} />}
|
|
352
357
|
{kind === 'workflowtemplates' && <WorkflowTemplateRenderer data={data} />}
|
|
353
|
-
{kind === 'networkpolicies' && <NetworkPolicyRenderer data={data} />}
|
|
358
|
+
{(kind === 'networkpolicies' || kind === 'networkpolicy') && <NetworkPolicyRenderer data={data} />}
|
|
359
|
+
{(kind === 'ciliumnetworkpolicies' || kind === 'ciliumnetworkpolicy' || kind === 'ciliumclusterwidenetworkpolicies' || kind === 'ciliumclusterwidenetworkpolicy') && <CiliumNetworkPolicyRenderer data={data} />}
|
|
360
|
+
{(kind === 'clusternetworkpolicies' || kind === 'clusternetworkpolicy') && <ClusterNetworkPolicyRenderer data={data} />}
|
|
354
361
|
{kind === 'poddisruptionbudgets' && <PodDisruptionBudgetRenderer data={data} />}
|
|
355
362
|
{kind === 'serviceaccounts' && <ServiceAccountRenderer data={data} />}
|
|
356
363
|
{(kind === 'roles' || kind === 'clusterroles') && <RoleRenderer data={data} />}
|
|
@@ -359,8 +366,8 @@ export function ResourceRendererDispatch({
|
|
|
359
366
|
{kind === 'gitrepositories' && <GitRepositoryRenderer data={data} />}
|
|
360
367
|
{kind === 'ocirepositories' && <OCIRepositoryRenderer data={data} />}
|
|
361
368
|
{kind === 'helmrepositories' && <HelmRepositoryRenderer data={data} />}
|
|
362
|
-
{kind === 'kustomizations' && <KustomizationRenderer data={data} />}
|
|
363
|
-
{kind === 'helmreleases' && <FluxHelmReleaseRenderer data={data} />}
|
|
369
|
+
{kind === 'kustomizations' && <KustomizationRenderer data={data} onNavigate={onNavigate} />}
|
|
370
|
+
{kind === 'helmreleases' && <FluxHelmReleaseRenderer data={data} onNavigate={onNavigate} />}
|
|
364
371
|
{kind === 'alerts' && <AlertRenderer data={data} />}
|
|
365
372
|
{kind === 'applications' && <ArgoApplicationRenderer data={data} />}
|
|
366
373
|
{kind === 'nodepools' && <KarpenterNodePoolRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -388,7 +395,7 @@ export function ResourceRendererDispatch({
|
|
|
388
395
|
{kind === 'backupstoragelocations' && <VeleroBSLRenderer data={data} />}
|
|
389
396
|
{kind === 'volumesnapshotlocations' && <VeleroVSLRenderer data={data} />}
|
|
390
397
|
{kind === 'externalsecrets' && <ExternalSecretRenderer data={data} onNavigate={onNavigate} />}
|
|
391
|
-
{kind === 'clusterexternalsecrets' && <ClusterExternalSecretRenderer data={data} />}
|
|
398
|
+
{kind === 'clusterexternalsecrets' && <ClusterExternalSecretRenderer data={data} onNavigate={onNavigate} />}
|
|
392
399
|
{(kind === 'secretstores' || kind === 'clustersecretstores') && <SecretStoreRenderer data={data} />}
|
|
393
400
|
{kind === 'clusters' && <CNPGClusterRenderer data={data} onNavigate={onNavigate} />}
|
|
394
401
|
{kind === 'scheduledbackups' && <CNPGScheduledBackupRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from './ResourceRendererDispatch'
|
|
2
2
|
export { EditableYamlView, SaveSuccessAnimation } from './EditableYamlView'
|
|
3
3
|
export { ResourceActionsBar, RevisionHistoryDialog } from './ResourceActionsBar'
|
|
4
|
+
export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
|
|
@@ -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 && (
|
|
@@ -302,6 +302,7 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
302
302
|
const canCollapse = isPodGroup && onCollapse && isExpanded
|
|
303
303
|
const statusIssue = nodeData.statusIssue as string | undefined
|
|
304
304
|
const issueTooltip = getIssueTooltip(statusIssue)
|
|
305
|
+
const policyStatus = nodeData.policyStatus as string | undefined
|
|
305
306
|
|
|
306
307
|
// CSS class for icon (replaces Lucide SVG - saves ~5 DOM elements per node)
|
|
307
308
|
const iconClass = `topology-icon topology-icon-${kind.toLowerCase()}`
|
|
@@ -417,6 +418,20 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
417
418
|
)}
|
|
418
419
|
</div>
|
|
419
420
|
|
|
421
|
+
{/* Network Policy coverage indicator */}
|
|
422
|
+
{policyStatus && (
|
|
423
|
+
<Tooltip content={policyStatus === 'protected' ? 'Protected by NetworkPolicy' : 'No NetworkPolicy coverage'} position="right">
|
|
424
|
+
<span className={clsx(
|
|
425
|
+
'inline-flex items-center justify-center w-3.5 h-3.5 rounded-sm text-[8px] font-bold cursor-help',
|
|
426
|
+
policyStatus === 'protected'
|
|
427
|
+
? 'bg-green-500/20 text-green-500 border border-green-500/30'
|
|
428
|
+
: 'bg-yellow-500/20 text-yellow-500 border border-yellow-500/30',
|
|
429
|
+
)}>
|
|
430
|
+
{policyStatus === 'protected' ? '✓' : '!'}
|
|
431
|
+
</span>
|
|
432
|
+
</Tooltip>
|
|
433
|
+
)}
|
|
434
|
+
|
|
420
435
|
{/* Name */}
|
|
421
436
|
<div className="text-sm font-medium text-theme-text-primary truncate pr-1">
|
|
422
437
|
{name}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FolderTree } from 'lucide-react'
|
|
1
|
+
import { FolderTree, ShieldCheck } from 'lucide-react'
|
|
2
2
|
import type { TopologyMode, GroupingMode } from '../../types/core'
|
|
3
3
|
|
|
4
4
|
interface TopologyControlsProps {
|
|
@@ -7,6 +7,8 @@ interface TopologyControlsProps {
|
|
|
7
7
|
groupingMode: GroupingMode
|
|
8
8
|
onGroupingModeChange: (mode: GroupingMode) => void
|
|
9
9
|
showNoGrouping?: boolean
|
|
10
|
+
showPolicyEffect?: boolean
|
|
11
|
+
onShowPolicyEffectChange?: (show: boolean) => void
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function TopologyControls({
|
|
@@ -15,9 +17,27 @@ export function TopologyControls({
|
|
|
15
17
|
groupingMode,
|
|
16
18
|
onGroupingModeChange,
|
|
17
19
|
showNoGrouping = true,
|
|
20
|
+
showPolicyEffect = false,
|
|
21
|
+
onShowPolicyEffectChange,
|
|
18
22
|
}: TopologyControlsProps) {
|
|
19
23
|
return (
|
|
20
24
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
25
|
+
{/* Policy effect toggle */}
|
|
26
|
+
{onShowPolicyEffectChange && (
|
|
27
|
+
<button
|
|
28
|
+
onClick={() => onShowPolicyEffectChange(!showPolicyEffect)}
|
|
29
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg border transition-colors ${
|
|
30
|
+
showPolicyEffect
|
|
31
|
+
? 'bg-indigo-600 text-white border-indigo-600'
|
|
32
|
+
: 'bg-theme-surface/90 backdrop-blur text-theme-text-secondary border-theme-border hover:text-theme-text-primary'
|
|
33
|
+
}`}
|
|
34
|
+
title="Show NetworkPolicy effects on edges"
|
|
35
|
+
>
|
|
36
|
+
<ShieldCheck className="w-3.5 h-3.5" />
|
|
37
|
+
Policies
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
|
|
21
41
|
{/* Grouping selector */}
|
|
22
42
|
<div className="flex items-center gap-1.5 px-2 py-1.5 bg-theme-surface/90 backdrop-blur border border-theme-border rounded-lg">
|
|
23
43
|
<FolderTree className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
@@ -73,7 +73,7 @@ const LARGE_CLUSTER_NS_THRESHOLD = 5
|
|
|
73
73
|
|
|
74
74
|
// Build edges, handling collapsed groups
|
|
75
75
|
function buildEdges(
|
|
76
|
-
topologyEdges:
|
|
76
|
+
topologyEdges: TopologyEdge[],
|
|
77
77
|
collapsedGroups: Set<string>,
|
|
78
78
|
groupMap: Map<string, string[]>,
|
|
79
79
|
groupingMode: GroupingMode,
|
|
@@ -73,6 +73,10 @@ const KIND: Record<string, string> = {
|
|
|
73
73
|
HorizontalPodAutoscaler: 'bg-pink-100 text-pink-800 border-pink-300 dark:bg-pink-950/50 dark:text-pink-300 dark:border-pink-700/40',
|
|
74
74
|
PersistentVolumeClaim: 'bg-cyan-100 text-cyan-800 border-cyan-300 dark:bg-cyan-950/50 dark:text-cyan-400 dark:border-cyan-700/40',
|
|
75
75
|
PodDisruptionBudget: 'bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-950/50 dark:text-orange-300 dark:border-orange-700/40',
|
|
76
|
+
NetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
|
|
77
|
+
CiliumNetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
|
|
78
|
+
CiliumClusterwideNetworkPolicy: 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-700/40',
|
|
79
|
+
ClusterNetworkPolicy: 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950/50 dark:text-violet-300 dark:border-violet-700/40',
|
|
76
80
|
|
|
77
81
|
// Cluster-scoped
|
|
78
82
|
Node: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40',
|
|
@@ -147,6 +151,10 @@ export function getKindColorClass(kind: string): string {
|
|
|
147
151
|
horizontalpodautoscalers: 'HorizontalPodAutoscaler',
|
|
148
152
|
persistentvolumeclaims: 'PersistentVolumeClaim',
|
|
149
153
|
poddisruptionbudgets: 'PodDisruptionBudget',
|
|
154
|
+
networkpolicies: 'NetworkPolicy',
|
|
155
|
+
ciliumnetworkpolicies: 'CiliumNetworkPolicy',
|
|
156
|
+
ciliumclusterwidenetworkpolicies: 'CiliumClusterwideNetworkPolicy',
|
|
157
|
+
clusternetworkpolicies: 'ClusterNetworkPolicy',
|
|
150
158
|
rollouts: 'Rollout', httproutes: 'HTTPRoute', grpcroutes: 'GRPCRoute',
|
|
151
159
|
events: 'Event', helmreleases: 'HelmRelease',
|
|
152
160
|
}
|
|
@@ -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 }>
|
|
@@ -117,7 +164,7 @@ export function Property({ label, value, copyable, onCopy, copied }: PropertyPro
|
|
|
117
164
|
|
|
118
165
|
return (
|
|
119
166
|
<div className="flex items-start gap-2 text-sm">
|
|
120
|
-
<span className="text-theme-text-tertiary w-
|
|
167
|
+
<span className="text-theme-text-tertiary w-40 shrink-0">{label}</span>
|
|
121
168
|
<span className="text-theme-text-primary break-all flex-1">{displayValue}</span>
|
|
122
169
|
{copyable && onCopy && !isReactElement(value) && (
|
|
123
170
|
<button
|
|
@@ -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
|
|
|
@@ -568,9 +615,25 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
568
615
|
{relationships.scalers && relationships.scalers.length > 0 && (
|
|
569
616
|
<RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
|
|
570
617
|
)}
|
|
571
|
-
{relationships.policies && relationships.policies.length > 0 && (
|
|
572
|
-
|
|
573
|
-
|
|
618
|
+
{relationships.policies && relationships.policies.length > 0 && (() => {
|
|
619
|
+
const policyKinds = new Set(['NetworkPolicy', 'CiliumNetworkPolicy', 'CiliumClusterwideNetworkPolicy', 'ClusterNetworkPolicy'])
|
|
620
|
+
const pdbs = relationships.policies.filter(r => r.kind === 'PodDisruptionBudget')
|
|
621
|
+
const netpols = relationships.policies.filter(r => policyKinds.has(r.kind))
|
|
622
|
+
const other = relationships.policies.filter(r => r.kind !== 'PodDisruptionBudget' && !policyKinds.has(r.kind))
|
|
623
|
+
return (
|
|
624
|
+
<>
|
|
625
|
+
{pdbs.length > 0 && (
|
|
626
|
+
<RelationshipGroup label="Disruption Budget" refs={dedupeRefs(pdbs)} onNavigate={onNavigate} />
|
|
627
|
+
)}
|
|
628
|
+
{netpols.length > 0 && (
|
|
629
|
+
<RelationshipGroup label="Network Policies" refs={dedupeRefs(netpols)} onNavigate={onNavigate} />
|
|
630
|
+
)}
|
|
631
|
+
{other.length > 0 && (
|
|
632
|
+
<RelationshipGroup label="Policies" refs={dedupeRefs(other)} onNavigate={onNavigate} />
|
|
633
|
+
)}
|
|
634
|
+
</>
|
|
635
|
+
)
|
|
636
|
+
})()}
|
|
574
637
|
{relationships.scaleTarget && (
|
|
575
638
|
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
576
639
|
)}
|
|
@@ -121,6 +121,12 @@ interface WorkloadViewProps {
|
|
|
121
121
|
renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
122
122
|
/** Whether metrics are available for this resource kind */
|
|
123
123
|
isMetricsAvailable?: (kind: string, resource: any) => boolean
|
|
124
|
+
/** Render extra content at the bottom of the overview tab (e.g. audit findings) */
|
|
125
|
+
renderOverviewExtra?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
126
|
+
|
|
127
|
+
// ── Duplicate ────────────────────────────────────────────────────────────
|
|
128
|
+
/** Duplicate handler — opens create dialog with this resource's YAML */
|
|
129
|
+
onDuplicate?: (params: { kind: string; namespace: string; name: string; yaml: string }) => void
|
|
124
130
|
|
|
125
131
|
// ── ResourceActionsBar props (passed through) ────────────────────────────
|
|
126
132
|
/** All props for the actions bar (forwarded as-is) */
|
|
@@ -166,6 +172,9 @@ export function WorkloadView({
|
|
|
166
172
|
renderLogsTab,
|
|
167
173
|
renderMetricsTab,
|
|
168
174
|
isMetricsAvailable,
|
|
175
|
+
// Duplicate
|
|
176
|
+
onDuplicate,
|
|
177
|
+
renderOverviewExtra,
|
|
169
178
|
// Actions bar
|
|
170
179
|
actionsBarProps,
|
|
171
180
|
// Renderer overrides
|
|
@@ -430,22 +439,30 @@ export function WorkloadView({
|
|
|
430
439
|
onSave={onUpdateResource}
|
|
431
440
|
isSaving={isUpdatingResource}
|
|
432
441
|
saveError={updateResourceError}
|
|
442
|
+
onDuplicate={onDuplicate}
|
|
433
443
|
/>
|
|
434
444
|
) : (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
<>
|
|
446
|
+
<ResourceRendererDispatch
|
|
447
|
+
resource={selectedResource}
|
|
448
|
+
data={resource}
|
|
449
|
+
relationships={relationships}
|
|
450
|
+
certificateInfo={certificateInfo}
|
|
451
|
+
onCopy={copyToClipboard}
|
|
452
|
+
copied={copied}
|
|
453
|
+
onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
|
|
454
|
+
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
455
|
+
isSavingSecret={isUpdatingResource}
|
|
456
|
+
rendererOverrides={rendererOverrides}
|
|
457
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
458
|
+
renderMetrics={renderMetricsTab}
|
|
459
|
+
/>
|
|
460
|
+
{renderOverviewExtra && (
|
|
461
|
+
<div className="px-4 pb-4">
|
|
462
|
+
{renderOverviewExtra({ kind, namespace, name })}
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
</>
|
|
449
466
|
)}
|
|
450
467
|
</div>
|
|
451
468
|
</div>
|
|
@@ -571,21 +588,22 @@ export function WorkloadView({
|
|
|
571
588
|
{/* Tab Content */}
|
|
572
589
|
<div className="flex-1 overflow-hidden relative">
|
|
573
590
|
{activeTab === 'overview' && (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
591
|
+
<InfoTab
|
|
592
|
+
resource={resource}
|
|
593
|
+
selectedResource={selectedResource}
|
|
594
|
+
relationships={relationships}
|
|
595
|
+
isLoading={resourceLoading}
|
|
596
|
+
onNavigate={onNavigateToResource}
|
|
597
|
+
onCopy={copyToClipboard}
|
|
598
|
+
copied={copied}
|
|
599
|
+
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
600
|
+
isSavingSecret={isUpdatingResource}
|
|
601
|
+
onOpenLogs={handleOpenLogs}
|
|
602
|
+
onSwitchToTimeline={() => handleSetTab('timeline')}
|
|
603
|
+
rendererOverrides={rendererOverrides}
|
|
604
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
605
|
+
extraContent={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name })}
|
|
606
|
+
/>
|
|
589
607
|
)}
|
|
590
608
|
{activeTab === 'timeline' && (
|
|
591
609
|
<EventsTab
|
|
@@ -635,6 +653,7 @@ export function WorkloadView({
|
|
|
635
653
|
onSave={onUpdateResource}
|
|
636
654
|
isSaving={isUpdatingResource}
|
|
637
655
|
saveError={updateResourceError}
|
|
656
|
+
onDuplicate={onDuplicate}
|
|
638
657
|
/>
|
|
639
658
|
)}
|
|
640
659
|
</div>
|
|
@@ -1068,6 +1087,7 @@ function InfoTab({
|
|
|
1068
1087
|
onSwitchToTimeline,
|
|
1069
1088
|
rendererOverrides,
|
|
1070
1089
|
resolvedEnvFrom,
|
|
1090
|
+
extraContent,
|
|
1071
1091
|
}: {
|
|
1072
1092
|
resource: any
|
|
1073
1093
|
selectedResource: SelectedResource
|
|
@@ -1082,6 +1102,7 @@ function InfoTab({
|
|
|
1082
1102
|
onSwitchToTimeline?: () => void
|
|
1083
1103
|
rendererOverrides?: RendererOverrides
|
|
1084
1104
|
resolvedEnvFrom?: ResolvedEnvFrom
|
|
1105
|
+
extraContent?: ReactNode
|
|
1085
1106
|
}) {
|
|
1086
1107
|
if (isLoading) {
|
|
1087
1108
|
return (
|
|
@@ -1132,6 +1153,11 @@ function InfoTab({
|
|
|
1132
1153
|
</div>
|
|
1133
1154
|
)}
|
|
1134
1155
|
/>
|
|
1156
|
+
{extraContent && (
|
|
1157
|
+
<div className="px-4 pb-4">
|
|
1158
|
+
{extraContent}
|
|
1159
|
+
</div>
|
|
1160
|
+
)}
|
|
1135
1161
|
</div>
|
|
1136
1162
|
)
|
|
1137
1163
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,3 +33,6 @@ export * from './components/dock'
|
|
|
33
33
|
|
|
34
34
|
// Topology (TopologyGraph, TopologySearch, TopologyFilterSidebar, layout utilities)
|
|
35
35
|
export * from './components/topology'
|
|
36
|
+
|
|
37
|
+
// Cluster audit (AuditCard, AuditAlerts, AuditFindingsTable)
|
|
38
|
+
export * from './components/audit'
|
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/core.ts
CHANGED
|
@@ -145,6 +145,7 @@ export interface TopologyEdge {
|
|
|
145
145
|
type: EdgeType
|
|
146
146
|
label?: string
|
|
147
147
|
skipIfKindVisible?: string // Hide this edge if this kind is visible (for shortcut edges)
|
|
148
|
+
policyEffect?: 'allowed' | 'blocked' | 'unprotected'
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export interface Topology {
|
|
@@ -846,7 +847,7 @@ export interface TrafficFilters {
|
|
|
846
847
|
}
|
|
847
848
|
|
|
848
849
|
// Main view type now includes 'traffic' and 'cost'
|
|
849
|
-
export type ExtendedMainView = MainView | 'traffic' | 'cost'
|
|
850
|
+
export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit'
|
|
850
851
|
|
|
851
852
|
// ============================================================================
|
|
852
853
|
// Image Filesystem Types
|
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
|
/**
|
|
@@ -52,6 +52,13 @@ export const HELM_STATUS_COLORS: Record<string, string> = {
|
|
|
52
52
|
uninstalled: BADGE_SEVERITY_COLORS.neutral,
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Best practices category colors
|
|
56
|
+
export const BP_CATEGORY_BADGE: Record<string, string> = {
|
|
57
|
+
Security: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/40 dark:text-purple-400 dark:border-purple-800/40',
|
|
58
|
+
Reliability: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/40 dark:text-sky-400 dark:border-sky-800/40',
|
|
59
|
+
Efficiency: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800/40',
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Default fallback color
|
|
56
63
|
export const DEFAULT_BADGE_COLOR = 'bg-theme-elevated text-theme-text-secondary'
|
|
57
64
|
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Human-friendly summaries for common Kubernetes API error patterns.
|
|
2
|
+
// Falls back to null when no known pattern matches — caller shows raw error.
|
|
3
|
+
|
|
4
|
+
interface FriendlyError {
|
|
5
|
+
summary: string
|
|
6
|
+
suggestion: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string) => FriendlyError }> = [
|
|
10
|
+
// Admission webhook timeout
|
|
11
|
+
{
|
|
12
|
+
test: (msg) => /failed calling webhook.*context deadline exceeded/i.test(msg),
|
|
13
|
+
friendly: (msg) => {
|
|
14
|
+
const webhook = msg.match(/webhook "([^"]+)"/)?.[1] ?? 'unknown'
|
|
15
|
+
const service = webhook.split('.')[0] || webhook
|
|
16
|
+
return {
|
|
17
|
+
summary: `Admission webhook timed out (${service})`,
|
|
18
|
+
suggestion: 'A cluster validation webhook failed to respond. This is usually a temporary cluster issue, not a problem with your YAML. Try again or contact your cluster admin.',
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
// Admission webhook rejected
|
|
23
|
+
{
|
|
24
|
+
test: (msg) => /admission webhook.*denied the request/i.test(msg),
|
|
25
|
+
friendly: (msg) => {
|
|
26
|
+
const webhook = msg.match(/webhook "([^"]+)"/)?.[1] ?? 'unknown'
|
|
27
|
+
return {
|
|
28
|
+
summary: `Rejected by admission policy (${webhook.split('.')[0]})`,
|
|
29
|
+
suggestion: 'A cluster admission webhook rejected this resource. Check that your YAML meets the cluster\'s policies, or contact your cluster admin.',
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
// Validation error — invalid field value
|
|
34
|
+
{
|
|
35
|
+
test: (msg) => /is invalid:.*Invalid value/i.test(msg),
|
|
36
|
+
friendly: (msg) => {
|
|
37
|
+
const fields = [...msg.matchAll(/(\S+): Invalid value/g)].map(m => m[1])
|
|
38
|
+
const fieldList = fields.length > 0 ? fields.join(', ') : 'unknown field'
|
|
39
|
+
return {
|
|
40
|
+
summary: `Validation failed: invalid value for ${fieldList}`,
|
|
41
|
+
suggestion: 'One or more field values don\'t match what Kubernetes expects. Check the highlighted fields in the error details.',
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
// Validation error — selector doesn't match labels
|
|
46
|
+
{
|
|
47
|
+
test: (msg) => /`selector` does not match template `labels`/i.test(msg),
|
|
48
|
+
friendly: () => ({
|
|
49
|
+
summary: 'Selector doesn\'t match template labels',
|
|
50
|
+
suggestion: 'The spec.selector.matchLabels must match spec.template.metadata.labels exactly.',
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
// Validation error — required field
|
|
54
|
+
{
|
|
55
|
+
test: (msg) => /Required value|is required/i.test(msg),
|
|
56
|
+
friendly: (msg) => {
|
|
57
|
+
const field = msg.match(/(\S+): Required value/)?.[1] ?? msg.match(/(\S+) is required/)?.[1] ?? ''
|
|
58
|
+
return {
|
|
59
|
+
summary: `Missing required field${field ? `: ${field}` : ''}`,
|
|
60
|
+
suggestion: 'Add the missing required field to your YAML.',
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
// RBAC forbidden — use "is forbidden:" to avoid matching "Forbidden: field is immutable" or "forbidden: exceeded quota"
|
|
65
|
+
{
|
|
66
|
+
test: (msg) => /is forbidden:|cannot .* in the namespace/i.test(msg),
|
|
67
|
+
friendly: (msg) => {
|
|
68
|
+
const verb = msg.match(/cannot (\w+)/)?.[1] ?? 'perform this action on'
|
|
69
|
+
return {
|
|
70
|
+
summary: `Permission denied`,
|
|
71
|
+
suggestion: `Your cluster role doesn't allow you to ${verb} this resource. Contact your cluster admin for access.`,
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
// Already exists (create mode)
|
|
76
|
+
{
|
|
77
|
+
test: (msg) => /already exists/i.test(msg),
|
|
78
|
+
friendly: () => ({
|
|
79
|
+
summary: 'Resource already exists',
|
|
80
|
+
suggestion: 'Switch to Apply mode to update the existing resource, or change the name to create a new one.',
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
// Quota exceeded
|
|
84
|
+
{
|
|
85
|
+
test: (msg) => /exceeded quota|quota.*exceeded|forbidden.*quota/i.test(msg),
|
|
86
|
+
friendly: () => ({
|
|
87
|
+
summary: 'Resource quota exceeded',
|
|
88
|
+
suggestion: 'The namespace has reached its resource quota limit. Request a quota increase or reduce resource requests.',
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
// Namespace not found
|
|
92
|
+
{
|
|
93
|
+
test: (msg) => /namespace.*not found|namespaces.*not found/i.test(msg),
|
|
94
|
+
friendly: (msg) => {
|
|
95
|
+
const ns = msg.match(/namespace[s]? "([^"]+)"/)?.[1] ?? ''
|
|
96
|
+
return {
|
|
97
|
+
summary: `Namespace "${ns}" not found`,
|
|
98
|
+
suggestion: 'Create the namespace first or change the namespace in your YAML.',
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
// Immutable field
|
|
103
|
+
{
|
|
104
|
+
test: (msg) => /field is immutable/i.test(msg),
|
|
105
|
+
friendly: () => ({
|
|
106
|
+
summary: 'Cannot change immutable field',
|
|
107
|
+
suggestion: 'Some fields can\'t be modified after creation. Delete and recreate the resource, or remove the immutable field change.',
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
// Connection refused / cluster unreachable
|
|
111
|
+
{
|
|
112
|
+
test: (msg) => /connection refused|no such host|cluster.*unreachable/i.test(msg),
|
|
113
|
+
friendly: () => ({
|
|
114
|
+
summary: 'Cannot reach the cluster',
|
|
115
|
+
suggestion: 'The Kubernetes API server is unreachable. Check your cluster connection.',
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
// Strip the "failed to apply resource: " / "failed to create resource: " prefix our backend adds
|
|
121
|
+
function stripPrefix(msg: string): string {
|
|
122
|
+
return msg.replace(/^failed to (apply|create) resource:\s*/i, '')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getFriendlyError(rawError: string): { summary: string; suggestion: string; raw: string } | null {
|
|
126
|
+
const cleaned = stripPrefix(rawError)
|
|
127
|
+
for (const pattern of patterns) {
|
|
128
|
+
if (pattern.test(cleaned)) {
|
|
129
|
+
const { summary, suggestion } = pattern.friendly(cleaned)
|
|
130
|
+
return { summary, suggestion, raw: cleaned }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Always returns something displayable — friendly if matched, raw if not
|
|
137
|
+
export function formatApplyError(rawError: string): { summary: string; suggestion?: string; raw: string } {
|
|
138
|
+
const friendly = getFriendlyError(rawError)
|
|
139
|
+
if (friendly) return friendly
|
|
140
|
+
const cleaned = stripPrefix(rawError)
|
|
141
|
+
return { summary: cleaned, raw: rawError }
|
|
142
|
+
}
|