@skyhook-io/k8s-ui 1.14.4 → 1.14.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 (85) hide show
  1. package/package.json +5 -5
  2. package/src/components/audit/AuditCard.tsx +18 -7
  3. package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
  4. package/src/components/charts/AreaChart.test.tsx +173 -0
  5. package/src/components/charts/AreaChart.tsx +208 -53
  6. package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
  7. package/src/components/charts/PrometheusChartsView.tsx +205 -76
  8. package/src/components/charts/SeriesLegend.tsx +8 -3
  9. package/src/components/charts/annotations.ts +59 -0
  10. package/src/components/charts/axis.test.ts +48 -0
  11. package/src/components/charts/axis.ts +62 -0
  12. package/src/components/charts/colors.test.ts +27 -0
  13. package/src/components/charts/colors.ts +23 -0
  14. package/src/components/charts/format.test.ts +19 -0
  15. package/src/components/charts/format.ts +21 -1
  16. package/src/components/charts/index.ts +3 -1
  17. package/src/components/charts/nearestSample.test.ts +31 -0
  18. package/src/components/charts/nearestSample.ts +15 -0
  19. package/src/components/charts/types.ts +12 -0
  20. package/src/components/checks/ChecksView.tsx +20 -2
  21. package/src/components/gitops/GitOpsDetailLayout.tsx +13 -0
  22. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
  23. package/src/components/gitops/health-provenance.test.ts +54 -0
  24. package/src/components/gitops/health-provenance.ts +24 -0
  25. package/src/components/gitops/index.ts +2 -0
  26. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
  27. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
  28. package/src/components/gitops/tree/merge.test.ts +34 -0
  29. package/src/components/gitops/tree/merge.ts +7 -0
  30. package/src/components/resources/HPADiagnosisSummary.test.tsx +178 -0
  31. package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
  32. package/src/components/resources/ResourcesView.tsx +67 -2
  33. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  34. package/src/components/resources/index.ts +7 -0
  35. package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
  36. package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
  37. package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
  38. package/src/components/resources/renderers/HPARenderer.tsx +4 -57
  39. package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
  40. package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
  41. package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
  42. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
  43. package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
  44. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
  45. package/src/components/resources/renderers/PodRenderer.tsx +11 -0
  46. package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
  47. package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
  48. package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
  49. package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
  50. package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
  51. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -21
  52. package/src/components/resources/renderers/index.ts +3 -0
  53. package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
  54. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +116 -0
  55. package/src/components/resources/resource-utils.ts +18 -9
  56. package/src/components/shared/CreateResourceDialog.tsx +231 -11
  57. package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
  58. package/src/components/shared/DrainPlanDialog.tsx +297 -0
  59. package/src/components/shared/ResourceActionsBar.tsx +53 -26
  60. package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
  61. package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
  62. package/src/components/shared/index.ts +4 -0
  63. package/src/components/topology/K8sResourceNode.test.ts +9 -0
  64. package/src/components/topology/K8sResourceNode.tsx +56 -5
  65. package/src/components/ui/Badge.tsx +5 -1
  66. package/src/components/ui/FetchResult.test.tsx +20 -2
  67. package/src/components/ui/FetchResult.tsx +33 -3
  68. package/src/components/ui/FreshnessControl.tsx +8 -2
  69. package/src/components/ui/index.ts +5 -0
  70. package/src/components/ui/monacoRuntime.ts +14 -0
  71. package/src/components/workload/WorkloadView.tsx +2 -2
  72. package/src/monaco-deep.d.ts +10 -0
  73. package/src/theme/components.css +16 -0
  74. package/src/types/core.ts +38 -11
  75. package/src/types/gitops-insights.ts +23 -0
  76. package/src/types/gitops-tree.ts +29 -0
  77. package/src/utils/index.ts +1 -0
  78. package/src/utils/navigation.test.ts +20 -1
  79. package/src/utils/navigation.ts +25 -0
  80. package/src/utils/network-policy.test.ts +42 -0
  81. package/src/utils/network-policy.ts +59 -0
  82. package/src/utils/reflector.ts +139 -0
  83. package/src/utils/yaml-file-import.test.ts +189 -0
  84. package/src/utils/yaml-file-import.ts +95 -0
  85. package/src/components/logs/JsonLogLine.tsx +0 -93
@@ -2,6 +2,7 @@ import { Shield, ArrowDownToLine, ArrowUpFromLine, GitFork } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, LabelSelectorDisplay } from '../../ui/drawer-components'
4
4
  import { NetworkPolicyDiagram } from './NetworkPolicyDiagram'
