Skip to content

Commit

Permalink
[SLO Form] Refactor to use kibana data view component (#173513)
Browse files Browse the repository at this point in the history
## Summary

Fixes #171687

Refactor to use kibana data view component !!

<img width="1726" alt="image"
src="https://github.com/elastic/kibana/assets/3505601/d314a300-f157-4857-80f1-fd71bf7e3e23">


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

<img width="1728" alt="image"
src="https://github.com/elastic/kibana/assets/3505601/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="https://github.com/elastic/kibana/assets/3505601/7f2bff43-4298-4251-91da-b767a4c9d336">


### Before
<img width="1728" alt="image"
src="https://github.com/elastic/kibana/assets/3505601/a6448b57-0bd3-4b6c-9440-d1d629e656de">
  • Loading branch information
shahzad31 authored Jan 3, 2024
1 parent c44ae67 commit d56e221
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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[],
})),
};
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function useCreateDataView({ indexPatternString }: UseCreateDataViewProps
useEffect(() => {
const createDataView = () =>
dataViews.create({
id: `${indexPatternString}-id`,
title: indexPatternString,
allowNoIndex: true,
});
Expand Down
16 changes: 5 additions & 11 deletions x-pack/plugins/observability/public/hooks/use_fetch_data_views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,7 +80,7 @@ export function QueryBuilder({
disableAutoFocus
disableLanguageSwitcher
indexPatterns={dataView ? [dataView] : []}
isDisabled={!indexPatternString}
isDisabled={!dataView}
isInvalid={fieldState.invalid}
languageSwitcherPopoverAnchorPosition="rightDown"
placeholder={placeholder}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,144 +5,127 @@
* 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 [adHocDataViews, setAdHocDataViews] = useState<DataView[]>([]);

const currentIndexPattern = watch('indicator.params.index');

const onSearchChange = debounce((value: string) => setSearchValue(value), 300);
useEffect(() => {
if (!isDataViewsLoading) {
const missingAdHocDataView =
dataViews.find((dataView) => dataView.title === currentIndexPattern) ||
adHocDataViews.find((dataView) => dataView.getIndexPattern() === currentIndexPattern);

const placeholder = i18n.translate('xpack.observability.slo.sloEdit.indexSelection.placeholder', {
defaultMessage: 'Select an index pattern',
});
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
/>
)}
/>
</EuiFormRow>
);
}

function createDataViewLabel(dataView: DataView) {
return `${dataView.getName()} (${dataView.getIndexPattern()})`;
}

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;
const SELECT_DATA_VIEW = i18n.translate(
'xpack.observability.slo.sloEdit.customKql.dataViewSelection.label',
{
defaultMessage: 'Select a Data view',
}
);

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',
}
);
Loading

0 comments on commit d56e221

Please sign in to comment.