feat: add drag and drop column rearrangement for table viz (#19381)
This commit is contained in:
parent
ce547f4098
commit
7e9b85f76c
|
|
@ -66,6 +66,7 @@ function loadData(
|
|||
alignPn = false,
|
||||
showCellBars = true,
|
||||
includeSearch = true,
|
||||
allowRearrangeColumns = false,
|
||||
},
|
||||
): TableChartProps {
|
||||
if (!props.queriesData || !props.queriesData[0]) return props;
|
||||
|
|
@ -86,6 +87,7 @@ function loadData(
|
|||
page_length: pageLength,
|
||||
show_cell_bars: showCellBars,
|
||||
include_search: includeSearch,
|
||||
allow_rearrange_columns: allowRearrangeColumns,
|
||||
},
|
||||
height: window.innerHeight - 130,
|
||||
};
|
||||
|
|
@ -117,8 +119,12 @@ export const BigTable = ({ width, height }) => {
|
|||
const cols = number('Columns', 8, { range: true, min: 1, max: 20 });
|
||||
const pageLength = number('Page size', 50, { range: true, min: 0, max: 100 });
|
||||
const includeSearch = boolean('Include search', true);
|
||||
const alignPn = boolean('Algin PosNeg', false);
|
||||
const alignPn = boolean('Align PosNeg', false);
|
||||
const showCellBars = boolean('Show Cell Bars', true);
|
||||
const allowRearrangeColumns = boolean(
|
||||
'Allow end user to drag-and-drop column headers to rearrange them.',
|
||||
false,
|
||||
);
|
||||
const chartProps = loadData(birthNames, {
|
||||
pageLength,
|
||||
rows,
|
||||
|
|
@ -126,6 +132,7 @@ export const BigTable = ({ width, height }) => {
|
|||
alignPn,
|
||||
showCellBars,
|
||||
includeSearch,
|
||||
allowRearrangeColumns,
|
||||
});
|
||||
return (
|
||||
<SuperChart
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
usePagination,
|
||||
useSortBy,
|
||||
useGlobalFilter,
|
||||
useColumnOrder,
|
||||
PluginHook,
|
||||
TableOptions,
|
||||
FilterType,
|
||||
|
|
@ -64,6 +65,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
|||
sticky?: boolean;
|
||||
rowCount: number;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement>;
|
||||
onColumnOrderChange: () => void;
|
||||
}
|
||||
|
||||
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||
|
|
@ -95,12 +97,14 @@ export default typedMemo(function DataTable<D extends object>({
|
|||
hooks,
|
||||
serverPagination,
|
||||
wrapperRef: userWrapperRef,
|
||||
onColumnOrderChange,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useColumnOrder,
|
||||
doSticky ? useSticky : [],
|
||||
hooks || [],
|
||||
].flat();
|
||||
|
|
@ -172,6 +176,8 @@ export default typedMemo(function DataTable<D extends object>({
|
|||
setGlobalFilter,
|
||||
setPageSize: setPageSize_,
|
||||
wrapStickyTable,
|
||||
setColumnOrder,
|
||||
allColumns,
|
||||
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
|
||||
} = useTable<D>(
|
||||
{
|
||||
|
|
@ -211,6 +217,33 @@ export default typedMemo(function DataTable<D extends object>({
|
|||
|
||||
const shouldRenderFooter = columns.some(x => !!x.Footer);
|
||||
|
||||
let columnBeingDragged = -1;
|
||||
|
||||
const onDragStart = (e: React.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: React.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>
|
||||
|
|
@ -223,6 +256,8 @@ export default typedMemo(function DataTable<D extends object>({
|
|||
column.render('Header', {
|
||||
key: column.id,
|
||||
...column.getSortByToggleProps(),
|
||||
onDragStart,
|
||||
onDrop,
|
||||
}),
|
||||
)}
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ function useInstance<D extends object>(instance: TableInstance<D>) {
|
|||
data,
|
||||
page,
|
||||
rows,
|
||||
allColumns,
|
||||
getTableSize = () => undefined,
|
||||
} = instance;
|
||||
|
||||
|
|
@ -370,7 +371,7 @@ function useInstance<D extends object>(instance: TableInstance<D>) {
|
|||
useMountedMemo(getTableSize, [getTableSize]) || sticky;
|
||||
// only change of data should trigger re-render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const table = useMemo(renderer, [page, rows]);
|
||||
const table = useMemo(renderer, [page, rows, allColumns]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!width || !height) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ import {
|
|||
UseSortByState,
|
||||
UseTableHooks,
|
||||
UseSortByHooks,
|
||||
UseColumnOrderState,
|
||||
UseColumnOrderInstanceProps,
|
||||
Renderer,
|
||||
HeaderProps,
|
||||
TableFooterProps,
|
||||
|
|
@ -64,6 +66,7 @@ declare module 'react-table' {
|
|||
UseRowSelectInstanceProps<D>,
|
||||
UseRowStateInstanceProps<D>,
|
||||
UseSortByInstanceProps<D>,
|
||||
UseColumnOrderInstanceProps<D>,
|
||||
UseStickyInstanceProps {}
|
||||
|
||||
export interface TableState<D extends object>
|
||||
|
|
@ -73,6 +76,7 @@ declare module 'react-table' {
|
|||
UsePaginationState<D>,
|
||||
UseRowSelectState<D>,
|
||||
UseSortByState<D>,
|
||||
UseColumnOrderState<D>,
|
||||
UseStickyState {}
|
||||
|
||||
// Typing from @types/react-table is incomplete
|
||||
|
|
@ -82,12 +86,19 @@ declare module 'react-table' {
|
|||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
interface TableRearrangeColumnsProps {
|
||||
onDragStart: (e: React.DragEvent) => void;
|
||||
onDrop: (e: React.DragEvent) => void;
|
||||
}
|
||||
|
||||
export interface ColumnInterface<D extends object>
|
||||
extends UseGlobalFiltersColumnOptions<D>,
|
||||
UseSortByColumnOptions<D> {
|
||||
// must define as a new property because it's not possible to override
|
||||
// the existing `Header` renderer option
|
||||
Header?: Renderer<TableSortByToggleProps & HeaderProps<D>>;
|
||||
Header?: Renderer<
|
||||
TableSortByToggleProps & HeaderProps<D> & TableRearrangeColumnsProps
|
||||
>;
|
||||
Footer?: Renderer<TableFooterProps<D>>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { CSSProperties, useCallback, useMemo } from 'react';
|
||||
import React, { CSSProperties, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ColumnInstance,
|
||||
ColumnWithLooseAccessor,
|
||||
|
|
@ -192,12 +192,16 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||
filters,
|
||||
sticky = true, // whether to use sticky header
|
||||
columnColorFormatters,
|
||||
allowRearrangeColumns = false,
|
||||
} = props;
|
||||
const timestampFormatter = useCallback(
|
||||
value => getTimeFormatterForGranularity(timeGrain)(value),
|
||||
[timeGrain],
|
||||
);
|
||||
|
||||
// keep track of whether column order changed, so that column widths can too
|
||||
const [columnOrderToggle, setColumnOrderToggle] = useState(false);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(filters: { [x: string]: DataRecordValue[] }) => {
|
||||
if (!emitFilter) {
|
||||
|
|
@ -413,7 +417,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||
// render `Cell`. This saves some time for large tables.
|
||||
return <StyledCell {...cellProps}>{text}</StyledCell>;
|
||||
},
|
||||
Header: ({ column: col, onClick, style }) => (
|
||||
Header: ({ column: col, onClick, style, onDragStart, onDrop }) => (
|
||||
<th
|
||||
title="Shift + Click to sort by multiple columns"
|
||||
className={[className, col.isSorted ? 'is-sorted' : ''].join(' ')}
|
||||
|
|
@ -422,6 +426,14 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||
...style,
|
||||
}}
|
||||
onClick={onClick}
|
||||
data-column-name={col.id}
|
||||
{...(allowRearrangeColumns && {
|
||||
draggable: 'true',
|
||||
onDragStart,
|
||||
onDragOver: e => e.preventDefault(),
|
||||
onDragEnter: e => e.preventDefault(),
|
||||
onDrop,
|
||||
})}
|
||||
>
|
||||
{/* can't use `columnWidth &&` because it may also be zero */}
|
||||
{config.columnWidth ? (
|
||||
|
|
@ -434,12 +446,13 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||
/>
|
||||
) : null}
|
||||
<div
|
||||
data-column-name={col.id}
|
||||
css={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span data-column-name={col.id}>{label}</span>
|
||||
<SortIcon column={col} />
|
||||
</div>
|
||||
</th>
|
||||
|
|
@ -469,6 +482,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||
toggleFilter,
|
||||
totals,
|
||||
columnColorFormatters,
|
||||
columnOrderToggle,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -498,6 +512,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
|||
height={height}
|
||||
serverPagination={serverPagination}
|
||||
onServerPaginationChange={handleServerPaginationChange}
|
||||
onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}
|
||||
// 9 page items in > 340px works well even for 100+ pages
|
||||
maxPageItemCount={width > 340 ? 9 : 7}
|
||||
noResults={getNoResultsMessage}
|
||||
|
|
|
|||
|
|
@ -455,6 +455,20 @@ const config: ControlPanelConfig = {
|
|||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'allow_rearrange_columns',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Allow columns to be rearranged'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
"Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.",
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'column_config',
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ const transformProps = (
|
|||
query_mode: queryMode,
|
||||
show_totals: showTotals,
|
||||
conditional_formatting: conditionalFormatting,
|
||||
allow_rearrange_columns: allowRearrangeColumns,
|
||||
} = formData;
|
||||
const timeGrain = extractTimegrain(formData);
|
||||
|
||||
|
|
@ -272,6 +273,7 @@ const transformProps = (
|
|||
onChangeFilter,
|
||||
columnColorFormatters,
|
||||
timeGrain,
|
||||
allowRearrangeColumns,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export type TableChartFormData = QueryFormData & {
|
|||
emit_filter?: boolean;
|
||||
time_grain_sqla?: TimeGranularity;
|
||||
column_config?: Record<string, ColumnConfig>;
|
||||
allow_rearrange_columns?: boolean;
|
||||
};
|
||||
|
||||
export interface TableChartProps extends ChartProps {
|
||||
|
|
@ -109,6 +110,7 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
|||
emitFilter?: boolean;
|
||||
onChangeFilter?: ChartProps['hooks']['onAddFilter'];
|
||||
columnColorFormatters?: ColorFormatters;
|
||||
allowRearrangeColumns?: boolean;
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
|
|
|||
Loading…
Reference in New Issue