@64mb/freelens-power 1.2.2 → 1.4.0
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/README.md +3 -1
- package/out/main/common/store/monitoring-preferences-store.js +10 -0
- package/out/main/common/store/monitoring-preferences-store.js.map +1 -1
- package/out/renderer/ai-diagnostic-handoff.js +52 -0
- package/out/renderer/ai-diagnostic-handoff.js.map +1 -0
- package/out/renderer/ai-extras.js +40 -4
- package/out/renderer/ai-extras.js.map +1 -1
- package/out/renderer/ai.js +48 -56
- package/out/renderer/ai.js.map +1 -1
- package/out/renderer/details/hpa-diagnostics-details.js +46 -0
- package/out/renderer/details/hpa-diagnostics-details.js.map +1 -0
- package/out/renderer/details/namespace-health-details.js +91 -0
- package/out/renderer/details/namespace-health-details.js.map +1 -0
- package/out/renderer/details/resource-health-details.js +135 -0
- package/out/renderer/details/resource-health-details.js.map +1 -0
- package/out/renderer/details/service-diagnostics-details.js +163 -0
- package/out/renderer/details/service-diagnostics-details.js.map +1 -0
- package/out/renderer/diagnostic-explain-button.js +16 -0
- package/out/renderer/diagnostic-explain-button.js.map +1 -0
- package/out/renderer/diagnostics-state.js +53 -0
- package/out/renderer/diagnostics-state.js.map +1 -0
- package/out/renderer/diagnostics.css.js +10 -1
- package/out/renderer/diagnostics.css.js.map +1 -1
- package/out/renderer/diagnostics.js +486 -114
- package/out/renderer/diagnostics.js.map +1 -1
- package/out/renderer/incident-copy-button.js +50 -0
- package/out/renderer/incident-copy-button.js.map +1 -0
- package/out/renderer/index.js +91 -19
- package/out/renderer/index.js.map +1 -1
- package/out/renderer/monitoring-history-collection.js +1 -1
- package/out/renderer/monitoring.js +15 -0
- package/out/renderer/monitoring.js.map +1 -1
- package/out/renderer/preferences/monitoring-preferences.js +10 -0
- package/out/renderer/preferences/monitoring-preferences.js.map +1 -1
- package/out/renderer/preferences/preferences.js +98 -0
- package/out/renderer/preferences/preferences.js.map +1 -1
- package/out/renderer/resource-relationships.css.js +7 -0
- package/out/renderer/resource-relationships.css.js.map +1 -0
- package/out/renderer/resource-relationships.js +571 -0
- package/out/renderer/resource-relationships.js.map +1 -0
- package/out/renderer/resource-search-distance.js +24 -0
- package/out/renderer/resource-search-distance.js.map +1 -0
- package/out/renderer/resource-search-fields.js +55 -0
- package/out/renderer/resource-search-fields.js.map +1 -0
- package/out/renderer/resource-search-model.js +193 -0
- package/out/renderer/resource-search-model.js.map +1 -0
- package/out/renderer/resource-search.js +365 -0
- package/out/renderer/resource-search.js.map +1 -0
- package/out/renderer/src/common/hpa-diagnostics.js +24 -0
- package/out/renderer/src/common/hpa-diagnostics.js.map +1 -0
- package/out/renderer/src/common/incident-summary.js +73 -0
- package/out/renderer/src/common/incident-summary.js.map +1 -0
- package/out/renderer/src/common/resource-health.js +92 -0
- package/out/renderer/src/common/resource-health.js.map +1 -0
- package/out/renderer/src/common/resource-relationships.js +155 -0
- package/out/renderer/src/common/resource-relationships.js.map +1 -0
- package/out/renderer/src/common/service-diagnostics.js +50 -0
- package/out/renderer/src/common/service-diagnostics.js.map +1 -0
- package/out/renderer/src/common/store/monitoring-preferences-store.js +10 -0
- package/out/renderer/src/common/store/monitoring-preferences-store.js.map +1 -1
- package/out/renderer/src/common/timeline-groups.js +36 -0
- package/out/renderer/src/common/timeline-groups.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
const require_extensions = require("./_virtual/_global-external_@freelensapp/extensions.js");
|
|
2
|
+
const require__global_external_mobx = require("./_virtual/_global-external_mobx.js");
|
|
3
|
+
const require__global_external_react = require("./_virtual/_global-external_react.js");
|
|
4
|
+
const require__global_external_react_dom = require("./_virtual/_global-external_react-dom.js");
|
|
5
|
+
const require_jsx_runtime = require("./_virtual/_global-external_react/jsx-runtime.js");
|
|
6
|
+
const require_ai_icon = require("./ai-icon.js");
|
|
7
|
+
const require_ai_diagnostic_handoff = require("./ai-diagnostic-handoff.js");
|
|
8
|
+
const require_diagnostic_explain_button = require("./diagnostic-explain-button.js");
|
|
9
|
+
const require_resource_search_fields = require("./resource-search-fields.js");
|
|
10
|
+
const require_resource_search_model = require("./resource-search-model.js");
|
|
11
|
+
//#region src/renderer/resource-search.tsx
|
|
12
|
+
var OPEN = "power-resource-search";
|
|
13
|
+
var activeId = () => require_extensions.Renderer.Catalog.activeCluster.get()?.metadata.uid;
|
|
14
|
+
var inventory = () => [
|
|
15
|
+
[
|
|
16
|
+
"Pod",
|
|
17
|
+
require_extensions.Renderer.K8sApi.podsStore,
|
|
18
|
+
"view_in_ar"
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"Node",
|
|
22
|
+
require_extensions.Renderer.K8sApi.nodesStore,
|
|
23
|
+
"dns"
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
"Deployment",
|
|
27
|
+
require_extensions.Renderer.K8sApi.deploymentStore,
|
|
28
|
+
"layers"
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"StatefulSet",
|
|
32
|
+
require_extensions.Renderer.K8sApi.statefulSetStore,
|
|
33
|
+
"layers"
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"DaemonSet",
|
|
37
|
+
require_extensions.Renderer.K8sApi.daemonSetStore,
|
|
38
|
+
"layers"
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"ReplicaSet",
|
|
42
|
+
require_extensions.Renderer.K8sApi.replicaSetStore,
|
|
43
|
+
"layers"
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
"Job",
|
|
47
|
+
require_extensions.Renderer.K8sApi.jobStore,
|
|
48
|
+
"work"
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"CronJob",
|
|
52
|
+
require_extensions.Renderer.K8sApi.cronJobStore,
|
|
53
|
+
"schedule"
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"Service",
|
|
57
|
+
require_extensions.Renderer.K8sApi.serviceStore,
|
|
58
|
+
"settings_ethernet"
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"Ingress",
|
|
62
|
+
require_extensions.Renderer.K8sApi.ingressStore,
|
|
63
|
+
"alt_route"
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"Namespace",
|
|
67
|
+
require_extensions.Renderer.K8sApi.namespaceStore,
|
|
68
|
+
"folder"
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"HorizontalPodAutoscaler",
|
|
72
|
+
require_extensions.Renderer.K8sApi.hpaStore,
|
|
73
|
+
"swap_vert"
|
|
74
|
+
]
|
|
75
|
+
];
|
|
76
|
+
var icons = new Map(inventory().map(([kind, , icon]) => [kind, icon]));
|
|
77
|
+
function project(kind, objects) {
|
|
78
|
+
return objects.slice(0, 500).flatMap((object) => object.metadata.name ? [{
|
|
79
|
+
kind,
|
|
80
|
+
name: object.metadata.name,
|
|
81
|
+
namespace: object.metadata.namespace,
|
|
82
|
+
labels: Object.fromEntries(Object.entries(object.metadata.labels ?? {}).filter((entry) => typeof entry[1] === "string")),
|
|
83
|
+
path: object.selfLink,
|
|
84
|
+
fields: require_resource_search_fields.projectSearchFields(kind, object)
|
|
85
|
+
}] : []);
|
|
86
|
+
}
|
|
87
|
+
function SearchSnippet({ field, query }) {
|
|
88
|
+
if (!field) return null;
|
|
89
|
+
const token = require_resource_search_model.queryTokens(query).find((token) => field.text.toLocaleLowerCase().includes(token));
|
|
90
|
+
const offset = token ? field.text.toLocaleLowerCase().indexOf(token) : -1;
|
|
91
|
+
return /* @__PURE__ */ require_jsx_runtime.jsxs("small", {
|
|
92
|
+
className: "power-resource-search-snippet",
|
|
93
|
+
children: [
|
|
94
|
+
field.label,
|
|
95
|
+
":",
|
|
96
|
+
" ",
|
|
97
|
+
offset < 0 || !token ? field.text : /* @__PURE__ */ require_jsx_runtime.jsxs(require_jsx_runtime.Fragment, { children: [
|
|
98
|
+
field.text.slice(0, offset),
|
|
99
|
+
/* @__PURE__ */ require_jsx_runtime.jsx("mark", { children: field.text.slice(offset, offset + token.length) }),
|
|
100
|
+
field.text.slice(offset + token.length)
|
|
101
|
+
] })
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function SearchDialog({ close, clusterId }) {
|
|
106
|
+
const [query, setQuery] = require__global_external_react.useState("");
|
|
107
|
+
const [resources, setResources] = require__global_external_react.useState([]);
|
|
108
|
+
const [loading, setLoading] = require__global_external_react.useState(true);
|
|
109
|
+
const [failed, setFailed] = require__global_external_react.useState(0);
|
|
110
|
+
const [selectedPath, setSelectedPath] = require__global_external_react.useState();
|
|
111
|
+
const [error, setError] = require__global_external_react.useState("");
|
|
112
|
+
const dialog = require__global_external_react.useRef(null);
|
|
113
|
+
const input = require__global_external_react.useRef(null);
|
|
114
|
+
const searchIndex = require__global_external_react.useMemo(() => require_resource_search_model.createResourceSearchIndex(resources), [resources]);
|
|
115
|
+
const matches = searchIndex.search(query);
|
|
116
|
+
const selectedResource = matches.find((resource) => resource.path === selectedPath) ?? matches[0];
|
|
117
|
+
const results = matches.slice(0, 100);
|
|
118
|
+
if (selectedResource && !results.includes(selectedResource)) results[results.length - 1] = selectedResource;
|
|
119
|
+
const selected = results.findIndex((resource) => resource.path === selectedResource?.path);
|
|
120
|
+
require__global_external_react.useEffect(() => {
|
|
121
|
+
if (selectedPath !== void 0 && selectedPath !== selectedResource?.path) setSelectedPath(void 0);
|
|
122
|
+
}, [selectedPath, selectedResource?.path]);
|
|
123
|
+
require__global_external_react.useEffect(() => {
|
|
124
|
+
const previous = document.activeElement;
|
|
125
|
+
input.current?.focus();
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeout = window.setTimeout(() => controller.abort(), 15e3);
|
|
128
|
+
let live = true;
|
|
129
|
+
const sources = inventory();
|
|
130
|
+
const parts = sources.map(([kind, store]) => project(kind, store.items));
|
|
131
|
+
setResources(parts.flat());
|
|
132
|
+
let next = 0;
|
|
133
|
+
const worker = async () => {
|
|
134
|
+
while (next < sources.length && live && activeId() === clusterId) {
|
|
135
|
+
const index = next++;
|
|
136
|
+
const [kind, store] = sources[index];
|
|
137
|
+
try {
|
|
138
|
+
const objects = await store.api.list({
|
|
139
|
+
namespace: "",
|
|
140
|
+
reqInit: { signal: controller.signal }
|
|
141
|
+
}, { limit: 500 });
|
|
142
|
+
if (!live || activeId() !== clusterId) return;
|
|
143
|
+
parts[index] = project(kind, objects ?? []);
|
|
144
|
+
setResources(parts.flat());
|
|
145
|
+
} catch {
|
|
146
|
+
if (live && activeId() === clusterId) setFailed((count) => count + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
Promise.all([worker(), worker()]).then(() => {
|
|
151
|
+
if (live) setLoading(false);
|
|
152
|
+
});
|
|
153
|
+
return () => {
|
|
154
|
+
live = false;
|
|
155
|
+
window.clearTimeout(timeout);
|
|
156
|
+
controller.abort();
|
|
157
|
+
if (previous?.isConnected) previous.focus();
|
|
158
|
+
};
|
|
159
|
+
}, [clusterId]);
|
|
160
|
+
require__global_external_react.useEffect(() => {
|
|
161
|
+
document.getElementById(`power-resource-result-${selected}`)?.scrollIntoView?.({ block: "nearest" });
|
|
162
|
+
}, [selected, selectedResource?.path]);
|
|
163
|
+
const open = (resource) => {
|
|
164
|
+
if (activeId() !== clusterId) return;
|
|
165
|
+
Promise.resolve().then(() => require_extensions.Renderer.Navigation.showDetails(resource.path)).then(() => {
|
|
166
|
+
if (activeId() === clusterId) close();
|
|
167
|
+
}).catch(() => setError("Could not open this resource. Try again or refresh the inventory."));
|
|
168
|
+
};
|
|
169
|
+
return /* @__PURE__ */ require_jsx_runtime.jsx("div", {
|
|
170
|
+
className: "power-resource-search-backdrop",
|
|
171
|
+
"data-freelens-power-ai-overlay": "",
|
|
172
|
+
onClick: (event) => event.stopPropagation(),
|
|
173
|
+
onMouseDown: (event) => {
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
if (event.target === event.currentTarget) close();
|
|
176
|
+
},
|
|
177
|
+
children: /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
|
|
178
|
+
ref: dialog,
|
|
179
|
+
role: "dialog",
|
|
180
|
+
"aria-modal": "true",
|
|
181
|
+
"aria-labelledby": "power-resource-search-title",
|
|
182
|
+
className: "power-resource-search-dialog",
|
|
183
|
+
onKeyDown: (event) => {
|
|
184
|
+
if (event.key === "Escape") {
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
close();
|
|
187
|
+
}
|
|
188
|
+
if (event.key === "Tab") {
|
|
189
|
+
const controls = Array.from(dialog.current?.querySelectorAll("input, button:not(:disabled)") ?? []);
|
|
190
|
+
const index = controls.indexOf(document.activeElement);
|
|
191
|
+
if (event.shiftKey && index <= 0) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
controls.at(-1)?.focus();
|
|
194
|
+
} else if (!event.shiftKey && index === controls.length - 1) {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
controls[0]?.focus();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ require_jsx_runtime.jsxs("div", {
|
|
202
|
+
className: "power-resource-search-heading",
|
|
203
|
+
children: [/* @__PURE__ */ require_jsx_runtime.jsxs("strong", {
|
|
204
|
+
id: "power-resource-search-title",
|
|
205
|
+
style: {
|
|
206
|
+
display: "flex",
|
|
207
|
+
alignItems: "center",
|
|
208
|
+
gap: 8
|
|
209
|
+
},
|
|
210
|
+
children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
|
|
211
|
+
material: "search",
|
|
212
|
+
size: 18
|
|
213
|
+
}), "Search resources"]
|
|
214
|
+
}), /* @__PURE__ */ require_jsx_runtime.jsx("button", {
|
|
215
|
+
type: "button",
|
|
216
|
+
"aria-label": "Close resource search",
|
|
217
|
+
onClick: close,
|
|
218
|
+
children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: "close" })
|
|
219
|
+
})]
|
|
220
|
+
}),
|
|
221
|
+
/* @__PURE__ */ require_jsx_runtime.jsx("input", {
|
|
222
|
+
ref: input,
|
|
223
|
+
"aria-label": "Resource name, namespace, kind, label or indexed text",
|
|
224
|
+
"aria-controls": "power-resource-search-results",
|
|
225
|
+
placeholder: "Name, namespace, label, image, condition…",
|
|
226
|
+
maxLength: 300,
|
|
227
|
+
value: query,
|
|
228
|
+
onChange: (event) => {
|
|
229
|
+
setQuery(event.target.value);
|
|
230
|
+
setSelectedPath(void 0);
|
|
231
|
+
},
|
|
232
|
+
onKeyDown: (event) => {
|
|
233
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
setSelectedPath(results[results.length ? (selected + (event.key === "ArrowDown" ? 1 : -1) + results.length) % results.length : 0]?.path);
|
|
236
|
+
}
|
|
237
|
+
if (event.key === "Enter" && !event.nativeEvent.isComposing && results[selected]) {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
open(results[selected]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}),
|
|
243
|
+
/* @__PURE__ */ require_jsx_runtime.jsxs("p", {
|
|
244
|
+
role: "status",
|
|
245
|
+
children: [
|
|
246
|
+
loading ? "Loading supported kinds; cached results are available. " : "Supported kinds loaded. ",
|
|
247
|
+
"Partial inventory: up to 500 objects per kind, subject to permissions. Secrets, ConfigMaps and custom resources are excluded. Full-text index covers safe metadata, images, selectors and status only.",
|
|
248
|
+
failed > 0 && ` ${failed} kinds unavailable; cached results retained.`
|
|
249
|
+
]
|
|
250
|
+
}),
|
|
251
|
+
error && /* @__PURE__ */ require_jsx_runtime.jsx("p", {
|
|
252
|
+
role: "alert",
|
|
253
|
+
children: error
|
|
254
|
+
}),
|
|
255
|
+
/* @__PURE__ */ require_jsx_runtime.jsxs("div", {
|
|
256
|
+
id: "power-resource-search-results",
|
|
257
|
+
role: "list",
|
|
258
|
+
"aria-label": "Matching resources",
|
|
259
|
+
className: "power-resource-search-results",
|
|
260
|
+
children: [results.map((resource, index) => /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
|
|
261
|
+
id: `power-resource-result-${index}`,
|
|
262
|
+
role: "listitem",
|
|
263
|
+
className: `power-resource-search-row ${selected === index ? "selected" : ""}`,
|
|
264
|
+
children: [/* @__PURE__ */ require_jsx_runtime.jsxs("button", {
|
|
265
|
+
className: "power-resource-search-open",
|
|
266
|
+
type: "button",
|
|
267
|
+
onClick: () => open(resource),
|
|
268
|
+
children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: icons.get(resource.kind) ?? "view_in_ar" }), /* @__PURE__ */ require_jsx_runtime.jsxs("span", { children: [
|
|
269
|
+
/* @__PURE__ */ require_jsx_runtime.jsx("strong", { children: resource.name }),
|
|
270
|
+
/* @__PURE__ */ require_jsx_runtime.jsxs("small", { children: [
|
|
271
|
+
resource.kind,
|
|
272
|
+
" · ",
|
|
273
|
+
resource.namespace ?? "cluster scoped"
|
|
274
|
+
] }),
|
|
275
|
+
searchIndex.nameParts(resource) && /* @__PURE__ */ require_jsx_runtime.jsx("small", { children: "Name parts" }),
|
|
276
|
+
searchIndex.similarName(resource) && /* @__PURE__ */ require_jsx_runtime.jsx("small", { children: "Similar name" }),
|
|
277
|
+
/* @__PURE__ */ require_jsx_runtime.jsx(SearchSnippet, {
|
|
278
|
+
field: searchIndex.snippet(resource, query),
|
|
279
|
+
query
|
|
280
|
+
})
|
|
281
|
+
] })]
|
|
282
|
+
}), /* @__PURE__ */ require_jsx_runtime.jsx(require_diagnostic_explain_button.DiagnosticExplainButton, {
|
|
283
|
+
className: "power-resource-search-ai",
|
|
284
|
+
"aria-label": `Ask AI about ${resource.kind} ${resource.name}`,
|
|
285
|
+
title: "Ask AI about this resource",
|
|
286
|
+
onClick: () => {
|
|
287
|
+
if (activeId() !== clusterId) return;
|
|
288
|
+
close();
|
|
289
|
+
require_ai_diagnostic_handoff.explainDiagnosticFinding({
|
|
290
|
+
clusterId,
|
|
291
|
+
title: `${resource.kind}: ${resource.name}`,
|
|
292
|
+
summary: `Resource selected from search. Kind: ${resource.kind}; namespace: ${resource.namespace ?? "cluster scoped"}; name: ${resource.name}. No live diagnostic data was collected.`,
|
|
293
|
+
resource: {
|
|
294
|
+
kind: resource.kind,
|
|
295
|
+
name: resource.name,
|
|
296
|
+
namespace: resource.namespace
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: "auto_awesome" })
|
|
301
|
+
})]
|
|
302
|
+
}, resource.path)), !results.length && /* @__PURE__ */ require_jsx_runtime.jsx("p", { children: loading ? "Searching…" : "No matching resources in the available inventory" })]
|
|
303
|
+
}),
|
|
304
|
+
matches.length > 100 && /* @__PURE__ */ require_jsx_runtime.jsx("p", { children: "Showing 100 matches. Refine your search." }),
|
|
305
|
+
/* @__PURE__ */ require_jsx_runtime.jsx("p", {
|
|
306
|
+
className: "power-resource-search-shortcuts",
|
|
307
|
+
children: "↑↓ Navigate · Enter Open · Esc Close"
|
|
308
|
+
})
|
|
309
|
+
]
|
|
310
|
+
})
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function startResourceSearch() {
|
|
314
|
+
const container = document.createElement("div");
|
|
315
|
+
const style = document.createElement("style");
|
|
316
|
+
style.textContent = `.power-resource-search-backdrop{position:fixed;inset:0;z-index:10000;background:#0007;display:flex;align-items:flex-start;justify-content:center;padding:12vh 20px}.power-resource-search-dialog{width:680px;max-width:100%;max-height:75vh;display:flex;flex-direction:column;padding:20px;gap:12px;background:var(--mainBackground,#222);color:var(--textColorPrimary,#eee);border:1px solid var(--borderColor,#555);border-radius:8px;box-shadow:0 16px 60px #0008}.power-resource-search-heading{display:flex;align-items:center;justify-content:space-between}.power-resource-search-dialog input{padding:12px;font-size:16px;background:var(--inputControlBackground,#333);color:inherit;border:1px solid var(--borderColor,#666);border-radius:4px}.power-resource-search-dialog p{margin:0;font-size:12px;color:var(--textColorSecondary,#bbb)}.power-resource-search-results{overflow:auto;min-height:60px}.power-resource-search-row{display:flex;align-items:center;gap:12px;padding:4px;border-radius:4px}.power-resource-search-row{border-left:3px solid transparent}.power-resource-search-row.selected{background:var(--tableSelectedBackground,#394b66);border-left-color:var(--primary,#668fff)}.power-resource-search-row.selected .Icon{color:var(--primary,#8daaff)}.power-resource-search-row:hover{background:var(--tableHoveredBackground,#ffffff0b)}.power-resource-search-dialog button:hover:not(:disabled){background:#ffffff12;border-radius:4px}.power-resource-search-row>button:last-child{border:1px solid var(--borderColor,#555);border-radius:4px}.power-resource-search-shortcuts{padding-top:8px;border-top:1px solid var(--borderColor,#555)}.power-resource-search-dialog button{min-width:32px;min-height:32px;padding:6px;color:inherit;background:transparent;border:0;cursor:pointer}.power-resource-search-dialog button:disabled{opacity:.45;cursor:default}.power-resource-search-open{display:flex;align-items:center;gap:12px;flex:1;text-align:left;min-width:0}.power-resource-search-open span{min-width:0}.power-resource-search-open strong,.power-resource-search-open small{display:block;overflow-wrap:anywhere}.power-resource-search-open small{opacity:.7}.power-resource-search-row>button.power-resource-search-ai{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:34px;height:34px;min-width:34px;padding:0;background:var(--contentColor,#292d32);border:1px solid var(--borderColor,#666)}.power-resource-search-row.selected>button.power-resource-search-ai{background:var(--mainBackground,#202326);border-color:color-mix(in srgb,var(--textColorPrimary,#ddd) 55%,var(--borderColor,#666));box-shadow:0 1px 3px #0002}.power-resource-search-row.selected>button.power-resource-search-ai .Icon{color:var(--textColorAccent,#eee)}.power-resource-search-row>button.power-resource-search-ai:hover:not(:disabled){background:var(--contentColor,#30343a);border-color:var(--primary,#8daaff);color:var(--primary,#8daaff)}.power-resource-search-row>button.power-resource-search-ai:hover:not(:disabled) .Icon{color:inherit}.power-resource-search-dialog button:focus-visible{outline:2px solid var(--primary,#668fff)}`;
|
|
317
|
+
document.head.append(style);
|
|
318
|
+
document.body.append(container);
|
|
319
|
+
let visible = false;
|
|
320
|
+
const close = () => {
|
|
321
|
+
if (visible) {
|
|
322
|
+
visible = false;
|
|
323
|
+
require__global_external_react_dom.default.unmountComponentAtNode(container);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const show = () => {
|
|
327
|
+
const clusterId = activeId();
|
|
328
|
+
if (window.top === window || !clusterId || visible || document.querySelector("[aria-modal=\"true\"], .Dialog.is-opened")) return;
|
|
329
|
+
visible = true;
|
|
330
|
+
require__global_external_react_dom.default.render(/* @__PURE__ */ require_jsx_runtime.jsx(SearchDialog, {
|
|
331
|
+
close,
|
|
332
|
+
clusterId
|
|
333
|
+
}), container);
|
|
334
|
+
};
|
|
335
|
+
let shift = require_resource_search_model.createDoubleShift();
|
|
336
|
+
const resetShift = () => {
|
|
337
|
+
shift = require_resource_search_model.createDoubleShift();
|
|
338
|
+
};
|
|
339
|
+
const key = (event) => {
|
|
340
|
+
if (event.isComposing || event.key === "Process") {
|
|
341
|
+
resetShift();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (shift(event, performance.now())) show();
|
|
345
|
+
};
|
|
346
|
+
window.addEventListener("blur", resetShift);
|
|
347
|
+
window.addEventListener("keydown", key);
|
|
348
|
+
window.addEventListener("keyup", key);
|
|
349
|
+
window.addEventListener(OPEN, show);
|
|
350
|
+
const dispose = require__global_external_mobx.reaction(activeId, close);
|
|
351
|
+
return () => {
|
|
352
|
+
close();
|
|
353
|
+
dispose();
|
|
354
|
+
window.removeEventListener("blur", resetShift);
|
|
355
|
+
window.removeEventListener("keydown", key);
|
|
356
|
+
window.removeEventListener("keyup", key);
|
|
357
|
+
window.removeEventListener(OPEN, show);
|
|
358
|
+
container.remove();
|
|
359
|
+
style.remove();
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
exports.startResourceSearch = startResourceSearch;
|
|
364
|
+
|
|
365
|
+
//# sourceMappingURL=resource-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-search.js","names":[],"sources":["../../src/renderer/resource-search.tsx"],"sourcesContent":["import { Renderer } from \"@freelensapp/extensions\";\nimport { reaction } from \"mobx\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { explainDiagnosticFinding } from \"./ai-diagnostic-handoff\";\nimport { AiIcon } from \"./ai-icon\";\nimport { DiagnosticExplainButton } from \"./diagnostic-explain-button\";\nimport { projectSearchFields } from \"./resource-search-fields\";\nimport {\n createDoubleShift,\n createResourceSearchIndex,\n queryTokens,\n type SearchResource,\n} from \"./resource-search-model\";\n\nconst OPEN = \"power-resource-search\";\nconst activeId = () => Renderer.Catalog.activeCluster.get()?.metadata.uid;\nconst inventory = () =>\n [\n [\"Pod\", Renderer.K8sApi.podsStore, \"view_in_ar\"],\n [\"Node\", Renderer.K8sApi.nodesStore, \"dns\"],\n [\"Deployment\", Renderer.K8sApi.deploymentStore, \"layers\"],\n [\"StatefulSet\", Renderer.K8sApi.statefulSetStore, \"layers\"],\n [\"DaemonSet\", Renderer.K8sApi.daemonSetStore, \"layers\"],\n [\"ReplicaSet\", Renderer.K8sApi.replicaSetStore, \"layers\"],\n [\"Job\", Renderer.K8sApi.jobStore, \"work\"],\n [\"CronJob\", Renderer.K8sApi.cronJobStore, \"schedule\"],\n [\"Service\", Renderer.K8sApi.serviceStore, \"settings_ethernet\"],\n [\"Ingress\", Renderer.K8sApi.ingressStore, \"alt_route\"],\n [\"Namespace\", Renderer.K8sApi.namespaceStore, \"folder\"],\n [\"HorizontalPodAutoscaler\", Renderer.K8sApi.hpaStore, \"swap_vert\"],\n ] as const;\nconst icons = new Map(inventory().map(([kind, , icon]) => [kind as string, icon]));\nfunction project(\n kind: string,\n objects: {\n metadata: { name?: string; namespace?: string; labels?: Partial<Record<string, string>> };\n selfLink: string;\n }[],\n): SearchResource[] {\n return objects.slice(0, 500).flatMap((object) =>\n object.metadata.name\n ? [\n {\n kind,\n name: object.metadata.name,\n namespace: object.metadata.namespace,\n labels: Object.fromEntries(\n Object.entries(object.metadata.labels ?? {}).filter(\n (entry): entry is [string, string] => typeof entry[1] === \"string\",\n ),\n ),\n path: object.selfLink,\n fields: projectSearchFields(kind, object),\n },\n ]\n : [],\n );\n}\n\nexport function ResourceSearchLauncher() {\n return (\n <button\n type=\"button\"\n title=\"Search resources (double Shift)\"\n aria-label=\"Search resources\"\n onClick={() => window.dispatchEvent(new Event(OPEN))}\n >\n <AiIcon material=\"search\" />\n </button>\n );\n}\n\nfunction SearchSnippet({ field, query }: { field?: { label: string; text: string }; query: string }) {\n if (!field) return null;\n const token = queryTokens(query).find((token) => field.text.toLocaleLowerCase().includes(token));\n const offset = token ? field.text.toLocaleLowerCase().indexOf(token) : -1;\n return (\n <small className=\"power-resource-search-snippet\">\n {field.label}:{\" \"}\n {offset < 0 || !token ? (\n field.text\n ) : (\n <>\n {field.text.slice(0, offset)}\n <mark>{field.text.slice(offset, offset + token.length)}</mark>\n {field.text.slice(offset + token.length)}\n </>\n )}\n </small>\n );\n}\n\nfunction SearchDialog({ close, clusterId }: { close: () => void; clusterId: string }) {\n const [query, setQuery] = useState(\"\");\n const [resources, setResources] = useState<SearchResource[]>([]);\n const [loading, setLoading] = useState(true);\n const [failed, setFailed] = useState(0);\n const [selectedPath, setSelectedPath] = useState<string>();\n const [error, setError] = useState(\"\");\n const dialog = useRef<HTMLDivElement>(null);\n const input = useRef<HTMLInputElement>(null);\n const searchIndex = useMemo(() => createResourceSearchIndex(resources), [resources]);\n const matches = searchIndex.search(query);\n const selectedResource = matches.find((resource) => resource.path === selectedPath) ?? matches[0];\n const results = matches.slice(0, 100);\n if (selectedResource && !results.includes(selectedResource)) results[results.length - 1] = selectedResource;\n const selected = results.findIndex((resource) => resource.path === selectedResource?.path);\n useEffect(() => {\n if (selectedPath !== undefined && selectedPath !== selectedResource?.path) setSelectedPath(undefined);\n }, [selectedPath, selectedResource?.path]);\n useEffect(() => {\n const previous = document.activeElement as HTMLElement | null;\n input.current?.focus();\n const controller = new AbortController();\n const timeout = window.setTimeout(() => controller.abort(), 15000);\n let live = true;\n const sources = inventory();\n const parts = sources.map(([kind, store]) => project(kind, store.items));\n setResources(parts.flat());\n let next = 0;\n const worker = async () => {\n while (next < sources.length && live && activeId() === clusterId) {\n const index = next++;\n const [kind, store] = sources[index];\n try {\n const objects = await store.api.list(\n { namespace: \"\", reqInit: { signal: controller.signal } },\n { limit: 500 },\n );\n if (!live || activeId() !== clusterId) return;\n parts[index] = project(kind, objects ?? []);\n setResources(parts.flat());\n } catch {\n if (live && activeId() === clusterId) setFailed((count) => count + 1);\n }\n }\n };\n void Promise.all([worker(), worker()]).then(() => {\n if (live) setLoading(false);\n });\n return () => {\n live = false;\n window.clearTimeout(timeout);\n controller.abort();\n if (previous?.isConnected) previous.focus();\n };\n }, [clusterId]);\n useEffect(() => {\n document.getElementById(`power-resource-result-${selected}`)?.scrollIntoView?.({ block: \"nearest\" });\n }, [selected, selectedResource?.path]);\n const open = (resource: SearchResource) => {\n if (activeId() !== clusterId) return;\n void Promise.resolve()\n .then(() => Renderer.Navigation.showDetails(resource.path))\n .then(() => {\n if (activeId() === clusterId) close();\n })\n .catch(() => setError(\"Could not open this resource. Try again or refresh the inventory.\"));\n };\n return (\n <div\n className=\"power-resource-search-backdrop\"\n data-freelens-power-ai-overlay=\"\"\n onClick={(event) => event.stopPropagation()}\n onMouseDown={(event) => {\n event.stopPropagation();\n if (event.target === event.currentTarget) close();\n }}\n >\n <div\n ref={dialog}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"power-resource-search-title\"\n className=\"power-resource-search-dialog\"\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n close();\n }\n if (event.key === \"Tab\") {\n const controls = Array.from(\n dialog.current?.querySelectorAll<HTMLElement>(\"input, button:not(:disabled)\") ?? [],\n );\n const index = controls.indexOf(document.activeElement as HTMLElement);\n if (event.shiftKey && index <= 0) {\n event.preventDefault();\n controls.at(-1)?.focus();\n } else if (!event.shiftKey && index === controls.length - 1) {\n event.preventDefault();\n controls[0]?.focus();\n }\n }\n }}\n >\n <div className=\"power-resource-search-heading\">\n <strong id=\"power-resource-search-title\" style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n <AiIcon material=\"search\" size={18} />\n Search resources\n </strong>\n <button type=\"button\" aria-label=\"Close resource search\" onClick={close}>\n <AiIcon material=\"close\" />\n </button>\n </div>\n <input\n ref={input}\n aria-label=\"Resource name, namespace, kind, label or indexed text\"\n aria-controls=\"power-resource-search-results\"\n placeholder=\"Name, namespace, label, image, condition…\"\n maxLength={300}\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setSelectedPath(undefined);\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n const next = results.length\n ? (selected + (event.key === \"ArrowDown\" ? 1 : -1) + results.length) % results.length\n : 0;\n setSelectedPath(results[next]?.path);\n }\n if (event.key === \"Enter\" && !event.nativeEvent.isComposing && results[selected]) {\n event.preventDefault();\n open(results[selected]);\n }\n }}\n />\n <p role=\"status\">\n {loading ? \"Loading supported kinds; cached results are available. \" : \"Supported kinds loaded. \"}Partial\n inventory: up to 500 objects per kind, subject to permissions. Secrets, ConfigMaps and custom resources are\n excluded. Full-text index covers safe metadata, images, selectors and status only.\n {failed > 0 && ` ${failed} kinds unavailable; cached results retained.`}\n </p>\n {error && <p role=\"alert\">{error}</p>}\n <div\n id=\"power-resource-search-results\"\n role=\"list\"\n aria-label=\"Matching resources\"\n className=\"power-resource-search-results\"\n >\n {results.map((resource, index) => (\n <div\n key={resource.path}\n id={`power-resource-result-${index}`}\n role=\"listitem\"\n className={`power-resource-search-row ${selected === index ? \"selected\" : \"\"}`}\n >\n <button className=\"power-resource-search-open\" type=\"button\" onClick={() => open(resource)}>\n <AiIcon material={icons.get(resource.kind) ?? \"view_in_ar\"} />\n <span>\n <strong>{resource.name}</strong>\n <small>\n {resource.kind} · {resource.namespace ?? \"cluster scoped\"}\n </small>\n {searchIndex.nameParts(resource) && <small>Name parts</small>}\n {searchIndex.similarName(resource) && <small>Similar name</small>}\n <SearchSnippet field={searchIndex.snippet(resource, query)} query={query} />\n </span>\n </button>\n <DiagnosticExplainButton\n className=\"power-resource-search-ai\"\n aria-label={`Ask AI about ${resource.kind} ${resource.name}`}\n title=\"Ask AI about this resource\"\n onClick={() => {\n if (activeId() !== clusterId) return;\n close();\n explainDiagnosticFinding({\n clusterId,\n title: `${resource.kind}: ${resource.name}`,\n summary: `Resource selected from search. Kind: ${resource.kind}; namespace: ${resource.namespace ?? \"cluster scoped\"}; name: ${resource.name}. No live diagnostic data was collected.`,\n resource: { kind: resource.kind, name: resource.name, namespace: resource.namespace },\n });\n }}\n >\n <AiIcon material=\"auto_awesome\" />\n </DiagnosticExplainButton>\n </div>\n ))}\n {!results.length && <p>{loading ? \"Searching…\" : \"No matching resources in the available inventory\"}</p>}\n </div>\n {matches.length > 100 && <p>Showing 100 matches. Refine your search.</p>}\n <p className=\"power-resource-search-shortcuts\">↑↓ Navigate · Enter Open · Esc Close</p>\n </div>\n </div>\n );\n}\n\nexport function startResourceSearch(): () => void {\n const container = document.createElement(\"div\");\n const style = document.createElement(\"style\");\n style.textContent = `.power-resource-search-backdrop{position:fixed;inset:0;z-index:10000;background:#0007;display:flex;align-items:flex-start;justify-content:center;padding:12vh 20px}.power-resource-search-dialog{width:680px;max-width:100%;max-height:75vh;display:flex;flex-direction:column;padding:20px;gap:12px;background:var(--mainBackground,#222);color:var(--textColorPrimary,#eee);border:1px solid var(--borderColor,#555);border-radius:8px;box-shadow:0 16px 60px #0008}.power-resource-search-heading{display:flex;align-items:center;justify-content:space-between}.power-resource-search-dialog input{padding:12px;font-size:16px;background:var(--inputControlBackground,#333);color:inherit;border:1px solid var(--borderColor,#666);border-radius:4px}.power-resource-search-dialog p{margin:0;font-size:12px;color:var(--textColorSecondary,#bbb)}.power-resource-search-results{overflow:auto;min-height:60px}.power-resource-search-row{display:flex;align-items:center;gap:12px;padding:4px;border-radius:4px}.power-resource-search-row{border-left:3px solid transparent}.power-resource-search-row.selected{background:var(--tableSelectedBackground,#394b66);border-left-color:var(--primary,#668fff)}.power-resource-search-row.selected .Icon{color:var(--primary,#8daaff)}.power-resource-search-row:hover{background:var(--tableHoveredBackground,#ffffff0b)}.power-resource-search-dialog button:hover:not(:disabled){background:#ffffff12;border-radius:4px}.power-resource-search-row>button:last-child{border:1px solid var(--borderColor,#555);border-radius:4px}.power-resource-search-shortcuts{padding-top:8px;border-top:1px solid var(--borderColor,#555)}.power-resource-search-dialog button{min-width:32px;min-height:32px;padding:6px;color:inherit;background:transparent;border:0;cursor:pointer}.power-resource-search-dialog button:disabled{opacity:.45;cursor:default}.power-resource-search-open{display:flex;align-items:center;gap:12px;flex:1;text-align:left;min-width:0}.power-resource-search-open span{min-width:0}.power-resource-search-open strong,.power-resource-search-open small{display:block;overflow-wrap:anywhere}.power-resource-search-open small{opacity:.7}.power-resource-search-row>button.power-resource-search-ai{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:34px;height:34px;min-width:34px;padding:0;background:var(--contentColor,#292d32);border:1px solid var(--borderColor,#666)}.power-resource-search-row.selected>button.power-resource-search-ai{background:var(--mainBackground,#202326);border-color:color-mix(in srgb,var(--textColorPrimary,#ddd) 55%,var(--borderColor,#666));box-shadow:0 1px 3px #0002}.power-resource-search-row.selected>button.power-resource-search-ai .Icon{color:var(--textColorAccent,#eee)}.power-resource-search-row>button.power-resource-search-ai:hover:not(:disabled){background:var(--contentColor,#30343a);border-color:var(--primary,#8daaff);color:var(--primary,#8daaff)}.power-resource-search-row>button.power-resource-search-ai:hover:not(:disabled) .Icon{color:inherit}.power-resource-search-dialog button:focus-visible{outline:2px solid var(--primary,#668fff)}`;\n document.head.append(style);\n document.body.append(container);\n let visible = false;\n const close = () => {\n if (visible) {\n visible = false;\n ReactDOM.unmountComponentAtNode(container);\n }\n };\n const show = () => {\n const clusterId = activeId();\n if (\n window.top === window ||\n !clusterId ||\n visible ||\n document.querySelector('[aria-modal=\"true\"], .Dialog.is-opened')\n )\n return;\n visible = true;\n ReactDOM.render(<SearchDialog close={close} clusterId={clusterId} />, container);\n };\n let shift = createDoubleShift();\n const resetShift = () => {\n shift = createDoubleShift();\n };\n const key = (event: KeyboardEvent) => {\n if (event.isComposing || event.key === \"Process\") {\n resetShift();\n return;\n }\n if (shift(event, performance.now())) show();\n };\n window.addEventListener(\"blur\", resetShift);\n window.addEventListener(\"keydown\", key);\n window.addEventListener(\"keyup\", key);\n window.addEventListener(OPEN, show);\n const dispose = reaction(activeId, close);\n return () => {\n close();\n dispose();\n window.removeEventListener(\"blur\", resetShift);\n window.removeEventListener(\"keydown\", key);\n window.removeEventListener(\"keyup\", key);\n window.removeEventListener(OPEN, show);\n container.remove();\n style.remove();\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/common/hpa-diagnostics.ts
|
|
2
|
+
function diagnoseHpa(hpa) {
|
|
3
|
+
const min = hpa.spec?.minReplicas ?? 1;
|
|
4
|
+
const max = hpa.spec?.maxReplicas;
|
|
5
|
+
const conditions = hpa.status?.conditions ?? [];
|
|
6
|
+
const evidence = conditions.map((c) => `${c.type}=${c.status}${c.reason ? ` (${c.reason})` : ""}${c.message ? `: ${c.message}` : ""}`);
|
|
7
|
+
const disabled = hpa.status?.currentReplicas === 0 && conditions.some((c) => c.type === "ScalingActive" && c.status === "False" && c.reason === "ScalingDisabled");
|
|
8
|
+
const failed = conditions.some((c) => (c.type === "ScalingActive" || c.type === "AbleToScale") && c.status === "False" && !(disabled && c.type === "ScalingActive" && c.reason === "ScalingDisabled"));
|
|
9
|
+
const limited = conditions.find((c) => c.type === "ScalingLimited" && c.status === "True");
|
|
10
|
+
const maxBlocked = limited?.reason === "TooManyReplicas" && min !== max;
|
|
11
|
+
return {
|
|
12
|
+
state: failed || maxBlocked ? "warning" : "neutral",
|
|
13
|
+
summary: failed ? "Autoscaling reports a controller or metric failure" : disabled ? "Autoscaling is disabled while the target has zero replicas" : min === max ? "Replica count is fixed by equal minimum and maximum" : maxBlocked ? "Desired scale exceeds the configured maximum" : limited?.reason === "TooFewReplicas" ? "Minimum replicas constrain downscaling" : limited ? "Scaling is limited; inspect the controller reason" : conditions.length ? "No scaling blocker reported by available conditions" : "Autoscaling conditions are unavailable",
|
|
14
|
+
evidence,
|
|
15
|
+
current: hpa.status?.currentReplicas,
|
|
16
|
+
desired: hpa.status?.desiredReplicas,
|
|
17
|
+
min,
|
|
18
|
+
max
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
exports.diagnoseHpa = diagnoseHpa;
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=hpa-diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hpa-diagnostics.js","names":[],"sources":["../../../../src/common/hpa-diagnostics.ts"],"sourcesContent":["export interface HpaDiagnosticsInput {\n spec?: { minReplicas?: number; maxReplicas: number };\n status?: {\n currentReplicas?: number;\n desiredReplicas?: number;\n conditions?: { type: string; status: string; reason?: string; message?: string }[];\n };\n}\nexport function diagnoseHpa(hpa: HpaDiagnosticsInput) {\n const min = hpa.spec?.minReplicas ?? 1;\n const max = hpa.spec?.maxReplicas;\n const conditions = hpa.status?.conditions ?? [];\n const evidence = conditions.map(\n (c) => `${c.type}=${c.status}${c.reason ? ` (${c.reason})` : \"\"}${c.message ? `: ${c.message}` : \"\"}`,\n );\n const disabled =\n hpa.status?.currentReplicas === 0 &&\n conditions.some((c) => c.type === \"ScalingActive\" && c.status === \"False\" && c.reason === \"ScalingDisabled\");\n const failed = conditions.some(\n (c) =>\n (c.type === \"ScalingActive\" || c.type === \"AbleToScale\") &&\n c.status === \"False\" &&\n !(disabled && c.type === \"ScalingActive\" && c.reason === \"ScalingDisabled\"),\n );\n const limited = conditions.find((c) => c.type === \"ScalingLimited\" && c.status === \"True\");\n const maxBlocked = limited?.reason === \"TooManyReplicas\" && min !== max;\n const state = failed || maxBlocked ? \"warning\" : \"neutral\";\n const summary = failed\n ? \"Autoscaling reports a controller or metric failure\"\n : disabled\n ? \"Autoscaling is disabled while the target has zero replicas\"\n : min === max\n ? \"Replica count is fixed by equal minimum and maximum\"\n : maxBlocked\n ? \"Desired scale exceeds the configured maximum\"\n : limited?.reason === \"TooFewReplicas\"\n ? \"Minimum replicas constrain downscaling\"\n : limited\n ? \"Scaling is limited; inspect the controller reason\"\n : conditions.length\n ? \"No scaling blocker reported by available conditions\"\n : \"Autoscaling conditions are unavailable\";\n return {\n state,\n summary,\n evidence,\n current: hpa.status?.currentReplicas,\n desired: hpa.status?.desiredReplicas,\n min,\n max,\n };\n}\n"],"mappings":";AAQA,SAAgB,YAAY,KAA0B;CACpD,MAAM,MAAM,IAAI,MAAM,eAAe;CACrC,MAAM,MAAM,IAAI,MAAM;CACtB,MAAM,aAAa,IAAI,QAAQ,cAAc,CAAC;CAC9C,MAAM,WAAW,WAAW,KACzB,MAAM,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS,EAAE,SAAS,KAAK,EAAE,OAAO,KAAK,KAAK,EAAE,UAAU,KAAK,EAAE,YAAY,IACnG;CACA,MAAM,WACJ,IAAI,QAAQ,oBAAoB,KAChC,WAAW,MAAM,MAAM,EAAE,SAAS,mBAAmB,EAAE,WAAW,WAAW,EAAE,WAAW,iBAAiB;CAC7G,MAAM,SAAS,WAAW,MACvB,OACE,EAAE,SAAS,mBAAmB,EAAE,SAAS,kBAC1C,EAAE,WAAW,WACb,EAAE,YAAY,EAAE,SAAS,mBAAmB,EAAE,WAAW,kBAC7D;CACA,MAAM,UAAU,WAAW,MAAM,MAAM,EAAE,SAAS,oBAAoB,EAAE,WAAW,MAAM;CACzF,MAAM,aAAa,SAAS,WAAW,qBAAqB,QAAQ;CAiBpE,OAAO;EACL,OAjBY,UAAU,aAAa,YAAY;EAkB/C,SAjBc,SACZ,uDACA,WACE,+DACA,QAAQ,MACN,wDACA,aACE,iDACA,SAAS,WAAW,mBAClB,2CACA,UACE,sDACA,WAAW,SACT,wDACA;EAId;EACA,SAAS,IAAI,QAAQ;EACrB,SAAS,IAAI,QAAQ;EACrB;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const require_ai_terminal_context = require("./ai-terminal-context.js");
|
|
2
|
+
const require_date_time = require("./date-time.js");
|
|
3
|
+
//#region src/common/incident-summary.ts
|
|
4
|
+
var api = {
|
|
5
|
+
Pod: ["/api/v1", "pods"],
|
|
6
|
+
Node: [
|
|
7
|
+
"/api/v1",
|
|
8
|
+
"nodes",
|
|
9
|
+
"cluster"
|
|
10
|
+
],
|
|
11
|
+
Namespace: [
|
|
12
|
+
"/api/v1",
|
|
13
|
+
"namespaces",
|
|
14
|
+
"cluster"
|
|
15
|
+
],
|
|
16
|
+
PersistentVolume: [
|
|
17
|
+
"/api/v1",
|
|
18
|
+
"persistentvolumes",
|
|
19
|
+
"cluster"
|
|
20
|
+
],
|
|
21
|
+
PersistentVolumeClaim: ["/api/v1", "persistentvolumeclaims"],
|
|
22
|
+
Service: ["/api/v1", "services"],
|
|
23
|
+
HorizontalPodAutoscaler: ["/apis/autoscaling/v2", "horizontalpodautoscalers"],
|
|
24
|
+
Ingress: ["/apis/networking.k8s.io/v1", "ingresses"],
|
|
25
|
+
EndpointSlice: ["/apis/discovery.k8s.io/v1", "endpointslices"],
|
|
26
|
+
Deployment: ["/apis/apps/v1", "deployments"],
|
|
27
|
+
ReplicaSet: ["/apis/apps/v1", "replicasets"],
|
|
28
|
+
StatefulSet: ["/apis/apps/v1", "statefulsets"],
|
|
29
|
+
DaemonSet: ["/apis/apps/v1", "daemonsets"],
|
|
30
|
+
Job: ["/apis/batch/v1", "jobs"],
|
|
31
|
+
CronJob: ["/apis/batch/v1", "cronjobs"]
|
|
32
|
+
};
|
|
33
|
+
function incidentResourcePath(resource) {
|
|
34
|
+
if (!Object.hasOwn(api, resource.kind)) return;
|
|
35
|
+
const route = api[resource.kind];
|
|
36
|
+
const validName = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value);
|
|
37
|
+
if (!validName(resource.name)) return;
|
|
38
|
+
if (route[2] === "cluster") {
|
|
39
|
+
if (resource.namespace !== void 0) return;
|
|
40
|
+
return `${route[0]}/${route[1]}/${encodeURIComponent(resource.name)}`;
|
|
41
|
+
}
|
|
42
|
+
if (!validName(resource.namespace)) return;
|
|
43
|
+
return `${route[0]}/namespaces/${encodeURIComponent(resource.namespace)}/${route[1]}/${encodeURIComponent(resource.name)}`;
|
|
44
|
+
}
|
|
45
|
+
var safe = (value, limit) => require_ai_terminal_context.redactAiTerminalText(value).slice(0, limit);
|
|
46
|
+
function formatIncidentSummary(incident) {
|
|
47
|
+
const date = incident.time === void 0 ? void 0 : new Date(incident.time);
|
|
48
|
+
const offset = date && Number.isFinite(date.getTime()) ? -date.getTimezoneOffset() : void 0;
|
|
49
|
+
const timezone = offset === void 0 ? "" : ` UTC${offset < 0 ? "-" : "+"}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0")}:${String(Math.abs(offset) % 60).padStart(2, "0")}`;
|
|
50
|
+
const links = [...new Set((incident.resources ?? []).slice(0, 12).map(incidentResourcePath).filter((value) => !!value))];
|
|
51
|
+
const lines = [
|
|
52
|
+
`Problem: ${safe(incident.problem, 240)}`,
|
|
53
|
+
...incident.clusterName ? [`Cluster: ${safe(incident.clusterName, 160)}`] : [],
|
|
54
|
+
`Snapshot time: ${date ? require_date_time.formatDateTime(date) : "Unavailable"}${timezone}`,
|
|
55
|
+
"Facts:",
|
|
56
|
+
...incident.facts.slice(0, 12).map((fact) => `- ${safe(fact, 400)}`),
|
|
57
|
+
...links.length ? ["Resource API links (Freelens resource detail paths):", ...links.map((link) => `- ${link}`)] : []
|
|
58
|
+
];
|
|
59
|
+
const footer = "Read-only snapshot; evidence may be partial or stale";
|
|
60
|
+
const selected = [];
|
|
61
|
+
let length = 52;
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (length + line.length + 1 > 8e3) break;
|
|
64
|
+
selected.push(line);
|
|
65
|
+
length += line.length + 1;
|
|
66
|
+
}
|
|
67
|
+
return [...selected, footer].join("\n");
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
exports.formatIncidentSummary = formatIncidentSummary;
|
|
71
|
+
exports.incidentResourcePath = incidentResourcePath;
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=incident-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incident-summary.js","names":[],"sources":["../../../../src/common/incident-summary.ts"],"sourcesContent":["import { redactAiTerminalText } from \"./ai-terminal-context\";\nimport { formatDateTime } from \"./date-time\";\nexport interface IncidentResource {\n kind: string;\n namespace?: string;\n name: string;\n}\nexport interface IncidentSummary {\n clusterName?: string;\n problem: string;\n facts: string[];\n time?: string | number;\n resources?: IncidentResource[];\n}\nconst api: Record<string, [string, string, \"cluster\"?]> = {\n Pod: [\"/api/v1\", \"pods\"],\n Node: [\"/api/v1\", \"nodes\", \"cluster\"],\n Namespace: [\"/api/v1\", \"namespaces\", \"cluster\"],\n PersistentVolume: [\"/api/v1\", \"persistentvolumes\", \"cluster\"],\n PersistentVolumeClaim: [\"/api/v1\", \"persistentvolumeclaims\"],\n Service: [\"/api/v1\", \"services\"],\n HorizontalPodAutoscaler: [\"/apis/autoscaling/v2\", \"horizontalpodautoscalers\"],\n Ingress: [\"/apis/networking.k8s.io/v1\", \"ingresses\"],\n EndpointSlice: [\"/apis/discovery.k8s.io/v1\", \"endpointslices\"],\n Deployment: [\"/apis/apps/v1\", \"deployments\"],\n ReplicaSet: [\"/apis/apps/v1\", \"replicasets\"],\n StatefulSet: [\"/apis/apps/v1\", \"statefulsets\"],\n DaemonSet: [\"/apis/apps/v1\", \"daemonsets\"],\n Job: [\"/apis/batch/v1\", \"jobs\"],\n CronJob: [\"/apis/batch/v1\", \"cronjobs\"],\n};\nexport function incidentResourcePath(resource: IncidentResource): string | undefined {\n if (!Object.hasOwn(api, resource.kind)) return;\n const route = api[resource.kind];\n const validName = (value: string | undefined) =>\n typeof value === \"string\" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value);\n if (!validName(resource.name)) return;\n if (route[2] === \"cluster\") {\n if (resource.namespace !== undefined) return;\n return `${route[0]}/${route[1]}/${encodeURIComponent(resource.name)}`;\n }\n if (!validName(resource.namespace)) return;\n return `${route[0]}/namespaces/${encodeURIComponent(resource.namespace!)}/${route[1]}/${encodeURIComponent(resource.name)}`;\n}\nconst safe = (value: string, limit: number) => redactAiTerminalText(value).slice(0, limit);\nexport function formatIncidentSummary(incident: IncidentSummary): string {\n const date = incident.time === undefined ? undefined : new Date(incident.time);\n const offset = date && Number.isFinite(date.getTime()) ? -date.getTimezoneOffset() : undefined;\n const timezone =\n offset === undefined\n ? \"\"\n : ` UTC${offset < 0 ? \"-\" : \"+\"}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, \"0\")}:${String(Math.abs(offset) % 60).padStart(2, \"0\")}`;\n const links = [\n ...new Set(\n (incident.resources ?? [])\n .slice(0, 12)\n .map(incidentResourcePath)\n .filter((value): value is string => !!value),\n ),\n ];\n const lines = [\n `Problem: ${safe(incident.problem, 240)}`,\n ...(incident.clusterName ? [`Cluster: ${safe(incident.clusterName, 160)}`] : []),\n `Snapshot time: ${date ? formatDateTime(date) : \"Unavailable\"}${timezone}`,\n \"Facts:\",\n ...incident.facts.slice(0, 12).map((fact) => `- ${safe(fact, 400)}`),\n ...(links.length\n ? [\"Resource API links (Freelens resource detail paths):\", ...links.map((link) => `- ${link}`)]\n : []),\n ];\n const footer = \"Read-only snapshot; evidence may be partial or stale\";\n const selected: string[] = [];\n let length = footer.length;\n for (const line of lines) {\n if (length + line.length + 1 > 8000) break;\n selected.push(line);\n length += line.length + 1;\n }\n return [...selected, footer].join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/common/resource-health.ts
|
|
2
|
+
function summarizeObjectHealth(object) {
|
|
3
|
+
const status = object.status;
|
|
4
|
+
const conditions = status?.conditions ?? [];
|
|
5
|
+
const rows = [];
|
|
6
|
+
let ready;
|
|
7
|
+
let issue;
|
|
8
|
+
if (object.kind === "Pod") {
|
|
9
|
+
rows.push({
|
|
10
|
+
name: "Phase",
|
|
11
|
+
value: status?.phase ?? "Unknown"
|
|
12
|
+
});
|
|
13
|
+
const condition = conditions.find((condition) => condition.type === "Ready");
|
|
14
|
+
ready = condition?.status === "True" ? true : condition?.status === "False" ? false : void 0;
|
|
15
|
+
const containers = status?.containerStatuses ?? [];
|
|
16
|
+
rows.push({
|
|
17
|
+
name: "Readiness",
|
|
18
|
+
value: status?.phase === "Succeeded" ? "Completed" : ready === void 0 ? "Readiness unknown" : ready ? "Ready" : "Not ready"
|
|
19
|
+
});
|
|
20
|
+
rows.push({
|
|
21
|
+
name: "Containers ready",
|
|
22
|
+
value: `${containers.filter((container) => container.ready).length}/${object.spec?.containers?.length ?? "?"}`
|
|
23
|
+
});
|
|
24
|
+
rows.push({
|
|
25
|
+
name: "Restarts",
|
|
26
|
+
value: String([...containers, ...status?.initContainerStatuses ?? []].reduce((sum, container) => sum + container.restartCount, 0))
|
|
27
|
+
});
|
|
28
|
+
if (status?.phase === "Succeeded") ready = true;
|
|
29
|
+
if (status?.phase === "Failed") {
|
|
30
|
+
ready = false;
|
|
31
|
+
issue = "Pod failed";
|
|
32
|
+
}
|
|
33
|
+
} else if (object.kind === "Node") {
|
|
34
|
+
const condition = conditions.find((condition) => condition.type === "Ready");
|
|
35
|
+
ready = condition?.status === "True" ? true : condition?.status === "False" ? false : void 0;
|
|
36
|
+
rows.push({
|
|
37
|
+
name: "Readiness",
|
|
38
|
+
value: ready === void 0 ? "Readiness unknown" : ready ? "Ready" : "Not ready"
|
|
39
|
+
});
|
|
40
|
+
rows.push({
|
|
41
|
+
name: "Scheduling",
|
|
42
|
+
value: object.spec?.unschedulable ? "Cordoned" : "Enabled"
|
|
43
|
+
});
|
|
44
|
+
const pressure = conditions.find((condition) => /Pressure$|NetworkUnavailable/.test(condition.type) && condition.status === "True");
|
|
45
|
+
if (pressure) {
|
|
46
|
+
issue = pressure.reason || pressure.type;
|
|
47
|
+
ready = false;
|
|
48
|
+
}
|
|
49
|
+
} else if (object.kind === "Namespace") {
|
|
50
|
+
rows.push({
|
|
51
|
+
name: "Phase",
|
|
52
|
+
value: status?.phase ?? "Unknown"
|
|
53
|
+
});
|
|
54
|
+
ready = status?.phase ? status.phase === "Active" : void 0;
|
|
55
|
+
} else {
|
|
56
|
+
const desired = object.kind === "DaemonSet" ? status?.desiredNumberScheduled : object.spec?.replicas ?? 1;
|
|
57
|
+
const count = object.kind === "DaemonSet" ? status?.numberReady : status?.readyReplicas;
|
|
58
|
+
const observed = status?.observedGeneration;
|
|
59
|
+
const stale = object.metadata.generation !== void 0 && (observed === void 0 || observed < object.metadata.generation);
|
|
60
|
+
ready = status && desired !== void 0 ? (count ?? 0) >= desired && !stale : void 0;
|
|
61
|
+
rows.push({
|
|
62
|
+
name: "Ready replicas",
|
|
63
|
+
value: `${status ? count ?? 0 : "?"}/${desired ?? "?"}`
|
|
64
|
+
});
|
|
65
|
+
const updated = object.kind === "DaemonSet" ? status?.updatedNumberScheduled : status?.updatedReplicas;
|
|
66
|
+
rows.push({
|
|
67
|
+
name: "Updated replicas",
|
|
68
|
+
value: status ? String(updated ?? 0) : "Unknown"
|
|
69
|
+
});
|
|
70
|
+
if (stale) issue = "Controller has not observed the latest generation";
|
|
71
|
+
}
|
|
72
|
+
const failure = conditions.find((condition) => condition.type === "ReplicaFailure" && condition.status === "True" || [
|
|
73
|
+
"Ready",
|
|
74
|
+
"Available",
|
|
75
|
+
"Progressing",
|
|
76
|
+
"PodScheduled"
|
|
77
|
+
].includes(condition.type) && condition.status === "False");
|
|
78
|
+
if (!issue && failure && !(object.kind === "Pod" && status?.phase === "Succeeded")) {
|
|
79
|
+
issue = failure.reason || `${failure.type}: ${failure.status}`;
|
|
80
|
+
ready = false;
|
|
81
|
+
}
|
|
82
|
+
if (!issue && ready === false) issue = object.kind === "Namespace" ? "Namespace is not active" : "Readiness target is not met";
|
|
83
|
+
return {
|
|
84
|
+
state: ready === void 0 ? "unknown" : ready ? "healthy" : "warning",
|
|
85
|
+
rows,
|
|
86
|
+
issue
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
exports.summarizeObjectHealth = summarizeObjectHealth;
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=resource-health.js.map
|