feat(viz-type): Ag grid table plugin Integration (#33517)

Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Amaan Nawab <nelsondrew07@gmail.com>
Co-authored-by: Levis Mbote <111055098+LevisNgigi@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Paul Rhodes <withnale@users.noreply.github.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
This commit is contained in:
amaannawab923
2025-07-07 19:20:44 +05:30
committed by GitHub
parent 0fc4119728
commit 0a5941edd7
47 changed files with 5468 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable theme-colors/no-literal-colors */
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useRef, useState } from 'react';
import { t } from '@superset-ui/core';
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import FilterIcon from './Filter';
import KebabMenu from './KebabMenu';
import {
CustomColDef,
CustomHeaderParams,
SortState,
UserProvidedColDef,
} from '../../types';
import CustomPopover from './CustomPopover';
import {
Container,
FilterIconWrapper,
HeaderContainer,
HeaderLabel,
MenuContainer,
SortIconWrapper,
} from '../../styles';
const getSortIcon = (sortState: SortState[], colId: string | null) => {
if (!sortState?.length || !colId) return null;
const { colId: currentCol, sort } = sortState[0];
if (currentCol === colId) {
return sort === 'asc' ? (
<ArrowUpOutlined />
) : sort === 'desc' ? (
<ArrowDownOutlined />
) : null;
}
return null;
};
const CustomHeader: React.FC<CustomHeaderParams> = ({
displayName,
enableSorting,
setSort,
context,
column,
api,
}) => {
const { initialSortState, onColumnHeaderClicked } = context;
const colId = column?.getColId();
const colDef = column?.getColDef() as CustomColDef;
const userColDef = column.getUserProvidedColDef() as UserProvidedColDef;
const isPercentMetric = colDef?.context?.isPercentMetric;
const [isFilterVisible, setFilterVisible] = useState(false);
const [isMenuVisible, setMenuVisible] = useState(false);
const filterRef = useRef<HTMLDivElement>(null);
const isFilterActive = column?.isFilterActive();
const currentSort = initialSortState?.[0];
const isMain = userColDef?.isMain;
const isTimeComparison = !isMain && userColDef?.timeComparisonKey;
const sortKey = isMain ? colId.replace('Main', '').trim() : colId;
// Sorting logic
const clearSort = () => {
onColumnHeaderClicked({ column: { colId: sortKey, sort: null } });
setSort(null, false);
};
const applySort = (direction: 'asc' | 'desc') => {
onColumnHeaderClicked({ column: { colId: sortKey, sort: direction } });
setSort(direction, false);
};
const getNextSortDirection = (): 'asc' | 'desc' | null => {
if (currentSort?.colId !== colId) return 'asc';
if (currentSort?.sort === 'asc') return 'desc';
return null;
};
const toggleSort = () => {
if (!enableSorting || isTimeComparison) return;
const next = getNextSortDirection();
if (next) applySort(next);
else clearSort();
};
const handleFilterClick = async (e: React.MouseEvent) => {
e.stopPropagation();
setFilterVisible(!isFilterVisible);
const filterInstance = await api.getColumnFilterInstance<any>(column);
const filterEl = filterInstance?.eGui;
if (filterEl && filterRef.current) {
filterRef.current.innerHTML = '';
filterRef.current.appendChild(filterEl);
}
};
const handleMenuClick = (e: React.MouseEvent) => {
e.stopPropagation();
setMenuVisible(!isMenuVisible);
};
const isCurrentColSorted = currentSort?.colId === colId;
const currentDirection = isCurrentColSorted ? currentSort?.sort : null;
const shouldShowAsc =
!isTimeComparison && (!currentDirection || currentDirection === 'desc');
const shouldShowDesc =
!isTimeComparison && (!currentDirection || currentDirection === 'asc');
const menuContent = (
<MenuContainer>
{shouldShowAsc && (
<div onClick={() => applySort('asc')} className="menu-item">
<ArrowUpOutlined /> {t('Sort Ascending')}
</div>
)}
{shouldShowDesc && (
<div onClick={() => applySort('desc')} className="menu-item">
<ArrowDownOutlined /> {t('Sort Descending')}
</div>
)}
{currentSort && currentSort?.colId === colId && (
<div onClick={clearSort} className="menu-item">
<span style={{ fontSize: 16 }}></span> {t('Clear Sort')}
</div>
)}
</MenuContainer>
);
return (
<Container>
<HeaderContainer onClick={toggleSort} className="custom-header">
<HeaderLabel>{displayName}</HeaderLabel>
<SortIconWrapper>
{getSortIcon(initialSortState, colId)}
</SortIconWrapper>
</HeaderContainer>
<CustomPopover
content={<div ref={filterRef} />}
isOpen={isFilterVisible}
onClose={() => setFilterVisible(false)}
>
<FilterIconWrapper
className="header-filter"
onClick={handleFilterClick}
isFilterActive={isFilterActive}
>
<FilterIcon />
</FilterIconWrapper>
</CustomPopover>
{!isPercentMetric && !isTimeComparison && (
<CustomPopover
content={menuContent}
isOpen={isMenuVisible}
onClose={() => setMenuVisible(false)}
>
<div className="three-dots-menu" onClick={handleMenuClick}>
<KebabMenu />
</div>
</CustomPopover>
)}
</Container>
);
};
export default CustomHeader;

