@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.
Files changed (106) hide show
  1. package/Models/DatabaseModels/KubernetesCluster.ts +7 -0
  2. package/Models/DatabaseModels/Project.ts +5 -5
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +11 -8
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +137 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Services/AIBillingService.ts +2 -2
  8. package/Server/Services/BillingService.ts +116 -48
  9. package/Server/Services/DatabaseService.ts +10 -27
  10. package/Server/Services/KubernetesResourceService.ts +33 -10
  11. package/Server/Services/NotificationService.ts +2 -2
  12. package/Server/Types/Database/QueryHelper.ts +127 -0
  13. package/Server/Types/Database/QueryUtil.ts +250 -0
  14. package/Server/Utils/Monitor/MonitorAlert.ts +79 -0
  15. package/Server/Utils/Monitor/MonitorIncident.ts +79 -0
  16. package/Types/BaseDatabase/EndsWith.ts +41 -0
  17. package/Types/BaseDatabase/IncludesAll.ts +45 -0
  18. package/Types/BaseDatabase/IncludesNone.ts +45 -0
  19. package/Types/BaseDatabase/NotContains.ts +41 -0
  20. package/Types/BaseDatabase/StartsWith.ts +41 -0
  21. package/Types/Email.ts +50 -0
  22. package/Types/JSON.ts +20 -0
  23. package/Types/SerializableObjectDictionary.ts +10 -0
  24. package/UI/Components/Filters/BooleanFilter.tsx +1 -0
  25. package/UI/Components/Filters/DateFilter.tsx +220 -25
  26. package/UI/Components/Filters/DropdownFilter.tsx +1 -0
  27. package/UI/Components/Filters/EntityFilter.tsx +229 -41
  28. package/UI/Components/Filters/FilterViewer.tsx +231 -147
  29. package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
  30. package/UI/Components/Filters/FiltersForm.tsx +146 -97
  31. package/UI/Components/Filters/NumberFilter.tsx +220 -34
  32. package/UI/Components/Filters/OperatorSelector.tsx +91 -0
  33. package/UI/Components/Filters/TextFilter.tsx +183 -71
  34. package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
  35. package/UI/Components/ModelTable/BaseModelTable.tsx +10 -0
  36. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +9 -1
  37. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  38. package/build/dist/Models/DatabaseModels/Project.js +5 -5
  39. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  40. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +1 -1
  41. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -1
  42. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +125 -0
  43. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
  44. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +12 -0
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +1 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  48. package/build/dist/Server/Services/AIBillingService.js +2 -2
  49. package/build/dist/Server/Services/AIBillingService.js.map +1 -1
  50. package/build/dist/Server/Services/BillingService.js +99 -39
  51. package/build/dist/Server/Services/BillingService.js.map +1 -1
  52. package/build/dist/Server/Services/DatabaseService.js +9 -6
  53. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  54. package/build/dist/Server/Services/KubernetesResourceService.js +4 -2
  55. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  56. package/build/dist/Server/Services/NotificationService.js +2 -2
  57. package/build/dist/Server/Services/NotificationService.js.map +1 -1
  58. package/build/dist/Server/Types/Database/QueryHelper.js +110 -0
  59. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  60. package/build/dist/Server/Types/Database/QueryUtil.js +186 -0
  61. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  62. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +68 -0
  63. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  64. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +68 -0
  65. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  66. package/build/dist/Types/BaseDatabase/EndsWith.js +31 -0
  67. package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
  68. package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
  69. package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
  70. package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
  71. package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
  72. package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
  73. package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
  74. package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
  75. package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
  76. package/build/dist/Types/Email.js +42 -0
  77. package/build/dist/Types/Email.js.map +1 -1
  78. package/build/dist/Types/JSON.js +5 -0
  79. package/build/dist/Types/JSON.js.map +1 -1
  80. package/build/dist/Types/SerializableObjectDictionary.js +10 -0
  81. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  82. package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
  83. package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
  84. package/build/dist/UI/Components/Filters/DateFilter.js +155 -14
  85. package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
  86. package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
  87. package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
  88. package/build/dist/UI/Components/Filters/EntityFilter.js +181 -30
  89. package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
  90. package/build/dist/UI/Components/Filters/FilterViewer.js +188 -98
  91. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  92. package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
  93. package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
  94. package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
  95. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  96. package/build/dist/UI/Components/Filters/NumberFilter.js +164 -23
  97. package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
  98. package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
  99. package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
  100. package/build/dist/UI/Components/Filters/TextFilter.js +131 -53
  101. package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
  102. package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
  103. package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
  104. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +9 -0
  105. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  106. 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="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 && (
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
- {props.showFilter && props.filterError && (
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
- {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>
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
- 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
+ }, [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;