@oneuptime/common 10.0.86 → 10.0.89

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 (126) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +51 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +83 -0
  4. package/Server/API/GlobalConfigAPI.ts +59 -0
  5. package/Server/API/MetricAPI.ts +149 -0
  6. package/Server/API/TelemetryAPI.ts +24 -0
  7. package/Server/EnvironmentConfig.ts +10 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  10. package/Server/Infrastructure/Queue.ts +4 -4
  11. package/Server/Services/AnalyticsDatabaseService.ts +21 -0
  12. package/Server/Services/MetricService.ts +193 -1
  13. package/Server/Services/TelemetryAttributeService.ts +37 -3
  14. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
  15. package/Tests/Types/Date.test.ts +46 -0
  16. package/Types/Dashboard/DashboardComponentType.ts +3 -0
  17. package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
  18. package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
  19. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
  20. package/Types/Date.ts +9 -4
  21. package/Types/JSONFunctions.ts +61 -1
  22. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
  23. package/UI/Components/Dictionary/Dictionary.tsx +188 -26
  24. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
  25. package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
  26. package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
  27. package/UI/Components/Filters/FilterViewer.tsx +81 -16
  28. package/UI/Components/Filters/FiltersForm.tsx +18 -3
  29. package/UI/Components/Filters/JSONFilter.tsx +11 -2
  30. package/UI/Components/Filters/Types/Filter.ts +3 -0
  31. package/UI/Components/Forms/Fields/FormField.tsx +6 -1
  32. package/UI/Components/Forms/Types/Field.ts +5 -0
  33. package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
  34. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
  35. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
  36. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
  37. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
  39. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
  40. package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
  41. package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
  42. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
  43. package/Utils/Dashboard/Components/Index.ts +21 -0
  44. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
  45. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
  47. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  48. package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
  49. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  50. package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
  51. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  52. package/build/dist/Server/API/MetricAPI.js +123 -0
  53. package/build/dist/Server/API/MetricAPI.js.map +1 -0
  54. package/build/dist/Server/API/TelemetryAPI.js +9 -0
  55. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  56. package/build/dist/Server/EnvironmentConfig.js +3 -0
  57. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  62. package/build/dist/Server/Infrastructure/Queue.js +3 -3
  63. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  64. package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
  65. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  66. package/build/dist/Server/Services/MetricService.js +151 -1
  67. package/build/dist/Server/Services/MetricService.js.map +1 -1
  68. package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
  69. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  70. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
  71. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  72. package/build/dist/Tests/Types/Date.test.js +40 -0
  73. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  74. package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
  75. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  76. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
  77. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
  78. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
  79. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
  80. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
  81. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
  82. package/build/dist/Types/Date.js +7 -2
  83. package/build/dist/Types/Date.js.map +1 -1
  84. package/build/dist/Types/JSONFunctions.js +47 -1
  85. package/build/dist/Types/JSONFunctions.js.map +1 -1
  86. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
  87. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  88. package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
  89. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  90. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
  91. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
  92. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
  93. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
  94. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
  95. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  96. package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
  97. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  98. package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
  99. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  100. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  101. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  102. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  103. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  104. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
  105. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  106. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
  107. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  108. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
  109. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
  110. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
  111. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  112. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  113. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
  118. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
  119. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
  120. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
  121. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
  122. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
  123. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
  124. package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
  125. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  126. package/package.json +1 -1
