@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.
- package/Server/API/TelemetryAPI.ts +81 -0
- package/Server/Services/LabelService.ts +14 -1
- package/Server/Services/MonitorProbeService.ts +27 -0
- package/Server/Services/ProjectService.ts +8 -4
- package/Server/Services/TelemetryAttributeService.ts +117 -4
- package/Server/Utils/StartServer.ts +6 -7
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +60 -38
- package/UI/Components/Dictionary/Dictionary.tsx +13 -1
- package/UI/Components/Filters/FiltersForm.tsx +2 -0
- package/UI/Components/Filters/JSONFilter.tsx +5 -1
- package/UI/Components/Filters/Types/Filter.ts +2 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +10 -6
- package/build/dist/Server/API/TelemetryAPI.js +39 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +10 -1
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MonitorProbeService.js +19 -1
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +8 -4
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +83 -6
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +6 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +31 -30
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +9 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +9 -5
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
findBy.props.userGlobalAccessPermission?.projectIds
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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="
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
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}
|