@oneuptime/common 10.0.51 → 10.0.53

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 (35) hide show
  1. package/Server/API/TelemetryAPI.ts +81 -0
  2. package/Server/Services/LabelService.ts +14 -1
  3. package/Server/Services/MonitorProbeService.ts +27 -0
  4. package/Server/Services/ProjectService.ts +8 -4
  5. package/Server/Services/TelemetryAttributeService.ts +117 -4
  6. package/Server/Utils/StartServer.ts +6 -7
  7. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +60 -38
  8. package/UI/Components/Dictionary/Dictionary.tsx +13 -1
  9. package/UI/Components/Filters/FiltersForm.tsx +2 -0
  10. package/UI/Components/Filters/JSONFilter.tsx +5 -1
  11. package/UI/Components/Filters/Types/Filter.ts +2 -0
  12. package/UI/Components/ModelTable/BaseModelTable.tsx +10 -6
  13. package/build/dist/Server/API/TelemetryAPI.js +39 -0
  14. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  15. package/build/dist/Server/Services/LabelService.js +10 -1
  16. package/build/dist/Server/Services/LabelService.js.map +1 -1
  17. package/build/dist/Server/Services/MonitorProbeService.js +19 -1
  18. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  19. package/build/dist/Server/Services/ProjectService.js +8 -4
  20. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  21. package/build/dist/Server/Services/TelemetryAttributeService.js +83 -6
  22. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  23. package/build/dist/Server/Utils/StartServer.js +6 -7
  24. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  25. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +31 -30
  26. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  27. package/build/dist/UI/Components/Dictionary/Dictionary.js +9 -1
  28. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  29. package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
  30. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  31. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  32. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  33. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +9 -5
  34. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  35. package/package.json +1 -1
@@ -54,6 +54,14 @@ router.post(
54
54
  },
55
55
  );
56
56
 
57
+ router.post(
58
+ "/telemetry/metrics/get-attribute-values",
59
+ UserMiddleware.getUserMiddleware,
60
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
61
+ return getAttributeValues(req, res, next, TelemetryType.Metric);
62
+ },
63
+ );
64
+
57
65
  router.post(
58
66
  "/telemetry/logs/get-attributes",
59
67
  UserMiddleware.getUserMiddleware,
@@ -103,10 +111,16 @@ const getAttributes: GetAttributesFunction = async (
103
111
  );
104
112
  }
105
113
 
114
+ const metricName: string | undefined =
115
+ req.body["metricName"] && typeof req.body["metricName"] === "string"
116
+ ? (req.body["metricName"] as string)
117
+ : undefined;
118
+
106
119
  const attributes: string[] =
107
120
  await TelemetryAttributeService.fetchAttributes({
108
121
  projectId: databaseProps.tenantId,
109
122
  telemetryType,
123
+ metricName,
110
124
  });
111
125
 
