dosubot[bot] commented on issue #29657:
URL: https://github.com/apache/superset/issues/29657#issuecomment-2243427547

   <!-- Greeting -->
   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!
   
   <!-- Answer -->
   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:
   
   1. **Enable Server Pagination**: Add a checkbox control to enable 
server-side pagination.
   2. **Set Page Length**: Add a select control to set the number of rows per 
page.
   
   Here is the relevant code snippet from `controlPanel.tsx`:
   
   ```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`:
   
   ```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:
   - Add the `server_pagination` checkbox to enable server-side pagination.
   - Add the `server_page_length` select control to set the number of rows per 
page, which is only visible when server pagination is enabled.
   - Implement the server-side pagination logic in your chart component to 
handle data fetching and updating the data mask.
   
   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 
<sup>[[1]](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx)</sup><sup>[[2]](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx)</sup><sup>[[3]](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx)</sup><sup>[[4]](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx)</sup>.
   
   @rusackas, this one's a bit out of my wheelhouse, could you step in and 
provide your guidance?
   
   
   <!-- Dosu Comment Footer -->
   *To continue talking to [Dosu](https://dosu.dev), mention @dosu.*
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to