From 7a17104a0ee3741f8fe5c61e9bb12136f1c77e1e Mon Sep 17 00:00:00 2001 From: Udaybir Singh Date: Tue, 30 Jun 2026 15:22:27 +0530 Subject: [PATCH 1/3] feat(DataTableToolbar): add reusable toolbar component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add DataTableToolbar — a pure presentational layout shell that provisions named slots for toolbar actions: - Left section: primaryActions, secondaryActions - Right section: filter, search, columnVisibility, viewSwitch All slots are optional ReactNode render props. Key decisions: - Uses Sistent theme tokens exclusively (no hardcoded px/rem) - Responsive via theme.breakpoints.down('sm') — no useMediaQuery - Accepts sx prop for consumer overrides - Replaces ToolWrapper's fixed height with flexible minHeight Part of meshery/meshery#18965 follow-up. Signed-off-by: Udaybir Singh --- src/__testing__/DataTableToolbar.test.tsx | 64 +++++++++++++++++++ .../DataTableToolbar/DataTableToolbar.tsx | 62 ++++++++++++++++++ .../DataTableToolbar.types.ts | 24 +++++++ src/custom/DataTableToolbar/index.tsx | 2 + src/custom/index.tsx | 3 + 5 files changed, 155 insertions(+) create mode 100644 src/__testing__/DataTableToolbar.test.tsx create mode 100644 src/custom/DataTableToolbar/DataTableToolbar.tsx create mode 100644 src/custom/DataTableToolbar/DataTableToolbar.types.ts create mode 100644 src/custom/DataTableToolbar/index.tsx diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx new file mode 100644 index 000000000..b913d27a1 --- /dev/null +++ b/src/__testing__/DataTableToolbar.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { DataTableToolbar } from '../custom/DataTableToolbar'; +import { SistentThemeProvider } from '../theme'; + +const renderWithTheme = (ui: React.ReactElement) => + render({ui}); + +describe('DataTableToolbar', () => { + it('renders primaryActions content', () => { + renderWithTheme(Add} />); + expect(screen.getByRole('button', { name: 'Add' })).toBeTruthy(); + }); + + it('renders secondaryActions content', () => { + renderWithTheme(Export} />); + expect(screen.getByRole('button', { name: 'Export' })).toBeTruthy(); + }); + + it('renders search slot', () => { + renderWithTheme(} />); + expect(screen.getByPlaceholderText('Search')).toBeTruthy(); + }); + + it('renders filter slot', () => { + renderWithTheme(Filter} />); + expect(screen.getByText('Filter')).toBeTruthy(); + }); + + it('renders columnVisibility slot', () => { + renderWithTheme(Columns} />); + expect(screen.getByText('Columns')).toBeTruthy(); + }); + + it('renders viewSwitch slot', () => { + renderWithTheme(Grid/Table} />); + expect(screen.getByText('Grid/Table')).toBeTruthy(); + }); + + it('renders all slots simultaneously', () => { + renderWithTheme( + Add} + search={} + filter={
Filter
} + /> + ); + expect(screen.getByRole('button', { name: 'Add' })).toBeTruthy(); + expect(screen.getByPlaceholderText('Search')).toBeTruthy(); + expect(screen.getByText('Filter')).toBeTruthy(); + }); + + it('renders without any props (empty state)', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeTruthy(); + }); + + it('applies custom sx styles', () => { + const { container } = renderWithTheme(); + const root = container.firstChild as HTMLElement; + expect(root).toBeTruthy(); + }); +}); diff --git a/src/custom/DataTableToolbar/DataTableToolbar.tsx b/src/custom/DataTableToolbar/DataTableToolbar.tsx new file mode 100644 index 000000000..06965a920 --- /dev/null +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -0,0 +1,62 @@ +import { Box } from '../../base'; +import { styled } from '../../theme'; +import type { DataTableToolbarProps } from './DataTableToolbar.types'; + +const ToolbarRoot = styled(Box)(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing(4), + minHeight: theme.spacing(8), + padding: theme.spacing(1.5), + backgroundColor: theme.palette.background.card, + borderRadius: theme.spacing(1), + boxShadow: theme.shadows[2], + zIndex: 101, + + [theme.breakpoints.down('sm')]: { + height: 'auto', + flexWrap: 'wrap', + padding: theme.spacing(1), + gap: theme.spacing(1) + } +})); + +const Section = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1) +})); + +export function DataTableToolbar({ + primaryActions, + secondaryActions, + search, + filter, + columnVisibility, + viewSwitch, + sx +}: DataTableToolbarProps): JSX.Element { + const hasLeftContent = Boolean(primaryActions) || Boolean(secondaryActions); + const hasRightContent = + Boolean(filter) || Boolean(search) || Boolean(columnVisibility) || Boolean(viewSwitch); + + return ( + + {hasLeftContent && ( +
+ {primaryActions} + {secondaryActions} +
+ )} + {hasRightContent && ( +
+ {filter} + {search} + {columnVisibility} + {viewSwitch} +
+ )} +
+ ); +} diff --git a/src/custom/DataTableToolbar/DataTableToolbar.types.ts b/src/custom/DataTableToolbar/DataTableToolbar.types.ts new file mode 100644 index 000000000..9eaf9955f --- /dev/null +++ b/src/custom/DataTableToolbar/DataTableToolbar.types.ts @@ -0,0 +1,24 @@ +import type { SxProps, Theme } from '@mui/material'; + +export interface DataTableToolbarProps { + /** Left side: primary action buttons (Add, Create, Import) */ + primaryActions?: React.ReactNode; + + /** Left side next to primary: secondary actions (Export, bulk delete) */ + secondaryActions?: React.ReactNode; + + /** Right side: SearchBar component */ + search?: React.ReactNode; + + /** Right side: UniversalFilter component */ + filter?: React.ReactNode; + + /** Right side: Column visibility control */ + columnVisibility?: React.ReactNode; + + /** Right side: Grid/table view toggle */ + viewSwitch?: React.ReactNode; + + /** Custom styles for migration compatibility */ + sx?: SxProps; +} diff --git a/src/custom/DataTableToolbar/index.tsx b/src/custom/DataTableToolbar/index.tsx new file mode 100644 index 000000000..47a241fc8 --- /dev/null +++ b/src/custom/DataTableToolbar/index.tsx @@ -0,0 +1,2 @@ +export { DataTableToolbar } from './DataTableToolbar'; +export type { DataTableToolbarProps } from './DataTableToolbar.types'; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 51a104ce5..074966387 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -14,6 +14,7 @@ import { } from './CustomColumnVisibilityControl/CustomColumnVisibilityControl'; import { CustomImage } from './CustomImage'; import { CustomTooltip, InfoTooltip } from './CustomTooltip'; +import { DataTableToolbar } from './DataTableToolbar'; import { CustomDialog, StyledDialogActions, @@ -94,6 +95,7 @@ export { CustomImage, CustomTooltip, DataTableEllipsisMenu, + DataTableToolbar, EmptyState, EmptyStateCard, ErrorBoundary, @@ -154,6 +156,7 @@ export type { CustomColumn, CustomColumnVisibilityControlProps, CustomDialogProps, + DataTableToolbarProps, FlipCardProps, IPopperListener, ResponsiveDataTableProps, From 8e0caca7e0f21b2baf1881f4437a06e7bd69237e Mon Sep 17 00:00:00 2001 From: Udaybir Singh Date: Tue, 30 Jun 2026 15:33:46 +0530 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Udaybir Singh --- src/__testing__/DataTableToolbar.test.tsx | 2 +- src/custom/DataTableToolbar/DataTableToolbar.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx index b913d27a1..4de42a409 100644 --- a/src/__testing__/DataTableToolbar.test.tsx +++ b/src/__testing__/DataTableToolbar.test.tsx @@ -59,6 +59,6 @@ describe('DataTableToolbar', () => { it('applies custom sx styles', () => { const { container } = renderWithTheme(); const root = container.firstChild as HTMLElement; - expect(root).toBeTruthy(); + expect(root).toHaveStyle('margin-top: 32px'); }); }); diff --git a/src/custom/DataTableToolbar/DataTableToolbar.tsx b/src/custom/DataTableToolbar/DataTableToolbar.tsx index 06965a920..44b956668 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.tsx +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -12,7 +12,6 @@ const ToolbarRoot = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.background.card, borderRadius: theme.spacing(1), boxShadow: theme.shadows[2], - zIndex: 101, [theme.breakpoints.down('sm')]: { height: 'auto', From b562e5b43b110fce1756cc803887af0398ea98cb Mon Sep 17 00:00:00 2001 From: Udaybir Singh Date: Thu, 2 Jul 2026 23:35:26 +0530 Subject: [PATCH 3/3] feat: update SearchBar and ResponsiveDataTable styles for improved layout Signed-off-by: Udaybir Singh --- src/__testing__/DataTableToolbar.test.tsx | 24 ++++++++++++- .../DataTableToolbar/DataTableToolbar.tsx | 21 ++++++----- .../DataTableToolbar.types.ts | 3 ++ src/custom/ResponsiveDataTable.tsx | 2 +- src/custom/SearchBar.tsx | 2 +- src/custom/UniversalFilter.tsx | 35 ++++++++++++++----- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx index 4de42a409..7893344cc 100644 --- a/src/__testing__/DataTableToolbar.test.tsx +++ b/src/__testing__/DataTableToolbar.test.tsx @@ -38,19 +38,41 @@ describe('DataTableToolbar', () => { expect(screen.getByText('Grid/Table')).toBeTruthy(); }); - it('renders all slots simultaneously', () => { + it('renders bulkOperations content in right section', () => { + renderWithTheme(Select All} />); + expect(screen.getByRole('button', { name: 'Select All' })).toBeTruthy(); + }); + + it('renders all slots simultaneously with bulkOperations', () => { renderWithTheme( Add} + secondaryActions={} + bulkOperations={} search={} filter={
Filter
} /> ); expect(screen.getByRole('button', { name: 'Add' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Export' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Select All' })).toBeTruthy(); expect(screen.getByPlaceholderText('Search')).toBeTruthy(); expect(screen.getByText('Filter')).toBeTruthy(); }); + it('renders search before filter in DOM order', () => { + const { container } = renderWithTheme( + Search} + filter={Filter} + /> + ); + const children = container.querySelectorAll('[data-testid="search"], [data-testid="filter"]'); + expect(children.length).toBe(2); + expect(children[0].getAttribute('data-testid')).toBe('search'); + expect(children[1].getAttribute('data-testid')).toBe('filter'); + }); + it('renders without any props (empty state)', () => { const { container } = renderWithTheme(); expect(container.firstChild).toBeTruthy(); diff --git a/src/custom/DataTableToolbar/DataTableToolbar.tsx b/src/custom/DataTableToolbar/DataTableToolbar.tsx index 44b956668..4a73b3558 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.tsx +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -30,28 +30,31 @@ const Section = styled(Box)(({ theme }) => ({ export function DataTableToolbar({ primaryActions, secondaryActions, + bulkOperations, search, filter, columnVisibility, viewSwitch, sx }: DataTableToolbarProps): JSX.Element { - const hasLeftContent = Boolean(primaryActions) || Boolean(secondaryActions); + const hasLeftContent = Boolean(primaryActions); const hasRightContent = - Boolean(filter) || Boolean(search) || Boolean(columnVisibility) || Boolean(viewSwitch); + Boolean(bulkOperations) || + Boolean(secondaryActions) || + Boolean(filter) || + Boolean(search) || + Boolean(columnVisibility) || + Boolean(viewSwitch); return ( - {hasLeftContent && ( -
- {primaryActions} - {secondaryActions} -
- )} + {hasLeftContent &&
{primaryActions}
} {hasRightContent && (
- {filter} + {bulkOperations} + {secondaryActions} {search} + {filter} {columnVisibility} {viewSwitch}
diff --git a/src/custom/DataTableToolbar/DataTableToolbar.types.ts b/src/custom/DataTableToolbar/DataTableToolbar.types.ts index 9eaf9955f..54d6b0ed6 100644 --- a/src/custom/DataTableToolbar/DataTableToolbar.types.ts +++ b/src/custom/DataTableToolbar/DataTableToolbar.types.ts @@ -7,6 +7,9 @@ export interface DataTableToolbarProps { /** Left side next to primary: secondary actions (Export, bulk delete) */ secondaryActions?: React.ReactNode; + /** Right side: bulk action controls (select all, batch delete) */ + bulkOperations?: React.ReactNode; + /** Right side: SearchBar component */ search?: React.ReactNode; diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index 9cfb6f0f7..9678cf7d4 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -12,7 +12,7 @@ import { TooltipIcon } from './TooltipIconButton'; export const IconWrapper = styled('div', { shouldForwardProp: (prop) => prop !== 'disabled' })<{ disabled?: boolean }>(({ disabled = false }) => ({ - width: '100%', + width: 'auto', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? '0.5' : '1', display: 'flex', diff --git a/src/custom/SearchBar.tsx b/src/custom/SearchBar.tsx index 8558b2725..099a0d49c 100644 --- a/src/custom/SearchBar.tsx +++ b/src/custom/SearchBar.tsx @@ -153,7 +153,7 @@ function SearchBar({ placeholder={placeholder} data-testid="searchbar-input" style={{ - width: expanded ? '150px' : '0', + width: expanded ? '15rem' : '0', opacity: expanded ? 1 : 0, transition: 'width 0.3s ease, opacity 0.3s ease' }} diff --git a/src/custom/UniversalFilter.tsx b/src/custom/UniversalFilter.tsx index 345f65f4d..0a71645ff 100644 --- a/src/custom/UniversalFilter.tsx +++ b/src/custom/UniversalFilter.tsx @@ -1,7 +1,7 @@ import { Drawer, styled, useMediaQuery } from '@mui/material'; import { SelectChangeEvent } from '@mui/material/Select'; import React from 'react'; -import { Button } from '../base/Button'; +import { Badge, Button } from '../base'; import { ClickAwayListener } from '../base/ClickAwayListener'; import { InputLabel } from '../base/InputLabel'; import { MenuItem } from '../base/MenuItem'; @@ -10,7 +10,6 @@ import { Select } from '../base/Select'; import { FilterIcon } from '../icons'; import { useTheme } from '../theme'; import PopperListener from './PopperListener'; -import { TooltipIcon } from './TooltipIconButton'; export interface FilterColumn { name: string; @@ -176,15 +175,35 @@ function UniversalFilter({ ); + const activeFilterCount = Object.values(selectedFilters).filter( + (value) => value && value !== 'All' + ).length; + return ( <>
- } - arrow - /> + t.typography.caption.fontSize, + height: 20, + minWidth: 20 + } + }} + > + + {!isMobile ? (