@oneuptime/common 10.0.69 → 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 (82) hide show
  1. package/Models/DatabaseModels/KubernetesCluster.ts +5 -0
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +11 -8
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +134 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/DatabaseService.ts +10 -27
  6. package/Server/Services/KubernetesResourceService.ts +33 -10
  7. package/Server/Types/Database/QueryHelper.ts +127 -0
  8. package/Server/Types/Database/QueryUtil.ts +244 -0
  9. package/Types/BaseDatabase/EndsWith.ts +41 -0
  10. package/Types/BaseDatabase/IncludesAll.ts +45 -0
  11. package/Types/BaseDatabase/IncludesNone.ts +48 -0
  12. package/Types/BaseDatabase/NotContains.ts +41 -0
  13. package/Types/BaseDatabase/StartsWith.ts +41 -0
  14. package/Types/JSON.ts +20 -0
  15. package/Types/SerializableObjectDictionary.ts +10 -0
  16. package/UI/Components/Filters/BooleanFilter.tsx +1 -0
  17. package/UI/Components/Filters/DateFilter.tsx +212 -25
  18. package/UI/Components/Filters/DropdownFilter.tsx +1 -0
  19. package/UI/Components/Filters/EntityFilter.tsx +214 -41
  20. package/UI/Components/Filters/FilterViewer.tsx +228 -146
  21. package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
  22. package/UI/Components/Filters/FiltersForm.tsx +148 -97
  23. package/UI/Components/Filters/NumberFilter.tsx +219 -34
  24. package/UI/Components/Filters/OperatorSelector.tsx +91 -0
  25. package/UI/Components/Filters/TextFilter.tsx +182 -71
  26. package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
  27. package/UI/Components/ModelTable/BaseModelTable.tsx +8 -0
  28. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +7 -1
  29. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  30. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +1 -1
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -1
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +123 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  36. package/build/dist/Server/Services/DatabaseService.js +9 -6
  37. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  38. package/build/dist/Server/Services/KubernetesResourceService.js +4 -2
  39. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  40. package/build/dist/Server/Types/Database/QueryHelper.js +110 -0
  41. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  42. package/build/dist/Server/Types/Database/QueryUtil.js +180 -0
  43. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  44. package/build/dist/Types/BaseDatabase/EndsWith.js +31 -0
  45. package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
  46. package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
  47. package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
  48. package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
  49. package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
  50. package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
  51. package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
  52. package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
  53. package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
  54. package/build/dist/Types/JSON.js +5 -0
  55. package/build/dist/Types/JSON.js.map +1 -1
  56. package/build/dist/Types/SerializableObjectDictionary.js +10 -0
  57. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  58. package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
  59. package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
  60. package/build/dist/UI/Components/Filters/DateFilter.js +158 -14
  61. package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
  62. package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
  63. package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
  64. package/build/dist/UI/Components/Filters/EntityFilter.js +174 -30
  65. package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
  66. package/build/dist/UI/Components/Filters/FilterViewer.js +188 -97
  67. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  68. package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
  69. package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
  70. package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
  71. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  72. package/build/dist/UI/Components/Filters/NumberFilter.js +165 -23
  73. package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
  74. package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
  75. package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
  76. package/build/dist/UI/Components/Filters/TextFilter.js +130 -53
  77. package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
  78. package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
  79. package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
  80. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +7 -0
  81. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  82. package/package.json +1 -1
@@ -1,11 +1,18 @@
1
- import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
2
- import StartAndEndDate, { StartAndEndDateType } from "../Date/StartAndEndDate";
1
+ import Input, { InputType } from "../Input/Input";
3
2
  import FieldType from "../Types/FieldType";
4
3
  import Filter from "./Types/Filter";
5
4
  import FilterData from "./Types/FilterData";
5
+ import FilterOperator from "./Types/FilterOperator";
6
+ import OperatorSelector from "./OperatorSelector";
6
7
  import InBetween from "../../../Types/BaseDatabase/InBetween";
8
+ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
9
+ import LessThan from "../../../Types/BaseDatabase/LessThan";
10
+ import EqualTo from "../../../Types/BaseDatabase/EqualTo";
11
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
12
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
13
+ import OneUptimeDate from "../../../Types/Date";
7
14
  import GenericObject from "../../../Types/GenericObject";
8
- import React, { ReactElement } from "react";
15
+ import React, { ReactElement, useEffect, useState } from "react";
9
16
 
10
17
  export interface ComponentProps<T extends GenericObject> {
11
18
  filter: Filter<T>;
@@ -17,38 +24,218 @@ type DateFilterFunction = <T extends GenericObject>(
17
24
  props: ComponentProps<T>,
18
25
  ) => ReactElement;
19
26
 
27
+ const DATE_OPERATORS: Array<FilterOperator> = [
28
+ FilterOperator.Is,
29
+ FilterOperator.Before,
30
+ FilterOperator.After,
31
+ FilterOperator.Between,
32
+ FilterOperator.IsEmpty,
33
+ FilterOperator.IsNotEmpty,
34
+ ];
35
+
36
+ type DateState = {
37
+ operator: FilterOperator;
38
+ start: Date | null;
39
+ end: Date | null;
40
+ };
41
+
42
+ const toDate = (value: unknown): Date | null => {
43
+ if (!value) {
44
+ return null;
45
+ }
46
+ if (value instanceof Date) {
47
+ return value;
48
+ }
49
+ try {
50
+ return OneUptimeDate.fromString(value as string);
51
+ } catch {
52
+ return null;
53
+ }
54
+ };
55
+
56
+ const detectState = (rawValue: unknown): DateState => {
57
+ if (rawValue instanceof InBetween) {
58
+ const start: Date | null = toDate(rawValue.startValue as unknown);
59
+ const end: Date | null = toDate(rawValue.endValue as unknown);
60
+
61
+ // If start/end match the bounds of a single day, treat as "Is".
62
+ if (start && end) {
63
+ const startOfDay: Date = OneUptimeDate.getStartOfDay(start);
64
+ const endOfDay: Date = OneUptimeDate.getEndOfDay(start);
65
+ if (
66
+ start.getTime() === startOfDay.getTime() &&
67
+ end.getTime() === endOfDay.getTime()
68
+ ) {
69
+ return { operator: FilterOperator.Is, start, end: null };
70
+ }
71
+ }
72
+
73
+ return { operator: FilterOperator.Between, start, end };
74
+ }
75
+ if (rawValue instanceof GreaterThan) {
76
+ return {
77
+ operator: FilterOperator.After,
78
+ start: toDate(rawValue.value as unknown),
79
+ end: null,
80
+ };
81
+ }
82
+ if (rawValue instanceof LessThan) {
83
+ return {
84
+ operator: FilterOperator.Before,
85
+ start: toDate(rawValue.value as unknown),
86
+ end: null,
87
+ };
88
+ }
89
+ if (rawValue instanceof EqualTo) {
90
+ return {
91
+ operator: FilterOperator.Is,
92
+ start: toDate(rawValue.value as unknown),
93
+ end: null,
94
+ };
95
+ }
96
+ if (rawValue instanceof IsNull) {
97
+ return { operator: FilterOperator.IsEmpty, start: null, end: null };
98
+ }
99
+ if (rawValue instanceof NotNull) {
100
+ return { operator: FilterOperator.IsNotEmpty, start: null, end: null };
101
+ }
102
+ return { operator: FilterOperator.Is, start: null, end: null };
103
+ };
104
+
105
+ const buildValue = (state: DateState, isDateTime: boolean): unknown => {
106
+ switch (state.operator) {
107
+ case FilterOperator.Is: {
108
+ if (!state.start) {
109
+ return undefined;
110
+ }
111
+ if (isDateTime) {
112
+ return new EqualTo(state.start as any);
113
+ }
114
+ return new InBetween(
115
+ OneUptimeDate.getStartOfDay(state.start) as any,
116
+ OneUptimeDate.getEndOfDay(state.start) as any,
117
+ );
118
+ }
119
+ case FilterOperator.Before:
120
+ return state.start ? new LessThan(state.start) : undefined;
121
+ case FilterOperator.After:
122
+ return state.start ? new GreaterThan(state.start) : undefined;
123
+ case FilterOperator.Between: {
124
+ if (!state.start || !state.end) {
125
+ return undefined;
126
+ }
127
+ return new InBetween(
128
+ (isDateTime
129
+ ? state.start
130
+ : OneUptimeDate.getStartOfDay(state.start)) as any,
131
+ (isDateTime
132
+ ? state.end
133
+ : OneUptimeDate.getEndOfDay(state.end)) as any,
134
+ );
135
+ }
136
+ case FilterOperator.IsEmpty:
137
+ return new IsNull();
138
+ case FilterOperator.IsNotEmpty:
139
+ return new NotNull();
140
+ default:
141
+ return undefined;
142
+ }
143
+ };
144
+
20
145
  const DateFilter: DateFilterFunction = <T extends GenericObject>(
21
146
  props: ComponentProps<T>,
22
147
  ): ReactElement => {
23
148
  const filter: Filter<T> = props.filter;
24
- const filterData: FilterData<T> = { ...props.filterData };
25
149
 
26
150
  if (filter.type !== FieldType.Date && filter.type !== FieldType.DateTime) {
27
151
  return <></>;
28
152
  }
29
153
 
154
+ const isDateTime: boolean = filter.type === FieldType.DateTime;
155
+ const detected: DateState = detectState(props.filterData[filter.key]);
156
+
157
+ const [localOperator, setLocalOperator] = useState<FilterOperator>(
158
+ detected.operator,
159
+ );
160
+
161
+ useEffect(() => {
162
+ const raw: unknown = props.filterData[filter.key];
163
+ if (raw !== undefined && raw !== null) {
164
+ setLocalOperator(detected.operator);
165
+ }
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, [props.filterData[filter.key]]);
168
+
169
+ const state: DateState = { ...detected, operator: localOperator };
170
+
171
+ const valuelessOperator: boolean =
172
+ state.operator === FilterOperator.IsEmpty ||
173
+ state.operator === FilterOperator.IsNotEmpty;
174
+ const isBetween: boolean = state.operator === FilterOperator.Between;
175
+
176
+ type ApplyFunction = (nextState: DateState) => void;
177
+
178
+ const apply: ApplyFunction = (nextState: DateState): void => {
179
+ if (!filter.key) {
180
+ return;
181
+ }
182
+ setLocalOperator(nextState.operator);
183
+ const next: FilterData<T> = { ...props.filterData };
184
+ const built: unknown = buildValue(nextState, isDateTime);
185
+ if (built === undefined) {
186
+ delete next[filter.key];
187
+ } else {
188
+ next[filter.key] = built as any;
189
+ }
190
+ props.onFilterChanged?.(next);
191
+ };
192
+
193
+ const inputType: InputType = isDateTime
194
+ ? InputType.DATETIME_LOCAL
195
+ : InputType.DATE;
196
+
30
197
  return (
31
- <StartAndEndDate
32
- value={filterData[filter.key] as InBetween<Date>}
33
- onValueChanged={(inBetween: InBetween<Date> | null) => {
34
- filterData[filter.key] = inBetween as FindWhereProperty<
35
- NonNullable<T[keyof T]>
36
- >;
37
-
38
- if (!filterData[filter.key]) {
39
- delete filterData[filter.key];
40
- }
41
-
42
- if (props.onFilterChanged) {
43
- props.onFilterChanged(filterData);
44
- }
45
- }}
46
- type={
47
- filter.type === FieldType.DateTime
48
- ? StartAndEndDateType.DateTime
49
- : StartAndEndDateType.Date
50
- }
51
- />
198
+ <div className="flex gap-2 items-start">
199
+ <OperatorSelector
200
+ value={state.operator}
201
+ options={DATE_OPERATORS}
202
+ onChange={(nextOperator: FilterOperator) => {
203
+ apply({ ...state, operator: nextOperator });
204
+ }}
205
+ />
206
+ {!valuelessOperator && (
207
+ <div className={isBetween ? "flex-1 flex gap-2 min-w-0" : "flex-1 min-w-0"}>
208
+ <div className="flex-1 min-w-0">
209
+ <Input
210
+ key={`${filter.key as string}-start-${state.operator}`}
211
+ onChange={(changed: string | Date) => {
212
+ const parsed: Date | null = toDate(changed);
213
+ apply({ ...state, start: parsed });
214
+ }}
215
+ value={state.start || ""}
216
+ placeholder={isBetween ? "From" : `Filter by ${filter.title}`}
217
+ type={inputType}
218
+ outerDivClassName="relative rounded-md w-full"
219
+ />
220
+ </div>
221
+ {isBetween && (
222
+ <div className="flex-1 min-w-0">
223
+ <Input
224
+ key={`${filter.key as string}-end-${state.operator}`}
225
+ onChange={(changed: string | Date) => {
226
+ const parsed: Date | null = toDate(changed);
227
+ apply({ ...state, end: parsed });
228
+ }}
229
+ value={state.end || ""}
230
+ placeholder="To"
231
+ type={inputType}
232
+ outerDivClassName="relative rounded-md w-full"
233
+ />
234
+ </div>
235
+ )}
236
+ </div>
237
+ )}
238
+ </div>
52
239
  );
53
240
  };
54
241
 
@@ -61,6 +61,7 @@ const DropdownFilter: DropdownFilterFunction = <T extends GenericObject>(
61
61
  value={dropdownValues}
62
62
  isMultiSelect={props.isMultiSelect || false}
63
63
  placeholder={`Filter by ${filter.title}`}
64
+ className="relative rounded-md w-full overflow-visible"
64
65
  />
65
66
  );
66
67
  }
@@ -2,8 +2,15 @@ import Dropdown, { DropdownOption, DropdownValue } from "../Dropdown/Dropdown";
2
2
  import FieldType from "../Types/FieldType";
3
3
  import Filter from "./Types/Filter";
4
4
  import FilterData from "./Types/FilterData";
5
+ import FilterOperator from "./Types/FilterOperator";
6
+ import OperatorSelector from "./OperatorSelector";
5
7
  import GenericObject from "../../../Types/GenericObject";
6
- import React, { ReactElement } from "react";
8
+ import Includes from "../../../Types/BaseDatabase/Includes";
9
+ import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
10
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
11
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
12
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
13
+ import React, { ReactElement, useEffect, useState } from "react";
7
14
 
8
15
  export interface ComponentProps<T extends GenericObject> {
9
16
  filter: Filter<T>;
@@ -15,56 +22,222 @@ type EntityFilterFunction = <T extends GenericObject>(
15
22
  props: ComponentProps<T>,
16
23
  ) => ReactElement;
17
24
 
25
+ const ENTITY_ARRAY_OPERATORS: Array<FilterOperator> = [
26
+ FilterOperator.HasAnyOf,
27
+ FilterOperator.HasAllOf,
28
+ FilterOperator.HasNoneOf,
29
+ FilterOperator.IsEmpty,
30
+ FilterOperator.IsNotEmpty,
31
+ ];
32
+
33
+ const ENTITY_OPERATORS: Array<FilterOperator> = [
34
+ FilterOperator.Is,
35
+ FilterOperator.IsNot,
36
+ FilterOperator.IsEmpty,
37
+ FilterOperator.IsNotEmpty,
38
+ ];
39
+
40
+ type EntityState = {
41
+ operator: FilterOperator;
42
+ values: Array<string>;
43
+ };
44
+
45
+ const detectArrayState = (rawValue: unknown): EntityState => {
46
+ if (rawValue instanceof IncludesAll) {
47
+ return {
48
+ operator: FilterOperator.HasAllOf,
49
+ values: (rawValue.values as Array<string>).map((v: string) => {
50
+ return v.toString();
51
+ }),
52
+ };
53
+ }
54
+ if (rawValue instanceof IncludesNone) {
55
+ return {
56
+ operator: FilterOperator.HasNoneOf,
57
+ values: (rawValue.values as Array<string>).map((v: string) => {
58
+ return v.toString();
59
+ }),
60
+ };
61
+ }
62
+ if (rawValue instanceof Includes) {
63
+ return {
64
+ operator: FilterOperator.HasAnyOf,
65
+ values: (rawValue.values as Array<string>).map((v: string) => {
66
+ return v.toString();
67
+ }),
68
+ };
69
+ }
70
+ if (Array.isArray(rawValue)) {
71
+ return {
72
+ operator: FilterOperator.HasAnyOf,
73
+ values: (rawValue as Array<string>).map((v: string) => {
74
+ return v.toString();
75
+ }),
76
+ };
77
+ }
78
+ if (rawValue instanceof IsNull) {
79
+ return { operator: FilterOperator.IsEmpty, values: [] };
80
+ }
81
+ if (rawValue instanceof NotNull) {
82
+ return { operator: FilterOperator.IsNotEmpty, values: [] };
83
+ }
84
+ return { operator: FilterOperator.HasAnyOf, values: [] };
85
+ };
86
+
87
+ const detectSingleState = (rawValue: unknown): EntityState => {
88
+ if (rawValue instanceof IsNull) {
89
+ return { operator: FilterOperator.IsEmpty, values: [] };
90
+ }
91
+ if (rawValue instanceof NotNull) {
92
+ return { operator: FilterOperator.IsNotEmpty, values: [] };
93
+ }
94
+ if (typeof rawValue === "string" && rawValue) {
95
+ return { operator: FilterOperator.Is, values: [rawValue] };
96
+ }
97
+ return { operator: FilterOperator.Is, values: [] };
98
+ };
99
+
100
+ const buildArrayValue = (state: EntityState): unknown => {
101
+ switch (state.operator) {
102
+ case FilterOperator.HasAllOf:
103
+ return state.values.length > 0 ? new IncludesAll(state.values) : undefined;
104
+ case FilterOperator.HasNoneOf:
105
+ return state.values.length > 0 ? new IncludesNone(state.values) : undefined;
106
+ case FilterOperator.HasAnyOf:
107
+ return state.values.length > 0 ? state.values : undefined;
108
+ case FilterOperator.IsEmpty:
109
+ return new IsNull();
110
+ case FilterOperator.IsNotEmpty:
111
+ return new NotNull();
112
+ default:
113
+ return undefined;
114
+ }
115
+ };
116
+
117
+ const buildSingleValue = (state: EntityState): unknown => {
118
+ switch (state.operator) {
119
+ case FilterOperator.Is:
120
+ return state.values[0] || undefined;
121
+ case FilterOperator.IsNot:
122
+ // Use IncludesNone with single-item array to represent "is not".
123
+ return state.values[0]
124
+ ? new IncludesNone([state.values[0]])
125
+ : undefined;
126
+ case FilterOperator.IsEmpty:
127
+ return new IsNull();
128
+ case FilterOperator.IsNotEmpty:
129
+ return new NotNull();
130
+ default:
131
+ return undefined;
132
+ }
133
+ };
134
+
18
135
  const EntityFilter: EntityFilterFunction = <T extends GenericObject>(
19
136
  props: ComponentProps<T>,
20
137
  ): ReactElement => {
21
138
  const filter: Filter<T> = props.filter;
22
- const filterData: FilterData<T> = { ...props.filterData };
139
+
140
+ if (
141
+ filter.type !== FieldType.Entity &&
142
+ filter.type !== FieldType.EntityArray
143
+ ) {
144
+ return <></>;
145
+ }
146
+
147
+ if (!filter.filterDropdownOptions) {
148
+ return <></>;
149
+ }
150
+
151
+ const isArray: boolean = filter.type === FieldType.EntityArray;
152
+ const detectedState: EntityState = isArray
153
+ ? detectArrayState(props.filterData[filter.key])
154
+ : detectSingleState(props.filterData[filter.key]);
155
+
156
+ // Hold the operator locally so the user's choice persists even when no
157
+ // values are selected yet (otherwise `buildArrayValue` would return
158
+ // undefined, filterData wouldn't carry the operator, and the next render
159
+ // would reset back to the default).
160
+ const [localOperator, setLocalOperator] = useState<FilterOperator>(
161
+ detectedState.operator,
162
+ );
163
+
164
+ // When the external filter data changes and can unambiguously tell us
165
+ // which operator it represents, sync local state.
166
+ useEffect(() => {
167
+ const raw: unknown = props.filterData[filter.key];
168
+ if (raw !== undefined && raw !== null) {
169
+ setLocalOperator(detectedState.operator);
170
+ }
171
+ // Intentionally only re-run when the underlying filter data reference
172
+ // changes — not on every detectedState re-computation.
173
+ // eslint-disable-next-line react-hooks/exhaustive-deps
174
+ }, [props.filterData[filter.key]]);
175
+
176
+ const operator: FilterOperator = localOperator;
177
+ const state: EntityState = { ...detectedState, operator };
178
+
179
+ const valuelessOperator: boolean =
180
+ operator === FilterOperator.IsEmpty ||
181
+ operator === FilterOperator.IsNotEmpty;
23
182
 
24
183
  const dropdownValues: Array<DropdownOption> =
25
- props.filter.filterDropdownOptions?.filter((option: DropdownOption) => {
26
- if (filterData[filter.key] instanceof Array) {
27
- return (filterData[filter.key] as Array<string>)
28
- .map((value: string) => {
29
- return value.toString();
30
- })
31
- .includes(option.value.toString());
32
- }
33
-
34
- return option.value.toString() === filterData[filter.key]?.toString();
184
+ filter.filterDropdownOptions?.filter((option: DropdownOption) => {
185
+ return state.values.includes(option.value.toString());
35
186
  }) || [];
36
187
 
37
- if (
38
- (filter.type === FieldType.Entity ||
39
- filter.type === FieldType.EntityArray) &&
40
- filter.filterDropdownOptions
41
- ) {
42
- return (
43
- <Dropdown
44
- options={filter.filterDropdownOptions}
45
- onChange={(value: DropdownValue | Array<DropdownValue> | null) => {
46
- if (!filter.key) {
47
- return;
48
- }
49
-
50
- if (!value || (Array.isArray(value) && value.length === 0)) {
51
- delete filterData[filter.key];
52
- } else {
53
- filterData[filter.key] = value;
54
- }
55
-
56
- if (props.onFilterChanged) {
57
- props.onFilterChanged(filterData);
58
- }
188
+ type ApplyFunction = (nextState: EntityState) => void;
189
+
190
+ const apply: ApplyFunction = (nextState: EntityState): void => {
191
+ if (!filter.key) {
192
+ return;
193
+ }
194
+ setLocalOperator(nextState.operator);
195
+ const next: FilterData<T> = { ...props.filterData };
196
+ const built: unknown = isArray
197
+ ? buildArrayValue(nextState)
198
+ : buildSingleValue(nextState);
199
+ if (built === undefined) {
200
+ delete next[filter.key];
201
+ } else {
202
+ next[filter.key] = built as any;
203
+ }
204
+ props.onFilterChanged?.(next);
205
+ };
206
+
207
+ return (
208
+ <div className="flex gap-2 items-start">
209
+ <OperatorSelector
210
+ value={operator}
211
+ options={isArray ? ENTITY_ARRAY_OPERATORS : ENTITY_OPERATORS}
212
+ onChange={(nextOperator: FilterOperator) => {
213
+ apply({ ...state, operator: nextOperator });
59
214
  }}
60
- value={dropdownValues}
61
- isMultiSelect={filter.type === FieldType.EntityArray}
62
- placeholder={`Filter by ${filter.title}`}
63
215
  />
64
- );
65
- }
66
-
67
- return <></>;
216
+ {!valuelessOperator && (
217
+ <div className="flex-1 min-w-0">
218
+ <Dropdown
219
+ options={filter.filterDropdownOptions}
220
+ onChange={(value: DropdownValue | Array<DropdownValue> | null) => {
221
+ if (!value || (Array.isArray(value) && value.length === 0)) {
222
+ apply({ ...state, values: [] });
223
+ return;
224
+ }
225
+ const nextValues: Array<string> = Array.isArray(value)
226
+ ? (value as Array<DropdownValue>).map((v: DropdownValue) => {
227
+ return v.toString();
228
+ })
229
+ : [value.toString()];
230
+ apply({ ...state, values: nextValues });
231
+ }}
232
+ value={dropdownValues}
233
+ isMultiSelect={isArray}
234
+ placeholder={`Filter by ${filter.title}`}
235
+ className="relative rounded-md w-full overflow-visible"
236
+ />
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
68
241
  };
69
242
 
70
243
  export default EntityFilter;