@oneuptime/common 8.0.5438 → 8.0.5462

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 (56) hide show
  1. package/Models/DatabaseModels/StatusPage.ts +80 -0
  2. package/Server/API/StatusPageAPI.ts +138 -52
  3. package/Server/EnvironmentConfig.ts +34 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1761232578396-MigrationName.ts +29 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  6. package/Server/Services/OpenTelemetryIngestService.ts +1 -39
  7. package/Server/Services/StatusPageService.ts +117 -0
  8. package/Server/Services/TelemetryUsageBillingService.ts +208 -15
  9. package/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.ts +5 -0
  10. package/Server/Utils/Telemetry/Telemetry.ts +129 -81
  11. package/Server/Utils/VM/VMRunner.ts +3 -4
  12. package/UI/Components/Dictionary/Dictionary.tsx +3 -0
  13. package/UI/Components/Forms/Fields/FieldLabel.tsx +7 -3
  14. package/UI/Components/LogsViewer/LogItem.tsx +12 -4
  15. package/UI/Components/LogsViewer/LogsViewer.tsx +131 -29
  16. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -2
  17. package/UI/Components/ModelFilter/Filter.ts +1 -0
  18. package/UI/Components/ModelTable/BaseModelTable.tsx +2 -1
  19. package/UI/Components/Table/TableRow.tsx +89 -77
  20. package/UI/esbuild-config.js +32 -1
  21. package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
  22. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  23. package/build/dist/Server/API/StatusPageAPI.js +157 -74
  24. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  25. package/build/dist/Server/EnvironmentConfig.js +14 -0
  26. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  27. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1761232578396-MigrationName.js +16 -0
  28. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1761232578396-MigrationName.js.map +1 -0
  29. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  30. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  31. package/build/dist/Server/Services/OpenTelemetryIngestService.js +0 -30
  32. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  33. package/build/dist/Server/Services/StatusPageService.js +95 -0
  34. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  35. package/build/dist/Server/Services/TelemetryUsageBillingService.js +168 -8
  36. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  37. package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js +4 -0
  38. package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js.map +1 -1
  39. package/build/dist/Server/Utils/Telemetry/Telemetry.js +84 -60
  40. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  41. package/build/dist/Server/Utils/VM/VMRunner.js +2 -2
  42. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  43. package/build/dist/UI/Components/Dictionary/Dictionary.js +3 -3
  44. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  45. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js +2 -1
  46. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js.map +1 -1
  47. package/build/dist/UI/Components/LogsViewer/LogItem.js +5 -3
  48. package/build/dist/UI/Components/LogsViewer/LogItem.js.map +1 -1
  49. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +73 -22
  50. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  51. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -2
  52. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -1
  53. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  54. package/build/dist/UI/Components/Table/TableRow.js +18 -6
  55. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  56. package/package.json +4 -4
@@ -1,16 +1,31 @@
1
1
  import { MeteredPlanUtil } from "../Types/Billing/MeteredPlan/AllMeteredPlans";
2
2
  import TelemetryMeteredPlan from "../Types/Billing/MeteredPlan/TelemetryMeteredPlan";
3
- import QueryHelper from "../Types/Database/QueryHelper";
4
3
  import DatabaseService from "./DatabaseService";
5
4
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
6
- import LIMIT_MAX from "../../Types/Database/LimitMax";
5
+ import LIMIT_MAX, { LIMIT_INFINITY } from "../../Types/Database/LimitMax";
7
6
  import OneUptimeDate from "../../Types/Date";
8
7
  import Decimal from "../../Types/Decimal";
9
8
  import BadDataException from "../../Types/Exception/BadDataException";
10
9
  import ProductType from "../../Types/MeteredPlan/ProductType";
11
10
  import ObjectID from "../../Types/ObjectID";
