mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SearchBar] Improve rendering performance (#119189)
This commit is contained in:
parent
eab0485fa3
commit
00d1ad30f4
20 changed files with 729 additions and 542 deletions
|
@ -287,7 +287,7 @@
|
|||
"markdown-it": "^10.0.0",
|
||||
"md5": "^2.1.0",
|
||||
"mdast-util-to-hast": "10.0.1",
|
||||
"memoize-one": "^5.0.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^2.4.4",
|
||||
"mime-types": "^2.1.27",
|
||||
"mini-css-extract-plugin": "1.1.0",
|
||||
|
@ -611,7 +611,6 @@
|
|||
"@types/lz-string": "^1.3.34",
|
||||
"@types/markdown-it": "^0.0.7",
|
||||
"@types/md5": "^2.2.0",
|
||||
"@types/memoize-one": "^4.1.0",
|
||||
"@types/mime": "^2.0.1",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/minimatch": "^2.0.29",
|
||||
|
|
|
@ -42,6 +42,10 @@ export class TimeHistory {
|
|||
get() {
|
||||
return this.history.get();
|
||||
}
|
||||
|
||||
get$() {
|
||||
return this.history.get$();
|
||||
}
|
||||
}
|
||||
|
||||
export type TimeHistoryContract = PublicMethodsOf<TimeHistory>;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { TimefilterService, TimeHistoryContract, TimefilterContract } from '.';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TimeRange } from '../../../common';
|
||||
|
||||
export type TimefilterServiceClientContract = PublicMethodsOf<TimefilterService>;
|
||||
|
||||
|
@ -43,6 +44,7 @@ const createSetupContractMock = () => {
|
|||
const historyMock: jest.Mocked<TimeHistoryContract> = {
|
||||
add: jest.fn(),
|
||||
get: jest.fn(),
|
||||
get$: jest.fn(() => new Observable<TimeRange[]>()),
|
||||
};
|
||||
|
||||
const setupContract = {
|
||||
|
|
|
@ -40,7 +40,7 @@ interface Props {
|
|||
timeRangeForSuggestionsOverride?: boolean;
|
||||
}
|
||||
|
||||
function FilterBarUI(props: Props) {
|
||||
const FilterBarUI = React.memo(function FilterBarUI(props: Props) {
|
||||
const groupRef = useRef<HTMLDivElement>(null);
|
||||
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
|
@ -226,6 +226,6 @@ function FilterBarUI(props: Props) {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const FilterBar = injectI18n(FilterBarUI);
|
||||
|
|
|
@ -32,7 +32,7 @@ export interface QueryLanguageSwitcherProps {
|
|||
nonKqlModeHelpText?: string;
|
||||
}
|
||||
|
||||
export function QueryLanguageSwitcher({
|
||||
export const QueryLanguageSwitcher = React.memo(function QueryLanguageSwitcher({
|
||||
language,
|
||||
anchorPosition,
|
||||
onSelectLanguage,
|
||||
|
@ -148,4 +148,4 @@ export function QueryLanguageSwitcher({
|
|||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,9 +11,9 @@ import { mockPersistedLogFactory } from './query_string_input.test.mocks';
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { EMPTY } from 'rxjs';
|
||||
|
||||
import QueryBarTopRow from './query_bar_top_row';
|
||||
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
import { dataPluginMock } from '../../mocks';
|
||||
import { KibanaContextProvider } from 'src/plugins/kibana_react/public';
|
||||
|
@ -26,6 +26,7 @@ const mockTimeHistory = {
|
|||
get: () => {
|
||||
return [];
|
||||
},
|
||||
get$: () => EMPTY,
|
||||
};
|
||||
|
||||
startMock.uiSettings.get.mockImplementation((key: string) => {
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import classNames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
|
@ -17,9 +21,10 @@ import {
|
|||
EuiFieldText,
|
||||
prettyDuration,
|
||||
EuiIconProps,
|
||||
EuiSuperUpdateButton,
|
||||
OnRefreshProps,
|
||||
} from '@elastic/eui';
|
||||
// @ts-ignore
|
||||
import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui';
|
||||
|
||||
import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..';
|
||||
import { useKibana, withKibana } from '../../../../kibana_react/public';
|
||||
import QueryStringInputUI from './query_string_input';
|
||||
|
@ -27,6 +32,14 @@ import { UI_SETTINGS } from '../../../common';
|
|||
import { getQueryLog } from '../../query';
|
||||
import type { PersistedLog } from '../../query';
|
||||
import { NoDataPopover } from './no_data_popover';
|
||||
import { shallowEqual } from '../../utils/shallow_equal';
|
||||
|
||||
const SuperDatePicker = React.memo(
|
||||
EuiSuperDatePicker as any
|
||||
) as unknown as typeof EuiSuperDatePicker;
|
||||
const SuperUpdateButton = React.memo(
|
||||
EuiSuperUpdateButton as any
|
||||
) as unknown as typeof EuiSuperUpdateButton;
|
||||
|
||||
const QueryStringInput = withKibana(QueryStringInputUI);
|
||||
|
||||
|
@ -63,105 +76,15 @@ export interface QueryBarTopRowProps {
|
|||
timeRangeForSuggestionsOverride?: boolean;
|
||||
}
|
||||
|
||||
// Needed for React.lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function QueryBarTopRow(props: QueryBarTopRowProps) {
|
||||
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
|
||||
const [isQueryInputFocused, setIsQueryInputFocused] = useState(false);
|
||||
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const { uiSettings, storage, appName } = kibana.services;
|
||||
|
||||
const queryLanguage = props.query && props.query.language;
|
||||
const persistedLog: PersistedLog | undefined = React.useMemo(
|
||||
() =>
|
||||
queryLanguage && uiSettings && storage && appName
|
||||
? getQueryLog(uiSettings!, storage, appName, queryLanguage)
|
||||
: undefined,
|
||||
[appName, queryLanguage, uiSettings, storage]
|
||||
);
|
||||
|
||||
function onClickSubmitButton(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
if (persistedLog && props.query) {
|
||||
persistedLog.add(props.query.query);
|
||||
}
|
||||
event.preventDefault();
|
||||
onSubmit({ query: props.query, dateRange: getDateRange() });
|
||||
}
|
||||
|
||||
function getDateRange() {
|
||||
const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS);
|
||||
return {
|
||||
from: props.dateRangeFrom || defaultTimeSetting.from,
|
||||
to: props.dateRangeTo || defaultTimeSetting.to,
|
||||
};
|
||||
}
|
||||
|
||||
function onQueryChange(query: Query) {
|
||||
props.onChange({
|
||||
query,
|
||||
dateRange: getDateRange(),
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeQueryInputFocus(isFocused: boolean) {
|
||||
setIsQueryInputFocused(isFocused);
|
||||
}
|
||||
|
||||
function onTimeChange({
|
||||
start,
|
||||
end,
|
||||
isInvalid,
|
||||
isQuickSelection,
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
isInvalid: boolean;
|
||||
isQuickSelection: boolean;
|
||||
}) {
|
||||
setIsDateRangeInvalid(isInvalid);
|
||||
const retVal = {
|
||||
query: props.query,
|
||||
dateRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
};
|
||||
|
||||
if (isQuickSelection) {
|
||||
props.onSubmit(retVal);
|
||||
} else {
|
||||
props.onChange(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
function onRefresh({ start, end }: OnRefreshProps) {
|
||||
const retVal = {
|
||||
dateRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
};
|
||||
if (props.onRefresh) {
|
||||
props.onRefresh(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit({ query, dateRange }: { query?: Query; dateRange: TimeRange }) {
|
||||
if (props.timeHistory) {
|
||||
props.timeHistory.add(dateRange);
|
||||
}
|
||||
|
||||
props.onSubmit({ query, dateRange });
|
||||
}
|
||||
|
||||
function onInputSubmit(query: Query) {
|
||||
onSubmit({
|
||||
query,
|
||||
dateRange: getDateRange(),
|
||||
});
|
||||
}
|
||||
|
||||
const SharingMetaFields = React.memo(function SharingMetaFields({
|
||||
from,
|
||||
to,
|
||||
dateFormat,
|
||||
}: {
|
||||
from: string;
|
||||
to: string;
|
||||
dateFormat: string;
|
||||
}) {
|
||||
function toAbsoluteString(value: string, roundUp = false) {
|
||||
const valueAsMoment = dateMath.parse(value, { roundUp });
|
||||
if (!valueAsMoment) {
|
||||
|
@ -170,158 +93,311 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) {
|
|||
return valueAsMoment.toISOString();
|
||||
}
|
||||
|
||||
function renderQueryInput() {
|
||||
if (!shouldRenderQueryInput()) return;
|
||||
const dateRangePretty = prettyDuration(
|
||||
toAbsoluteString(from),
|
||||
toAbsoluteString(to),
|
||||
[],
|
||||
dateFormat
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<QueryStringInput
|
||||
disableAutoFocus={props.disableAutoFocus}
|
||||
indexPatterns={props.indexPatterns!}
|
||||
prepend={props.prepend}
|
||||
query={props.query!}
|
||||
screenTitle={props.screenTitle}
|
||||
onChange={onQueryChange}
|
||||
onChangeQueryInputFocus={onChangeQueryInputFocus}
|
||||
onSubmit={onInputSubmit}
|
||||
persistedLog={persistedLog}
|
||||
dataTestSubj={props.dataTestSubj}
|
||||
placeholder={props.placeholder}
|
||||
isClearable={props.isClearable}
|
||||
iconType={props.iconType}
|
||||
nonKqlMode={props.nonKqlMode}
|
||||
nonKqlModeHelpText={props.nonKqlModeHelpText}
|
||||
timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-shared-timefilter-duration={dateRangePretty}
|
||||
data-test-subj="dataSharedTimefilterDuration"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
function renderSharingMetaFields() {
|
||||
const { from, to } = getDateRange();
|
||||
const dateRangePretty = prettyDuration(
|
||||
toAbsoluteString(from),
|
||||
toAbsoluteString(to),
|
||||
[],
|
||||
uiSettings.get('dateFormat')
|
||||
);
|
||||
return (
|
||||
<div
|
||||
data-shared-timefilter-duration={dateRangePretty}
|
||||
data-test-subj="dataSharedTimefilterDuration"
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const QueryBarTopRow = React.memo(
|
||||
function QueryBarTopRow(props: QueryBarTopRowProps) {
|
||||
const { showQueryInput = true, showDatePicker = true, showAutoRefreshOnly = false } = props;
|
||||
|
||||
function shouldRenderDatePicker(): boolean {
|
||||
return Boolean(props.showDatePicker || props.showAutoRefreshOnly);
|
||||
}
|
||||
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
|
||||
const [isQueryInputFocused, setIsQueryInputFocused] = useState(false);
|
||||
|
||||
function shouldRenderQueryInput(): boolean {
|
||||
return Boolean(props.showQueryInput && props.indexPatterns && props.query && storage);
|
||||
}
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const { uiSettings, storage, appName } = kibana.services;
|
||||
|
||||
function renderUpdateButton() {
|
||||
const button = props.customSubmitButton ? (
|
||||
React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton })
|
||||
) : (
|
||||
<EuiSuperUpdateButton
|
||||
needsUpdate={props.isDirty}
|
||||
isDisabled={isDateRangeInvalid}
|
||||
isLoading={props.isLoading}
|
||||
onClick={onClickSubmitButton}
|
||||
fill={props.fillSubmitButton}
|
||||
data-test-subj="querySubmitButton"
|
||||
/>
|
||||
const queryLanguage = props.query && props.query.language;
|
||||
const queryRef = useRef<Query | undefined>(props.query);
|
||||
queryRef.current = props.query;
|
||||
|
||||
const persistedLog: PersistedLog | undefined = React.useMemo(
|
||||
() =>
|
||||
queryLanguage && uiSettings && storage && appName
|
||||
? getQueryLog(uiSettings!, storage, appName, queryLanguage)
|
||||
: undefined,
|
||||
[appName, queryLanguage, uiSettings, storage]
|
||||
);
|
||||
|
||||
if (!shouldRenderDatePicker()) {
|
||||
return button;
|
||||
function getDateRange() {
|
||||
const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS);
|
||||
return {
|
||||
from: props.dateRangeFrom || defaultTimeSetting.from,
|
||||
to: props.dateRangeTo || defaultTimeSetting.to,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<NoDataPopover storage={storage} showNoDataPopover={props.indicateNoData}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="s">
|
||||
{renderDatePicker()}
|
||||
<EuiFlexItem grow={false}>{button}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</NoDataPopover>
|
||||
);
|
||||
}
|
||||
const currentDateRange = getDateRange();
|
||||
const dateRangeRef = useRef<{ from: string; to: string }>(currentDateRange);
|
||||
dateRangeRef.current = currentDateRange;
|
||||
|
||||
function renderDatePicker() {
|
||||
if (!shouldRenderDatePicker()) {
|
||||
return null;
|
||||
}
|
||||
const propsOnSubmit = props.onSubmit;
|
||||
|
||||
let recentlyUsedRanges;
|
||||
if (props.timeHistory) {
|
||||
recentlyUsedRanges = props.timeHistory
|
||||
.get()
|
||||
.map(({ from, to }: { from: string; to: string }) => {
|
||||
return {
|
||||
start: from,
|
||||
end: to,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const commonlyUsedRanges = uiSettings!
|
||||
.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES)
|
||||
.map(({ from, to, display }: { from: string; to: string; display: string }) => {
|
||||
const toRecentlyUsedRanges = (ranges: TimeRange[]) =>
|
||||
ranges.map(({ from, to }: { from: string; to: string }) => {
|
||||
return {
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
};
|
||||
});
|
||||
const timeHistory = props.timeHistory;
|
||||
const timeHistory$ = useMemo(
|
||||
() => timeHistory?.get$().pipe(map(toRecentlyUsedRanges)) ?? EMPTY,
|
||||
[timeHistory]
|
||||
);
|
||||
|
||||
const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'kbnQueryBar__datePickerWrapper-isHidden': isQueryInputFocused,
|
||||
const recentlyUsedRanges = useObservable(
|
||||
timeHistory$,
|
||||
toRecentlyUsedRanges(timeHistory?.get() ?? [])
|
||||
);
|
||||
const [commonlyUsedRanges] = useState(() => {
|
||||
return (
|
||||
uiSettings
|
||||
?.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES)
|
||||
?.map(({ from, to, display }: { from: string; to: string; display: string }) => {
|
||||
return {
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => {
|
||||
if (timeHistory) {
|
||||
timeHistory.add(dateRange);
|
||||
}
|
||||
|
||||
propsOnSubmit({ query, dateRange });
|
||||
},
|
||||
[timeHistory, propsOnSubmit]
|
||||
);
|
||||
|
||||
const onClickSubmitButton = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (persistedLog && queryRef.current) {
|
||||
persistedLog.add(queryRef.current.query);
|
||||
}
|
||||
event.preventDefault();
|
||||
onSubmit({
|
||||
query: queryRef.current,
|
||||
dateRange: dateRangeRef.current,
|
||||
});
|
||||
},
|
||||
[persistedLog, onSubmit]
|
||||
);
|
||||
|
||||
const propsOnChange = props.onChange;
|
||||
const onQueryChange = useCallback(
|
||||
(query: Query) => {
|
||||
return propsOnChange({
|
||||
query,
|
||||
dateRange: dateRangeRef.current,
|
||||
});
|
||||
},
|
||||
[propsOnChange]
|
||||
);
|
||||
|
||||
const onChangeQueryInputFocus = useCallback((isFocused: boolean) => {
|
||||
setIsQueryInputFocused(isFocused);
|
||||
}, []);
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({
|
||||
start,
|
||||
end,
|
||||
isInvalid,
|
||||
isQuickSelection,
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
isInvalid: boolean;
|
||||
isQuickSelection: boolean;
|
||||
}) => {
|
||||
setIsDateRangeInvalid(isInvalid);
|
||||
const retVal = {
|
||||
query: queryRef.current,
|
||||
dateRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
};
|
||||
|
||||
if (isQuickSelection) {
|
||||
onSubmit(retVal);
|
||||
} else {
|
||||
propsOnChange(retVal);
|
||||
}
|
||||
},
|
||||
[propsOnChange, onSubmit]
|
||||
);
|
||||
|
||||
const propsOnRefresh = props.onRefresh;
|
||||
const onRefresh = useCallback(
|
||||
({ start, end }: OnRefreshProps) => {
|
||||
const retVal = {
|
||||
dateRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
};
|
||||
if (propsOnRefresh) {
|
||||
propsOnRefresh(retVal);
|
||||
}
|
||||
},
|
||||
[propsOnRefresh]
|
||||
);
|
||||
|
||||
const onInputSubmit = useCallback(
|
||||
(query: Query) => {
|
||||
onSubmit({
|
||||
query,
|
||||
dateRange: dateRangeRef.current,
|
||||
});
|
||||
},
|
||||
[onSubmit]
|
||||
);
|
||||
|
||||
function shouldRenderQueryInput(): boolean {
|
||||
return Boolean(showQueryInput && props.indexPatterns && props.query && storage);
|
||||
}
|
||||
|
||||
function shouldRenderDatePicker(): boolean {
|
||||
return Boolean(showDatePicker || showAutoRefreshOnly);
|
||||
}
|
||||
|
||||
function renderDatePicker() {
|
||||
if (!shouldRenderDatePicker()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'kbnQueryBar__datePickerWrapper-isHidden': isQueryInputFocused,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem className={wrapperClasses}>
|
||||
<SuperDatePicker
|
||||
start={props.dateRangeFrom}
|
||||
end={props.dateRangeTo}
|
||||
isPaused={props.isRefreshPaused}
|
||||
refreshInterval={props.refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={props.onRefreshChange}
|
||||
showUpdateButton={false}
|
||||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
isAutoRefreshOnly={showAutoRefreshOnly}
|
||||
className="kbnQueryBar__datePicker"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUpdateButton() {
|
||||
const button = props.customSubmitButton ? (
|
||||
React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton })
|
||||
) : (
|
||||
<SuperUpdateButton
|
||||
needsUpdate={props.isDirty}
|
||||
isDisabled={isDateRangeInvalid}
|
||||
isLoading={props.isLoading}
|
||||
onClick={onClickSubmitButton}
|
||||
fill={props.fillSubmitButton}
|
||||
data-test-subj="querySubmitButton"
|
||||
/>
|
||||
);
|
||||
|
||||
if (!shouldRenderDatePicker()) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<NoDataPopover storage={storage} showNoDataPopover={props.indicateNoData}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="s">
|
||||
{renderDatePicker()}
|
||||
<EuiFlexItem grow={false}>{button}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</NoDataPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function renderQueryInput() {
|
||||
if (!shouldRenderQueryInput()) return;
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<QueryStringInput
|
||||
disableAutoFocus={props.disableAutoFocus}
|
||||
indexPatterns={props.indexPatterns!}
|
||||
prepend={props.prepend}
|
||||
query={props.query!}
|
||||
screenTitle={props.screenTitle}
|
||||
onChange={onQueryChange}
|
||||
onChangeQueryInputFocus={onChangeQueryInputFocus}
|
||||
onSubmit={onInputSubmit}
|
||||
persistedLog={persistedLog}
|
||||
dataTestSubj={props.dataTestSubj}
|
||||
placeholder={props.placeholder}
|
||||
isClearable={props.isClearable}
|
||||
iconType={props.iconType}
|
||||
nonKqlMode={props.nonKqlMode}
|
||||
nonKqlModeHelpText={props.nonKqlModeHelpText}
|
||||
timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames('kbnQueryBar', {
|
||||
'kbnQueryBar--withDatePicker': showDatePicker,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem className={wrapperClasses}>
|
||||
<EuiSuperDatePicker
|
||||
start={props.dateRangeFrom}
|
||||
end={props.dateRangeTo}
|
||||
isPaused={props.isRefreshPaused}
|
||||
refreshInterval={props.refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={props.onRefreshChange}
|
||||
showUpdateButton={false}
|
||||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
dateFormat={uiSettings!.get('dateFormat')}
|
||||
isAutoRefreshOnly={props.showAutoRefreshOnly}
|
||||
className="kbnQueryBar__datePicker"
|
||||
<EuiFlexGroup
|
||||
className={classes}
|
||||
responsive={!!showDatePicker}
|
||||
gutterSize="s"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
{renderQueryInput()}
|
||||
<SharingMetaFields
|
||||
from={currentDateRange.from}
|
||||
to={currentDateRange.to}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{renderUpdateButton()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
({ query: prevQuery, ...prevProps }, { query: nextQuery, ...nextProps }) => {
|
||||
let isQueryEqual = true;
|
||||
if (prevQuery !== nextQuery) {
|
||||
if (!deepEqual(prevQuery, nextQuery)) {
|
||||
isQueryEqual = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isQueryEqual && shallowEqual(prevProps, nextProps);
|
||||
}
|
||||
);
|
||||
|
||||
const classes = classNames('kbnQueryBar', {
|
||||
'kbnQueryBar--withDatePicker': props.showDatePicker,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className={classes}
|
||||
responsive={!!props.showDatePicker}
|
||||
gutterSize="s"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
{renderQueryInput()}
|
||||
{renderSharingMetaFields()}
|
||||
<EuiFlexItem grow={false}>{renderUpdateButton()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
QueryBarTopRow.defaultProps = {
|
||||
showQueryInput: true,
|
||||
showDatePicker: true,
|
||||
showAutoRefreshOnly: false,
|
||||
};
|
||||
// Needed for React.lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default QueryBarTopRow;
|
||||
|
|
|
@ -6,31 +6,31 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Component, RefObject, createRef } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
EuiTextArea,
|
||||
EuiOutsideClickDetector,
|
||||
PopoverAnchorPosition,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
htmlIdGenerator,
|
||||
EuiPortal,
|
||||
EuiIcon,
|
||||
EuiIconProps,
|
||||
EuiLink,
|
||||
EuiOutsideClickDetector,
|
||||
EuiPortal,
|
||||
EuiTextArea,
|
||||
htmlIdGenerator,
|
||||
PopoverAnchorPosition,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { debounce, compact, isEqual, isFunction } from 'lodash';
|
||||
import { compact, debounce, isEqual, isFunction } from 'lodash';
|
||||
import { Toast } from 'src/core/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
import { IDataPluginServices, IIndexPattern, Query } from '../..';
|
||||
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete';
|
||||
|
||||
import { KibanaReactContextValue, toMountPoint } from '../../../../kibana_react/public';
|
||||
import { fetchIndexPatterns } from './fetch_index_patterns';
|
||||
import { QueryLanguageSwitcher } from './language_switcher';
|
||||
|
@ -38,7 +38,8 @@ import { getQueryLog, matchPairs, toUser, fromUser } from '../../query';
|
|||
import type { PersistedLog } from '../../query';
|
||||
import type { SuggestionsListSize } from '../typeahead/suggestions_component';
|
||||
import { SuggestionsComponent } from '..';
|
||||
import { KIBANA_USER_QUERY_LANGUAGE_KEY, getFieldSubtypeNested } from '../../../common';
|
||||
import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '../../../common';
|
||||
import { onRaf } from '../utils';
|
||||
|
||||
export interface QueryStringInputProps {
|
||||
indexPatterns: Array<IIndexPattern | string>;
|
||||
|
@ -96,7 +97,11 @@ interface State {
|
|||
selectionStart: number | null;
|
||||
selectionEnd: number | null;
|
||||
indexPatterns: IIndexPattern[];
|
||||
queryBarRect: DOMRect | undefined;
|
||||
|
||||
/**
|
||||
* Part of state because passed down to child components
|
||||
*/
|
||||
queryBarInputDiv: HTMLDivElement | null;
|
||||
}
|
||||
|
||||
const KEY_CODES = {
|
||||
|
@ -113,7 +118,7 @@ const KEY_CODES = {
|
|||
|
||||
// Needed for React.lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default class QueryStringInputUI extends Component<Props, State> {
|
||||
export default class QueryStringInputUI extends PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
storageKey: KIBANA_USER_QUERY_LANGUAGE_KEY,
|
||||
};
|
||||
|
@ -126,7 +131,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
indexPatterns: [],
|
||||
queryBarRect: undefined,
|
||||
queryBarInputDiv: null,
|
||||
};
|
||||
|
||||
public inputRef: HTMLTextAreaElement | null = null;
|
||||
|
@ -140,7 +145,6 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
this.services.appName
|
||||
);
|
||||
private componentIsUnmounting = false;
|
||||
private queryBarInputDivRefInstance: RefObject<HTMLDivElement> = createRef();
|
||||
|
||||
/**
|
||||
* If any element within the container is currently focused
|
||||
|
@ -280,7 +284,9 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
suggestionLimit: 50,
|
||||
});
|
||||
|
||||
this.onChange({ query: value, language: this.props.query.language });
|
||||
if (this.props.query.query !== value) {
|
||||
this.onChange({ query: value, language: this.props.query.language });
|
||||
}
|
||||
};
|
||||
|
||||
private onInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
@ -318,10 +324,16 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
const { value, selectionStart, selectionEnd } = target;
|
||||
const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => {
|
||||
this.onQueryStringChange(query);
|
||||
this.setState({
|
||||
selectionStart: newSelectionStart,
|
||||
selectionEnd: newSelectionEnd,
|
||||
});
|
||||
|
||||
if (
|
||||
this.inputRef?.selectionStart !== newSelectionStart ||
|
||||
this.inputRef?.selectionEnd !== newSelectionEnd
|
||||
) {
|
||||
this.setState({
|
||||
selectionStart: newSelectionStart,
|
||||
selectionEnd: newSelectionEnd,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switch (event.keyCode) {
|
||||
|
@ -576,7 +588,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
: getQueryLog(uiSettings, storage, appName, this.props.query.language);
|
||||
};
|
||||
|
||||
public onMouseEnterSuggestion = (index: number) => {
|
||||
public onMouseEnterSuggestion = (suggestion: QuerySuggestion, index: number) => {
|
||||
this.setState({ index });
|
||||
};
|
||||
|
||||
|
@ -590,13 +602,9 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
|
||||
this.initPersistedLog();
|
||||
this.fetchIndexPatterns();
|
||||
this.handleListUpdate();
|
||||
this.handleAutoHeight();
|
||||
|
||||
window.addEventListener('resize', this.handleAutoHeight);
|
||||
window.addEventListener('scroll', this.handleListUpdate, {
|
||||
passive: true, // for better performance as we won't call preventDefault
|
||||
capture: true, // scroll events don't bubble, they must be captured instead
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
|
@ -621,11 +629,12 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
});
|
||||
if (document.activeElement !== null && document.activeElement.id === this.textareaId) {
|
||||
this.handleAutoHeight();
|
||||
} else {
|
||||
this.handleRemoveHeight();
|
||||
}
|
||||
}
|
||||
|
||||
if (document.activeElement !== null && document.activeElement.id === this.textareaId) {
|
||||
this.handleAutoHeight();
|
||||
} else {
|
||||
this.handleRemoveHeight();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -634,47 +643,35 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
if (this.updateSuggestions.cancel) this.updateSuggestions.cancel();
|
||||
this.componentIsUnmounting = true;
|
||||
window.removeEventListener('resize', this.handleAutoHeight);
|
||||
window.removeEventListener('scroll', this.handleListUpdate, { capture: true });
|
||||
}
|
||||
|
||||
handleListUpdate = () => {
|
||||
if (this.componentIsUnmounting) return;
|
||||
|
||||
return this.setState({
|
||||
queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(),
|
||||
});
|
||||
};
|
||||
|
||||
handleAutoHeight = () => {
|
||||
handleAutoHeight = onRaf(() => {
|
||||
if (this.inputRef !== null && document.activeElement === this.inputRef) {
|
||||
this.inputRef.classList.add('kbnQueryBar__textarea--autoHeight');
|
||||
this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important');
|
||||
}
|
||||
this.handleListUpdate();
|
||||
};
|
||||
});
|
||||
|
||||
handleRemoveHeight = () => {
|
||||
handleRemoveHeight = onRaf(() => {
|
||||
if (this.inputRef !== null) {
|
||||
this.inputRef.style.removeProperty('height');
|
||||
this.inputRef.classList.remove('kbnQueryBar__textarea--autoHeight');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
handleBlurHeight = () => {
|
||||
handleBlurHeight = onRaf(() => {
|
||||
if (this.inputRef !== null) {
|
||||
this.handleRemoveHeight();
|
||||
this.inputRef.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
handleOnFocus = () => {
|
||||
if (this.props.onChangeQueryInputFocus) {
|
||||
this.props.onChangeQueryInputFocus(true);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.handleAutoHeight();
|
||||
});
|
||||
this.handleAutoHeight();
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
@ -700,16 +697,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClassName}
|
||||
onFocus={(e) => {
|
||||
this.isFocusWithin = true;
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
this.isFocusWithin = false;
|
||||
this.scheduleOnInputBlur();
|
||||
}}
|
||||
>
|
||||
<div className={containerClassName} onFocus={this.onFocusWithin} onBlur={this.onBlurWithin}>
|
||||
{this.props.prepend}
|
||||
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
|
||||
<div
|
||||
|
@ -723,11 +711,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
aria-expanded={this.state.isSuggestionsVisible}
|
||||
data-skip-axe="aria-required-children"
|
||||
>
|
||||
<div
|
||||
role="search"
|
||||
className={inputWrapClassName}
|
||||
ref={this.queryBarInputDivRefInstance}
|
||||
>
|
||||
<div role="search" className={inputWrapClassName} ref={this.assignQueryInputDivRef}>
|
||||
<EuiTextArea
|
||||
placeholder={
|
||||
this.props.placeholder ||
|
||||
|
@ -749,11 +733,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
autoFocus={
|
||||
this.props.onChangeQueryInputFocus ? false : !this.props.disableAutoFocus
|
||||
}
|
||||
inputRef={(node: any) => {
|
||||
if (node) {
|
||||
this.inputRef = node;
|
||||
}
|
||||
}}
|
||||
inputRef={this.assignInputRef}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
aria-label={i18n.translate('data.query.queryBar.searchInputAriaLabel', {
|
||||
|
@ -810,8 +790,8 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
onClick={this.onClickSuggestion}
|
||||
onMouseEnter={this.onMouseEnterSuggestion}
|
||||
loadMore={this.increaseLimit}
|
||||
queryBarRect={this.state.queryBarRect}
|
||||
size={this.props.size}
|
||||
inputContainer={this.state.queryBarInputDiv}
|
||||
/>
|
||||
</EuiPortal>
|
||||
</div>
|
||||
|
@ -858,4 +838,21 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
return formattedNewQueryString;
|
||||
}
|
||||
}
|
||||
|
||||
private assignInputRef = (node: HTMLTextAreaElement | null) => {
|
||||
this.inputRef = node;
|
||||
};
|
||||
|
||||
private assignQueryInputDivRef = (node: HTMLDivElement | null) => {
|
||||
this.setState({ queryBarInputDiv: node });
|
||||
};
|
||||
|
||||
private onFocusWithin = () => {
|
||||
this.isFocusWithin = true;
|
||||
};
|
||||
|
||||
private onBlurWithin = () => {
|
||||
this.isFocusWithin = false;
|
||||
this.scheduleOnInputBlur();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Query } from '../../..';
|
||||
import type { QueryStringContract } from '../../../query/query_string';
|
||||
|
@ -36,5 +36,13 @@ export const useQueryStringManager = (props: UseQueryStringProps) => {
|
|||
};
|
||||
}, [props.queryStringManager]);
|
||||
|
||||
return { query };
|
||||
const stableQuery = useMemo(
|
||||
() => ({
|
||||
language: query.language,
|
||||
query: query.query,
|
||||
}),
|
||||
[query.language, query.query]
|
||||
);
|
||||
|
||||
return { query: stableQuery };
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import classNames from 'classnames';
|
|||
import React, { Component } from 'react';
|
||||
import { get, isEqual } from 'lodash';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { Query, Filter } from '@kbn/es-query';
|
||||
|
@ -186,6 +187,10 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
);
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.renderSavedQueryManagement.clear();
|
||||
}
|
||||
|
||||
private shouldRenderQueryBar() {
|
||||
const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly;
|
||||
const showQueryInput =
|
||||
|
@ -343,18 +348,6 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const savedQueryManagement = this.state.query && this.props.onClearSavedQuery && (
|
||||
<SavedQueryManagementComponent
|
||||
showSaveQuery={this.props.showSaveQuery}
|
||||
loadedSavedQuery={this.props.savedQuery}
|
||||
onSave={this.onInitiateSave}
|
||||
onSaveAsNew={this.onInitiateSaveNew}
|
||||
onLoad={this.onLoadSavedQuery}
|
||||
savedQueryService={this.savedQueryService}
|
||||
onClearSavedQuery={this.props.onClearSavedQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
const timeRangeForSuggestionsOverride = this.props.showDatePicker ? undefined : false;
|
||||
|
||||
let queryBar;
|
||||
|
@ -368,7 +361,15 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
indexPatterns={this.props.indexPatterns}
|
||||
isLoading={this.props.isLoading}
|
||||
fillSubmitButton={this.props.fillSubmitButton || false}
|
||||
prepend={this.props.showFilterBar ? savedQueryManagement : undefined}
|
||||
prepend={
|
||||
this.props.showFilterBar && this.state.query
|
||||
? this.renderSavedQueryManagement(
|
||||
this.props.onClearSavedQuery,
|
||||
this.props.showSaveQuery,
|
||||
this.props.savedQuery
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
showDatePicker={this.props.showDatePicker}
|
||||
dateRangeFrom={this.state.dateRangeFrom}
|
||||
dateRangeTo={this.state.dateRangeTo}
|
||||
|
@ -447,6 +448,28 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSavedQueryManagement = memoizeOne(
|
||||
(
|
||||
onClearSavedQuery: SearchBarOwnProps['onClearSavedQuery'],
|
||||
showSaveQuery: SearchBarOwnProps['showSaveQuery'],
|
||||
savedQuery: SearchBarOwnProps['savedQuery']
|
||||
) => {
|
||||
const savedQueryManagement = onClearSavedQuery && (
|
||||
<SavedQueryManagementComponent
|
||||
showSaveQuery={showSaveQuery}
|
||||
loadedSavedQuery={savedQuery}
|
||||
onSave={this.onInitiateSave}
|
||||
onSaveAsNew={this.onInitiateSaveNew}
|
||||
onLoad={this.onLoadSavedQuery}
|
||||
savedQueryService={this.savedQueryService}
|
||||
onClearSavedQuery={onClearSavedQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
return savedQueryManagement;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Needed for React.lazy
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SuggestionsComponent Passing the index should control which suggestion is selected 1`] = `
|
||||
<styled.div
|
||||
queryBarRect={
|
||||
Object {
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
verticalListPosition="bottom: 768px;"
|
||||
>
|
||||
<div
|
||||
className="kbnTypeahead"
|
||||
>
|
||||
<div
|
||||
className="kbnTypeahead__popover kbnTypeahead__popover--top"
|
||||
>
|
||||
<div
|
||||
id="kbnTypeahead__items"
|
||||
onScroll={[Function]}
|
||||
role="listbox"
|
||||
>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-0"
|
||||
index={0}
|
||||
innerRef={[Function]}
|
||||
key="value - as promised, not helpful"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
selected={false}
|
||||
shouldDisplayDescription={false}
|
||||
suggestion={
|
||||
Object {
|
||||
"description": "This is not a helpful suggestion",
|
||||
"end": 0,
|
||||
"start": 42,
|
||||
"text": "as promised, not helpful",
|
||||
"type": "value",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-1"
|
||||
index={1}
|
||||
innerRef={[Function]}
|
||||
key="field - yep"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
selected={true}
|
||||
shouldDisplayDescription={false}
|
||||
suggestion={
|
||||
Object {
|
||||
"description": "This is another unhelpful suggestion",
|
||||
"end": 0,
|
||||
"start": 42,
|
||||
"text": "yep",
|
||||
"type": "field",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</styled.div>
|
||||
`;
|
||||
|
||||
exports[`SuggestionsComponent Should display given suggestions if the show prop is true 1`] = `
|
||||
<styled.div
|
||||
queryBarRect={
|
||||
Object {
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
verticalListPosition="bottom: 768px;"
|
||||
>
|
||||
<div
|
||||
className="kbnTypeahead"
|
||||
>
|
||||
<div
|
||||
className="kbnTypeahead__popover kbnTypeahead__popover--top"
|
||||
>
|
||||
<div
|
||||
id="kbnTypeahead__items"
|
||||
onScroll={[Function]}
|
||||
role="listbox"
|
||||
>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-0"
|
||||
index={0}
|
||||
innerRef={[Function]}
|
||||
key="value - as promised, not helpful"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
selected={true}
|
||||
shouldDisplayDescription={false}
|
||||
suggestion={
|
||||
Object {
|
||||
"description": "This is not a helpful suggestion",
|
||||
"end": 0,
|
||||
"start": 42,
|
||||
"text": "as promised, not helpful",
|
||||
"type": "value",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-1"
|
||||
index={1}
|
||||
innerRef={[Function]}
|
||||
key="field - yep"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
selected={false}
|
||||
shouldDisplayDescription={false}
|
||||
suggestion={
|
||||
Object {
|
||||
"description": "This is another unhelpful suggestion",
|
||||
"end": 0,
|
||||
"start": 42,
|
||||
"text": "yep",
|
||||
"type": "field",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</styled.div>
|
||||
`;
|
|
@ -59,9 +59,10 @@ describe('SuggestionComponent', () => {
|
|||
});
|
||||
|
||||
it('Should call innerRef with a reference to the root div element', () => {
|
||||
const innerRefCallback = (ref: HTMLDivElement) => {
|
||||
const innerRefCallback = (index: number, ref: HTMLDivElement) => {
|
||||
expect(ref.className).toBe('kbnTypeahead__item');
|
||||
expect(ref.id).toBe('suggestion-1');
|
||||
expect(index).toBe(0);
|
||||
};
|
||||
|
||||
mount(
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { QuerySuggestion } from '../../autocomplete';
|
||||
import { SuggestionOnClick } from './types';
|
||||
import { SuggestionOnClick, SuggestionOnMouseEnter } from './types';
|
||||
|
||||
function getEuiIconType(type: string) {
|
||||
switch (type) {
|
||||
|
@ -31,16 +31,32 @@ function getEuiIconType(type: string) {
|
|||
|
||||
interface Props {
|
||||
onClick: SuggestionOnClick;
|
||||
onMouseEnter: () => void;
|
||||
onMouseEnter: SuggestionOnMouseEnter;
|
||||
selected: boolean;
|
||||
index: number;
|
||||
suggestion: QuerySuggestion;
|
||||
innerRef: (node: HTMLDivElement) => void;
|
||||
innerRef: (index: number, node: HTMLDivElement) => void;
|
||||
ariaId: string;
|
||||
shouldDisplayDescription: boolean;
|
||||
}
|
||||
|
||||
export function SuggestionComponent(props: Props) {
|
||||
export const SuggestionComponent = React.memo(function SuggestionComponent(props: Props) {
|
||||
const { index, innerRef, onClick, onMouseEnter, suggestion } = props;
|
||||
const setRef = useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
innerRef(index, node);
|
||||
},
|
||||
[index, innerRef]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(suggestion, index);
|
||||
}, [index, onClick, suggestion]);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
onMouseEnter(suggestion, index);
|
||||
}, [index, onMouseEnter, suggestion]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
|
||||
<div
|
||||
|
@ -50,9 +66,9 @@ export function SuggestionComponent(props: Props) {
|
|||
active: props.selected,
|
||||
})}
|
||||
role="option"
|
||||
onClick={() => props.onClick(props.suggestion, props.index)}
|
||||
onMouseEnter={props.onMouseEnter}
|
||||
ref={props.innerRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onClick={handleClick}
|
||||
ref={setRef}
|
||||
id={props.ariaId}
|
||||
aria-selected={props.selected}
|
||||
data-test-subj={`autocompleteSuggestion-${
|
||||
|
@ -72,4 +88,4 @@ export function SuggestionComponent(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,6 +16,8 @@ const noop = () => {
|
|||
return;
|
||||
};
|
||||
|
||||
const mockContainerDiv = document.createElement('div');
|
||||
|
||||
const mockSuggestions: QuerySuggestion[] = [
|
||||
{
|
||||
description: 'This is not a helpful suggestion',
|
||||
|
@ -43,7 +45,7 @@ describe('SuggestionsComponent', () => {
|
|||
show={false}
|
||||
suggestions={mockSuggestions}
|
||||
loadMore={noop}
|
||||
queryBarRect={{ top: 0 } as DOMRect}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -59,7 +61,7 @@ describe('SuggestionsComponent', () => {
|
|||
show={true}
|
||||
suggestions={[]}
|
||||
loadMore={noop}
|
||||
queryBarRect={{ top: 0 } as DOMRect}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -67,7 +69,7 @@ describe('SuggestionsComponent', () => {
|
|||
});
|
||||
|
||||
it('Should display given suggestions if the show prop is true', () => {
|
||||
const component = shallow(
|
||||
const component = mount(
|
||||
<SuggestionsComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
|
@ -75,16 +77,16 @@ describe('SuggestionsComponent', () => {
|
|||
show={true}
|
||||
suggestions={mockSuggestions}
|
||||
loadMore={noop}
|
||||
queryBarRect={{ top: 0 } as DOMRect}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.isEmptyRender()).toBe(false);
|
||||
expect(component).toMatchSnapshot();
|
||||
expect(component.find(SuggestionComponent)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Passing the index should control which suggestion is selected', () => {
|
||||
const component = shallow(
|
||||
const component = mount(
|
||||
<SuggestionsComponent
|
||||
index={1}
|
||||
onClick={noop}
|
||||
|
@ -92,11 +94,11 @@ describe('SuggestionsComponent', () => {
|
|||
show={true}
|
||||
suggestions={mockSuggestions}
|
||||
loadMore={noop}
|
||||
queryBarRect={{ top: 0 } as DOMRect}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
expect(component.find(SuggestionComponent).at(1).prop('selected')).toBe(true);
|
||||
});
|
||||
|
||||
it('Should call onClick with the selected suggestion when it is clicked', () => {
|
||||
|
@ -109,7 +111,7 @@ describe('SuggestionsComponent', () => {
|
|||
show={true}
|
||||
suggestions={mockSuggestions}
|
||||
loadMore={noop}
|
||||
queryBarRect={{ top: 0 } as DOMRect}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -128,12 +130,12 @@ describe('SuggestionsComponent', () => {
|
|||
show={true}
|
||||
suggestions={mockSuggestions}
|
||||
loadMore={noop}
|
||||
queryBarRect={{ top: 0 } as DOMRect}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
component.find(SuggestionComponent).at(1).simulate('mouseenter');
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import useRafState from 'react-use/lib/useRafState';
|
||||
import { QuerySuggestion } from '../../autocomplete';
|
||||
import { SuggestionComponent } from './suggestion_component';
|
||||
import {
|
||||
|
@ -17,86 +19,86 @@ import {
|
|||
SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET,
|
||||
SUGGESTIONS_LIST_REQUIRED_WIDTH,
|
||||
} from './constants';
|
||||
import { SuggestionOnClick } from './types';
|
||||
import { SuggestionOnClick, SuggestionOnMouseEnter } from './types';
|
||||
import { onRaf } from '../utils';
|
||||
import { shallowEqual } from '../../utils/shallow_equal';
|
||||
|
||||
interface SuggestionsComponentProps {
|
||||
index: number | null;
|
||||
onClick: SuggestionOnClick;
|
||||
onMouseEnter: (index: number) => void;
|
||||
onMouseEnter: SuggestionOnMouseEnter;
|
||||
show: boolean;
|
||||
suggestions: QuerySuggestion[];
|
||||
loadMore: () => void;
|
||||
queryBarRect?: DOMRect;
|
||||
size?: SuggestionsListSize;
|
||||
inputContainer: HTMLElement | null;
|
||||
}
|
||||
|
||||
export type SuggestionsListSize = 's' | 'l';
|
||||
|
||||
// Needed for React.lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default class SuggestionsComponent extends Component<SuggestionsComponentProps> {
|
||||
export default class SuggestionsComponent extends PureComponent<SuggestionsComponentProps> {
|
||||
private childNodes: HTMLDivElement[] = [];
|
||||
private parentNode: HTMLDivElement | null = null;
|
||||
|
||||
constructor(props: SuggestionsComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.assignParentNode = this.assignParentNode.bind(this);
|
||||
this.assignChildNode = this.assignChildNode.bind(this);
|
||||
}
|
||||
|
||||
private assignParentNode(node: HTMLDivElement) {
|
||||
this.parentNode = node;
|
||||
}
|
||||
|
||||
private assignChildNode(index: number, node: HTMLDivElement) {
|
||||
this.childNodes[index] = node;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.props.queryBarRect || !this.props.show || isEmpty(this.props.suggestions)) {
|
||||
if (!this.props.inputContainer || !this.props.show || isEmpty(this.props.suggestions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suggestions = this.props.suggestions.map((suggestion, index) => {
|
||||
const isDescriptionFittable =
|
||||
this.props.queryBarRect!.width >= SUGGESTIONS_LIST_REQUIRED_WIDTH;
|
||||
return (
|
||||
<SuggestionComponent
|
||||
innerRef={(node) => (this.childNodes[index] = node)}
|
||||
selected={index === this.props.index}
|
||||
index={index}
|
||||
suggestion={suggestion}
|
||||
onClick={this.props.onClick}
|
||||
onMouseEnter={() => this.props.onMouseEnter(index)}
|
||||
ariaId={'suggestion-' + index}
|
||||
key={`${suggestion.type} - ${suggestion.text}`}
|
||||
shouldDisplayDescription={isDescriptionFittable}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const renderSuggestions = (containerWidth: number) => {
|
||||
const isDescriptionFittable = containerWidth >= SUGGESTIONS_LIST_REQUIRED_WIDTH;
|
||||
const suggestions = this.props.suggestions.map((suggestion, index) => {
|
||||
return (
|
||||
<SuggestionComponent
|
||||
innerRef={this.assignChildNode}
|
||||
selected={index === this.props.index}
|
||||
index={index}
|
||||
suggestion={suggestion}
|
||||
onClick={this.props.onClick}
|
||||
onMouseEnter={this.props.onMouseEnter}
|
||||
ariaId={'suggestion-' + index}
|
||||
key={`${suggestion.type} - ${suggestion.text}`}
|
||||
shouldDisplayDescription={isDescriptionFittable}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const documentHeight = document.documentElement.clientHeight || window.innerHeight;
|
||||
const { queryBarRect } = this.props;
|
||||
|
||||
// reflects if the suggestions list has enough space below to be opened down
|
||||
const isSuggestionsListFittable =
|
||||
documentHeight - (queryBarRect.top + queryBarRect.height) >
|
||||
SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE;
|
||||
const verticalListPosition = isSuggestionsListFittable
|
||||
? `top: ${window.scrollY + queryBarRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;`
|
||||
: `bottom: ${documentHeight - (window.scrollY + queryBarRect.top)}px;`;
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSuggestionsListDiv
|
||||
queryBarRect={queryBarRect}
|
||||
verticalListPosition={verticalListPosition}
|
||||
<ResizableSuggestionsListDiv
|
||||
inputContainer={this.props.inputContainer}
|
||||
suggestionsSize={this.props.size}
|
||||
>
|
||||
<div
|
||||
className={classNames('kbnTypeahead', { 'kbnTypeahead--small': this.props.size === 's' })}
|
||||
>
|
||||
{(containerWidth: number) => (
|
||||
<div
|
||||
className={classNames('kbnTypeahead__popover', {
|
||||
['kbnTypeahead__popover--bottom']: isSuggestionsListFittable,
|
||||
['kbnTypeahead__popover--top']: !isSuggestionsListFittable,
|
||||
})}
|
||||
id="kbnTypeahead__items"
|
||||
role="listbox"
|
||||
ref={this.assignParentNode}
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
<div
|
||||
id="kbnTypeahead__items"
|
||||
role="listbox"
|
||||
ref={(node) => (this.parentNode = node)}
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
{suggestions}
|
||||
</div>
|
||||
{renderSuggestions(containerWidth)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledSuggestionsListDiv>
|
||||
)}
|
||||
</ResizableSuggestionsListDiv>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -106,7 +108,7 @@ export default class SuggestionsComponent extends Component<SuggestionsComponent
|
|||
}
|
||||
}
|
||||
|
||||
private scrollIntoView = () => {
|
||||
private scrollIntoView = onRaf(() => {
|
||||
if (this.props.index === null) {
|
||||
return;
|
||||
}
|
||||
|
@ -123,9 +125,9 @@ export default class SuggestionsComponent extends Component<SuggestionsComponent
|
|||
);
|
||||
|
||||
parent.scrollTop = scrollTop;
|
||||
};
|
||||
});
|
||||
|
||||
private handleScroll = () => {
|
||||
private handleScroll = onRaf(() => {
|
||||
if (!this.props.loadMore || !this.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
@ -141,14 +143,130 @@ export default class SuggestionsComponent extends Component<SuggestionsComponent
|
|||
if (remaining <= margin) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const StyledSuggestionsListDiv = styled.div`
|
||||
${(props: { queryBarRect: DOMRect; verticalListPosition: string }) => `
|
||||
${(props: { left: number; width: number; verticalListPosition: string }) => `
|
||||
position: absolute;
|
||||
z-index: 4001;
|
||||
left: ${props.queryBarRect.left}px;
|
||||
width: ${props.queryBarRect.width}px;
|
||||
left: ${props.left}px;
|
||||
width: ${props.width}px;
|
||||
${props.verticalListPosition}`}
|
||||
`;
|
||||
|
||||
const ResizableSuggestionsListDiv: React.FC<{
|
||||
inputContainer: HTMLElement;
|
||||
suggestionsSize?: SuggestionsListSize;
|
||||
}> = React.memo((props) => {
|
||||
const inputContainer = props.inputContainer;
|
||||
const children = props.children as (rect: DOMRect) => ReactNode;
|
||||
|
||||
const [{ documentHeight }, { pageYOffset }, containerRect] = useDimensions(inputContainer);
|
||||
|
||||
if (!containerRect) return null;
|
||||
|
||||
// reflects if the suggestions list has enough space below to be opened down
|
||||
const isSuggestionsListFittable =
|
||||
documentHeight - (containerRect.top + containerRect.height) >
|
||||
SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE;
|
||||
const verticalListPosition = isSuggestionsListFittable
|
||||
? `top: ${pageYOffset + containerRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;`
|
||||
: `bottom: ${documentHeight - (pageYOffset + containerRect.top)}px;`;
|
||||
|
||||
return (
|
||||
<StyledSuggestionsListDiv
|
||||
left={containerRect.left}
|
||||
width={containerRect.width}
|
||||
verticalListPosition={verticalListPosition}
|
||||
>
|
||||
<div
|
||||
className={classNames('kbnTypeahead', {
|
||||
'kbnTypeahead--small': props.suggestionsSize === 's',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames('kbnTypeahead__popover', {
|
||||
['kbnTypeahead__popover--bottom']: isSuggestionsListFittable,
|
||||
['kbnTypeahead__popover--top']: !isSuggestionsListFittable,
|
||||
})}
|
||||
>
|
||||
{children(containerRect)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledSuggestionsListDiv>
|
||||
);
|
||||
});
|
||||
|
||||
function useDimensions(
|
||||
container: HTMLElement | null
|
||||
): [{ documentHeight: number }, { pageYOffset: number; pageXOffset: number }, DOMRect | null] {
|
||||
const [documentHeight, setDocumentHeight] = useRafState(
|
||||
() => document.documentElement.clientHeight || window.innerHeight
|
||||
);
|
||||
|
||||
const [pageOffset, setPageOffset] = useRafState<{ pageXOffset: number; pageYOffset: number }>(
|
||||
() => ({
|
||||
pageXOffset: window.pageXOffset,
|
||||
pageYOffset: window.pageYOffset,
|
||||
})
|
||||
);
|
||||
|
||||
const [containerRect, setContainerRect] = useRafState<DOMRect | null>(() => {
|
||||
return container?.getBoundingClientRect() ?? null;
|
||||
});
|
||||
|
||||
const updateContainerRect = React.useCallback(() => {
|
||||
setContainerRect((oldRect: DOMRect | null) => {
|
||||
const newRect = container?.getBoundingClientRect() ?? null;
|
||||
const rectsEqual = shallowEqual(oldRect?.toJSON(), newRect?.toJSON());
|
||||
return rectsEqual ? oldRect : newRect;
|
||||
});
|
||||
}, [container, setContainerRect]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = () => {
|
||||
setDocumentHeight(document.documentElement.clientHeight || window.innerHeight);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handler, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handler);
|
||||
};
|
||||
}, [setDocumentHeight]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = () => {
|
||||
setPageOffset((state) => {
|
||||
const { pageXOffset, pageYOffset } = window;
|
||||
return state.pageXOffset !== pageXOffset || state.pageYOffset !== pageYOffset
|
||||
? {
|
||||
pageXOffset,
|
||||
pageYOffset,
|
||||
}
|
||||
: state;
|
||||
});
|
||||
|
||||
updateContainerRect();
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handler, { passive: true, capture: true });
|
||||
|
||||
const resizeObserver =
|
||||
typeof window.ResizeObserver !== 'undefined' &&
|
||||
new ResizeObserver(() => {
|
||||
updateContainerRect();
|
||||
});
|
||||
if (container && resizeObserver) {
|
||||
resizeObserver.observe(container);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handler, { capture: true });
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
};
|
||||
}, [setPageOffset, container, updateContainerRect]);
|
||||
|
||||
return [{ documentHeight }, pageOffset, containerRect];
|
||||
}
|
||||
|
|
|
@ -9,3 +9,5 @@
|
|||
import { QuerySuggestion } from '../../autocomplete';
|
||||
|
||||
export type SuggestionOnClick = (suggestion: QuerySuggestion, index: number) => void;
|
||||
|
||||
export type SuggestionOnMouseEnter = (suggestion: QuerySuggestion, index: number) => void;
|
||||
|
|
9
src/plugins/data/public/ui/utils/index.ts
Normal file
9
src/plugins/data/public/ui/utils/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { onRaf } from './on_raf';
|
22
src/plugins/data/public/ui/utils/on_raf.ts
Normal file
22
src/plugins/data/public/ui/utils/on_raf.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Debounce a function till next animation frame
|
||||
* @param fn
|
||||
*/
|
||||
export function onRaf(fn: Function) {
|
||||
let req: number | null;
|
||||
return (...args: unknown[]) => {
|
||||
if (req) window.cancelAnimationFrame(req);
|
||||
req = window.requestAnimationFrame(() => {
|
||||
req = null;
|
||||
fn(...args);
|
||||
});
|
||||
};
|
||||
}
|
36
src/plugins/data/public/utils/shallow_equal.ts
Normal file
36
src/plugins/data/public/utils/shallow_equal.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Shallow Equal check adapted from react-redux
|
||||
* Copy-pasted to avoid importing copy of react-redux into data plugin async chunk
|
||||
**/
|
||||
export function shallowEqual(objA: unknown, objB: unknown): boolean {
|
||||
if (Object.is(objA, objB)) return true;
|
||||
|
||||
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
|
||||
// @ts-ignore
|
||||
!Object.is(objA[keysA[i]], objB[keysA[i]])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
10
yarn.lock
10
yarn.lock
|
@ -6178,11 +6178,6 @@
|
|||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/memoize-one@^4.1.0":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.1.tgz#41dd138a4335b5041f7d8fc038f9d593d88b3369"
|
||||
integrity sha512-+9djKUUn8hOyktLCfCy4hLaIPgDNovaU36fsnZe9trFHr6ddlbIn2q0SEsnkCkNR+pBWEU440Molz/+Mpyf+gQ==
|
||||
|
||||
"@types/micromatch@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
|
||||
|
@ -19571,6 +19566,11 @@ memfs@^3.1.2:
|
|||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||
|
||||
memoizee@0.4.X:
|
||||
version "0.4.14"
|
||||
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue