diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss index 4c0ecd8f9ce44..c231c405b5369 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss @@ -1,4 +1,5 @@ @import 'pages/analytics_exploration/components/exploration/index'; +@import 'pages/analytics_exploration/components/regression_exploration/index'; @import 'pages/analytics_management/components/analytics_list/index'; @import 'pages/analytics_management/components/create_analytics_form/index'; @import 'pages/analytics_management/components/create_analytics_flyout/index'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index 04dff6e0b4dc5..b1eedc1378d43 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -9,8 +9,11 @@ import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { idx } from '@kbn/elastic-idx'; +import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; +import { Dictionary } from '../../../common/types/common'; import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; +import { SavedSearchQuery } from '../../contexts/kibana'; export type IndexName = string; export type IndexPattern = string; @@ -38,8 +41,8 @@ export enum INDEX_STATUS { } export interface Eval { - meanSquaredError: number | ''; - rSquared: number | ''; + meanSquaredError: number | string; + rSquared: number | string; error: null | string; } @@ -119,6 +122,13 @@ export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; }; +export const isRegressionResultsSearchBoolQuery = ( + arg: any +): arg is RegressionResultsSearchBoolQuery => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === 'bool'; +}; + export interface DataFrameAnalyticsConfig { id: DataFrameAnalyticsId; // Description attribute is not supported yet @@ -212,6 +222,42 @@ export function getValuesFromResponse(response: RegressionEvaluateResponse) { return { meanSquaredError, rSquared }; } +interface RegressionResultsSearchBoolQuery { + bool: Dictionary; +} +interface RegressionResultsSearchTermQuery { + term: Dictionary; +} + +export type RegressionResultsSearchQuery = + | RegressionResultsSearchBoolQuery + | RegressionResultsSearchTermQuery + | SavedSearchQuery; + +export function getEvalQueryBody({ + resultsField, + isTraining, + searchQuery, + ignoreDefaultQuery, +}: { + resultsField: string; + isTraining: boolean; + searchQuery?: RegressionResultsSearchQuery; + ignoreDefaultQuery?: boolean; +}) { + let query: RegressionResultsSearchQuery = { + term: { [`${resultsField}.is_training`]: { value: isTraining } }, + }; + + if (searchQuery !== undefined && ignoreDefaultQuery === true) { + query = searchQuery; + } else if (isRegressionResultsSearchBoolQuery(searchQuery)) { + const searchQueryClone = cloneDeep(searchQuery); + searchQueryClone.bool.must.push(query); + query = searchQueryClone; + } + return query; +} export const loadEvalData = async ({ isTraining, @@ -219,12 +265,16 @@ export const loadEvalData = async ({ dependentVariable, resultsField, predictionFieldName, + searchQuery, + ignoreDefaultQuery, }: { isTraining: boolean; index: string; dependentVariable: string; resultsField: string; predictionFieldName?: string; + searchQuery?: RegressionResultsSearchQuery; + ignoreDefaultQuery?: boolean; }) => { const results: LoadEvaluateResult = { success: false, eval: null, error: null }; const defaultPredictionField = `${dependentVariable}_prediction`; @@ -232,7 +282,7 @@ export const loadEvalData = async ({ predictionFieldName ? predictionFieldName : defaultPredictionField }`; - const query = { term: { [`${resultsField}.is_training`]: { value: isTraining } } }; + const query = getEvalQueryBody({ resultsField, isTraining, searchQuery, ignoreDefaultQuery }); const config = { index, diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss new file mode 100644 index 0000000000000..bb948785d3efa --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss @@ -0,0 +1 @@ +@import 'regression_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss new file mode 100644 index 0000000000000..edcc9870ff93b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss @@ -0,0 +1,3 @@ +.mlDataFrameAnalyticsRegression__evaluateStat { + padding-top: $euiSizeL; +} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx index d0633d586063a..9765192f0e446 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx @@ -62,6 +62,29 @@ export const ErrorCallout: FC = ({ error }) => {

); + } else if (error.includes('userProvidedQueryBuilder')) { + // query bar syntax is incorrect + errorCallout = ( + +

+ {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody', + { + defaultMessage: + 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', + } + )} +

+
+ ); } return {errorCallout}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 20ab6678da820..8bb44da74087c 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -6,7 +6,8 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { ErrorCallout } from './error_callout'; import { getValuesFromResponse, @@ -16,33 +17,42 @@ import { Eval, DataFrameAnalyticsConfig, } from '../../../../common'; +import { ml } from '../../../../../services/ml_api_service'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { EvaluateStat } from './evaluate_stat'; +import { + getEvalQueryBody, + isRegressionResultsSearchBoolQuery, + RegressionResultsSearchQuery, +} from '../../../../common/analytics'; +import { SearchQuery } from './use_explore_data'; interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus: DATA_FRAME_TASK_STATE; + searchQuery: RegressionResultsSearchQuery; +} + +interface TrackTotalHitsSearchResponse { + hits: { + total: { + value: number; + relation: string; + }; + hits: any[]; + }; } -const meanSquaredErrorText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', - { - defaultMessage: 'Mean squared error', - } -); -const rSquaredText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', - { - defaultMessage: 'R squared', - } -); const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; -export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { +export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { const [trainingEval, setTrainingEval] = useState(defaultEval); const [generalizationEval, setGeneralizationEval] = useState(defaultEval); const [isLoadingTraining, setIsLoadingTraining] = useState(false); const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false); + const [trainingDocsCount, setTrainingDocsCount] = useState(null); + const [generalizationDocsCount, setGeneralizationDocsCount] = useState(null); const index = jobConfig.dest.index; const dependentVariable = getDependentVar(jobConfig.analysis); @@ -50,9 +60,42 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { // default is 'ml' const resultsField = jobConfig.dest.results_field; - const loadData = async () => { + const loadDocsCount = async ({ + ignoreDefaultQuery = true, + isTraining, + }: { + ignoreDefaultQuery?: boolean; + isTraining: boolean; + }): Promise<{ + docsCount: number | null; + success: boolean; + }> => { + const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery }); + + try { + const body: SearchQuery = { + track_total_hits: true, + query, + }; + + const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ + index: jobConfig.dest.index, + size: 0, + body, + }); + + const docsCount = resp.hits.total && resp.hits.total.value; + return { docsCount, success: true }; + } catch (e) { + return { + docsCount: null, + success: false, + }; + } + }; + + const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => { setIsLoadingGeneralization(true); - setIsLoadingTraining(true); const genErrorEval = await loadEvalData({ isTraining: false, @@ -60,6 +103,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { dependentVariable, resultsField, predictionFieldName, + searchQuery, + ignoreDefaultQuery, }); if (genErrorEval.success === true && genErrorEval.eval) { @@ -78,6 +123,10 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { error: genErrorEval.error, }); } + }; + + const loadTrainingData = async (ignoreDefaultQuery: boolean = true) => { + setIsLoadingTraining(true); const trainingErrorEval = await loadEvalData({ isTraining: true, @@ -85,6 +134,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { dependentVariable, resultsField, predictionFieldName, + searchQuery, + ignoreDefaultQuery, }); if (trainingErrorEval.success === true && trainingErrorEval.eval) { @@ -100,14 +151,89 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { setTrainingEval({ meanSquaredError: '', rSquared: '', - error: genErrorEval.error, + error: trainingErrorEval.error, + }); + } + }; + + const loadData = async ({ + isTrainingClause, + }: { + isTrainingClause?: { query: string; operator: string }; + }) => { + // searchBar query is filtering for testing data + if (isTrainingClause !== undefined && isTrainingClause.query === 'false') { + loadGeneralizationData(); + + const docsCountResp = await loadDocsCount({ isTraining: false }); + if (docsCountResp.success === true) { + setGeneralizationDocsCount(docsCountResp.docsCount); + } else { + setGeneralizationDocsCount(null); + } + + setTrainingDocsCount(0); + setTrainingEval({ + meanSquaredError: '--', + rSquared: '--', + error: null, }); + } else if (isTrainingClause !== undefined && isTrainingClause.query === 'true') { + // searchBar query is filtering for training data + loadTrainingData(); + + const docsCountResp = await loadDocsCount({ isTraining: true }); + if (docsCountResp.success === true) { + setTrainingDocsCount(docsCountResp.docsCount); + } else { + setTrainingDocsCount(null); + } + + setGeneralizationDocsCount(0); + setGeneralizationEval({ + meanSquaredError: '--', + rSquared: '--', + error: null, + }); + } else { + // No is_training clause/filter from search bar so load both + loadGeneralizationData(false); + const genDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: false, + }); + if (genDocsCountResp.success === true) { + setGeneralizationDocsCount(genDocsCountResp.docsCount); + } else { + setGeneralizationDocsCount(null); + } + + loadTrainingData(false); + const trainDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: true, + }); + if (trainDocsCountResp.success === true) { + setTrainingDocsCount(trainDocsCountResp.docsCount); + } else { + setTrainingDocsCount(null); + } } }; useEffect(() => { - loadData(); - }, []); + const hasIsTrainingClause = + isRegressionResultsSearchBoolQuery(searchQuery) && + searchQuery.bool.must.filter( + (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined + ); + const isTrainingClause = + hasIsTrainingClause && + hasIsTrainingClause[0] && + hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + + loadData({ isTrainingClause }); + }, [JSON.stringify(searchQuery)]); return ( @@ -129,7 +255,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { - + {i18n.translate( 'xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle', @@ -139,27 +265,32 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { )} + {generalizationDocsCount !== null && ( + + + + )} {generalizationEval.error !== null && } {generalizationEval.error === null && ( - - @@ -167,7 +298,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { - + {i18n.translate( 'xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle', @@ -177,27 +308,32 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => { )} + {trainingDocsCount !== null && ( + + + + )} {trainingEval.error !== null && } {trainingEval.error === null && ( - - diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx new file mode 100644 index 0000000000000..692a2afc729d5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface Props { + isLoading: boolean; + title: number | string; + isMSE: boolean; +} + +const meanSquaredErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', + { + defaultMessage: 'Mean squared error', + } +); +const rSquaredText = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', + { + defaultMessage: 'R squared', + } +); +const meanSquaredErrorTooltipContent = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent', + { + defaultMessage: + 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.', + } +); +const rSquaredTooltipContent = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent', + { + defaultMessage: + 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.', + } +); + +export const EvaluateStat: FC = ({ isLoading, isMSE, title }) => ( + + + + + + + + +); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 7beea07f9502d..b188334934ae0 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -12,6 +12,8 @@ import { DataFrameAnalyticsConfig } from '../../../../common'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { defaultSearchQuery } from './use_explore_data'; +import { RegressionResultsSearchQuery } from '../../../../common/analytics'; interface GetDataFrameAnalyticsResponse { count: number; @@ -44,6 +46,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { const [jobConfig, setJobConfig] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const loadJobConfig = async () => { setIsLoadingJobConfig(true); @@ -98,12 +101,16 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { {isLoadingJobConfig === true && jobConfig === undefined && } {isLoadingJobConfig === false && jobConfig !== undefined && ( - + )} {isLoadingJobConfig === true && jobConfig === undefined && } {isLoadingJobConfig === false && jobConfig !== undefined && ( - + )} ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 5ba3b8ed45939..a1d4261d2cf32 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -62,301 +62,280 @@ const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus: DATA_FRAME_TASK_STATE; + setEvaluateSearchQuery: React.Dispatch>; } -export const ResultsTable: FC = React.memo(({ jobConfig, jobStatus }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(25); - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [searchError, setSearchError] = useState(undefined); - const [searchString, setSearchString] = useState(undefined); - - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } +export const ResultsTable: FC = React.memo( + ({ jobConfig, jobStatus, setEvaluateSearchQuery }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchError, setSearchError] = useState(undefined); + const [searchString, setSearchString] = useState(undefined); + + function toggleColumnsPopover() { + setColumnsPopoverVisible(!isColumnsPopoverVisible); + } - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } + function closeColumnsPopover() { + setColumnsPopoverVisible(false); + } - function toggleColumn(column: EsFieldName) { - if (tableItems.length > 0 && jobConfig !== undefined) { - // spread to a new array otherwise the component wouldn't re-render - setSelectedFields([...toggleSelectedField(selectedFields, column)]); + function toggleColumn(column: EsFieldName) { + if (tableItems.length > 0 && jobConfig !== undefined) { + // spread to a new array otherwise the component wouldn't re-render + setSelectedFields([...toggleSelectedField(selectedFields, column)]); + } } - } - const { - errorMessage, - loadExploreData, - sortField, - sortDirection, - status, - tableItems, - } = useExploreData(jobConfig, selectedFields, setSelectedFields); - - let docFields: EsFieldName[] = []; - let docFieldsCount = 0; - if (tableItems.length > 0) { - docFields = Object.keys(tableItems[0]); - docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)); - docFieldsCount = docFields.length; - } + const { + errorMessage, + loadExploreData, + sortField, + sortDirection, + status, + tableItems, + } = useExploreData(jobConfig, selectedFields, setSelectedFields); + + let docFields: EsFieldName[] = []; + let docFieldsCount = 0; + if (tableItems.length > 0) { + docFields = Object.keys(tableItems[0]); + docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)); + docFieldsCount = docFields.length; + } - const columns: ColumnType[] = []; - - if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { - columns.push( - ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { - const column: ColumnType = { - field: k, - name: k, - sortable: true, - truncateText: true, - }; - - const render = (d: any, fullItem: EsDoc) => { - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent', + const columns: ColumnType[] = []; + + if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { + columns.push( + ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { + const column: ColumnType = { + field: k, + name: k, + sortable: true, + truncateText: true, + }; + + const render = (d: any, fullItem: EsDoc) => { + if (Array.isArray(d) && d.every(item => typeof item === 'string')) { + // If the cells data is an array of strings, return as a comma separated list. + // The list will get limited to 5 items with `…` at the end if there's more in the original array. + return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; + } else if (Array.isArray(d)) { + // If the cells data is an array of e.g. objects, display a 'array' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + - - ); - } else if (typeof d === 'object' && d !== null) { - // If the cells data is an object, display a 'object' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent', + > + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent', + { + defaultMessage: 'array', + } + )} + + + ); + } else if (typeof d === 'object' && d !== null) { + // If the cells data is an object, display a 'object' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + - - ); - } - - return d; - }; + > + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent', + { + defaultMessage: 'object', + } + )} + + + ); + } - let columnType; + return d; + }; - if (tableItems.length > 0) { - columnType = typeof tableItems[0][k]; - } + let columnType; - if (typeof columnType !== 'undefined') { - switch (columnType) { - case 'boolean': - column.dataType = 'boolean'; - break; - case 'Date': - column.align = 'right'; - column.render = (d: any) => - formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - break; - case 'number': - column.dataType = 'number'; - column.render = render; - break; - default: - column.render = render; - break; + if (tableItems.length > 0) { + columnType = typeof tableItems[0][k]; } - } else { - column.render = render; - } - return column; - }) - ); - } + if (typeof columnType !== 'undefined') { + switch (columnType) { + case 'boolean': + column.dataType = 'boolean'; + break; + case 'Date': + column.align = 'right'; + column.render = (d: any) => + formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); + break; + case 'number': + column.dataType = 'number'; + column.render = render; + break; + default: + column.render = render; + break; + } + } else { + column.render = render; + } - useEffect(() => { - if (jobConfig !== undefined) { - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis + return column; + }) ); - const predictedFieldSelected = selectedFields.includes(predictedFieldName); - - const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction, searchQuery }); } - }, [JSON.stringify(searchQuery)]); - - useEffect(() => { - // by default set the sorting to descending on the prediction field (`_prediction`). - // if that's not available sort ascending on the first column. - // also check if the current sorting field is still available. - if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const predictedFieldSelected = selectedFields.includes(predictedFieldName); - const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction, searchQuery }); + useEffect(() => { + if (jobConfig !== undefined) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [JSON.stringify(searchQuery)]); + + useEffect(() => { + // by default set the sorting to descending on the prediction field (`_prediction`). + // if that's not available sort ascending on the first column. + // also check if the current sorting field is still available. + if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); + + let sorting: SortingPropType = false; + let onTableChange; + + if (columns.length > 0 && sortField !== '' && sortField !== undefined) { + sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: sortField, direction: sortDirection }, + }: OnTableChangeArg) => { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + + if (sort.field !== sortField || sort.direction !== sortDirection) { + loadExploreData({ ...sort, searchQuery }); + } + }; } - }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); - let sorting: SortingPropType = false; - let onTableChange; - - if (columns.length > 0 && sortField !== '' && sortField !== undefined) { - sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, + const pagination = { + initialPageIndex: pageIndex, + initialPageSize: pageSize, + totalItemCount: tableItems.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + hidePerPageOptions: false, }; - onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: sortField, direction: sortDirection }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - if (sort.field !== sortField || sort.direction !== sortDirection) { - loadExploreData({ ...sort, searchQuery }); + const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { + if (error) { + setSearchError(error.message); + } else { + try { + const esQueryDsl = Query.toESQuery(query); + setSearchQuery(esQueryDsl); + setSearchString(query.text); + setSearchError(undefined); + // set query for use in evaluate panel + setEvaluateSearchQuery(esQueryDsl); + } catch (e) { + setSearchError(e.toString()); + } } }; - } - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: tableItems.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - hidePerPageOptions: false, - }; - - const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { - if (error) { - setSearchError(error.message); - } else { - try { - const esQueryDsl = Query.toESQuery(query); - setSearchQuery(esQueryDsl); - setSearchString(query.text); - setSearchError(undefined); - } catch (e) { - setSearchError(e.toString()); - } - } - }; - - const search = { - onChange: onQueryChange, - defaultQuery: searchString, - box: { - incremental: false, - placeholder: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', - { - defaultMessage: 'E.g. avg>0.5', - } - ), - }, - filters: [ - { - type: 'field_value_toggle_group', - field: `${jobConfig.dest.results_field}.is_training`, - items: [ - { - value: false, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', - { - defaultMessage: 'Testing', - } - ), - }, + const search = { + onChange: onQueryChange, + defaultQuery: searchString, + box: { + incremental: false, + placeholder: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', { - value: true, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', - { - defaultMessage: 'Training', - } - ), - }, - ], + defaultMessage: 'E.g. avg>0.5', + } + ), }, - ], - }; - - if (jobConfig === undefined) { - return null; - } - - if (status === INDEX_STATUS.ERROR) { - return ( - - - - - - - {getTaskStateBadge(jobStatus)} - - - -

{errorMessage}

-
-
- ); - } + filters: [ + { + type: 'field_value_toggle_group', + field: `${jobConfig.dest.results_field}.is_training`, + items: [ + { + value: false, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', + { + defaultMessage: 'Testing', + } + ), + }, + { + value: true, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', + { + defaultMessage: 'Training', + } + ), + }, + ], + }, + ], + }; - return ( - - - + if (jobConfig === undefined) { + return null; + } + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + return ( + @@ -365,103 +344,136 @@ export const ResultsTable: FC = React.memo(({ jobConfig, jobStatus }) => {getTaskStateBadge(jobStatus)} - - - - - {docFieldsCount > MAX_COLUMNS && ( + +

{errorMessage}

+
+
+ ); + } + + const tableError = + status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') + ? errorMessage + : searchError; + + return ( + + + + + + + + + {getTaskStateBadge(jobStatus)} + + + + + + + {docFieldsCount > MAX_COLUMNS && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', + { + defaultMessage: + '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', + values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + } + )} + + )} + + - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + } - )} - - )} - - - - + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', { - defaultMessage: 'Select columns', + defaultMessage: 'Select fields', } )} - /> + +
+ {docFields.map(d => ( + toggleColumn(d)} + disabled={selectedFields.includes(d) && selectedFields.length === 1} + /> + ))} +
+
+
+
+
+
+
+ {status === INDEX_STATUS.LOADING && } + {status !== INDEX_STATUS.LOADING && ( + + )} + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + + {tableItems.length === SEARCH_SIZE && ( + - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', - { - defaultMessage: 'Select fields', - } - )} - -
- {docFields.map(d => ( - toggleColumn(d)} - disabled={selectedFields.includes(d) && selectedFields.length === 1} - /> - ))} -
- - -
-
- - - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - + + )} - > - - - - - - )} -
- ); -}); + + + + )} + + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index 3e7266eb89474..332451c6e4d7a 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -49,7 +49,8 @@ export interface UseExploreDataReturnType { tableItems: TableItem[]; } -interface SearchQuery { +export interface SearchQuery { + track_total_hits?: boolean; query: SavedSearchQuery; sort?: any; }