@red-hat-developer-hub/plugin-cost-management-common 2.0.1

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 (62) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +17 -0
  3. package/dist/DefaultApiClient.cjs.js +8 -0
  4. package/dist/DefaultApiClient.cjs.js.map +1 -0
  5. package/dist/DefaultApiClient.d.ts +83 -0
  6. package/dist/DefaultApiClient.esm.js +2 -0
  7. package/dist/DefaultApiClient.esm.js.map +1 -0
  8. package/dist/clients/cost-management/CostManagementSlimClient.cjs.js +378 -0
  9. package/dist/clients/cost-management/CostManagementSlimClient.cjs.js.map +1 -0
  10. package/dist/clients/cost-management/CostManagementSlimClient.esm.js +372 -0
  11. package/dist/clients/cost-management/CostManagementSlimClient.esm.js.map +1 -0
  12. package/dist/clients/optimizations/OptimizationsClient.cjs.js +99 -0
  13. package/dist/clients/optimizations/OptimizationsClient.cjs.js.map +1 -0
  14. package/dist/clients/optimizations/OptimizationsClient.esm.js +92 -0
  15. package/dist/clients/optimizations/OptimizationsClient.esm.js.map +1 -0
  16. package/dist/clients/orchestrator-slim/OrchestratorSlimClient.cjs.js +107 -0
  17. package/dist/clients/orchestrator-slim/OrchestratorSlimClient.cjs.js.map +1 -0
  18. package/dist/clients/orchestrator-slim/OrchestratorSlimClient.esm.js +105 -0
  19. package/dist/clients/orchestrator-slim/OrchestratorSlimClient.esm.js.map +1 -0
  20. package/dist/clients.cjs.js +12 -0
  21. package/dist/clients.cjs.js.map +1 -0
  22. package/dist/clients.d.ts +432 -0
  23. package/dist/clients.esm.js +4 -0
  24. package/dist/clients.esm.js.map +1 -0
  25. package/dist/generated/apis/DefaultApi.client.cjs.js +91 -0
  26. package/dist/generated/apis/DefaultApi.client.cjs.js.map +1 -0
  27. package/dist/generated/apis/DefaultApi.client.esm.js +66 -0
  28. package/dist/generated/apis/DefaultApi.client.esm.js.map +1 -0
  29. package/dist/generated/pluginId.cjs.js +6 -0
  30. package/dist/generated/pluginId.cjs.js.map +1 -0
  31. package/dist/generated/pluginId.esm.js +4 -0
  32. package/dist/generated/pluginId.esm.js.map +1 -0
  33. package/dist/index.cjs.js +14 -0
  34. package/dist/index.cjs.js.map +1 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.esm.js +5 -0
  37. package/dist/index.esm.js.map +1 -0
  38. package/dist/json-utils.cjs.js +22 -0
  39. package/dist/json-utils.cjs.js.map +1 -0
  40. package/dist/json-utils.d.ts +46 -0
  41. package/dist/json-utils.esm.js +20 -0
  42. package/dist/json-utils.esm.js.map +1 -0
  43. package/dist/models.cjs.js +3 -0
  44. package/dist/models.cjs.js.map +1 -0
  45. package/dist/models.d.ts +25 -0
  46. package/dist/models.esm.js +2 -0
  47. package/dist/models.esm.js.map +1 -0
  48. package/dist/permissions.cjs.js +47 -0
  49. package/dist/permissions.cjs.js.map +1 -0
  50. package/dist/permissions.d.ts +24 -0
  51. package/dist/permissions.esm.js +36 -0
  52. package/dist/permissions.esm.js.map +1 -0
  53. package/dist/types/RecommendationList.model.d-J74WXrJI.d.ts +414 -0
  54. package/dist/util/lib/json.cjs.js +20 -0
  55. package/dist/util/lib/json.cjs.js.map +1 -0
  56. package/dist/util/lib/json.esm.js +15 -0
  57. package/dist/util/lib/json.esm.js.map +1 -0
  58. package/dist/util/lib/json_node.cjs.js +191 -0
  59. package/dist/util/lib/json_node.cjs.js.map +1 -0
  60. package/dist/util/lib/json_node.esm.js +189 -0
  61. package/dist/util/lib/json_node.esm.js.map +1 -0
  62. package/package.json +124 -0
