@oneuptime/common 10.8.0 → 10.8.2

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 (67) hide show
  1. package/Models/DatabaseModels/CloudResourceInstance.ts +1 -2
  2. package/Models/DatabaseModels/CloudResourceOwnerTeam.ts +1 -2
  3. package/Models/DatabaseModels/CloudResourceOwnerUser.ts +1 -2
  4. package/Models/DatabaseModels/TelemetryException.ts +3 -2
  5. package/Server/API/TelemetryAPI.ts +78 -25
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1780931863719-AddTelemetryResourceMetadataColumns.ts +1 -3
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1780933132562-AddServerlessFunctionTables.ts +6 -2
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1780935387827-AddCloudResourceTables.ts +18 -6
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1781011482945-MigrationName.ts +2317 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  11. package/Server/Services/CloudResourceLabelRuleEngineService.ts +4 -1
  12. package/Server/Services/CloudResourceOwnerRuleEngineService.ts +4 -1
  13. package/Server/Services/LogAggregationService.ts +41 -16
  14. package/Server/Services/OpenTelemetryIngestService.ts +3 -1
  15. package/Server/Services/RumApplicationLabelRuleEngineService.ts +4 -1
  16. package/Server/Services/RumApplicationOwnerRuleEngineService.ts +4 -1
  17. package/Server/Services/ServerlessFunctionInstanceService.ts +4 -2
  18. package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +7 -2
  19. package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +7 -2
  20. package/Server/Services/TraceAggregationService.ts +128 -4
  21. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +4 -3
  22. package/Types/Permission.ts +4 -2
  23. package/UI/Components/BulkUpdate/BulkLabelActions.tsx +14 -14
  24. package/UI/Components/Forms/Fields/FormField.tsx +18 -12
  25. package/UI/Components/Input/Input.tsx +3 -1
  26. package/UI/Components/TextArea/TextArea.tsx +3 -1
  27. package/UI/Components/Toast/Toast.tsx +4 -2
  28. package/build/dist/Models/DatabaseModels/CloudResourceInstance.js.map +1 -1
  29. package/build/dist/Models/DatabaseModels/CloudResourceOwnerTeam.js.map +1 -1
  30. package/build/dist/Models/DatabaseModels/CloudResourceOwnerUser.js.map +1 -1
  31. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  32. package/build/dist/Server/API/TelemetryAPI.js +61 -23
  33. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780931863719-AddTelemetryResourceMetadataColumns.js.map +1 -1
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780933132562-AddServerlessFunctionTables.js.map +1 -1
  36. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780935387827-AddCloudResourceTables.js.map +1 -1
  37. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781011482945-MigrationName.js +798 -0
  38. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1781011482945-MigrationName.js.map +1 -0
  39. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  40. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  41. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
  42. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
  43. package/build/dist/Server/Services/LogAggregationService.js +33 -10
  44. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  45. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  46. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
  47. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
  48. package/build/dist/Server/Services/ServerlessFunctionInstanceService.js +4 -2
  49. package/build/dist/Server/Services/ServerlessFunctionInstanceService.js.map +1 -1
  50. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +2 -1
  51. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
  52. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +2 -1
  53. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
  54. package/build/dist/Server/Services/TraceAggregationService.js +105 -4
  55. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  56. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  57. package/build/dist/Types/Permission.js.map +1 -1
  58. package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js.map +1 -1
  59. package/build/dist/UI/Components/Forms/Fields/FormField.js +15 -12
  60. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  61. package/build/dist/UI/Components/Input/Input.js +3 -1
  62. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  63. package/build/dist/UI/Components/TextArea/TextArea.js +3 -1
  64. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  65. package/build/dist/UI/Components/Toast/Toast.js +4 -2
  66. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  67. package/package.json +1 -1
@@ -378,6 +378,7 @@ import { AddRumApplicationRuleTables1780940998002 } from "./1780940998002-AddRum
378
378
  import { AddTelemetryResourceInventoryTables1780941762204 } from "./1780941762204-AddTelemetryResourceInventoryTables";
379
379
  import { AddRumApplicationSdkLanguage1780985763463 } from "./1780985763463-AddRumApplicationSdkLanguage";