12
- import Model from "../../Models/DatabaseModels/TelemetryUsageBilling";
13
- import { IsBillingEnabled } from "../EnvironmentConfig";
11
+ import Model, {
12
+ DEFAULT_RETENTION_IN_DAYS,
13
+ } from "../../Models/DatabaseModels/TelemetryUsageBilling";
14
+ import TelemetryServiceService from "./TelemetryServiceService";
15
+ import SpanService from "./SpanService";
16
+ import LogService from "./LogService";
17
+ import MetricService from "./MetricService";
18
+ import AnalyticsQueryHelper from "../Types/AnalyticsDatabase/QueryHelper";
19
+ import DiskSize from "../../Types/DiskSize";
20
+ import logger from "../Utils/Logger";
21
+ import PositiveNumber from "../../Types/PositiveNumber";
22
+ import TelemetryServiceModel from "../../Models/DatabaseModels/TelemetryService";
23
+ import {
24
+ AverageSpanRowSizeInBytes,
25
+ AverageLogRowSizeInBytes,
26
+ AverageMetricRowSizeInBytes,
27
+ IsBillingEnabled,
28
+ } from "../EnvironmentConfig";
14
29
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
15
30
 
16
31
  export class Service extends DatabaseService<Model> {
@@ -31,9 +46,6 @@ export class Service extends DatabaseService<Model> {
31
46
  projectId: data.projectId,
32
47
  productType: data.productType,
33
48
  isReportedToBillingProvider: false,
34
- createdAt: QueryHelper.lessThan(
35
- OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -1),
36
- ), // we need to get everything that's not today.
37
49
  },
38
50
  skip: 0,
39
51
  limit: LIMIT_MAX, /// because a project can have MANY telemetry services.
@@ -47,6 +59,159 @@ export class Service extends DatabaseService<Model> {
47
59
  });
48
60
  }
49
61
 
62
+ @CaptureSpan()
63
+ public async stageTelemetryUsageForProject(data: {
64
+ projectId: ObjectID;
65
+ productType: ProductType;
66
+ usageDate?: Date;
67
+ }): Promise<void> {
68
+ if (!IsBillingEnabled) {
69
+ return;
70
+ }
71
+
72
+ const usageDate: Date = data.usageDate
73
+ ? OneUptimeDate.fromString(data.usageDate)
74
+ : OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -1);
75
+
76
+ const averageRowSizeInBytes: number = this.getAverageRowSizeForProduct(
77
+ data.productType,
78
+ );
79
+
80
+ if (averageRowSizeInBytes <= 0) {
81
+ return;
82
+ }
83
+
84
+ const usageDayString: string = OneUptimeDate.getDateString(usageDate);
85
+ const startOfDay: Date = OneUptimeDate.getStartOfDay(usageDate);
86
+ const endOfDay: Date = OneUptimeDate.getEndOfDay(usageDate);
87
+
88
+ const telemetryServices: Array<TelemetryServiceModel> =
89
+ await TelemetryServiceService.findBy({
90
+ query: {
91
+ projectId: data.projectId,
92
+ },
93
+ select: {
94
+ _id: true,
95
+ retainTelemetryDataForDays: true,
96
+ },
97
+ skip: 0,
98
+ limit: LIMIT_MAX,
99
+ props: {
100
+ isRoot: true,
101
+ },
102
+ });
103
+
104
+ if (!telemetryServices || telemetryServices.length === 0) {
105
+ return;
106
+ }
107
+
108
+ for (const telemetryService of telemetryServices) {
109
+ if (!telemetryService?.id) {
110
+ continue;
111
+ }
112
+
113
+ const existingEntry: Model | null = await this.findOneBy({
114
+ query: {
115
+ projectId: data.projectId,
116
+ productType: data.productType,
117
+ telemetryServiceId: telemetryService.id,
118
+ day: usageDayString,
119
+ },
120
+ select: {
121
+ _id: true,
122
+ },
123
+ props: {
124
+ isRoot: true,
125
+ },
126
+ });
127
+
128
+ if (existingEntry) {
129
+ continue;
130
+ }
131
+
132
+ let rowCount: number = 0;
133
+
134
+ try {
135
+ if (data.productType === ProductType.Traces) {
136
+ const count: PositiveNumber = await SpanService.countBy({
137
+ query: {
138
+ projectId: data.projectId,
139
+ serviceId: telemetryService.id,
140
+ startTime: AnalyticsQueryHelper.inBetween(startOfDay, endOfDay),
141
+ },
142
+ skip: 0,
143
+ limit: LIMIT_INFINITY,
144
+ props: {
145
+ isRoot: true,
146
+ },
147
+ });
148
+
149
+ rowCount = count.toNumber();
150
+ } else if (data.productType === ProductType.Logs) {
151
+ const count: PositiveNumber = await LogService.countBy({
152
+ query: {
153
+ projectId: data.projectId,
154
+ serviceId: telemetryService.id,
155
+ time: AnalyticsQueryHelper.inBetween(startOfDay, endOfDay),
156
+ },
157
+ skip: 0,
158
+ limit: LIMIT_INFINITY,
159
+ props: {
160
+ isRoot: true,
161
+ },
162
+ });
163
+
164
+ rowCount = count.toNumber();
165
+ } else if (data.productType === ProductType.Metrics) {
166
+ const count: PositiveNumber = await MetricService.countBy({
167
+ query: {
168
+ projectId: data.projectId,
169
+ serviceId: telemetryService.id,
170
+ time: AnalyticsQueryHelper.inBetween(startOfDay, endOfDay),
171
+ },
172
+ skip: 0,
173
+ limit: LIMIT_INFINITY,
174
+ props: {
175
+ isRoot: true,
176
+ },
177
+ });
178
+
179
+ rowCount = count.toNumber();
180
+ }
181
+ } catch (error) {
182
+ logger.error(
183
+ `Failed to compute telemetry usage for service ${telemetryService.id?.toString()}:`,
184
+ );
185
+ logger.error(error as Error);
186
+ continue;
187
+ }
188
+
189
+ if (rowCount <= 0) {
190
+ continue;
191
+ }
192
+
193
+ const estimatedBytes: number = rowCount * averageRowSizeInBytes;
194
+ const estimatedGigabytes: number = DiskSize.byteSizeToGB(estimatedBytes);
195
+
196
+ if (!Number.isFinite(estimatedGigabytes) || estimatedGigabytes <= 0) {
197
+ continue;
198
+ }
199
+
200
+ const dataRetentionInDays: number =
201
+ telemetryService.retainTelemetryDataForDays ||
202
+ DEFAULT_RETENTION_IN_DAYS;
203
+
204
+ await this.updateUsageBilling({
205
+ projectId: data.projectId,
206
+ productType: data.productType,
207
+ telemetryServiceId: telemetryService.id,
208
+ dataIngestedInGB: estimatedGigabytes,
209
+ retentionInDays: dataRetentionInDays,
210
+ usageDate: usageDate,
211
+ });
212
+ }
213
+ }
214
+
50
215
  @CaptureSpan()