View File

@@ -0,0 +1,104 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useRef, useState, cloneElement } from 'react';
import { PopoverContainer, PopoverWrapper } from '../../styles';
interface Props {
content: React.ReactNode;
children: React.ReactElement;
isOpen: boolean;
onClose: () => void;
}
const CustomPopover: React.FC<Props> = ({
content,
children,
isOpen,
onClose,
}) => {
const [position, setPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const updatePosition = () => {
const rect = triggerRef.current?.getBoundingClientRect();
if (rect) {
const popoverWidth = popoverRef.current?.offsetWidth || 200;
const windowWidth = window.innerWidth;
const rightEdgePosition = rect.left + 10 + 160 + popoverWidth;
// Check if popover would spill out of the window
const shouldUseOffset = rightEdgePosition <= windowWidth;
setPosition({
top: rect.bottom + 8,
left: Math.max(
0,
rect.right -
(popoverRef.current?.offsetWidth || 0) +
(shouldUseOffset ? 170 : 0),
),
});
}
};
if (isOpen) {
updatePosition();
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('scroll', updatePosition);
window.addEventListener('resize', updatePosition);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', updatePosition);
window.removeEventListener('resize', updatePosition);
};
}, [isOpen]);
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
!triggerRef.current?.contains(event.target as Node)
) {
onClose();
}
};
return (
<PopoverWrapper>
{cloneElement(children, { ref: triggerRef })}
{isOpen && (
<PopoverContainer
ref={popoverRef}
style={{
top: `${position.top}px`,
left: `${position.left}px`,
}}
>
{content}
</PopoverContainer>
)}
</PopoverWrapper>
);
};
export default CustomPopover;

View File

@@ -0,0 +1,27 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const FilterIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<rect x="3" y="6" width="18" height="2" rx="1" />
<rect x="6" y="11" width="12" height="2" rx="1" />
<rect x="9" y="16" width="6" height="2" rx="1" />
</svg>
);
export default FilterIcon;

View File

@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const KebabMenu = ({ size = 14 }) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="8" cy="3" r="1.2" />
<circle cx="8" cy="8" r="1.2" />
<circle cx="8" cy="13" r="1.2" />
</svg>
);
export default KebabMenu;

View File

