mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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 {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
indices: Array.from({ length: 5 }, (_, i) => ({
|
indices: [
|
||||||
name: `.index${i}`,
|
...Array(10)
|
||||||
})) as Index[],
|
.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 },
|
notifications: { toasts },
|
||||||
} = useKibana().services;
|
} = useKibana().services;
|
||||||
|
|
||||||
const { control, watch, getFieldState, getValues, formState, trigger } = useForm({
|
const { control, watch, getFieldState, getValues, formState } = useForm({
|
||||||
defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES,
|
defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES,
|
||||||
values: transformSloResponseToCreateSloInput(slo),
|
values: transformSloResponseToCreateSloInput(slo),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
|
@ -144,7 +144,7 @@ export function SloEditForm({ slo }: Props) {
|
||||||
<EuiSpacer size="xxl" />
|
<EuiSpacer size="xxl" />
|
||||||
|
|
||||||
{watch('indicator.type') === 'sli.kql.custom' ? (
|
{watch('indicator.type') === 'sli.kql.custom' ? (
|
||||||
<SloEditFormDefinitionCustomKql control={control} trigger={trigger} />
|
<SloEditFormDefinitionCustomKql control={control} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
|
@ -26,7 +26,7 @@ const Template: ComponentStory<typeof Component> = (props: Props) => {
|
||||||
const methods = useForm({ defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES });
|
const methods = useForm({ defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES });
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<Component {...props} control={methods.control} trigger={methods.trigger} />
|
<Component {...props} control={methods.control} />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,89 +5,23 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiSuggest } from '@elastic/eui';
|
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiSuggest } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
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 type { CreateSLOInput } from '@kbn/slo-schema';
|
||||||
|
|
||||||
import { useFetchIndices } from '../../../hooks/use_fetch_indices';
|
import { IndexSelection } from './custom_kql/index_selection';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
control: Control<CreateSLOInput>;
|
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 (
|
return (
|
||||||
<EuiFlexGroup direction="column" gutterSize="l">
|
<EuiFlexGroup direction="column" gutterSize="l">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiFormLabel>
|
<IndexSelection control={control} />
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
|
|
|
@ -166,9 +166,8 @@ describe('SLO Edit Page', () => {
|
||||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
expect(screen.queryByTestId('indexSelectionSelectedValue')).toBeNull();
|
||||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.index
|
|
||||||
);
|
|
||||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
||||||
|
@ -222,7 +221,7 @@ describe('SLO Edit Page', () => {
|
||||||
|
|
||||||
render(<SloEditPage />, config);
|
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('sloFormCustomKqlFilterQueryInput'), 'irrelevant');
|
||||||
userEvent.type(screen.getByTestId('sloFormCustomKqlGoodQueryInput'), 'irrelevant');
|
userEvent.type(screen.getByTestId('sloFormCustomKqlGoodQueryInput'), 'irrelevant');
|
||||||
userEvent.type(screen.getByTestId('sloFormCustomKqlTotalQueryInput'), 'irrelevant');
|
userEvent.type(screen.getByTestId('sloFormCustomKqlTotalQueryInput'), 'irrelevant');
|
||||||
|
@ -307,9 +306,10 @@ describe('SLO Edit Page', () => {
|
||||||
slo.indicator.type
|
slo.indicator.type
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId('sloFormCustomKqlIndexInput')).toHaveValue(
|
expect(screen.queryByTestId('indexSelectionSelectedValue')).toHaveTextContent(
|
||||||
slo.indicator.params.index
|
slo.indicator.params.index!
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
expect(screen.queryByTestId('sloFormCustomKqlFilterQueryInput')).toHaveValue(
|
||||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.filter : ''
|
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.filter : ''
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue