@patternfly/react-core 4.247.0 → 4.248.0
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/CHANGELOG.md +19 -0
- package/dist/esm/components/Menu/Menu.d.ts.map +1 -1
- package/dist/esm/components/Menu/Menu.js +10 -3
- package/dist/esm/components/Menu/Menu.js.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/js/components/Menu/Menu.d.ts.map +1 -1
- package/dist/js/components/Menu/Menu.js +10 -3
- package/dist/js/components/Menu/Menu.js.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/umd/react-core.min.css +1942 -1942
- package/dist/umd/react-core.min.js +1 -1
- package/package.json +2 -2
- package/src/components/Menu/Menu.tsx +13 -7
- package/src/components/MenuToggle/MenuToggle.tsx +1 -0
- package/src/demos/Filters/FilterDemos.md +27 -5
- package/src/demos/Filters/examples/FilterAttributeSearch.tsx +701 -0
- package/src/demos/Filters/examples/FilterCheckboxSelect.tsx +66 -53
- package/src/demos/Filters/examples/FilterFaceted.tsx +512 -0
- package/src/demos/Filters/examples/FilterMixedSelectGroup.tsx +569 -0
- package/src/demos/Filters/examples/FilterSameSelectGroup.tsx +542 -0
- package/src/demos/Filters/examples/FilterSearchInput.tsx +91 -68
- package/src/demos/Filters/examples/FilterSingleSelect.tsx +63 -52
- package/src/demos/Page.md +47 -0
- package/src/demos/examples/DashboardWrapper.js +3 -1
- package/src/demos/examples/Page/PageStickySectionBreadcrumb.tsx +305 -0
- package/src/demos/examples/Page/PageStickySectionGroup.tsx +306 -0
- package/src/demos/examples/Page/PageStickySectionGroupAlternate.tsx +301 -0
- package/src/demos/examples/Page/PageStickySectionGroupUsingPageHeader.tsx +293 -0
- package/src/demos/Page/Page.md +0 -1184
- package/src/demos/Page/imgAvatar.svg +0 -33
- package/src/demos/Page/imgBrand.svg +0 -1
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Toolbar,
|
|
4
|
+
ToolbarContent,
|
|
5
|
+
ToolbarItem,
|
|
6
|
+
Menu,
|
|
7
|
+
MenuContent,
|
|
8
|
+
MenuList,
|
|
9
|
+
MenuItem,
|
|
10
|
+
MenuToggle,
|
|
11
|
+
MenuToggleCheckbox,
|
|
12
|
+
Popper,
|
|
13
|
+
Pagination,
|
|
14
|
+
EmptyState,
|
|
15
|
+
EmptyStateIcon,
|
|
16
|
+
Title,
|
|
17
|
+
EmptyStateBody,
|
|
18
|
+
EmptyStatePrimary,
|
|
19
|
+
Button,
|
|
20
|
+
Bullseye,
|
|
21
|
+
Badge,
|
|
22
|
+
ToolbarFilter,
|
|
23
|
+
ToolbarToggleGroup
|
|
24
|
+
} from '@patternfly/react-core';
|
|
25
|
+
import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
|
|
26
|
+
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
|
|
27
|
+
import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
|
|
28
|
+
|
|
29
|
+
interface Repository {
|
|
30
|
+
name: string;
|
|
31
|
+
threads: string;
|
|
32
|
+
apps: string;
|
|
33
|
+
workspaces: string;
|
|
34
|
+
status: string;
|
|
35
|
+
location: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// In real usage, this data would come from some external source like an API via props.
|
|
39
|
+
const repositories: Repository[] = [
|
|
40
|
+
{ name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },
|
|
41
|
+
{ name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },
|
|
42
|
+
{ name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },
|
|
43
|
+
{ name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },
|
|
44
|
+
{ name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },
|
|
45
|
+
{ name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },
|
|
46
|
+
{ name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },
|
|
47
|
+
{ name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },
|
|
48
|
+
{ name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },
|
|
49
|
+
{ name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const columnNames = {
|
|
53
|
+
name: 'Servers',
|
|
54
|
+
threads: 'Threads',
|
|
55
|
+
apps: 'Applications',
|
|
56
|
+
workspaces: 'Workspaces',
|
|
57
|
+
status: 'Status',
|
|
58
|
+
location: 'Location'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/* eslint-disable patternfly-react/no-anonymous-functions */
|
|
62
|
+
export const FilterMixedSelectGroup: React.FunctionComponent = () => {
|
|
63
|
+
// Set up repo filtering
|
|
64
|
+
const [locationSelections, setLocationSelections] = React.useState<string[]>([]);
|
|
65
|
+
const [statusSelection, setStatusSelection] = React.useState('');
|
|
66
|
+
|
|
67
|
+
const onFilter = (repo: Repository) => {
|
|
68
|
+
// Search status with status selection
|
|
69
|
+
const matchesStatusValue = repo.status.toLowerCase() === statusSelection.toLowerCase();
|
|
70
|
+
|
|
71
|
+
// Search location with location selections
|
|
72
|
+
const matchesLocationValue = locationSelections.includes(repo.location);
|
|
73
|
+
|
|
74
|
+
return (statusSelection === '' || matchesStatusValue) && (locationSelections.length === 0 || matchesLocationValue);
|
|
75
|
+
};
|
|
76
|
+
const filteredRepos = repositories.filter(onFilter);
|
|
77
|
+
|
|
78
|
+
// Set up table row selection
|
|
79
|
+
// In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.
|
|
80
|
+
// This is to prevent state from being based on row order index in case we later add sorting.
|
|
81
|
+
const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example
|
|
82
|
+
const [selectedRepoNames, setSelectedRepoNames] = React.useState<string[]>([]);
|
|
83
|
+
const setRepoSelected = (repo: Repository, isSelecting = true) =>
|
|
84
|
+
setSelectedRepoNames(prevSelected => {
|
|
85
|
+
const otherSelectedRepoNames = prevSelected.filter(r => r !== repo.name);
|
|
86
|
+
return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;
|
|
87
|
+
});
|
|
88
|
+
const selectAllRepos = (isSelecting = true) =>
|
|
89
|
+
setSelectedRepoNames(isSelecting ? filteredRepos.map(r => r.name) : []); // Selecting all should only select all currently filtered rows
|
|
90
|
+
const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;
|
|
91
|
+
const areSomeReposSelected = selectedRepoNames.length > 0;
|
|
92
|
+
const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);
|
|
93
|
+
|
|
94
|
+
// To allow shift+click to select/deselect multiple rows
|
|
95
|
+
const [recentSelectedRowIndex, setRecentSelectedRowIndex] = React.useState<number | null>(null);
|
|
96
|
+
const [shifting, setShifting] = React.useState(false);
|
|
97
|
+
|
|
98
|
+
const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {
|
|
99
|
+
// If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected
|
|
100
|
+
if (shifting && recentSelectedRowIndex !== null) {
|
|
101
|
+
const numberSelected = rowIndex - recentSelectedRowIndex;
|
|
102
|
+
const intermediateIndexes =
|
|
103
|
+
numberSelected > 0
|
|
104
|
+
? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)
|
|
105
|
+
: Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);
|
|
106
|
+
intermediateIndexes.forEach(index => setRepoSelected(repositories[index], isSelecting));
|
|
107
|
+
} else {
|
|
108
|
+
setRepoSelected(repo, isSelecting);
|
|
109
|
+
}
|
|
110
|
+
setRecentSelectedRowIndex(rowIndex);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
115
|
+
if (e.key === 'Shift') {
|
|
116
|
+
setShifting(true);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
120
|
+
if (e.key === 'Shift') {
|
|
121
|
+
setShifting(false);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
document.addEventListener('keydown', onKeyDown);
|
|
126
|
+
document.addEventListener('keyup', onKeyUp);
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
130
|
+
document.removeEventListener('keyup', onKeyUp);
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
// Set up bulk selection menu
|
|
135
|
+
const bulkSelectMenuRef = React.useRef<HTMLDivElement>(null);
|
|
136
|
+
const bulkSelectToggleRef = React.useRef<any>(null);
|
|
137
|
+
const bulkSelectContainerRef = React.useRef<HTMLDivElement>(null);
|
|
138
|
+
|
|
139
|
+
const [isBulkSelectOpen, setIsBulkSelectOpen] = React.useState<boolean>(false);
|
|
140
|
+
|
|
141
|
+
const handleBulkSelectClickOutside = (event: MouseEvent) => {
|
|
142
|
+
if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {
|
|
143
|
+
setIsBulkSelectOpen(false);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {
|
|
148
|
+
if (!isBulkSelectOpen) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
bulkSelectMenuRef.current?.contains(event.target as Node) ||
|
|
153
|
+
bulkSelectToggleRef.current?.contains(event.target as Node)
|
|
154
|
+
) {
|
|
155
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
156
|
+
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
157
|
+
bulkSelectToggleRef.current?.querySelector('button').focus();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
React.useEffect(() => {
|
|
163
|
+
window.addEventListener('keydown', handleBulkSelectMenuKeys);
|
|
164
|
+
window.addEventListener('click', handleBulkSelectClickOutside);
|
|
165
|
+
return () => {
|
|
166
|
+
window.removeEventListener('keydown', handleBulkSelectMenuKeys);
|
|
167
|
+
window.removeEventListener('click', handleBulkSelectClickOutside);
|
|
168
|
+
};
|
|
169
|
+
}, [isBulkSelectOpen, bulkSelectMenuRef]);
|
|
170
|
+
|
|
171
|
+
const onBulkSelectToggleClick = (ev: React.MouseEvent) => {
|
|
172
|
+
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
if (bulkSelectMenuRef.current) {
|
|
175
|
+
const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');
|
|
176
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
177
|
+
}
|
|
178
|
+
}, 0);
|
|
179
|
+
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let menuToggleCheckmark: boolean | null = false;
|
|
183
|
+
if (areAllReposSelected) {
|
|
184
|
+
menuToggleCheckmark = true;
|
|
185
|
+
} else if (areSomeReposSelected) {
|
|
186
|
+
menuToggleCheckmark = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const bulkSelectToggle = (
|
|
190
|
+
<MenuToggle
|
|
191
|
+
ref={bulkSelectToggleRef}
|
|
192
|
+
onClick={onBulkSelectToggleClick}
|
|
193
|
+
isExpanded={isBulkSelectOpen}
|
|
194
|
+
splitButtonOptions={{
|
|
195
|
+
items: [
|
|
196
|
+
<MenuToggleCheckbox
|
|
197
|
+
id="mixed-group-input-bulk-select"
|
|
198
|
+
key="mixed-group-input-bulk-select"
|
|
199
|
+
aria-label="Select all"
|
|
200
|
+
isChecked={menuToggleCheckmark}
|
|
201
|
+
onChange={(checked, _event) => selectAllRepos(checked)}
|
|
202
|
+
/>
|
|
203
|
+
]
|
|
204
|
+
}}
|
|
205
|
+
aria-label="Full table selection checkbox"
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const bulkSelectMenu = (
|
|
210
|
+
<Menu
|
|
211
|
+
id="mixed-group-input-bulk-select"
|
|
212
|
+
ref={bulkSelectMenuRef}
|
|
213
|
+
onSelect={(_ev, itemId) => {
|
|
214
|
+
selectAllRepos(itemId === 1 || itemId === 2);
|
|
215
|
+
setIsBulkSelectOpen(!isBulkSelectOpen);
|
|
216
|
+
bulkSelectToggleRef.current?.querySelector('button').focus();
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<MenuContent>
|
|
220
|
+
<MenuList>
|
|
221
|
+
<MenuItem itemId={0}>Select none (0 items)</MenuItem>
|
|
222
|
+
<MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>
|
|
223
|
+
<MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>
|
|
224
|
+
</MenuList>
|
|
225
|
+
</MenuContent>
|
|
226
|
+
</Menu>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const toolbarBulkSelect = (
|
|
230
|
+
<div ref={bulkSelectContainerRef}>
|
|
231
|
+
<Popper
|
|
232
|
+
trigger={bulkSelectToggle}
|
|
233
|
+
popper={bulkSelectMenu}
|
|
234
|
+
appendTo={bulkSelectContainerRef.current || undefined}
|
|
235
|
+
isVisible={isBulkSelectOpen}
|
|
236
|
+
popperMatchesTriggerWidth={false}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Set up status single select
|
|
242
|
+
const [isStatusMenuOpen, setIsStatusMenuOpen] = React.useState<boolean>(false);
|
|
243
|
+
const statusToggleRef = React.useRef<HTMLButtonElement>(null);
|
|
244
|
+
const statusMenuRef = React.useRef<HTMLDivElement>(null);
|
|
245
|
+
const statusContainerRef = React.useRef<HTMLDivElement>(null);
|
|
246
|
+
|
|
247
|
+
const handleStatusMenuKeys = (event: KeyboardEvent) => {
|
|
248
|
+
if (isStatusMenuOpen && statusMenuRef.current?.contains(event.target as Node)) {
|
|
249
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
250
|
+
setIsStatusMenuOpen(!isStatusMenuOpen);
|
|
251
|
+
statusToggleRef.current?.focus();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handleStatusClickOutside = (event: MouseEvent) => {
|
|
257
|
+
if (isStatusMenuOpen && !statusMenuRef.current?.contains(event.target as Node)) {
|
|
258
|
+
setIsStatusMenuOpen(false);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
React.useEffect(() => {
|
|
263
|
+
window.addEventListener('keydown', handleStatusMenuKeys);
|
|
264
|
+
window.addEventListener('click', handleStatusClickOutside);
|
|
265
|
+
return () => {
|
|
266
|
+
window.removeEventListener('keydown', handleStatusMenuKeys);
|
|
267
|
+
window.removeEventListener('click', handleStatusClickOutside);
|
|
268
|
+
};
|
|
269
|
+
}, [isStatusMenuOpen, statusMenuRef]);
|
|
270
|
+
|
|
271
|
+
const onStatusToggleClick = (ev: React.MouseEvent) => {
|
|
272
|
+
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
273
|
+
setTimeout(() => {
|
|
274
|
+
if (statusMenuRef.current) {
|
|
275
|
+
const firstElement = statusMenuRef.current.querySelector('li > button:not(:disabled)');
|
|
276
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
277
|
+
}
|
|
278
|
+
}, 0);
|
|
279
|
+
setIsStatusMenuOpen(!isStatusMenuOpen);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
function onStatusSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {
|
|
283
|
+
if (typeof itemId === 'undefined') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
setStatusSelection(itemId.toString());
|
|
288
|
+
setIsStatusMenuOpen(!isStatusMenuOpen);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const statusToggle = (
|
|
292
|
+
<MenuToggle
|
|
293
|
+
ref={statusToggleRef}
|
|
294
|
+
onClick={onStatusToggleClick}
|
|
295
|
+
isExpanded={isStatusMenuOpen}
|
|
296
|
+
icon={<FilterIcon />}
|
|
297
|
+
style={
|
|
298
|
+
{
|
|
299
|
+
width: '200px'
|
|
300
|
+
} as React.CSSProperties
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
Status
|
|
304
|
+
</MenuToggle>
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const statusMenu = (
|
|
308
|
+
<Menu ref={statusMenuRef} id="mixed-group-status-menu" onSelect={onStatusSelect} selected={statusSelection}>
|
|
309
|
+
<MenuContent>
|
|
310
|
+
<MenuList>
|
|
311
|
+
<MenuItem itemId="Degraded">Degraded</MenuItem>
|
|
312
|
+
<MenuItem itemId="Down">Down</MenuItem>
|
|
313
|
+
<MenuItem itemId="Needs maintenance">Needs maintenance</MenuItem>
|
|
314
|
+
<MenuItem itemId="Running">Running</MenuItem>
|
|
315
|
+
<MenuItem itemId="Stopped">Stopped</MenuItem>
|
|
316
|
+
</MenuList>
|
|
317
|
+
</MenuContent>
|
|
318
|
+
</Menu>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const statusSelect = (
|
|
322
|
+
<div ref={statusContainerRef}>
|
|
323
|
+
<Popper
|
|
324
|
+
trigger={statusToggle}
|
|
325
|
+
popper={statusMenu}
|
|
326
|
+
appendTo={statusContainerRef.current || undefined}
|
|
327
|
+
isVisible={isStatusMenuOpen}
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Set up location checkbox select
|
|
333
|
+
const [isLocationMenuOpen, setIsLocationMenuOpen] = React.useState<boolean>(false);
|
|
334
|
+
const locationToggleRef = React.useRef<HTMLButtonElement>(null);
|
|
335
|
+
const locationMenuRef = React.useRef<HTMLDivElement>(null);
|
|
336
|
+
const locationContainerRef = React.useRef<HTMLDivElement>(null);
|
|
337
|
+
|
|
338
|
+
const handleLocationMenuKeys = (event: KeyboardEvent) => {
|
|
339
|
+
if (isLocationMenuOpen && locationMenuRef.current?.contains(event.target as Node)) {
|
|
340
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
341
|
+
setIsLocationMenuOpen(!isLocationMenuOpen);
|
|
342
|
+
locationToggleRef.current?.focus();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const handleLocationClickOutside = (event: MouseEvent) => {
|
|
348
|
+
if (isLocationMenuOpen && !locationMenuRef.current?.contains(event.target as Node)) {
|
|
349
|
+
setIsLocationMenuOpen(false);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
React.useEffect(() => {
|
|
354
|
+
window.addEventListener('keydown', handleLocationMenuKeys);
|
|
355
|
+
window.addEventListener('click', handleLocationClickOutside);
|
|
356
|
+
return () => {
|
|
357
|
+
window.removeEventListener('keydown', handleLocationMenuKeys);
|
|
358
|
+
window.removeEventListener('click', handleLocationClickOutside);
|
|
359
|
+
};
|
|
360
|
+
}, [isLocationMenuOpen, locationMenuRef]);
|
|
361
|
+
|
|
362
|
+
const onLocationMenuToggleClick = (ev: React.MouseEvent) => {
|
|
363
|
+
ev.stopPropagation(); // Stop handleClickOutside from handling
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
if (locationMenuRef.current) {
|
|
366
|
+
const firstElement = locationMenuRef.current.querySelector('li > button:not(:disabled)');
|
|
367
|
+
firstElement && (firstElement as HTMLElement).focus();
|
|
368
|
+
}
|
|
369
|
+
}, 0);
|
|
370
|
+
setIsLocationMenuOpen(!isLocationMenuOpen);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
function onLocationMenuSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {
|
|
374
|
+
if (typeof itemId === 'undefined') {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const itemStr = itemId.toString();
|
|
379
|
+
|
|
380
|
+
setLocationSelections(
|
|
381
|
+
locationSelections.includes(itemStr)
|
|
382
|
+
? locationSelections.filter(selection => selection !== itemStr)
|
|
383
|
+
: [itemStr, ...locationSelections]
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const locationToggle = (
|
|
388
|
+
<MenuToggle
|
|
389
|
+
ref={locationToggleRef}
|
|
390
|
+
onClick={onLocationMenuToggleClick}
|
|
391
|
+
isExpanded={isLocationMenuOpen}
|
|
392
|
+
{...(locationSelections.length > 0 && { badge: <Badge isRead>{locationSelections.length}</Badge> })}
|
|
393
|
+
icon={<FilterIcon />}
|
|
394
|
+
style={
|
|
395
|
+
{
|
|
396
|
+
width: '200px'
|
|
397
|
+
} as React.CSSProperties
|
|
398
|
+
}
|
|
399
|
+
>
|
|
400
|
+
Location
|
|
401
|
+
</MenuToggle>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const locationMenu = (
|
|
405
|
+
<Menu
|
|
406
|
+
ref={locationMenuRef}
|
|
407
|
+
id="mixed-group-location-menu"
|
|
408
|
+
onSelect={onLocationMenuSelect}
|
|
409
|
+
selected={locationSelections}
|
|
410
|
+
>
|
|
411
|
+
<MenuContent>
|
|
412
|
+
<MenuList>
|
|
413
|
+
<MenuItem hasCheck isSelected={locationSelections.includes('Bangalore')} itemId="Bangalore">
|
|
414
|
+
Bangalore
|
|
415
|
+
</MenuItem>
|
|
416
|
+
<MenuItem hasCheck isSelected={locationSelections.includes('Boston')} itemId="Boston">
|
|
417
|
+
Boston
|
|
418
|
+
</MenuItem>
|
|
419
|
+
<MenuItem hasCheck isSelected={locationSelections.includes('Brno')} itemId="Brno">
|
|
420
|
+
Brno
|
|
421
|
+
</MenuItem>
|
|
422
|
+
<MenuItem hasCheck isSelected={locationSelections.includes('Raleigh')} itemId="Raleigh">
|
|
423
|
+
Raleigh
|
|
424
|
+
</MenuItem>
|
|
425
|
+
<MenuItem hasCheck isSelected={locationSelections.includes('Westford')} itemId="Westford">
|
|
426
|
+
Westford
|
|
427
|
+
</MenuItem>
|
|
428
|
+
</MenuList>
|
|
429
|
+
</MenuContent>
|
|
430
|
+
</Menu>
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const locationSelect = (
|
|
434
|
+
<div ref={locationContainerRef}>
|
|
435
|
+
<Popper
|
|
436
|
+
trigger={locationToggle}
|
|
437
|
+
popper={locationMenu}
|
|
438
|
+
appendTo={locationContainerRef.current || undefined}
|
|
439
|
+
isVisible={isLocationMenuOpen}
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Set up pagination and toolbar
|
|
445
|
+
const toolbarPagination = (
|
|
446
|
+
<Pagination
|
|
447
|
+
titles={{ paginationTitle: 'Mixed filter group pagination' }}
|
|
448
|
+
perPageComponent="button"
|
|
449
|
+
itemCount={repositories.length}
|
|
450
|
+
perPage={10}
|
|
451
|
+
page={1}
|
|
452
|
+
widgetId="mixed-group-mock-pagination"
|
|
453
|
+
isCompact
|
|
454
|
+
/>
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const toolbar = (
|
|
458
|
+
<Toolbar
|
|
459
|
+
id="mixed-group-toolbar"
|
|
460
|
+
clearAllFilters={() => {
|
|
461
|
+
setStatusSelection('');
|
|
462
|
+
setLocationSelections([]);
|
|
463
|
+
}}
|
|
464
|
+
>
|
|
465
|
+
<ToolbarContent>
|
|
466
|
+
<ToolbarItem>{toolbarBulkSelect}</ToolbarItem>
|
|
467
|
+
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="xl">
|
|
468
|
+
<ToolbarFilter
|
|
469
|
+
chips={statusSelection !== '' ? [statusSelection] : ([] as string[])}
|
|
470
|
+
deleteChip={() => setStatusSelection('')}
|
|
471
|
+
deleteChipGroup={() => setStatusSelection('')}
|
|
472
|
+
categoryName="Status"
|
|
473
|
+
>
|
|
474
|
+
{statusSelect}
|
|
475
|
+
</ToolbarFilter>
|
|
476
|
+
<ToolbarFilter
|
|
477
|
+
chips={locationSelections}
|
|
478
|
+
deleteChip={(category, chip) => onLocationMenuSelect(undefined, chip as string)}
|
|
479
|
+
deleteChipGroup={() => setLocationSelections([])}
|
|
480
|
+
categoryName="Location"
|
|
481
|
+
>
|
|
482
|
+
{locationSelect}
|
|
483
|
+
</ToolbarFilter>
|
|
484
|
+
</ToolbarToggleGroup>
|
|
485
|
+
<ToolbarItem variant="pagination">{toolbarPagination}</ToolbarItem>
|
|
486
|
+
</ToolbarContent>
|
|
487
|
+
</Toolbar>
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const emptyState = (
|
|
491
|
+
<EmptyState>
|
|
492
|
+
<EmptyStateIcon icon={SearchIcon} />
|
|
493
|
+
<Title size="lg" headingLevel="h4">
|
|
494
|
+
No results found
|
|
495
|
+
</Title>
|
|
496
|
+
<EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>
|
|
497
|
+
<EmptyStatePrimary>
|
|
498
|
+
<Button
|
|
499
|
+
variant="link"
|
|
500
|
+
onClick={() => {
|
|
501
|
+
setStatusSelection('');
|
|
502
|
+
setLocationSelections([]);
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
Clear all filters
|
|
506
|
+
</Button>
|
|
507
|
+
</EmptyStatePrimary>
|
|
508
|
+
</EmptyState>
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
return (
|
|
512
|
+
<React.Fragment>
|
|
513
|
+
{toolbar}
|
|
514
|
+
<TableComposable aria-label="Selectable table">
|
|
515
|
+
<Thead>
|
|
516
|
+
<Tr>
|
|
517
|
+
<Th />
|
|
518
|
+
<Th width={20}>{columnNames.name}</Th>
|
|
519
|
+
<Th width={10}>{columnNames.threads}</Th>
|
|
520
|
+
<Th width={10}>{columnNames.apps}</Th>
|
|
521
|
+
<Th width={10}>{columnNames.workspaces}</Th>
|
|
522
|
+
<Th width={20}>{columnNames.status}</Th>
|
|
523
|
+
<Th width={20}>{columnNames.location}</Th>
|
|
524
|
+
</Tr>
|
|
525
|
+
</Thead>
|
|
526
|
+
<Tbody>
|
|
527
|
+
{filteredRepos.length > 0 &&
|
|
528
|
+
filteredRepos.map((repo, rowIndex) => (
|
|
529
|
+
<Tr key={repo.name}>
|
|
530
|
+
<Td
|
|
531
|
+
select={{
|
|
532
|
+
rowIndex,
|
|
533
|
+
onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),
|
|
534
|
+
isSelected: isRepoSelected(repo),
|
|
535
|
+
disable: !isRepoSelectable(repo)
|
|
536
|
+
}}
|
|
537
|
+
/>
|
|
538
|
+
<Td dataLabel={columnNames.name} modifier="truncate">
|
|
539
|
+
{repo.name}
|
|
540
|
+
</Td>
|
|
541
|
+
<Td dataLabel={columnNames.threads} modifier="truncate">
|
|
542
|
+
{repo.threads}
|
|
543
|
+
</Td>
|
|
544
|
+
<Td dataLabel={columnNames.apps} modifier="truncate">
|
|
545
|
+
{repo.apps}
|
|
546
|
+
</Td>
|
|
547
|
+
<Td dataLabel={columnNames.workspaces} modifier="truncate">
|
|
548
|
+
{repo.workspaces}
|
|
549
|
+
</Td>
|
|
550
|
+
<Td dataLabel={columnNames.status} modifier="truncate">
|
|
551
|
+
{repo.status}
|
|
552
|
+
</Td>
|
|
553
|
+
<Td dataLabel={columnNames.location} modifier="truncate">
|
|
554
|
+
{repo.location}
|
|
555
|
+
</Td>
|
|
556
|
+
</Tr>
|
|
557
|
+
))}
|
|
558
|
+
{filteredRepos.length === 0 && (
|
|
559
|
+
<Tr>
|
|
560
|
+
<Td colSpan={8}>
|
|
561
|
+
<Bullseye>{emptyState}</Bullseye>
|
|
562
|
+
</Td>
|
|
563
|
+
</Tr>
|
|
564
|
+
)}
|
|
565
|
+
</Tbody>
|
|
566
|
+
</TableComposable>
|
|
567
|
+
</React.Fragment>
|
|
568
|
+
);
|
|
569
|
+
};
|