@oneuptime/common 10.0.68 → 10.0.70

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 (95) hide show
  1. package/Models/DatabaseModels/KubernetesCluster.ts +5 -0
  2. package/Models/DatabaseModels/KubernetesResource.ts +19 -0
  3. package/Server/API/KubernetesResourceAPI.ts +2 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +17 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +134 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  7. package/Server/Services/DatabaseService.ts +19 -4
  8. package/Server/Services/KubernetesResourceService.ts +323 -8
  9. package/Server/Types/Database/QueryHelper.ts +127 -0
  10. package/Server/Types/Database/QueryUtil.ts +244 -0
  11. package/Server/Utils/VM/VMRunner.ts +39 -22
  12. package/Types/BaseDatabase/EndsWith.ts +41 -0
  13. package/Types/BaseDatabase/IncludesAll.ts +45 -0
  14. package/Types/BaseDatabase/IncludesNone.ts +48 -0
  15. package/Types/BaseDatabase/NotContains.ts +41 -0
  16. package/Types/BaseDatabase/StartsWith.ts +41 -0
  17. package/Types/IsolatedVM/ReturnResult.ts +6 -0
  18. package/Types/JSON.ts +20 -0
  19. package/Types/Kubernetes/KubernetesInventoryExtractor.ts +15 -1
  20. package/Types/SerializableObjectDictionary.ts +10 -0
  21. package/UI/Components/Filters/BooleanFilter.tsx +1 -0
  22. package/UI/Components/Filters/DateFilter.tsx +212 -25
  23. package/UI/Components/Filters/DropdownFilter.tsx +1 -0
  24. package/UI/Components/Filters/EntityFilter.tsx +214 -41
  25. package/UI/Components/Filters/FilterViewer.tsx +228 -146
  26. package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
  27. package/UI/Components/Filters/FiltersForm.tsx +148 -97
  28. package/UI/Components/Filters/NumberFilter.tsx +219 -34
  29. package/UI/Components/Filters/OperatorSelector.tsx +91 -0
  30. package/UI/Components/Filters/TextFilter.tsx +182 -71
  31. package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
  32. package/UI/Components/ModelTable/BaseModelTable.tsx +8 -0
  33. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +7 -1
  34. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  35. package/build/dist/Models/DatabaseModels/KubernetesResource.js +20 -0
  36. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  37. package/build/dist/Server/API/KubernetesResourceAPI.js +2 -0
  38. package/build/dist/Server/API/KubernetesResourceAPI.js.map +1 -1
  39. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +12 -0
  40. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -0
  41. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +123 -0
  42. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
  43. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  44. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  45. package/build/dist/Server/Services/DatabaseService.js +18 -4
  46. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  47. package/build/dist/Server/Services/KubernetesResourceService.js +204 -8
  48. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  49. package/build/dist/Server/Types/Database/QueryHelper.js +110 -0
  50. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  51. package/build/dist/Server/Types/Database/QueryUtil.js +180 -0
  52. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  53. package/build/dist/Server/Utils/VM/VMRunner.js +33 -19
  54. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  55. package/build/dist/Types/BaseDatabase/EndsWith.js +31 -0
  56. package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
  57. package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
  58. package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
  59. package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
  60. package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
  61. package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
  62. package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
  63. package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
  64. package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
  65. package/build/dist/Types/JSON.js +5 -0
  66. package/build/dist/Types/JSON.js.map +1 -1
  67. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js +7 -1
  68. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js.map +1 -1
  69. package/build/dist/Types/SerializableObjectDictionary.js +10 -0
  70. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  71. package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
  72. package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
  73. package/build/dist/UI/Components/Filters/DateFilter.js +158 -14
  74. package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
  75. package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
  76. package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
  77. package/build/dist/UI/Components/Filters/EntityFilter.js +174 -30
  78. package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
  79. package/build/dist/UI/Components/Filters/FilterViewer.js +188 -97
  80. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  81. package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
  82. package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
  83. package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
  84. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  85. package/build/dist/UI/Components/Filters/NumberFilter.js +165 -23
  86. package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
  87. package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
  88. package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
  89. package/build/dist/UI/Components/Filters/TextFilter.js +130 -53
  90. package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
  91. package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
  92. package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
  93. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +7 -0
  94. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  95. 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,160 @@ 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 (
76
+ showMoreFilters && !props.isFilterLoading && !props.filterError
77
+ );
78
+ }
79
+ return true;
80
+ },
81
+ );
82
+
64
83
  return (
65
84
  <div id={props.id}>
66
- <div className="pt-3 pb-5">
67
- <div className="space-y-5">
68
- {props.showFilter &&
69
- props.filters &&
70
- props.filters
71
- .filter((filter: Filter<T>) => {
72
- if (filter.isAdvancedFilter) {
73
- // Hide advanced filters if not toggled on, or if they are still loading/errored
74
- return (
75
- showMoreFilters &&
76
- !props.isFilterLoading &&
77
- !props.filterError
78
- );
79
- }
80
- return true;
81
- })
82
- .map((filter: Filter<T>, i: number) => {
83
- return (
84
- <div key={i} className="col-span-3 sm:col-span-3 ">
85
- <FieldLabelElement required={true} title={filter.title} />
86
-
87
- <DropdownFilter
88
- filter={filter}
89
- filterData={props.filterData}
90
- onFilterChanged={changeFilterData}
91
- isMultiSelect={
92
- filter.type === FieldType.MultiSelectDropdown
93
- }
94
- />
95
-
96
- <EntityFilter
97
- filter={filter}
98
- filterData={props.filterData}
99
- onFilterChanged={changeFilterData}
100
- />
101
-
102
- <BooleanFilter
103
- filter={filter}
104
- filterData={props.filterData}
105
- onFilterChanged={changeFilterData}
106
- />
107
-
108
- <DateFilter
109
- filter={filter}
110
- filterData={props.filterData}
111
- onFilterChanged={changeFilterData}
112
- />
113
-
114
- <TextFilter
115
- filter={filter}
116
- filterData={props.filterData}
117
- onFilterChanged={changeFilterData}
118
- />
119
-
120
- <NumberFilter
121
- filter={filter}
122
- filterData={props.filterData}
123
- onFilterChanged={changeFilterData}
124
- />
125
-
126
- <JSONFilter
127
- filter={filter}
128
- filterData={props.filterData}
129
- onFilterChanged={changeFilterData}
130
- jsonKeys={filter.jsonKeys}
131
- jsonValueSuggestions={filter.jsonValueSuggestions}
132
- onJsonKeySelected={filter.onJsonKeySelected}
133
- />
134
- </div>
135
- );
136
- })}
137
- </div>
138
- {props.showFilter && props.isFilterLoading && !props.filterError && (
85
+ <div className="space-y-3">
86
+ {visibleFilters.map((filter: Filter<T>, i: number) => {
87
+ const hasValue: boolean =
88
+ filter.key !== undefined &&
89
+ props.filterData[filter.key] !== undefined &&
90
+ props.filterData[filter.key] !== null;
91
+
92
+ return (
93
+ <div
94
+ key={i}
95
+ className="grid grid-cols-[140px_1fr_auto] items-center gap-3"
96
+ >
97
+ {/* Label column */}
98
+ <div className="flex items-center min-w-0">
99
+ <label className="text-sm font-medium text-gray-700 truncate">
100
+ {filter.title}
101
+ </label>
102
+ </div>
103
+
104
+ {/* Controls column */}
105
+ <div className="min-w-0">
106
+ <DropdownFilter
107
+ filter={filter}
108
+ filterData={props.filterData}
109
+ onFilterChanged={changeFilterData}
110
+ isMultiSelect={filter.type === FieldType.MultiSelectDropdown}
111
+ />
112
+
113
+ <EntityFilter
114
+ filter={filter}
115
+ filterData={props.filterData}
116
+ onFilterChanged={changeFilterData}
117
+ />
118
+
119
+ <BooleanFilter
120
+ filter={filter}
121
+ filterData={props.filterData}
122
+ onFilterChanged={changeFilterData}
123
+ />
124
+
125
+ <DateFilter
126
+ filter={filter}
127
+ filterData={props.filterData}
128
+ onFilterChanged={changeFilterData}
129
+ />
130
+
131
+ <TextFilter
132
+ filter={filter}
133
+ filterData={props.filterData}
134
+ onFilterChanged={changeFilterData}
135
+ />
136
+
137
+ <NumberFilter
138
+ filter={filter}
139
+ filterData={props.filterData}
140
+ onFilterChanged={changeFilterData}
141
+ />
142
+
143
+ <JSONFilter
144
+ filter={filter}
145
+ filterData={props.filterData}
146
+ onFilterChanged={changeFilterData}
147
+ jsonKeys={filter.jsonKeys}
148
+ jsonValueSuggestions={filter.jsonValueSuggestions}
149
+ onJsonKeySelected={filter.onJsonKeySelected}
150
+ />
151
+ </div>
152
+
153
+ {/* Clear column */}
154
+ <div className="flex items-center">
155
+ {hasValue && filter.key ? (
156
+ <button
157
+ type="button"
158
+ onClick={() => {
159
+ return clearFilter(filter.key as keyof T);
160
+ }}
161
+ className="p-1.5 rounded-md text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors"
162
+ aria-label={`Clear ${filter.title} filter`}
163
+ title={`Clear ${filter.title}`}
164
+ >
165
+ <svg
166
+ xmlns="http://www.w3.org/2000/svg"
167
+ viewBox="0 0 20 20"
168
+ fill="currentColor"
169
+ className="w-4 h-4"
170
+ >
171
+ <path
172
+ fillRule="evenodd"
173
+ 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"
174
+ clipRule="evenodd"
175
+ />
176
+ </svg>
177
+ </button>
178
+ ) : (
179
+ <div className="w-7" aria-hidden="true" />
180
+ )}
181
+ </div>
182
+ </div>
183
+ );
184
+ })}
185
+ </div>
186
+ {props.showFilter && props.isFilterLoading && !props.filterError && (
187
+ <div className="py-4">
139
188
  <ComponentLoader />
140
- )}
189
+ </div>
190
+ )}
141
191
 
142
- {props.showFilter && props.filterError && (
192
+ {props.showFilter && props.filterError && (
193
+ <div className="py-4">
143
194
  <ErrorMessage
144
195
  message={props.filterError}
145
196
  onRefreshClick={props.onFilterRefreshClick}
146
197
  />
147
- )}
148
- {showAdvancedFilterButton && (
149
- <Button
150
- className="-ml-3 mt-1"
151
- buttonStyle={ButtonStyleType.SECONDARY_LINK}
152
- title={
153
- showMoreFilters
154
- ? "Hide Advanced Filters"
155
- : "Show Advanced Filters"
156
- }
157
- onClick={() => {
158
- setShowMoreFilters((currentValue: boolean) => {
159
- const newValue: boolean = !currentValue;
160
- props.onAdvancedFiltersToggle?.(newValue);
161
- return newValue;
162
- });
163
- }}
164
- />
165
- )}
166
- </div>
198
+ </div>
199
+ )}
200
+ {showAdvancedFilterButton && (
201
+ <Button
202
+ className="-ml-3 mt-3"
203
+ buttonSize={ButtonSize.Small}
204
+ buttonStyle={ButtonStyleType.SECONDARY_LINK}
205
+ icon={showMoreFilters ? IconProp.ChevronUp : IconProp.ChevronDown}
206
+ title={
207
+ showMoreFilters ? "Hide Advanced Filters" : "Show Advanced Filters"
208
+ }
209
+ onClick={() => {
210
+ setShowMoreFilters((currentValue: boolean) => {
211
+ const newValue: boolean = !currentValue;
212
+ props.onAdvancedFiltersToggle?.(newValue);
213
+ return newValue;
214
+ });
215
+ }}
216
+ />
217
+ )}
167
218
  </div>
168
219
  );