@@ -0,0 +1,372 @@
1
+ import crossFetch from 'cross-fetch';
2
+ import { pluginId } from '../../generated/pluginId.esm.js';
3
+
4
+ class CostManagementSlimClient {
5
+ discoveryApi;
6
+ fetchApi;
7
+ constructor(options) {
8
+ this.discoveryApi = options.discoveryApi;
9
+ this.fetchApi = options.fetchApi ?? { fetch: crossFetch };
10
+ }
11
+ async getCostManagementReport(request) {
12
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
13
+ const uri = "/proxy/reports/openshift/costs/";
14
+ const queryParams = this.buildCostManagementQueryParams(request);
15
+ const queryString = queryParams.toString();
16
+ const queryPart = queryString ? `?${queryString}` : "";
17
+ const url = `${baseUrl}${uri}${queryPart}`;
18
+ return await this.fetchViaBackendProxy(url);
19
+ }
20
+ async downloadCostManagementReport(request) {
21
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
22
+ const uri = "/proxy/reports/openshift/costs/";
23
+ const downloadRequest = {
24
+ query: {
25
+ ...request.query,
26
+ "filter[limit]": 0
27
+ }
28
+ };
29
+ delete downloadRequest.query["filter[offset]"];
30
+ const queryParams = this.buildCostManagementQueryParams(downloadRequest);
31
+ const queryString = queryParams.toString();
32
+ const queryPart = queryString ? `?${queryString}` : "";
33
+ const url = `${baseUrl}${uri}${queryPart}`;
34
+ const acceptHeader = request.format === "csv" ? "text/csv" : "application/json";
35
+ const response = await this.fetchApi.fetch(url, {
36
+ headers: {
37
+ Accept: acceptHeader
38
+ },
39
+ method: "GET"
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error(response.statusText);
43
+ }
44
+ const responseContentType = response.headers.get("content-type") || "";
45
+ const responseText = await response.text();
46
+ let data;
47
+ let mimeType;
48
+ if (request.format === "csv") {
49
+ data = responseText;
50
+ mimeType = "text/csv";
51
+ } else {
52
+ if (responseContentType.includes("application/json")) {
53
+ const jsonData = JSON.parse(responseText);
54
+ data = JSON.stringify(jsonData, null, 2);
55
+ } else {
56
+ data = this.convertCsvToJson(responseText);
57
+ }
58
+ mimeType = "application/json";
59
+ }
60
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "-");
61
+ const extension = request.format;
62
+ const filename = `openshift-costs-${timestamp}.${extension}`;
63
+ const blob = new Blob([data], { type: mimeType });
64
+ const downloadUrl = URL.createObjectURL(blob);
65
+ const link = document.createElement("a");
66
+ link.href = downloadUrl;
67
+ link.download = filename;
68
+ document.body.appendChild(link);
69
+ link.click();
70
+ link.remove();
71
+ URL.revokeObjectURL(downloadUrl);
72
+ }
73
+ /**
74
+ * Converts CSV string to JSON string
75
+ * @param csv - The CSV data as a string
76
+ * @returns JSON string representation of the CSV data
77
+ */
78
+ convertCsvToJson(csv) {
79
+ const normalizedCsv = csv.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
80
+ const lines = normalizedCsv.trim().split("\n");
81
+ if (lines.length === 0) {
82
+ return JSON.stringify({ data: [] }, null, 2);
83
+ }
84
+ const headers = this.parseCsvLine(lines[0]);
85
+ const data = [];
86
+ for (let i = 1; i < lines.length; i++) {
87
+ const values = this.parseCsvLine(lines[i]);
88
+ const row = {};
89
+ for (let j = 0; j < headers.length; j++) {
90
+ const header = headers[j];
91
+ const value = values[j] || "";
92
+ if (value === "") {
93
+ row[header] = null;
94
+ } else if (!Number.isNaN(Number(value)) && value !== "") {
95
+ row[header] = Number(value);
96
+ } else {
97
+ row[header] = value;
98
+ }
99
+ }
100
+ data.push(row);
101
+ }
102
+ return JSON.stringify({ data }, null, 2);
103
+ }
104
+ /**
105
+ * Parses a CSV line handling quoted values
106
+ * @param line - A single line from CSV
107
+ * @returns Array of values
108
+ */
109
+ parseCsvLine(line) {
110
+ const result = [];
111
+ let current = "";
112
+ let inQuotes = false;
113
+ let skipNext = false;
114
+ for (let i = 0; i < line.length; i++) {
115
+ if (skipNext) {
116
+ skipNext = false;
117
+ continue;
118
+ }
119
+ const char = line[i];
120
+ if (char === '"') {
121
+ if (inQuotes && line[i + 1] === '"') {
122
+ current += '"';
123
+ skipNext = true;
124
+ } else {
125
+ inQuotes = !inQuotes;
126
+ }
127
+ } else if (char === "," && !inQuotes) {
128
+ result.push(current);
129
+ current = "";
130
+ } else {
131
+ current += char;
132
+ }
133
+ }
134
+ result.push(current);
135
+ return result;
136
+ }
137
+ /**
138
+ * Builds query parameters for cost management report request
139
+ * @param request - The cost management request
140
+ * @returns URLSearchParams with all query parameters
141
+ */
142
+ buildCostManagementQueryParams(request) {
143
+ const queryParams = new URLSearchParams();
144
+ this.appendBasicQueryParams(queryParams, request);
145
+ this.appendDynamicParams(queryParams, request, "group_by[");
146
+ this.appendDynamicParams(queryParams, request, "order_by[");
147
+ this.appendDynamicFilterParams(queryParams, request);
148
+ this.appendDynamicExcludeParams(queryParams, request);
149
+ return queryParams;
150
+ }
151
+ /**
152
+ * Appends basic query parameters (currency, delta, and standard filters)
153
+ */
154
+ appendBasicQueryParams(queryParams, request) {
155
+ const basicParams = [
156
+ { key: "currency", paramName: "currency" },
157
+ { key: "delta", paramName: "delta" },
158
+ { key: "category", paramName: "category" },
159
+ {
160
+ key: "filter[limit]",
161
+ paramName: "filter[limit]",
162
+ needsStringConversion: true
163
+ },
164
+ {
165
+ key: "filter[offset]",
166
+ paramName: "filter[offset]",
167
+ needsStringConversion: true
168
+ },
169
+ { key: "filter[resolution]", paramName: "filter[resolution]" },
170
+ {
171
+ key: "filter[time_scope_units]",
172
+ paramName: "filter[time_scope_units]"
173
+ },
174
+ {
175
+ key: "filter[time_scope_value]",
176
+ paramName: "filter[time_scope_value]",
177
+ needsStringConversion: true
178
+ },
179
+ {
180
+ key: "filter[exact:cluster]",
181
+ paramName: "filter[exact:cluster]"
182
+ }
183
+ ];
184
+ for (const { key, paramName, needsStringConversion } of basicParams) {
185
+ const value = request.query[key];
186
+ if (value !== void 0 && value !== null && value !== "") {
187
+ queryParams.append(
188
+ paramName,
189
+ needsStringConversion ? String(value) : value
190
+ );
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Appends dynamic parameters that match a specific prefix (e.g., 'group_by[', 'order_by[')
196
+ */
197
+ appendDynamicParams(queryParams, request, prefix) {
198
+ for (const key of Object.keys(request.query)) {
199
+ if (key.startsWith(prefix) && key.endsWith("]")) {
200
+ const value = request.query[key];
201
+ if (value) {
202
+ queryParams.append(key, String(value));
203
+ }
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * Appends dynamic filter parameters, excluding those already handled explicitly
209
+ */
210
+ appendDynamicFilterParams(queryParams, request) {
211
+ const handledFilterKeys = /* @__PURE__ */ new Set([
212
+ "filter[limit]",
213
+ "filter[offset]",
214
+ "filter[resolution]",
215
+ "filter[time_scope_units]",
216
+ "filter[time_scope_value]"
217
+ ]);
218
+ for (const key of Object.keys(request.query)) {
219
+ if (key.startsWith("filter[") && key.endsWith("]") && !handledFilterKeys.has(key)) {
220
+ const value = request.query[key];
221
+ if (value) {
222
+ queryParams.append(key, String(value));
223
+ }
224
+ }
225
+ }
226
+ }
227
+ /**
228
+ * Appends dynamic exclude parameters (e.g., exclude[project], exclude[cluster], exclude[tag:key])
229
+ */
230
+ appendDynamicExcludeParams(queryParams, request) {
231
+ for (const key of Object.keys(request.query)) {
232
+ if (key.startsWith("exclude[") && key.endsWith("]")) {
233
+ const value = request.query[key];
234
+ if (value) {
235
+ queryParams.append(key, String(value));
236
+ }
237
+ }
238
+ }
239
+ }
240
+ /**
241
+ * Search OpenShift projects
242
+ * @param search - Search term to filter projects
243
+ * @param options - Optional request options including token and limit
244
+ * @param options.token - Bearer token for authentication (backend use)
245
+ * @param options.limit - Maximum number of results to return (default: 10, use large number for all)
246
+ */
247
+ async searchOpenShiftProjects(search = "", options) {
248
+ const url = await this.buildResourceTypeUrl(
249
+ "openshift-projects",
250
+ search,
251
+ options?.limit
252
+ );
253
+ if (options?.token) {
254
+ return await this.fetchWithExternalToken(url, options.token);
255
+ }
256
+ return await this.fetchResourceType(url);
257
+ }
258
+ /**
259
+ * Search OpenShift clusters
260
+ * @param search - Search term to filter clusters
261
+ */
262
+ async searchOpenShiftClusters(search = "", options) {
263
+ const url = await this.buildResourceTypeUrl(
264
+ "openshift-clusters",
265
+ search,
266
+ options?.limit
267
+ );
268
+ if (options?.token) {
269
+ return await this.fetchWithExternalToken(url, options.token);
270
+ }
271
+ return await this.fetchResourceType(url);
272
+ }
273
+ /**
274
+ * Search OpenShift nodes
275
+ * @param search - Search term to filter nodes
276
+ */
277
+ async searchOpenShiftNodes(search = "") {
278
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
279
+ const searchParam = search ? `?search=${encodeURIComponent(search)}` : "";
280
+ const url = `${baseUrl}/proxy/resource-types/openshift-nodes/${searchParam}`;
281
+ return await this.fetchResourceType(url);
282
+ }
283
+ /**
284
+ * Get OpenShift tags
285
+ * @param timeScopeValue - Time scope value (-1 for month-to-date, -2 for previous-month). Defaults to -1.
286
+ * @returns TypedResponse with array of tag strings
287
+ */
288
+ async getOpenShiftTags(timeScopeValue = -1) {
289
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
290
+ const url = `${baseUrl}/proxy/tags/openshift/?filter[time_scope_value]=${timeScopeValue}&key_only=true&limit=1000`;
291
+ return await this.fetchViaBackendProxy(url);
292
+ }
293
+ /**
294
+ * Get OpenShift tag values for a specific tag key
295
+ * @param tagKey - The tag key to get values for
296
+ * @param timeScopeValue - Time scope value (-1 for month-to-date, -2 for previous-month). Defaults to -1.
297
+ * @returns TypedResponse with array of tag value objects
298
+ */
299
+ async getOpenShiftTagValues(tagKey, timeScopeValue = -1) {
300
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
301
+ const url = `${baseUrl}/proxy/tags/openshift/?filter[key]=${encodeURIComponent(
302
+ tagKey
303
+ )}&filter[time_scope_value]=${timeScopeValue}`;
304
+ return await this.fetchViaBackendProxy(url);
305
+ }
306
+ async fetchResourceType(url) {
307
+ return await this.fetchViaBackendProxy(url);
308
+ }
309
+ /**
310
+ * Builds resource type URL routed through the backend secure proxy
311
+ */
312
+ async buildResourceTypeUrl(resourceType, search, limit) {
313
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
314
+ const params = new URLSearchParams();
315
+ if (search) {
316
+ params.append("search", search);
317
+ }
318
+ if (limit !== void 0) {
319
+ params.append("limit", String(limit));
320
+ }
321
+ const queryString = params.toString();
322
+ const queryPart = queryString ? `?${queryString}` : "";
323
+ return `${baseUrl}/proxy/resource-types/${resourceType}/${queryPart}`;
324
+ }
325
+ /**
326
+ * Fetches with externally provided token (for backend use).
327
+ * This path bypasses the secure proxy since it's already server-side.
328
+ */
329
+ async fetchWithExternalToken(url, token) {
330
+ const response = await this.fetchApi.fetch(url, {
331
+ headers: {
332
+ "Content-Type": "application/json",
333
+ Authorization: `Bearer ${token}`
334
+ },
335
+ method: "GET"
336
+ });
337
+ if (!response.ok) {
338
+ throw new Error(response.statusText);
339
+ }
340
+ return {
341
+ ok: response.ok,
342
+ status: response.status,
343
+ json: async () => response.json()
344
+ };
345
+ }
346
+ /**
347
+ * Fetches a URL via the backend secure proxy. No Authorization header
348
+ * is sent; the backend injects the SSO token and RBAC filters
349
+ * server-side. Authentication is via the Backstage session cookie.
350
+ */
351
+ async fetchViaBackendProxy(url) {
352
+ const response = await this.fetchApi.fetch(url, {
353
+ headers: {
354
+ "Content-Type": "application/json"
355
+ },
356
+ method: "GET"
357
+ });
358
+ if (!response.ok) {
359
+ throw new Error(response.statusText);
360
+ }
361
+ return {
362
+ ...response,
363
+ json: async () => {
364
+ const data = await response.json();
365
+ return data;
366
+ }
367
+ };
368
+ }
369
+ }
370
+
371
+ export { CostManagementSlimClient };
372
+ //# sourceMappingURL=CostManagementSlimClient.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CostManagementSlimClient.esm.js","sources":["../../../src/clients/cost-management/CostManagementSlimClient.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';\nimport crossFetch from 'cross-fetch';\nimport { pluginId } from '../../generated/pluginId';\nimport type { TypedResponse } from '../../generated/apis';\nimport type {\n CostManagementReport,\n GetCostManagementRequest,\n DownloadCostManagementRequest,\n} from './types';\nimport type { CostManagementSlimApi } from './CostManagementSlimApi';\n/**\n * Client for Cost Management data.\n *\n * In the frontend, requests are routed through the backend plugin's secure\n * proxy (`/api/cost-management/proxy/...`), which keeps the SSO token\n * server-side and enforces RBAC before forwarding to the Cost Management API.\n *\n * @public\n */\nexport class CostManagementSlimClient implements CostManagementSlimApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi ?? { fetch: crossFetch };\n }\n\n public async getCostManagementReport(\n request: GetCostManagementRequest,\n ): Promise<TypedResponse<CostManagementReport>> {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n const uri = '/proxy/reports/openshift/costs/';\n\n const queryParams = this.buildCostManagementQueryParams(request);\n\n const queryString = queryParams.toString();\n const queryPart = queryString ? `?${queryString}` : '';\n const url = `${baseUrl}${uri}${queryPart}`;\n\n return await this.fetchViaBackendProxy<CostManagementReport>(url);\n }\n\n public async downloadCostManagementReport(\n request: DownloadCostManagementRequest,\n ): Promise<void> {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n const uri = '/proxy/reports/openshift/costs/';\n\n const downloadRequest: GetCostManagementRequest = {\n query: {\n ...request.query,\n 'filter[limit]': 0,\n },\n };\n delete downloadRequest.query['filter[offset]'];\n\n const queryParams = this.buildCostManagementQueryParams(downloadRequest);\n\n const queryString = queryParams.toString();\n const queryPart = queryString ? `?${queryString}` : '';\n const url = `${baseUrl}${uri}${queryPart}`;\n\n const acceptHeader =\n request.format === 'csv' ? 'text/csv' : 'application/json';\n\n const response = await this.fetchApi.fetch(url, {\n headers: {\n Accept: acceptHeader,\n },\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n // Get the response data\n const responseContentType = response.headers.get('content-type') || '';\n const responseText = await response.text();\n\n let data: string;\n let mimeType: string;\n\n if (request.format === 'csv') {\n // User wants CSV - use response as-is (API returns CSV by default)\n data = responseText;\n mimeType = 'text/csv';\n } else {\n // User wants JSON\n if (responseContentType.includes('application/json')) {\n // Response is already JSON\n const jsonData = JSON.parse(responseText);\n data = JSON.stringify(jsonData, null, 2);\n } else {\n // Response is CSV, need to convert to JSON\n data = this.convertCsvToJson(responseText);\n }\n mimeType = 'application/json';\n }\n\n // Generate filename with timestamp\n const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');\n const extension = request.format;\n const filename = `openshift-costs-${timestamp}.${extension}`;\n\n // Trigger download in browser\n const blob = new Blob([data], { type: mimeType });\n const downloadUrl = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = downloadUrl;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(downloadUrl);\n }\n\n /**\n * Converts CSV string to JSON string\n * @param csv - The CSV data as a string\n * @returns JSON string representation of the CSV data\n */\n private convertCsvToJson(csv: string): string {\n // Normalize line endings (handle both \\r\\n and \\r)\n const normalizedCsv = csv.replaceAll('\\r\\n', '\\n').replaceAll('\\r', '\\n');\n const lines = normalizedCsv.trim().split('\\n');\n if (lines.length === 0) {\n return JSON.stringify({ data: [] }, null, 2);\n }\n\n const headers = this.parseCsvLine(lines[0]);\n const data: Record<string, string | number | null>[] = [];\n\n for (let i = 1; i < lines.length; i++) {\n const values = this.parseCsvLine(lines[i]);\n const row: Record<string, string | number | null> = {};\n\n for (let j = 0; j < headers.length; j++) {\n const header = headers[j];\n const value = values[j] || '';\n\n // Try to parse as number if it looks like one\n if (value === '') {\n row[header] = null;\n } else if (!Number.isNaN(Number(value)) && value !== '') {\n row[header] = Number(value);\n } else {\n row[header] = value;\n }\n }\n\n data.push(row);\n }\n\n return JSON.stringify({ data }, null, 2);\n }\n\n /**\n * Parses a CSV line handling quoted values\n * @param line - A single line from CSV\n * @returns Array of values\n */\n private parseCsvLine(line: string): string[] {\n const result: string[] = [];\n let current = '';\n let inQuotes = false;\n let skipNext = false;\n\n for (let i = 0; i < line.length; i++) {\n if (skipNext) {\n skipNext = false;\n continue;\n }\n\n const char = line[i];\n\n if (char === '\"') {\n if (inQuotes && line[i + 1] === '\"') {\n // Escaped quote\n current += '\"';\n skipNext = true;\n } else {\n // Toggle quote mode\n inQuotes = !inQuotes;\n }\n } else if (char === ',' && !inQuotes) {\n result.push(current);\n current = '';\n } else {\n current += char;\n }\n }\n\n result.push(current);\n return result;\n }\n\n /**\n * Builds query parameters for cost management report request\n * @param request - The cost management request\n * @returns URLSearchParams with all query parameters\n */\n private buildCostManagementQueryParams(\n request: GetCostManagementRequest,\n ): URLSearchParams {\n const queryParams = new URLSearchParams();\n this.appendBasicQueryParams(queryParams, request);\n this.appendDynamicParams(queryParams, request, 'group_by[');\n this.appendDynamicParams(queryParams, request, 'order_by[');\n this.appendDynamicFilterParams(queryParams, request);\n this.appendDynamicExcludeParams(queryParams, request);\n return queryParams;\n }\n\n /**\n * Appends basic query parameters (currency, delta, and standard filters)\n */\n private appendBasicQueryParams(\n queryParams: URLSearchParams,\n request: GetCostManagementRequest,\n ): void {\n const basicParams: Array<{\n key: keyof typeof request.query;\n paramName: string;\n needsStringConversion?: boolean;\n }> = [\n { key: 'currency', paramName: 'currency' },\n { key: 'delta', paramName: 'delta' },\n { key: 'category', paramName: 'category' },\n {\n key: 'filter[limit]',\n paramName: 'filter[limit]',\n needsStringConversion: true,\n },\n {\n key: 'filter[offset]',\n paramName: 'filter[offset]',\n needsStringConversion: true,\n },\n { key: 'filter[resolution]', paramName: 'filter[resolution]' },\n {\n key: 'filter[time_scope_units]',\n paramName: 'filter[time_scope_units]',\n },\n {\n key: 'filter[time_scope_value]',\n paramName: 'filter[time_scope_value]',\n needsStringConversion: true,\n },\n {\n key: 'filter[exact:cluster]',\n paramName: 'filter[exact:cluster]',\n },\n ];\n\n for (const { key, paramName, needsStringConversion } of basicParams) {\n const value = request.query[key];\n if (value !== undefined && value !== null && value !== '') {\n queryParams.append(\n paramName,\n needsStringConversion ? String(value) : (value as string),\n );\n }\n }\n }\n\n /**\n * Appends dynamic parameters that match a specific prefix (e.g., 'group_by[', 'order_by[')\n */\n private appendDynamicParams(\n queryParams: URLSearchParams,\n request: GetCostManagementRequest,\n prefix: string,\n ): void {\n for (const key of Object.keys(request.query)) {\n if (key.startsWith(prefix) && key.endsWith(']')) {\n const value = request.query[key as keyof typeof request.query];\n if (value) {\n queryParams.append(key, String(value));\n }\n }\n }\n }\n\n /**\n * Appends dynamic filter parameters, excluding those already handled explicitly\n */\n private appendDynamicFilterParams(\n queryParams: URLSearchParams,\n request: GetCostManagementRequest,\n ): void {\n const handledFilterKeys = new Set([\n 'filter[limit]',\n 'filter[offset]',\n 'filter[resolution]',\n 'filter[time_scope_units]',\n 'filter[time_scope_value]',\n ]);\n\n for (const key of Object.keys(request.query)) {\n if (\n key.startsWith('filter[') &&\n key.endsWith(']') &&\n !handledFilterKeys.has(key)\n ) {\n const value = request.query[key as keyof typeof request.query];\n if (value) {\n queryParams.append(key, String(value));\n }\n }\n }\n }\n\n /**\n * Appends dynamic exclude parameters (e.g., exclude[project], exclude[cluster], exclude[tag:key])\n */\n private appendDynamicExcludeParams(\n queryParams: URLSearchParams,\n request: GetCostManagementRequest,\n ): void {\n for (const key of Object.keys(request.query)) {\n if (key.startsWith('exclude[') && key.endsWith(']')) {\n const value = request.query[key as keyof typeof request.query];\n if (value) {\n queryParams.append(key, String(value));\n }\n }\n }\n }\n\n /**\n * Search OpenShift projects\n * @param search - Search term to filter projects\n * @param options - Optional request options including token and limit\n * @param options.token - Bearer token for authentication (backend use)\n * @param options.limit - Maximum number of results to return (default: 10, use large number for all)\n */\n public async searchOpenShiftProjects(\n search: string = '',\n options?: { token?: string; limit?: number },\n ): Promise<\n TypedResponse<{ data: Array<{ value: string }>; meta?: any; links?: any }>\n > {\n const url = await this.buildResourceTypeUrl(\n 'openshift-projects',\n search,\n options?.limit,\n );\n\n // If token provided externally (backend use), use it directly\n if (options?.token) {\n return await this.fetchWithExternalToken(url, options.token);\n }\n\n // If no external token, use internal token retrieval\n return await this.fetchResourceType(url);\n }\n\n /**\n * Search OpenShift clusters\n * @param search - Search term to filter clusters\n */\n public async searchOpenShiftClusters(\n search: string = '',\n options?: { token?: string; limit?: number },\n ): Promise<\n TypedResponse<{\n data: Array<{ value: string; cluster_alias: string }>;\n meta?: any;\n links?: any;\n }>\n > {\n const url = await this.buildResourceTypeUrl(\n 'openshift-clusters',\n search,\n options?.limit,\n );\n\n // If token provided externally (backend use), use it directly\n if (options?.token) {\n return await this.fetchWithExternalToken(url, options.token);\n }\n\n // If no external token, use internal token retrieval\n return await this.fetchResourceType(url);\n }\n\n /**\n * Search OpenShift nodes\n * @param search - Search term to filter nodes\n */\n public async searchOpenShiftNodes(\n search: string = '',\n ): Promise<\n TypedResponse<{ data: Array<{ value: string }>; meta?: any; links?: any }>\n > {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n const searchParam = search ? `?search=${encodeURIComponent(search)}` : '';\n const url = `${baseUrl}/proxy/resource-types/openshift-nodes/${searchParam}`;\n\n return await this.fetchResourceType(url);\n }\n\n /**\n * Get OpenShift tags\n * @param timeScopeValue - Time scope value (-1 for month-to-date, -2 for previous-month). Defaults to -1.\n * @returns TypedResponse with array of tag strings\n */\n public async getOpenShiftTags(\n timeScopeValue: number = -1,\n ): Promise<TypedResponse<{ data: string[]; meta?: any; links?: any }>> {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n const url = `${baseUrl}/proxy/tags/openshift/?filter[time_scope_value]=${timeScopeValue}&key_only=true&limit=1000`;\n\n return await this.fetchViaBackendProxy<{\n data: string[];\n meta?: any;\n links?: any;\n }>(url);\n }\n\n /**\n * Get OpenShift tag values for a specific tag key\n * @param tagKey - The tag key to get values for\n * @param timeScopeValue - Time scope value (-1 for month-to-date, -2 for previous-month). Defaults to -1.\n * @returns TypedResponse with array of tag value objects\n */\n public async getOpenShiftTagValues(\n tagKey: string,\n timeScopeValue: number = -1,\n ): Promise<\n TypedResponse<{\n data: Array<{ key: string; values: string[]; enabled: boolean }>;\n meta?: any;\n links?: any;\n }>\n > {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n const url = `${baseUrl}/proxy/tags/openshift/?filter[key]=${encodeURIComponent(\n tagKey,\n )}&filter[time_scope_value]=${timeScopeValue}`;\n\n return await this.fetchViaBackendProxy<{\n data: Array<{ key: string; values: string[]; enabled: boolean }>;\n meta?: any;\n links?: any;\n }>(url);\n }\n\n private async fetchResourceType(url: string): Promise<\n TypedResponse<{\n data: Array<{ value: string; cluster_alias: string }>;\n meta?: any;\n links?: any;\n }>\n > {\n return await this.fetchViaBackendProxy<{\n data: Array<{ value: string; cluster_alias: string }>;\n meta?: any;\n links?: any;\n }>(url);\n }\n\n /**\n * Builds resource type URL routed through the backend secure proxy\n */\n private async buildResourceTypeUrl(\n resourceType: string,\n search?: string,\n limit?: number,\n ): Promise<string> {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n\n const params = new URLSearchParams();\n if (search) {\n params.append('search', search);\n }\n if (limit !== undefined) {\n params.append('limit', String(limit));\n }\n const queryString = params.toString();\n const queryPart = queryString ? `?${queryString}` : '';\n\n return `${baseUrl}/proxy/resource-types/${resourceType}/${queryPart}`;\n }\n\n /**\n * Fetches with externally provided token (for backend use).\n * This path bypasses the secure proxy since it's already server-side.\n */\n private async fetchWithExternalToken<T>(\n url: string,\n token: string,\n ): Promise<TypedResponse<T>> {\n const response = await this.fetchApi.fetch(url, {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n return {\n ok: response.ok,\n status: response.status,\n json: async () => response.json(),\n } as TypedResponse<T>;\n }\n\n /**\n * Fetches a URL via the backend secure proxy. No Authorization header\n * is sent; the backend injects the SSO token and RBAC filters\n * server-side. Authentication is via the Backstage session cookie.\n */\n private async fetchViaBackendProxy<T>(\n url: string,\n ): Promise<TypedResponse<T>> {\n const response = await this.fetchApi.fetch(url, {\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n return {\n ...response,\n json: async () => {\n const data = (await response.json()) as T;\n return data;\n },\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ var jsonUtils = require('../../json-utils.cjs.js');
4
+ var camelCase = require('lodash/camelCase');
5
+ var snakeCase = require('lodash/snakeCase');
6
+ var pluginId = require('../../generated/pluginId.cjs.js');
7
+ var DefaultApi_client = require('../../generated/apis/DefaultApi.client.cjs.js');
8
+
9
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
+
11
+ var camelCase__default = /*#__PURE__*/_interopDefaultCompat(camelCase);
12
+ var snakeCase__default = /*#__PURE__*/_interopDefaultCompat(snakeCase);
13
+
14
+ class OptimizationsClient {
15
+ static requestKeysToSkip = {
16
+ getRecommendationById: [/path\.recommendationId$/]
17
+ };
18
+ static responseKeysToSkip = {
19
+ getRecommendationById: [
20
+ /recommendations\.recommendation_terms\.(long|medium|short)_term\.plots\.plots_data\['\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\]$/
21
+ ]
22
+ };
23
+ /** Used for backend-to-RHCC calls (token provided by caller). */
24
+ directClient;
25
+ /** Used for frontend calls routed through the backend secure proxy. */
26
+ proxyClient;
27
+ constructor(options) {
28
+ const outerDiscovery = options.discoveryApi;
29
+ this.directClient = new DefaultApi_client.DefaultApiClient({
30
+ fetchApi: options.fetchApi,
31
+ discoveryApi: {
32
+ async getBaseUrl() {
33
+ const baseUrl = await outerDiscovery.getBaseUrl("proxy");
34
+ return `${baseUrl}/cost-management/v1`;
35
+ }
36
+ }
37
+ });
38
+ this.proxyClient = new DefaultApi_client.DefaultApiClient({
39
+ fetchApi: options.fetchApi,
40
+ discoveryApi: {
41
+ async getBaseUrl() {
42
+ const baseUrl = await outerDiscovery.getBaseUrl(pluginId.pluginId);
43
+ return `${baseUrl}/proxy`;
44
+ }
45
+ }
46
+ });
47
+ }
48
+ async getRecommendationById(request) {
49
+ const snakeCaseTransformedRequest = jsonUtils.deepMapKeys(request, {
50
+ onVisitJsonObjectKey: snakeCase__default.default,
51
+ skipList: OptimizationsClient.requestKeysToSkip.getRecommendationById
52
+ });
53
+ const response = await this.proxyClient.getRecommendationById(
54
+ snakeCaseTransformedRequest
55
+ );
56
+ if (!response.ok) {
57
+ throw new Error(response.statusText);
58
+ }
59
+ return {
60
+ ...response,
61
+ json: async () => {
62
+ const data = await response.json();
63
+ const camelCaseTransformedResponse = jsonUtils.deepMapKeys(data, {
64
+ onVisitJsonObjectKey: camelCase__default.default,
65
+ skipList: OptimizationsClient.responseKeysToSkip.getRecommendationById
66
+ });
67
+ return camelCaseTransformedResponse;
68
+ }
69
+ };
70
+ }
71
+ async getRecommendationList(request, options) {
72
+ const snakeCaseTransformedRequest = jsonUtils.deepMapKeys(
73
+ request,
74
+ snakeCase__default.default
75
+ );
76
+ const client = options?.token ? this.directClient : this.proxyClient;
77
+ const response = await client.getRecommendationList(
78
+ snakeCaseTransformedRequest,
79
+ options
80
+ );
81
+ if (!response.ok) {
82
+ throw new Error(response.statusText);
83
+ }
84
+ return {
85
+ ...response,
86
+ json: async () => {
87
+ const data = await response.json();
88
+ const camelCaseTransformedResponse = jsonUtils.deepMapKeys(
89
+ data,
90
+ camelCase__default.default
91
+ );
92
+ return camelCaseTransformedResponse;
93
+ }
94
+ };
95
+ }
96
+ }
97
+
98
+ exports.OptimizationsClient = OptimizationsClient;
99
+ //# sourceMappingURL=OptimizationsClient.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OptimizationsClient.cjs.js","sources":["../../../src/clients/optimizations/OptimizationsClient.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';\nimport { deepMapKeys } from '../../util/mod';\nimport camelCase from 'lodash/camelCase';\nimport snakeCase from 'lodash/snakeCase';\nimport { pluginId } from '../../generated/pluginId';\nimport {\n DefaultApiClient,\n type RequestOptions,\n type TypedResponse,\n} from '../../generated/apis';\nimport type {\n RecommendationBoxPlots,\n RecommendationList,\n} from '../../generated/models';\nimport type {\n GetRecommendationByIdRequest,\n GetRecommendationListRequest,\n OptimizationsApi,\n} from './types';\n\n/**\n * Client for the Optimizations (ROS) API.\n *\n * In the frontend, requests are routed through the backend plugin's secure\n * proxy (`/api/cost-management/proxy/...`), which keeps the SSO token\n * server-side and enforces RBAC before forwarding to the Cost Management API.\n *\n * When a `token` is provided via `RequestOptions` (backend use-case), the\n * request goes directly to the upstream API.\n *\n * @public\n */\nexport class OptimizationsClient implements OptimizationsApi {\n private static requestKeysToSkip = {\n getRecommendationById: [/path\\.recommendationId$/],\n };\n private static responseKeysToSkip = {\n getRecommendationById: [\n /recommendations\\.recommendation_terms\\.(long|medium|short)_term\\.plots\\.plots_data\\['\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z'\\]$/,\n ],\n };\n\n /** Used for backend-to-RHCC calls (token provided by caller). */\n private readonly directClient: DefaultApiClient;\n /** Used for frontend calls routed through the backend secure proxy. */\n private readonly proxyClient: DefaultApiClient;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) {\n const outerDiscovery = options.discoveryApi;\n\n this.directClient = new DefaultApiClient({\n fetchApi: options.fetchApi,\n discoveryApi: {\n async getBaseUrl() {\n const baseUrl = await outerDiscovery.getBaseUrl('proxy');\n return `${baseUrl}/cost-management/v1`;\n },\n },\n });\n\n this.proxyClient = new DefaultApiClient({\n fetchApi: options.fetchApi,\n discoveryApi: {\n async getBaseUrl() {\n const baseUrl = await outerDiscovery.getBaseUrl(pluginId);\n return `${baseUrl}/proxy`;\n },\n },\n });\n }\n\n public async getRecommendationById(\n request: GetRecommendationByIdRequest,\n ): Promise<TypedResponse<RecommendationBoxPlots>> {\n const snakeCaseTransformedRequest = deepMapKeys(request, {\n onVisitJsonObjectKey: snakeCase as (value: string | number) => string,\n skipList: OptimizationsClient.requestKeysToSkip.getRecommendationById,\n }) as GetRecommendationByIdRequest;\n\n const response = await this.proxyClient.getRecommendationById(\n snakeCaseTransformedRequest,\n );\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n return {\n ...response,\n json: async () => {\n const data = await response.json();\n const camelCaseTransformedResponse = deepMapKeys(data, {\n onVisitJsonObjectKey: camelCase as (value: string | number) => string,\n skipList:\n OptimizationsClient.responseKeysToSkip.getRecommendationById,\n }) as RecommendationBoxPlots;\n return camelCaseTransformedResponse;\n },\n };\n }\n\n public async getRecommendationList(\n request: GetRecommendationListRequest,\n options?: RequestOptions,\n ): Promise<TypedResponse<RecommendationList>> {\n const snakeCaseTransformedRequest = deepMapKeys(\n request,\n snakeCase as (value: string | number) => string,\n ) as GetRecommendationListRequest;\n\n // Backend use-case: token provided, call RHCC directly\n const client = options?.token ? this.directClient : this.proxyClient;\n\n const response = await client.getRecommendationList(\n snakeCaseTransformedRequest,\n options,\n );\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n return {\n ...response,\n json: async () => {\n const data = await response.json();\n const camelCaseTransformedResponse = deepMapKeys(\n data,\n camelCase as (value: string | number) => string,\n ) as RecommendationList;\n return camelCaseTransformedResponse;\n },\n };\n }\n}\n"],"names":["DefaultApiClient","pluginId","deepMapKeys","snakeCase","camelCase"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import { deepMapKeys } from '../../json-utils.esm.js';
2
+ import camelCase from 'lodash/camelCase';
3
+ import snakeCase from 'lodash/snakeCase';
4
+ import { pluginId } from '../../generated/pluginId.esm.js';
5
+ import { DefaultApiClient } from '../../generated/apis/DefaultApi.client.esm.js';
6
+
7
+ class OptimizationsClient {
8
+ static requestKeysToSkip = {
9
+ getRecommendationById: [/path\.recommendationId$/]
10
+ };
11
+ static responseKeysToSkip = {
12
+ getRecommendationById: [
13
+ /recommendations\.recommendation_terms\.(long|medium|short)_term\.plots\.plots_data\['\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\]$/
14
+ ]
15
+ };
16
+ /** Used for backend-to-RHCC calls (token provided by caller). */
17
+ directClient;
18
+ /** Used for frontend calls routed through the backend secure proxy. */
19
+ proxyClient;
20
+ constructor(options) {
21
+ const outerDiscovery = options.discoveryApi;
22
+ this.directClient = new DefaultApiClient({
23
+ fetchApi: options.fetchApi,
24
+ discoveryApi: {
25
+ async getBaseUrl() {
26
+ const baseUrl = await outerDiscovery.getBaseUrl("proxy");
27
+ return `${baseUrl}/cost-management/v1`;
28
+ }
29
+ }
30
+ });
31
+ this.proxyClient = new DefaultApiClient({
32
+ fetchApi: options.fetchApi,
33
+ discoveryApi: {
34
+ async getBaseUrl() {
35
+ const baseUrl = await outerDiscovery.getBaseUrl(pluginId);
36
+ return `${baseUrl}/proxy`;
37
+ }
38
+ }
39
+ });
40
+ }
41
+ async getRecommendationById(request) {
42
+ const snakeCaseTransformedRequest = deepMapKeys(request, {
43
+ onVisitJsonObjectKey: snakeCase,
44
+ skipList: OptimizationsClient.requestKeysToSkip.getRecommendationById
45
+ });
46
+ const response = await this.proxyClient.getRecommendationById(
47
+ snakeCaseTransformedRequest
48
+ );
49
+ if (!response.ok) {
50
+ throw new Error(response.statusText);
51
+ }
52
+ return {
53
+ ...response,
54
+ json: async () => {
55
+ const data = await response.json();
56
+ const camelCaseTransformedResponse = deepMapKeys(data, {
57
+ onVisitJsonObjectKey: camelCase,
58
+ skipList: OptimizationsClient.responseKeysToSkip.getRecommendationById
59
+ });
60
+ return camelCaseTransformedResponse;
61
+ }
62
+ };
63
+ }
64
+ async getRecommendationList(request, options) {
65
+ const snakeCaseTransformedRequest = deepMapKeys(
66
+ request,
67
+ snakeCase
68
+ );
69
+ const client = options?.token ? this.directClient : this.proxyClient;
70
+ const response = await client.getRecommendationList(
71
+ snakeCaseTransformedRequest,
72
+ options
73
+ );
74
+ if (!response.ok) {
75
+ throw new Error(response.statusText);
76
+ }
77
+ return {
78
+ ...response,
79
+ json: async () => {
80
+ const data = await response.json();
81
+ const camelCaseTransformedResponse = deepMapKeys(
82
+ data,
83
+ camelCase
84
+ );
85
+ return camelCaseTransformedResponse;
86
+ }
87
+ };
88
+ }
89
+ }
90
+
91
+ export { OptimizationsClient };
92
+ //# sourceMappingURL=OptimizationsClient.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OptimizationsClient.esm.js","sources":["../../../src/clients/optimizations/OptimizationsClient.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';\nimport { deepMapKeys } from '../../util/mod';\nimport camelCase from 'lodash/camelCase';\nimport snakeCase from 'lodash/snakeCase';\nimport { pluginId } from '../../generated/pluginId';\nimport {\n DefaultApiClient,\n type RequestOptions,\n type TypedResponse,\n} from '../../generated/apis';\nimport type {\n RecommendationBoxPlots,\n RecommendationList,\n} from '../../generated/models';\nimport type {\n GetRecommendationByIdRequest,\n GetRecommendationListRequest,\n OptimizationsApi,\n} from './types';\n\n/**\n * Client for the Optimizations (ROS) API.\n *\n * In the frontend, requests are routed through the backend plugin's secure\n * proxy (`/api/cost-management/proxy/...`), which keeps the SSO token\n * server-side and enforces RBAC before forwarding to the Cost Management API.\n *\n * When a `token` is provided via `RequestOptions` (backend use-case), the\n * request goes directly to the upstream API.\n *\n * @public\n */\nexport class OptimizationsClient implements OptimizationsApi {\n private static requestKeysToSkip = {\n getRecommendationById: [/path\\.recommendationId$/],\n };\n private static responseKeysToSkip = {\n getRecommendationById: [\n /recommendations\\.recommendation_terms\\.(long|medium|short)_term\\.plots\\.plots_data\\['\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z'\\]$/,\n ],\n };\n\n /** Used for backend-to-RHCC calls (token provided by caller). */\n private readonly directClient: DefaultApiClient;\n /** Used for frontend calls routed through the backend secure proxy. */\n private readonly proxyClient: DefaultApiClient;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) {\n const outerDiscovery = options.discoveryApi;\n\n this.directClient = new DefaultApiClient({\n fetchApi: options.fetchApi,\n discoveryApi: {\n async getBaseUrl() {\n const baseUrl = await outerDiscovery.getBaseUrl('proxy');\n return `${baseUrl}/cost-management/v1`;\n },\n },\n });\n\n this.proxyClient = new DefaultApiClient({\n fetchApi: options.fetchApi,\n discoveryApi: {\n async getBaseUrl() {\n const baseUrl = await outerDiscovery.getBaseUrl(pluginId);\n return `${baseUrl}/proxy`;\n },\n },\n });\n }\n\n public async getRecommendationById(\n request: GetRecommendationByIdRequest,\n ): Promise<TypedResponse<RecommendationBoxPlots>> {\n const snakeCaseTransformedRequest = deepMapKeys(request, {\n onVisitJsonObjectKey: snakeCase as (value: string | number) => string,\n skipList: OptimizationsClient.requestKeysToSkip.getRecommendationById,\n }) as GetRecommendationByIdRequest;\n\n const response = await this.proxyClient.getRecommendationById(\n snakeCaseTransformedRequest,\n );\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n return {\n ...response,\n json: async () => {\n const data = await response.json();\n const camelCaseTransformedResponse = deepMapKeys(data, {\n onVisitJsonObjectKey: camelCase as (value: string | number) => string,\n skipList:\n OptimizationsClient.responseKeysToSkip.getRecommendationById,\n }) as RecommendationBoxPlots;\n return camelCaseTransformedResponse;\n },\n };\n }\n\n public async getRecommendationList(\n request: GetRecommendationListRequest,\n options?: RequestOptions,\n ): Promise<TypedResponse<RecommendationList>> {\n const snakeCaseTransformedRequest = deepMapKeys(\n request,\n snakeCase as (value: string | number) => string,\n ) as GetRecommendationListRequest;\n\n // Backend use-case: token provided, call RHCC directly\n const client = options?.token ? this.directClient : this.proxyClient;\n\n const response = await client.getRecommendationList(\n snakeCaseTransformedRequest,\n options,\n );\n\n if (!response.ok) {\n throw new Error(response.statusText);\n }\n\n return {\n ...response,\n json: async () => {\n const data = await response.json();\n const camelCaseTransformedResponse = deepMapKeys(\n data,\n camelCase as (value: string | number) => string,\n ) as RecommendationList;\n return camelCaseTransformedResponse;\n },\n };\n }\n}\n"],"names":[],"mappings":";;;;;;AAgDO,MAAM,mBAAgD,CAAA;AAAA,EAC3D,OAAe,iBAAoB,GAAA;AAAA,IACjC,qBAAA,EAAuB,CAAC,yBAAyB;AAAA,GACnD;AAAA,EACA,OAAe,kBAAqB,GAAA;AAAA,IAClC,qBAAuB,EAAA;AAAA,MACrB;AAAA;AACF,GACF;AAAA;AAAA,EAGiB,YAAA;AAAA;AAAA,EAEA,WAAA;AAAA,EAEjB,YAAY,OAA8D,EAAA;AACxE,IAAA,MAAM,iBAAiB,OAAQ,CAAA,YAAA;AAE/B,IAAK,IAAA,CAAA,YAAA,GAAe,IAAI,gBAAiB,CAAA;AAAA,MACvC,UAAU,OAAQ,CAAA,QAAA;AAAA,MAClB,YAAc,EAAA;AAAA,QACZ,MAAM,UAAa,GAAA;AACjB,UAAA,MAAM,OAAU,GAAA,MAAM,cAAe,CAAA,UAAA,CAAW,OAAO,CAAA;AACvD,UAAA,OAAO,GAAG,OAAO,CAAA,mBAAA,CAAA;AAAA;AACnB;AACF,KACD,CAAA;AAED,IAAK,IAAA,CAAA,WAAA,GAAc,IAAI,gBAAiB,CAAA;AAAA,MACtC,UAAU,OAAQ,CAAA,QAAA;AAAA,MAClB,YAAc,EAAA;AAAA,QACZ,MAAM,UAAa,GAAA;AACjB,UAAA,MAAM,OAAU,GAAA,MAAM,cAAe,CAAA,UAAA,CAAW,QAAQ,CAAA;AACxD,UAAA,OAAO,GAAG,OAAO,CAAA,MAAA,CAAA;AAAA;AACnB;AACF,KACD,CAAA;AAAA;AACH,EAEA,MAAa,sBACX,OACgD,EAAA;AAChD,IAAM,MAAA,2BAAA,GAA8B,YAAY,OAAS,EAAA;AAAA,MACvD,oBAAsB,EAAA,SAAA;AAAA,MACtB,QAAA,EAAU,oBAAoB,iBAAkB,CAAA;AAAA,KACjD,CAAA;AAED,IAAM,MAAA,QAAA,GAAW,MAAM,IAAA,CAAK,WAAY,CAAA,qBAAA;AAAA,MACtC;AAAA,KACF;AAEA,IAAI,IAAA,CAAC,SAAS,EAAI,EAAA;AAChB,MAAM,MAAA,IAAI,KAAM,CAAA,QAAA,CAAS,UAAU,CAAA;AAAA;AAGrC,IAAO,OAAA;AAAA,MACL,GAAG,QAAA;AAAA,MACH,MAAM,YAAY;AAChB,QAAM,MAAA,IAAA,GAAO,MAAM,QAAA,CAAS,IAAK,EAAA;AACjC,QAAM,MAAA,4BAAA,GAA+B,YAAY,IAAM,EAAA;AAAA,UACrD,oBAAsB,EAAA,SAAA;AAAA,UACtB,QAAA,EACE,oBAAoB,kBAAmB,CAAA;AAAA,SAC1C,CAAA;AACD,QAAO,OAAA,4BAAA;AAAA;AACT,KACF;AAAA;AACF,EAEA,MAAa,qBACX,CAAA,OAAA,EACA,OAC4C,EAAA;AAC5C,IAAA,MAAM,2BAA8B,GAAA,WAAA;AAAA,MAClC,OAAA;AAAA,MACA;AAAA,KACF;AAGA,IAAA,MAAM,MAAS,GAAA,OAAA,EAAS,KAAQ,GAAA,IAAA,CAAK,eAAe,IAAK,CAAA,WAAA;AAEzD,IAAM,MAAA,QAAA,GAAW,MAAM,MAAO,CAAA,qBAAA;AAAA,MAC5B,2BAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAI,IAAA,CAAC,SAAS,EAAI,EAAA;AAChB,MAAM,MAAA,IAAI,KAAM,CAAA,QAAA,CAAS,UAAU,CAAA;AAAA;AAGrC,IAAO,OAAA;AAAA,MACL,GAAG,QAAA;AAAA,MACH,MAAM,YAAY;AAChB,QAAM,MAAA,IAAA,GAAO,MAAM,QAAA,CAAS,IAAK,EAAA;AACjC,QAAA,MAAM,4BAA+B,GAAA,WAAA;AAAA,UACnC,IAAA;AAAA,UACA;AAAA,SACF;AACA,QAAO,OAAA,4BAAA;AAAA;AACT,KACF;AAAA;AAEJ;;;;"}