@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.
Files changed (55) hide show
  1. package/package.json +2 -2
  2. package/src/components/charts/PrometheusChartsView.tsx +3 -8
  3. package/src/components/gitops/GitOpsDetailLayout.tsx +2 -3
  4. package/src/components/gitops/GitOpsTableView.tsx +2 -4
  5. package/src/components/gitops/insights/GitOpsInsightViews.tsx +3 -6
  6. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +3 -7
  7. package/src/components/logs/LogCore.tsx +1 -1
  8. package/src/components/resources/ResourcesView.tsx +34 -5
  9. package/src/components/resources/get-pod-phase-display.test.ts +115 -3
  10. package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
  11. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
  12. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
  13. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
  14. package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
  15. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
  16. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
  17. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
  18. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
  19. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
  20. package/src/components/resources/renderers/JobRenderer.tsx +6 -5
  21. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
  22. package/src/components/resources/renderers/ServiceRenderer.tsx +1 -1
  23. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
  24. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
  25. package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
  26. package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
  27. package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
  28. package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
  29. package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
  30. package/src/components/resources/renderers/WorkloadRenderer.tsx +1 -1
  31. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
  32. package/src/components/resources/renderers/contour-cells.tsx +6 -5
  33. package/src/components/resources/renderers/index.ts +4 -0
  34. package/src/components/resources/renderers/istio-cells.tsx +16 -25
  35. package/src/components/resources/resource-utils-cnpg.ts +1 -1
  36. package/src/components/resources/resource-utils-keda.ts +12 -6
  37. package/src/components/resources/resource-utils.ts +172 -57
  38. package/src/components/shared/ResourceActionsBar.tsx +1 -1
  39. package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
  40. package/src/components/topology/TopologyControls.tsx +41 -9
  41. package/src/components/topology/TopologyGraph.tsx +20 -7
  42. package/src/components/topology/layout-elk-graph.test.ts +222 -0
  43. package/src/components/topology/layout.ts +96 -33
  44. package/src/components/topology/layout.worker.ts +18 -34
  45. package/src/components/ui/Badge.tsx +66 -3
  46. package/src/components/ui/ClusterName.tsx +12 -4
  47. package/src/components/ui/CodeViewer.tsx +1 -1
  48. package/src/components/ui/PaneLoader.tsx +6 -1
  49. package/src/components/ui/RestrictedState.tsx +152 -0
  50. package/src/components/ui/YamlEditor.tsx +1 -1
  51. package/src/components/ui/drawer-components.tsx +1 -1
  52. package/src/components/ui/index.ts +1 -0
  53. package/src/components/workload/WorkloadView.tsx +1 -1
  54. package/src/types/core.ts +37 -0
  55. 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
+ }
@@ -233,7 +233,7 @@ export function YamlEditor({
233
233
  }}
234
234
  loading={
235
235
  <div className="flex items-center justify-center h-full bg-theme-surface text-theme-text-secondary">
236
- Loading editor...
236
+ Loading editor…
237
237
  </div>
238
238
  }
239
239
  />
@@ -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...</div>
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
+ }