@oneuptime/common 11.7.2 → 11.7.3
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/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
- package/Models/DatabaseModels/TableView.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
- package/Tests/Types/JSONFunctions.test.ts +103 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
- package/Tests/Utils/UserPreferences.test.ts +563 -0
- package/Types/JSONFunctions.ts +7 -2
- package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
- package/UI/Components/MasterPage/MasterPage.tsx +65 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +387 -4
- package/UI/Components/ModelTable/Column.ts +17 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
- package/UI/Components/ModelTable/TableView.tsx +24 -2
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
- package/UI/Components/SideMenu/SideMenu.tsx +24 -4
- package/UI/Components/Table/Table.tsx +14 -1
- package/UI/Components/Table/TableRow.tsx +180 -175
- package/UI/Components/Table/Types/Column.ts +2 -0
- package/Utils/UserPreferences.ts +53 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TableView.js +40 -0
- package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.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/Types/JSONFunctions.js +8 -3
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +239 -7
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
- package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +15 -2
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +11 -6
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/Utils/UserPreferences.js +33 -0
- package/build/dist/Utils/UserPreferences.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { CustomizableColumn } from "./ColumnPreference";
|
|
2
|
+
import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
|
|
3
|
+
import CheckboxElement from "../Checkbox/Checkbox";
|
|
4
|
+
import Icon from "../Icon/Icon";
|
|
5
|
+
import Input, { InputType } from "../Input/Input";
|
|
6
|
+
import Modal, { ModalWidth } from "../Modal/Modal";
|
|
7
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
8
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
9
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
10
|
+
import React, { ReactElement, useMemo, useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
DragDropContext,
|
|
13
|
+
Draggable,
|
|
14
|
+
DraggableProvided,
|
|
15
|
+
Droppable,
|
|
16
|
+
DroppableProvided,
|
|
17
|
+
DropResult,
|
|
18
|
+
} from "react-beautiful-dnd";
|
|
19
|
+
|
|
20
|
+
export interface ComponentProps<
|
|
21
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
22
|
+
> {
|
|
23
|
+
// Every customizable column, already in the order the table renders them.
|
|
24
|
+
columns: Array<CustomizableColumn<TBaseModel>>;
|
|
25
|
+
onSave: (columns: Array<CustomizableColumn<TBaseModel>>) => void;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
// Drop the stored layout and go back to what the table ships with.
|
|
28
|
+
onReset: () => void;
|
|
29
|
+
title?: string | undefined;
|
|
30
|
+
description?: string | undefined;
|
|
31
|
+
/*
|
|
32
|
+
* False once the viewer has customized anything, which is when Reset earns
|
|
33
|
+
* its place in the footer.
|
|
34
|
+
*/
|
|
35
|
+
isDefaultLayout?: boolean | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ColumnCustomizationModalFunction = <
|
|
39
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
40
|
+
>(
|
|
41
|
+
props: ComponentProps<TBaseModel>,
|
|
42
|
+
) => ReactElement;
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* The "Customize Columns" picker: a checkbox and a drag handle per column.
|
|
46
|
+
*
|
|
47
|
+
* Two constraints shape it:
|
|
48
|
+
*
|
|
49
|
+
* - The last visible column cannot be switched off. A table with no columns
|
|
50
|
+
* renders as an empty shell with no header row, and the only control that
|
|
51
|
+
* could undo that is the one the viewer just closed.
|
|
52
|
+
*
|
|
53
|
+
* - Reordering is offered through Up/Down buttons as well as drag. Drag alone
|
|
54
|
+
* is unusable with a keyboard or a screen reader, and pointer-only
|
|
55
|
+
* reordering is also the part that cannot be exercised in tests.
|
|
56
|
+
*
|
|
57
|
+
* Changes are staged locally and only handed back on Save, so Cancel really
|
|
58
|
+
* does cancel.
|
|
59
|
+
*/
|
|
60
|
+
const ColumnCustomizationModal: ColumnCustomizationModalFunction = <
|
|
61
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
62
|
+
>(
|
|
63
|
+
props: ComponentProps<TBaseModel>,
|
|
64
|
+
): ReactElement => {
|
|
65
|
+
const [entries, setEntries] = useState<Array<CustomizableColumn<TBaseModel>>>(
|
|
66
|
+
[...props.columns],
|
|
67
|
+
);
|
|
68
|
+
const [searchText, setSearchText] = useState<string>("");
|
|
69
|
+
|
|
70
|
+
const visibleCount: number = useMemo(() => {
|
|
71
|
+
return entries.filter((entry: CustomizableColumn<TBaseModel>) => {
|
|
72
|
+
return entry.isVisible;
|
|
73
|
+
}).length;
|
|
74
|
+
}, [entries]);
|
|
75
|
+
|
|
76
|
+
const normalizedSearch: string = searchText.trim().toLowerCase();
|
|
77
|
+
const isSearching: boolean = normalizedSearch.length > 0;
|
|
78
|
+
|
|
79
|
+
const shownEntries: Array<CustomizableColumn<TBaseModel>> = useMemo(() => {
|
|
80
|
+
if (!isSearching) {
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return entries.filter((entry: CustomizableColumn<TBaseModel>) => {
|
|
85
|
+
return (entry.column.title || "")
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.includes(normalizedSearch);
|
|
88
|
+
});
|
|
89
|
+
}, [entries, normalizedSearch, isSearching]);
|
|
90
|
+
|
|
91
|
+
type ToggleFunction = (id: string, isVisible: boolean) => void;
|
|
92
|
+
|
|
93
|
+
const toggleColumn: ToggleFunction = (
|
|
94
|
+
id: string,
|
|
95
|
+
isVisible: boolean,
|
|
96
|
+
): void => {
|
|
97
|
+
setEntries((current: Array<CustomizableColumn<TBaseModel>>) => {
|
|
98
|
+
const isLastVisible: boolean =
|
|
99
|
+
!isVisible &&
|
|
100
|
+
current.filter((entry: CustomizableColumn<TBaseModel>) => {
|
|
101
|
+
return entry.isVisible;
|
|
102
|
+
}).length <= 1;
|
|
103
|
+
|
|
104
|
+
if (isLastVisible) {
|
|
105
|
+
// Refuse rather than render a table with nothing in it.
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return current.map((entry: CustomizableColumn<TBaseModel>) => {
|
|
110
|
+
return entry.id === id ? { ...entry, isVisible } : entry;
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type MoveFunction = (fromIndex: number, toIndex: number) => void;
|
|
116
|
+
|
|
117
|
+
const moveColumn: MoveFunction = (
|
|
118
|
+
fromIndex: number,
|
|
119
|
+
toIndex: number,
|
|
120
|
+
): void => {
|
|
121
|
+
setEntries((current: Array<CustomizableColumn<TBaseModel>>) => {
|
|
122
|
+
if (
|
|
123
|
+
fromIndex === toIndex ||
|
|
124
|
+
fromIndex < 0 ||
|
|
125
|
+
toIndex < 0 ||
|
|
126
|
+
fromIndex >= current.length ||
|
|
127
|
+
toIndex >= current.length
|
|
128
|
+
) {
|
|
129
|
+
return current;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const next: Array<CustomizableColumn<TBaseModel>> = [...current];
|
|
133
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
134
|
+
|
|
135
|
+
if (!moved) {
|
|
136
|
+
return current;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
next.splice(toIndex, 0, moved);
|
|
140
|
+
|
|
141
|
+
return next;
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type OnDragEndFunction = (result: DropResult) => void;
|
|
146
|
+
|
|
147
|
+
const onDragEnd: OnDragEndFunction = (result: DropResult): void => {
|
|
148
|
+
/*
|
|
149
|
+
* Guard on the object, not on the index: a drop into the first slot has
|
|
150
|
+
* `index === 0`, which a truthiness check would throw away.
|
|
151
|
+
*/
|
|
152
|
+
if (!result.destination) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
moveColumn(result.source.index, result.destination.index);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type SetAllVisibleFunction = (isVisible: boolean) => void;
|
|
160
|
+
|
|
161
|
+
const setAllVisible: SetAllVisibleFunction = (isVisible: boolean): void => {
|
|
162
|
+
setEntries((current: Array<CustomizableColumn<TBaseModel>>) => {
|
|
163
|
+
if (!isVisible) {
|
|
164
|
+
/*
|
|
165
|
+
* "Hide all" still has to leave one column standing, so it keeps the
|
|
166
|
+
* first one - the leftmost, which is normally the row's name.
|
|
167
|
+
*/
|
|
168
|
+
let isFirstKept: boolean = false;
|
|
169
|
+
|
|
170
|
+
return current.map((entry: CustomizableColumn<TBaseModel>) => {
|
|
171
|
+
if (!isFirstKept) {
|
|
172
|
+
isFirstKept = true;
|
|
173
|
+
return { ...entry, isVisible: true };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { ...entry, isVisible: false };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return current.map((entry: CustomizableColumn<TBaseModel>) => {
|
|
181
|
+
return { ...entry, isVisible: true };
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
type GetRowFunction = (data: {
|
|
187
|
+
entry: CustomizableColumn<TBaseModel>;
|
|
188
|
+
index: number;
|
|
189
|
+
dragHandle?: ReactElement | undefined;
|
|
190
|
+
}) => ReactElement;
|
|
191
|
+
|
|
192
|
+
const getRowContents: GetRowFunction = (data: {
|
|
193
|
+
entry: CustomizableColumn<TBaseModel>;
|
|
194
|
+
index: number;
|
|
195
|
+
dragHandle?: ReactElement | undefined;
|
|
196
|
+
}): ReactElement => {
|
|
197
|
+
const { entry, index } = data;
|
|
198
|
+
|
|
199
|
+
const isOnlyVisibleColumn: boolean = entry.isVisible && visibleCount <= 1;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex items-center gap-3 px-3 py-2">
|
|
203
|
+
{data.dragHandle || <div className="w-4" />}
|
|
204
|
+
|
|
205
|
+
<div className="flex-1 min-w-0">
|
|
206
|
+
<CheckboxElement
|
|
207
|
+
value={entry.isVisible}
|
|
208
|
+
disabled={isOnlyVisibleColumn}
|
|
209
|
+
dataTestId={`column-toggle-${entry.id}`}
|
|
210
|
+
title={entry.column.title}
|
|
211
|
+
onChange={(value: boolean) => {
|
|
212
|
+
toggleColumn(entry.id, value);
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="flex flex-none items-center gap-1">
|
|
218
|
+
<Button
|
|
219
|
+
buttonSize={ButtonSize.Small}
|
|
220
|
+
buttonStyle={ButtonStyleType.ICON}
|
|
221
|
+
icon={IconProp.ChevronUp}
|
|
222
|
+
title=""
|
|
223
|
+
dataTestId={`column-move-up-${entry.id}`}
|
|
224
|
+
disabled={isSearching || index === 0}
|
|
225
|
+
onClick={() => {
|
|
226
|
+
moveColumn(index, index - 1);
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
<Button
|
|
230
|
+
buttonSize={ButtonSize.Small}
|
|
231
|
+
buttonStyle={ButtonStyleType.ICON}
|
|
232
|
+
icon={IconProp.ChevronDown}
|
|
233
|
+
title=""
|
|
234
|
+
dataTestId={`column-move-down-${entry.id}`}
|
|
235
|
+
disabled={isSearching || index === entries.length - 1}
|
|
236
|
+
onClick={() => {
|
|
237
|
+
moveColumn(index, index + 1);
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
type GetListFunction = () => ReactElement;
|
|
246
|
+
|
|
247
|
+
const getList: GetListFunction = (): ReactElement => {
|
|
248
|
+
if (shownEntries.length === 0) {
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
className="px-3 py-6 text-center text-sm text-gray-400"
|
|
252
|
+
data-testid="column-customization-no-results"
|
|
253
|
+
>
|
|
254
|
+
No columns match "{searchText}".
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/*
|
|
260
|
+
* Dragging a filtered list would move a column to a position that only
|
|
261
|
+
* makes sense inside the filter, so while a search is active the list is
|
|
262
|
+
* rendered plain and the reorder controls are disabled.
|
|
263
|
+
*/
|
|
264
|
+
if (isSearching) {
|
|
265
|
+
return (
|
|
266
|
+
<div className="divide-y divide-gray-100">
|
|
267
|
+
{shownEntries.map((entry: CustomizableColumn<TBaseModel>) => {
|
|
268
|
+
return (
|
|
269
|
+
<div key={entry.id}>
|
|
270
|
+
{getRowContents({
|
|
271
|
+
entry,
|
|
272
|
+
index: entries.indexOf(entry),
|
|
273
|
+
})}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<DragDropContext onDragEnd={onDragEnd}>
|
|
283
|
+
<Droppable droppableId="model-table-columns">
|
|
284
|
+
{(droppableProvided: DroppableProvided) => {
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
ref={droppableProvided.innerRef}
|
|
288
|
+
{...droppableProvided.droppableProps}
|
|
289
|
+
className="divide-y divide-gray-100"
|
|
290
|
+
>
|
|
291
|
+
{shownEntries.map(
|
|
292
|
+
(entry: CustomizableColumn<TBaseModel>, index: number) => {
|
|
293
|
+
return (
|
|
294
|
+
<Draggable
|
|
295
|
+
draggableId={entry.id}
|
|
296
|
+
index={index}
|
|
297
|
+
key={entry.id}
|
|
298
|
+
>
|
|
299
|
+
{(draggableProvided: DraggableProvided) => {
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
ref={draggableProvided.innerRef}
|
|
303
|
+
{...draggableProvided.draggableProps}
|
|
304
|
+
className="bg-white"
|
|
305
|
+
>
|
|
306
|
+
{getRowContents({
|
|
307
|
+
entry,
|
|
308
|
+
index,
|
|
309
|
+
dragHandle: (
|
|
310
|
+
<div
|
|
311
|
+
{...draggableProvided.dragHandleProps}
|
|
312
|
+
className="flex-none cursor-grab text-gray-400 hover:text-gray-600 active:cursor-grabbing"
|
|
313
|
+
aria-label={`Drag to reorder ${entry.column.title}`}
|
|
314
|
+
title="Drag to reorder"
|
|
315
|
+
>
|
|
316
|
+
<Icon
|
|
317
|
+
icon={IconProp.GripVertical}
|
|
318
|
+
className="h-4 w-4"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
),
|
|
322
|
+
})}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}}
|
|
326
|
+
</Draggable>
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
)}
|
|
330
|
+
{droppableProvided.placeholder}
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}}
|
|
334
|
+
</Droppable>
|
|
335
|
+
</DragDropContext>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<Modal
|
|
341
|
+
title={props.title || "Customize Columns"}
|
|
342
|
+
description={
|
|
343
|
+
props.description ||
|
|
344
|
+
"Choose which columns to show and drag them into the order you want."
|
|
345
|
+
}
|
|
346
|
+
modalWidth={ModalWidth.Medium}
|
|
347
|
+
icon={IconProp.TableCells}
|
|
348
|
+
submitButtonText="Save"
|
|
349
|
+
closeButtonText="Cancel"
|
|
350
|
+
onClose={props.onClose}
|
|
351
|
+
onSubmit={() => {
|
|
352
|
+
props.onSave(entries);
|
|
353
|
+
}}
|
|
354
|
+
leftFooterElement={
|
|
355
|
+
props.isDefaultLayout ? (
|
|
356
|
+
<></>
|
|
357
|
+
) : (
|
|
358
|
+
<Button
|
|
359
|
+
title="Reset to default"
|
|
360
|
+
buttonSize={ButtonSize.Normal}
|
|
361
|
+
buttonStyle={ButtonStyleType.OUTLINE}
|
|
362
|
+
dataTestId="column-customization-reset"
|
|
363
|
+
onClick={() => {
|
|
364
|
+
props.onReset();
|
|
365
|
+
}}
|
|
366
|
+
/>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
>
|
|
370
|
+
<div className="space-y-3" data-testid="column-customization-modal">
|
|
371
|
+
<div className="flex items-center justify-between gap-3">
|
|
372
|
+
<div className="flex-1">
|
|
373
|
+
<Input
|
|
374
|
+
type={InputType.TEXT}
|
|
375
|
+
placeholder="Search columns..."
|
|
376
|
+
value={searchText}
|
|
377
|
+
dataTestId="column-customization-search"
|
|
378
|
+
onChange={(value: string) => {
|
|
379
|
+
setSearchText(value);
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
<div className="flex flex-none gap-2">
|
|
384
|
+
<Button
|
|
385
|
+
title="Show all"
|
|
386
|
+
buttonSize={ButtonSize.Small}
|
|
387
|
+
buttonStyle={ButtonStyleType.OUTLINE}
|
|
388
|
+
dataTestId="column-customization-show-all"
|
|
389
|
+
onClick={() => {
|
|
390
|
+
setAllVisible(true);
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
<Button
|
|
394
|
+
title="Hide all"
|
|
395
|
+
buttonSize={ButtonSize.Small}
|
|
396
|
+
buttonStyle={ButtonStyleType.OUTLINE}
|
|
397
|
+
dataTestId="column-customization-hide-all"
|
|
398
|
+
onClick={() => {
|
|
399
|
+
setAllVisible(false);
|
|
400
|
+
}}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div
|
|
406
|
+
className="text-xs text-gray-500"
|
|
407
|
+
data-testid="column-customization-count"
|
|
408
|
+
>
|
|
409
|
+
{visibleCount} of {entries.length} columns shown
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div className="rounded-md border border-gray-200 overflow-hidden">
|
|
413
|
+
{getList()}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</Modal>
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export default ColumnCustomizationModal;
|