[Observability] Shared Field Suggestion value component (#94841)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2021-03-23 09:25:00 +01:00 committed by GitHub
parent 7d303eb42d
commit 2b4bd10781
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 394 additions and 2 deletions

View file

@ -0,0 +1,101 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ComponentType, useEffect, useState } from 'react';
import { IntlProvider } from 'react-intl';
import { Observable } from 'rxjs';
import { CoreStart } from 'src/core/public';
import { text } from '@storybook/addon-knobs';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public';
import { FieldValueSelection, FieldValueSelectionProps } from '../field_value_selection';
const KibanaReactContext = createKibanaReactContext(({
uiSettings: { get: () => {}, get$: () => new Observable() },
} as unknown) as Partial<CoreStart>);
export default {
title: 'app/Shared/FieldValueSuggestions',
component: FieldValueSelection,
decorators: [
(Story: ComponentType<FieldValueSelectionProps>) => (
<IntlProvider locale="en">
<KibanaReactContext.Provider>
<EuiThemeProvider>
<FieldValueSelection
label="Service name"
values={['elastic co frontend', 'apm server', 'opbean python']}
onChange={() => {}}
value={''}
loading={false}
setQuery={() => {}}
/>
</EuiThemeProvider>
</KibanaReactContext.Provider>
</IntlProvider>
),
],
};
export function ValuesLoaded() {
return (
<FieldValueSelection
label="Service name"
values={['elastic co frontend', 'apm server', 'opbean python']}
onChange={() => {}}
value={''}
loading={false}
setQuery={() => {}}
/>
);
}
export function LoadingState() {
return (
<FieldValueSelection
label="Service name"
values={['elastic co frontend', 'apm server', 'opbean python']}
onChange={() => {}}
value={''}
loading={true}
setQuery={() => {}}
/>
);
}
export function EmptyState() {
return (
<FieldValueSelection
label="Service name"
values={[]}
onChange={() => {}}
value={''}
loading={false}
setQuery={() => {}}
/>
);
}
export function SearchState(args: FieldValueSelectionProps) {
const name = text('Query', '');
const [, setQuery] = useState('');
useEffect(() => {
setQuery(name);
}, [name]);
return (
<FieldValueSelection
label="Service name"
values={['elastic co frontend', 'apm server', 'opbean python']}
onChange={() => {}}
value={''}
loading={false}
setQuery={setQuery}
/>
);
}

View file

@ -0,0 +1,53 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount, render } from 'enzyme';
import { FieldValueSelection } from './field_value_selection';
import { EuiButton, EuiSelectableList } from '@elastic/eui';
describe('FieldValueSelection', () => {
it('renders a label for button', async () => {
const wrapper = render(
<FieldValueSelection
label="Service name"
values={['elastic co frontend', 'apm server', 'opbean python']}
onChange={() => {}}
value={''}
loading={false}
setQuery={() => {}}
/>
);
const btn = wrapper.find('[data-test-subj=fieldValueSelectionBtn]');
expect(btn.text()).toBe('Service name');
});
it('renders a list on click', async () => {
const wrapper = mount(
<FieldValueSelection
label="Service name"
values={['elastic co frontend', 'apm server', 'opbean python']}
onChange={() => {}}
value={''}
loading={false}
setQuery={() => {}}
/>
);
const btn = wrapper.find(EuiButton);
btn.simulate('click');
const list = wrapper.find(EuiSelectableList);
expect((list.props() as any).visibleOptions).toEqual([
{ label: 'elastic co frontend' },
{ label: 'apm server' },
{ label: 'opbean python' },
]);
});
});

View file

