Files
redash/client/app/visualizations/table/Renderer.jsx
Levko Kravets 3650f0c45b Table visualization: Show which columns are being used for search (#4680)
* Table visualization: Show which columns are being used for search

* Fix accidental bug
2020-03-01 14:15:49 +02:00

134 lines
4.3 KiB
JavaScript

import { filter, map, initial, last, reduce } from "lodash";
import React, { useMemo, useState, useRef, useCallback, useEffect } from "react";
import Table from "antd/lib/table";
import Input from "antd/lib/input";
import Icon from "antd/lib/icon";
import Popover from "antd/lib/popover";
import { RendererPropTypes } from "@/visualizations/prop-types";
import { prepareColumns, initRows, filterRows, sortRows } from "./utils";
import "./renderer.less";
function joinColumns(array, separator = ", ") {
return reduce(
array,
(result, item, index) => {
if (index > 0) {
result.push(separator);
}
result.push(item);
return result;
},
[]
);
}
function getSearchColumns(columns, { limit = Infinity, renderColumn = col => col.title } = {}) {
const firstColumns = map(columns.slice(0, limit), col => renderColumn(col));
const restColumns = map(columns.slice(limit), col => col.title);
if (restColumns.length > 0) {
return [...joinColumns(firstColumns), ` and ${restColumns.length} others`];
}
if (firstColumns.length > 1) {
return [...joinColumns(initial(firstColumns)), ` and `, last(firstColumns)];
}
return firstColumns;
}
function SearchInputInfoIcon({ searchColumns }) {
return (
<Popover
arrowPointAtCenter
placement="topRight"
content={
<div className="table-visualization-search-info-content">
Search {getSearchColumns(searchColumns, { renderColumn: col => <code key={col.name}>{col.title}</code> })}
</div>
}>
<Icon className="table-visualization-search-info-icon" type="info-circle" theme="filled" />
</Popover>
);
}
const SearchInput = React.forwardRef(({ searchColumns, ...props }, ref) => {
if (searchColumns.length <= 0) {
return null;
}
const searchColumnsLimit = 3;
return (
<Input.Search
{...props}
ref={ref}
placeholder={`Search ${getSearchColumns(searchColumns, { limit: searchColumnsLimit }).join("")}...`}
suffix={searchColumns.length > searchColumnsLimit ? <SearchInputInfoIcon searchColumns={searchColumns} /> : null}
/>
);
});
export default function Renderer({ options, data, context }) {
const [searchTerm, setSearchTerm] = useState("");
const [orderBy, setOrderBy] = useState([]);
const searchColumns = useMemo(() => filter(options.columns, "allowSearch"), [options.columns]);
const searchInputRef = useRef();
const onSearchInputChange = useCallback(event => setSearchTerm(event.target.value), [setSearchTerm]);
const tableColumns = useMemo(() => {
const searchInput =
searchColumns.length > 0 ? (
<SearchInput ref={searchInputRef} searchColumns={searchColumns} onChange={onSearchInputChange} />
) : null;
return prepareColumns(options.columns, searchInput, orderBy, newOrderBy => {
setOrderBy(newOrderBy);
// Remove text selection - may occur accidentally
document.getSelection().removeAllRanges();
});
}, [options.columns, searchColumns, searchInputRef, onSearchInputChange, orderBy, setOrderBy]);
const preparedRows = useMemo(() => sortRows(filterRows(initRows(data.rows), searchTerm, searchColumns), orderBy), [
data.rows,
searchTerm,
searchColumns,
orderBy,
]);
// If data or config columns change - reset sorting and search
useEffect(() => {
setSearchTerm("");
// Do not use `<Input value={searchTerm}>` because it leads to many renderings and lags on user
// input. This is the only place where we need to change search input's value from "outer world",
// so let's use this "hack" for better performance.
if (searchInputRef.current) {
// pass value and fake event-like object
searchInputRef.current.input.setValue("", { target: { value: "" } });
}
setOrderBy([]);
}, [options.columns, data.columns, searchInputRef]);
if (data.rows.length === 0) {
return null;
}
return (
<div className="table-visualization-container">
<Table
data-percy="show-scrollbars"
data-test="TableVisualization"
columns={tableColumns}
dataSource={preparedRows}
pagination={{
size: context === "widget" ? "small" : "",
position: "bottom",
pageSize: options.itemsPerPage,
hideOnSinglePage: true,
}}
/>
</div>
);
}
Renderer.propTypes = RendererPropTypes;