mirror of
https://github.com/apache/superset.git
synced 2026-04-27 03:55:47 +00:00
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:
@@ -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);
|
||||
Reference in New Issue
Block a user