112
126
  return Response.sendJsonObjectResponse(req, res, {
@@ -117,6 +131,73 @@ const getAttributes: GetAttributesFunction = async (
117
131
  }
118
132
  };
119
133
 
134
+ type GetAttributeValuesFunction = (
135
+ req: ExpressRequest,
136
+ res: ExpressResponse,
137
+ next: NextFunction,
138
+ telemetryType: TelemetryType,
139
+ ) => Promise<void>;
140
+
141
+ const getAttributeValues: GetAttributeValuesFunction = async (
142
+ req: ExpressRequest,
143
+ res: ExpressResponse,
144
+ next: NextFunction,
145
+ telemetryType: TelemetryType,
146
+ ) => {
147
+ try {
148
+ const databaseProps: DatabaseCommonInteractionProps =
149
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
150
+
151
+ if (!databaseProps) {
152
+ return Response.sendErrorResponse(
153
+ req,
154
+ res,
155
+ new BadDataException("Invalid User Session"),
156
+ );
157
+ }
158
+
159
+ if (!databaseProps.tenantId) {
160
+ return Response.sendErrorResponse(
161
+ req,
162
+ res,
163
+ new BadDataException("Invalid Project ID"),
164
+ );
165
+ }
166
+
167
+ const attributeKey: string | undefined =
168
+ req.body["attributeKey"] && typeof req.body["attributeKey"] === "string"
169
+ ? (req.body["attributeKey"] as string)
170
+ : undefined;
171
+
172
+ if (!attributeKey) {
173
+ return Response.sendErrorResponse(
174
+ req,
175
+ res,
176
+ new BadDataException("attributeKey is required"),
177
+ );
178
+ }
179
+
180
+ const metricName: string | undefined =
181
+ req.body["metricName"] && typeof req.body["metricName"] === "string"
182
+ ? (req.body["metricName"] as string)
183
+ : undefined;
184
+
185
+ const values: string[] =
186
+ await TelemetryAttributeService.fetchAttributeValues({
187
+ projectId: databaseProps.tenantId,
188
+ telemetryType,
189
+ metricName,
190
+ attributeKey,
191
+ });
192
+
193
+ return Response.sendJsonObjectResponse(req, res, {
194
+ values: values,
195
+ });
196
+ } catch (err: any) {
197
+ next(err);
198
+ }
199
+ };
200
+
120
201
  // --- Log Histogram Endpoint ---
121
202
 
122
203
  router.post(
@@ -3,6 +3,7 @@ import { OnCreate } from "../Types/Database/Hooks";
3
3
  import QueryHelper from "../Types/Database/QueryHelper";
4
4
  import DatabaseService from "./DatabaseService";
5
5
  import BadDataException from "../../Types/Exception/BadDataException";
6
+ import ObjectID from "../../Types/ObjectID";
6
7
  import Model from "../../Models/DatabaseModels/Label";
7
8
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
8
9
  export class Service extends DatabaseService<Model> {
@@ -14,13 +15,25 @@ export class Service extends DatabaseService<Model> {
14
15
  protected override async onBeforeCreate(
15
16
  createBy: CreateBy<Model>,
16
17
  ): Promise<OnCreate<Model>> {
18
+ let projectId: ObjectID | undefined = createBy.props.tenantId;
19
+
20
+ if (createBy.props.isMasterAdmin || createBy.props.isRoot) {
21
+ if (createBy.data.projectId) {
22
+ projectId = createBy.data.projectId;
23
+ }
24
+ }
25
+
26
+ if (!projectId) {
27
+ throw new BadDataException("Project ID is required to create a label.");
28
+ }
29
+
17
30
  let existingProjectWithSameNameCount: number = 0;
18
31
 
19
32
  existingProjectWithSameNameCount = (
20
33
  await this.countBy({
21
34
  query: {
22
35
  name: QueryHelper.findWithSameText(createBy.data.name!),
23
- projectId: createBy.props.tenantId!,
36
+ projectId: projectId,
24
37
  },
25
38
  props: {
26
39
  isRoot: true,
@@ -5,9 +5,11 @@ import DatabaseService, { EntityManager } from "./DatabaseService";
5
5
  import OneUptimeDate from "../../Types/Date";
6
6
  import BadDataException from "../../Types/Exception/BadDataException";
7
7
  import MonitorProbe from "../../Models/DatabaseModels/MonitorProbe";
8
+ import Monitor from "../../Models/DatabaseModels/Monitor";
8
9
  import QueryHelper from "../Types/Database/QueryHelper";
9
10
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
10
11
  import MonitorService from "./MonitorService";
12
+ import { MonitorTypeHelper } from "../../Types/Monitor/MonitorType";
11
13
  import CronTab from "../Utils/CronTab";
12
14
  import logger from "../Utils/Logger";
13
15
 
@@ -189,6 +191,31 @@ export class Service extends DatabaseService<MonitorProbe> {
189
191
  }
190
192
  }
191
193
 
194
+ // Check if the monitor type supports probes
195
+ const monitorId: ObjectID | undefined | null =
196
+ createBy.data.monitorId || createBy.data.monitor?.id;
197
+
198
+ if (monitorId) {
199
+ const monitor: Monitor | null = await MonitorService.findOneById({
200
+ id: monitorId,
201
+ select: {
202
+ monitorType: true,
203
+ },
204
+ props: {
205
+ isRoot: true,
206
+ },
207
+ });
208
+
209
+ if (
210
+ monitor?.monitorType &&
211
+ !MonitorTypeHelper.isProbableMonitor(monitor.monitorType)
212
+ ) {
213
+ throw new BadDataException(
214
+ "Probes cannot be added to this monitor type.",
215
+ );
216
+ }
217
+ }
218
+
192
219
  if (!createBy.data.nextPingAt) {
193
220
  createBy.data.nextPingAt = OneUptimeDate.getCurrentDate();
194
221
  }
@@ -1294,11 +1294,15 @@ export class ProjectService extends DatabaseService<Model> {
1294
1294
  protected override async onBeforeFind(
1295
1295
  findBy: FindBy<Model>,
1296
1296
  ): Promise<OnFind<Model>> {
1297
- // if user has no project id, then he should not be able to access any project.
1297
+ /*
1298
+ * if user has no project id, then he should not be able to access any project.
1299
+ * Master admins should be able to access all projects.
1300
+ */
1298
1301
  if (
1299
- (!findBy.props.isRoot &&
1300
- !findBy.props.userGlobalAccessPermission?.projectIds) ||
1301
- findBy.props.userGlobalAccessPermission?.projectIds.length === 0
1302
+ !findBy.props.isRoot &&
1303
+ !findBy.props.isMasterAdmin &&
1304
+ (!findBy.props.userGlobalAccessPermission?.projectIds ||
1305
+ findBy.props.userGlobalAccessPermission?.projectIds.length === 0)
1302
1306
  ) {
1303
1307
  findBy.props.isRoot = true;
1304
1308
  findBy.query._id = ObjectID.getZeroObjectID().toString(); // should not get any projects.
@@ -71,6 +71,7 @@ export class TelemetryAttributeService {
71
71
  public async fetchAttributes(data: {
72
72
  projectId: ObjectID;
73
73
  telemetryType: TelemetryType;
74
+ metricName?: string | undefined;
74
75
  }): Promise<string[]> {
75
76
  const source: TelemetrySource | null = this.getTelemetrySource(
76
77
  data.telemetryType,
@@ -83,6 +84,7 @@ export class TelemetryAttributeService {
83
84
  const cacheKey: string = TelemetryAttributeService.getCacheKey(
84
85
  data.projectId,
85
86
  data.telemetryType,
87
+ data.metricName,
86
88
  );
87
89
 
88
90
  const cachedEntry: TelemetryAttributesCacheEntry | null =
@@ -98,6 +100,7 @@ export class TelemetryAttributeService {
98
100
  attributes = await TelemetryAttributeService.fetchAttributesFromDatabase({
99
101
  projectId: data.projectId,
100
102
  source,
103
+ metricName: data.metricName,
101
104
  });
102
105
  } catch (error) {
103
106
  if (cachedEntry) {
@@ -122,8 +125,13 @@ export class TelemetryAttributeService {
122
125
  private static getCacheKey(
123
126
  projectId: ObjectID,
124
127
  telemetryType: TelemetryType,
128
+ metricName?: string | undefined,
125
129
  ): string {
126
- return `${projectId.toString()}:${telemetryType}`;
130
+ const base: string = `${projectId.toString()}:${telemetryType}`;
131
+ if (metricName) {
132
+ return `${base}:${metricName}`;
133
+ }
134
+ return base;
127
135
  }
128
136
 
129
137
  private static getLookbackStartDate(): Date {
@@ -219,6 +227,7 @@ export class TelemetryAttributeService {
219
227
  attributesColumn: string;
220
228
  attributeKeysColumn: string;
221
229
  timeColumn: string;
230
+ metricName?: string | undefined;
222
231
  }): Statement {
223
232
  const lookbackStartDate: Date =
224
233
  TelemetryAttributeService.getLookbackStartDate();
@@ -244,7 +253,20 @@ export class TelemetryAttributeService {
244
253
  AND ${data.timeColumn} >= ${{
245
254
  type: TableColumnType.Date,
246
255
  value: lookbackStartDate,
247
- }}
256
+ }}`;
257
+
258
+ if (data.metricName) {
259
+ statement.append(
260
+ SQL`
261
+ AND name = ${{
262
+ type: TableColumnType.Text,
263
+ value: data.metricName,
264
+ }}`,
265
+ );
266
+ }
267
+
268
+ statement.append(
269
+ SQL`
248
270
  ORDER BY ${data.timeColumn} DESC
249
271
  LIMIT ${{
250
272
  type: TableColumnType.Number,
@@ -258,8 +280,8 @@ export class TelemetryAttributeService {
258
280
  LIMIT ${{
259
281
  type: TableColumnType.Number,
260
282
  value: TelemetryAttributeService.ATTRIBUTES_LIMIT,
261
- }}
262
- `;
283
+ }}`,
284
+ );
263
285
 
264
286
  return statement;
265
287
  }
@@ -267,6 +289,7 @@ export class TelemetryAttributeService {
267
289
  private static async fetchAttributesFromDatabase(data: {
268
290
  projectId: ObjectID;
269
291
  source: TelemetrySource;
292
+ metricName?: string | undefined;
270
293
  }): Promise<Array<string>> {
271
294
  const statement: Statement =
272
295
  TelemetryAttributeService.buildAttributesStatement({
@@ -275,6 +298,7 @@ export class TelemetryAttributeService {
275
298
  attributesColumn: data.source.attributesColumn,
276
299
  attributeKeysColumn: data.source.attributeKeysColumn,
277
300
  timeColumn: data.source.timeColumn,
301
+ metricName: data.metricName,
278
302
  });
279
303
 
280
304
  const dbResult: Results = await data.source.service.executeQuery(statement);
@@ -295,6 +319,95 @@ export class TelemetryAttributeService {
295
319
 
296
320
  return Array.from(new Set(attributeKeys));
297
321
  }
322
+
323
+ private static readonly ATTRIBUTE_VALUES_LIMIT: number = 100;
324
+
325
+ @CaptureSpan()
326
+ public async fetchAttributeValues(data: {
327
+ projectId: ObjectID;
328
+ telemetryType: TelemetryType;
329
+ metricName?: string | undefined;
330
+ attributeKey: string;
331
+ }): Promise<string[]> {
332
+ const source: TelemetrySource | null = this.getTelemetrySource(
333
+ data.telemetryType,
334
+ );
335
+
336
+ if (!source) {
337
+ return [];
338
+ }
339
+
340
+ return TelemetryAttributeService.fetchAttributeValuesFromDatabase({
341
+ projectId: data.projectId,
342
+ source,
343
+ metricName: data.metricName,
344
+ attributeKey: data.attributeKey,
345
+ });
346
+ }
347
+
348
+ private static async fetchAttributeValuesFromDatabase(data: {
349
+ projectId: ObjectID;
350
+ source: TelemetrySource;
351
+ metricName?: string | undefined;
352
+ attributeKey: string;
353
+ }): Promise<Array<string>> {
354
+ const lookbackStartDate: Date =
355
+ TelemetryAttributeService.getLookbackStartDate();
356
+
357
+ const statement: Statement = SQL`
358
+ SELECT DISTINCT ${data.source.attributesColumn}[${{
359
+ type: TableColumnType.Text,
360
+ value: data.attributeKey,
361
+ }}] AS attributeValue
362
+ FROM ${data.source.tableName}
363
+ WHERE projectId = ${{
364
+ type: TableColumnType.ObjectID,
365
+ value: data.projectId,
366
+ }}
367
+ AND ${data.source.timeColumn} >= ${{
368
+ type: TableColumnType.Date,
369
+ value: lookbackStartDate,
370
+ }}
371
+ AND mapContains(${data.source.attributesColumn}, ${{
372
+ type: TableColumnType.Text,
373
+ value: data.attributeKey,
374
+ }})`;
375
+
376
+ if (data.metricName) {
377
+ statement.append(
378
+ SQL`
379
+ AND name = ${{
380
+ type: TableColumnType.Text,
381
+ value: data.metricName,
382
+ }}`,
383
+ );
384
+ }
385
+
386
+ statement.append(
387
+ SQL`
388
+ ORDER BY attributeValue ASC
389
+ LIMIT ${{
390
+ type: TableColumnType.Number,
391
+ value: TelemetryAttributeService.ATTRIBUTE_VALUES_LIMIT,
392
+ }}`,
393
+ );
394
+
395
+ const dbResult: Results = await data.source.service.executeQuery(statement);
396
+ const response: DbJSONResponse = await dbResult.json<{
397
+ data?: Array<JSONObject>;
398
+ }>();
399
+
400
+ const rows: Array<JSONObject> = response.data || [];
401
+
402
+ return rows
403
+ .map((row: JSONObject) => {
404
+ const val: unknown = row["attributeValue"];
405
+ return typeof val === "string" ? val.trim() : null;
406
+ })
407
+ .filter((val: string | null): val is string => {
408
+ return Boolean(val);
409
+ });
410
+ }
298
411
  }
299
412
 
300
413
  export default new TelemetryAttributeService();
@@ -266,14 +266,13 @@ const init: InitFunction = async (
266
266
  },
267
267
  );
268
268
 
269
- // Return 404 for missing static assets instead of falling through to SPA catch-all.
270
- // Without this, missing JS/CSS chunks get served as HTML (index.ejs),
271
- // which causes "Failed to fetch dynamically imported module" errors.
269
+ /*
270
+ * Return 404 for missing static assets instead of falling through to SPA catch-all.
271
+ * Without this, missing JS/CSS chunks get served as HTML (index.ejs),
272
+ * which causes "Failed to fetch dynamically imported module" errors.
273
+ */
272
274
  app.get(
273
- [
274
- `/${appName}/dist/*`,
275
- `/${appName}/assets/*`,
276
- ],
275
+ [`/${appName}/dist/*`, `/${appName}/assets/*`],
277
276
  (_req: ExpressRequest, res: ExpressResponse) => {
278
277
  res.status(404).send("Not found");
279
278
  },
@@ -6,7 +6,6 @@ import ConfirmModal, {
6
6
  } from "../Modal/ConfirmModal";
7
7
  import ProgressBar, { ProgressBarSize } from "../ProgressBar/ProgressBar";
8
8
  import ShortcutKey from "../ShortcutKey/ShortcutKey";
9
- import SimpleLogViewer from "../SimpleLogViewer/SimpleLogViewer";
10
9
  import { Green, Red } from "../../../Types/BrandColors";
11
10
  import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
12
11
  import GenericObject from "../../../Types/GenericObject";
@@ -83,8 +82,6 @@ const BulkUpdateForm: <T extends GenericObject>(
83
82
  const [actionInProgress, setActionInProgress] =
84
83
  React.useState<boolean>(false);
85
84
 
86
- const [actionName, setActionName] = React.useState<string>("");
87
-
88
85
  if (props.selectedItems.length === 0) {
89
86
  return <></>;
90
87
  }
@@ -102,44 +99,71 @@ const BulkUpdateForm: <T extends GenericObject>(
102
99
  }
103
100
 
104
101
  if (!actionInProgress && progressInfo) {
102
+ const hasFailures: boolean = progressInfo.failed.length > 0;
103
+ const hasSuccesses: boolean = progressInfo.successItems.length > 0;
104
+
105
105
  return (
106
- <div className="mt-1 mb-1 space-y-1">
107
- <div className="flex">
108
- <Icon
109
- className="h-5 w-5"
110
- icon={IconProp.CheckCircle}
111
- color={Green}
112
- />
113
- <div className="ml-1 font-medium">
114
- {progressInfo.successItems.length} {props.pluralLabel} Succeeded
115
- </div>
116
- </div>
117
- {progressInfo.failed.length > 0 && (
118
- <div>
119
- <div className="flex mt-3">
120
- <Icon className="h-5 w-5" icon={IconProp.Close} color={Red} />
121
- <div className="ml-1 font-medium">
122
- {progressInfo.failed.length} {props.pluralLabel} Failed. Here
123
- is more information:
106
+ <div className="space-y-4">
107
+ {/* Summary counts */}
108
+ <div className="flex flex-col space-y-3">
109
+ {hasSuccesses && (
110
+ <div className="flex items-center rounded-lg bg-green-50 p-3">
111
+ <Icon
112
+ className="h-5 w-5 flex-shrink-0"
113
+ icon={IconProp.CheckCircle}
114
+ color={Green}
115
+ />
116
+ <div className="ml-2 text-sm font-medium text-green-800">
117
+ {progressInfo.successItems.length}{" "}
118
+ {progressInfo.successItems.length === 1
119
+ ? props.singularLabel
120
+ : props.pluralLabel}{" "}
121
+ succeeded
124
122
  </div>
125
123
  </div>
126
- <div>
127
- <SimpleLogViewer>
128
- {progressInfo.failed.map(
129
- (failedItem: BulkActionFailed<T>, i: number) => {
130
- return (
131
- <div className="flex mb-2" key={i}>
132
- {actionName}{" "}
133
- {props.itemToString
134
- ? props.itemToString(failedItem.item)
135
- : ""}{" "}
136
- {"Failed: "}
124
+ )}
125
+ {hasFailures && (
126
+ <div className="flex items-center rounded-lg bg-red-50 p-3">
127
+ <Icon
128
+ className="h-5 w-5 flex-shrink-0"
129
+ icon={IconProp.Close}
130
+ color={Red}
131
+ />
132
+ <div className="ml-2 text-sm font-medium text-red-800">
133
+ {progressInfo.failed.length}{" "}
134
+ {progressInfo.failed.length === 1
135
+ ? props.singularLabel
136
+ : props.pluralLabel}{" "}
137
+ failed
138
+ </div>
139
+ </div>
140
+ )}
141
+ </div>
142
+
143
+ {/* Failure details */}
144
+ {hasFailures && (
145
+ <div className="rounded-lg border border-gray-200 overflow-hidden">
146
+ <div className="max-h-64 overflow-y-auto divide-y divide-gray-200">
147
+ {progressInfo.failed.map(
148
+ (failedItem: BulkActionFailed<T>, i: number) => {
149
+ const itemName: string = props.itemToString
150
+ ? props.itemToString(failedItem.item)
151
+ : "";
152
+
153
+ return (
154
+ <div className="px-4 py-3 text-sm" key={i}>
155
+ {itemName && (
156
+ <div className="font-medium text-gray-900">
157
+ {itemName}
158
+ </div>
159
+ )}
160
+ <div className="text-gray-500 mt-0.5">
137
161
  {failedItem.failedMessage}
138
162
  </div>
139
- );
140
- },
141
- )}
142
- </SimpleLogViewer>
163
+ </div>
164
+ );
165
+ },
166
+ )}
143
167
  </div>
144
168
  </div>
145
169
  )}
@@ -217,7 +241,6 @@ const BulkUpdateForm: <T extends GenericObject>(
217
241
  },
218
242
  onBulkActionStart: () => {
219
243
  setShowProgressInfoModal(true);
220
- setActionName(button.title);
221
244
  setProgressInfo({
222
245
  inProgressItems: props.selectedItems,
223
246
  successItems: [],
@@ -231,7 +254,6 @@ const BulkUpdateForm: <T extends GenericObject>(
231
254
  },
232
255
  onBulkActionEnd: () => {
233
256
  setActionInProgress(false);
234
- setActionName("");
235
257
  },
236
258
  };
237
259
 
@@ -28,6 +28,8 @@ export interface ComponentProps {
28
28
  addButtonSuffix?: string | undefined;
29
29
  valueTypes?: Array<ValueType>; // by default it'll be Text
30
30
  keys?: Array<string> | undefined;
31
+ valueSuggestions?: Record<string, Array<string>> | undefined;
32
+ onKeySelected?: ((key: string) => void) | undefined;
31
33
  }
32
34
 
33
35
  interface Item {
@@ -160,6 +162,11 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
160
162
  newData[index]!.key = value;
161
163
  setData(newData);
162
164
  onDataChange(newData);
165
+
166
+ // If this key matches one of the known keys, notify parent to fetch values
167
+ if (props.onKeySelected && props.keys?.includes(value)) {
168
+ props.onKeySelected(value);
169
+ }
163
170
  }}
164
171
  />
165
172
  </div>
@@ -212,9 +219,14 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
212
219
  />
213
220
  </div>
214
221
  {item.type === ValueType.Text && (
215
- <Input
222
+ <AutocompleteTextInput
216
223
  value={item.value.toString()}
217
224
  placeholder={props.valuePlaceholder}
225
+ suggestions={
226
+ item.key && props.valueSuggestions?.[item.key]
227
+ ? props.valueSuggestions[item.key]
228
+ : undefined
229
+ }
218
230
  onChange={(value: string) => {
219
231
  const newData: Array<Item> = [...data];
220
232
  newData[index]!.value = value;
@@ -128,6 +128,8 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
128
128
  filterData={props.filterData}
129
129
  onFilterChanged={changeFilterData}
130
130
  jsonKeys={filter.jsonKeys}
131
+ jsonValueSuggestions={filter.jsonValueSuggestions}
132
+ onJsonKeySelected={filter.onJsonKeySelected}
131
133
  />
132
134
  </div>
133
135
  );
@@ -12,6 +12,8 @@ export interface ComponentProps<T extends GenericObject> {
12
12
  onFilterChanged?: undefined | ((filterData: FilterData<T>) => void);
13
13
  filterData: FilterData<T>;
14
14
  jsonKeys?: Array<string> | undefined;
15
+ jsonValueSuggestions?: Record<string, Array<string>> | undefined;
16
+ onJsonKeySelected?: ((key: string) => void) | undefined;
15
17
  }
16
18
 
17
19
  type JSONFilterFunction = <T extends GenericObject>(
@@ -28,10 +30,12 @@ const JSONFilter: JSONFilterFunction = <T extends GenericObject>(
28
30
  return (
29
31
  <DictionaryForm
30
32
  keys={props.jsonKeys}
33
+ valueSuggestions={props.jsonValueSuggestions}
34
+ onKeySelected={props.onJsonKeySelected}
31
35
  addButtonSuffix={filter.title}
32
36
  keyPlaceholder={"Key"}
33
37
  valuePlaceholder={"Value"}
34
- valueTypes={[ValueType.Text, ValueType.Number, ValueType.Boolean]}
38
+ valueTypes={[ValueType.Text]}
35
39
  initialValue={(filterData[filter.key] as Dictionary<string>) || {}}
36
40
  onChange={(value: Dictionary<string | number | boolean>) => {
37
41
  // if no keys in the dictionary, remove the filter
@@ -8,5 +8,7 @@ export default interface Filter<T extends GenericObject> {
8
8
  key: keyof T;
9
9
  type: FieldType;
10
10
  jsonKeys?: Array<string> | undefined;
11
+ jsonValueSuggestions?: Record<string, Array<string>> | undefined;
12
+ onJsonKeySelected?: ((key: string) => void) | undefined;
11
13
  isAdvancedFilter?: boolean | undefined;
12
14
  }
@@ -1689,12 +1689,16 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
1689
1689
  }}
1690
1690
  matchBulkSelectedItemByField={matchBulkSelectedItemByField || "_id"}
1691
1691
  bulkItemToString={(item: TBaseModel) => {
1692
- return (
1693
- (props.singularName || item.singularName || "") +
1694
- " " +
1695
- item[matchBulkSelectedItemByField]?.toString() +
1696
- " " || ""
1697
- );
1692
+ const label: string = props.singularName || item.singularName || "";
1693
+ const name: string =
1694
+ (item as unknown as Record<string, unknown>)["name"]?.toString() ||
1695
+ "";
1696
+ if (name) {
1697
+ return label ? `${label}: ${name}` : name;
1698
+ }
1699
+ const id: string =
1700
+ item[matchBulkSelectedItemByField]?.toString() || "";
1701
+ return label ? `${label} ${id}` : id;
1698
1702
  }}
1699
1703
  filters={classicTableFilters}
1700
1704
  filterError={tableFilterError}