@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,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
+ };