Open jsanko9 opened 3 months ago
Unfortunately I'm unable to solve this on my own :(
Hey @jsanko9! I'm here to help you out with any issues you're facing. I'm a bot designed to assist with bug solving and answering questions. Let me know how I can assist you!
To enable server-side pagination in a Handlebars chart in Superset, you need to configure the server_pagination
and server_page_length
controls. Here is how you can do it:
Here is the relevant code snippet from controlPanel.tsx
:
import {
ControlPanelsContainerProps,
ControlSetItem,
ControlSetRow,
} from '@superset-ui/chart-controls';
import { t } from '@superset-ui/core';
import { PAGE_SIZE_OPTIONS } from './consts';
export const serverPaginationControlSetRow: ControlSetRow = [
{
name: 'server_pagination',
config: {
type: 'CheckboxControl',
label: t('Server pagination'),
description: t(
'Enable server side pagination of results (experimental feature)',
),
default: false,
},
},
];
export const serverPageLengthControlSetItem: ControlSetItem = {
name: 'server_page_length',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Server Page Length'),
default: 10,
choices: PAGE_SIZE_OPTIONS,
description: t('Rows per page, 0 means no pagination'),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.server_pagination?.value),
},
};
Additionally, you need to handle the server-side pagination logic in your chart component. Here is an example from DataTable.tsx
:
import {
useTable,
usePagination,
useSortBy,
useGlobalFilter,
useColumnOrder,
PluginHook,
TableOptions,
FilterType,
IdType,
Row,
} from 'react-table';
import { matchSorter, rankings } from 'match-sorter';
import { typedMemo, usePrevious } from '@superset-ui/core';
import { isEqual } from 'lodash';
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
import SelectPageSize, {
SelectPageSizeProps,
SizeOption,
} from './components/SelectPageSize';
import SimplePagination from './components/Pagination';
import useSticky from './hooks/useSticky';
import { PAGE_SIZE_OPTIONS } from '../consts';
import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
export interface DataTableProps<D extends object> extends TableOptions<D> {
tableClassName?: string;
searchInput?: boolean | GlobalFilterProps<D>['searchInput'];
selectPageSize?: boolean | SelectPageSizeProps['selectRenderer'];
pageSizeOptions?: SizeOption[]; // available page size options
maxPageItemCount?: number;
hooks?: PluginHook<D>[]; // any additional hooks
width?: string | number;
height?: string | number;
serverPagination?: boolean;
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
serverPaginationData: { pageSize?: number; currentPage?: number };
pageSize?: number;
noResults?: string | ((filterString: string) => ReactNode);
sticky?: boolean;
rowCount: number;
wrapperRef?: MutableRefObject<HTMLDivElement>;
onColumnOrderChange: () => void;
renderGroupingHeaders?: () => JSX.Element;
renderTimeComparisonDropdown?: () => JSX.Element;
}
export default typedMemo(function DataTable<D extends object>({
tableClassName,
columns,
data,
serverPaginationData,
width: initialWidth = '100%',
height: initialHeight = 300,
pageSize: initialPageSize = 0,
initialState: initialState_ = {},
pageSizeOptions = PAGE_SIZE_OPTIONS,
maxPageItemCount = 9,
sticky: doSticky,
searchInput = true,
onServerPaginationChange,
rowCount,
selectPageSize,
noResults: noResultsText = 'No data found',
hooks,
serverPagination,
wrapperRef: userWrapperRef,
onColumnOrderChange,
renderGroupingHeaders,
renderTimeComparisonDropdown,
...moreUseTableOptions
}: DataTableProps<D>): JSX.Element {
const tableHooks: PluginHook<D>[] = [
useGlobalFilter,
useSortBy,
usePagination,
useColumnOrder,
doSticky ? useSticky : [],
hooks || [],
].flat();
const columnNames = Object.keys(data?.[0] || {});
const previousColumnNames = usePrevious(columnNames);
const resultsSize = serverPagination ? rowCount : data.length;
const sortByRef = useRef([]); // cache initial `sortby` so sorting doesn't trigger page reset
const pageSizeRef = useRef([initialPageSize, resultsSize]);
const hasPagination = initialPageSize > 0 && resultsSize > 0; // pageSize == 0 means no pagination
const hasGlobalControl =
hasPagination || !!searchInput || renderTimeComparisonDropdown;
const initialState = {
...initialState_,
// zero length means all pages, the `usePagination` plugin does not
// understand pageSize = 0
sortBy: sortByRef.current,
pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
};
const defaultWrapperRef = useRef<HTMLDivElement>(null);
const globalControlRef = useRef<HTMLDivElement>(null);
const paginationRef = useRef<HTMLDivElement>(null);
const wrapperRef = userWrapperRef || defaultWrapperRef;
const paginationData = JSON.stringify(serverPaginationData);
const defaultGetTableSize = useCallback(() => {
if (wrapperRef.current) {
// `initialWidth` and `initialHeight` could be also parameters like `100%`
// `Number` returns `NaN` on them, then we fallback to computed size
const width = Number(initialWidth) || wrapperRef.current.clientWidth;
const height =
(Number(initialHeight) || wrapperRef.current.clientHeight) -
(globalControlRef.current?.clientHeight || 0) -
(paginationRef.current?.clientHeight || 0);
return { width, height };
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
initialHeight,
initialWidth,
wrapperRef,
hasPagination,
hasGlobalControl,
paginationRef,
resultsSize,
paginationData,
]);
const defaultGlobalFilter: FilterType<D> = useCallback(
(rows: Row<D>[], columnIds: IdType<D>[], filterValue: string) => {
// allow searching by "col1_value col2_value"
const joinedString = (row: Row<D>) =>
columnIds.map(x => row.values[x]).join(' ');
return matchSorter(rows, filterValue, {
keys: [...columnIds, joinedString],
threshold: rankings.ACRONYM,
}) as typeof rows;
},
[],
);
const {
getTableProps,
getTableBodyProps,
prepareRow,
headerGroups,
footerGroups,
page,
pageCount,
gotoPage,
preGlobalFilteredRows,
setGlobalFilter,
setPageSize: setPageSize_,
wrapStickyTable,
setColumnOrder,
allColumns,
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
} = useTable<D>(
{
columns,
data,
initialState,
getTableSize: defaultGetTableSize,
globalFilter: defaultGlobalFilter,
sortTypes,
autoResetSortBy: !isEqual(columnNames, previousColumnNames),
...moreUseTableOptions,
},
...tableHooks,
);
// make setPageSize accept 0
const setPageSize = (size: number) => {
if (serverPagination) {
onServerPaginationChange(0, size);
}
// keep the original size if data is empty
if (size || resultsSize !== 0) {
setPageSize_(size === 0 ? resultsSize : size);
}
};
const noResults =
typeof noResultsText === 'function'
? noResultsText(filterValue as string)
: noResultsText;
const getNoResults = () => <div className="dt-no-results">{noResults}</div>;
if (!columns || columns.length === 0) {
return (
wrapStickyTable ? wrapStickyTable(getNoResults) : getNoResults()
) as JSX.Element;
}
const shouldRenderFooter = columns.some(x => !!x.Footer);
let columnBeingDragged = -1;
const onDragStart = (e: DragEvent) => {
const el = e.target as HTMLTableCellElement;
columnBeingDragged = allColumns.findIndex(
col => col.id === el.dataset.columnName,
);
e.dataTransfer.setData('text/plain', `${columnBeingDragged}`);
};
const onDrop = (e: DragEvent) => {
const el = e.target as HTMLTableCellElement;
const newPosition = allColumns.findIndex(
col => col.id === el.dataset.columnName,
);
if (newPosition !== -1) {
const currentCols = allColumns.map(c => c.id);
const colToBeMoved = currentCols.splice(columnBeingDragged, 1);
currentCols.splice(newPosition, 0, colToBeMoved[0]);
setColumnOrder(currentCols);
// toggle value in TableChart to trigger column width recalc
onColumnOrderChange();
}
e.preventDefault();
};
const renderTable = () => (
<table {...getTableProps({ className: tableClassName })}>
<thead>
{renderGroupingHeaders ? renderGroupingHeaders() : null}
{headerGroups.map(headerGroup => {
const { key: headerGroupKey, ...headerGroupProps } =
headerGroup.getHeaderGroupProps();
return (
<tr key={headerGroupKey || headerGroup.id} {...headerGroupProps}>
{headerGroup.headers.map(column =>
column.render('Header', {
key: column.id,
...column.getSortByToggleProps(),
onDragStart,
onDrop,
}),
)}
</tr>
);
})}
</thead>
<tbody {...getTableBodyProps()}>
{page && page.length > 0 ? (
page.map(row => {
prepareRow(row);
const { key: rowKey, ...rowProps } = row.getRowProps();
return (
<tr key={rowKey || row.id} {...rowProps} role="row">
{row.cells.map(cell =>
cell.render('Cell', { key: cell.column.id }),
)}
</tr>
);
})
) : (
<tr>
<td className="dt-no-results" colSpan={columns.length}>
{noResults}
</td>
</tr>
)}
</tbody>
{shouldRenderFooter && (
<tfoot>
{footerGroups.map(footerGroup => {
const { key: footerGroupKey, ...footerGroupProps } =
footerGroup.getHeaderGroupProps();
return (
<tr
key={footerGroupKey || footerGroup.id}
{...footerGroupProps}
role="row"
>
{footerGroup.headers.map(column =>
column.render('Footer', { key: column.id }),
)}
</tr>
);
})}
</tfoot>
)}
</table>
);
// force update the pageSize when it's been update from the initial state
if (
pageSizeRef.current[0] !== initialPageSize ||
// when initialPageSize stays as zero, but total number of records changed,
// we'd also need to update page size
(initialPageSize === 0 && pageSizeRef.current[1] !== resultsSize)
) {
pageSizeRef.current = [initialPageSize, resultsSize];
setPageSize(initialPageSize);
}
const paginationStyle: CSSProperties = sticky.height
? {}
: { visibility: 'hidden' };
let resultPageCount = pageCount;
let resultCurrentPageSize = pageSize;
let resultCurrentPage = pageIndex;
let resultOnPageChange: (page: number) => void = gotoPage;
if (serverPagination) {
const serverPageSize = serverPaginationData?.pageSize ?? initialPageSize;
resultPageCount = Math.ceil(rowCount / serverPageSize);
if (!Number.isFinite(resultPageCount)) {
resultPageCount = 0;
}
resultCurrentPageSize = serverPageSize;
const foundPageSizeIndex = pageSizeOptions.findIndex(
([option]) => option >= resultCurrentPageSize,
);
if (foundPageSizeIndex === -1) {
resultCurrentPageSize = 0;
}
resultCurrentPage = serverPaginationData?.currentPage ?? 0;
resultOnPageChange = (pageNumber: number) =>
onServerPaginationChange(pageNumber, serverPageSize);
}
return (
<div
ref={wrapperRef}
style={{ width: initialWidth, height: initialHeight }}
>
{hasGlobalControl ? (
<div ref={globalControlRef} className="form-inline dt-controls">
<div className="row">
<div
className={renderTimeComparisonDropdown ? 'col-sm-5' : 'col-sm-6'}
>
{hasPagination ? (
<SelectPageSize
total={resultsSize}
current={resultCurrentPageSize}
options={pageSizeOptions}
selectRenderer={
typeof selectPageSize === 'boolean'
? undefined
: selectPageSize
}
onChange={setPageSize}
/>
) : null}
</div>
{searchInput ? (
<div className="col-sm-6">
<GlobalFilter<D>
searchInput={
typeof searchInput === 'boolean' ? undefined : searchInput
}
preGlobalFilteredRows={preGlobalFilteredRows}
setGlobalFilter={setGlobalFilter}
filterValue={filterValue}
/>
</div>
) : null}
{renderTimeComparisonDropdown ? (
<div
className="col-sm-1"
style={{ float: 'right', marginTop: '6px' }}
>
{renderTimeComparisonDropdown()}
</div>
) : null}
</div>
</div>
) : null}
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
{hasPagination && resultPageCount > 1 ? (
<SimplePagination
ref={paginationRef}
style={paginationStyle}
maxPageItemCount={maxPageItemCount}
pageCount={resultPageCount}
currentPage={resultCurrentPage}
onPageChange={resultOnPageChange}
/>
) : null}
</div>
);
});
To summarize:
server_pagination
checkbox to enable server-side pagination.server_page_length
select control to set the number of rows per page, which is only visible when server pagination is enabled.This setup will allow the chart to display a pagination list, a "show entries" option menu, and re-render the viewer with updated data on page change [1][2][3][4].
@rusackas, this one's a bit out of my wheelhouse, could you step in and provide your guidance?
To continue talking to Dosu, mention @dosu.
Bug description
When enabling "SERVER PAGINATION" option in Handlebars chart, these is no impact on the chart generated.
Expected result: Pagination list and "show entries" option menu, same as for Table chart.
Each time we click on different page, I would expect to viewer re-render with updated 'data' variable.
How to reproduce the bug
Any dataset with more than 10 rows is not properly paginated in Handlebars chart
Screenshots/recordings
No response
Superset version
master / latest-dev
Python version
3.9
Node version
16
Browser
Chrome
Additional context
No response
Checklist