mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
feat(slo): improve index selection input (#149786)
This commit is contained in:
parent
15ddb87cdf
commit
c4ea96e5ce
7 changed files with 195 additions and 83 deletions
|
@ -11,8 +11,17 @@ export const useFetchIndices = (): UseFetchIndicesResponse => {
|
|||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
indices: Array.from({ length: 5 }, (_, i) => ({
|
||||
name: `.index${i}`,
|
||||
})) as Index[],
|
||||
indices: [
|
||||
...Array(10)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
name: `.index-${i}`,
|
||||
})),
|
||||
...Array(10)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
name: `.some-other-index-${i}`,
|
||||
})),
|
||||
] as Index[],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { KibanaReactStorybookDecorator } from '../../../../utils/kibana_react.storybook_decorator';
|
||||
import { IndexSelection as Component, Props } from './index_selection';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/EditPage/CustomKQL/IndexSelection',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Component> = (props: Props) => {
|
||||
const methods = useForm({ defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES });
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<Component {...props} control={methods.control} />
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultProps = {};
|
||||
|
||||
export const IndexSelection = Template.bind({});
|
||||
IndexSelection.args = defaultProps;
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { useFetchIndices, Index } from '../../../../hooks/use_fetch_indices';
|
||||
|
||||
export interface Props {
|
||||
control: Control<CreateSLOInput>;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
options: Array<{ value: string; label: string }>;
|
||||
}
|
||||
|
||||
export function IndexSelection({ control }: Props) {
|
||||
const { loading, indices = [] } = useFetchIndices();
|
||||
const [indexOptions, setIndexOptions] = useState<Option[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setIndexOptions([createIndexOptions(indices)]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [indices.length]);
|
||||
|
||||
const onSearchChange = (search: string) => {
|
||||
const options: Option[] = [];
|
||||
if (!search) {
|
||||
return setIndexOptions([createIndexOptions(indices)]);
|
||||
}
|
||||
|
||||
const searchPattern = search.endsWith('*') ? search.substring(0, search.length - 1) : search;
|
||||
const matchingIndices = indices.filter(({ name }) => name.startsWith(searchPattern));
|
||||
|
||||
if (matchingIndices.length === 0) {
|
||||
return setIndexOptions([]);
|
||||
}
|
||||
|
||||
options.push(createIndexOptions(matchingIndices));
|
||||
|
||||
const searchWithStarSuffix = search.endsWith('*') ? search : `${search}*`;
|
||||
options.push({
|
||||
label: i18n.translate(
|
||||
'xpack.observability.slos.sloEdit.customKql.indexSelection.indexPatternLabel',
|
||||
{ defaultMessage: 'Use an index pattern' }
|
||||
),
|
||||
options: [{ value: searchWithStarSuffix, label: searchWithStarSuffix }],
|
||||
});
|
||||
|
||||
setIndexOptions(options);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.observability.slos.sloEdit.customKql.indexSelection.label', {
|
||||
defaultMessage: 'Index',
|
||||
})}
|
||||
helpText={i18n.translate(
|
||||
'xpack.observability.slos.sloEdit.customKql.indexSelection.helpText',
|
||||
{
|
||||
defaultMessage: 'Use * to broaden your query.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Controller
|
||||
name="indicator.params.index"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field, fieldState }) => (
|
||||
<EuiComboBox
|
||||
{...field}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.observability.slos.sloEdit.customKql.indexSelection.placeholder',
|
||||
{
|
||||
defaultMessage: 'Select an index or index pattern',
|
||||
}
|
||||
)}
|
||||
async
|
||||
data-test-subj="indexSelection"
|
||||
isClearable={true}
|
||||
isInvalid={!!fieldState.error}
|
||||
isLoading={loading}
|
||||
onChange={(selected: EuiComboBoxOptionOption[]) => {
|
||||
if (selected.length) {
|
||||
return field.onChange(selected[0].value);
|
||||
}
|
||||
|
||||
field.onChange('');
|
||||
}}
|
||||
onSearchChange={onSearchChange}
|
||||
options={indexOptions}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slos.sloEdit.customKql.indexSelection.placeholder',
|
||||
{
|
||||
defaultMessage: 'Select an index or index pattern',
|
||||
}
|
||||
)}
|
||||
selectedOptions={
|
||||
!!field.value
|
||||
? [
|
||||
{
|
||||
value: field.value,
|
||||
label: field.value,
|
||||
'data-test-subj': 'indexSelectionSelectedValue',
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
singleSelection
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
function createIndexOptions(indices: Index[]): Option {
|
||||
return {
|
||||
label: i18n.translate(
|
||||
'xpack.observability.slos.sloEdit.customKql.indexSelection.indexOptionsLabel',
|
||||
{ defaultMessage: 'Select an existing index' }
|
||||
),
|
||||
options: indices
|
||||
.map(({ name }) => ({ label: name, value: name }))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label)),
|
||||
};
|
||||
}
|
|
@ -50,7 +50,7 @@ export function SloEditForm({ slo }: Props) {
|
|||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const { control, watch, getFieldState, getValues, formState, trigger } = useForm({
|
||||
const { control, watch, getFieldState, getValues, formState } = useForm({
|
||||
defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||
values: transformSloResponseToCreateSloInput(slo),
|
||||
mode: 'all',
|
||||
|
@ -144,7 +144,7 @@ export function SloEditForm({ slo }: Props) {
|
|||
<EuiSpacer size="xxl" />
|
||||
|
||||
{watch('indicator.type') === 'sli.kql.custom' ? (
|
||||
<SloEditFormDefinitionCustomKql control={control} trigger={trigger} />
|
||||
<SloEditFormDefinitionCustomKql control={control} />
|
||||
) : null}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -26,7 +26,7 @@ const Template: ComponentStory<typeof Component> = (props: Props) => {
|
|||
const methods = useForm({ defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES });
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<Component {...props} control={methods.control} trigger={methods.trigger} />
|
||||
<Component {...props} control={methods.control} />
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,89 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiSuggest } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Control, Controller, UseFormTrigger } from 'react-hook-form';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import type { CreateSLOInput } from '@kbn/slo-schema';
|
||||
|
||||
import { useFetchIndices } from '../../../hooks/use_fetch_indices';
|
||||
import { IndexSelection } from './custom_kql/index_selection';
|
||||
|
||||
export interface Props {
|
||||
control: Control<CreateSLOInput>;
|
||||
trigger: UseFormTrigger<CreateSLOInput>;
|
||||
}
|
||||
|
||||
export function SloEditFormDefinitionCustomKql({ control, trigger }: Props) {
|
||||
const { loading, indices = [] } = useFetchIndices();
|
||||
|
||||
const indicesNames = indices.map(({ name }) => ({
|
||||
type: { iconType: '', color: '' },
|
||||
label: name,
|
||||
description: '',
|
||||
}));
|
||||
|
||||
// Indices are loading in asynchrously, so trigger field validation
|
||||
// once results are returned from API
|
||||
useEffect(() => {
|
||||
if (!loading && indices.length) {
|
||||
trigger();
|
||||
}
|
||||
}, [indices.length, loading, trigger]);
|
||||
|
||||
function valueMatchIndex(value: string | undefined, index: string): boolean {
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.length > 0 && value.substring(value.length - 1) === '*') {
|
||||
return index.indexOf(value.substring(0, value.length - 1), 0) > -1;
|
||||
}
|
||||
|
||||
return index === value;
|
||||
}
|
||||
|
||||
export function SloEditFormDefinitionCustomKql({ control }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.observability.slos.sloEdit.sloDefinition.customKql.index', {
|
||||
defaultMessage: 'Index',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
|
||||
<Controller
|
||||
name="indicator.params.index"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
validate: (value) => indices.some((index) => valueMatchIndex(value, index.name)),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<EuiSuggest
|
||||
fullWidth
|
||||
isClearable
|
||||
aria-label="Indices"
|
||||
data-test-subj="sloFormCustomKqlIndexInput"
|
||||
status={loading ? 'loading' : field.value ? 'unchanged' : 'unchanged'}
|
||||
onItemClick={({ label }) => {
|
||||
field.onChange(label);
|
||||
}}
|
||||
isInvalid={
|
||||
fieldState.isDirty &&
|
||||
!indicesNames.some((index) => valueMatchIndex(field.value, index.label))
|
||||
}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observability.slos.sloEdit.sloDefinition.customKql.index.selectIndex',
|
||||
{
|
||||
defaultMessage: 'Select an index',
|
||||
}
|
||||
)}
|
||||
suggestions={indicesNames}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<IndexSelection control={control} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -166,9 +166,8 @@ describe('SLO Edit Page', () => {
|
|||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.index
|
||||
);
|
||||
expect(screen.queryByTestId('indexSelectionSelectedValue')).toBeNull();
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
||||
|
@ -222,7 +221,7 @@ describe('SLO Edit Page', () => {
|
|||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlIndexInput'), 'some-index');
|
||||
userEvent.type(screen.getByTestId('indexSelection'), 'some-index');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlFilterQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlGoodQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormCustomKqlTotalQueryInput'), 'irrelevant');
|
||||
|
@ -307,9 +306,10 @@ describe('SLO Edit Page', () => {
|
|||
slo.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
||||
slo.indicator.params.index
|
||||
expect(screen.queryByTestId('indexSelectionSelectedValue')).toHaveTextContent(
|
||||
slo.indicator.params.index!
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.filter : ''
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue