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