5
+ import { effectivePolicyTypeNames, effectivePolicyTypes, formatNetworkPolicyPort } from '../../../utils/network-policy'
5
6
 
6
7
  interface NetworkPolicyRendererProps {
7
8
  data: any
@@ -11,12 +12,13 @@ interface NetworkPolicyRendererProps {
11
12
  export function NetworkPolicyRenderer({ data, staged = false }: NetworkPolicyRendererProps) {
12
13
  const spec = data.spec || {}
13
14
  const podSelector = spec.podSelector || {}
14
- const policyTypes: string[] = spec.policyTypes || []
15
15
  const ingress: any[] | undefined = spec.ingress
16
16
  const egress: any[] | undefined = spec.egress
17
17
 
18
- const hasIngress = policyTypes.includes('Ingress')
19
- const hasEgress = policyTypes.includes('Egress')
18
+ const types = effectivePolicyTypes(spec)
19
+ const policyTypes = effectivePolicyTypeNames(spec)
20
+ const hasIngress = types.ingress
21
+ const hasEgress = types.egress
20
22
 
21
23
  const hasDiagramContent = hasIngress || hasEgress
22
24
 
@@ -33,26 +35,29 @@ export function NetworkPolicyRenderer({ data, staged = false }: NetworkPolicyRen
33
35
  <div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
34
36
  <LabelSelectorDisplay selector={podSelector} emptyText="All pods in namespace" />
35
37
  </div>
36
- {policyTypes.length > 0 && (
37
- <div className="mt-2">
38
- <div className="text-xs text-theme-text-tertiary mb-1">Policy Types</div>
39
- <div className="flex flex-wrap gap-1">
40
- {policyTypes.map((type) => (
41
- <span
42
- key={type}
43
- className={clsx(
44
- 'badge',
45
- type === 'Ingress'
46
- ? 'status-blue'
47
- : 'status-purple'
48
- )}
49
- >
50
- {type}
51
- </span>
52
- ))}
53
- </div>
38
+ <div className="mt-2">
39
+ <div className="text-xs text-theme-text-tertiary mb-1">Policy Types</div>
40
+ <div className="flex flex-wrap items-center gap-1">
41
+ {policyTypes.map((type) => (
42
+ <span
43
+ key={type}
44
+ className={clsx(
45
+ 'badge',
46
+ type === 'Ingress'
47
+ ? 'status-blue'
48
+ : 'status-purple'
49
+ )}
50
+ >
51
+ {type}
52
+ </span>
53
+ ))}
54
+ {!types.explicit && (
55
+ <span className="text-xs text-theme-text-tertiary">
56
+ by default — policyTypes not set
57
+ </span>
58
+ )}
54
59
  </div>
55
- )}
60
+ </div>
56
61
  </Section>
57
62
 
58
63
  {hasIngress && (
@@ -118,7 +123,7 @@ function IngressEgressRuleCard({
118
123
  key={j}
119
124
  className="badge bg-theme-elevated text-theme-text-secondary"
120
125
  >
121
- {port.protocol || 'TCP'}/{port.port}
126
+ {formatNetworkPolicyPort(port)}
122
127
  </span>
123
128
  ))}
124
129
  </div>
@@ -135,20 +140,29 @@ function IngressEgressRuleCard({
135
140
  }
136
141
 
137
142
  function PeerEntry({ peer }: { peer: any }) {
138
- if (peer.podSelector) {
143
+ // podSelector and namespaceSelector on one peer are ANDed: the peer is the
144
+ // pods matching the first inside the namespaces matching the second. Both
145
+ // have to be shown, or a narrow cross-namespace grant reads as a broad
146
+ // same-namespace one.
147
+ const hasPod = peer.podSelector !== undefined
148
+ const hasNs = peer.namespaceSelector !== undefined
149
+ if (hasPod || hasNs) {
139
150
  return (
140
- <div className="text-sm">
141
- <span className="text-theme-text-secondary text-xs">podSelector: </span>
142
- <LabelSelectorDisplay selector={peer.podSelector} emptyText="all pods" inline />
143
- </div>
144
- )
145
- }
146
-
147
- if (peer.namespaceSelector) {
148
- return (
149
- <div className="text-sm">
150
- <span className="text-theme-text-secondary text-xs">namespaceSelector: </span>
151
- <LabelSelectorDisplay selector={peer.namespaceSelector} emptyText="all namespaces" inline />
151
+ <div className="text-sm space-y-0.5">
152
+ {hasPod && (
153
+ <div>
154
+ <span className="text-theme-text-secondary text-xs">podSelector: </span>
155
+ <LabelSelectorDisplay selector={peer.podSelector} emptyText="all pods" inline />
156
+ </div>
157
+ )}
158
+ {hasNs && (
159
+ <div>
160
+ <span className="text-theme-text-secondary text-xs">
161
+ {hasPod ? 'in namespaceSelector: ' : 'namespaceSelector: '}
162
+ </span>
163
+ <LabelSelectorDisplay selector={peer.namespaceSelector} emptyText="all namespaces" inline />
164
+ </div>
165
+ )}
152
166
  </div>
153
167
  )
154
168
  }
@@ -14,6 +14,7 @@ import {
14
14
  import { resolvedEnvFromKey } from '../../../utils/env-from'
15
15
  import { detectBlastRadius, rulePermissivenessScore } from '../../../utils/rbac-blast-radius'
16
16
  import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
17
+ import { NamespaceLimitRangeLink } from './LimitRangeRenderer'
17
18
  import type {
18
19
  PodEnvironmentResponse,
19
20
  PodEnvironmentRevealResponse,
@@ -114,6 +115,9 @@ interface PodRendererProps {
114
115
  policyData?: PolicyResourceResponse | null
115
116
  policyLoading?: boolean
116
117
  policyError?: Error | null
118
+ /** Names of the LimitRanges in this Pod's namespace. Absent while the lookup
119
+ * is pending or unreadable, which leaves the contextual link unrendered. */
120
+ namespaceLimitRangeNames?: string[] | null
117
121
  }
118
122
 
119
123
  // ── Env vars section — extracted to use hooks (useState for reveal) ──────────
@@ -296,6 +300,7 @@ export function PodRenderer({
296
300
  policyData,
297
301
  policyLoading,
298
302
  policyError,
303
+ namespaceLimitRangeNames,
299
304
  }: PodRendererProps) {
300
305
  const containerStatuses = data.status?.containerStatuses || []
301
306
  const containers = data.spec?.containers || []
@@ -763,6 +768,12 @@ export function PodRenderer({
763
768
  )
764
769
  })}
765
770
  </div>
771
+ <NamespaceLimitRangeLink
772
+ namespace={namespace || ''}
773
+ names={namespaceLimitRangeNames}
774
+ scope="pod"
775
+ onNavigate={onNavigate}
776
+ />
766
777
  </Section>
767
778
 
768
779
  {/* Environment Variables */}
@@ -0,0 +1,281 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { ReflectorSection, ReflectorSummary } from "./ReflectorSection";
4
+ import { ResourceRendererDispatch } from "../../shared/ResourceRendererDispatch";
5
+ import type { Relationships } from "../../../types";
6
+
7
+ const prefix = "reflector.v1.k8s.emberstack.com/";
8
+ const source = { kind: "ConfigMap", namespace: "source", name: "settings" };
9
+ const mirror = { kind: "ConfigMap", namespace: "app", name: "mirror" };
10
+ function resource(annotations: Record<string, string> = {}) {
11
+ return {
12
+ apiVersion: "v1",
13
+ metadata: {
14
+ namespace: "app",
15
+ name: "mirror",
16
+ annotations: Object.fromEntries(
17
+ Object.entries(annotations).map(([k, v]) => [prefix + k, v]),
18
+ ),
19
+ },
20
+ };
21
+ }
22
+ function render(
23
+ annotations: Record<string, string>,
24
+ reflection?: Relationships["reflection"],
25
+ ) {
26
+ return renderToStaticMarkup(
27
+ <>
28
+ <ReflectorSummary
29
+ data={resource(annotations)}
30
+ reflection={reflection}
31
+ onNavigate={() => {}}
32
+ />
33
+ <ReflectorSection
34
+ data={resource(annotations)}
35
+ reflection={reflection}
36
+ onNavigate={() => {}}
37
+ />
38
+ </>,
39
+ );
40
+ }
41
+
42
+ describe("Reflector evidence and copy", () => {
43
+ it("leaves ordinary objects and non-core API groups alone", () => {
44
+ expect(render({})).toBe("");
45
+ expect(render({ unrelated: "true" })).toBe("");
46
+ expect(
47
+ renderToStaticMarkup(
48
+ <ReflectorSection
49
+ data={{
50
+ ...resource({ "reflection-allowed": "true" }),
51
+ apiVersion: "example.io/v1",
52
+ }}
53
+ />,
54
+ ),
55
+ ).toBe("");
56
+ });
57
+
58
+ it("shows source settings with OR semantics and bounded visible mirrors", () => {
59
+ const mirrors = Array.from({ length: 12 }, (_, i) => ({
60
+ ...mirror,
61
+ name: `mirror-${i}`,
62
+ }));
63
+ const html = render(
64
+ {
65
+ "reflection-allowed": "true",
66
+ "reflection-auto-enabled": "true",
67
+ "reflection-allowed-namespaces": "prod-.*",
68
+ "reflection-allowed-namespaces-selector": "team=app",
69
+ },
70
+ { mirrors },
71
+ );
72
+ expect(html).toContain("Source");
73
+ expect(html).toContain("Name patterns:");
74
+ expect(html).toContain(">or</span>");
75
+ expect(html).toContain("Label selector:");
76
+ expect(html).toContain("Show all 12 visible mirrors");
77
+ expect(html).toContain("app/mirror-4");
78
+ expect(html).not.toContain("app/mirror-5");
79
+ expect(html).toContain("other mirrors may exist");
80
+ });
81
+
82
+ it.each(["True", "TRUE", " true "])(
83
+ "accepts controller boolean %s and raw .NET timestamp",
84
+ (automatic) => {
85
+ const html = render(
86
+ {
87
+ reflects: "source/settings",
88
+ "auto-reflects": automatic,
89
+ "reflected-at": "2026-09-13T12:34:56.1234567+00:00",
90
+ },
91
+ { source },
92
+ );
93
+ expect(html).toContain("Automatic mirror");
94
+ expect(html).toContain("2026-09-13T12:34:56.1234567+00:00");
95
+ expect(html).not.toContain("Check reflection configuration");
96
+ },
97
+ );
98
+
99
+ it("keeps manual pre-copy state neutral and offers only observed navigation", () => {
100
+ const html = render({ reflects: "source/settings" }, { source });
101
+ expect(html).toContain("Manual mirror");
102
+ expect(html).toContain("No version recorded");
103
+ expect(html).toContain("source/settings</button>");
104
+ expect(html).not.toContain("Check reflection configuration");
105
+ expect(html).toContain("Edit the source for lasting changes");
106
+ });
107
+
108
+ it("does not turn a hidden source into missing, a link, or version evidence", () => {
109
+ const html = render(
110
+ { reflects: "hidden/settings", "reflected-version": "41" },
111
+ { source, sourceResourceVersion: "42" },
112
+ );
113
+ expect(html).toContain("hidden/settings");
114
+ expect(html).toContain("not available in this view");
115
+ expect(html).not.toContain("hidden/settings</button>");
116
+ expect(html).not.toContain("Source version (snapshot)");
117
+ expect(html).not.toContain("missing");
118
+ expect(html).not.toContain("Check reflection configuration");
119
+ });
120
+
121
+ it.each([
122
+ ["opaque-x", "opaque-x", "matches the source snapshot"],
123
+ ["opaque-x", "opaque-y", "differs from the source snapshot"],
124
+ ])(
125
+ "reports version equality without contents or health claims",
126
+ (copied, current, message) => {
127
+ const html = render(
128
+ { reflects: "source/settings", "reflected-version": copied },
129
+ { source, sourceResourceVersion: current },
130
+ );
131
+ expect(html).toContain(message);
132
+ expect(html).toContain("do not prove matching contents");
133
+ expect(html).not.toContain("Synced");
134
+ expect(html).not.toContain("Check reflection configuration");
135
+ },
136
+ );
137
+
138
+ it.each(["", "missing-namespace", "ns/name/extra"])(
139
+ "flags malformed declarations (%s)",
140
+ (reflects) => {
141
+ expect(render({ reflects })).toContain(
142
+ "must name a source as namespace/name",
143
+ );
144
+ },
145
+ );
146
+
147
+ it("flags self reference and contradictory auto-source settings", () => {
148
+ expect(render({ reflects: "app/mirror" })).toContain(
149
+ "points to this object itself",
150
+ );
151
+ expect(render({ "reflection-auto-enabled": "true" })).toContain(
152
+ "requires reflection-allowed to be true",
153
+ );
154
+ expect(render({ "reflection-allowed": "yes" })).toContain(
155
+ "must be true or false",
156
+ );
157
+ });
158
+
159
+ it("explains a declared chain without dropping either direction", () => {
160
+ const html = render(
161
+ { reflects: "source/settings", "reflection-allowed": "true" },
162
+ { source, mirrors: [{ ...mirror, namespace: "leaf" }] },
163
+ );
164
+ expect(html).toContain("source/settings</button>");
165
+ expect(html).toContain("leaf/mirror</button>");
166
+ expect(html).toContain("does not propagate its updates");
167
+ });
168
+
169
+ it("shows only explicit reflection fields rather than inferring from kind equality", () => {
170
+ const html = renderToStaticMarkup(
171
+ <ResourceRendererDispatch
172
+ resource={{ kind: "configmaps", namespace: "app", name: "mirror" }}
173
+ data={resource()}
174
+ relationships={{ consumers: [mirror] }}
175
+ onCopy={() => {}}
176
+ copied={null}
177
+ showCommonSections={false}
178
+ />,
179
+ );
180
+ expect(html).not.toContain("Reflector");
181
+ });
182
+
183
+ it.each(["configmaps", "secrets"])(
184
+ "wires the %s renderer and avoids duplicate generic mirror links",
185
+ (kind) => {
186
+ const ref = {
187
+ ...source,
188
+ kind: kind === "secrets" ? "Secret" : "ConfigMap",
189
+ };
190
+ const html = renderToStaticMarkup(
191
+ <ResourceRendererDispatch
192
+ resource={{ kind, namespace: "app", name: "mirror" }}
193
+ data={{ ...resource({ reflects: "source/settings" }), data: {} }}
194
+ relationships={{ configRefs: [ref], reflection: { source: ref } }}
195
+ onCopy={() => {}}
196
+ copied={null}
197
+ onNavigate={() => {}}
198
+ />,
199
+ );
200
+ expect(html).toContain("Reflector");
201
+ expect(html).toContain("source/settings</button>");
202
+ expect(html).not.toContain("Related Resources");
203
+ expect(html).not.toContain("Check reflection configuration");
204
+ },
205
+ );
206
+ });
207
+
208
+ describe("Reflector configuration diagnostics", () => {
209
+ it("does not describe a malformed reference as an active mirror chain", () => {
210
+ const html = render({
211
+ reflects: "missing-namespace",
212
+ "reflection-allowed": "true",
213
+ });
214
+ expect(html).toContain("Invalid source reference");
215
+ expect(html).not.toContain("does not propagate");
216
+ expect(html).not.toContain("Edit the source for lasting changes");
217
+ });
218
+ it("warns when visible mirrors reference a source that no longer allows reflection", () => {
219
+ const html = render({}, { mirrors: [mirror] });
220
+ expect(html).toContain("Reflection is not enabled on this source");
221
+ });
222
+ });
223
+
224
+ describe("Reflector resource hierarchy", () => {
225
+ it("keeps ConfigMap data ahead of detailed settings and source mirror list", () => {
226
+ const html = renderToStaticMarkup(
227
+ <ResourceRendererDispatch
228
+ kind="configmaps"
229
+ data={{
230
+ ...resource({ "reflection-allowed": "true" }),
231
+ data: { setting: "value" },
232
+ }}
233
+ resource={{ kind: "configmaps", namespace: "app", name: "mirror" }}
234
+ relationships={{ reflection: { mirrors: [mirror] } }}
235
+ />,
236
+ );
237
+ expect(html.indexOf("Data (1 keys)")).toBeLessThan(
238
+ html.indexOf("Reflector (1 visible mirror)"),
239
+ );
240
+ expect(html).not.toContain("Reflector mirror</");
241
+ });
242
+ it("puts TLS expiry and mirror provenance ahead of data, with details below", () => {
243
+ const html = renderToStaticMarkup(
244
+ <ResourceRendererDispatch
245
+ kind="secrets"
246
+ data={{
247
+ ...resource({ reflects: "source/settings" }),
248
+ type: "kubernetes.io/tls",
249
+ data: { "tls.crt": "dGVzdA==" },
250
+ }}
251
+ resource={{ kind: "secrets", namespace: "app", name: "mirror" }}
252
+ relationships={{
253
+ reflection: { source: { ...source, kind: "Secret" } },
254
+ }}
255
+ certificateInfo={
256
+ {
257
+ certificates: [
258
+ {
259
+ serialNumber: "fixture",
260
+ expired: true,
261
+ daysLeft: -1,
262
+ notAfter: "2026-01-01T00:00:00Z",
263
+ notBefore: "2025-01-01T00:00:00Z",
264
+ subject: "test",
265
+ issuer: "test",
266
+ dnsNames: [],
267
+ },
268
+ ],
269
+ } as any
270
+ }
271
+ />,
272
+ );
273
+ expect(html.indexOf("Certificate has expired")).toBeLessThan(
274
+ html.indexOf("Reflector mirror"),
275
+ );
276
+ expect(html.indexOf("Reflector mirror")).toBeLessThan(
277
+ html.indexOf(">Data</"),
278
+ );
279
+ expect(html.indexOf(">Data</")).toBeLessThan(html.indexOf(">Reflector</"));
280
+ });
281
+ });