Skip to content

Commit

Permalink
[ML] Anomaly Detection: Use data view esTypes instead of custom fie…
Browse files Browse the repository at this point in the history
…ld caps wrapper endpoint. (#182588)

## Summary

Fixes #182514.

The links menu for the anomalies table used a custom field caps wrapper
API endpoint to identify the type of the categorization field. This PR
changes this to use the data view API instead to use `esTypes` for the
same check. This allows us to remove the custom field caps API endpoint
completely.

Removes the route `POST /internal/ml/indices/field_caps` as it is no
longer required by the ml plugin.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
  • Loading branch information
walterra committed May 10, 2024
1 parent 977c509 commit 71e7f5a
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 304 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import moment from 'moment';
import rison from '@kbn/rison';
import type { FC } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import useMountedState from 'react-use/lib/useMountedState';

import {
EuiButtonIcon,
Expand Down Expand Up @@ -58,7 +59,6 @@ import type { SourceIndicesWithGeoFields } from '../../explorer/explorer_utils';
import { escapeDoubleQuotes, getDateFormatTz } from '../../explorer/explorer_utils';
import { usePermissionCheck } from '../../capabilities/check_capabilities';
import { useMlKibana } from '../../contexts/kibana';
import { getFieldTypeFromMapping } from '../../services/mapping_service';
import { useMlIndexUtils } from '../../util/index_service';

import { getQueryStringForInfluencers } from './get_query_string_for_influencers';
Expand All @@ -79,8 +79,13 @@ interface LinksMenuProps {
}

export const LinksMenuUI = (props: LinksMenuProps) => {
const isMounted = useMountedState();

const [dataViewId, setDataViewId] = useState<string | null>(null);
const [dataViewIdWithTemporary, setDataViewIdWithTemporary] = useState<string | null>(null);
const [openInDiscoverUrl, setOpenInDiscoverUrl] = useState<string | undefined>();
const [discoverUrlError, setDiscoverUrlError] = useState<string | undefined>();
const [openInDiscoverUrlError, setOpenInDiscoverUrlError] = useState<string | undefined>();
const [viewExamplesUrlError, setViewExamplesUrlError] = useState<string | undefined>();
const [openInLogRateAnalysisUrl, setOpenInLogRateAnalysisUrl] = useState<string | undefined>();

const [messageField, setMessageField] = useState<{
Expand All @@ -96,16 +101,46 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const {
services: { data, share, application, uiActions },
} = kibana;
const { getDataViewIdFromName } = useMlIndexUtils();
const { getDataViewById, getDataViewIdFromName } = useMlIndexUtils();

const job = useMemo(() => {
return mlJobService.getJob(props.anomaly.jobId);
}, [props.anomaly.jobId]);

const getAnomaliesMapsLink = async (anomaly: MlAnomaliesTableRecord) => {
const index = job.datafeed_config.indices[0];
const dataViewId = await getDataViewIdFromName(index);
const categorizationFieldName = job.analysis_config.categorization_field_name;
const datafeedIndices = job.datafeed_config.indices;
const indexPattern = datafeedIndices.join(',');
const autoGeneratedDiscoverLinkError = i18n.translate(
'xpack.ml.anomaliesTable.linksMenu.autoGeneratedDiscoverLinkErrorMessage',
{
defaultMessage: `Unable to link to Discover; no data view exists for index pattern '{index}'`,
values: { index: indexPattern },
}
);

useEffect(
() => {
async function initDataViewId() {
const newDataViewId = await getDataViewIdFromName(indexPattern);
setDataViewId(newDataViewId);
if (newDataViewId === null) {
setViewExamplesUrlError(autoGeneratedDiscoverLinkError);
}

const newDataViewIdWithTemporary = await getDataViewIdFromName(indexPattern, job);
setDataViewIdWithTemporary(newDataViewIdWithTemporary);
if (newDataViewIdWithTemporary === null) {
setOpenInDiscoverUrlError(autoGeneratedDiscoverLinkError);
}
}

initDataViewId();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

const getAnomaliesMapsLink = async (anomaly: MlAnomaliesTableRecord) => {
const initialLayers = getInitialAnomaliesLayers(anomaly.jobId);
const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(getDateFormatTz());
const anomalyBucketStart = anomalyBucketStartMoment.toISOString();
Expand Down Expand Up @@ -143,9 +178,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
anomaly: MlAnomaliesTableRecord,
sourceIndicesWithGeoFields: SourceIndicesWithGeoFields
) => {
const index = job.datafeed_config.indices[0];
const dataViewId = await getDataViewIdFromName(index);

// Create a layer for each of the geoFields
const initialLayers = getInitialSourceIndexFieldLayers(
sourceIndicesWithGeoFields[anomaly.jobId]
Expand Down Expand Up @@ -198,9 +230,10 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
};

useEffect(() => {
let unmounted = false;
const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR');

if (dataViewIdWithTemporary === null) return;

if (!discoverLocator) {
const discoverLocatorMissing = i18n.translate(
'xpack.ml.anomaliesTable.linksMenu.discoverLocatorMissingErrorMessage',
Expand All @@ -209,39 +242,17 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
}
);

if (!unmounted) {
setDiscoverUrlError(discoverLocatorMissing);
if (isMounted()) {
setOpenInDiscoverUrlError(discoverLocatorMissing);
setViewExamplesUrlError(discoverLocatorMissing);
}
return;
}

const getDataViewId = async () => {
const index = job.datafeed_config.indices[0];

const dataViewId = await getDataViewIdFromName(index, job);

// If data view doesn't exist for some reasons
if (!dataViewId && !unmounted) {
const autoGeneratedDiscoverLinkError = i18n.translate(
'xpack.ml.anomaliesTable.linksMenu.autoGeneratedDiscoverLinkErrorMessage',
{
defaultMessage: `Unable to link to Discover; no data view exists for index '{index}'`,
values: { index },
}
);

setDiscoverUrlError(autoGeneratedDiscoverLinkError);
}
return dataViewId;
};

(async () => {
const index = job.datafeed_config.indices[0];
const dataView = (await data.dataViews.find(index)).find(
(dv) => dv.getIndexPattern() === index
);
const dataView = dataViewId ? await getDataViewById(dataViewId) : null;

if (dataView === undefined) {
if (dataView === null) {
return;
}

Expand All @@ -260,7 +271,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
): Promise<SerializableRecord> => {
const interval = props.interval;

const dataViewId = await getDataViewId();
const record = props.anomaly.source;

// Use the exact timestamp for Log Rate Analysis,
Expand Down Expand Up @@ -333,8 +343,10 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
)}"`;
}

const indexPatternId = dataViewIdWithTemporary;

return {
indexPatternId: dataViewId,
indexPatternId,
[timeAttribute]: {
from,
to,
Expand All @@ -345,9 +357,9 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
query: kqlQuery,
},
filters:
dataViewId === null
indexPatternId === null
? []
: getFiltersForDSLQuery(job.datafeed_config.query, dataViewId, job.job_id),
: getFiltersForDSLQuery(job.datafeed_config.query, indexPatternId, job.job_id),
...(withWindowParameters
? {
wp: { bMin, bMax, dMin, dMax },
Expand All @@ -360,7 +372,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const pageState = await generateRedirectUrlPageState();
const url = await discoverLocator.getRedirectUrl(pageState);

if (!unmounted) {
if (isMounted()) {
setOpenInDiscoverUrl(url);
}
};
Expand All @@ -373,7 +385,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
(job.analysis_config.summary_count_field_name !== undefined &&
job.analysis_config.summary_count_field_name !== 'doc_count')
) {
if (!unmounted) {
if (isMounted()) {
setOpenInLogRateAnalysisUrl(undefined);
}
return;
Expand Down Expand Up @@ -410,23 +422,18 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
},
});

if (!unmounted) {
if (isMounted()) {
setOpenInLogRateAnalysisUrl(url);
}
};

if (!isCategorizationAnomalyRecord) {
generateDiscoverUrl();
generateLogRateAnalysisUrl();
} else {
getDataViewId();
}

return () => {
unmounted = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(props.anomaly)]);
}, [dataViewId, dataViewIdWithTemporary, JSON.stringify(props.anomaly)]);

const openCustomUrl = (customUrl: MlKibanaUrlConfig) => {
const { anomaly, interval, isAggregatedData } = props;
Expand Down Expand Up @@ -598,7 +605,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
window.open(singleMetricViewerLink, '_blank');
};

const viewExamples = () => {
const viewExamples = async () => {
const categoryId = props.anomaly.entityValue;
const record = props.anomaly.source;

Expand All @@ -616,35 +623,10 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
);
return;
}
const categorizationFieldName = job.analysis_config.categorization_field_name;
const datafeedIndices = job.datafeed_config.indices;

// Find the type of the categorization field i.e. text (preferred) or keyword.
// Uses the first matching field found in the list of indices in the datafeed_config.
// attempt to load the field type using each index. we have to do it this way as _field_caps
// doesn't specify which index a field came from unless there is a clash.
let i = 0;
findFieldType(datafeedIndices[i]);

const error = () => {
// eslint-disable-next-line no-console
console.log(
`viewExamples(): error finding type of field ${categorizationFieldName} in indices:`,
datafeedIndices
);
const { toasts } = kibana.services.notifications;
toasts.addDanger(
i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', {
defaultMessage:
'Unable to view examples of documents with mlcategory {categoryId} ' +
'as no mapping could be found for the categorization field {categorizationFieldName}',
values: {
categoryId,
categorizationFieldName,
},
})
);
};
if (!categorizationFieldName) {
return;
}

const createAndOpenUrl = (index: string, categorizationFieldType: string) => {
// Get the definition of the category and use the terms or regex to view the
Expand All @@ -653,12 +635,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
ml.results
.getCategoryDefinition(record.job_id, categoryId)
.then(async (resp) => {
// Find the ID of the data view with a title attribute which matches the
// index configured in the datafeed. If a Kibana data view has not been created
// for this index, then the user will see a warning message on the Discover tab advising
// them that no matching data view has been configured.
const dataViewId = await getDataViewIdFromName(index);

// We should not redirect to Discover if data view doesn't exist
if (!dataViewId) return;

Expand Down Expand Up @@ -736,23 +712,39 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
});
};

function findFieldType(index: string) {
getFieldTypeFromMapping(index, categorizationFieldName)
.then((resp: string) => {
if (resp !== '') {
createAndOpenUrl(datafeedIndices.join(), resp);
} else {
i++;
if (i < datafeedIndices.length) {
findFieldType(datafeedIndices[i]);
} else {
error();
}
}
// Find the type of the categorization field i.e. text (preferred) or keyword.
// Uses the first matching field found in the list of indices in the datafeed_config.
let fieldType;

const dataView = dataViewIdWithTemporary
? await getDataViewById(dataViewIdWithTemporary)
: null;

const field = dataView?.getFieldByName(categorizationFieldName);
if (field && Array.isArray(field.esTypes) && field.esTypes.length > 0) {
fieldType = field.esTypes[0];
}

if (fieldType) {
createAndOpenUrl(datafeedIndices.join(), fieldType);
} else {
// eslint-disable-next-line no-console
console.log(
`viewExamples(): error finding type of field ${categorizationFieldName} in indices:`,
datafeedIndices
);
const { toasts } = kibana.services.notifications;
toasts.addDanger(
i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', {
defaultMessage:
'Unable to view examples of documents with mlcategory {categoryId} ' +
'as no mapping could be found for the categorization field {categorizationFieldName}',
values: {
categoryId,
categorizationFieldName,
},
})
.catch(() => {
error();
});
);
}
};

Expand Down Expand Up @@ -782,18 +774,18 @@ export const LinksMenuUI = (props: LinksMenuProps) => {

if (application.capabilities.discover?.show && !isCategorizationAnomalyRecord) {
// Add item from the start, but disable it during the URL generation.
const isLoading = discoverUrlError === undefined && openInDiscoverUrl === undefined;
const isLoading = openInDiscoverUrlError === undefined && openInDiscoverUrl === undefined;

items.push(
<EuiContextMenuItem
key={`auto_raw_data_url`}
icon="discoverApp"
disabled={discoverUrlError !== undefined || isLoading}
disabled={openInDiscoverUrlError !== undefined || isLoading}
href={openInDiscoverUrl}
data-test-subj={`mlAnomaliesListRowAction_viewInDiscoverButton`}
>
{discoverUrlError ? (
<EuiToolTip content={discoverUrlError}>
{openInDiscoverUrlError ? (
<EuiToolTip content={openInDiscoverUrlError}>
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewInDiscover"
defaultMessage="View in Discover"
Expand Down Expand Up @@ -883,10 +875,10 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
viewExamples();
}}
data-test-subj="mlAnomaliesListRowActionViewExamplesButton"
disabled={discoverUrlError !== undefined}
disabled={viewExamplesUrlError !== undefined}
>
{discoverUrlError !== undefined ? (
<EuiToolTip content={discoverUrlError}>
{viewExamplesUrlError !== undefined ? (
<EuiToolTip content={viewExamplesUrlError}>
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewExamplesLabel"
defaultMessage="View examples"
Expand Down Expand Up @@ -976,8 +968,11 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
return items;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
dataViewId,
dataViewIdWithTemporary,
openInDiscoverUrl,
discoverUrlError,
openInDiscoverUrlError,
viewExamplesUrlError,
viewExamples,
viewSeries,
canConfigureRules,
Expand Down

0 comments on commit 71e7f5a

Please sign in to comment.