@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.
Files changed (63) hide show
  1. package/README.md +3 -1
  2. package/out/main/common/store/monitoring-preferences-store.js +10 -0
  3. package/out/main/common/store/monitoring-preferences-store.js.map +1 -1
  4. package/out/renderer/ai-diagnostic-handoff.js +52 -0
  5. package/out/renderer/ai-diagnostic-handoff.js.map +1 -0
  6. package/out/renderer/ai-extras.js +40 -4
  7. package/out/renderer/ai-extras.js.map +1 -1
  8. package/out/renderer/ai.js +48 -56
  9. package/out/renderer/ai.js.map +1 -1
  10. package/out/renderer/details/hpa-diagnostics-details.js +46 -0
  11. package/out/renderer/details/hpa-diagnostics-details.js.map +1 -0
  12. package/out/renderer/details/namespace-health-details.js +91 -0
  13. package/out/renderer/details/namespace-health-details.js.map +1 -0
  14. package/out/renderer/details/resource-health-details.js +135 -0
  15. package/out/renderer/details/resource-health-details.js.map +1 -0
  16. package/out/renderer/details/service-diagnostics-details.js +163 -0
  17. package/out/renderer/details/service-diagnostics-details.js.map +1 -0
  18. package/out/renderer/diagnostic-explain-button.js +16 -0
  19. package/out/renderer/diagnostic-explain-button.js.map +1 -0
  20. package/out/renderer/diagnostics-state.js +53 -0
  21. package/out/renderer/diagnostics-state.js.map +1 -0
  22. package/out/renderer/diagnostics.css.js +10 -1
  23. package/out/renderer/diagnostics.css.js.map +1 -1
  24. package/out/renderer/diagnostics.js +486 -114
  25. package/out/renderer/diagnostics.js.map +1 -1
  26. package/out/renderer/incident-copy-button.js +50 -0
  27. package/out/renderer/incident-copy-button.js.map +1 -0
  28. package/out/renderer/index.js +91 -19
  29. package/out/renderer/index.js.map +1 -1
  30. package/out/renderer/monitoring-history-collection.js +1 -1
  31. package/out/renderer/monitoring.js +15 -0
  32. package/out/renderer/monitoring.js.map +1 -1
  33. package/out/renderer/preferences/monitoring-preferences.js +10 -0
  34. package/out/renderer/preferences/monitoring-preferences.js.map +1 -1
  35. package/out/renderer/preferences/preferences.js +98 -0
  36. package/out/renderer/preferences/preferences.js.map +1 -1
  37. package/out/renderer/resource-relationships.css.js +7 -0
  38. package/out/renderer/resource-relationships.css.js.map +1 -0
  39. package/out/renderer/resource-relationships.js +571 -0
  40. package/out/renderer/resource-relationships.js.map +1 -0
  41. package/out/renderer/resource-search-distance.js +24 -0
  42. package/out/renderer/resource-search-distance.js.map +1 -0
  43. package/out/renderer/resource-search-fields.js +55 -0
  44. package/out/renderer/resource-search-fields.js.map +1 -0
  45. package/out/renderer/resource-search-model.js +193 -0
  46. package/out/renderer/resource-search-model.js.map +1 -0
  47. package/out/renderer/resource-search.js +365 -0
  48. package/out/renderer/resource-search.js.map +1 -0
  49. package/out/renderer/src/common/hpa-diagnostics.js +24 -0
  50. package/out/renderer/src/common/hpa-diagnostics.js.map +1 -0
  51. package/out/renderer/src/common/incident-summary.js +73 -0
  52. package/out/renderer/src/common/incident-summary.js.map +1 -0
  53. package/out/renderer/src/common/resource-health.js +92 -0
  54. package/out/renderer/src/common/resource-health.js.map +1 -0
  55. package/out/renderer/src/common/resource-relationships.js +155 -0
  56. package/out/renderer/src/common/resource-relationships.js.map +1 -0
  57. package/out/renderer/src/common/service-diagnostics.js +50 -0
  58. package/out/renderer/src/common/service-diagnostics.js.map +1 -0
  59. package/out/renderer/src/common/store/monitoring-preferences-store.js +10 -0
  60. package/out/renderer/src/common/store/monitoring-preferences-store.js.map +1 -1
  61. package/out/renderer/src/common/timeline-groups.js +36 -0
  62. package/out/renderer/src/common/timeline-groups.js.map +1 -0
  63. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ //#region src/renderer/resource-search-distance.ts
2
+ function boundedNameDistance(left, right, limit) {
3
+ if (left.length > 64 || right.length > 64 || Math.abs(left.length - right.length) > limit) return limit + 1;
4
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
5
+ let beforePrevious = previous;
6
+ for (let row = 1; row <= left.length; row++) {
7
+ const current = Array(right.length + 1).fill(limit + 1);
8
+ current[0] = row;
9
+ let minimum = limit + 1;
10
+ for (let column = Math.max(1, row - limit); column <= Math.min(right.length, row + limit); column++) {
11
+ current[column] = Math.min(previous[column] + 1, current[column - 1] + 1, previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1));
12
+ if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) current[column] = Math.min(current[column], beforePrevious[column - 2] + 1);
13
+ minimum = Math.min(minimum, current[column]);
14
+ }
15
+ if (minimum > limit) return limit + 1;
16
+ beforePrevious = previous;
17
+ previous = current;
18
+ }
19
+ return previous[right.length];
20
+ }
21
+ //#endregion
22
+ exports.boundedNameDistance = boundedNameDistance;
23
+
24
+ //# sourceMappingURL=resource-search-distance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search-distance.js","names":[],"sources":["../../src/renderer/resource-search-distance.ts"],"sourcesContent":["export function boundedNameDistance(left: string, right: string, limit: number): number {\n if (left.length > 64 || right.length > 64 || Math.abs(left.length - right.length) > limit) return limit + 1;\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let beforePrevious = previous;\n for (let row = 1; row <= left.length; row++) {\n const current = Array<number>(right.length + 1).fill(limit + 1);\n current[0] = row;\n let minimum = limit + 1;\n for (let column = Math.max(1, row - limit); column <= Math.min(right.length, row + limit); column++) {\n current[column] = Math.min(\n previous[column] + 1,\n current[column - 1] + 1,\n previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1),\n );\n if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1])\n current[column] = Math.min(current[column], beforePrevious[column - 2] + 1);\n minimum = Math.min(minimum, current[column]);\n }\n if (minimum > limit) return limit + 1;\n beforePrevious = previous;\n previous = current;\n }\n return previous[right.length];\n}\n"],"mappings":";AAAA,SAAgB,oBAAoB,MAAc,OAAe,OAAuB;CACtF,IAAI,KAAK,SAAS,MAAM,MAAM,SAAS,MAAM,KAAK,IAAI,KAAK,SAAS,MAAM,MAAM,IAAI,OAAO,OAAO,QAAQ;CAC1G,IAAI,WAAW,MAAM,KAAK,EAAE,QAAQ,MAAM,SAAS,EAAE,IAAI,GAAG,UAAU,KAAK;CAC3E,IAAI,iBAAiB;CACrB,KAAK,IAAI,MAAM,GAAG,OAAO,KAAK,QAAQ,OAAO;EAC3C,MAAM,UAAU,MAAc,MAAM,SAAS,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;EAC9D,QAAQ,KAAK;EACb,IAAI,UAAU,QAAQ;EACtB,KAAK,IAAI,SAAS,KAAK,IAAI,GAAG,MAAM,KAAK,GAAG,UAAU,KAAK,IAAI,MAAM,QAAQ,MAAM,KAAK,GAAG,UAAU;GACnG,QAAQ,UAAU,KAAK,IACrB,SAAS,UAAU,GACnB,QAAQ,SAAS,KAAK,GACtB,SAAS,SAAS,MAAM,KAAK,MAAM,OAAO,MAAM,SAAS,KAAK,IAAI,EACpE;GACA,IAAI,MAAM,KAAK,SAAS,KAAK,KAAK,MAAM,OAAO,MAAM,SAAS,MAAM,KAAK,MAAM,OAAO,MAAM,SAAS,IACnG,QAAQ,UAAU,KAAK,IAAI,QAAQ,SAAS,eAAe,SAAS,KAAK,CAAC;GAC5E,UAAU,KAAK,IAAI,SAAS,QAAQ,OAAO;EAC7C;EACA,IAAI,UAAU,OAAO,OAAO,QAAQ;EACpC,iBAAiB;EACjB,WAAW;CACb;CACA,OAAO,SAAS,MAAM;AACxB"}
@@ -0,0 +1,55 @@
1
+ const require_ai_terminal_context = require("./src/common/ai-terminal-context.js");
2
+ //#region src/renderer/resource-search-fields.ts
3
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4
+ var array = (value) => Array.isArray(value) ? value.slice(0, 32) : [];
5
+ function projectSearchFields(kind, value) {
6
+ if (kind === "Secret" || kind === "ConfigMap") return [];
7
+ const object = record(value);
8
+ const status = record(object.status);
9
+ const spec = record(object.spec);
10
+ const metadata = record(object.metadata);
11
+ const fields = [];
12
+ let remaining = 4096;
13
+ const add = (label, value, redact = false) => {
14
+ if (typeof value !== "string" || !remaining || fields.length >= 32) return;
15
+ const bounded = value.slice(0, 4096);
16
+ const text = (redact ? require_ai_terminal_context.redactAiTerminalText(bounded) : bounded).slice(0, Math.min(512, remaining));
17
+ if (text) {
18
+ fields.push({
19
+ label,
20
+ text
21
+ });
22
+ remaining -= text.length;
23
+ }
24
+ };
25
+ add("Phase", status.phase);
26
+ for (const condition of array(status.conditions)) {
27
+ const item = record(condition);
28
+ add("Condition", item.type);
29
+ add("Reason", item.reason, true);
30
+ add("Message", item.message, true);
31
+ }
32
+ const template = record(record(spec.template).spec);
33
+ for (const container of [...array(spec.containers), ...array(template.containers)]) {
34
+ const item = record(container);
35
+ add("Container", item.name);
36
+ add("Image", item.image);
37
+ }
38
+ const selector = record(spec.selector);
39
+ for (const [key, value] of Object.entries({
40
+ ...selector,
41
+ ...record(selector.matchLabels)
42
+ }).slice(0, 32)) if (typeof value === "string") add("Selector", `${key}=${value}`);
43
+ const target = record(spec.scaleTargetRef);
44
+ add("Scale target", target.kind);
45
+ add("Scale target", target.name);
46
+ for (const owner of array(metadata.ownerReferences)) {
47
+ add("Owner kind", record(owner).kind);
48
+ add("Owner", record(owner).name);
49
+ }
50
+ return fields;
51
+ }
52
+ //#endregion
53
+ exports.projectSearchFields = projectSearchFields;
54
+
55
+ //# sourceMappingURL=resource-search-fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search-fields.js","names":[],"sources":["../../src/renderer/resource-search-fields.ts"],"sourcesContent":["import { redactAiTerminalText } from \"../common/ai-terminal-context\";\n\nexport interface SearchField {\n label: string;\n text: string;\n}\nconst record = (value: unknown): Record<string, unknown> =>\n value && typeof value === \"object\" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};\nconst array = (value: unknown): unknown[] => (Array.isArray(value) ? value.slice(0, 32) : []);\nexport function projectSearchFields(kind: string, value: unknown): SearchField[] {\n if (kind === \"Secret\" || kind === \"ConfigMap\") return [];\n const object = record(value);\n const status = record(object.status);\n const spec = record(object.spec);\n const metadata = record(object.metadata);\n const fields: SearchField[] = [];\n let remaining = 4096;\n const add = (label: string, value: unknown, redact = false) => {\n if (typeof value !== \"string\" || !remaining || fields.length >= 32) return;\n const bounded = value.slice(0, 4096);\n const text = (redact ? redactAiTerminalText(bounded) : bounded).slice(0, Math.min(512, remaining));\n if (text) {\n fields.push({ label, text });\n remaining -= text.length;\n }\n };\n add(\"Phase\", status.phase);\n for (const condition of array(status.conditions)) {\n const item = record(condition);\n add(\"Condition\", item.type);\n add(\"Reason\", item.reason, true);\n add(\"Message\", item.message, true);\n }\n const template = record(record(spec.template).spec);\n for (const container of [...array(spec.containers), ...array(template.containers)]) {\n const item = record(container);\n add(\"Container\", item.name);\n add(\"Image\", item.image);\n }\n const selector = record(spec.selector);\n for (const [key, value] of Object.entries({ ...selector, ...record(selector.matchLabels) }).slice(0, 32)) {\n if (typeof value === \"string\") add(\"Selector\", `${key}=${value}`);\n }\n const target = record(spec.scaleTargetRef);\n add(\"Scale target\", target.kind);\n add(\"Scale target\", target.name);\n for (const owner of array(metadata.ownerReferences)) {\n add(\"Owner kind\", record(owner).kind);\n add(\"Owner\", record(owner).name);\n }\n return fields;\n}\n"],"mappings":";;AAMA,IAAM,UAAU,UACd,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAK,QAAoC,CAAC;AACtG,IAAM,SAAS,UAA+B,MAAM,QAAQ,KAAK,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC;AAC3F,SAAgB,oBAAoB,MAAc,OAA+B;CAC/E,IAAI,SAAS,YAAY,SAAS,aAAa,OAAO,CAAC;CACvD,MAAM,SAAS,OAAO,KAAK;CAC3B,MAAM,SAAS,OAAO,OAAO,MAAM;CACnC,MAAM,OAAO,OAAO,OAAO,IAAI;CAC/B,MAAM,WAAW,OAAO,OAAO,QAAQ;CACvC,MAAM,SAAwB,CAAC;CAC/B,IAAI,YAAY;CAChB,MAAM,OAAO,OAAe,OAAgB,SAAS,UAAU;EAC7D,IAAI,OAAO,UAAU,YAAY,CAAC,aAAa,OAAO,UAAU,IAAI;EACpE,MAAM,UAAU,MAAM,MAAM,GAAG,IAAI;EACnC,MAAM,QAAQ,SAAS,4BAAA,qBAAqB,OAAO,IAAI,QAAA,CAAS,MAAM,GAAG,KAAK,IAAI,KAAK,SAAS,CAAC;EACjG,IAAI,MAAM;GACR,OAAO,KAAK;IAAE;IAAO;GAAK,CAAC;GAC3B,aAAa,KAAK;EACpB;CACF;CACA,IAAI,SAAS,OAAO,KAAK;CACzB,KAAK,MAAM,aAAa,MAAM,OAAO,UAAU,GAAG;EAChD,MAAM,OAAO,OAAO,SAAS;EAC7B,IAAI,aAAa,KAAK,IAAI;EAC1B,IAAI,UAAU,KAAK,QAAQ,IAAI;EAC/B,IAAI,WAAW,KAAK,SAAS,IAAI;CACnC;CACA,MAAM,WAAW,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,IAAI;CAClD,KAAK,MAAM,aAAa,CAAC,GAAG,MAAM,KAAK,UAAU,GAAG,GAAG,MAAM,SAAS,UAAU,CAAC,GAAG;EAClF,MAAM,OAAO,OAAO,SAAS;EAC7B,IAAI,aAAa,KAAK,IAAI;EAC1B,IAAI,SAAS,KAAK,KAAK;CACzB;CACA,MAAM,WAAW,OAAO,KAAK,QAAQ;CACrC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ;EAAE,GAAG;EAAU,GAAG,OAAO,SAAS,WAAW;CAAE,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,GACrG,IAAI,OAAO,UAAU,UAAU,IAAI,YAAY,GAAG,IAAI,GAAG,OAAO;CAElE,MAAM,SAAS,OAAO,KAAK,cAAc;CACzC,IAAI,gBAAgB,OAAO,IAAI;CAC/B,IAAI,gBAAgB,OAAO,IAAI;CAC/B,KAAK,MAAM,SAAS,MAAM,SAAS,eAAe,GAAG;EACnD,IAAI,cAAc,OAAO,KAAK,CAAC,CAAC,IAAI;EACpC,IAAI,SAAS,OAAO,KAAK,CAAC,CAAC,IAAI;CACjC;CACA,OAAO;AACT"}
@@ -0,0 +1,193 @@
1
+ const require_resource_search_distance = require("./resource-search-distance.js");
2
+ //#region src/renderer/resource-search-model.ts
3
+ var RESOURCE_SEARCH_MAX_DOCUMENT_CHARS = 4096;
4
+ var queryTokens = (query) => query.slice(0, 300).toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
5
+ var metadataText = (resource) => [
6
+ resource.kind,
7
+ resource.name,
8
+ resource.namespace ?? "",
9
+ ...Object.entries(resource.labels).map(([key, value]) => `${key}=${value}`)
10
+ ].join(" ");
11
+ function rankResources(resources, tokens) {
12
+ const score = (resource) => tokens.reduce((total, token) => {
13
+ const name = resource.name.toLocaleLowerCase();
14
+ const kind = resource.kind.toLocaleLowerCase();
15
+ const namespace = (resource.namespace ?? "").toLocaleLowerCase();
16
+ return total + (name === token ? 0 : name.startsWith(token) ? 1 : kind === token ? 2 : name.includes(token) ? 3 : kind.includes(token) ? 4 : namespace.includes(token) ? 5 : 6);
17
+ }, 0);
18
+ return resources.sort((a, b) => score(a) - score(b) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
19
+ }
20
+ function createResourceSearchIndex(resources) {
21
+ const documents = resources.filter((resource) => resource.kind !== "Secret" && resource.kind !== "ConfigMap");
22
+ const vocabulary = /* @__PURE__ */ new Map();
23
+ const names = /* @__PURE__ */ new Map();
24
+ let lastQuery;
25
+ let lastResults = [];
26
+ let similarPaths = /* @__PURE__ */ new Set();
27
+ let partsPaths = /* @__PURE__ */ new Set();
28
+ const nameParts = /* @__PURE__ */ new Map();
29
+ const partVocabulary = /* @__PURE__ */ new Map();
30
+ const snippets = /* @__PURE__ */ new Map();
31
+ const metadata = /* @__PURE__ */ new Map();
32
+ documents.forEach((resource, id) => {
33
+ const parts = resource.name.toLocaleLowerCase().split(/[-._]/).filter(Boolean).slice(0, 16);
34
+ nameParts.set(id, parts);
35
+ for (const part of parts) {
36
+ let ids = partVocabulary.get(part);
37
+ if (!ids) {
38
+ ids = /* @__PURE__ */ new Set();
39
+ partVocabulary.set(part, ids);
40
+ }
41
+ ids.add(id);
42
+ }
43
+ for (const name of new Set([resource.name, ...resource.name.split(/[-.]/)].map((name) => name.toLocaleLowerCase()))) {
44
+ if (name.length < 3 || name.length > 64) continue;
45
+ let bucket = names.get(name.length);
46
+ if (!bucket) {
47
+ bucket = /* @__PURE__ */ new Map();
48
+ names.set(name.length, bucket);
49
+ }
50
+ let ids = bucket.get(name);
51
+ if (!ids) {
52
+ ids = /* @__PURE__ */ new Set();
53
+ bucket.set(name, ids);
54
+ }
55
+ ids.add(id);
56
+ }
57
+ const identity = metadataText(resource).slice(0, RESOURCE_SEARCH_MAX_DOCUMENT_CHARS);
58
+ metadata.set(resource.path, identity.toLocaleLowerCase());
59
+ let remaining = RESOURCE_SEARCH_MAX_DOCUMENT_CHARS - identity.length;
60
+ const fields = [];
61
+ for (const field of resource.fields ?? []) {
62
+ if (remaining <= 1) break;
63
+ const text = field.text.slice(0, remaining - 1);
64
+ fields.push({
65
+ label: field.label,
66
+ text
67
+ });
68
+ remaining -= text.length + 1;
69
+ }
70
+ snippets.set(resource.path, fields);
71
+ for (const word of new Set([identity, ...fields.map((field) => field.text)].join(" ").toLocaleLowerCase().split(/\s+/))) {
72
+ if (!word) continue;
73
+ let ids = vocabulary.get(word);
74
+ if (!ids) {
75
+ ids = /* @__PURE__ */ new Set();
76
+ vocabulary.set(word, ids);
77
+ }
78
+ ids.add(id);
79
+ }
80
+ });
81
+ return {
82
+ search(query) {
83
+ query = query.slice(0, 300);
84
+ if (lastQuery === query) return lastResults;
85
+ lastQuery = query;
86
+ similarPaths = /* @__PURE__ */ new Set();
87
+ partsPaths = /* @__PURE__ */ new Set();
88
+ lastResults = [];
89
+ const tokens = queryTokens(query);
90
+ const penalties = /* @__PURE__ */ new Map();
91
+ let candidates;
92
+ for (const token of tokens) {
93
+ const found = /* @__PURE__ */ new Set();
94
+ for (const [word, ids] of vocabulary) if (word.includes(token)) for (const id of ids) found.add(id);
95
+ if (/^[a-z0-9]+(?:[-._][a-z0-9]+)+$/.test(token)) {
96
+ const parts = token.split(/[-._]/);
97
+ if (parts.length <= 16) {
98
+ let possible;
99
+ for (const part of parts) {
100
+ const ids = partVocabulary.get(part) ?? /* @__PURE__ */ new Set();
101
+ possible = possible === void 0 ? ids : new Set([...possible].filter((id) => ids.has(id)));
102
+ }
103
+ for (const id of possible ?? []) {
104
+ if (found.has(id)) continue;
105
+ let offset = 0;
106
+ for (const part of nameParts.get(id) ?? []) if (part === parts[offset]) offset++;
107
+ if (offset === parts.length) {
108
+ found.add(id);
109
+ penalties.set(id, (penalties.get(id) ?? 0) + 1);
110
+ partsPaths.add(documents[id].path);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ const exact = new Set(found);
116
+ const limit = token.length >= 7 && token.length <= 64 ? 2 : token.length >= 4 && token.length <= 6 ? 1 : 0;
117
+ if (limit) {
118
+ const distances = /* @__PURE__ */ new Map();
119
+ for (let length = token.length - limit; length <= token.length + limit; length++) for (const [name, ids] of names.get(length) ?? []) {
120
+ const distance = require_resource_search_distance.boundedNameDistance(token, name, limit);
121
+ if (distance > limit) continue;
122
+ for (const id of ids) if (!exact.has(id)) distances.set(id, Math.min(distances.get(id) ?? Infinity, distance));
123
+ }
124
+ for (const [id, distance] of distances) {
125
+ found.add(id);
126
+ penalties.set(id, (penalties.get(id) ?? 0) + 10 * distance);
127
+ similarPaths.add(documents[id].path);
128
+ }
129
+ }
130
+ candidates = candidates === void 0 ? found : new Set([...candidates].filter((id) => found.has(id)));
131
+ if (!candidates.size) return [];
132
+ }
133
+ lastResults = rankResources(candidates === void 0 ? [...documents] : [...candidates].map((id) => documents[id]), tokens);
134
+ const penaltyByPath = new Map([...penalties].map(([id, penalty]) => [documents[id].path, penalty]));
135
+ lastResults.sort((left, right) => (penaltyByPath.get(left.path) ?? 0) - (penaltyByPath.get(right.path) ?? 0));
136
+ return lastResults;
137
+ },
138
+ nameParts(resource) {
139
+ return partsPaths.has(resource.path);
140
+ },
141
+ similarName(resource) {
142
+ return similarPaths.has(resource.path);
143
+ },
144
+ snippet(resource, query) {
145
+ const tokens = queryTokens(query);
146
+ const identity = metadata.get(resource.path) ?? "";
147
+ const bodyTokens = tokens.filter((token) => !identity.includes(token));
148
+ if (!bodyTokens.length) return;
149
+ const field = snippets.get(resource.path)?.find((item) => bodyTokens.some((token) => item.text.toLocaleLowerCase().includes(token)));
150
+ if (!field) return;
151
+ const offset = Math.max(0, Math.min(...bodyTokens.map((token) => field.text.toLocaleLowerCase().indexOf(token)).filter((index) => index >= 0)) - 35);
152
+ return {
153
+ label: field.label,
154
+ text: (offset ? "…" : "") + field.text.slice(offset, offset + 150) + (field.text.length > offset + 150 ? "…" : "")
155
+ };
156
+ }
157
+ };
158
+ }
159
+ function createDoubleShift() {
160
+ let last = 0;
161
+ let held = false;
162
+ const otherKeys = /* @__PURE__ */ new Set();
163
+ return (event, now) => {
164
+ const physicalKey = event.code || event.key;
165
+ if (event.type === "keyup") {
166
+ if (event.key === "Shift") held = false;
167
+ else otherKeys.delete(physicalKey);
168
+ if (event.key === "Meta") {
169
+ otherKeys.clear();
170
+ held = false;
171
+ last = 0;
172
+ }
173
+ return false;
174
+ }
175
+ if (event.key !== "Shift") otherKeys.add(physicalKey);
176
+ if (event.key !== "Shift" || otherKeys.size > 0 || event.altKey || event.ctrlKey || event.metaKey) {
177
+ last = 0;
178
+ return false;
179
+ }
180
+ if (event.repeat || held) return false;
181
+ held = true;
182
+ const hit = last > 0 && now - last <= 400;
183
+ last = hit ? 0 : now;
184
+ return hit;
185
+ };
186
+ }
187
+ //#endregion
188
+ exports.RESOURCE_SEARCH_MAX_DOCUMENT_CHARS = RESOURCE_SEARCH_MAX_DOCUMENT_CHARS;
189
+ exports.createDoubleShift = createDoubleShift;
190
+ exports.createResourceSearchIndex = createResourceSearchIndex;
191
+ exports.queryTokens = queryTokens;
192
+
193
+ //# sourceMappingURL=resource-search-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search-model.js","names":[],"sources":["../../src/renderer/resource-search-model.ts"],"sourcesContent":["import { boundedNameDistance } from \"./resource-search-distance\";\n\nimport type { SearchField } from \"./resource-search-fields\";\nexport interface SearchResource {\n kind: string;\n name: string;\n namespace?: string;\n labels: Record<string, string>;\n path: string;\n fields?: SearchField[];\n}\n\nexport const RESOURCE_SEARCH_MAX_DOCUMENT_CHARS = 4096;\nexport const queryTokens = (query: string) =>\n query.slice(0, 300).toLocaleLowerCase().trim().split(/\\s+/).filter(Boolean);\nconst metadataText = (resource: SearchResource) =>\n [\n resource.kind,\n resource.name,\n resource.namespace ?? \"\",\n ...Object.entries(resource.labels).map(([key, value]) => `${key}=${value}`),\n ].join(\" \");\nfunction rankResources(resources: SearchResource[], tokens: string[]) {\n const score = (resource: SearchResource) =>\n tokens.reduce((total, token) => {\n const name = resource.name.toLocaleLowerCase();\n const kind = resource.kind.toLocaleLowerCase();\n const namespace = (resource.namespace ?? \"\").toLocaleLowerCase();\n return (\n total +\n (name === token\n ? 0\n : name.startsWith(token)\n ? 1\n : kind === token\n ? 2\n : name.includes(token)\n ? 3\n : kind.includes(token)\n ? 4\n : namespace.includes(token)\n ? 5\n : 6)\n );\n }, 0);\n\n return resources.sort((a, b) => score(a) - score(b) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));\n}\n\nexport function createResourceSearchIndex(resources: SearchResource[]) {\n const documents = resources.filter((resource) => resource.kind !== \"Secret\" && resource.kind !== \"ConfigMap\");\n const vocabulary = new Map<string, Set<number>>();\n const names = new Map<number, Map<string, Set<number>>>();\n let lastQuery: string | undefined;\n let lastResults: SearchResource[] = [];\n let similarPaths = new Set<string>();\n let partsPaths = new Set<string>();\n const nameParts = new Map<number, string[]>();\n const partVocabulary = new Map<string, Set<number>>();\n const snippets = new Map<string, SearchField[]>();\n const metadata = new Map<string, string>();\n documents.forEach((resource, id) => {\n const parts = resource.name.toLocaleLowerCase().split(/[-._]/).filter(Boolean).slice(0, 16);\n nameParts.set(id, parts);\n for (const part of parts) {\n let ids = partVocabulary.get(part);\n if (!ids) {\n ids = new Set();\n partVocabulary.set(part, ids);\n }\n ids.add(id);\n }\n for (const name of new Set(\n [resource.name, ...resource.name.split(/[-.]/)].map((name) => name.toLocaleLowerCase()),\n )) {\n if (name.length < 3 || name.length > 64) continue;\n let bucket = names.get(name.length);\n if (!bucket) {\n bucket = new Map();\n names.set(name.length, bucket);\n }\n let ids = bucket.get(name);\n if (!ids) {\n ids = new Set();\n bucket.set(name, ids);\n }\n ids.add(id);\n }\n const identity = metadataText(resource).slice(0, RESOURCE_SEARCH_MAX_DOCUMENT_CHARS);\n metadata.set(resource.path, identity.toLocaleLowerCase());\n let remaining = RESOURCE_SEARCH_MAX_DOCUMENT_CHARS - identity.length;\n const fields: SearchField[] = [];\n for (const field of resource.fields ?? []) {\n if (remaining <= 1) break;\n const text = field.text.slice(0, remaining - 1);\n fields.push({ label: field.label, text });\n remaining -= text.length + 1;\n }\n snippets.set(resource.path, fields);\n for (const word of new Set(\n [identity, ...fields.map((field) => field.text)].join(\" \").toLocaleLowerCase().split(/\\s+/),\n )) {\n if (!word) continue;\n let ids = vocabulary.get(word);\n if (!ids) {\n ids = new Set();\n vocabulary.set(word, ids);\n }\n ids.add(id);\n }\n });\n return {\n search(query: string): SearchResource[] {\n query = query.slice(0, 300);\n if (lastQuery === query) return lastResults;\n lastQuery = query;\n similarPaths = new Set();\n partsPaths = new Set();\n lastResults = [];\n const tokens = queryTokens(query);\n const penalties = new Map<number, number>();\n let candidates: Set<number> | undefined;\n for (const token of tokens) {\n const found = new Set<number>();\n for (const [word, ids] of vocabulary) if (word.includes(token)) for (const id of ids) found.add(id);\n if (/^[a-z0-9]+(?:[-._][a-z0-9]+)+$/.test(token)) {\n const parts = token.split(/[-._]/);\n if (parts.length <= 16) {\n let possible: Set<number> | undefined;\n for (const part of parts) {\n const ids = partVocabulary.get(part) ?? new Set<number>();\n possible = possible === undefined ? ids : new Set([...possible].filter((id) => ids.has(id)));\n }\n for (const id of possible ?? []) {\n if (found.has(id)) continue;\n let offset = 0;\n for (const part of nameParts.get(id) ?? []) if (part === parts[offset]) offset++;\n if (offset === parts.length) {\n found.add(id);\n penalties.set(id, (penalties.get(id) ?? 0) + 1);\n partsPaths.add(documents[id].path);\n }\n }\n }\n }\n const exact = new Set(found);\n const limit = token.length >= 7 && token.length <= 64 ? 2 : token.length >= 4 && token.length <= 6 ? 1 : 0;\n if (limit) {\n const distances = new Map<number, number>();\n for (let length = token.length - limit; length <= token.length + limit; length++) {\n for (const [name, ids] of names.get(length) ?? []) {\n const distance = boundedNameDistance(token, name, limit);\n if (distance > limit) continue;\n for (const id of ids)\n if (!exact.has(id)) distances.set(id, Math.min(distances.get(id) ?? Infinity, distance));\n }\n }\n for (const [id, distance] of distances) {\n found.add(id);\n penalties.set(id, (penalties.get(id) ?? 0) + 10 * distance);\n similarPaths.add(documents[id].path);\n }\n }\n candidates = candidates === undefined ? found : new Set([...candidates].filter((id) => found.has(id)));\n if (!candidates.size) return [];\n }\n lastResults = rankResources(\n candidates === undefined ? [...documents] : [...candidates].map((id) => documents[id]),\n tokens,\n );\n const penaltyByPath = new Map([...penalties].map(([id, penalty]) => [documents[id].path, penalty]));\n lastResults.sort((left, right) => (penaltyByPath.get(left.path) ?? 0) - (penaltyByPath.get(right.path) ?? 0));\n return lastResults;\n },\n nameParts(resource: SearchResource): boolean {\n return partsPaths.has(resource.path);\n },\n similarName(resource: SearchResource): boolean {\n return similarPaths.has(resource.path);\n },\n snippet(resource: SearchResource, query: string): SearchField | undefined {\n const tokens = queryTokens(query);\n const identity = metadata.get(resource.path) ?? \"\";\n const bodyTokens = tokens.filter((token) => !identity.includes(token));\n if (!bodyTokens.length) return;\n const field = snippets\n .get(resource.path)\n ?.find((item) => bodyTokens.some((token) => item.text.toLocaleLowerCase().includes(token)));\n if (!field) return;\n const offset = Math.max(\n 0,\n Math.min(\n ...bodyTokens.map((token) => field.text.toLocaleLowerCase().indexOf(token)).filter((index) => index >= 0),\n ) - 35,\n );\n return {\n label: field.label,\n text:\n (offset ? \"…\" : \"\") + field.text.slice(offset, offset + 150) + (field.text.length > offset + 150 ? \"…\" : \"\"),\n };\n },\n };\n}\n\nexport function matchResources(resources: SearchResource[], query: string): SearchResource[] {\n return createResourceSearchIndex(resources).search(query);\n}\n\nexport function createDoubleShift() {\n let last = 0;\n let held = false;\n const otherKeys = new Set<string>();\n return (\n event: Pick<KeyboardEvent, \"key\" | \"type\" | \"repeat\" | \"altKey\" | \"ctrlKey\" | \"metaKey\"> &\n Partial<Pick<KeyboardEvent, \"code\">>,\n now: number,\n ) => {\n const physicalKey = event.code || event.key;\n if (event.type === \"keyup\") {\n if (event.key === \"Shift\") held = false;\n else otherKeys.delete(physicalKey);\n if (event.key === \"Meta\") {\n otherKeys.clear();\n held = false;\n last = 0;\n }\n return false;\n }\n if (event.key !== \"Shift\") otherKeys.add(physicalKey);\n if (event.key !== \"Shift\" || otherKeys.size > 0 || event.altKey || event.ctrlKey || event.metaKey) {\n last = 0;\n return false;\n }\n if (event.repeat || held) return false;\n held = true;\n const hit = last > 0 && now - last <= 400;\n last = hit ? 0 : now;\n return hit;\n };\n}\n"],"mappings":"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"}