@@ -0,0 +1,357 @@
1
+ import EqualTo from "../../../Types/BaseDatabase/EqualTo";
2
+ import NotEqual from "../../../Types/BaseDatabase/NotEqual";
3
+ import Search from "../../../Types/BaseDatabase/Search";
4
+ import NotContains from "../../../Types/BaseDatabase/NotContains";
5
+ import StartsWith from "../../../Types/BaseDatabase/StartsWith";
6
+ import EndsWith from "../../../Types/BaseDatabase/EndsWith";
7
+ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
8
+ import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
9
+ import LessThan from "../../../Types/BaseDatabase/LessThan";
10
+ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
11
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
12
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
13
+ import { ObjectType } from "../../../Types/JSON";
14
+
15
+ /*
16
+ * UI-facing operator identifiers. We store these in the Dictionary form
17
+ * state and translate to the corresponding backend operator wrapper at
18
+ * the API boundary so the existing serialization pipeline works.
19
+ */
20
+ export enum DictionaryFilterOperator {
21
+ EqualTo = "EqualTo",
22
+ NotEqual = "NotEqual",
23
+ Contains = "Contains",
24
+ NotContains = "NotContains",
25
+ StartsWith = "StartsWith",
26
+ EndsWith = "EndsWith",
27
+ GreaterThan = "GreaterThan",
28
+ LessThan = "LessThan",
29
+ GreaterThanOrEqual = "GreaterThanOrEqual",
30
+ LessThanOrEqual = "LessThanOrEqual",
31
+ IsEmpty = "IsEmpty",
32
+ IsNotEmpty = "IsNotEmpty",
33
+ }
34
+
35
+ export interface DictionaryFilterOperatorOption {
36
+ operator: DictionaryFilterOperator;
37
+ label: string;
38
+ symbol: string;
39
+ // Operators like IsEmpty/IsNotEmpty don't take a user-supplied value.
40
+ hidesValueInput?: boolean | undefined;
41
+ // Numeric operators force a numeric value input.
42
+ expectsNumericValue?: boolean | undefined;
43
+ }
44
+
45
+ export const DICTIONARY_FILTER_OPERATOR_OPTIONS: ReadonlyArray<DictionaryFilterOperatorOption> =
46
+ [
47
+ {
48
+ operator: DictionaryFilterOperator.EqualTo,
49
+ label: "equals",
50
+ symbol: "=",
51
+ },
52
+ {
53
+ operator: DictionaryFilterOperator.NotEqual,
54
+ label: "does not equal",
55
+ symbol: "!=",
56
+ },
57
+ {
58
+ operator: DictionaryFilterOperator.Contains,
59
+ label: "contains",
60
+ symbol: "contains",
61
+ },
62
+ {
63
+ operator: DictionaryFilterOperator.NotContains,
64
+ label: "does not contain",
65
+ symbol: "does not contain",
66
+ },
67
+ {
68
+ operator: DictionaryFilterOperator.StartsWith,
69
+ label: "starts with",
70
+ symbol: "starts with",
71
+ },
72
+ {
73
+ operator: DictionaryFilterOperator.EndsWith,
74
+ label: "ends with",
75
+ symbol: "ends with",
76
+ },
77
+ {
78
+ operator: DictionaryFilterOperator.GreaterThan,
79
+ label: "greater than",
80
+ symbol: ">",
81
+ expectsNumericValue: true,
82
+ },
83
+ {
84
+ operator: DictionaryFilterOperator.GreaterThanOrEqual,
85
+ label: "greater than or equal",
86
+ symbol: ">=",
87
+ expectsNumericValue: true,
88
+ },
89
+ {
90
+ operator: DictionaryFilterOperator.LessThan,
91
+ label: "less than",
92
+ symbol: "<",
93
+ expectsNumericValue: true,
94
+ },
95
+ {
96
+ operator: DictionaryFilterOperator.LessThanOrEqual,
97
+ label: "less than or equal",
98
+ symbol: "<=",
99
+ expectsNumericValue: true,
100
+ },
101
+ {
102
+ operator: DictionaryFilterOperator.IsEmpty,
103
+ label: "is empty",
104
+ symbol: "is empty",
105
+ hidesValueInput: true,
106
+ },
107
+ {
108
+ operator: DictionaryFilterOperator.IsNotEmpty,
109
+ label: "is not empty",
110
+ symbol: "is not empty",
111
+ hidesValueInput: true,
112
+ },
113
+ ];
114
+
115
+ export type DictionaryEntryValue =
116
+ | string
117
+ | number
118
+ | boolean
119
+ | EqualTo<string>
120
+ | NotEqual<string>
121
+ | Search<string>
122
+ | NotContains<string>
123
+ | StartsWith<string>
124
+ | EndsWith<string>
125
+ | GreaterThan<number>
126
+ | GreaterThanOrEqual<number>
127
+ | LessThan<number>
128
+ | LessThanOrEqual<number>
129
+ | IsNull
130
+ | NotNull;
131
+
132
+ export const getOperatorOption: (
133
+ operator: DictionaryFilterOperator,
134
+ ) => DictionaryFilterOperatorOption = (
135
+ operator: DictionaryFilterOperator,
136
+ ): DictionaryFilterOperatorOption => {
137
+ return (
138
+ DICTIONARY_FILTER_OPERATOR_OPTIONS.find(
139
+ (option: DictionaryFilterOperatorOption) => {
140
+ return option.operator === operator;
141
+ },
142
+ ) ?? DICTIONARY_FILTER_OPERATOR_OPTIONS[0]!
143
+ );
144
+ };
145
+
146
+ /*
147
+ * Detect operator wrapper instances or `_type`-tagged plain objects
148
+ * (already-deserialized vs raw-from-storage).
149
+ */
150
+ type ObjectTypeLike = { _type?: string };
151
+
152
+ const matchesObjectType: (value: unknown, type: ObjectType) => boolean = (
153
+ value: unknown,
154
+ type: ObjectType,
155
+ ): boolean => {
156
+ return Boolean(
157
+ value &&
158
+ typeof value === "object" &&
159
+ (value as ObjectTypeLike)._type === type,
160
+ );
161
+ };
162
+
163
+ interface RawValueAndOperator {
164
+ operator: DictionaryFilterOperator;
165
+ rawValue: string;
166
+ }
167
+
168
+ /**
169
+ * Inspect a stored dictionary entry value (which may be a plain string,
170
+ * a hydrated operator instance, or a raw `{_type, value}` JSON shape)
171
+ * and recover the operator + display value used to populate the form.
172
+ */
173
+ export const detectOperatorFromValue: (
174
+ value: unknown,
175
+ ) => RawValueAndOperator = (value: unknown): RawValueAndOperator => {
176
+ if (value === null || value === undefined) {
177
+ return {
178
+ operator: DictionaryFilterOperator.EqualTo,
179
+ rawValue: "",
180
+ };
181
+ }
182
+
183
+ if (
184
+ typeof value === "string" ||
185
+ typeof value === "number" ||
186
+ typeof value === "boolean"
187
+ ) {
188
+ return {
189
+ operator: DictionaryFilterOperator.EqualTo,
190
+ rawValue: String(value),
191
+ };
192
+ }
193
+
194
+ if (value instanceof IsNull || matchesObjectType(value, ObjectType.IsNull)) {
195
+ return { operator: DictionaryFilterOperator.IsEmpty, rawValue: "" };
196
+ }
197
+ if (
198
+ value instanceof NotNull ||
199
+ matchesObjectType(value, ObjectType.NotNull)
200
+ ) {
201
+ return { operator: DictionaryFilterOperator.IsNotEmpty, rawValue: "" };
202
+ }
203
+
204
+ const wrapperValue: string =
205
+ value instanceof Object && "value" in value
206
+ ? String(
207
+ (value as { value?: unknown }).value === undefined ||
208
+ (value as { value?: unknown }).value === null
209
+ ? ""
210
+ : (value as { value?: unknown }).value,
211
+ )
212
+ : "";
213
+
214
+ if (
215
+ value instanceof NotEqual ||
216
+ matchesObjectType(value, ObjectType.NotEqual)
217
+ ) {
218
+ return {
219
+ operator: DictionaryFilterOperator.NotEqual,
220
+ rawValue: wrapperValue,
221
+ };
222
+ }
223
+ if (
224
+ value instanceof EqualTo ||
225
+ matchesObjectType(value, ObjectType.EqualTo)
226
+ ) {
227
+ return {
228
+ operator: DictionaryFilterOperator.EqualTo,
229
+ rawValue: wrapperValue,
230
+ };
231
+ }
232
+ if (value instanceof Search || matchesObjectType(value, ObjectType.Search)) {
233
+ return {
234
+ operator: DictionaryFilterOperator.Contains,
235
+ rawValue: wrapperValue,
236
+ };
237
+ }
238
+ if (
239
+ value instanceof NotContains ||
240
+ matchesObjectType(value, ObjectType.NotContains)
241
+ ) {
242
+ return {
243
+ operator: DictionaryFilterOperator.NotContains,
244
+ rawValue: wrapperValue,
245
+ };
246
+ }
247
+ if (
248
+ value instanceof StartsWith ||
249
+ matchesObjectType(value, ObjectType.StartsWith)
250
+ ) {
251
+ return {
252
+ operator: DictionaryFilterOperator.StartsWith,
253
+ rawValue: wrapperValue,
254
+ };
255
+ }
256
+ if (
257
+ value instanceof EndsWith ||
258
+ matchesObjectType(value, ObjectType.EndsWith)
259
+ ) {
260
+ return {
261
+ operator: DictionaryFilterOperator.EndsWith,
262
+ rawValue: wrapperValue,
263
+ };
264
+ }
265
+ if (
266
+ value instanceof GreaterThan ||
267
+ matchesObjectType(value, ObjectType.GreaterThan)
268
+ ) {
269
+ return {
270
+ operator: DictionaryFilterOperator.GreaterThan,
271
+ rawValue: wrapperValue,
272
+ };
273
+ }
274
+ if (
275
+ value instanceof GreaterThanOrEqual ||
276
+ matchesObjectType(value, ObjectType.GreaterThanOrEqual)
277
+ ) {
278
+ return {
279
+ operator: DictionaryFilterOperator.GreaterThanOrEqual,
280
+ rawValue: wrapperValue,
281
+ };
282
+ }
283
+ if (
284
+ value instanceof LessThan ||
285
+ matchesObjectType(value, ObjectType.LessThan)
286
+ ) {
287
+ return {
288
+ operator: DictionaryFilterOperator.LessThan,
289
+ rawValue: wrapperValue,
290
+ };
291
+ }
292
+ if (
293
+ value instanceof LessThanOrEqual ||
294
+ matchesObjectType(value, ObjectType.LessThanOrEqual)
295
+ ) {
296
+ return {
297
+ operator: DictionaryFilterOperator.LessThanOrEqual,
298
+ rawValue: wrapperValue,
299
+ };
300
+ }
301
+
302
+ // Unknown structure — fall back to bare equality with stringified value.
303
+ return {
304
+ operator: DictionaryFilterOperator.EqualTo,
305
+ rawValue: wrapperValue,
306
+ };
307
+ };
308
+
309
+ /**
310
+ * Build the actual stored dictionary value for an operator + raw value.
311
+ * `EqualTo` produces a bare string for backwards compatibility with
312
+ * existing saved filters; everything else produces an operator wrapper
313
+ * instance.
314
+ */
315
+ export const buildDictionaryValue: (input: {
316
+ operator: DictionaryFilterOperator;
317
+ rawValue: string;
318
+ }) => DictionaryEntryValue = (input: {
319
+ operator: DictionaryFilterOperator;
320
+ rawValue: string;
321
+ }): DictionaryEntryValue => {
322
+ const { operator, rawValue } = input;
323
+ const trimmed: string = rawValue ?? "";
324
+
325
+ switch (operator) {
326
+ case DictionaryFilterOperator.EqualTo:
327
+ return trimmed;
328
+ case DictionaryFilterOperator.NotEqual:
329
+ return new NotEqual<string>(trimmed);
330
+ case DictionaryFilterOperator.Contains:
331
+ /*
332
+ * Statement.serialize already wraps Search instances with `%...%`,
333
+ * so pass the bare needle here.
334
+ */
335
+ return new Search<string>(trimmed);
336
+ case DictionaryFilterOperator.NotContains:
337
+ return new NotContains<string>(trimmed);
338
+ case DictionaryFilterOperator.StartsWith:
339
+ return new StartsWith<string>(trimmed);
340
+ case DictionaryFilterOperator.EndsWith:
341
+ return new EndsWith<string>(trimmed);
342
+ case DictionaryFilterOperator.GreaterThan:
343
+ return new GreaterThan<number>(Number(trimmed));
344
+ case DictionaryFilterOperator.GreaterThanOrEqual:
345
+ return new GreaterThanOrEqual<number>(Number(trimmed));
346
+ case DictionaryFilterOperator.LessThan:
347
+ return new LessThan<number>(Number(trimmed));
348
+ case DictionaryFilterOperator.LessThanOrEqual:
349
+ return new LessThanOrEqual<number>(Number(trimmed));
350
+ case DictionaryFilterOperator.IsEmpty:
351
+ return new IsNull();
352
+ case DictionaryFilterOperator.IsNotEmpty:
353
+ return new NotNull();
354
+ default:
355
+ return trimmed;
356
+ }
357
+ };
@@ -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<string | number | boolean>) => {
22
- const stringDict: Dictionary<string> = value as Dictionary<string>;
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
- // convert all values to strings
25
-
26
- for (const key in stringDict) {
27
- if (stringDict[key]) {
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
- return licenseValid ? "bg-emerald-500" : "bg-red-500";
180
- }, [isConfigLoading, licenseValid]);
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
- return licenseValid ? "View details" : "Validate license";
192
- }, [isConfigLoading, licenseValid]);
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={`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 ${
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="text-[11px] text-indigo-500 group-hover:text-indigo-600">
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 &&