Skip to content

Commit

Permalink
[SLO] Adding Data Views to index pattern selector (#158033)
Browse files Browse the repository at this point in the history
## Summary

This PR adds support for picking Data Views to the index selector for
the Custom KQL indicator. However, this doesn't actually attach the Data
View to the SLO definition, this is only a convenient way to re-use the
index pattern defined in the Data View. Keep in mind that if a user
updates a Data View, they will need to update the SLO as well since the
SLO service uses transforms behind the scenes; transforms do not support
Kibana Data Views and there isn't a way to detect when the Data View is
updated to re-create the transform.

This PR also filters out hidden indices OR indices whose names are
prefixed with a dot.

<img width="953" alt="image"
src="https://github.com/elastic/kibana/assets/41702/ef477f63-f89d-4733-aa19-95138d735967">
  • Loading branch information
simianhacker committed May 18, 2023
1 parent 839da37 commit a9b13c0
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 36 deletions.
46 changes: 46 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_fetch_data_views.ts
@@ -0,0 +1,46 @@
/*
* 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 {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
useQuery,
} from '@tanstack/react-query';
import { DataView } from '@kbn/data-views-plugin/public';
import { useKibana } from '../utils/kibana_react';

export interface UseFetchDataViewsResponse {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
dataViews: DataView[] | undefined;
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<Index[], unknown>>;
}
export interface Index {
name: string;
}

export function useFetchDataViews(): UseFetchDataViewsResponse {
const { dataViews } = useKibana().services;

const { isLoading, isError, isSuccess, data, refetch } = useQuery({
queryKey: ['fetchDataViews'],
queryFn: async () => {
try {
const response = await dataViews.find('');
return response;
} catch (error) {
throw new Error(`Something went wrong. Error: ${error}`);
}
},
});

return { isLoading, isError, isSuccess, dataViews: data, refetch };
}
Expand Up @@ -10,7 +10,9 @@ import { Controller, useFormContext } from 'react-hook-form';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CreateSLOInput } from '@kbn/slo-schema';
import { DataView } from '@kbn/data-views-plugin/public';

import { useFetchDataViews } from '../../../../hooks/use_fetch_data_views';
import { useFetchIndices, Index } from '../../../../hooks/use_fetch_indices';

interface Option {
Expand All @@ -20,28 +22,33 @@ interface Option {

export function IndexSelection() {
const { control, getFieldState } = useFormContext<CreateSLOInput>();
const { isLoading, indices = [] } = useFetchIndices();
const { isLoading: isIndicesLoading, indices = [] } = useFetchIndices();
const { isLoading: isDataViewsLoading, dataViews = [] } = useFetchDataViews();
const [indexOptions, setIndexOptions] = useState<Option[]>([]);

useEffect(() => {
setIndexOptions([createIndexOptions(indices)]);
setIndexOptions(createIndexOptions(indices, dataViews));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [indices.length]);
}, [indices.length, dataViews.length]);

const onSearchChange = (search: string) => {
const options: Option[] = [];
if (!search) {
return setIndexOptions([createIndexOptions(indices)]);
return setIndexOptions(createIndexOptions(indices, dataViews));
}

const searchPattern = search.endsWith('*') ? search.substring(0, search.length - 1) : search;
const matchingIndices = indices.filter(({ name }) => name.startsWith(searchPattern));
const matchingDataViews = dataViews.filter(
(view) =>
view.getName().startsWith(searchPattern) || view.getIndexPattern().startsWith(searchPattern)
);

if (matchingIndices.length === 0) {
if (matchingIndices.length === 0 && matchingDataViews.length === 0) {
return setIndexOptions([]);
}

options.push(createIndexOptions(matchingIndices));
createIndexOptions(matchingIndices, matchingDataViews).map((option) => options.push(option));

const searchWithStarSuffix = search.endsWith('*') ? search : `${search}*`;
options.push({
Expand All @@ -55,6 +62,13 @@ export function IndexSelection() {
setIndexOptions(options);
};

const placeholder = i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.placeholder',
{
defaultMessage: 'Select an index or index pattern',
}
);

return (
<EuiFormRow
label={i18n.translate('xpack.observability.slo.sloEdit.customKql.indexSelection.label', {
Expand All @@ -77,17 +91,12 @@ export function IndexSelection() {
render={({ field, fieldState }) => (
<EuiComboBox
{...field}
aria-label={i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.placeholder',
{
defaultMessage: 'Select an index or index pattern',
}
)}
aria-label={placeholder}
async
data-test-subj="indexSelection"
isClearable
isInvalid={fieldState.invalid}
isLoading={isLoading}
isLoading={isIndicesLoading && isDataViewsLoading}
onChange={(selected: EuiComboBoxOptionOption[]) => {
if (selected.length) {
return field.onChange(selected[0].value);
Expand All @@ -97,22 +106,9 @@ export function IndexSelection() {
}}
onSearchChange={onSearchChange}
options={indexOptions}
placeholder={i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.placeholder',
{
defaultMessage: 'Select an index or index pattern',
}
)}
placeholder={placeholder}
selectedOptions={
!!field.value
? [
{
value: field.value,
label: field.value,
'data-test-subj': 'indexSelectionSelectedValue',
},
]
: []
!!field.value ? [findSelectedIndexPattern(dataViews, field.value)] : []
}
singleSelection
/>
Expand All @@ -122,14 +118,53 @@ export function IndexSelection() {
);
}

function createIndexOptions(indices: Index[]): Option {
function findSelectedIndexPattern(dataViews: DataView[], indexPattern: string) {
const selectedDataView = dataViews.find((view) => view.getIndexPattern() === indexPattern);
if (selectedDataView) {
return {
value: selectedDataView.getIndexPattern(),
label: createDataViewLabel(selectedDataView),
'data-test-subj': 'indexSelectionSelectedValue',
};
}

return {
label: i18n.translate(
'xpack.observability.slo.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)),
value: indexPattern,
label: indexPattern,
'data-test-subj': 'indexSelectionSelectedValue',
};
}

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

function createIndexOptions(indices: Index[], dataViews: DataView[]): Option[] {
const options = [
{
label: i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.indexOptionsLabel',
{ defaultMessage: 'Select an existing index' }
),
options: indices
.filter(({ name }) => !name.startsWith('.'))
.map(({ name }) => ({ label: name, value: name }))
.sort((a, b) => String(a.label).localeCompare(b.label)),
},
];
if (dataViews.length > 0) {
options.unshift({
label: i18n.translate(
'xpack.observability.slo.sloEdit.customKql.indexSelection.dataViewOptionsLabel',
{ defaultMessage: 'Select an existing Data View' }
),
options: dataViews
.map((view) => ({
label: createDataViewLabel(view),
value: view.getIndexPattern(),
}))
.sort((a, b) => String(a.label).localeCompare(b.label)),
});
}
return options;
}

0 comments on commit a9b13c0

Please sign in to comment.