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: (
{column.title}
), 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: , 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; }); }