[SLO Form] Refactor to use kibana data view component (#173513)

## Summary

Fixes https://github.com/elastic/kibana/issues/171687

Refactor to use kibana data view component !!

<img width="1726" alt="image"
src="d314a300-f157-4857-80f1-fd71bf7e3e23">


user can also save a new data or use ad-hoc data view 

<img width="1728" alt="image"
src="fe81b0a1-24e7-418c-aa0d-714c4924c938">


This also avoid so many unnecessary requests being loaded, notice the
difference almost avoid 15 extra requests

### After
<img width="1725" alt="image"
src="7f2bff43-4298-4251-91da-b767a4c9d336">


### Before
<img width="1728" alt="image"
src="a6448b57-0bd3-4b6c-9440-d1d629e656de">
This commit is contained in:
Shahzad 2024-01-03 09:40:37 +01:00 committed by GitHub
parent c44ae67e20
commit d56e22101b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 123 additions and 174 deletions

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { DataView } from '@kbn/data-views-plugin/public';
import { UseFetchDataViewsResponse } from '../use_fetch_data_views';
export const useFetchDataViews = (): UseFetchDataViewsResponse => {
@ -16,10 +15,9 @@ export const useFetchDataViews = (): UseFetchDataViewsResponse => {
data: Array(20)
.fill(0)
.map((_, i) => ({
id: `dataview-${i}`,
title: `dataview-${i}`,
type: 'foo',
getName: () => `dataview-${i}`,
getIndexPattern: () => `.index-pattern-dataview-${i}`,
})) as DataView[],
})),
};
};

View file