380
380
  import { RecastCloudResourcesByEnvironment1780987192743 } from "./1780987192743-RecastCloudResourcesByEnvironment";
381
+ import { MigrationName1781011482945 } from "./1781011482945-MigrationName";
381
382
 
382
383
  export default [
383
384
  InitialMigration,
@@ -760,4 +761,5 @@ export default [
760
761
  AddTelemetryResourceInventoryTables1780941762204,
761
762
  AddRumApplicationSdkLanguage1780985763463,
762
763
  RecastCloudResourcesByEnvironment1780987192743,
764
+ MigrationName1781011482945,
763
765
  ];
@@ -153,7 +153,10 @@ class CloudResourceLabelRuleEngineServiceClass {
153
153
  if (
154
154
  rule.descriptionRegexPattern &&
155
155
  (!cloudResource.description ||
156
- !this.testRegex(rule.descriptionRegexPattern, cloudResource.description))
156
+ !this.testRegex(
157
+ rule.descriptionRegexPattern,
158
+ cloudResource.description,
159
+ ))
157
160
  ) {
158
161
  return false;
159
162
  }
@@ -170,7 +170,10 @@ class CloudResourceOwnerRuleEngineServiceClass {
170
170
  if (
171
171
  rule.descriptionRegexPattern &&
172
172
  (!cloudResource.description ||
173
- !this.testRegex(rule.descriptionRegexPattern, cloudResource.description))
173
+ !this.testRegex(
174
+ rule.descriptionRegexPattern,
175
+ cloudResource.description,
176
+ ))
174
177
  ) {
175
178
  return false;
176
179
  }
@@ -165,40 +165,65 @@ export class LogAggregationService {
165
165
  private static buildHistogramStatement(request: HistogramRequest): Statement {
166
166
  const intervalSeconds: number = request.bucketSizeInMinutes * 60;
167
167
 
168
+ /*
169
+ * Two-stage aggregation mirroring TraceAggregationService.getHistogram.
170
+ * The inner query groups by toStartOfInterval(time, INTERVAL 1 MINUTE) —
171
+ * the exact key expression of the proj_severity_histogram projection
172
+ * (projectId, severityText, minute) — and filters the window on that same
173
+ * expression rather than raw `time`. A raw `time` predicate references a
174
+ * column the aggregate projection does not store, so ClickHouse rejects
175
+ * the projection and full-scans (verified: 2.1M rows / ~46ms vs 978 rows /
176
+ * ~7ms with this form). The outer query re-buckets the tiny minute-level
177
+ * result to the requested size. Window edges round to the minute, which is
178
+ * consistent with the minute-bucketed output and only shifts the first/last
179
+ * bucket by the partial boundary minute when the range is not minute-aligned.
180
+ *
181
+ * A non-projection filter (serviceId, traceId, spanId, attributes) makes
182
+ * the inner query transparently fall back to a base-table scan — same cost
183
+ * as before, still correct.
184
+ */
168
185
  const statement: Statement = SQL`
169
186
  SELECT
170
- toStartOfInterval(time, INTERVAL ${{
187
+ toStartOfInterval(minute, INTERVAL ${{
171
188
  type: TableColumnType.Number,
172
189
  value: intervalSeconds,
173
190
  }} SECOND) AS bucket,
174
191
  severityText,
175
- count() AS cnt
176
- FROM ${LogAggregationService.TABLE_NAME}
177
- WHERE projectId = ${{
178
- type: TableColumnType.ObjectID,
179
- value: request.projectId,
180
- }}
181
- AND time >= ${{
182
- type: TableColumnType.Date,
183
- value: request.startTime,
184
- }}
185
- AND time <= ${{
186
- type: TableColumnType.Date,
187
- value: request.endTime,
192
+ sum(cnt_minute) AS cnt
193
+ FROM (
194
+ SELECT
195
+ toStartOfInterval(time, INTERVAL 1 MINUTE) AS minute,
196
+ severityText,
197
+ count() AS cnt_minute
198
+ FROM ${LogAggregationService.TABLE_NAME}
199
+ WHERE projectId = ${{
200
+ type: TableColumnType.ObjectID,
201
+ value: request.projectId,
188
202
  }}
203
+ AND toStartOfInterval(time, INTERVAL 1 MINUTE) >= toStartOfInterval(${{
204
+ type: TableColumnType.Date,
205
+ value: request.startTime,
206
+ }}, INTERVAL 1 MINUTE)
207
+ AND toStartOfInterval(time, INTERVAL 1 MINUTE) <= toStartOfInterval(${{
208
+ type: TableColumnType.Date,
209
+ value: request.endTime,
210
+ }}, INTERVAL 1 MINUTE)
189
211
  `;
190
212
 
191
213
  LogAggregationService.appendCommonFilters(statement, request);
192
214
 
193
- statement.append(" GROUP BY bucket, severityText ORDER BY bucket ASC");
215
+ statement.append(
216
+ " GROUP BY minute, severityText ) GROUP BY bucket, severityText ORDER BY bucket ASC",
217
+ );
194
218
 
195
219
  /*
196
220
  * Defense in depth: cap histogram runtime below nginx's 60s
197
221
  * proxy_read_timeout. 'break' returns partial aggregated results
198
222
  * rather than throwing, which is acceptable for a density viz.
223
+ * Explicitly enable projection use.
199
224
  */
200
225
  statement.append(
201
- " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'",
226
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break', optimize_use_projections = 1",
202
227
  );
203
228
 
204
229
  return statement;
@@ -108,7 +108,9 @@ export default class OTelIngestService {
108
108
  attribute["value"] &&
109
109
  (attribute["value"] as JSONObject)["stringValue"]
110
110
  ) {
111
- const value: unknown = (attribute["value"] as JSONObject)["stringValue"];
111
+ const value: unknown = (attribute["value"] as JSONObject)[
112
+ "stringValue"
113
+ ];
112
114
  if (typeof value === "string" && value.trim()) {
113
115
  return value.trim();
114
116
  }
@@ -153,7 +153,10 @@ class RumApplicationLabelRuleEngineServiceClass {
153
153
  if (
154
154
  rule.descriptionRegexPattern &&
155
155
  (!rumApplication.description ||
156
- !this.testRegex(rule.descriptionRegexPattern, rumApplication.description))
156
+ !this.testRegex(
157
+ rule.descriptionRegexPattern,
158
+ rumApplication.description,
159
+ ))
157
160
  ) {
158
161
  return false;
159
162
  }
@@ -170,7 +170,10 @@ class RumApplicationOwnerRuleEngineServiceClass {
170
170
  if (
171
171
  rule.descriptionRegexPattern &&
172
172
  (!rumApplication.description ||
173
- !this.testRegex(rule.descriptionRegexPattern, rumApplication.description))
173
+ !this.testRegex(
174
+ rule.descriptionRegexPattern,
175
+ rumApplication.description,
176
+ ))
174
177
  ) {
175
178
  return false;
176
179
  }
@@ -47,8 +47,10 @@ export class Service extends DatabaseService<Model> {
47
47
  item.lastSeenAt = OneUptimeDate.getCurrentDate();
48
48
  await this.create({ data: item, props: { isRoot: true } });
49
49
  } catch (err) {
50
- // Inventory is best-effort — a unique-violation race or transient error
51
- // must never fail ingest.
50
+ /*
51
+ * Inventory is best-effort — a unique-violation race or transient error
52
+ * must never fail ingest.
53
+ */
52
54
  logger.warn(
53
55
  `ServerlessFunctionInstanceService.recordInstance failed: ${
54
56
  err instanceof Error ? err.message : String(err)
@@ -126,7 +126,10 @@ class ServerlessFunctionLabelRuleEngineServiceClass {
126
126
  rule: ServerlessFunctionLabelRule,
127
127
  ): boolean {
128
128
  if (rule.matchLabels && rule.matchLabels.length > 0) {
129
- if (!serverlessFunction.labels || serverlessFunction.labels.length === 0) {
129
+ if (
130
+ !serverlessFunction.labels ||
131
+ serverlessFunction.labels.length === 0
132
+ ) {
130
133
  return false;
131
134
  }
132
135
  const ruleLabelIds: Array<string> = rule.matchLabels.map((l: Label) => {
@@ -173,7 +176,9 @@ class ServerlessFunctionLabelRuleEngineServiceClass {
173
176
  const regex: RegExp = new RegExp(pattern, "i");
174
177
  return regex.test(value);
175
178
  } catch {
176
- logger.warn(`Invalid regex in serverless function label rule: ${pattern}`);
179
+ logger.warn(
180
+ `Invalid regex in serverless function label rule: ${pattern}`,
181
+ );
177
182
  return false;
178
183
  }
179
184
  }
@@ -143,7 +143,10 @@ class ServerlessFunctionOwnerRuleEngineServiceClass {
143
143
  rule: ServerlessFunctionOwnerRule,
144
144
  ): boolean {
145
145
  if (rule.matchLabels && rule.matchLabels.length > 0) {
146
- if (!serverlessFunction.labels || serverlessFunction.labels.length === 0) {
146
+ if (
147
+ !serverlessFunction.labels ||
148
+ serverlessFunction.labels.length === 0
149
+ ) {
147
150
  return false;
148
151
  }
149
152
  const ruleLabelIds: Array<string> = rule.matchLabels.map((l: Label) => {
@@ -190,7 +193,9 @@ class ServerlessFunctionOwnerRuleEngineServiceClass {
190
193
  const regex: RegExp = new RegExp(pattern, "i");
191
194
  return regex.test(value);
192
195
  } catch {
193
- logger.warn(`Invalid regex in serverless function owner rule: ${pattern}`);
196
+ logger.warn(
197
+ `Invalid regex in serverless function owner rule: ${pattern}`,
198
+ );
194
199
  return false;
195
200
  }
196
201
  }
@@ -345,6 +345,121 @@ export class TraceAggregationService {
345
345
  return result;
346
346
  }
347
347
 
348
+ /*
349
+ * Accurate per-service and per-status counts over the FULL time window,
350
+ * computed with a real GROUP BY instead of the recent-N sample in
351
+ * getFacetValuesFromSample(). The sample saturates with whichever service
352
+ * is chattiest right now, so high-volume services that aren't in the most
353
+ * recent N root spans read 0 — the "top 1000" symptom. This GROUP BY is
354
+ * exact regardless of skew.
355
+ *
356
+ * It is cheap because it rides the proj_hist_by_minute aggregate projection
357
+ * (projectId, minute, serviceId, statusCode, isRootSpan -> count): a 1-month
358
+ * window reads a few thousand pre-aggregated minute rows in single-digit ms
359
+ * instead of scanning tens of millions of raw spans. Two things are required
360
+ * for ClickHouse to actually pick that projection, both load-bearing:
361
+ * 1. The time predicate must be on toStartOfMinute(startTime) — the
362
+ * projection's key expression — NOT raw startTime. A raw startTime
363
+ * filter references a column the aggregate projection does not store, so
364
+ * ClickHouse rejects the projection and full-scans. (Window edges land
365
+ * on minute boundaries, consistent with the minute-bucketed histogram.)
366
+ * 2. Every other predicate must be on a projection column. isRootSpan,
367
+ * serviceId and statusCode all are, so the default sidebar load and
368
+ * drill-down-by-service stay on the projection. A non-projection filter
369
+ * (kind / name / traceId / attributes) transparently falls back to a
370
+ * base-table scan — still correct, still bounded by max_execution_time.
371
+ *
372
+ * serviceId is intentionally NOT disambiguated by serviceType here. Resource
373
+ * IDs are globally unique, so a single serviceId -> count map correctly
374
+ * serves the service / host / docker host / k8s cluster facets once merged
375
+ * against each Postgres source-of-truth list (a host id never collides with
376
+ * a service id, so an unrelated entry is simply never looked up). Omitting
377
+ * the serviceType predicate keeps the query projection-eligible.
378
+ */
379
+ @CaptureSpan()
380
+ public static async getResourceFacetCounts(
381
+ request: MultiFacetRequest,
382
+ ): Promise<{
383
+ serviceCounts: Map<string, number>;
384
+ statusCounts: Map<string, number>;
385
+ }> {
386
+ const statement: Statement = new Statement();
387
+ statement.append(
388
+ `SELECT serviceId, statusCode, count() AS cnt FROM ${TraceAggregationService.TABLE_NAME}`,
389
+ );
390
+ statement.append(
391
+ SQL` WHERE projectId = ${{
392
+ type: TableColumnType.ObjectID,
393
+ value: request.projectId,
394
+ }} AND toStartOfMinute(startTime) >= toStartOfMinute(${{
395
+ type: TableColumnType.Date,
396
+ value: request.startTime,
397
+ }}) AND toStartOfMinute(startTime) <= toStartOfMinute(${{
398
+ type: TableColumnType.Date,
399
+ value: request.endTime,
400
+ }})`,
401
+ );
402
+
403
+ TraceAggregationService.appendCommonFilters(statement, request);
404
+
405
+ statement.append(" GROUP BY serviceId, statusCode");
406
+
407
+ /*
408
+ * Cap runtime below nginx's 60s proxy_read_timeout and explicitly allow
409
+ * projection use so proj_hist_by_minute is read when eligible (see the
410
+ * toStartOfMinute note above).
411
+ */
412
+ statement.append(
413
+ " SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break', optimize_use_projections = 1",
414
+ );
415
+
416
+ const serviceCounts: Map<string, number> = new Map<string, number>();
417
+ const statusCounts: Map<string, number> = new Map<string, number>();
418
+
419
+ const dbResult: Results = await SpanService.executeQuery(statement);
420
+
421
+ let rows: Array<JSONObject> = [];
422
+ try {
423
+ const response: DbJSONResponse = await dbResult.json<{
424
+ data?: Array<JSONObject>;
425
+ }>();
426
+ rows = response.data || [];
427
+ } catch {
428
+ /*
429
+ * 'break' mode can return truncated JSON on timeout. Degrade to empty
430
+ * counts — the Postgres resolver still lists every resource (count 0),
431
+ * which is no worse than the prior transient-failure behavior.
432
+ */
433
+ logger.warn(
434
+ "Resource facet count query returned unparseable response, returning empty counts",
435
+ );
436
+ return { serviceCounts, statusCounts };
437
+ }
438
+
439
+ for (const row of rows) {
440
+ const cnt: number = Number(row["cnt"] || 0);
441
+
442
+ const rawServiceId: unknown = row["serviceId"];
443
+ if (rawServiceId !== undefined && rawServiceId !== null) {
444
+ const serviceId: string = String(rawServiceId);
445
+ if (serviceId.length > 0) {
446
+ serviceCounts.set(
447
+ serviceId,
448
+ (serviceCounts.get(serviceId) || 0) + cnt,
449
+ );
450
+ }
451
+ }
452
+
453
+ const rawStatusCode: unknown = row["statusCode"];
454
+ if (rawStatusCode !== undefined && rawStatusCode !== null) {
455
+ const statusCode: string = String(rawStatusCode);
456
+ statusCounts.set(statusCode, (statusCounts.get(statusCode) || 0) + cnt);
457
+ }
458
+ }
459
+
460
+ return { serviceCounts, statusCounts };
461
+ }
462
+
348
463
  private static mapStatusCodeToSeries(code: number): string {
349
464
  if (code === 1) {
350
465
  return "ok";
@@ -366,6 +481,15 @@ export class TraceAggregationService {
366
481
  * even for multi-week ranges. The outer query then re-buckets the tiny
367
482
  * minute-level result to the requested bucket size.
368
483
  *
484
+ * The time window is filtered on toStartOfMinute(startTime) — the
485
+ * projection's key expression — NOT raw startTime. A raw startTime
486
+ * predicate references a column the aggregate projection does not store,
487
+ * so ClickHouse rejects proj_hist_by_minute and full-scans the base table
488
+ * (verified: 32M rows / ~220ms vs 1.5K rows / ~6ms with this form). The
489
+ * window edges round to the minute, which is consistent with the
490
+ * minute-bucketed output and only shifts the first/last bucket by the
491
+ * partial boundary minute when the range is not minute-aligned.
492
+ *
369
493
  * If any non-projection filter (kind, name, traceId, nameSearchText,
370
494
  * attributes) is active, ClickHouse transparently falls back to
371
495
  * scanning the main table for the inner query — same cost as before.
@@ -388,14 +512,14 @@ export class TraceAggregationService {
388
512
  type: TableColumnType.ObjectID,
389
513
  value: request.projectId,
390
514
  }}
391
- AND startTime >= ${{
515
+ AND toStartOfMinute(startTime) >= toStartOfMinute(${{
392
516
  type: TableColumnType.Date,
393
517
  value: request.startTime,
394
- }}
395
- AND startTime <= ${{
518
+ }})
519
+ AND toStartOfMinute(startTime) <= toStartOfMinute(${{
396
520
  type: TableColumnType.Date,
397
521
  value: request.endTime,
398
- }}
522
+ }})
399
523
  `;
400
524
 
401
525
  TraceAggregationService.appendCommonFilters(statement, request);
@@ -389,8 +389,8 @@ export default class ResourceFacetResolver {
389
389
  });
390
390
  }
391
391
 
392
- const apps: Array<RumApplicationModel> =
393
- await RumApplicationService.findBy({
392
+ const apps: Array<RumApplicationModel> = await RumApplicationService.findBy(
393
+ {
394
394
  query: query as any,
395
395
  select: {
396
396
  _id: true,
@@ -400,7 +400,8 @@ export default class ResourceFacetResolver {
400
400
  limit: new PositiveNumber(limit),
401
401
  skip: new PositiveNumber(0),
402
402
  props: { isRoot: true },
403
- });
403
+ },
404
+ );
404
405
 
405
406
  return ResourceFacetResolver.mergeCounts(
406
407
  apps.map(
@@ -7627,7 +7627,8 @@ export class PermissionHelper {
7627
7627
  {
7628
7628
  permission: Permission.EditRumApplication,
7629
7629
  title: "Edit RUM Application",
7630
- description: "This permission can edit RUM Application of this project.",
7630
+ description:
7631
+ "This permission can edit RUM Application of this project.",
7631
7632
  isAssignableToTenant: true,
7632
7633
  isAccessControlPermission: true,
7633
7634
  isRolePermission: false,
@@ -7636,7 +7637,8 @@ export class PermissionHelper {
7636
7637
  {
7637
7638
  permission: Permission.ReadRumApplication,
7638
7639
  title: "Read RUM Application",
7639
- description: "This permission can read RUM Application of this project.",
7640
+ description:
7641
+ "This permission can read RUM Application of this project.",
7640
7642
  isAssignableToTenant: true,
7641
7643
  isAccessControlPermission: true,
7642
7644
  isRolePermission: false,
@@ -208,9 +208,9 @@ function useBulkLabelActions<T extends BaseModel>(
208
208
  * modal offers only labels that can really be removed instead of every
209
209
  * label in the project.
210
210
  */
211
- const loadLabelsForSelectedItems: (
211
+ const loadLabelsForSelectedItems: (items: Array<T>) => Promise<void> = async (
212
212
  items: Array<T>,
213
- ) => Promise<void> = async (items: Array<T>): Promise<void> => {
213
+ ): Promise<void> => {
214
214
  setIsLoadingRemoveLabels(true);
215
215
 
216
216
  try {
@@ -259,20 +259,20 @@ function useBulkLabelActions<T extends BaseModel>(
259
259
  }
260
260
  }
261
261
 
262
- const options: Array<DropdownOption> = Array.from(
263
- labelById.values(),
264
- ).map((label: Label) => {
265
- const option: DropdownOption = {
266
- label: label.name || "",
267
- value: label._id?.toString() || "",
268
- };
262
+ const options: Array<DropdownOption> = Array.from(labelById.values()).map(
263
+ (label: Label) => {
264
+ const option: DropdownOption = {
265
+ label: label.name || "",
266
+ value: label._id?.toString() || "",
267
+ };
269
268
 
270
- if (label.color) {
271
- option.color = label.color;
272
- }
269
+ if (label.color) {
270
+ option.color = label.color;
271
+ }
273
272
 
274
- return option;
275
- });
273
+ return option;
274
+ },
275
+ );
276
276
 
277
277
  options.sort((a: DropdownOption, b: DropdownOption) => {
278
278
  return a.label.localeCompare(b.label);
@@ -40,6 +40,7 @@ import Radio, { RadioValue } from "../../Radio/Radio";
40
40
  import { BasicRadioButtonOption } from "../../RadioButtons/BasicRadioButtons";
41
41
  import HorizontalRule from "../../HorizontalRule/HorizontalRule";
42
42
  import MarkdownEditor from "../../Markdown.tsx/MarkdownEditor";
43
+ import useTranslateValue from "../../../Utils/Translation";
43
44
 
44
45
  export interface ComponentProps<T extends GenericObject> {
45
46
  field: Field<T>;
@@ -61,6 +62,11 @@ const FormField: <T extends GenericObject>(
61
62
  ) => ReactElement = <T extends GenericObject>(
62
63
  props: ComponentProps<T>,
63
64
  ): ReactElement => {
65
+ const { translateString } = useTranslateValue();
66
+ const translatedPlaceholder: string | undefined = translateString(
67
+ props.field.placeholder,
68
+ );
69
+
64
70
  type onChangeFunction = (value: JSONValue) => void;
65
71
 
66
72
  const onChange: onChangeFunction = (value: JSONValue): void => {
@@ -337,7 +343,7 @@ const FormField: <T extends GenericObject>(
337
343
  (props.field.defaultValue as any) ||
338
344
  undefined
339
345
  }
340
- placeholder={props.field.placeholder || undefined}
346
+ placeholder={translatedPlaceholder || undefined}
341
347
  />
342
348
  )}
343
349
 
@@ -351,7 +357,7 @@ const FormField: <T extends GenericObject>(
351
357
  props.setFieldTouched(props.fieldName, true);
352
358
  }}
353
359
  tabIndex={index}
354
- placeholder={props.field.placeholder || ""}
360
+ placeholder={translatedPlaceholder || ""}
355
361
  initialValue={
356
362
  props.currentValues &&
357
363
  (props.currentValues as any)[props.fieldName]
@@ -371,7 +377,7 @@ const FormField: <T extends GenericObject>(
371
377
  props.setFieldTouched(props.fieldName, true);
372
378
  }}
373
379
  tabIndex={index}
374
- placeholder={props.field.placeholder || ""}
380
+ placeholder={translatedPlaceholder || ""}
375
381
  initialValue={
376
382
  props.currentValues &&
377
383
  (props.currentValues as any)[props.fieldName]
@@ -414,7 +420,7 @@ const FormField: <T extends GenericObject>(
414
420
  labelField={props.field.dropdownModal.labelField}
415
421
  valueField={props.field.dropdownModal.valueField}
416
422
  options={props.field.dropdownOptions || []}
417
- placeholder={props.field.placeholder || ""}
423
+ placeholder={translatedPlaceholder || ""}
418
424
  value={
419
425
  props.currentValues &&
420
426
  (props.currentValues as any)[props.fieldName]
@@ -442,7 +448,7 @@ const FormField: <T extends GenericObject>(
442
448
  FormFieldSchemaType.MultiSelectDropdown
443
449
  }
444
450
  options={props.field.dropdownOptions || []}
445
- placeholder={props.field.placeholder || ""}
451
+ placeholder={translatedPlaceholder || ""}
446
452
  value={
447
453
  props.currentValues &&
448
454
  (props.currentValues as any)[props.fieldName]
@@ -585,7 +591,7 @@ const FormField: <T extends GenericObject>(
585
591
  ? (props.currentValues as any)[props.fieldName]
586
592
  : ""
587
593
  }
588
- placeholder={props.field.placeholder || ""}
594
+ placeholder={translatedPlaceholder || ""}
589
595
  />
590
596
  )}
591
597
 
@@ -614,7 +620,7 @@ const FormField: <T extends GenericObject>(
614
620
  ? (props.currentValues as any)[props.fieldName]
615
621
  : ""
616
622
  }
617
- placeholder={props.field.placeholder || ""}
623
+ placeholder={translatedPlaceholder || ""}
618
624
  />
619
625
  )}
620
626
 
@@ -637,7 +643,7 @@ const FormField: <T extends GenericObject>(
637
643
  ? (props.currentValues as any)[props.fieldName]
638
644
  : ""
639
645
  }
640
- placeholder={props.field.placeholder || ""}
646
+ placeholder={translatedPlaceholder || ""}
641
647
  />
642
648
  )}
643
649
 
@@ -660,7 +666,7 @@ const FormField: <T extends GenericObject>(
660
666
  ? (props.currentValues as any)[props.fieldName]
661
667
  : "",
662
668
 
663
- placeholder: props.field.placeholder || "",
669
+ placeholder: translatedPlaceholder || "",
664
670
  })}
665
671
 
666
672
  {(props.field.fieldType === FormFieldSchemaType.HTML ||
@@ -684,7 +690,7 @@ const FormField: <T extends GenericObject>(
684
690
  ? (props.currentValues as any)[props.fieldName]
685
691
  : ""
686
692
  }
687
- placeholder={props.field.placeholder || ""}
693
+ placeholder={translatedPlaceholder || ""}
688
694
  />
689
695
  )}
690
696
 
@@ -741,7 +747,7 @@ const FormField: <T extends GenericObject>(
741
747
  ? []
742
748
  : undefined
743
749
  }
744
- placeholder={props.field.placeholder || ""}
750
+ placeholder={translatedPlaceholder || ""}
745
751
  />
746
752
  )}
747
753
 
@@ -849,7 +855,7 @@ const FormField: <T extends GenericObject>(
849
855
  ? (props.currentValues as any)[props.fieldName]
850
856
  : props.field.defaultValue || ""
851
857
  }
852
- placeholder={props.field.placeholder || ""}
858
+ placeholder={translatedPlaceholder || ""}
853
859
  />
854
860
  )}
855
861
  </div>
@@ -1,5 +1,6 @@
1
1
  // Tailwind
2
2
  import { Logger } from "../../Utils/Logger";
3
+ import useTranslateValue from "../../Utils/Translation";
3
4
  import Icon from "../Icon/Icon";
4
5
  import OneUptimeDate from "../../../Types/Date";
5
6
  import IconProp from "../../../Types/Icon/IconProp";
@@ -46,6 +47,7 @@ export interface ComponentProps {
46
47
  const Input: FunctionComponent<ComponentProps> = (
47
48
  props: ComponentProps,
48
49
  ): ReactElement => {
50
+ const { translateString } = useTranslateValue();
49
51
  let className: string = "";
50
52
 
51
53
  if (!props.className) {
@@ -199,7 +201,7 @@ const Input: FunctionComponent<ComponentProps> = (
199
201
  ? "1"
200
202
  : undefined
201
203
  }
202
- placeholder={props.placeholder}
204
+ placeholder={translateString(props.placeholder)}
203
205
  className={className}
204
206
  onBlur={() => {
205
207
  if (props.onBlur) {
@@ -1,5 +1,6 @@
1
1
  import Icon from "../Icon/Icon";
2
2
  import IconProp from "../../../Types/Icon/IconProp";
3
+ import useTranslateValue from "../../Utils/Translation";
3
4
  import React, {
4
5
  FunctionComponent,
5
6
  ReactElement,
@@ -25,6 +26,7 @@ export interface ComponentProps {
25
26
  const TextArea: FunctionComponent<ComponentProps> = (
26
27
  props: ComponentProps,
27
28
  ): ReactElement => {
29
+ const { translateString } = useTranslateValue();
28
30
  const [text, setText] = useState<string>(props.initialValue || "");
29
31
 
30
32
  let className: string = "";
@@ -61,7 +63,7 @@ const TextArea: FunctionComponent<ComponentProps> = (
61
63
  <div className="relative mt-2 mb-1 rounded-md shadow-sm">
62
64
  <textarea
63
65
  autoFocus={props.autoFocus}
64
- placeholder={props.placeholder}
66
+ placeholder={translateString(props.placeholder)}
65
67
  data-testid={props.dataTestId}
66
68
  className={`${className || ""}`}
67
69
  value={text}