feat: add drag and drop column rearrangement for table viz (#19381)

This commit is contained in:
stevetracvc 2022-05-24 20:10:57 -06:00 committed by GitHub
parent ce547f4098
commit 7e9b85f76c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 6 deletions

View File

@ -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

View File

@ -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>

View File

@ -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) {

View File

@ -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>>;
}

View File

@ -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}

View File

@ -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',

View File

@ -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,
};
};

View File

@ -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 {};