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