[Observability] Shared Field Suggestion value component (#94841)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7d303eb42d
commit
2b4bd10781
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
36
x-pack/plugins/observability/public/hooks/use_values_list.ts
Normal file
36
x-pack/plugins/observability/public/hooks/use_values_list.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; 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' };
|
||||
};
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue