@skyhook-io/k8s-ui 1.8.4 → 1.8.6
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/charts/PrometheusChartsView.tsx +3 -8
- package/src/components/gitops/GitOpsDetailLayout.tsx +2 -3
- package/src/components/gitops/GitOpsTableView.tsx +2 -4
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +3 -6
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +3 -7
- package/src/components/logs/LogCore.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +34 -5
- package/src/components/resources/get-pod-phase-display.test.ts +115 -3
- package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
- package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
- package/src/components/resources/renderers/JobRenderer.tsx +6 -5
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
- package/src/components/resources/renderers/ServiceRenderer.tsx +1 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
- package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
- package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
- package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
- package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +1 -1
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
- package/src/components/resources/renderers/contour-cells.tsx +6 -5
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/istio-cells.tsx +16 -25
- package/src/components/resources/resource-utils-cnpg.ts +1 -1
- package/src/components/resources/resource-utils-keda.ts +12 -6
- package/src/components/resources/resource-utils.ts +172 -57
- package/src/components/shared/ResourceActionsBar.tsx +1 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
- package/src/components/topology/TopologyControls.tsx +41 -9
- package/src/components/topology/TopologyGraph.tsx +20 -7
- package/src/components/topology/layout-elk-graph.test.ts +222 -0
- package/src/components/topology/layout.ts +96 -33
- package/src/components/topology/layout.worker.ts +18 -34
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/CodeViewer.tsx +1 -1
- package/src/components/ui/PaneLoader.tsx +6 -1
- package/src/components/ui/RestrictedState.tsx +152 -0
- package/src/components/ui/YamlEditor.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/types/core.ts +37 -0
- package/src/utils/asset-url.ts +13 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Shield, ChevronDown, Copy, Check } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
// RestrictedState is the shared "you can't see this because of Kubernetes RBAC"
|
|
6
|
+
// surface — distinct from EmptyState (which covers healthy / filtered / no-data).
|
|
7
|
+
// It never claims WHY the SAR failed (Radar can't prove tier vs custom-role vs
|
|
8
|
+
// disabled-value from a denied check); it states the fact and hands the operator
|
|
9
|
+
// something to forward to whoever administers their cluster.
|
|
10
|
+
//
|
|
11
|
+
// Reused across the resource list, topology, search, and the per-cluster access
|
|
12
|
+
// summary so the messaging can't drift.
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Display kind, e.g. "Node". */
|
|
16
|
+
kindLabel: string
|
|
17
|
+
/** API group for the kind ("" for core). Used to build the example RBAC. */
|
|
18
|
+
group?: string
|
|
19
|
+
/** Plural resource name, e.g. "nodes". When omitted the snippet uses a
|
|
20
|
+
* placeholder the admin fills in. */
|
|
21
|
+
resource?: string
|
|
22
|
+
/** Why the kind is hidden. "rbac_denied" (default): Radar can read it but the
|
|
23
|
+
* user's RBAC can't — show the grant request. "unavailable": Radar's
|
|
24
|
+
* ServiceAccount can't read it at all (not installed / SA RBAC / feature off)
|
|
25
|
+
* — a user grant won't help, so show a different message and no snippet. */
|
|
26
|
+
reason?: 'rbac_denied' | 'unavailable' | string
|
|
27
|
+
/** Tightens spacing for inline/embedded use (topology overlay, etc.). */
|
|
28
|
+
compact?: boolean
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildRbacRequest(group: string, resource: string): string {
|
|
33
|
+
// resource is the placeholder when we don't have a confident API plural
|
|
34
|
+
// (e.g. a CRD deep-link before discovery resolves the kind) — use a generic
|
|
35
|
+
// role name rather than radar-read-<Kind>.
|
|
36
|
+
const roleName = resource === '<resource>' ? 'radar-read-access' : `radar-read-${resource}`
|
|
37
|
+
return [
|
|
38
|
+
`apiVersion: rbac.authorization.k8s.io/v1`,
|
|
39
|
+
`kind: ClusterRole`,
|
|
40
|
+
`metadata:`,
|
|
41
|
+
` name: ${roleName}`,
|
|
42
|
+
`rules:`,
|
|
43
|
+
` - apiGroups: ["${group}"]`,
|
|
44
|
+
` resources: ["${resource}"]`,
|
|
45
|
+
` verbs: ["get", "list", "watch"]`,
|
|
46
|
+
`---`,
|
|
47
|
+
`apiVersion: rbac.authorization.k8s.io/v1`,
|
|
48
|
+
`kind: ClusterRoleBinding`,
|
|
49
|
+
`metadata:`,
|
|
50
|
+
` name: ${roleName}`,
|
|
51
|
+
`roleRef:`,
|
|
52
|
+
` apiGroup: rbac.authorization.k8s.io`,
|
|
53
|
+
` kind: ClusterRole`,
|
|
54
|
+
` name: ${roleName}`,
|
|
55
|
+
`subjects:`,
|
|
56
|
+
` - kind: Group # or User / ServiceAccount`,
|
|
57
|
+
` name: <your-identity>`,
|
|
58
|
+
` apiGroup: rbac.authorization.k8s.io`,
|
|
59
|
+
].join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function RestrictedState({ kindLabel, group = '', resource, reason, compact, className }: Props) {
|
|
63
|
+
const [expanded, setExpanded] = useState(false)
|
|
64
|
+
const [copied, setCopied] = useState(false)
|
|
65
|
+
const isUnavailable = reason === 'unavailable'
|
|
66
|
+
|
|
67
|
+
// RBAC `resources` must be the lowercase API plural. selectedKind.name is
|
|
68
|
+
// that in normal use, but a CRD deep-link can transiently carry the Kind
|
|
69
|
+
// (e.g. "HTTPRoute") before discovery resolves it — emitting that would
|
|
70
|
+
// produce a snippet that doesn't grant access. Only inline the resource when
|
|
71
|
+
// it looks like a valid resource name; otherwise leave a clear placeholder.
|
|
72
|
+
const validResource = resource && /^[a-z0-9.-]+$/.test(resource) ? resource : '<resource>'
|
|
73
|
+
const snippet = buildRbacRequest(group, validResource)
|
|
74
|
+
|
|
75
|
+
const copy = () => {
|
|
76
|
+
navigator.clipboard.writeText(snippet).then(
|
|
77
|
+
() => {
|
|
78
|
+
setCopied(true)
|
|
79
|
+
setTimeout(() => setCopied(false), 2000)
|
|
80
|
+
},
|
|
81
|
+
() => {},
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className={clsx(
|
|
88
|
+
'flex flex-col items-center justify-center text-center text-theme-text-tertiary',
|
|
89
|
+
compact ? 'p-4' : 'p-6',
|
|
90
|
+
className,
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<Shield className="w-8 h-8 text-amber-400 mb-2" />
|
|
94
|
+
{isUnavailable ? (
|
|
95
|
+
// Radar's ServiceAccount can't read this kind — a user grant won't help.
|
|
96
|
+
<>
|
|
97
|
+
<p className="text-theme-text-secondary font-medium">{kindLabel} isn't available here</p>
|
|
98
|
+
<p className="text-sm mt-1 max-w-md">
|
|
99
|
+
Radar can't read {kindLabel} resources in this cluster — the type may not be installed,
|
|
100
|
+
or read access isn't granted to Radar's ServiceAccount (some kinds, like RBAC objects
|
|
101
|
+
and Secrets, are off unless enabled in the Radar chart). Granting your own identity
|
|
102
|
+
access won't surface it.
|
|
103
|
+
</p>
|
|
104
|
+
</>
|
|
105
|
+
) : (
|
|
106
|
+
<>
|
|
107
|
+
<p className="text-theme-text-secondary font-medium">You don't have access to {kindLabel}</p>
|
|
108
|
+
<p className="text-sm mt-1 max-w-md">
|
|
109
|
+
Your Kubernetes RBAC doesn't allow listing {kindLabel} resources in this cluster. This
|
|
110
|
+
isn't an empty cluster — Radar is hiding what your identity can't read.
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
<div className="mt-3 w-full max-w-md">
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => setExpanded((v) => !v)}
|
|
116
|
+
className="flex items-center gap-1.5 mx-auto text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
117
|
+
>
|
|
118
|
+
<ChevronDown className={clsx('w-4 h-4 transition-transform', expanded && 'rotate-180')} />
|
|
119
|
+
How to get access
|
|
120
|
+
</button>
|
|
121
|
+
|
|
122
|
+
{expanded && (
|
|
123
|
+
<div className="mt-2 text-left">
|
|
124
|
+
<p className="text-xs text-theme-text-tertiary mb-2">
|
|
125
|
+
Apply this, or send it to whoever administers your cluster, to grant your identity
|
|
126
|
+
read access.
|
|
127
|
+
</p>
|
|
128
|
+
<div className="rounded-md border border-theme-border bg-theme-base overflow-hidden">
|
|
129
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-b border-theme-border bg-theme-elevated">
|
|
130
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
131
|
+
ClusterRole + ClusterRoleBinding
|
|
132
|
+
</span>
|
|
133
|
+
<button
|
|
134
|
+
onClick={copy}
|
|
135
|
+
className="flex items-center gap-1 text-xs text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
136
|
+
>
|
|
137
|
+
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
|
138
|
+
{copied ? 'Copied' : 'Copy'}
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
<pre className="text-xs p-3 max-h-72 overflow-auto text-theme-text-secondary">
|
|
142
|
+
{snippet}
|
|
143
|
+
</pre>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -990,7 +990,7 @@ export function EventsSection({ events, updates = [], isLoading, eventsError, up
|
|
|
990
990
|
if (isLoading) {
|
|
991
991
|
return (
|
|
992
992
|
<Section title="Recent Events" defaultExpanded>
|
|
993
|
-
<div className="text-sm text-theme-text-tertiary">Loading events
|
|
993
|
+
<div className="text-sm text-theme-text-tertiary">Loading events…</div>
|
|
994
994
|
</Section>
|
|
995
995
|
)
|
|
996
996
|
}
|
|
@@ -5,6 +5,7 @@ export { MiddleEllipsis } from './MiddleEllipsis'
|
|
|
5
5
|
export type { MiddleEllipsisProps } from './MiddleEllipsis'
|
|
6
6
|
export { EmptyState } from './EmptyState'
|
|
7
7
|
export type { EmptyStateTone, EmptyStateVariant } from './EmptyState'
|
|
8
|
+
export { RestrictedState } from './RestrictedState'
|
|
8
9
|
export { FetchResult } from './FetchResult'
|
|
9
10
|
export { SearchBox } from './SearchBox'
|
|
10
11
|
export { FilterPill } from './FilterPill'
|
|
@@ -1126,7 +1126,7 @@ function EventsTab({
|
|
|
1126
1126
|
return (
|
|
1127
1127
|
<div className="flex items-center justify-center h-full text-theme-text-tertiary">
|
|
1128
1128
|
<RefreshCw className="w-5 h-5 animate-spin mr-2" />
|
|
1129
|
-
Loading events
|
|
1129
|
+
Loading events…
|
|
1130
1130
|
</div>
|
|
1131
1131
|
)
|
|
1132
1132
|
}
|
package/src/types/core.ts
CHANGED
|
@@ -554,6 +554,8 @@ export interface HelmRelease {
|
|
|
554
554
|
status: string
|
|
555
555
|
revision: number
|
|
556
556
|
updated: string // ISO date string
|
|
557
|
+
lastOperation?: HelmOperation
|
|
558
|
+
operations?: HelmOperation[]
|
|
557
559
|
// Health summary from owned resources
|
|
558
560
|
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
559
561
|
healthIssue?: string // Primary issue if unhealthy (e.g., "OOMKilled")
|
|
@@ -574,6 +576,27 @@ export interface HelmRevision {
|
|
|
574
576
|
updated: string // ISO date string
|
|
575
577
|
}
|
|
576
578
|
|
|
579
|
+
export type HelmOperationKind = 'release_failed' | 'upgrade_failed' | 'upgrade_rolled_back' | 'rollback' | 'pending'
|
|
580
|
+
export type HelmOperationStatus = 'failed' | 'rolled_back' | 'completed' | 'stuck_pending'
|
|
581
|
+
export type HelmOperationConfidence = 'high' | 'medium' | 'low'
|
|
582
|
+
export type HelmOperationSource = 'helm_status' | 'helm_history'
|
|
583
|
+
|
|
584
|
+
export interface HelmOperation {
|
|
585
|
+
kind: HelmOperationKind
|
|
586
|
+
status: HelmOperationStatus
|
|
587
|
+
source: HelmOperationSource
|
|
588
|
+
confidence: HelmOperationConfidence
|
|
589
|
+
message: string
|
|
590
|
+
evidence?: string
|
|
591
|
+
failureDescription?: string
|
|
592
|
+
revision?: number
|
|
593
|
+
failedRevision?: number
|
|
594
|
+
rollbackRevision?: number
|
|
595
|
+
targetRevision?: number
|
|
596
|
+
pendingStatus?: string
|
|
597
|
+
updated?: string
|
|
598
|
+
}
|
|
599
|
+
|
|
577
600
|
export interface HelmReleaseDetail {
|
|
578
601
|
name: string
|
|
579
602
|
namespace: string
|
|
@@ -589,9 +612,14 @@ export interface HelmReleaseDetail {
|
|
|
589
612
|
notes: string
|
|
590
613
|
history: HelmRevision[]
|
|
591
614
|
resources: HelmOwnedResource[]
|
|
615
|
+
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
616
|
+
healthIssue?: string
|
|
617
|
+
healthSummary?: string
|
|
592
618
|
hooks?: HelmHook[]
|
|
593
619
|
readme?: string
|
|
594
620
|
dependencies?: ChartDependency[]
|
|
621
|
+
lastOperation?: HelmOperation
|
|
622
|
+
operations?: HelmOperation[]
|
|
595
623
|
// When set, this release was installed by Flux's helm-controller — see
|
|
596
624
|
// HelmRelease.managedByFluxHelmRelease for context. Format: "namespace/name".
|
|
597
625
|
managedByFluxHelmRelease?: string
|
|
@@ -649,7 +677,16 @@ export interface UpgradeInfo {
|
|
|
649
677
|
latestVersion?: string
|
|
650
678
|
updateAvailable: boolean
|
|
651
679
|
repositoryName?: string
|
|
680
|
+
// 'repository' for classic HTTP-repo matches, 'oci' when discovered via a
|
|
681
|
+
// registered OCI chart source. Absent when the source couldn't be determined.
|
|
682
|
+
sourceType?: 'repository' | 'oci'
|
|
683
|
+
// oci:// chart reference an OCI-sourced upgrade lives at (display only).
|
|
684
|
+
chartRef?: string
|
|
652
685
|
error?: string
|
|
686
|
+
// True only when the error is a genuinely untracked source (registering a
|
|
687
|
+
// chart source could fix it) — NOT for repo-side errors like a stale index or
|
|
688
|
+
// classic ambiguity. Gates the "track source" affordance.
|
|
689
|
+
untracked?: boolean
|
|
653
690
|
}
|
|
654
691
|
|
|
655
692
|
// Batch upgrade info keyed by "storageNamespace/name".
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// k8s-ui ships TS source, so each consumer's bundler types asset imports
|
|
2
|
+
// differently: Vite resolves `import x from './x.png'` to a URL string, while
|
|
3
|
+
// webpack/Next resolves it to a StaticImageData object. Reading `.src` off the
|
|
4
|
+
// object (vs Vite's bare string) yields the bundler's own emitted URL, which is
|
|
5
|
+
// the one that's correct in both client and SSR output — unlike
|
|
6
|
+
// `new URL(asset, import.meta.url)`, which can produce a `file://` URL under SSR.
|
|
7
|
+
// The structural `{ src: string }` type matches Next's StaticImageData without
|
|
8
|
+
// depending on the Next-only type, so this also type-checks under Vite.
|
|
9
|
+
export type AssetImport = string | { src: string }
|
|
10
|
+
|
|
11
|
+
export function assetUrl(asset: AssetImport): string {
|
|
12
|
+
return typeof asset === 'string' ? asset : asset.src
|
|
13
|
+
}
|