169
220
  };
@@ -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,219 @@ 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
- const filterData: FilterData<T> = { ...props.filterData };
23
-
24
- const inputType: InputType = InputType.NUMBER;
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
- return <></>;
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
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [props.filterData[filter.key]]);
173
+
174
+ const state: NumberState = { ...detected, operator: localOperator };
175
+
176
+ const valuelessOperator: boolean =
177
+ state.operator === FilterOperator.IsEmpty ||
178
+ state.operator === FilterOperator.IsNotEmpty;
179
+ const isBetween: boolean = state.operator === FilterOperator.Between;
180
+
181
+ type ApplyFunction = (nextState: NumberState) => void;
182
+
183
+ const apply: ApplyFunction = (nextState: NumberState): void => {
184
+ if (!filter.key) {
185
+ return;
186
+ }
187
+
188
+ setLocalOperator(nextState.operator);
189
+
190
+ const next: FilterData<T> = { ...props.filterData };
191
+ const built: unknown = buildValue(nextState);
192
+
193
+ if (built === undefined) {
194
+ delete next[filter.key];
195
+ } else {
196
+ next[filter.key] = built as any;
197
+ }
198
+
199
+ props.onFilterChanged?.(next);
200
+ };
201
+
202
+ return (
203
+ <div className="flex gap-2 items-start">
204
+ <OperatorSelector
205
+ value={state.operator}
206
+ options={NUMBER_OPERATORS}
207
+ onChange={(nextOperator: FilterOperator) => {
208
+ apply({ ...state, operator: nextOperator });
209
+ }}
210
+ />
211
+ {!valuelessOperator && (
212
+ <div className={isBetween ? "flex-1 flex gap-2 min-w-0" : "flex-1 min-w-0"}>
213
+ <div className="flex-1 min-w-0">
214
+ <input
215
+ type="number"
216
+ value={state.value}
217
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
218
+ apply({ ...state, value: e.target.value });
219
+ }}
220
+ placeholder={isBetween ? "From" : `Filter by ${filter.title}`}
221
+ 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"
222
+ />
223
+ </div>
224
+ {isBetween && (
225
+ <div className="flex-1 min-w-0">
226
+ <input
227
+ type="number"
228
+ value={state.endValue}
229
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
230
+ apply({ ...state, endValue: e.target.value });
231
+ }}
232
+ placeholder="To"
233
+ 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"
234
+ />
235
+ </div>
236
+ )}
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
56
241
  };
57
242
 
58
243
  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;