@ -0,0 +1,131 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FormEvent, Fragment, useEffect, useState, Dispatch, SetStateAction } from 'react';
import {
EuiButton,
EuiPopover,
EuiPopoverFooter,
EuiPopoverTitle,
EuiSelectable,
EuiSelectableOption,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export interface FieldValueSelectionProps {
value?: string;
label: string;
loading: boolean;
onChange: (val?: string) => void;
values?: string[];
setQuery: Dispatch<SetStateAction<string>>;
}
const formatOptions = (values?: string[], value?: string): EuiSelectableOption[] => {
return (values ?? []).map((val) => ({
label: val,
...(value === val ? { checked: 'on' } : {}),
}));
};
export function FieldValueSelection({
label,
value,
loading,
values,
setQuery,
onChange: onSelectionChange,
}: FieldValueSelectionProps) {
const [options, setOptions] = useState<EuiSelectableOption[]>(formatOptions(values, value));
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
useEffect(() => {
setOptions(formatOptions(values, value));
}, [values, value]);
const onButtonClick = () => {
setIsPopoverOpen(!isPopoverOpen);
};
const closePopover = () => {
setIsPopoverOpen(false);
};
const onChange = (optionsN: EuiSelectableOption[]) => {
setOptions(optionsN);
};
const onValueChange = (evt: FormEvent<HTMLInputElement>) => {
setQuery((evt.target as HTMLInputElement).value);
};
const button = (
<EuiButton
size="s"
iconType="arrowDown"
iconSide="right"
onClick={onButtonClick}
data-test-subj={'fieldValueSelectionBtn'}
>
{label}
</EuiButton>
);
return (
<Fragment>
<EuiPopover
id="popover"
panelPaddingSize="none"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
>
<EuiSelectable
searchable
singleSelection
searchProps={{
placeholder: i18n.translate('xpack.observability.fieldValueSelection.placeholder', {
defaultMessage: 'Filter {label}',
values: { label },
}),
compressed: true,
onInput: onValueChange,
}}
options={options}
onChange={onChange}
isLoading={loading}
>
{(list, search) => (
<div style={{ width: 240 }}>
<EuiPopoverTitle paddingSize="s">{search}</EuiPopoverTitle>
{list}
<EuiPopoverFooter paddingSize="s">
<EuiButton
size="s"
fullWidth
disabled={
!value &&
(options.length === 0 || !options.find((opt) => opt?.checked === 'on'))
}
onClick={() => {
const selected = options.find((opt) => opt?.checked === 'on');
onSelectionChange(selected?.label);
setIsPopoverOpen(false);
}}
>
{i18n.translate('xpack.observability.fieldValueSelection.apply', {
defaultMessage: 'Apply',
})}
</EuiButton>
</EuiPopoverFooter>
</div>
)}
</EuiSelectable>
</EuiPopover>
</Fragment>
);
}

View file

@ -0,0 +1,56 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { useDebounce } from 'react-use';
import { useValuesList } from '../../../hooks/use_values_list';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { FieldValueSelection } from './field_value_selection';
export interface FieldValueSuggestionsProps {
value?: string;
label: string;
indexPattern: IIndexPattern;
sourceField: string;
onChange: (val?: string) => void;
}
export function FieldValueSuggestions({
sourceField,
label,
indexPattern,
value,
onChange: onSelectionChange,
}: FieldValueSuggestionsProps) {
const [query, setQuery] = useState('');
const [debouncedValue, setDebouncedValue] = useState('');
const { values, loading } = useValuesList({ indexPattern, query, sourceField });
useDebounce(
() => {
setQuery(debouncedValue);
},
400,
[debouncedValue]
);
return (
<FieldValueSelection
values={values as string[]}
label={label}
onChange={onSelectionChange}
setQuery={setDebouncedValue}
loading={loading}
value={value}
/>
);
}
// eslint-disable-next-line import/no-default-export
export default FieldValueSuggestions;

View file

@ -6,7 +6,8 @@
*/
import React, { lazy, Suspense } from 'react';
import { CoreVitalProps, HeaderMenuPortalProps } from './types';
import type { CoreVitalProps, HeaderMenuPortalProps } from './types';
import type { FieldValueSuggestionsProps } from './field_value_suggestions';
const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index'));
@ -27,3 +28,13 @@ export function HeaderMenuPortal(props: HeaderMenuPortalProps) {
</Suspense>
);
}
const FieldValueSuggestionsLazy = lazy(() => import('./field_value_suggestions/index'));
export function FieldValueSuggestions(props: FieldValueSuggestionsProps) {
return (
<Suspense fallback={null}>
<FieldValueSuggestionsLazy {...props} />
</Suspense>
);
}

View 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IIndexPattern } from '../../../../../src/plugins/data/common';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { useFetcher } from './use_fetcher';
import { ESFilter } from '../../../../../typings/elasticsearch';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
interface Props {
sourceField: string;
query?: string;
indexPattern: IIndexPattern;
filters?: ESFilter[];
}
export const useValuesList = ({ sourceField, indexPattern, query, filters }: Props) => {
const {
services: { data },
} = useKibana<{ data: DataPublicPluginStart }>();
const { data: values, status } = useFetcher(() => {
return data.autocomplete.getValueSuggestions({
indexPattern,
query: query || '',
field: indexPattern.fields.find(({ name }) => name === sourceField)!,
boolFilter: filters ?? [],
});
}, [sourceField, query, data.autocomplete, indexPattern, filters]);
return { values, loading: status === 'loading' || status === 'pending' };
};

View file

@ -18,7 +18,11 @@ export const plugin: PluginInitializer<ObservabilityPluginSetup, ObservabilityPl
export * from './components/shared/action_menu/';
export type { UXMetrics } from './components/shared/core_web_vitals/';
export { getCoreVitalsComponent, HeaderMenuPortal } from './components/shared/';
export {
getCoreVitalsComponent,
HeaderMenuPortal,
FieldValueSuggestions,
} from './components/shared/';
export {
useTrackPageview,