@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.
- package/Models/DatabaseModels/KubernetesCluster.ts +5 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +11 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +134 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/DatabaseService.ts +10 -27
- package/Server/Services/KubernetesResourceService.ts +33 -10
- package/Server/Types/Database/QueryHelper.ts +127 -0
- package/Server/Types/Database/QueryUtil.ts +244 -0
- package/Types/BaseDatabase/EndsWith.ts +41 -0
- package/Types/BaseDatabase/IncludesAll.ts +45 -0
- package/Types/BaseDatabase/IncludesNone.ts +48 -0
- package/Types/BaseDatabase/NotContains.ts +41 -0
- package/Types/BaseDatabase/StartsWith.ts +41 -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 +212 -25
- package/UI/Components/Filters/DropdownFilter.tsx +1 -0
- package/UI/Components/Filters/EntityFilter.tsx +214 -41
- package/UI/Components/Filters/FilterViewer.tsx +228 -146
- package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
- package/UI/Components/Filters/FiltersForm.tsx +148 -97
- package/UI/Components/Filters/NumberFilter.tsx +219 -34
- package/UI/Components/Filters/OperatorSelector.tsx +91 -0
- package/UI/Components/Filters/TextFilter.tsx +182 -71
- package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +8 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +7 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.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 +123 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.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/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/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 +180 -0
- package/build/dist/Server/Types/Database/QueryUtil.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/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 +158 -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 +174 -30
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +188 -97
- 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 +165 -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 +130 -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 +7 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import {
|
|
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
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
filter.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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;
|