mirror of
https://github.com/getredash/redash.git
synced 2026-03-22 10:00:17 -04:00
199 lines
6.5 KiB
JavaScript
199 lines
6.5 KiB
JavaScript
import { isNil, map, filter, each, sortBy, some, findIndex, toString } from 'lodash';
|
|
import React from 'react';
|
|
import cx from 'classnames';
|
|
import Icon from 'antd/lib/icon';
|
|
import Tooltip from 'antd/lib/tooltip';
|
|
|
|
import initTextColumn from './columns/text';
|
|
import initNumberColumn from './columns/number';
|
|
import initDateTimeColumn from './columns/datetime';
|
|
import initBooleanColumn from './columns/boolean';
|
|
import initLinkColumn from './columns/link';
|
|
import initImageColumn from './columns/image';
|
|
import initJsonColumn from './columns/json';
|
|
|
|
// this map should contain all possible values for `column.displayAs` property
|
|
export const ColumnTypes = {
|
|
string: initTextColumn,
|
|
number: initNumberColumn,
|
|
datetime: initDateTimeColumn,
|
|
boolean: initBooleanColumn,
|
|
link: initLinkColumn,
|
|
image: initImageColumn,
|
|
json: initJsonColumn,
|
|
};
|
|
|
|
function nextOrderByDirection(direction) {
|
|
switch (direction) {
|
|
case 'ascend': return 'descend';
|
|
case 'descend': return null;
|
|
default: return 'ascend';
|
|
}
|
|
}
|
|
|
|
function toggleOrderBy(columnName, orderBy = [], multiColumnSort = false) {
|
|
const index = findIndex(orderBy, i => i.name === columnName);
|
|
const item = { name: columnName, direction: 'ascend' };
|
|
if (index >= 0) {
|
|
item.direction = nextOrderByDirection(orderBy[index].direction);
|
|
}
|
|
|
|
if (multiColumnSort) {
|
|
if (!item.direction) {
|
|
return filter(orderBy, i => i.name !== columnName);
|
|
}
|
|
if (index >= 0) {
|
|
orderBy[index] = item;
|
|
} else {
|
|
orderBy.push(item);
|
|
}
|
|
return [...orderBy];
|
|
}
|
|
return item.direction ? [item] : [];
|
|
}
|
|
|
|
function getOrderByInfo(orderBy) {
|
|
const result = {};
|
|
each(orderBy, ({ name, direction }, index) => {
|
|
result[name] = { direction, index: index + 1 };
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export function prepareColumns(columns, searchInput, orderBy, onOrderByChange) {
|
|
columns = filter(columns, 'visible');
|
|
columns = sortBy(columns, 'order');
|
|
|
|
const isMultiColumnSort = orderBy.length > 1;
|
|
const orderByInfo = getOrderByInfo(orderBy);
|
|
|
|
let tableColumns = map(columns, (column) => {
|
|
const isAscend = orderByInfo[column.name] && (orderByInfo[column.name].direction === 'ascend');
|
|
const isDescend = orderByInfo[column.name] && (orderByInfo[column.name].direction === 'descend');
|
|
|
|
const sortColumnIndex = isMultiColumnSort && orderByInfo[column.name] ? orderByInfo[column.name].index : null;
|
|
|
|
const result = {
|
|
key: column.name, // set this because we don't use `dataIndex`
|
|
// Column name may contain any characters (or be empty at all), therefore
|
|
// we cannot use `dataIndex` because it has special syntax and will not work
|
|
// for all possible column names. Instead, we'll generate row key dynamically
|
|
// based on row index
|
|
dataIndex: null,
|
|
align: column.alignContent,
|
|
title: (
|
|
<React.Fragment>
|
|
<Tooltip placement="top" title={column.title}>
|
|
<div className="table-visualization-heading" data-sort-column-index={sortColumnIndex}>{column.title}</div>
|
|
</Tooltip>
|
|
<span className="ant-table-column-sorter">
|
|
<div className="ant-table-column-sorter-inner ant-table-column-sorter-inner-full">
|
|
<Icon
|
|
className={`ant-table-column-sorter-up ${isAscend ? 'on' : 'off'}`}
|
|
type="caret-up"
|
|
theme="filled"
|
|
/>
|
|
<Icon
|
|
className={`ant-table-column-sorter-down ${isDescend ? 'on' : 'off'}`}
|
|
type="caret-down"
|
|
theme="filled"
|
|
/>
|
|
</div>
|
|
</span>
|
|
</React.Fragment>
|
|
),
|
|
onHeaderCell: () => ({
|
|
className: cx(
|
|
'ant-table-column-has-actions ant-table-column-has-sorters',
|
|
{ 'table-visualization-column-is-sorted': isAscend || isDescend },
|
|
),
|
|
onClick: event => onOrderByChange(toggleOrderBy(column.name, orderBy, event.shiftKey)),
|
|
}),
|
|
};
|
|
|
|
const initColumn = ColumnTypes[column.displayAs];
|
|
const Component = initColumn(column);
|
|
result.render = (unused, row) => ({
|
|
children: <Component row={row} />,
|
|
props: { className: `display-as-${column.displayAs}` },
|
|
});
|
|
|
|
return result;
|
|
});
|
|
|
|
tableColumns.push({
|
|
key: '###Redash::Visualizations::Table::Spacer###',
|
|
dataIndex: null,
|
|
title: '',
|
|
className: 'table-visualization-spacer',
|
|
render: () => '',
|
|
onHeaderCell: () => ({ className: 'table-visualization-spacer' }),
|
|
});
|
|
|
|
if (searchInput) {
|
|
// We need a merged head cell through entire row. With Ant's Table the only way to do it
|
|
// is to add a single child to every column move `dataIndex` property to it and set
|
|
// `colSpan` to 0 for every child cell except of the 1st one - which should be expanded.
|
|
tableColumns = map(tableColumns, ({ title, align, key, onHeaderCell, ...rest }, index) => ({
|
|
key: key + '(parent)',
|
|
title,
|
|
align,
|
|
onHeaderCell,
|
|
children: [{
|
|
...rest,
|
|
key: key + '(child)',
|
|
align,
|
|
colSpan: index === 0 ? tableColumns.length : 0,
|
|
title: index === 0 ? searchInput : null,
|
|
onHeaderCell: () => ({ className: 'table-visualization-search' }),
|
|
}],
|
|
}));
|
|
}
|
|
|
|
return tableColumns;
|
|
}
|
|
|
|
export function filterRows(rows, searchTerm, searchColumns) {
|
|
if ((searchTerm !== '') && (searchColumns.length > 0)) {
|
|
searchTerm = searchTerm.toUpperCase();
|
|
const matchFields = map(searchColumns, (column) => {
|
|
const initColumn = ColumnTypes[column.displayAs];
|
|
const { prepareData } = initColumn(column);
|
|
return (row) => {
|
|
const { text } = prepareData(row);
|
|
return toString(text).toUpperCase().indexOf(searchTerm) >= 0;
|
|
};
|
|
});
|
|
|
|
return filter(rows, row => some(matchFields, match => match(row)));
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
export function sortRows(rows, orderBy) {
|
|
if ((orderBy.length === 0) || (rows.length === 0)) {
|
|
return rows;
|
|
}
|
|
|
|
const directions = { ascend: 1, descend: -1 };
|
|
|
|
// Create a copy of array before sorting, because .sort() will modify original array
|
|
return [...rows].sort((a, b) => {
|
|
let va;
|
|
let vb;
|
|
for (let i = 0; i < orderBy.length; i += 1) {
|
|
va = a[orderBy[i].name];
|
|
vb = b[orderBy[i].name];
|
|
if (isNil(va) || va < vb) {
|
|
// if a < b - we should return -1, but take in account direction
|
|
return -1 * directions[orderBy[i].direction];
|
|
}
|
|
if (va > vb || isNil(vb)) {
|
|
// if a > b - we should return 1, but take in account direction
|
|
return 1 * directions[orderBy[i].direction];
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
}
|