@oneuptime/common 10.0.85 → 10.0.88
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/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
- package/Models/DatabaseModels/GlobalConfig.ts +51 -0
- package/Server/API/EnterpriseLicenseAPI.ts +83 -0
- package/Server/API/GlobalConfigAPI.ts +59 -0
- package/Server/API/TelemetryAPI.ts +24 -0
- package/Server/EnvironmentConfig.ts +10 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Queue.ts +4 -4
- package/Server/Services/TelemetryAttributeService.ts +37 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
- package/Tests/Types/Date.test.ts +46 -0
- package/Types/Date.ts +9 -4
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
- package/UI/Components/Dictionary/Dictionary.tsx +188 -26
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
- package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
- package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
- package/UI/Components/Filters/FilterViewer.tsx +81 -16
- package/UI/Components/Filters/FiltersForm.tsx +18 -3
- package/UI/Components/Filters/JSONFilter.tsx +11 -2
- package/UI/Components/Filters/Types/Filter.ts +3 -0
- package/UI/Components/Forms/Fields/FormField.tsx +6 -1
- package/UI/Components/Forms/Types/Field.ts +5 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
- package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +9 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +3 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +3 -3
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Tests/Types/Date.test.js +40 -0
- package/build/dist/Tests/Types/Date.test.js.map +1 -1
- package/build/dist/Types/Date.js +7 -2
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
- 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/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import DictionaryForm, { ValueType } from "./Dictionary";
|
|
2
|
+
import { DictionaryEntryValue } from "./DictionaryFilterOperator";
|
|
2
3
|
import Dictionary from "../../../Types/Dictionary";
|
|
3
4
|
import React, { FunctionComponent, ReactElement } from "react";
|
|
4
5
|
|
|
@@ -18,15 +19,19 @@ const DictionaryOfStrings: FunctionComponent<ComponentProps> = (
|
|
|
18
19
|
<DictionaryForm
|
|
19
20
|
{...props}
|
|
20
21
|
valueTypes={[ValueType.Text]}
|
|
21
|
-
onChange={(value: Dictionary<
|
|
22
|
-
|
|
22
|
+
onChange={(value: Dictionary<DictionaryEntryValue>) => {
|
|
23
|
+
/*
|
|
24
|
+
* Operators are not enabled here, so values come back as bare
|
|
25
|
+
* strings/numbers/booleans only.
|
|
26
|
+
*/
|
|
27
|
+
const stringDict: Dictionary<string> = {};
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
stringDict[key] = stringDict[key]?.toString() || "";
|
|
29
|
+
for (const key of Object.keys(value)) {
|
|
30
|
+
const entry: DictionaryEntryValue | undefined = value[key];
|
|
31
|
+
if (entry === undefined || entry === null) {
|
|
32
|
+
continue;
|
|
29
33
|
}
|
|
34
|
+
stringDict[key] = entry.toString();
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
if (props.onChange) {
|
|
@@ -98,6 +98,23 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
if (typeof payload["userLimit"] === "number") {
|
|
102
|
+
configModel.enterpriseLicenseUserLimit = payload[
|
|
103
|
+
"userLimit"
|
|
104
|
+
] as number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof payload["currentUserCount"] === "number") {
|
|
108
|
+
configModel.enterpriseLicenseCurrentUserCount = payload[
|
|
109
|
+
"currentUserCount"
|
|
110
|
+
] as number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (payload["userCountUpdatedAt"]) {
|
|
114
|
+
configModel.enterpriseLicenseUserCountUpdatedAt =
|
|
115
|
+
OneUptimeDate.fromString(payload["userCountUpdatedAt"] as string);
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
setGlobalConfig(configModel);
|
|
102
119
|
|
|
103
120
|
if (!licenseInputEditedRef.current) {
|
|
@@ -153,6 +170,65 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
153
170
|
return expiresAt.toLocaleString();
|
|
154
171
|
}, [globalConfig?.enterpriseLicenseExpiresAt]);
|
|
155
172
|
|
|
173
|
+
const userLimit: number | null = useMemo(() => {
|
|
174
|
+
return typeof globalConfig?.enterpriseLicenseUserLimit === "number"
|
|
175
|
+
? globalConfig.enterpriseLicenseUserLimit
|
|
176
|
+
: null;
|
|
177
|
+
}, [globalConfig?.enterpriseLicenseUserLimit]);
|
|
178
|
+
|
|
179
|
+
const currentUserCount: number | null = useMemo(() => {
|
|
180
|
+
return typeof globalConfig?.enterpriseLicenseCurrentUserCount === "number"
|
|
181
|
+
? globalConfig.enterpriseLicenseCurrentUserCount
|
|
182
|
+
: null;
|
|
183
|
+
}, [globalConfig?.enterpriseLicenseCurrentUserCount]);
|
|
184
|
+
|
|
185
|
+
const userCountUpdatedAtText: string | null = useMemo(() => {
|
|
186
|
+
if (!globalConfig?.enterpriseLicenseUserCountUpdatedAt) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const reportedAt: Date = OneUptimeDate.fromString(
|
|
191
|
+
globalConfig.enterpriseLicenseUserCountUpdatedAt,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (Number.isNaN(reportedAt.getTime())) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return reportedAt.toLocaleString();
|
|
199
|
+
}, [globalConfig?.enterpriseLicenseUserCountUpdatedAt]);
|
|
200
|
+
|
|
201
|
+
const isUserLimitBreached: boolean = useMemo(() => {
|
|
202
|
+
if (!licenseValid) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof userLimit !== "number" || userLimit <= 0) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (typeof currentUserCount !== "number") {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return currentUserCount > userLimit;
|
|
215
|
+
}, [licenseValid, userLimit, currentUserCount]);
|
|
216
|
+
|
|
217
|
+
const userUsagePercent: number | null = useMemo(() => {
|
|
218
|
+
if (typeof userLimit !== "number" || userLimit <= 0) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof currentUserCount !== "number") {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return Math.min(
|
|
227
|
+
100,
|
|
228
|
+
Math.max(0, Math.round((currentUserCount / userLimit) * 100)),
|
|
229
|
+
);
|
|
230
|
+
}, [userLimit, currentUserCount]);
|
|
231
|
+
|
|
156
232
|
const editionName: string = useMemo(() => {
|
|
157
233
|
if (!IS_ENTERPRISE_EDITION) {
|
|
158
234
|
return "Community Edition";
|
|
@@ -176,8 +252,16 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
176
252
|
return "bg-yellow-400";
|
|
177
253
|
}
|
|
178
254
|
|
|
179
|
-
|
|
180
|
-
|
|
255
|
+
if (!licenseValid) {
|
|
256
|
+
return "bg-red-500";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isUserLimitBreached) {
|
|
260
|
+
return "bg-red-500";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return "bg-emerald-500";
|
|
264
|
+
}, [isConfigLoading, licenseValid, isUserLimitBreached]);
|
|
181
265
|
|
|
182
266
|
const ctaLabel: string = useMemo(() => {
|
|
183
267
|
if (!IS_ENTERPRISE_EDITION) {
|
|
@@ -188,8 +272,16 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
188
272
|
return "Checking";
|
|
189
273
|
}
|
|
190
274
|
|
|
191
|
-
|
|
192
|
-
|
|
275
|
+
if (!licenseValid) {
|
|
276
|
+
return "Validate license";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isUserLimitBreached) {
|
|
280
|
+
return "User limit exceeded";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return "View details";
|
|
284
|
+
}, [isConfigLoading, licenseValid, isUserLimitBreached]);
|
|
193
285
|
|
|
194
286
|
const communityFeatures: Array<string> = useMemo(() => {
|
|
195
287
|
return [
|
|
@@ -330,23 +422,40 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
330
422
|
: undefined
|
|
331
423
|
: false;
|
|
332
424
|
|
|
425
|
+
const showAlertedPill: boolean =
|
|
426
|
+
IS_ENTERPRISE_EDITION &&
|
|
427
|
+
!isConfigLoading &&
|
|
428
|
+
(isUserLimitBreached ||
|
|
429
|
+
(!licenseValid && Boolean(globalConfig?.enterpriseLicenseKey)));
|
|
430
|
+
|
|
431
|
+
const pillClassName: string = showAlertedPill
|
|
432
|
+
? "group inline-flex items-center gap-2 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-medium text-red-700 shadow-sm transition hover:border-red-300 hover:bg-red-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
|
|
433
|
+
: "group inline-flex items-center gap-2 rounded-full border border-indigo-100 bg-white px-3 py-1 text-xs font-medium text-indigo-700 shadow-sm transition hover:border-indigo-300 hover:bg-indigo-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
|
|
434
|
+
|
|
435
|
+
const pillCtaTextClassName: string = showAlertedPill
|
|
436
|
+
? "text-[11px] text-red-500 group-hover:text-red-600"
|
|
437
|
+
: "text-[11px] text-indigo-500 group-hover:text-indigo-600";
|
|
438
|
+
|
|
333
439
|
return (
|
|
334
440
|
<>
|
|
335
441
|
<button
|
|
336
442
|
type="button"
|
|
337
443
|
onClick={openDialog}
|
|
338
|
-
className={
|
|
339
|
-
props.className ? props.className : ""
|
|
340
|
-
}`}
|
|
444
|
+
className={`${pillClassName} ${props.className ? props.className : ""}`}
|
|
341
445
|
aria-label={`${editionName} details`}
|
|
342
446
|
>
|
|
447
|
+
{showAlertedPill && (
|
|
448
|
+
<Icon
|
|
449
|
+
icon={IconProp.Alert}
|
|
450
|
+
size={SizeProp.Small}
|
|
451
|
+
className="text-red-600"
|
|
452
|
+
/>
|
|
453
|
+
)}
|
|
343
454
|
<span
|
|
344
455
|
className={`h-2 w-2 rounded-full transition group-hover:scale-110 ${indicatorColor}`}
|
|
345
456
|
></span>
|
|
346
457
|
<span className="tracking-wide">{editionName}</span>
|
|
347
|
-
<span className=
|
|
348
|
-
{ctaLabel}
|
|
349
|
-
</span>
|
|
458
|
+
<span className={pillCtaTextClassName}>{ctaLabel}</span>
|
|
350
459
|
</button>
|
|
351
460
|
|
|
352
461
|
{isDialogOpen && (
|
|
@@ -397,6 +506,111 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
|
|
|
397
506
|
</div>
|
|
398
507
|
)}
|
|
399
508
|
|
|
509
|
+
{!configError && !isConfigLoading && licenseValid && (
|
|
510
|
+
<div
|
|
511
|
+
className={`rounded-lg border p-4 shadow-sm ${
|
|
512
|
+
isUserLimitBreached
|
|
513
|
+
? "border-red-200 bg-red-50"
|
|
514
|
+
: "border-gray-200 bg-white"
|
|
515
|
+
}`}
|
|
516
|
+
>
|
|
517
|
+
<div className="flex items-start gap-3">
|
|
518
|
+
<div
|
|
519
|
+
className={`flex h-9 w-9 items-center justify-center rounded-full ${
|
|
520
|
+
isUserLimitBreached
|
|
521
|
+
? "bg-red-100 text-red-600"
|
|
522
|
+
: "bg-indigo-100 text-indigo-600"
|
|
523
|
+
}`}
|
|
524
|
+
>
|
|
525
|
+
<Icon
|
|
526
|
+
icon={
|
|
527
|
+
isUserLimitBreached ? IconProp.Alert : IconProp.User
|
|
528
|
+
}
|
|
529
|
+
size={SizeProp.Regular}
|
|
530
|
+
/>
|
|
531
|
+
</div>
|
|
532
|
+
<div className="flex-1">
|
|
533
|
+
<div className="flex items-center justify-between">
|
|
534
|
+
<h3
|
|
535
|
+
className={`text-sm font-semibold ${
|
|
536
|
+
isUserLimitBreached
|
|
537
|
+
? "text-red-900"
|
|
538
|
+
: "text-gray-900"
|
|
539
|
+
}`}
|
|
540
|
+
>
|
|
541
|
+
User Usage
|
|
542
|
+
</h3>
|
|
543
|
+
{isUserLimitBreached && (
|
|
544
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">
|
|
545
|
+
Limit exceeded
|
|
546
|
+
</span>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div className="mt-2 flex items-baseline gap-1">
|
|
551
|
+
<span
|
|
552
|
+
className={`text-2xl font-semibold ${
|
|
553
|
+
isUserLimitBreached
|
|
554
|
+
? "text-red-700"
|
|
555
|
+
: "text-gray-900"
|
|
556
|
+
}`}
|
|
557
|
+
>
|
|
558
|
+
{typeof currentUserCount === "number"
|
|
559
|
+
? currentUserCount.toLocaleString()
|
|
560
|
+
: "—"}
|
|
561
|
+
</span>
|
|
562
|
+
<span className="text-sm text-gray-500">
|
|
563
|
+
{" / "}
|
|
564
|
+
{typeof userLimit === "number" && userLimit > 0
|
|
565
|
+
? `${userLimit.toLocaleString()} users`
|
|
566
|
+
: "unlimited"}
|
|
567
|
+
</span>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
{typeof userUsagePercent === "number" && (
|
|
571
|
+
<div className="mt-3">
|
|
572
|
+
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
573
|
+
<div
|
|
574
|
+
className={`h-full rounded-full transition-all ${
|
|
575
|
+
isUserLimitBreached
|
|
576
|
+
? "bg-red-500"
|
|
577
|
+
: userUsagePercent >= 80
|
|
578
|
+
? "bg-amber-500"
|
|
579
|
+
: "bg-emerald-500"
|
|
580
|
+
}`}
|
|
581
|
+
style={{ width: `${userUsagePercent}%` }}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
585
|
+
{userUsagePercent}% of licensed seats in use
|
|
586
|
+
</p>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
{isUserLimitBreached && (
|
|
591
|
+
<p className="mt-3 text-xs text-red-700">
|
|
592
|
+
Your installation has more users than your license
|
|
593
|
+
permits. Please contact{" "}
|
|
594
|
+
<a
|
|
595
|
+
href="mailto:sales@oneuptime.com"
|
|
596
|
+
className="font-medium text-red-800 underline hover:text-red-900"
|
|
597
|
+
>
|
|
598
|
+
sales@oneuptime.com
|
|
599
|
+
</a>{" "}
|
|
600
|
+
to expand your license.
|
|
601
|
+
</p>
|
|
602
|
+
)}
|
|
603
|
+
|
|
604
|
+
<p className="mt-3 text-xs text-gray-500">
|
|
605
|
+
{userCountUpdatedAtText
|
|
606
|
+
? `Last reported to OneUptime on ${userCountUpdatedAtText}.`
|
|
607
|
+
: "User count has not been reported to OneUptime yet. The first report will be sent within 24 hours."}
|
|
608
|
+
</p>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
|
|
400
614
|
{!configError &&
|
|
401
615
|
!isConfigLoading &&
|
|
402
616
|
!licenseValid &&
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DictionaryFilterOperator,
|
|
3
|
+
DictionaryFilterOperatorOption,
|
|
4
|
+
detectOperatorFromValue,
|
|
5
|
+
getOperatorOption,
|
|
6
|
+
} from "../Dictionary/DictionaryFilterOperator";
|
|
1
7
|
import Icon, { SizeProp } from "../Icon/Icon";
|
|
2
8
|
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
3
9
|
import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
|
|
@@ -81,20 +87,50 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
|
|
|
81
87
|
const formatJson: FormatJsonFunction = (
|
|
82
88
|
json: Dictionary<string | number | boolean>,
|
|
83
89
|
): ReactElement => {
|
|
90
|
+
/*
|
|
91
|
+
* Ignore entries with empty key or empty value, and translate
|
|
92
|
+
* operator wrappers into their display symbol so chips like
|
|
93
|
+
* `host.name != prod-1` or `path contains api` render correctly.
|
|
94
|
+
*/
|
|
95
|
+
const isMeaningfulEntry: (key: string, value: unknown) => boolean = (
|
|
96
|
+
key: string,
|
|
97
|
+
value: unknown,
|
|
98
|
+
): boolean => {
|
|
99
|
+
if (key.trim() === "" || value === undefined || value === null) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const detected: { operator: DictionaryFilterOperator; rawValue: string } =
|
|
103
|
+
detectOperatorFromValue(value);
|
|
104
|
+
const option: DictionaryFilterOperatorOption = getOperatorOption(
|
|
105
|
+
detected.operator,
|
|
106
|
+
);
|
|
107
|
+
if (option.hidesValueInput) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return detected.rawValue.trim() !== "";
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const visibleKeys: Array<string> = Object.keys(json).filter(
|
|
114
|
+
(key: string) => {
|
|
115
|
+
return isMeaningfulEntry(key, json[key]);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
84
119
|
return (
|
|
85
120
|
<div className="flex space-x-2 -mt-1">
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
{visibleKeys.map((key: string, i: number) => {
|
|
122
|
+
const rawValue: unknown = json[key];
|
|
123
|
+
const detected: {
|
|
124
|
+
operator: DictionaryFilterOperator;
|
|
125
|
+
rawValue: string;
|
|
126
|
+
} = detectOperatorFromValue(rawValue);
|
|
127
|
+
const option: DictionaryFilterOperatorOption = getOperatorOption(
|
|
128
|
+
detected.operator,
|
|
129
|
+
);
|
|
95
130
|
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
let jsonText: string = detected.rawValue;
|
|
132
|
+
if (typeof rawValue === "boolean") {
|
|
133
|
+
jsonText = rawValue ? "True" : "False";
|
|
98
134
|
}
|
|
99
135
|
|
|
100
136
|
return (
|
|
@@ -102,8 +138,14 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
|
|
|
102
138
|
key={i}
|
|
103
139
|
className="rounded-full h-7 bg-gray-100 text-gray-500 border-2 border-gray-200 p-1 pr-2 pl-2 text-xs"
|
|
104
140
|
>
|
|
105
|
-
<span className="font-medium">{key}</span>
|
|
106
|
-
<span
|
|
141
|
+
<span className="font-medium">{key}</span>{" "}
|
|
142
|
+
<span>{option.symbol}</span>
|
|
143
|
+
{!option.hidesValueInput && (
|
|
144
|
+
<>
|
|
145
|
+
{" "}
|
|
146
|
+
<span className="font-medium">{jsonText}</span>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
107
149
|
</div>
|
|
108
150
|
);
|
|
109
151
|
})}
|
|
@@ -334,13 +376,36 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
|
|
|
334
376
|
key
|
|
335
377
|
] as Dictionary<string | number | boolean>;
|
|
336
378
|
|
|
337
|
-
|
|
379
|
+
/*
|
|
380
|
+
* Count only meaningful entries — empty key/value pairs aren't
|
|
381
|
+
* real filters, and value-less operators (IsEmpty/IsNotEmpty) are
|
|
382
|
+
* valid even with no value.
|
|
383
|
+
*/
|
|
384
|
+
const nonEmptyEntryCount: number = Object.keys(json).filter(
|
|
385
|
+
(entryKey: string) => {
|
|
386
|
+
const value: unknown = json[entryKey];
|
|
387
|
+
if (entryKey.trim() === "" || value === undefined || value === null) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const detected: {
|
|
391
|
+
operator: DictionaryFilterOperator;
|
|
392
|
+
rawValue: string;
|
|
393
|
+
} = detectOperatorFromValue(value);
|
|
394
|
+
const option: DictionaryFilterOperatorOption = getOperatorOption(
|
|
395
|
+
detected.operator,
|
|
396
|
+
);
|
|
397
|
+
if (option.hidesValueInput) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
return detected.rawValue.trim() !== "";
|
|
401
|
+
},
|
|
402
|
+
).length;
|
|
338
403
|
|
|
339
|
-
if (
|
|
404
|
+
if (nonEmptyEntryCount === 0) {
|
|
340
405
|
return null;
|
|
341
406
|
}
|
|
342
407
|
|
|
343
|
-
const isPlural: boolean =
|
|
408
|
+
const isPlural: boolean = nonEmptyEntryCount > 1;
|
|
344
409
|
|
|
345
410
|
return (
|
|
346
411
|
<span className="inline-flex items-center space-x-1">
|
|
@@ -111,13 +111,21 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
|
|
|
111
111
|
props.filterData[filter.key] !== undefined &&
|
|
112
112
|
props.filterData[filter.key] !== null;
|
|
113
113
|
|
|
114
|
+
const isMultiRowFilter: boolean = filter.type === FieldType.JSON;
|
|
115
|
+
|
|
114
116
|
return (
|
|
115
117
|
<div
|
|
116
118
|
key={i}
|
|
117
|
-
className=
|
|
119
|
+
className={`grid grid-cols-[140px_1fr_auto] gap-3 ${
|
|
120
|
+
isMultiRowFilter ? "items-start" : "items-center"
|
|
121
|
+
}`}
|
|
118
122
|
>
|
|
119
123
|
{/* Label column */}
|
|
120
|
-
<div
|
|
124
|
+
<div
|
|
125
|
+
className={`flex items-center min-w-0 ${
|
|
126
|
+
isMultiRowFilter ? "pt-7" : ""
|
|
127
|
+
}`}
|
|
128
|
+
>
|
|
121
129
|
<label className="text-sm font-medium text-gray-700 truncate">
|
|
122
130
|
{filter.title}
|
|
123
131
|
</label>
|
|
@@ -169,11 +177,18 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
|
|
|
169
177
|
jsonKeys={filter.jsonKeys}
|
|
170
178
|
jsonValueSuggestions={filter.jsonValueSuggestions}
|
|
171
179
|
onJsonKeySelected={filter.onJsonKeySelected}
|
|
180
|
+
isLoadingJsonKeys={filter.isLoadingJsonKeys}
|
|
181
|
+
loadingJsonValueKeys={filter.loadingJsonValueKeys}
|
|
182
|
+
enableOperators={filter.jsonEnableOperators}
|
|
172
183
|
/>
|
|
173
184
|
</div>
|
|
174
185
|
|
|
175
186
|
{/* Clear column */}
|
|
176
|
-
<div
|
|
187
|
+
<div
|
|
188
|
+
className={`flex items-center ${
|
|
189
|
+
isMultiRowFilter ? "pt-7" : ""
|
|
190
|
+
}`}
|
|
191
|
+
>
|
|
177
192
|
{hasValue && filter.key ? (
|
|
178
193
|
<button
|
|
179
194
|
type="button"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
|
|
2
2
|
import DictionaryForm, { ValueType } from "../Dictionary/Dictionary";
|
|
3
|
+
import { DictionaryEntryValue } from "../Dictionary/DictionaryFilterOperator";
|
|
3
4
|
import FieldType from "../Types/FieldType";
|
|
4
5
|
import Filter from "./Types/Filter";
|
|
5
6
|
import FilterData from "./Types/FilterData";
|
|
@@ -14,6 +15,9 @@ export interface ComponentProps<T extends GenericObject> {
|
|
|
14
15
|
jsonKeys?: Array<string> | undefined;
|
|
15
16
|
jsonValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
16
17
|
onJsonKeySelected?: ((key: string) => void) | undefined;
|
|
18
|
+
isLoadingJsonKeys?: boolean | undefined;
|
|
19
|
+
loadingJsonValueKeys?: Array<string> | undefined;
|
|
20
|
+
enableOperators?: boolean | undefined;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
type JSONFilterFunction = <T extends GenericObject>(
|
|
@@ -32,12 +36,17 @@ const JSONFilter: JSONFilterFunction = <T extends GenericObject>(
|
|
|
32
36
|
keys={props.jsonKeys}
|
|
33
37
|
valueSuggestions={props.jsonValueSuggestions}
|
|
34
38
|
onKeySelected={props.onJsonKeySelected}
|
|
39
|
+
isLoadingKeys={props.isLoadingJsonKeys}
|
|
40
|
+
loadingValueKeys={props.loadingJsonValueKeys}
|
|
41
|
+
enableOperators={props.enableOperators}
|
|
35
42
|
addButtonSuffix={filter.title}
|
|
36
43
|
keyPlaceholder={"Key"}
|
|
37
44
|
valuePlaceholder={"Value"}
|
|
38
45
|
valueTypes={[ValueType.Text]}
|
|
39
|
-
initialValue={
|
|
40
|
-
|
|
46
|
+
initialValue={
|
|
47
|
+
(filterData[filter.key] as Dictionary<DictionaryEntryValue>) || {}
|
|
48
|
+
}
|
|
49
|
+
onChange={(value: Dictionary<DictionaryEntryValue>) => {
|
|
41
50
|
// if no keys in the dictionary, remove the filter
|
|
42
51
|
|
|
43
52
|
if (Object.keys(value).length > 0) {
|
|
@@ -10,5 +10,8 @@ export default interface Filter<T extends GenericObject> {
|
|
|
10
10
|
jsonKeys?: Array<string> | undefined;
|
|
11
11
|
jsonValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
12
12
|
onJsonKeySelected?: ((key: string) => void) | undefined;
|
|
13
|
+
isLoadingJsonKeys?: boolean | undefined;
|
|
14
|
+
loadingJsonValueKeys?: Array<string> | undefined;
|
|
15
|
+
jsonEnableOperators?: boolean | undefined;
|
|
13
16
|
isAdvancedFilter?: boolean | undefined;
|
|
14
17
|
}
|
|
@@ -457,6 +457,11 @@ const FormField: <T extends GenericObject>(
|
|
|
457
457
|
{props.field.fieldType === FormFieldSchemaType.Dictionary && (
|
|
458
458
|
<DictionaryForm
|
|
459
459
|
keys={props.field.jsonKeysForDictionary}
|
|
460
|
+
valueSuggestions={props.field.dictionaryValueSuggestions}
|
|
461
|
+
onKeySelected={props.field.onDictionaryKeySelected}
|
|
462
|
+
isLoadingKeys={props.field.isLoadingDictionaryKeys}
|
|
463
|
+
loadingValueKeys={props.field.loadingDictionaryValueKeys}
|
|
464
|
+
enableOperators={props.field.dictionaryEnableOperators}
|
|
460
465
|
addButtonSuffix={props.field.title}
|
|
461
466
|
keyPlaceholder={"Key"}
|
|
462
467
|
valuePlaceholder={"Value"}
|
|
@@ -467,7 +472,7 @@ const FormField: <T extends GenericObject>(
|
|
|
467
472
|
? (props.currentValues as any)[props.fieldName]
|
|
468
473
|
: props.field.defaultValue || {}
|
|
469
474
|
}
|
|
470
|
-
onChange={(value: Dictionary<
|
|
475
|
+
onChange={(value: Dictionary<any>) => {
|
|
471
476
|
onChange(value);
|
|
472
477
|
props.setFieldValue(props.fieldName, value);
|
|
473
478
|
}}
|
|
@@ -122,6 +122,11 @@ export default interface Field<TEntity> {
|
|
|
122
122
|
|
|
123
123
|
//
|
|
124
124
|
jsonKeysForDictionary?: Array<string> | undefined;
|
|
125
|
+
isLoadingDictionaryKeys?: boolean | undefined;
|
|
126
|
+
dictionaryValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
127
|
+
loadingDictionaryValueKeys?: Array<string> | undefined;
|
|
128
|
+
onDictionaryKeySelected?: ((key: string) => void) | undefined;
|
|
129
|
+
dictionaryEnableOperators?: boolean | undefined;
|
|
125
130
|
|
|
126
131
|
hideOptionalLabel?: boolean | undefined;
|
|
127
132
|
|