@ -1,24 +0,0 @@
/*
* 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 { Index, UseFetchIndicesResponse } from '../use_fetch_indices';
export const useFetchIndices = (): UseFetchIndicesResponse => {
return {
isLoading: false,
isError: false,
isSuccess: true,
data: [
...Array(10)
.fill(0)
.map((_, i) => `.index-${i}`),
...Array(10)
.fill(0)
.map((_, i) => `.some-other-index-${i}`),
] as Index[],
};
};

View file

@ -22,6 +22,7 @@ export function useCreateDataView({ indexPatternString }: UseCreateDataViewProps
useEffect(() => {
const createDataView = () =>
dataViews.create({
id: `${indexPatternString}-id`,
title: indexPatternString,
allowNoIndex: true,
});

View file

@ -6,29 +6,23 @@
*/
import { useQuery } from '@tanstack/react-query';
import { DataView } from '@kbn/data-views-plugin/public';
import { DataViewListItem } from '@kbn/data-views-plugin/public';
import { useKibana } from '../utils/kibana_react';
export interface UseFetchDataViewsResponse {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data: DataView[] | undefined;
data: DataViewListItem[] | undefined;
}
interface Params {
name?: string;
size?: number;
}
export function useFetchDataViews({ name = '', size = 10 }: Params): UseFetchDataViewsResponse {
export function useFetchDataViews(): UseFetchDataViewsResponse {
const { dataViews } = useKibana().services;
const search = name.endsWith('*') ? name : `${name}*`;
const { isLoading, isError, isSuccess, data } = useQuery({
queryKey: ['fetchDataViews', search],
queryKey: ['fetchDataViewsList'],
queryFn: async () => {
return dataViews.find(search, size);
return dataViews.getIdsWithTitle();
},
retry: false,
keepPreviousData: true,

View file

@ -6,16 +6,16 @@
*/
import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, ReactNode } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import { createOptionsFromFields, Option } from '../../helpers/create_options';
import { CreateSLOForm } from '../../types';
interface Props {
indexFields: FieldSpec[];
name: 'groupBy' | 'indicator.params.timestampField';
label: React.ReactNode | string;
label: ReactNode | string;
placeholder: string;
isDisabled: boolean;
isLoading: boolean;

View file

@ -32,11 +32,14 @@ export function QueryBuilder({
required,
tooltip,
}: Props) {
const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } =
const { data, docLinks, dataViews, http, notifications, storage, uiSettings, unifiedSearch } =
useKibana().services;
const { control, getFieldState } = useFormContext<CreateSLOForm>();
const { dataView } = useCreateDataView({ indexPatternString });
const { dataView } = useCreateDataView({
indexPatternString,
});
return (
<EuiFormRow
@ -77,7 +80,7 @@ export function QueryBuilder({
disableAutoFocus
disableLanguageSwitcher
indexPatterns={dataView ? [dataView] : []}
isDisabled={!indexPatternString}
isDisabled={!dataView}
isInvalid={fieldState.invalid}
languageSwitcherPopoverAnchorPosition="rightDown"
placeholder={placeholder}

View file

@ -5,93 +5,110 @@
* 2.0.
*/
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { EuiFormRow } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public';
import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { DataViewPicker } from '@kbn/unified-search-plugin/public';
import { useKibana } from '../../../../utils/kibana_react';
import { ObservabilityPublicPluginsStart } from '../../../..';
import { useFetchDataViews } from '../../../../hooks/use_fetch_data_views';
import { useFetchIndices } from '../../../../hooks/use_fetch_indices';
import { CreateSLOForm } from '../../types';
interface Option {
label: string;
options: Array<{ value: string; label: string }>;
}
export function IndexSelection() {
const { control, getFieldState } = useFormContext<CreateSLOForm>();
const [searchValue, setSearchValue] = useState<string>('');
const { control, getFieldState, setValue, watch } = useFormContext<CreateSLOForm>();
const { dataViews: dataViewsService } = useKibana().services;
const { isLoading: isIndicesLoading, data: indices = [] } = useFetchIndices({
search: searchValue,
});
const { isLoading: isDataViewsLoading, data: dataViews = [] } = useFetchDataViews({
name: searchValue,
});
const { isLoading: isDataViewsLoading, data: dataViews = [] } = useFetchDataViews();
const options: Option[] = [];
if (!isDataViewsLoading && dataViews.length > 0) {
options.push(createDataViewsOption(dataViews));
}
if (!isIndicesLoading && !!searchValue) {
options.push(createIndexPatternOption(searchValue, indices));
}
const { dataViewEditor } = useKibana<ObservabilityPublicPluginsStart>().services;
const onSearchChange = debounce((value: string) => setSearchValue(value), 300);
const [adHocDataViews, setAdHocDataViews] = useState<DataView[]>([]);
const placeholder = i18n.translate('xpack.observability.slo.sloEdit.indexSelection.placeholder', {
defaultMessage: 'Select an index pattern',
});
const currentIndexPattern = watch('indicator.params.index');
useEffect(() => {
if (!isDataViewsLoading) {
const missingAdHocDataView =
dataViews.find((dataView) => dataView.title === currentIndexPattern) ||
adHocDataViews.find((dataView) => dataView.getIndexPattern() === currentIndexPattern);
if (!missingAdHocDataView && currentIndexPattern) {
async function loadMissingDataView() {
const dataView = await dataViewsService.create(
{
title: currentIndexPattern,
allowNoIndex: true,
},
true
);
if (dataView.getIndexPattern() === currentIndexPattern) {
setAdHocDataViews((prev) => [...prev, dataView]);
}
}
loadMissingDataView();
}
}
}, [adHocDataViews, currentIndexPattern, dataViews, dataViewsService, isDataViewsLoading]);
const getDataViewPatternById = (id?: string) => {
return (
dataViews.find((dataView) => dataView.id === id)?.title ||
adHocDataViews.find((dataView) => dataView.id === id)?.getIndexPattern()
);
};
const getDataViewIdByIndexPattern = (indexPattern: string) => {
return (
dataViews.find((dataView) => dataView.title === indexPattern) ||
adHocDataViews.find((dataView) => dataView.getIndexPattern() === indexPattern)
);
};
return (
<EuiFormRow
label={i18n.translate('xpack.observability.slo.sloEdit.customKql.indexSelection.label', {
defaultMessage: 'Index',
})}
helpText={i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.helpText',
{ defaultMessage: 'Use * to broaden your query.' }
)}
isInvalid={getFieldState('indicator.params.index').invalid}
>
<EuiFormRow label={INDEX_LABEL} isInvalid={getFieldState('indicator.params.index').invalid}>
<Controller
defaultValue=""
name="indicator.params.index"
control={control}
rules={{ required: true }}
render={({ field, fieldState }) => (
<EuiComboBox
{...field}
aria-label={placeholder}
async
data-test-subj="indexSelection"
isClearable
isInvalid={fieldState.invalid}
isLoading={isIndicesLoading && isDataViewsLoading}
placeholder={placeholder}
onChange={(selected: EuiComboBoxOptionOption[]) => {
if (selected.length) {
return field.onChange(selected[0].value);
}
field.onChange('');
<DataViewPicker
adHocDataViews={adHocDataViews}
trigger={{
label: field.value || SELECT_DATA_VIEW,
fullWidth: true,
color: 'text',
isLoading: isDataViewsLoading,
'data-test-subj': 'indexSelection',
}}
onChangeDataView={(newId: string) => {
field.onChange(getDataViewPatternById(newId));
dataViewsService.get(newId).then((dataView) => {
if (dataView.timeFieldName) {
setValue('indicator.params.timestampField', dataView.timeFieldName);
}
});
}}
currentDataViewId={getDataViewIdByIndexPattern(field.value)?.id}
onDataViewCreated={() => {
dataViewEditor.openEditor({
allowAdHocDataView: true,
onSave: (dataView: DataView) => {
if (!dataView.isPersisted()) {
setAdHocDataViews([...adHocDataViews, dataView]);
field.onChange(dataView.getIndexPattern());
} else {
field.onChange(getDataViewPatternById(dataView.id));
}
if (dataView.timeFieldName) {
setValue('indicator.params.timestampField', dataView.timeFieldName);
}
},
});
}}
options={options}
onSearchChange={onSearchChange}
selectedOptions={
!!field.value
? [
{
value: field.value,
label: field.value,
'data-test-subj': 'indexSelectionSelectedValue',
},
]
: []
}
singleSelection
/>
)}
/>
@ -99,50 +116,16 @@ export function IndexSelection() {
);
}
function createDataViewLabel(dataView: DataView) {
return `${dataView.getName()} (${dataView.getIndexPattern()})`;
}
const SELECT_DATA_VIEW = i18n.translate(
'xpack.observability.slo.sloEdit.customKql.dataViewSelection.label',
{
defaultMessage: 'Select a Data view',
}
);
function createDataViewsOption(dataViews: DataView[]): Option {
return {
label: i18n.translate('xpack.observability.slo.sloEdit.indexSelection.dataViewOptionsLabel', {
defaultMessage: 'Select an index pattern from an existing Data View',
}),
options: dataViews
.map((view) => ({
label: createDataViewLabel(view),
value: view.getIndexPattern(),
}))
.sort((a, b) => String(a.label).localeCompare(b.label)),
};
}
function createIndexPatternOption(searchValue: string, indices: string[]): Option {
const indexPattern = searchValue.endsWith('*') ? searchValue : `${searchValue}*`;
const hasMatchingIndices = indices.length > 0;
return {
label: i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternLabel',
{ defaultMessage: 'Use the index pattern' }
),
options: [
{
value: indexPattern,
label: hasMatchingIndices
? i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternFoundLabel',
{
defaultMessage:
'{searchPattern} (match {num, plural, one {# index} other {# indices}})',
values: { searchPattern: indexPattern, num: indices.length },
}
)
: i18n.translate(
'xpack.observability.slo.sloEdit.indexSelection.indexPatternNoMatchLabel',
{ defaultMessage: '{searchPattern}', values: { searchPattern: indexPattern } }
),
},
],
};
}
const INDEX_LABEL = i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.label',
{
defaultMessage: 'Index',
}
);

View file

@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n';
import { ALL_VALUE } from '@kbn/slo-schema/src/schema/common';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
import { useFetchGroupByCardinality } from '../../../../hooks/slo/use_fetch_group_by_cardinality';
import { useFetchIndexPatternFields } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
import { CreateSLOForm } from '../../types';
import { DataPreviewChart } from '../common/data_preview_chart';
import { IndexFieldSelector } from '../common/index_field_selector';
@ -24,10 +24,11 @@ export function CustomKqlIndicatorTypeForm() {
const timestampField = watch('indicator.params.timestampField');
const groupByField = watch('groupBy');
const { isLoading: isIndexFieldsLoading, data: indexFields = [] } =
useFetchIndexPatternFields(index);
const timestampFields = indexFields.filter((field) => field.type === 'date');
const groupByFields = indexFields.filter((field) => field.aggregatable);
const { dataView, loading: isIndexFieldsLoading } = useCreateDataView({
indexPatternString: index,
});
const timestampFields = dataView?.fields?.filter((field) => field.type === 'date') ?? [];
const groupByFields = dataView?.fields?.filter((field) => field.aggregatable) ?? [];
const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } =
useFetchGroupByCardinality(index, timestampField, groupByField);

View file

@ -82,10 +82,13 @@ const mockKibana = () => {
dataViews: {
find: jest.fn().mockReturnValue([]),
get: jest.fn().mockReturnValue([]),
getDefault: jest.fn(),
},
},
dataViews: {
create: jest.fn().mockResolvedValue(42),
create: jest.fn().mockResolvedValue({
getIndexPattern: jest.fn().mockReturnValue('some-index'),
}),
},
docLinks: {
links: {
@ -110,7 +113,6 @@ const mockKibana = () => {
triggersActionsUi: {
getAddRuleFlyout: jest
.fn()
.mockReturnValue(<div data-test-subj="add-rule-flyout">Add Rule Flyout</div>),
},
uiSettings: {

View file

@ -28787,7 +28787,6 @@
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration} en cours",
"xpack.observability.slo.sloDetails.overview.timeslicesBudgetingMethodDetails": "{duration} sections, {target} cible",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "{duration}",
"xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternFoundLabel": "{searchPattern} (correspond à {num, plural, one {# index} many {# index} other {# index}})",
"xpack.observability.slo.sloEdit.rollingTimeWindow.days": "{number} jours",
"xpack.observability.slo.update.errorNotification": "Un problème est survenu lors de la mise à jour de {name}",
"xpack.observability.slo.update.successNotification": "Mise à jour réussie de {name}",
@ -29250,8 +29249,6 @@
"xpack.observability.slo.sloEdit.createAlert.ruleName": "Règle d'alerte de taux d'avancement du SLO",
"xpack.observability.slo.sloEdit.createAlert.title": "Créer",
"xpack.observability.slo.sloEdit.createSloButton": "Créer un SLO",
"xpack.observability.slo.sloEdit.customKql.indexSelection.helpText": "Utilisez le caractère * pour élargir votre recherche.",
"xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternLabel": "Utiliser le modèle d'indexation",
"xpack.observability.slo.sloEdit.customKql.indexSelection.label": "Index",
"xpack.observability.slo.sloEdit.dataPreviewChart.errorMessage": "Les paramètres d'indicateur actuels ne sont pas valides",
"xpack.observability.slo.sloEdit.dataPreviewChart.explanationMessage": "Remplir les champs d'indicateur pour visualiser les indicateurs actuels",

View file

@ -28787,7 +28787,6 @@
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration}ローリング",
"xpack.observability.slo.sloDetails.overview.timeslicesBudgetingMethodDetails": "{duration}スライス、{target}ターゲット",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "過去{duration}",
"xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternFoundLabel": "{searchPattern}{num, plural, other {#個のインデックス}}と一致)",
"xpack.observability.slo.sloEdit.rollingTimeWindow.days": "{number}日",
"xpack.observability.slo.update.errorNotification": "{name}の更新中にエラーが発生しました",
"xpack.observability.slo.update.successNotification": "正常に{name}を更新しました",
@ -29250,8 +29249,6 @@
"xpack.observability.slo.sloEdit.createAlert.ruleName": "SLOバーンレートアラートルール",
"xpack.observability.slo.sloEdit.createAlert.title": "作成",
"xpack.observability.slo.sloEdit.createSloButton": "SLOの作成",
"xpack.observability.slo.sloEdit.customKql.indexSelection.helpText": "* で検索クエリの範囲を広げます。",
"xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternLabel": "インデックスパターンを使用",
"xpack.observability.slo.sloEdit.customKql.indexSelection.label": "インデックス",
"xpack.observability.slo.sloEdit.dataPreviewChart.errorMessage": "現在のインジケーター設定は無効です",
"xpack.observability.slo.sloEdit.dataPreviewChart.explanationMessage": "インジケーターフィールドに入力すると、現在のメトリックが可視化されます。",

View file

@ -28771,7 +28771,6 @@
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration} 滚动",
"xpack.observability.slo.sloDetails.overview.timeslicesBudgetingMethodDetails": "{duration} 切片,{target} 目标",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "过去 {duration}",
"xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternFoundLabel": "{searchPattern}(匹配 {num, plural, other {# 个索引}}",
"xpack.observability.slo.sloEdit.rollingTimeWindow.days": "{number} 天",
"xpack.observability.slo.update.errorNotification": "更新 {name} 时出现问题",
"xpack.observability.slo.update.successNotification": "成功更新 {name}",
@ -29234,8 +29233,6 @@
"xpack.observability.slo.sloEdit.createAlert.ruleName": "SLO 消耗速度告警规则",
"xpack.observability.slo.sloEdit.createAlert.title": "创建",
"xpack.observability.slo.sloEdit.createSloButton": "创建 SLO",
"xpack.observability.slo.sloEdit.customKql.indexSelection.helpText": "使用 * 可扩大您的查询范围。",
"xpack.observability.slo.sloEdit.customKql.indexSelection.indexPatternLabel": "使用索引模式",
"xpack.observability.slo.sloEdit.customKql.indexSelection.label": "索引",
"xpack.observability.slo.sloEdit.dataPreviewChart.errorMessage": "当前指标设置无效",
"xpack.observability.slo.sloEdit.dataPreviewChart.explanationMessage": "填写指标字段以查看当前指标的可视化",