@@ -0,0 +1,139 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable theme-colors/no-literal-colors */
import { t } from '@superset-ui/core';
import {
VerticalLeftOutlined,
VerticalRightOutlined,
LeftOutlined,
RightOutlined,
} from '@ant-design/icons';
import { Select } from '@superset-ui/core/components';
import {
PaginationContainer,
SelectWrapper,
PageInfo,
PageCount,
PageButton,
ButtonGroup,
} from '../../styles';
interface PaginationProps {
currentPage: number;
pageSize: number;
totalRows: number;
pageSizeOptions: number[];
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
onServerPageSizeChange: (pageSize: number) => void;
sliceId: number;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage = 0,
pageSize = 10,
totalRows = 0,
pageSizeOptions = [10, 20, 50, 100, 200],
onServerPaginationChange = () => {},
onServerPageSizeChange = () => {},
sliceId,
}) => {
const totalPages = Math.ceil(totalRows / pageSize);
const startRow = currentPage * pageSize + 1;
const endRow = Math.min((currentPage + 1) * pageSize, totalRows);
const handleNextPage = (disabled: boolean) => () => {
if (disabled) return;
onServerPaginationChange(currentPage + 1, pageSize);
};
const handlePrevPage = (disabled: boolean) => () => {
if (disabled) return;
onServerPaginationChange(currentPage - 1, pageSize);
};
const handleNavigateToFirstPage = (disabled: boolean) => () => {
if (disabled) return;
onServerPaginationChange(0, pageSize);
};
const handleNavigateToLastPage = (disabled: boolean) => () => {
if (disabled) return;
onServerPaginationChange(totalPages - 1, pageSize);
};
const selectOptions = pageSizeOptions.map(size => ({
value: size,
label: size,
}));
return (
<PaginationContainer>
<span>{t('Page Size:')}</span>
<SelectWrapper>
<Select
value={`${pageSize}`}
options={selectOptions}
onChange={(value: string) => {
onServerPageSizeChange(Number(value));
}}
getPopupContainer={() =>
document.getElementById(`chart-id-${sliceId}`) as HTMLElement
}
/>
</SelectWrapper>
<PageInfo>
<span>{startRow}</span> {t('to')} <span>{endRow}</span> {t('of')}{' '}
<span>{totalRows}</span>
</PageInfo>
<ButtonGroup>
<PageButton
onClick={handleNavigateToFirstPage(currentPage === 0)}
disabled={currentPage === 0}
>
<VerticalRightOutlined />
</PageButton>
<PageButton
onClick={handlePrevPage(currentPage === 0)}
disabled={currentPage === 0}
>
<LeftOutlined />
</PageButton>
<PageCount>
{t('Page')} <span>{currentPage + 1}</span> {t('of')}{' '}
<span>{totalPages}</span>
</PageCount>
<PageButton
onClick={handleNextPage(!!(currentPage >= totalPages - 1))}
disabled={currentPage >= totalPages - 1}
>
<RightOutlined />
</PageButton>
<PageButton
onClick={handleNavigateToLastPage(!!(currentPage >= totalPages - 1))}
disabled={currentPage >= totalPages - 1}
>
<VerticalLeftOutlined />
</PageButton>
</ButtonGroup>
</PaginationContainer>
);
};
export default Pagination;

View File

@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SearchOption } from '../../types';
import { StyledSelect } from '../../styles';
interface SearchSelectDropdownProps {
/** The currently selected search column value */
value?: string;
/** Callback triggered when a new search column is selected */
onChange: (searchCol: string) => void;
/** Available search column options to populate the dropdown */
searchOptions: SearchOption[];
}
function SearchSelectDropdown({
value,
onChange,
searchOptions,
}: SearchSelectDropdownProps) {
return (
<StyledSelect
className="search-select"
value={value || (searchOptions?.[0]?.value ?? '')}
options={searchOptions}
onChange={onChange}
/>
);
}
export default SearchSelectDropdown;

View File

@@ -0,0 +1,109 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable import/no-extraneous-dependencies */
import { useState } from 'react';
import { Dropdown, Menu } from 'antd';
import { TableOutlined, DownOutlined, CheckOutlined } from '@ant-design/icons';
import { t } from '@superset-ui/core';
import { InfoText, ColumnLabel, CheckIconWrapper } from '../../styles';
interface ComparisonColumn {
key: string;
label: string;
}
interface TimeComparisonVisibilityProps {
comparisonColumns: ComparisonColumn[];
selectedComparisonColumns: string[];
onSelectionChange: (selectedColumns: string[]) => void;
}
const TimeComparisonVisibility: React.FC<TimeComparisonVisibilityProps> = ({
comparisonColumns,
selectedComparisonColumns,
onSelectionChange,
}) => {
const [showComparisonDropdown, setShowComparisonDropdown] = useState(false);
const allKey = comparisonColumns[0].key;
const handleOnClick = (data: any) => {
const { key } = data;
// Toggle 'All' key selection
if (key === allKey) {
onSelectionChange([allKey]);
} else if (selectedComparisonColumns.includes(allKey)) {
onSelectionChange([key]);
} else {
// Toggle selection for other keys
onSelectionChange(
selectedComparisonColumns.includes(key)
? selectedComparisonColumns.filter((k: string) => k !== key) // Deselect if already selected
: [...selectedComparisonColumns, key],
); // Select if not already selected
}
};
const handleOnBlur = () => {
if (selectedComparisonColumns.length === 3) {
onSelectionChange([comparisonColumns[0].key]);
}
};
return (
<Dropdown
placement="bottomRight"
visible={showComparisonDropdown}
onVisibleChange={(flag: boolean) => {
setShowComparisonDropdown(flag);
}}
overlay={
<Menu
multiple
onClick={handleOnClick}
onBlur={handleOnBlur}
selectedKeys={selectedComparisonColumns}
>
<InfoText>
{t(
'Select columns that will be displayed in the table. You can multiselect columns.',
)}
</InfoText>
{comparisonColumns.map((column: ComparisonColumn) => (
<Menu.Item key={column.key}>
<ColumnLabel>{column.label}</ColumnLabel>
<CheckIconWrapper>
{selectedComparisonColumns.includes(column.key) && (
<CheckOutlined />
)}
</CheckIconWrapper>
</Menu.Item>
))}
</Menu>
}
trigger={['click']}
>
<span>
<TableOutlined /> <DownOutlined />
</span>
</Dropdown>
);
};
export default TimeComparisonVisibility;

