<commit_before>import { filter, map, get, 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;<commit_msg>Fix: table viz crashing when search is enabled (#4899)<commit_after>import { filter, map, get, initial, last, reduce } from "lodash";
import React, { useMemo, useState, useEffect } from "react";
import PropTypes from "prop-types";
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>
  );
}

function SearchInput({ searchColumns, ...props }) {
  if (searchColumns.length <= 0) {
    return null;
  }

  const searchColumnsLimit = 3;
  return (
    <Input.Search
      {...props}
      placeholder={`Search ${getSearchColumns(searchColumns, { limit: searchColumnsLimit }).join("")}...`}
      suffix={searchColumns.length > searchColumnsLimit ? <SearchInputInfoIcon searchColumns={searchColumns} /> : null}
    />
  );
}

SearchInput.propTypes = {
  onChange: PropTypes.func,
};

SearchInput.defaultProps = {
  onChange: () => {},
};

export default function Renderer({ options, data }) {
  const [searchTerm, setSearchTerm] = useState("");
  const [orderBy, setOrderBy] = useState([]);

  const searchColumns = useMemo(() => filter(options.columns, "allowSearch"), [options.columns]);

  const tableColumns = useMemo(() => {
    const searchInput =
      searchColumns.length > 0 ? (
        <SearchInput searchColumns={searchColumns} onChange={event => setSearchTerm(event.target.value)} />
      ) : null;
    return prepareColumns(options.columns, searchInput, orderBy, newOrderBy => {
      setOrderBy(newOrderBy);
      // Remove text selection - may occur accidentally
      document.getSelection().removeAllRanges();
    });
  }, [options.columns, searchColumns, orderBy]);

  const preparedRows = useMemo(() => sortRows(filterRows(initRows(data.rows), searchTerm, searchColumns), orderBy), [
    data.rows,
    searchTerm,
    searchColumns,
    orderBy,
  ]);

  // If data or config columns change - reset sorting
  useEffect(() => {
    setOrderBy([]);
  }, [options.columns, data.columns]);

  if (data.rows.length === 0) {
    return null;
{'fraction_non_alphanumeric': 0.1479723314797233, 'fraction_numerical': 0.002712600027126, 'mean_word_length': 3.681904761904762, 'pattern_counts': {'":': 0, '<': 26, '<?xml version=': 0, '>': 58, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4033898', 'n_tokens_mistral': 2186, 'n_tokens_neox': 2155, 'n_words': 526}