@oneuptime/common 10.0.69 → 10.0.71
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/KubernetesCluster.ts +7 -0
- package/Models/DatabaseModels/Project.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +11 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AIBillingService.ts +2 -2
- package/Server/Services/BillingService.ts +116 -48
- package/Server/Services/DatabaseService.ts +10 -27
- package/Server/Services/KubernetesResourceService.ts +33 -10
- package/Server/Services/NotificationService.ts +2 -2
- package/Server/Types/Database/QueryHelper.ts +127 -0
- package/Server/Types/Database/QueryUtil.ts +250 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +79 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +79 -0
- package/Types/BaseDatabase/EndsWith.ts +41 -0
- package/Types/BaseDatabase/IncludesAll.ts +45 -0
- package/Types/BaseDatabase/IncludesNone.ts +45 -0
- package/Types/BaseDatabase/NotContains.ts +41 -0
- package/Types/BaseDatabase/StartsWith.ts +41 -0
- package/Types/Email.ts +50 -0
- package/Types/JSON.ts +20 -0
- package/Types/SerializableObjectDictionary.ts +10 -0
- package/UI/Components/Filters/BooleanFilter.tsx +1 -0
- package/UI/Components/Filters/DateFilter.tsx +220 -25
- package/UI/Components/Filters/DropdownFilter.tsx +1 -0
- package/UI/Components/Filters/EntityFilter.tsx +229 -41
- package/UI/Components/Filters/FilterViewer.tsx +231 -147
- package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
- package/UI/Components/Filters/FiltersForm.tsx +146 -97
- package/UI/Components/Filters/NumberFilter.tsx +220 -34
- package/UI/Components/Filters/OperatorSelector.tsx +91 -0
- package/UI/Components/Filters/TextFilter.tsx +183 -71
- package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +10 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +9 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +5 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +125 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-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/Services/AIBillingService.js +2 -2
- package/build/dist/Server/Services/AIBillingService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +99 -39
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +9 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +4 -2
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/NotificationService.js +2 -2
- package/build/dist/Server/Services/NotificationService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +110 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +186 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Types/BaseDatabase/EndsWith.js +31 -0
- package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
- package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
- package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
- package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
- package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
- package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
- package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
- package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
- package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
- package/build/dist/Types/Email.js +42 -0
- package/build/dist/Types/Email.js.map +1 -1
- package/build/dist/Types/JSON.js +5 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +10 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
- package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/DateFilter.js +155 -14
- package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
- package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/EntityFilter.js +181 -30
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +188 -98
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
- package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js +164 -23
- package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
- package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
- package/build/dist/UI/Components/Filters/TextFilter.js +131 -53
- package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
- package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +9 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import Button, { ButtonStyleType } from "../Button/Button";
|
|
1
|
+
import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
|
|
2
2
|
import ComponentLoader from "../ComponentLoader/ComponentLoader";
|
|
3
3
|
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
|
4
|
-
import FieldLabelElement from "../Forms/Fields/FieldLabel";
|
|
5
4
|
import FieldType from "../Types/FieldType";
|
|
5
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
6
6
|
import BooleanFilter from "./BooleanFilter";
|
|
7
7
|
import DateFilter from "./DateFilter";
|
|
8
8
|
import DropdownFilter from "./DropdownFilter";
|
|
@@ -61,109 +61,158 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
|
|
|
61
61
|
props.showAdvancedFiltersByDefault ?? false,
|
|
62
62
|
);
|
|
63
63
|
|
|
64
|
+
type ClearFilterFunction = (key: keyof T) => void;
|
|
65
|
+
|
|
66
|
+
const clearFilter: ClearFilterFunction = (key: keyof T): void => {
|
|
67
|
+
const next: FilterData<T> = { ...props.filterData };
|
|
68
|
+
delete next[key];
|
|
69
|
+
changeFilterData(next);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const visibleFilters: Array<Filter<T>> = props.filters.filter(
|
|
73
|
+
(filter: Filter<T>) => {
|
|
74
|
+
if (filter.isAdvancedFilter) {
|
|
75
|
+
return showMoreFilters && !props.isFilterLoading && !props.filterError;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
64
81
|
return (
|
|
65
82
|
<div id={props.id}>
|
|
66
|
-
<div className="
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
props.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
83
|
+
<div className="space-y-3">
|
|
84
|
+
{visibleFilters.map((filter: Filter<T>, i: number) => {
|
|
85
|
+
const hasValue: boolean =
|
|
86
|
+
filter.key !== undefined &&
|
|
87
|
+
props.filterData[filter.key] !== undefined &&
|
|
88
|
+
props.filterData[filter.key] !== null;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
key={i}
|
|
93
|
+
className="grid grid-cols-[140px_1fr_auto] items-center gap-3"
|
|
94
|
+
>
|
|
95
|
+
{/* Label column */}
|
|
96
|
+
<div className="flex items-center min-w-0">
|
|
97
|
+
<label className="text-sm font-medium text-gray-700 truncate">
|
|
98
|
+
{filter.title}
|
|
99
|
+
</label>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Controls column */}
|
|
103
|
+
<div className="min-w-0">
|
|
104
|
+
<DropdownFilter
|
|
105
|
+
filter={filter}
|
|
106
|
+
filterData={props.filterData}
|
|
107
|
+
onFilterChanged={changeFilterData}
|
|
108
|
+
isMultiSelect={filter.type === FieldType.MultiSelectDropdown}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<EntityFilter
|
|
112
|
+
filter={filter}
|
|
113
|
+
filterData={props.filterData}
|
|
114
|
+
onFilterChanged={changeFilterData}
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<BooleanFilter
|
|
118
|
+
filter={filter}
|
|
119
|
+
filterData={props.filterData}
|
|
120
|
+
onFilterChanged={changeFilterData}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<DateFilter
|
|
124
|
+
filter={filter}
|
|
125
|
+
filterData={props.filterData}
|
|
126
|
+
onFilterChanged={changeFilterData}
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
<TextFilter
|
|
130
|
+
filter={filter}
|
|
131
|
+
filterData={props.filterData}
|
|
132
|
+
onFilterChanged={changeFilterData}
|
|
133
|
+
/>
|
|
134
|
+
|
|
135
|
+
<NumberFilter
|
|
136
|
+
filter={filter}
|
|
137
|
+
filterData={props.filterData}
|
|
138
|
+
onFilterChanged={changeFilterData}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<JSONFilter
|
|
142
|
+
filter={filter}
|
|
143
|
+
filterData={props.filterData}
|
|
144
|
+
onFilterChanged={changeFilterData}
|
|
145
|
+
jsonKeys={filter.jsonKeys}
|
|
146
|
+
jsonValueSuggestions={filter.jsonValueSuggestions}
|
|
147
|
+
onJsonKeySelected={filter.onJsonKeySelected}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Clear column */}
|
|
152
|
+
<div className="flex items-center">
|
|
153
|
+
{hasValue && filter.key ? (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
onClick={() => {
|
|
157
|
+
return clearFilter(filter.key as keyof T);
|
|
158
|
+
}}
|
|
159
|
+
className="p-1.5 rounded-md text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors"
|
|
160
|
+
aria-label={`Clear ${filter.title} filter`}
|
|
161
|
+
title={`Clear ${filter.title}`}
|
|
162
|
+
>
|
|
163
|
+
<svg
|
|
164
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
165
|
+
viewBox="0 0 20 20"
|
|
166
|
+
fill="currentColor"
|
|
167
|
+
className="w-4 h-4"
|
|
168
|
+
>
|
|
169
|
+
<path
|
|
170
|
+
fillRule="evenodd"
|
|
171
|
+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
172
|
+
clipRule="evenodd"
|
|
173
|
+
/>
|
|
174
|
+
</svg>
|
|
175
|
+
</button>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="w-7" aria-hidden="true" />
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
{props.showFilter && props.isFilterLoading && !props.filterError && (
|
|
185
|
+
<div className="py-4">
|
|
139
186
|
<ComponentLoader />
|
|
140
|
-
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
141
189
|
|
|
142
|
-
|
|
190
|
+
{props.showFilter && props.filterError && (
|
|
191
|
+
<div className="py-4">
|
|
143
192
|
<ErrorMessage
|
|
144
193
|
message={props.filterError}
|
|
145
194
|
onRefreshClick={props.onFilterRefreshClick}
|
|
146
195
|
/>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
{showAdvancedFilterButton && (
|
|
199
|
+
<Button
|
|
200
|
+
className="-ml-3 mt-3"
|
|
201
|
+
buttonSize={ButtonSize.Small}
|
|
202
|
+
buttonStyle={ButtonStyleType.SECONDARY_LINK}
|
|
203
|
+
icon={showMoreFilters ? IconProp.ChevronUp : IconProp.ChevronDown}
|
|
204
|
+
title={
|
|
205
|
+
showMoreFilters ? "Hide Advanced Filters" : "Show Advanced Filters"
|
|
206
|
+
}
|
|
207
|
+
onClick={() => {
|
|
208
|
+
setShowMoreFilters((currentValue: boolean) => {
|
|
209
|
+
const newValue: boolean = !currentValue;
|
|
210
|
+
props.onAdvancedFiltersToggle?.(newValue);
|
|
211
|
+
return newValue;
|
|
212
|
+
});
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
167
216
|
</div>
|
|
168
217
|
);
|
|
169
218
|
};
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import Input, { InputType } from "../Input/Input";
|
|
2
1
|
import FieldType from "../Types/FieldType";
|
|
3
2
|
import Filter from "./Types/Filter";
|
|
4
3
|
import FilterData from "./Types/FilterData";
|
|
4
|
+
import FilterOperator from "./Types/FilterOperator";
|
|
5
|
+
import OperatorSelector from "./OperatorSelector";
|
|
6
|
+
import EqualTo from "../../../Types/BaseDatabase/EqualTo";
|
|
7
|
+
import NotEqual from "../../../Types/BaseDatabase/NotEqual";
|
|
8
|
+
import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
|
|
9
|
+
import LessThan from "../../../Types/BaseDatabase/LessThan";
|
|
10
|
+
import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
|
|
11
|
+
import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
|
|
12
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
13
|
+
import IsNull from "../../../Types/BaseDatabase/IsNull";
|
|
14
|
+
import NotNull from "../../../Types/BaseDatabase/NotNull";
|
|
5
15
|
import GenericObject from "../../../Types/GenericObject";
|
|
6
|
-
import React, { ReactElement } from "react";
|
|
16
|
+
import React, { ReactElement, useEffect, useState } from "react";
|
|
7
17
|
|
|
8
18
|
export interface ComponentProps<T extends GenericObject> {
|
|
9
19
|
filter: Filter<T>;
|
|
@@ -15,44 +25,220 @@ type NumberFilterFunction = <T extends GenericObject>(
|
|
|
15
25
|
props: ComponentProps<T>,
|
|
16
26
|
) => ReactElement;
|
|
17
27
|
|
|
28
|
+
const NUMBER_OPERATORS: Array<FilterOperator> = [
|
|
29
|
+
FilterOperator.EqualTo,
|
|
30
|
+
FilterOperator.NotEqualTo,
|
|
31
|
+
FilterOperator.GreaterThan,
|
|
32
|
+
FilterOperator.LessThan,
|
|
33
|
+
FilterOperator.GreaterThanOrEqualTo,
|
|
34
|
+
FilterOperator.LessThanOrEqualTo,
|
|
35
|
+
FilterOperator.Between,
|
|
36
|
+
FilterOperator.IsEmpty,
|
|
37
|
+
FilterOperator.IsNotEmpty,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
type NumberState = {
|
|
41
|
+
operator: FilterOperator;
|
|
42
|
+
value: string;
|
|
43
|
+
endValue: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type DetectStateFunction = (rawValue: unknown) => NumberState;
|
|
47
|
+
|
|
48
|
+
const detectState: DetectStateFunction = (rawValue: unknown): NumberState => {
|
|
49
|
+
if (rawValue instanceof InBetween) {
|
|
50
|
+
return {
|
|
51
|
+
operator: FilterOperator.Between,
|
|
52
|
+
value: (rawValue.startValue ?? "").toString(),
|
|
53
|
+
endValue: (rawValue.endValue ?? "").toString(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (rawValue instanceof EqualTo) {
|
|
57
|
+
return {
|
|
58
|
+
operator: FilterOperator.EqualTo,
|
|
59
|
+
value: rawValue.toString(),
|
|
60
|
+
endValue: "",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (rawValue instanceof NotEqual) {
|
|
64
|
+
return {
|
|
65
|
+
operator: FilterOperator.NotEqualTo,
|
|
66
|
+
value: rawValue.toString(),
|
|
67
|
+
endValue: "",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (rawValue instanceof GreaterThan) {
|
|
71
|
+
return {
|
|
72
|
+
operator: FilterOperator.GreaterThan,
|
|
73
|
+
value: rawValue.toString(),
|
|
74
|
+
endValue: "",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (rawValue instanceof LessThan) {
|
|
78
|
+
return {
|
|
79
|
+
operator: FilterOperator.LessThan,
|
|
80
|
+
value: rawValue.toString(),
|
|
81
|
+
endValue: "",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (rawValue instanceof GreaterThanOrEqual) {
|
|
85
|
+
return {
|
|
86
|
+
operator: FilterOperator.GreaterThanOrEqualTo,
|
|
87
|
+
value: rawValue.toString(),
|
|
88
|
+
endValue: "",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (rawValue instanceof LessThanOrEqual) {
|
|
92
|
+
return {
|
|
93
|
+
operator: FilterOperator.LessThanOrEqualTo,
|
|
94
|
+
value: rawValue.toString(),
|
|
95
|
+
endValue: "",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (rawValue instanceof IsNull) {
|
|
99
|
+
return { operator: FilterOperator.IsEmpty, value: "", endValue: "" };
|
|
100
|
+
}
|
|
101
|
+
if (rawValue instanceof NotNull) {
|
|
102
|
+
return { operator: FilterOperator.IsNotEmpty, value: "", endValue: "" };
|
|
103
|
+
}
|
|
104
|
+
if (typeof rawValue === "number") {
|
|
105
|
+
return {
|
|
106
|
+
operator: FilterOperator.EqualTo,
|
|
107
|
+
value: rawValue.toString(),
|
|
108
|
+
endValue: "",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { operator: FilterOperator.EqualTo, value: "", endValue: "" };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type BuildValueFunction = (state: NumberState) => unknown;
|
|
115
|
+
|
|
116
|
+
const buildValue: BuildValueFunction = (state: NumberState): unknown => {
|
|
117
|
+
const startNum: number = parseFloat(state.value);
|
|
118
|
+
const endNum: number = parseFloat(state.endValue);
|
|
119
|
+
|
|
120
|
+
const hasStart: boolean = !Number.isNaN(startNum) && state.value !== "";
|
|
121
|
+
const hasEnd: boolean = !Number.isNaN(endNum) && state.endValue !== "";
|
|
122
|
+
|
|
123
|
+
switch (state.operator) {
|
|
124
|
+
case FilterOperator.EqualTo:
|
|
125
|
+
return hasStart ? new EqualTo(startNum as any) : undefined;
|
|
126
|
+
case FilterOperator.NotEqualTo:
|
|
127
|
+
return hasStart ? new NotEqual(startNum as any) : undefined;
|
|
128
|
+
case FilterOperator.GreaterThan:
|
|
129
|
+
return hasStart ? new GreaterThan(startNum) : undefined;
|
|
130
|
+
case FilterOperator.LessThan:
|
|
131
|
+
return hasStart ? new LessThan(startNum) : undefined;
|
|
132
|
+
case FilterOperator.GreaterThanOrEqualTo:
|
|
133
|
+
return hasStart ? new GreaterThanOrEqual(startNum) : undefined;
|
|
134
|
+
case FilterOperator.LessThanOrEqualTo:
|
|
135
|
+
return hasStart ? new LessThanOrEqual(startNum) : undefined;
|
|
136
|
+
case FilterOperator.Between:
|
|
137
|
+
return hasStart && hasEnd ? new InBetween(startNum, endNum) : undefined;
|
|
138
|
+
case FilterOperator.IsEmpty:
|
|
139
|
+
return new IsNull();
|
|
140
|
+
case FilterOperator.IsNotEmpty:
|
|
141
|
+
return new NotNull();
|
|
142
|
+
default:
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
18
147
|
const NumberFilter: NumberFilterFunction = <T extends GenericObject>(
|
|
19
148
|
props: ComponentProps<T>,
|
|
20
149
|
): ReactElement => {
|
|
21
150
|
const filter: Filter<T> = props.filter;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!filter.filterDropdownOptions && filter.type === FieldType.Number) {
|
|
27
|
-
return (
|
|
28
|
-
<Input
|
|
29
|
-
onChange={(changedValue: string | number) => {
|
|
30
|
-
if (filter.key) {
|
|
31
|
-
if (!changedValue) {
|
|
32
|
-
delete filterData[filter.key];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (changedValue && filter.type === FieldType.Number) {
|
|
36
|
-
if (typeof changedValue === "string") {
|
|
37
|
-
changedValue = parseInt(changedValue);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
filterData[filter.key] = changedValue;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (props.onFilterChanged) {
|
|
44
|
-
props.onFilterChanged(filterData);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}}
|
|
48
|
-
initialValue={filterData[filter.key]! as string}
|
|
49
|
-
placeholder={`Filter by ${filter.title}`}
|
|
50
|
-
type={inputType}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
151
|
+
|
|
152
|
+
if (filter.filterDropdownOptions) {
|
|
153
|
+
return <></>;
|
|
53
154
|
}
|
|
54
155
|
|
|
55
|
-
|
|
156
|
+
if (filter.type !== FieldType.Number) {
|
|
157
|
+
return <></>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const detected: NumberState = detectState(props.filterData[filter.key]);
|
|
161
|
+
|
|
162
|
+
const [localOperator, setLocalOperator] = useState<FilterOperator>(
|
|
163
|
+
detected.operator,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const raw: unknown = props.filterData[filter.key];
|
|
168
|
+
if (raw !== undefined && raw !== null) {
|
|
169
|
+
setLocalOperator(detected.operator);
|
|
170
|
+
}
|
|
171
|
+
}, [props.filterData[filter.key]]);
|
|
172
|
+
|
|
173
|
+
const state: NumberState = { ...detected, operator: localOperator };
|
|
174
|
+
|
|
175
|
+
const valuelessOperator: boolean =
|
|
176
|
+
state.operator === FilterOperator.IsEmpty ||
|
|
177
|
+
state.operator === FilterOperator.IsNotEmpty;
|
|
178
|
+
const isBetween: boolean = state.operator === FilterOperator.Between;
|
|
179
|
+
|
|
180
|
+
type ApplyFunction = (nextState: NumberState) => void;
|
|
181
|
+
|
|
182
|
+
const apply: ApplyFunction = (nextState: NumberState): void => {
|
|
183
|
+
if (!filter.key) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setLocalOperator(nextState.operator);
|
|
188
|
+
|
|
189
|
+
const next: FilterData<T> = { ...props.filterData };
|
|
190
|
+
const built: unknown = buildValue(nextState);
|
|
191
|
+
|
|
192
|
+
if (built === undefined) {
|
|
193
|
+
delete next[filter.key];
|
|
194
|
+
} else {
|
|
195
|
+
next[filter.key] = built as any;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
props.onFilterChanged?.(next);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex gap-2 items-start">
|
|
203
|
+
<OperatorSelector
|
|
204
|
+
value={state.operator}
|
|
205
|
+
options={NUMBER_OPERATORS}
|
|
206
|
+
onChange={(nextOperator: FilterOperator) => {
|
|
207
|
+
apply({ ...state, operator: nextOperator });
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
{!valuelessOperator && (
|
|
211
|
+
<div
|
|
212
|
+
className={isBetween ? "flex-1 flex gap-2 min-w-0" : "flex-1 min-w-0"}
|
|
213
|
+
>
|
|
214
|
+
<div className="flex-1 min-w-0">
|
|
215
|
+
<input
|
|
216
|
+
type="number"
|
|
217
|
+
value={state.value}
|
|
218
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
219
|
+
apply({ ...state, value: e.target.value });
|
|
220
|
+
}}
|
|
221
|
+
placeholder={isBetween ? "From" : `Filter by ${filter.title}`}
|
|
222
|
+
className="block w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-3 text-sm placeholder-gray-500 focus:border-indigo-500 focus:text-gray-900 focus:placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
{isBetween && (
|
|
226
|
+
<div className="flex-1 min-w-0">
|
|
227
|
+
<input
|
|
228
|
+
type="number"
|
|
229
|
+
value={state.endValue}
|
|
230
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
231
|
+
apply({ ...state, endValue: e.target.value });
|
|
232
|
+
}}
|
|
233
|
+
placeholder="To"
|
|
234
|
+
className="block w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-3 text-sm placeholder-gray-500 focus:border-indigo-500 focus:text-gray-900 focus:placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
56
242
|
};
|
|
57
243
|
|
|
58
244
|
export default NumberFilter;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
2
|
+
import Icon, { SizeProp } from "../Icon/Icon";
|
|
3
|
+
import FilterOperator, { FilterOperatorLabel } from "./Types/FilterOperator";
|
|
4
|
+
import React, { ReactElement, useEffect, useRef, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export interface ComponentProps {
|
|
7
|
+
value: FilterOperator;
|
|
8
|
+
options: Array<FilterOperator>;
|
|
9
|
+
onChange: (value: FilterOperator) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const OperatorSelector: React.FunctionComponent<ComponentProps> = (
|
|
13
|
+
props: ComponentProps,
|
|
14
|
+
): ReactElement => {
|
|
15
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
16
|
+
const containerRef: React.MutableRefObject<HTMLDivElement | null> =
|
|
17
|
+
useRef<HTMLDivElement | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
type HandleClickOutsideFunction = (event: MouseEvent) => void;
|
|
21
|
+
const handleClickOutside: HandleClickOutsideFunction = (
|
|
22
|
+
event: MouseEvent,
|
|
23
|
+
): void => {
|
|
24
|
+
if (
|
|
25
|
+
containerRef.current &&
|
|
26
|
+
!containerRef.current.contains(event.target as Node)
|
|
27
|
+
) {
|
|
28
|
+
setIsOpen(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
33
|
+
return () => {
|
|
34
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
35
|
+
};
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="relative inline-block shrink-0" ref={containerRef}>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => {
|
|
43
|
+
setIsOpen((prev: boolean) => {
|
|
44
|
+
return !prev;
|
|
45
|
+
});
|
|
46
|
+
}}
|
|
47
|
+
className="inline-flex items-center justify-between gap-1.5 h-9 px-3 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border border-gray-300 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 min-w-[130px]"
|
|
48
|
+
>
|
|
49
|
+
<span className="truncate">{FilterOperatorLabel[props.value]}</span>
|
|
50
|
+
<Icon
|
|
51
|
+
icon={IconProp.ChevronDown}
|
|
52
|
+
size={SizeProp.Smaller}
|
|
53
|
+
className="text-gray-400 shrink-0"
|
|
54
|
+
/>
|
|
55
|
+
</button>
|
|
56
|
+
{isOpen && (
|
|
57
|
+
<div className="absolute z-20 mt-1 w-56 bg-white rounded-md shadow-lg border border-gray-200 py-1 max-h-60 overflow-auto">
|
|
58
|
+
{props.options.map((option: FilterOperator) => {
|
|
59
|
+
const isSelected: boolean = option === props.value;
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={option}
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => {
|
|
65
|
+
props.onChange(option);
|
|
66
|
+
setIsOpen(false);
|
|
67
|
+
}}
|
|
68
|
+
className={
|
|
69
|
+
isSelected
|
|
70
|
+
? "w-full text-left px-3 py-2 text-sm text-indigo-700 bg-indigo-50 hover:bg-indigo-100 flex items-center justify-between"
|
|
71
|
+
: "w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center justify-between"
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
<span>{FilterOperatorLabel[option]}</span>
|
|
75
|
+
{isSelected && (
|
|
76
|
+
<Icon
|
|
77
|
+
icon={IconProp.Check}
|
|
78
|
+
size={SizeProp.Smaller}
|
|
79
|
+
className="text-indigo-600"
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default OperatorSelector;
|