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