51
216
  public async updateUsageBilling(data: {
52
217
  projectId: ObjectID;
@@ -54,6 +219,7 @@ export class Service extends DatabaseService<Model> {
54
219
  telemetryServiceId: ObjectID;
55
220
  dataIngestedInGB: number;
56
221
  retentionInDays: number;
222
+ usageDate?: Date;
57
223
  }): Promise<void> {
58
224
  if (
59
225
  data.productType !== ProductType.Traces &&
@@ -70,6 +236,12 @@ export class Service extends DatabaseService<Model> {
70
236
  data.productType,
71
237
  ) as TelemetryMeteredPlan;
72
238
 
239
+ const usageDate: Date = data.usageDate
240
+ ? OneUptimeDate.fromString(data.usageDate)
241
+ : OneUptimeDate.getCurrentDate();
242
+
243
+ const usageDayString: string = OneUptimeDate.getDateString(usageDate);
244
+
73
245
  const totalCostOfThisOperationInUSD: number =
74
246
  serverMeteredPlan.getTotalCostInUSD({
75
247
  dataIngestedInGB: data.dataIngestedInGB,
@@ -82,10 +254,7 @@ export class Service extends DatabaseService<Model> {
82
254
  productType: data.productType,
83
255
  telemetryServiceId: data.telemetryServiceId,
84
256
  isReportedToBillingProvider: false,
85
- createdAt: QueryHelper.inBetween(
86
- OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -1),
87
- OneUptimeDate.getCurrentDate(),
88
- ),
257
+ day: usageDayString,
89
258
  },
90
259
  select: {
91
260
  _id: true,
@@ -135,11 +304,9 @@ export class Service extends DatabaseService<Model> {
135
304
  usageBilling.telemetryServiceId = data.telemetryServiceId;
136
305
  usageBilling.retainTelemetryDataForDays = data.retentionInDays;
137
306
  usageBilling.isReportedToBillingProvider = false;
138
- usageBilling.createdAt = OneUptimeDate.getCurrentDate();
307
+ usageBilling.createdAt = usageDate;
139
308
 
140
- usageBilling.day = OneUptimeDate.getDateString(
141
- OneUptimeDate.getCurrentDate(),
142
- );
309
+ usageBilling.day = usageDayString;
143
310
 
144
311
  usageBilling.totalCostInUSD = new Decimal(totalCostOfThisOperationInUSD);
145
312
 
@@ -151,6 +318,32 @@ export class Service extends DatabaseService<Model> {
151
318
  });
152
319
  }
153
320
  }
321
+
322
+ private getAverageRowSizeForProduct(productType: ProductType): number {
323
+ const fallbackSize: number = 1024;
324
+
325
+ // Narrow to telemetry product types before indexing to satisfy TypeScript
326
+ if (
327
+ productType !== ProductType.Traces &&
328
+ productType !== ProductType.Logs &&
329
+ productType !== ProductType.Metrics
330
+ ) {
331
+ return fallbackSize;
332
+ }
333
+
334
+ const value: number =
335
+ {
336
+ [ProductType.Traces]: AverageSpanRowSizeInBytes,
337
+ [ProductType.Logs]: AverageLogRowSizeInBytes,
338
+ [ProductType.Metrics]: AverageMetricRowSizeInBytes,
339
+ }[productType] ?? fallbackSize;
340
+
341
+ if (!Number.isFinite(value) || value <= 0) {
342
+ return fallbackSize;
343
+ }
344
+
345
+ return value;
346
+ }
154
347
  }
155
348
 
156
349
  export default new Service();
@@ -56,6 +56,11 @@ export default class TelemetryMeteredPlan extends ServerMeteredPlan {
56
56
  ): Promise<void> {
57
57
  // get all unreported logs
58
58
 
59
+ await TelemetryUsageBillingService.stageTelemetryUsageForProject({
60
+ projectId: projectId,
61
+ productType: this.productType,
62
+ });
63
+
59
64
  const usageBillings: Array<TelemetryUsageBilling> =
60
65
  await TelemetryUsageBillingService.getUnreportedUsageBilling({
61
66
  projectId: projectId,
@@ -143,10 +143,10 @@ export default class TelemetryUtil {
143
143
  let { prefixKeysWithString } = data;
144
144
 
145
145
  if (prefixKeysWithString) {
146
- prefixKeysWithString = prefixKeysWithString + ".";
146
+ prefixKeysWithString = `${prefixKeysWithString}.`;
147
147
  }
148
148
 
149
- let finalObj: Dictionary<AttributeType | Array<AttributeType>> = {};
149
+ const finalObj: Dictionary<AttributeType | Array<AttributeType>> = {};
150
150
  const attributes: JSONArray = items;
151
151
 
152
152
  if (!attributes) {
@@ -154,25 +154,40 @@ export default class TelemetryUtil {
154
154
  }
155
155
 
156
156
  for (const attribute of attributes) {
157
- if (attribute["key"] && typeof attribute["key"] === "string") {
158
- const keyWithPrefix: string = `${prefixKeysWithString}${attribute["key"]}`;
159
-
160
- const value:
161
- | AttributeType
162
- | Dictionary<AttributeType>
163
- | Array<AttributeType> = this.getAttributeValues(
164
- keyWithPrefix,
165
- attribute["value"],
166
- );
167
-
168
- if (Array.isArray(value)) {
169
- finalObj = { ...finalObj, [keyWithPrefix]: value };
170
- } else if (typeof value === "object" && value !== null) {
171
- finalObj = { ...finalObj, ...(value as Dictionary<AttributeType>) };
172
- } else {
173
- finalObj[keyWithPrefix] = value;
157
+ if (!attribute["key"] || typeof attribute["key"] !== "string") {
158
+ continue;
159
+ }
160
+
161
+ const keyWithPrefix: string = `${prefixKeysWithString}${attribute["key"]}`;
162
+
163
+ const value = this.getAttributeValues(
164
+ keyWithPrefix,
165
+ attribute["value"],
166
+ );
167
+
168
+ if (value === null) {
169
+ finalObj[keyWithPrefix] = null;
170
+ continue;
171
+ }
172
+
173
+ if (Array.isArray(value)) {
174
+ finalObj[keyWithPrefix] = value;
175
+ continue;
176
+ }
177
+
178
+ if (typeof value === "object") {
179
+ for (const [nestedKey, nestedValue] of Object.entries(
180
+ value as Dictionary<AttributeType | Array<AttributeType>>,
181
+ )) {
182
+ finalObj[nestedKey] = nestedValue as
183
+ | AttributeType
184
+ | Array<AttributeType>;
174
185
  }
186
+
187
+ continue;
175
188
  }
189
+
190
+ finalObj[keyWithPrefix] = value as AttributeType;
176
191
  }
177
192
 
178
193
  return finalObj;
@@ -181,73 +196,106 @@ export default class TelemetryUtil {
181
196
  public static getAttributeValues(
182
197
  prefixKeysWithString: string,
183
198
  value: JSONValue,
184
- ): AttributeType | Dictionary<AttributeType> | Array<AttributeType> {
199
+ ):
200
+ | AttributeType
201
+ | Dictionary<AttributeType | Array<AttributeType>>
202
+ | Array<AttributeType>
203
+ | null {
185
204
  let finalObj:
186
- | Dictionary<AttributeType>
205
+ | Dictionary<AttributeType | Array<AttributeType>>
187
206
  | AttributeType
188
- | Array<AttributeType> = null;
189
- value = value as JSONObject;
190
-
191
- if (value["stringValue"]) {
192
- finalObj = value["stringValue"] as string;
193
- } else if (value["intValue"]) {
194
- finalObj = value["intValue"] as number;
195
- } else if (value["doubleValue"]) {
196
- finalObj = value["doubleValue"] as number;
197
- } else if (value["boolValue"]) {
198
- finalObj = value["boolValue"] as boolean;
199
- } else if (
200
- value["arrayValue"] &&
201
- (value["arrayValue"] as JSONObject)["values"]
202
- ) {
203
- const values: JSONArray = (value["arrayValue"] as JSONObject)[
204
- "values"
205
- ] as JSONArray;
206
- finalObj = values.map((v: JSONObject) => {
207
- return this.getAttributeValues(
208
- prefixKeysWithString,
209
- v,
210
- ) as AttributeType;
211
- }) as Array<AttributeType>;
212
- } else if (
213
- value["mapValue"] &&
214
- (value["mapValue"] as JSONObject)["fields"]
215
- ) {
216
- const fields: JSONObject = (value["mapValue"] as JSONObject)?.[
217
- "fields"
218
- ] as JSONObject;
219
-
220
- const flattenedFields: Dictionary<AttributeType> = {};
221
- for (const key in fields) {
222
- const prefixKey: string = `${prefixKeysWithString}.${key}`;
223
- const nestedValue: AttributeType | Dictionary<AttributeType> =
224
- this.getAttributeValues(prefixKey, fields[key]) as AttributeType;
225
- if (typeof nestedValue === "object" && nestedValue !== null) {
226
- for (const nestedKey in nestedValue as Dictionary<AttributeType>) {
227
- flattenedFields[`${prefixKey}.${nestedKey}`] = (
228
- nestedValue as Dictionary<AttributeType>
229
- )[nestedKey] as AttributeType;
207
+ | Array<AttributeType>
208
+ | null = null;
209
+ const jsonValue: JSONObject = value as JSONObject;
210
+
211
+ if (jsonValue && typeof jsonValue === "object") {
212
+ if (Object.prototype.hasOwnProperty.call(jsonValue, "stringValue")) {
213
+ const stringValue: JSONValue = jsonValue["stringValue"];
214
+ finalObj =
215
+ stringValue !== undefined && stringValue !== null
216
+ ? (stringValue as string)
217
+ : "";
218
+ } else if (Object.prototype.hasOwnProperty.call(jsonValue, "intValue")) {
219
+ const intValue: JSONValue = jsonValue["intValue"];
220
+ if (intValue !== undefined && intValue !== null) {
221
+ finalObj = intValue as number;
222
+ }
223
+ } else if (
224
+ Object.prototype.hasOwnProperty.call(jsonValue, "doubleValue")
225
+ ) {
226
+ const doubleValue: JSONValue = jsonValue["doubleValue"];
227
+ if (doubleValue !== undefined && doubleValue !== null) {
228
+ finalObj = doubleValue as number;
229
+ }
230
+ } else if (Object.prototype.hasOwnProperty.call(jsonValue, "boolValue")) {
231
+ finalObj = jsonValue["boolValue"] as boolean;
232
+ } else if (
233
+ jsonValue["arrayValue"] &&
234
+ (jsonValue["arrayValue"] as JSONObject)["values"]
235
+ ) {
236
+ const values: JSONArray = (jsonValue["arrayValue"] as JSONObject)[
237
+ "values"
238
+ ] as JSONArray;
239
+ finalObj = values.map((v: JSONObject) => {
240
+ return this.getAttributeValues(prefixKeysWithString, v) as AttributeType;
241
+ }) as Array<AttributeType>;
242
+ } else if (
243
+ jsonValue["mapValue"] &&
244
+ (jsonValue["mapValue"] as JSONObject)["fields"]
245
+ ) {
246
+ const fields: JSONObject = (jsonValue["mapValue"] as JSONObject)[
247
+ "fields"
248
+ ] as JSONObject;
249
+
250
+ const flattenedFields: Dictionary<AttributeType | Array<AttributeType>> =
251
+ {};
252
+ for (const key in fields) {
253
+ const nestedPrefix: string = `${prefixKeysWithString}.${key}`;
254
+ const nestedValue = this.getAttributeValues(
255
+ nestedPrefix,
256
+ fields[key],
257
+ );
258
+
259
+ if (nestedValue === null) {
260
+ flattenedFields[nestedPrefix] = null;
261
+ continue;
230
262
  }
231
- } else {
232
- flattenedFields[prefixKey] = nestedValue;
263
+
264
+ if (Array.isArray(nestedValue)) {
265
+ flattenedFields[nestedPrefix] = nestedValue;
266
+ continue;
267
+ }
268
+
269
+ if (typeof nestedValue === "object") {
270
+ for (const [nestedKey, nestedEntry] of Object.entries(
271
+ nestedValue as Dictionary<AttributeType | Array<AttributeType>>,
272
+ )) {
273
+ flattenedFields[nestedKey] = nestedEntry as
274
+ | AttributeType
275
+ | Array<AttributeType>;
276
+ }
277
+
278
+ continue;
279
+ }
280
+
281
+ flattenedFields[nestedPrefix] = nestedValue as AttributeType;
233
282
  }
283
+
284
+ finalObj = flattenedFields;
285
+ } else if (
286
+ jsonValue["kvlistValue"] &&
287
+ (jsonValue["kvlistValue"] as JSONObject)["values"]
288
+ ) {
289
+ const values: JSONArray = (jsonValue["kvlistValue"] as JSONObject)[
290
+ "values"
291
+ ] as JSONArray;
292
+ finalObj = this.getAttributes({
293
+ prefixKeysWithString,
294
+ items: values,
295
+ });
296
+ } else if ("nullValue" in jsonValue) {
297
+ finalObj = null;
234
298
  }
235
- finalObj = flattenedFields;
236
- }
237
- // kvlistValue
238
- else if (
239
- value["kvlistValue"] &&
240
- (value["kvlistValue"] as JSONObject)["values"]
241
- ) {
242
- const values: JSONArray = (value["kvlistValue"] as JSONObject)[
243
- "values"
244
- ] as JSONArray;
245
- finalObj = this.getAttributes({
246
- prefixKeysWithString,
247
- items: values,
248
- }) as Dictionary<AttributeType>;
249
- } else if (value["nullValue"]) {
250
- finalObj = null;
251
299
  }
252
300
 
253
301
  return finalObj;
@@ -48,10 +48,9 @@ export default class VMRunner {
48
48
 
49
49
  vm.createContext(sandbox); // Contextify the object.
50
50
 
51
- const script: string =
52
- `(async()=>{
53
- ${code}
54
- })()` || "";
51
+ const script: string = `(async()=>{
52
+ ${code}
53
+ })()`;
55
54
 
56
55
  const returnVal: any = await vm.runInContext(script, sandbox, {
57
56
  timeout: options.timeout || 5000,
@@ -148,6 +148,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
148
148
  title="Key"
149
149
  required={true}
150
150
  hideOptionalLabel={true}
151
+ className="block text-xs text-gray-500 font-normal flex justify-between"
151
152
  />
152
153
  </div>
153
154
  <AutocompleteTextInput
@@ -173,6 +174,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
173
174
  title="Type"
174
175
  hideOptionalLabel={true}
175
176
  required={true}
177
+ className="block text-xs text-gray-500 font-normal flex justify-between"
176
178
  />
177
179
  </div>
178
180
  <Dropdown
@@ -206,6 +208,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
206
208
  title="Value"
207
209
  hideOptionalLabel={true}
208
210
  required={true}
211
+ className="block text-xs text-gray-500 font-normal flex justify-between"
209
212
  />
210
213
  </div>
211
214
  {item.type === ValueType.Text && (
@@ -9,6 +9,7 @@ export interface ComponentProps {
9
9
  description?: string | ReactElement | undefined;
10
10
  isHeading?: boolean | undefined;
11
11
  hideOptionalLabel?: boolean | undefined;
12
+ className?: string | undefined;
12
13
  }
13
14
 
14
15
  const FieldLabelElement: FunctionComponent<ComponentProps> = (
@@ -17,9 +18,12 @@ const FieldLabelElement: FunctionComponent<ComponentProps> = (
17
18
  return (
18
19
  <>
19
20
  <label
20
- className={`block ${
21
- props.isHeading ? "text-lg" : "text-sm"
22
- } font-medium text-gray-700 flex justify-between`}
21
+ className={
22
+ props.className ||
23
+ `block ${
24
+ props.isHeading ? "text-lg" : "text-sm"
25
+ } font-medium text-gray-700 flex justify-between`
26
+ }
23
27
  >
24
28
  <span>
25
29
  {props.title}{" "}
@@ -265,7 +265,7 @@ const LogItem: FunctionComponent<ComponentProps> = (
265
265
  if (isCollapsed) {
266
266
  return (
267
267
  <div
268
- className={`group relative text-slate-200 flex items-center gap-2 cursor-pointer hover:bg-slate-800/40 px-2 py-0.5 border border-transparent border-l ${leftBorderColor} rounded-sm transition-colors duration-100`}
268
+ className={`group relative text-slate-200 flex items-center gap-2 cursor-pointer hover:bg-slate-800/40 px-2 py-0.5 border border-transparent border-l ${leftBorderColor} rounded-sm transition-colors duration-100 font-mono`}
269
269
  onClick={toggleCollapsed}
270
270
  role="button"
271
271
  aria-expanded={!isCollapsed}
@@ -311,11 +311,14 @@ const LogItem: FunctionComponent<ComponentProps> = (
311
311
  className={`${bodyColor} font-mono text-[13px] md:text-sm leading-5 tracking-tight subpixel-antialiased flex-1 min-w-0`}
312
312
  >
313
313
  {isBodyInJSON ? (
314
- <div className="truncate" title={logBodyMinified}>
314
+ <div className="truncate font-mono" title={logBodyMinified}>
315
315
  {logBodyMinified}
316
316
  </div>
317
317
  ) : (
318
- <div className="truncate" title={props.log.body?.toString()}>
318
+ <div
319
+ className="truncate font-mono"
320
+ title={props.log.body?.toString()}
321
+ >
319
322
  {props.log.body?.toString()}
320
323
  </div>
321
324
  )}
@@ -339,7 +342,12 @@ const LogItem: FunctionComponent<ComponentProps> = (
339
342
  className={`group relative text-slate-200 bg-slate-950/70 border ${leftBorderColor} border-l border-slate-900 rounded-sm p-2 hover:border-slate-700 transition-colors`}
340
343
  >
341
344
  {/* Header with Service Name and Close Indicator */}
342
- <div className="flex items-center justify-between mb-1 pb-1 border-b border-slate-800/80">
345
+ <div
346
+ className="flex items-center justify-between mb-1 pb-1 border-b border-slate-800/80"
347
+ onClick={() => {
348
+ toggleCollapsed();
349
+ }}
350
+ >
343
351
  {serviceName && (
344
352
  <div
345
353
  className="text-[13px] font-semibold"