@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.
- 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/MetricAPI.ts +149 -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/AnalyticsDatabaseService.ts +21 -0
- package/Server/Services/MetricService.ts +193 -1
- 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/Dashboard/DashboardComponentType.ts +3 -0
- package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
- package/Types/Date.ts +9 -4
- package/Types/JSONFunctions.ts +61 -1
- 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/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
- package/Utils/Dashboard/Components/Index.ts +21 -0
- 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/MetricAPI.js +123 -0
- package/build/dist/Server/API/MetricAPI.js.map +1 -0
- 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/AnalyticsDatabaseService.js +18 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +151 -1
- package/build/dist/Server/Services/MetricService.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/Dashboard/DashboardComponentType.js +3 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Types/Date.js +7 -2
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +47 -1
- package/build/dist/Types/JSONFunctions.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/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- 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<
|
|
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 &&
|