Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SLO] Adding Data Views to index pattern selector #158033

Merged
merged 3 commits into from May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}