View File

@@ -0,0 +1,359 @@
/* eslint-disable camelcase */
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
useCallback,
useMemo,
useRef,
memo,
useState,
ChangeEvent,
useEffect,
} from 'react';
import {
AllCommunityModule,
ClientSideRowModelModule,
type ColDef,
ModuleRegistry,
GridReadyEvent,
GridState,
CellClickedEvent,
IMenuActionParams,
themeQuartz,
} from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';
import { type FunctionComponent } from 'react';
import { JsonObject, DataRecordValue, DataRecord } from '@superset-ui/core';
import { SearchOutlined } from '@ant-design/icons';
import { debounce, isEqual } from 'lodash';
import Pagination from './components/Pagination';
import SearchSelectDropdown from './components/SearchSelectDropdown';
import { SearchOption, SortByItem } from '../types';
import getInitialSortState, { shouldSort } from '../utils/getInitialSortState';
import { PAGE_SIZE_OPTIONS } from '../consts';
export interface AgGridTableProps {
gridTheme?: string;
isDarkMode?: boolean;
gridHeight?: number;
updateInterval?: number;
data?: any[];
onGridReady?: (params: GridReadyEvent) => void;
colDefsFromProps: any[];
includeSearch: boolean;
allowRearrangeColumns: boolean;
pagination: boolean;
pageSize: number;
serverPagination?: boolean;
rowCount?: number;
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
serverPaginationData: JsonObject;
onServerPageSizeChange: (pageSize: number) => void;
searchOptions: SearchOption[];
onSearchColChange: (searchCol: string) => void;
onSearchChange: (searchText: string) => void;
onSortChange: (sortBy: SortByItem[]) => void;
id: number;
percentMetrics: string[];
serverPageLength: number;
hasServerPageLengthChanged: boolean;
handleCrossFilter: (event: CellClickedEvent | IMenuActionParams) => void;
isActiveFilterValue: (key: string, val: DataRecordValue) => boolean;
renderTimeComparisonDropdown: () => JSX.Element | null;
cleanedTotals: DataRecord;
showTotals: boolean;
width: number;
}
ModuleRegistry.registerModules([AllCommunityModule, ClientSideRowModelModule]);
const isSearchFocused = new Map<string, boolean>();
const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
({
gridHeight,
data = [],
colDefsFromProps,
includeSearch,
allowRearrangeColumns,
pagination,
pageSize,
serverPagination,
rowCount,
onServerPaginationChange,
serverPaginationData,
onServerPageSizeChange,
searchOptions,
onSearchColChange,
onSearchChange,
onSortChange,
id,
percentMetrics,
serverPageLength,
hasServerPageLengthChanged,
handleCrossFilter,
isActiveFilterValue,
renderTimeComparisonDropdown,
cleanedTotals,
showTotals,
width,
}) => {
const gridRef = useRef<AgGridReact>(null);
const inputRef = useRef<HTMLInputElement>(null);
const rowData = useMemo(() => data, [data]);
const containerRef = useRef<HTMLDivElement>(null);
const searchId = `search-${id}`;
const gridInitialState: GridState = {
...(serverPagination && {
sort: {
sortModel: getInitialSortState(serverPaginationData?.sortBy || []),
},
}),
};
const defaultColDef = useMemo<ColDef>(
() => ({
flex: 1,
filter: true,
enableRowGroup: true,
enableValue: true,
sortable: true,
resizable: true,
minWidth: 100,
}),
[],
);
// Memoize container style
const containerStyles = useMemo(
() => ({
height: gridHeight,
width,
}),
[gridHeight, width],
);
const [quickFilterText, setQuickFilterText] = useState<string>();
const [searchValue, setSearchValue] = useState(
serverPaginationData?.searchText || '',
);
const debouncedSearch = useMemo(
() =>
debounce((value: string) => {
onSearchChange(value);
}, 500),
[onSearchChange],
);
useEffect(
() =>
// Cleanup debounced search
() => {
debouncedSearch.cancel();
},
[debouncedSearch],
);
useEffect(() => {
if (
serverPagination &&
isSearchFocused.get(searchId) &&
document.activeElement !== inputRef.current
) {
inputRef.current?.focus();
}
}, [searchValue, serverPagination, searchId]);
const handleSearchFocus = useCallback(() => {
isSearchFocused.set(searchId, true);
}, [searchId]);
const handleSearchBlur = useCallback(() => {
isSearchFocused.set(searchId, false);
}, [searchId]);
const onFilterTextBoxChanged = useCallback(
({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
if (serverPagination) {
setSearchValue(value);
debouncedSearch(value);
} else {
setQuickFilterText(value);
}
},
[serverPagination, debouncedSearch, searchId],
);
const handleColSort = (colId: string, sortDir: string) => {
const isSortable = shouldSort({
colId,
sortDir,
percentMetrics,
serverPagination: !!serverPagination,
gridInitialState,
});
if (!isSortable) return;
if (sortDir == null) {
onSortChange([]);
return;
}
onSortChange([
{
id: colId,
key: colId,
desc: sortDir === 'desc',
},
]);
};
const handleColumnHeaderClick = useCallback(
params => {
const colId = params?.column?.colId;
const sortDir = params?.column?.sort;
handleColSort(colId, sortDir);
},
[serverPagination, gridInitialState, percentMetrics, onSortChange],
);
useEffect(() => {
if (
hasServerPageLengthChanged &&
serverPaginationData?.pageSize &&
!isEqual(serverPaginationData?.pageSize, serverPageLength)
) {
// Explore editor handling
// if user updates server page length from control panel &
// if server page length & ownState pageSize are not equal
// they must be resynced
onServerPageSizeChange(serverPageLength);
}
}, [hasServerPageLengthChanged]);
const onGridReady = (params: GridReadyEvent) => {
// This will make columns fill the grid width
params.api.sizeColumnsToFit();
};
return (
<div
className="ag-theme-quartz"
style={containerStyles}
ref={containerRef}
>
<div className="dropdown-controls-container">
{renderTimeComparisonDropdown && (
<div className="time-comparison-dropdown">
{renderTimeComparisonDropdown()}
</div>
)}
{includeSearch && (
<div className="search-container">
{serverPagination && (
<div className="search-by-text-container">
<span className="search-by-text"> Search by :</span>
<SearchSelectDropdown
onChange={onSearchColChange}
searchOptions={searchOptions}
value={serverPaginationData?.searchColumn || ''}
/>
</div>
)}
<div className="input-wrapper">
<div className="input-container">
<SearchOutlined />
<input
ref={inputRef}
value={
serverPagination ? searchValue : quickFilterText || ''
}
type="text"
id="filter-text-box"
placeholder="Search"
onInput={onFilterTextBoxChanged}
onFocus={handleSearchFocus}
onBlur={handleSearchBlur}
/>
</div>
</div>
</div>
)}
</div>
<AgGridReact
ref={gridRef}
onGridReady={onGridReady}
theme={themeQuartz}
className="ag-container"
rowData={rowData}
headerHeight={36}
rowHeight={30}
columnDefs={colDefsFromProps}
defaultColDef={defaultColDef}
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
rowSelection="multiple"
animateRows
onCellClicked={handleCrossFilter}
initialState={gridInitialState}
suppressAggFuncInHeader
rowGroupPanelShow="always"
enableCellTextSelection
quickFilterText={serverPagination ? '' : quickFilterText}
suppressMovableColumns={!allowRearrangeColumns}
pagination={pagination}
paginationPageSize={pageSize}
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
suppressDragLeaveHidesColumns
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
context={{
onColumnHeaderClicked: handleColumnHeaderClick,
initialSortState: getInitialSortState(
serverPaginationData?.sortBy || [],
),
isActiveFilterValue,
}}
/>
{serverPagination && (
<Pagination
currentPage={serverPaginationData?.currentPage || 0}
pageSize={
hasServerPageLengthChanged
? serverPageLength
: serverPaginationData?.pageSize || 10
}
totalRows={rowCount || 0}
pageSizeOptions={[10, 20, 50, 100, 200]}
onServerPaginationChange={onServerPaginationChange}
onServerPageSizeChange={onServerPageSizeChange}
sliceId={id}
/>
)}
</div>
);
},
);
AgGridDataTable.displayName = 'AgGridDataTable';
export default memo(AgGridDataTable);