Multiselect dropdown slowness (fix) (#5221)

* created util to estimate reasonable width for dropdown

* removed unused import

* improved calculation of item percentile

* added getItemOfPercentileLength to relevant spots

* added getItemOfPercentileLength to relevant spots

* Added missing import

* created custom select element

* added check for property path

* removed uses of percentile util

* gave up on getting element reference

* finished testing Select component

* removed unused imports

* removed older uses of Option component

* added canvas calculation

* removed minWidth from Select

* improved calculation

* added fallbacks

* added estimated offset

* removed leftovers 😅

* replaced to percentiles to max value

* switched to memo and renamed component

* proper useMemo syntax

* Update client/app/components/Select.tsx

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

* created custom restrictive types

* added quick const

* fixed style

* fixed generics

* added pos absolute to fix percy

* removed custom select from ParameterMappingInput

* applied prettier

* Revert "added pos absolute to fix percy"

This reverts commit 4daf1d4bef9edf93cd9bb1f404bd022472ff17a2.

* Pin Percy version to 0.24.3

* Update client/app/components/ParameterMappingInput.jsx

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

* renamed Select.jsx to SelectWithVirtualScroll

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
This commit is contained in:
Rafael Wendel 2020-11-03 16:50:39 -03:00 committed by GitHub
parent cae088f35b
commit 12f71925c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 128 additions and 73 deletions

View File

@ -25,8 +25,6 @@ import CheckOutlinedIcon from "@ant-design/icons/CheckOutlined";
import "./ParameterMappingInput.less";
const { Option } = Select;
export const MappingType = {
DashboardAddNew: "dashboard-add-new",
DashboardMapToExisting: "dashboard-map-to-existing",
@ -208,19 +206,9 @@ export class ParameterMappingInput extends React.Component {
renderDashboardMapToExisting() {
const { mapping, existingParamNames } = this.props;
const options = map(existingParamNames, paramName => ({ label: paramName, value: paramName }));
return (
<Select
value={mapping.mapTo}
onChange={mapTo => this.updateParamMapping({ mapTo })}
dropdownMatchSelectWidth={false}>
{map(existingParamNames, name => (
<Option value={name} key={name}>
{name}
</Option>
))}
</Select>
);
return <Select value={mapping.mapTo} onChange={mapTo => this.updateParamMapping({ mapTo })} options={options} />;
}
renderStaticValue() {

View File

@ -1,7 +1,7 @@
import { isEqual, isEmpty } from "lodash";
import { isEqual, isEmpty, map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import SelectWithVirtualScroll from "@/components/SelectWithVirtualScroll";
import Input from "antd/lib/input";
import InputNumber from "antd/lib/input-number";
import DateParameter from "@/components/dynamic-parameters/DateParameter";
@ -10,8 +10,6 @@ import QueryBasedParameterInput from "./QueryBasedParameterInput";
import "./ParameterValueInput.less";
const { Option } = Select;
const multipleValuesProps = {
maxTagCount: 3,
maxTagTextLength: 10,
@ -98,25 +96,20 @@ class ParameterValueInput extends React.Component {
const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
// Antd Select doesn't handle null in multiple mode
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
return (
<Select
<SelectWithVirtualScroll
className={this.props.className}
mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children"
value={normalize(value)}
onChange={this.onSelect}
dropdownMatchSelectWidth={false}
options={map(enumOptionsArray, opt => ({ label: String(opt), value: opt }))}
showSearch
showArrow
style={{ minWidth: 60 }}
notFoundContent={isEmpty(enumOptionsArray) ? "No options available" : null}
{...multipleValuesProps}>
{enumOptionsArray.map(option => (
<Option key={option} value={option}>
{option}
</Option>
))}
</Select>
{...multipleValuesProps}
/>
);
}

View File

@ -1,9 +1,7 @@
import { find, isArray, get, first, map, intersection, isEqual, isEmpty } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
const { Option } = Select;
import SelectWithVirtualScroll from "@/components/SelectWithVirtualScroll";
export default class QueryBasedParameterInput extends React.Component {
static propTypes = {
@ -83,25 +81,20 @@ export default class QueryBasedParameterInput extends React.Component {
const { loading, options } = this.state;
return (
<span>
<Select
<SelectWithVirtualScroll
className={className}
disabled={loading}
loading={loading}
mode={mode}
value={this.state.value}
onChange={onSelect}
dropdownMatchSelectWidth={false}
options={map(options, ({ value, name }) => ({ label: String(name), value }))}
optionFilterProp="children"
showSearch
showArrow
notFoundContent={isEmpty(options) ? "No options available" : null}
{...otherProps}>
{options.map(option => (
<Option value={option.value} key={option.value}>
{option.name}
</Option>
))}
</Select>
{...otherProps}
/>
</span>
);
}

View File

@ -0,0 +1,38 @@
import React, { useMemo } from "react";
import { maxBy } from "lodash";
import AntdSelect, { SelectProps, LabeledValue } from "antd/lib/select";
import { calculateTextWidth } from "@/lib/calculateTextWidth";
const MIN_LEN_FOR_VIRTUAL_SCROLL = 400;
interface VirtualScrollLabeledValue extends LabeledValue {
label: string;
}
interface VirtualScrollSelectProps extends SelectProps<string> {
options: Array<VirtualScrollLabeledValue>;
}
function SelectWithVirtualScroll({ options, ...props }: VirtualScrollSelectProps): JSX.Element {
const dropdownMatchSelectWidth = useMemo<number | boolean>(() => {
if (options && options.length > MIN_LEN_FOR_VIRTUAL_SCROLL) {
const largestOpt = maxBy(options, "label.length");
if (largestOpt) {
const offset = 40;
const optionText = largestOpt.label;
const width = calculateTextWidth(optionText);
if (width) {
return width + offset;
}
}
return true;
}
return false;
}, [options]);
return <AntdSelect<string> dropdownMatchSelectWidth={dropdownMatchSelectWidth} options={options} {...props} />;
}
export default SelectWithVirtualScroll;

View File

@ -0,0 +1,20 @@
const canvas = document.createElement("canvas");
canvas.style.display = "none";
document.body.appendChild(canvas);
export function calculateTextWidth(text: string, container = document.body) {
const ctx = canvas.getContext("2d");
if (ctx) {
const containerStyle = window.getComputedStyle(container);
ctx.font = `${containerStyle.fontSize} ${containerStyle.fontFamily}`;
const textMetrics = ctx.measureText(text);
let actualWidth = textMetrics.width;
if ("actualBoundingBoxLeft" in textMetrics) {
// only available on evergreen browsers
actualWidth = Math.abs(textMetrics.actualBoundingBoxLeft) + Math.abs(textMetrics.actualBoundingBoxRight);
}
return actualWidth;
}
return null;
}

85
package-lock.json generated
View File

@ -4107,12 +4107,11 @@
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
@ -4270,9 +4269,9 @@
"dev": true
},
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
"dev": true
}
}
@ -4297,12 +4296,11 @@
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
@ -4355,13 +4353,21 @@
"dev": true
},
"jsonfile": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
"integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^1.0.0"
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
},
"string-width": {
@ -4668,9 +4674,9 @@
"dev": true
},
"@percy/agent": {
"version": "0.26.2",
"resolved": "https://registry.npmjs.org/@percy/agent/-/agent-0.26.2.tgz",
"integrity": "sha512-egwfhCOZnPDKh67Ldi3jN72fReaa3gU3OiXwhibT9NWkMuKh4O6m1zP55gYt0f8qDc5T9ZVfB0fbvsvJuTqujg==",
"version": "0.24.3",
"resolved": "https://registry.npmjs.org/@percy/agent/-/agent-0.24.3.tgz",
"integrity": "sha512-gSx1qqtTMLix/ZzGo9taz02zKR9CGAt5megYW3jPW+DhyTaHSHoQgGcEI41el7T7BqB4FbfS0rkeG2B991mNcw==",
"dev": true,
"requires": {
"@oclif/command": "1.5.19",
@ -4798,13 +4804,30 @@
}
},
"@percy/cypress": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@percy/cypress/-/cypress-2.3.1.tgz",
"integrity": "sha512-/kswOdqO/w6q7VLPeZENRd9c8aIb+W6oNHZDNkRZ9eIa1O+iMIqHj2aB+Bcbc6WhtlRZM/65ekrGPR7QyK9Y7A==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@percy/cypress/-/cypress-2.3.2.tgz",
"integrity": "sha512-pRBUft9gOM5Jduyu0VKfucHy8QTDTIw8y+Zu7JNrNHWa0JcfOJcpQbhZJ6AGmA5xzad05S6wjQ8CnG8y3iaj/w==",
"dev": true,
"requires": {
"@percy/agent": "~0",
"axios": "^0.19.0"
"axios": "^0.20.0"
},
"dependencies": {
"axios": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
"integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
"dev": true,
"requires": {
"follow-redirects": "^1.10.0"
}
},
"follow-redirects": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==",
"dev": true
}
}
},
"@plotly/d3-sankey": {
@ -5464,6 +5487,12 @@
"robust-orientation": "^1.1.3"
}
},
"agent-base": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
"dev": true
},
"aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@ -8262,9 +8291,9 @@
}
},
"color-string": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
"integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
@ -14155,12 +14184,6 @@
"debug": "4"
},
"dependencies": {
"agent-base": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
"dev": true
},
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",

View File

@ -86,8 +86,8 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.10.4",
"@cypress/code-coverage": "^3.8.1",
"@percy/agent": "^0.26.2",
"@percy/cypress": "^2.3.1",
"@percy/agent": "0.24.3",
"@percy/cypress": "^2.3.2",
"@types/classnames": "^2.2.10",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/lodash": "^4.14.157",