@skyhook-io/k8s-ui 1.14.5 → 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.
- package/package.json +5 -5
- package/src/components/audit/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/charts/AreaChart.test.tsx +13 -0
- package/src/components/charts/AreaChart.tsx +28 -25
- package/src/components/charts/PrometheusChartsView.tsx +12 -0
- package/src/components/charts/axis.test.ts +16 -0
- package/src/components/charts/axis.ts +3 -1
- package/src/components/charts/format.ts +4 -0
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/checks/ChecksView.tsx +20 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
- package/src/components/gitops/health-provenance.test.ts +54 -0
- package/src/components/gitops/health-provenance.ts +24 -0
- package/src/components/gitops/index.ts +2 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
- package/src/components/gitops/tree/merge.test.ts +34 -0
- package/src/components/gitops/tree/merge.ts +7 -0
- package/src/components/resources/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
- package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
- package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
- package/src/components/resources/renderers/PodRenderer.tsx +11 -0
- package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
- package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +116 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +231 -11
- package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
- package/src/components/shared/DrainPlanDialog.tsx +297 -0
- package/src/components/shared/ResourceActionsBar.tsx +53 -26
- package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
- package/src/components/shared/index.ts +4 -0
- package/src/components/ui/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/FreshnessControl.tsx +8 -2
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/theme/components.css +16 -0
- package/src/types/core.ts +15 -1
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +20 -1
- package/src/utils/navigation.ts +25 -0
- package/src/utils/network-policy.test.ts +42 -0
- package/src/utils/network-policy.ts +59 -0
- package/src/utils/reflector.ts +139 -0
- package/src/utils/yaml-file-import.test.ts +189 -0
- package/src/utils/yaml-file-import.ts +95 -0
- package/src/components/logs/JsonLogLine.tsx +0 -93
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Copy } from "lucide-react";
|
|
3
|
+
import type { ResourceRef } from "../../../types";
|
|
4
|
+
import { pluralize } from "../../../utils/pluralize";
|
|
5
|
+
import { formatRelativeAgeTime } from "../../../utils/format";
|
|
6
|
+
import {
|
|
7
|
+
hasReflectorDetails,
|
|
8
|
+
parseReflector,
|
|
9
|
+
type Reflection,
|
|
10
|
+
type ReflectorResource,
|
|
11
|
+
} from "../../../utils/reflector";
|
|
12
|
+
export {
|
|
13
|
+
hasReflectorDetails,
|
|
14
|
+
isReflectorMirror,
|
|
15
|
+
reflectorEditNotice,
|
|
16
|
+
} from "../../../utils/reflector";
|
|
17
|
+
import { Badge } from "../../ui/Badge";
|
|
18
|
+
import {
|
|
19
|
+
AlertBanner,
|
|
20
|
+
Property,
|
|
21
|
+
PropertyList,
|
|
22
|
+
ResourceLink,
|
|
23
|
+
Section,
|
|
24
|
+
} from "../../ui/drawer-components";
|
|
25
|
+
|
|
26
|
+
interface ReflectorSectionProps {
|
|
27
|
+
data: ReflectorResource;
|
|
28
|
+
reflection?: Reflection;
|
|
29
|
+
onNavigate?: (ref: ResourceRef) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function NamespaceRule({
|
|
33
|
+
patterns,
|
|
34
|
+
selector,
|
|
35
|
+
}: {
|
|
36
|
+
patterns?: string;
|
|
37
|
+
selector?: string;
|
|
38
|
+
}) {
|
|
39
|
+
if (!patterns && !selector) return <>No namespace restriction configured</>;
|
|
40
|
+
return (
|
|
41
|
+
<span className="space-y-1 block">
|
|
42
|
+
{patterns && (
|
|
43
|
+
<span className="block break-all">
|
|
44
|
+
<span className="text-theme-text-tertiary">Name patterns: </span>
|
|
45
|
+
{patterns}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
{patterns && selector && (
|
|
49
|
+
<span className="block text-theme-text-tertiary">or</span>
|
|
50
|
+
)}
|
|
51
|
+
{selector && (
|
|
52
|
+
<span className="block break-all">
|
|
53
|
+
<span className="text-theme-text-tertiary">Label selector: </span>
|
|
54
|
+
{selector}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ReflectorSection({
|
|
62
|
+
data,
|
|
63
|
+
reflection,
|
|
64
|
+
onNavigate,
|
|
65
|
+
}: ReflectorSectionProps) {
|
|
66
|
+
const [showAll, setShowAll] = useState(false);
|
|
67
|
+
if (!hasReflectorDetails(data, reflection)) return null;
|
|
68
|
+
const {
|
|
69
|
+
value,
|
|
70
|
+
hasDeclaration,
|
|
71
|
+
isMirror,
|
|
72
|
+
allowed,
|
|
73
|
+
autoEnabled,
|
|
74
|
+
automatic,
|
|
75
|
+
mirrors,
|
|
76
|
+
sourceSettings,
|
|
77
|
+
copiedVersion,
|
|
78
|
+
sourceVersion,
|
|
79
|
+
warnings,
|
|
80
|
+
} = parseReflector(data, reflection);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Section
|
|
84
|
+
title={
|
|
85
|
+
mirrors.length
|
|
86
|
+
? `Reflector (${pluralize(mirrors.length, "visible mirror")})`
|
|
87
|
+
: "Reflector"
|
|
88
|
+
}
|
|
89
|
+
icon={Copy}
|
|
90
|
+
defaultExpanded={warnings.length > 0}
|
|
91
|
+
>
|
|
92
|
+
<div className="space-y-3">
|
|
93
|
+
<PropertyList>
|
|
94
|
+
<Property
|
|
95
|
+
label="Role"
|
|
96
|
+
value={
|
|
97
|
+
<Badge tone="structural">
|
|
98
|
+
{isMirror
|
|
99
|
+
? automatic === true
|
|
100
|
+
? "Automatic mirror"
|
|
101
|
+
: automatic === false
|
|
102
|
+
? "Manual mirror"
|
|
103
|
+
: "Mirror"
|
|
104
|
+
: hasDeclaration
|
|
105
|
+
? "Invalid source reference"
|
|
106
|
+
: "Source"}
|
|
107
|
+
</Badge>
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
{sourceSettings && (
|
|
111
|
+
<>
|
|
112
|
+
<Property
|
|
113
|
+
label="Reflection allowed"
|
|
114
|
+
value={
|
|
115
|
+
allowed === undefined
|
|
116
|
+
? "Invalid annotation"
|
|
117
|
+
: allowed
|
|
118
|
+
? "Yes"
|
|
119
|
+
: "No"
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<Property
|
|
123
|
+
label="Allowed namespaces"
|
|
124
|
+
value={
|
|
125
|
+
<NamespaceRule
|
|
126
|
+
patterns={value("reflection-allowed-namespaces")}
|
|
127
|
+
selector={value("reflection-allowed-namespaces-selector")}
|
|
128
|
+
/>
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
<Property
|
|
132
|
+
label="Automatic creation"
|
|
133
|
+
value={
|
|
134
|
+
autoEnabled === undefined
|
|
135
|
+
? "Invalid annotation"
|
|
136
|
+
: autoEnabled
|
|
137
|
+
? "Enabled"
|
|
138
|
+
: "Disabled"
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
{(autoEnabled ||
|
|
142
|
+
value("reflection-auto-namespaces") ||
|
|
143
|
+
value("reflection-auto-namespaces-selector")) && (
|
|
144
|
+
<Property
|
|
145
|
+
label="Automatic namespaces"
|
|
146
|
+
value={
|
|
147
|
+
<NamespaceRule
|
|
148
|
+
patterns={value("reflection-auto-namespaces")}
|
|
149
|
+
selector={value("reflection-auto-namespaces-selector")}
|
|
150
|
+
/>
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
155
|
+
Automatic destinations must also satisfy the allowed rule.
|
|
156
|
+
Controller version and settings may further limit destinations.
|
|
157
|
+
</p>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
{isMirror && (
|
|
161
|
+
<>
|
|
162
|
+
<Property
|
|
163
|
+
label="Copied source version"
|
|
164
|
+
value={copiedVersion || "No version recorded"}
|
|
165
|
+
/>
|
|
166
|
+
<Property
|
|
167
|
+
label="Source version (snapshot)"
|
|
168
|
+
value={sourceVersion}
|
|
169
|
+
/>
|
|
170
|
+
<Property
|
|
171
|
+
label="Recorded copy time"
|
|
172
|
+
value={
|
|
173
|
+
value("reflected-at") ? (
|
|
174
|
+
<span title={value("reflected-at")}>
|
|
175
|
+
{formatRelativeAgeTime(value("reflected-at"))}
|
|
176
|
+
</span>
|
|
177
|
+
) : undefined
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
{copiedVersion && sourceVersion && (
|
|
181
|
+
<p className="text-xs text-theme-text-secondary">
|
|
182
|
+
{copiedVersion === sourceVersion
|
|
183
|
+
? "The recorded version matches the source snapshot."
|
|
184
|
+
: "The recorded version differs from the source snapshot."}{" "}
|
|
185
|
+
Cached versions can lag reconciliation; matching versions do
|
|
186
|
+
not prove matching contents.
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</PropertyList>
|
|
192
|
+
{mirrors.length > 0 && (
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
<div className="text-sm text-theme-text-secondary">
|
|
195
|
+
Visible mirrors{" "}
|
|
196
|
+
<Badge tone="structural" size="sm">
|
|
197
|
+
{mirrors.length}
|
|
198
|
+
</Badge>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="space-y-1 max-h-60 overflow-y-auto text-sm">
|
|
201
|
+
{(showAll ? mirrors : mirrors.slice(0, 5)).map((ref) => (
|
|
202
|
+
<div key={`${ref.namespace}/${ref.name}`} className="break-all">
|
|
203
|
+
<ResourceLink
|
|
204
|
+
{...ref}
|
|
205
|
+
label={`${ref.namespace}/${ref.name}`}
|
|
206
|
+
onNavigate={onNavigate}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
{mirrors.length > 5 && (
|
|
212
|
+
<button
|
|
213
|
+
className="text-xs text-accent-text hover:underline"
|
|
214
|
+
onClick={() => setShowAll(!showAll)}
|
|
215
|
+
>
|
|
216
|
+
{showAll
|
|
217
|
+
? "Show fewer"
|
|
218
|
+
: `Show all ${mirrors.length} visible mirrors`}
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
221
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
222
|
+
Visible relationships only; other mirrors may exist.
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
{sourceSettings && mirrors.length === 0 && (
|
|
227
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
228
|
+
No mirrors visible in this view.
|
|
229
|
+
</p>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</Section>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function ReflectorSummary({
|
|
237
|
+
data,
|
|
238
|
+
reflection,
|
|
239
|
+
onNavigate,
|
|
240
|
+
}: ReflectorSectionProps) {
|
|
241
|
+
if (!hasReflectorDetails(data, reflection)) return null;
|
|
242
|
+
const { isMirror, declaration, source, warnings } = parseReflector(
|
|
243
|
+
data,
|
|
244
|
+
reflection,
|
|
245
|
+
);
|
|
246
|
+
if (!isMirror && warnings.length === 0) return null;
|
|
247
|
+
return (
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
{isMirror && (
|
|
250
|
+
<AlertBanner
|
|
251
|
+
variant="info"
|
|
252
|
+
title="Reflector mirror"
|
|
253
|
+
message={
|
|
254
|
+
<>
|
|
255
|
+
<span className="block break-all">
|
|
256
|
+
Source:{" "}
|
|
257
|
+
{source ? (
|
|
258
|
+
<ResourceLink
|
|
259
|
+
{...source}
|
|
260
|
+
label={`${source.namespace}/${source.name}`}
|
|
261
|
+
onNavigate={onNavigate}
|
|
262
|
+
/>
|
|
263
|
+
) : (
|
|
264
|
+
declaration
|
|
265
|
+
)}
|
|
266
|
+
</span>
|
|
267
|
+
{!source && (
|
|
268
|
+
<span className="block">
|
|
269
|
+
Source is not available in this view; its existence and
|
|
270
|
+
version are unverified.
|
|
271
|
+
</span>
|
|
272
|
+
)}
|
|
273
|
+
<span className="block mt-1">
|
|
274
|
+
Edit the source for lasting changes. Local edits may persist
|
|
275
|
+
until the source changes, then be overwritten.
|
|
276
|
+
</span>
|
|
277
|
+
</>
|
|
278
|
+
}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
{warnings.length > 0 && (
|
|
282
|
+
<AlertBanner
|
|
283
|
+
variant="warning"
|
|
284
|
+
title="Check reflection configuration"
|
|
285
|
+
message={
|
|
286
|
+
<ul className="list-disc pl-4 space-y-1">
|
|
287
|
+
{warnings.map((warning) => (
|
|
288
|
+
<li key={warning}>{warning}</li>
|
|
289
|
+
))}
|
|
290
|
+
</ul>
|
|
291
|
+
}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|