[ML] DataFrame Analytics use field caps api to set column type (#54543)

* wip: initialize newJobCaps service in parent element

* wip: use jobCaps service to create columns

* add render and types to talble columns

* add keyword suffix when constructing query. ensure pagination works

* Ensure search query and sorting works

* wip: update regression table to use jobCaps api

* move shared resources to central location

* ensure 0 and false values show up in table

* add error handling to jobCaps initialization

* ensure outlier detection table can toggle columns

* check for undefined before using moment to create date

* add tests for fix for getNestedProperty
This commit is contained in:
Melissa Alvarez 2020-01-14 08:58:36 -05:00 committed by GitHub
parent 45f8ca90a3
commit 69730cef73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 619 additions and 389 deletions

View file

@ -23,7 +23,7 @@ export interface Field {
id: FieldId;
name: string;
type: ES_FIELD_TYPES;
aggregatable: boolean;
aggregatable?: boolean;
aggIds?: AggId[];
aggs?: Aggregation[];
}

View file

@ -13,6 +13,7 @@ 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';
import { SortDirection } from '../../components/ml_in_memory_table';
export type IndexName = string;
export type IndexPattern = string;
@ -39,6 +40,13 @@ interface ClassificationAnalysis {
};
}
export interface LoadExploreDataArg {
field: string;
direction: SortDirection;
searchQuery: SavedSearchQuery;
requiresKeyword?: boolean;
}
export const SEARCH_SIZE = 1000;
export const defaultSearchQuery = {
@ -182,7 +190,7 @@ export const getPredictedFieldName = (
const defaultPredictionField = `${getDependentVar(analysis)}_prediction`;
const predictedField = `${resultsField}.${
predictionFieldName ? predictionFieldName : defaultPredictionField
}${isClassificationAnalysis(analysis) && !forSort ? '.keyword' : ''}`;
}`;
return predictedField;
};

View file

@ -5,7 +5,15 @@
*/
import { getNestedProperty } from '../../util/object_utils';
import { DataFrameAnalyticsConfig, getPredictedFieldName, getDependentVar } from './analytics';
import {
DataFrameAnalyticsConfig,
getPredictedFieldName,
getDependentVar,
getPredictionFieldName,
} from './analytics';
import { Field } from '../../../../common/types/fields';
import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../services/new_job_capabilities_service';
export type EsId = string;
export type EsDocSource = Record<string, any>;
@ -19,8 +27,41 @@ export interface EsDoc extends Record<string, any> {
export const MAX_COLUMNS = 20;
export const DEFAULT_REGRESSION_COLUMNS = 8;
export const BASIC_NUMERICAL_TYPES = new Set([
ES_FIELD_TYPES.LONG,
ES_FIELD_TYPES.INTEGER,
ES_FIELD_TYPES.SHORT,
ES_FIELD_TYPES.BYTE,
]);
export const EXTENDED_NUMERICAL_TYPES = new Set([
ES_FIELD_TYPES.DOUBLE,
ES_FIELD_TYPES.FLOAT,
ES_FIELD_TYPES.HALF_FLOAT,
ES_FIELD_TYPES.SCALED_FLOAT,
]);
const ML__ID_COPY = 'ml__id_copy';
export const isKeywordAndTextType = (fieldName: string): boolean => {
const { fields } = newJobCapsService;
const fieldType = fields.find(field => field.name === fieldName)?.type;
let isBothTypes = false;
// If it's a keyword type - check if it has a corresponding text type
if (fieldType !== undefined && fieldType === ES_FIELD_TYPES.KEYWORD) {
const field = newJobCapsService.getFieldById(fieldName.replace(/\.keyword$/, ''));
isBothTypes = field !== null && field.type === ES_FIELD_TYPES.TEXT;
} else if (fieldType !== undefined && fieldType === ES_FIELD_TYPES.TEXT) {
// If text, check if has corresponding keyword type
const field = newJobCapsService.getFieldById(`${fieldName}.keyword`);
isBothTypes = field !== null && field.type === ES_FIELD_TYPES.KEYWORD;
}
return isBothTypes;
};
// Used to sort columns:
// - string based columns are moved to the left
// - followed by the outlier_score column
@ -90,10 +131,10 @@ export const sortRegressionResultsFields = (
if (b === predictedField) {
return 1;
}
if (a === dependentVariable) {
if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) {
return -1;
}
if (b === dependentVariable) {
if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) {
return 1;
}
@ -200,6 +241,50 @@ export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFi
return flatDocFields.filter(f => f !== ML__ID_COPY);
}
export const getDefaultFieldsFromJobCaps = (
fields: Field[],
jobConfig: DataFrameAnalyticsConfig
): { selectedFields: Field[]; docFields: Field[] } => {
const fieldsObj = { selectedFields: [], docFields: [] };
if (fields.length === 0) {
return fieldsObj;
}
const dependentVariable = getDependentVar(jobConfig.analysis);
const type = newJobCapsService.getFieldById(dependentVariable)?.type;
const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
// default is 'ml'
const resultsField = jobConfig.dest.results_field;
const defaultPredictionField = `${dependentVariable}_prediction`;
const predictedField = `${resultsField}.${
predictionFieldName ? predictionFieldName : defaultPredictionField
}`;
const allFields: any = [
{
id: `${resultsField}.is_training`,
name: `${resultsField}.is_training`,
type: ES_FIELD_TYPES.BOOLEAN,
},
{ id: predictedField, name: predictedField, type },
...fields,
].sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig));
let selectedFields = allFields
.slice(0, DEFAULT_REGRESSION_COLUMNS * 2)
.filter((field: any) => field.name === predictedField || !field.name.includes('.keyword'));
if (selectedFields.length > DEFAULT_REGRESSION_COLUMNS) {
selectedFields = selectedFields.slice(0, DEFAULT_REGRESSION_COLUMNS);
}
return {
selectedFields,
docFields: allFields,
};
};
export const getDefaultClassificationFields = (
docs: EsDoc[],
jobConfig: DataFrameAnalyticsConfig
@ -290,11 +375,12 @@ export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string):
.slice(0, MAX_COLUMNS);
};
export const toggleSelectedField = (
export const toggleSelectedFieldSimple = (
selectedFields: EsFieldName[],
column: EsFieldName
): EsFieldName[] => {
const index = selectedFields.indexOf(column);
if (index === -1) {
selectedFields.push(column);
} else {
@ -302,3 +388,16 @@ export const toggleSelectedField = (
}
return selectedFields;
};
export const toggleSelectedField = (selectedFields: Field[], column: EsFieldName): Field[] => {
const index = selectedFields.map(field => field.name).indexOf(column);
if (index === -1) {
const columnField = newJobCapsService.getFieldById(column);
if (columnField !== null) {
selectedFields.push(columnField);
}
} else {
selectedFields.splice(index, 1);
}
return selectedFields;
};

View file

@ -33,11 +33,13 @@ export {
getDefaultSelectableFields,
getDefaultRegressionFields,
getDefaultClassificationFields,
getDefaultFieldsFromJobCaps,
getFlattenedFields,
sortColumns,
sortRegressionResultsColumns,
sortRegressionResultsFields,
toggleSelectedField,
toggleSelectedFieldSimple,
EsId,
EsDoc,
EsDocSource,

View file

@ -14,6 +14,10 @@ import { ResultsTable } from './results_table';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
import { LoadingPanel } from '../loading_panel';
import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { useKibanaContext } from '../../../../../contexts/kibana';
interface GetDataFrameAnalyticsResponse {
count: number;
@ -31,6 +35,21 @@ export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
</EuiTitle>
);
const jobConfigErrorTitle = i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError',
{
defaultMessage:
'Unable to fetch results. An error occurred loading the job configuration data.',
}
);
const jobCapsErrorTitle = i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError',
{
defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.",
}
);
interface Props {
jobId: string;
jobStatus: DATA_FRAME_TASK_STATE;
@ -39,8 +58,13 @@ interface Props {
export const ClassificationExploration: FC<Props> = ({ jobId, jobStatus }) => {
const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined);
const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState<undefined | string>(
undefined
);
const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery);
const kibanaContext = useKibanaContext();
const loadJobConfig = async () => {
setIsLoadingJobConfig(true);
@ -78,23 +102,41 @@ export const ClassificationExploration: FC<Props> = ({ jobId, jobStatus }) => {
loadJobConfig();
}, []);
if (jobConfigErrorMessage !== undefined) {
const initializeJobCapsService = async () => {
if (jobConfig !== undefined) {
try {
const sourceIndex = jobConfig.source.index[0];
const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId);
if (indexPattern !== undefined) {
await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false);
}
setIsInitialized(true);
} catch (e) {
if (e.message !== undefined) {
setJobCapsServiceErrorMessage(e.message);
} else {
setJobCapsServiceErrorMessage(JSON.stringify(e));
}
}
}
};
useEffect(() => {
initializeJobCapsService();
}, [JSON.stringify(jobConfig)]);
if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) {
return (
<EuiPanel grow={false}>
<ExplorationTitle jobId={jobId} />
<EuiSpacer />
<EuiCallOut
title={i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError',
{
defaultMessage:
'Unable to fetch results. An error occurred loading the job configuration data.',
}
)}
title={jobConfigErrorMessage ? jobConfigErrorTitle : jobCapsErrorTitle}
color="danger"
iconType="cross"
>
<p>{jobConfigErrorMessage}</p>
<p>{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}</p>
</EuiCallOut>
</EuiPanel>
);
@ -103,12 +145,12 @@ export const ClassificationExploration: FC<Props> = ({ jobId, jobStatus }) => {
return (
<Fragment>
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
{isLoadingJobConfig === false && jobConfig !== undefined && (
{isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
<EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} />
)}
<EuiSpacer />
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
{isLoadingJobConfig === false && jobConfig !== undefined && (
{isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
<ResultsTable
jobConfig={jobConfig}
jobStatus={jobStatus}

View file

@ -28,6 +28,7 @@ import {
loadDocsCount,
DataFrameAnalyticsConfig,
} from '../../../../common';
import { isKeywordAndTextType } from '../../../../common/fields';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import {
@ -37,13 +38,8 @@ import {
ResultsSearchQuery,
ANALYSIS_CONFIG_TYPE,
} from '../../../../common/analytics';
import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns';
import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import { LoadingPanel } from '../loading_panel';
import { getColumnData } from './column_data';
import { useKibanaContext } from '../../../../../contexts/kibana';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
const defaultPanelWidth = 500;
@ -66,10 +62,8 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
const [visibleColumns, setVisibleColumns] = useState(() =>
columns.map(({ id }: { id: string }) => id)
);
const kibanaContext = useKibanaContext();
const index = jobConfig.dest.index;
const sourceIndex = jobConfig.source.index[0];
const dependentVariable = getDependentVar(jobConfig.analysis);
const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
// default is 'ml'
@ -86,25 +80,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
setIsLoading(true);
try {
const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId);
if (indexPattern !== undefined) {
await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false);
// If dependent_variable is of type keyword and text .keyword suffix is required for evaluate endpoint
const { fields } = newJobCapsService;
const depVarFieldType = fields.find(field => field.name === dependentVariable)?.type;
// If it's a keyword type - check if it has a corresponding text type
if (depVarFieldType !== undefined && depVarFieldType === ES_FIELD_TYPES.KEYWORD) {
const field = newJobCapsService.getFieldById(dependentVariable.replace(/\.keyword$/, ''));
requiresKeyword = field !== null && field.type === ES_FIELD_TYPES.TEXT;
} else if (depVarFieldType !== undefined && depVarFieldType === ES_FIELD_TYPES.TEXT) {
// If text, check if has corresponding keyword type
const field = newJobCapsService.getFieldById(`${dependentVariable}.keyword`);
requiresKeyword = field !== null && field.type === ES_FIELD_TYPES.KEYWORD;
}
}
requiresKeyword = isKeywordAndTextType(dependentVariable);
} catch (e) {
// Additional error handling due to missing field type is handled by loadEvalData
console.error('Unable to load new field types', error); // eslint-disable-line no-console
@ -359,9 +335,9 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
<Fragment />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} style={{ width: '90%' }}>
<EuiDataGrid
aria-label="Data grid demo"
aria-label="Classification confusion matrix"
columns={columns}
columnVisibility={{ visibleColumns, setVisibleColumns }}
rowCount={columnsData.length}

View file

@ -27,6 +27,7 @@ import {
} from '@elastic/eui';
import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common';
import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import {
ColumnType,
@ -37,20 +38,25 @@ import {
} from '../../../../../components/ml_in_memory_table';
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
import { Field } from '../../../../../../../common/types/fields';
import { SavedSearchQuery } from '../../../../../contexts/kibana';
import {
BASIC_NUMERICAL_TYPES,
EXTENDED_NUMERICAL_TYPES,
isKeywordAndTextType,
} from '../../../../common/fields';
import {
sortRegressionResultsColumns,
sortRegressionResultsFields,
toggleSelectedField,
EsDoc,
DataFrameAnalyticsConfig,
EsFieldName,
EsDoc,
MAX_COLUMNS,
getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
getDependentVar,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
@ -71,12 +77,20 @@ export const ResultsTable: FC<Props> = React.memo(
({ jobConfig, jobStatus, setEvaluateSearchQuery }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(25);
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
const [selectedFields, setSelectedFields] = useState([] as Field[]);
const [docFields, setDocFields] = useState([] as Field[]);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
const [searchError, setSearchError] = useState<any>(undefined);
const [searchString, setSearchString] = useState<string | undefined>(undefined);
const predictedFieldName = getPredictedFieldName(
jobConfig.dest.results_field,
jobConfig.analysis
);
const dependentVariable = getDependentVar(jobConfig.analysis);
function toggleColumnsPopover() {
setColumnsPopoverVisible(!isColumnsPopoverVisible);
}
@ -99,147 +113,140 @@ export const ResultsTable: FC<Props> = React.memo(
sortDirection,
status,
tableItems,
} = useExploreData(jobConfig, selectedFields, setSelectedFields);
} = useExploreData(jobConfig, selectedFields, setSelectedFields, setDocFields);
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: Array<ColumnType<TableItem>> = selectedFields.map(field => {
const { type } = field;
const isNumber =
type !== undefined &&
(BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
const columns: Array<ColumnType<TableItem>> = [];
const column: ColumnType<TableItem> = {
field: field.name,
name: field.name,
sortable: true,
truncateText: true,
};
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
columns.push(
...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
const column: ColumnType<TableItem> = {
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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent',
{
defaultMessage:
'The full content of this array based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
{
defaultMessage: 'array',
}
)}
</EuiBadge>
</EuiToolTip>
);
}
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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent',
{
defaultMessage:
'The full content of this array based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
{
defaultMessage: 'array',
}
)}
</EuiBadge>
</EuiToolTip>
);
} 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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexObjectToolTipContent',
{
defaultMessage:
'The full content of this object based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexObjectBadgeContent',
{
defaultMessage: 'object',
}
)}
</EuiBadge>
</EuiToolTip>
);
}
return d;
};
return d;
};
let columnType;
if (tableItems.length > 0) {
columnType = typeof tableItems[0][k];
}
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 {
if (isNumber) {
column.dataType = 'number';
column.render = render;
} else if (typeof type !== 'undefined') {
switch (type) {
case ES_FIELD_TYPES.BOOLEAN:
column.dataType = ES_FIELD_TYPES.BOOLEAN;
break;
case ES_FIELD_TYPES.DATE:
column.align = 'right';
column.render = (d: any) => {
if (d !== undefined) {
return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
}
return d;
};
break;
default:
column.render = render;
}
break;
}
} else {
column.render = render;
}
return column;
})
);
}
return column;
});
const docFieldsCount = docFields.length;
useEffect(() => {
if (jobConfig !== undefined) {
const predictedFieldName = getPredictedFieldName(
jobConfig.dest.results_field,
jobConfig.analysis
);
const predictedFieldSelected = selectedFields.includes(predictedFieldName);
if (
jobConfig !== undefined &&
columns.length > 0 &&
selectedFields.length > 0 &&
sortField !== undefined &&
sortDirection !== undefined &&
selectedFields.some(field => field.name === sortField)
) {
let field = sortField;
// If sorting by predictedField use dependentVar type
if (predictedFieldName === sortField) {
field = dependentVariable;
}
const requiresKeyword = isKeywordAndTextType(field);
const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field, direction, searchQuery });
loadExploreData({
field: sortField,
direction: sortDirection,
searchQuery,
requiresKeyword,
});
}
}, [JSON.stringify(searchQuery)]);
useEffect(() => {
// by default set the sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_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
// By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`).
// if that's not available sort ascending on the first column. Check if the current sorting field is still available.
if (
jobConfig !== undefined &&
columns.length > 0 &&
selectedFields.length > 0 &&
!selectedFields.some(field => field.name === sortField)
) {
const predictedFieldSelected = selectedFields.some(
field => field.name === predictedFieldName
);
const predictedFieldSelected = selectedFields.includes(predictedFieldName);
const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
// CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type)
let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name;
const requiresKeyword = isKeywordAndTextType(sortByField);
sortByField = predictedFieldSelected ? predictedFieldName : sortByField;
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field, direction, searchQuery });
loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword });
}
}, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
}, [
jobConfig,
columns.length,
selectedFields.length,
sortField,
sortDirection,
tableItems.length,
]);
let sorting: SortingPropType = false;
let onTableChange;
@ -261,7 +268,17 @@ export const ResultsTable: FC<Props> = React.memo(
setPageSize(size);
if (sort.field !== sortField || sort.direction !== sortDirection) {
loadExploreData({ ...sort, searchQuery });
let field = sort.field;
// If sorting by predictedField use depVar for type check
if (predictedFieldName === sort.field) {
field = dependentVariable;
}
loadExploreData({
...sort,
searchQuery,
requiresKeyword: isKeywordAndTextType(field),
});
}
};
}
@ -422,14 +439,17 @@ export const ResultsTable: FC<Props> = React.memo(
)}
</EuiPopoverTitle>
<div style={{ maxHeight: '400px', overflowY: 'scroll' }}>
{docFields.map(d => (
{docFields.map(({ name }) => (
<EuiCheckbox
key={d}
id={d}
label={d}
checked={selectedFields.includes(d)}
onChange={() => toggleColumn(d)}
disabled={selectedFields.includes(d) && selectedFields.length === 1}
key={name}
id={name}
label={name}
checked={selectedFields.some(field => field.name === name)}
onChange={() => toggleColumn(name)}
disabled={
selectedFields.some(field => field.name === name) &&
selectedFields.length === 1
}
/>
))}
</div>

View file

@ -17,27 +17,22 @@ import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_m
import { ml } from '../../../../../services/ml_api_service';
import { getNestedProperty } from '../../../../../util/object_utils';
import { SavedSearchQuery } from '../../../../../contexts/kibana';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { Field } from '../../../../../../../common/types/fields';
import { LoadExploreDataArg } from '../../../../common/analytics';
import {
getDefaultClassificationFields,
getDefaultFieldsFromJobCaps,
getFlattenedFields,
DataFrameAnalyticsConfig,
EsFieldName,
getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
SearchQuery,
} from '../../../../common';
export type TableItem = Record<string, any>;
interface LoadExploreDataArg {
field: string;
direction: SortDirection;
searchQuery: SavedSearchQuery;
}
export interface UseExploreDataReturnType {
errorMessage: string;
loadExploreData: (arg: LoadExploreDataArg) => void;
@ -49,8 +44,9 @@ export interface UseExploreDataReturnType {
export const useExploreData = (
jobConfig: DataFrameAnalyticsConfig | undefined,
selectedFields: EsFieldName[],
setSelectedFields: React.Dispatch<React.SetStateAction<EsFieldName[]>>
selectedFields: Field[],
setSelectedFields: React.Dispatch<React.SetStateAction<Field[]>>,
setDocFields: React.Dispatch<React.SetStateAction<Field[]>>
): UseExploreDataReturnType => {
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
@ -58,7 +54,26 @@ export const useExploreData = (
const [sortField, setSortField] = useState<string>('');
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => {
const getDefaultSelectedFields = () => {
const { fields } = newJobCapsService;
if (selectedFields.length === 0 && jobConfig !== undefined) {
const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps(
fields,
jobConfig
);
setSelectedFields(defaultSelected);
setDocFields(docFields);
}
};
const loadExploreData = async ({
field,
direction,
searchQuery,
requiresKeyword,
}: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
@ -72,7 +87,7 @@ export const useExploreData = (
if (field !== undefined) {
body.sort = [
{
[field]: {
[`${field}${requiresKeyword ? '.keyword' : ''}`]: {
order: direction,
},
},
@ -96,11 +111,6 @@ export const useExploreData = (
return;
}
if (selectedFields.length === 0) {
const newSelectedFields = getDefaultClassificationFields(docs, jobConfig);
setSelectedFields(newSelectedFields);
}
// Create a version of the doc's source with flattened field names.
// This avoids confusion later on if a field name has dots in its name
// or is a nested fields when displaying it via EuiInMemoryTable.
@ -144,11 +154,7 @@ export const useExploreData = (
useEffect(() => {
if (jobConfig !== undefined) {
loadExploreData({
field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis),
direction: SORT_DIRECTION.DESC,
searchQuery: defaultSearchQuery,
});
getDefaultSelectedFields();
}
}, [jobConfig && jobConfig.id]);

View file

@ -46,7 +46,7 @@ import { ml } from '../../../../../services/ml_api_service';
import {
sortColumns,
toggleSelectedField,
toggleSelectedFieldSimple,
DataFrameAnalyticsConfig,
EsFieldName,
EsDoc,
@ -138,7 +138,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
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)]);
setSelectedFields([...toggleSelectedFieldSimple(selectedFields, column)]);
}
}

View file

@ -14,6 +14,10 @@ import { ResultsTable } from './results_table';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
import { LoadingPanel } from '../loading_panel';
import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { useKibanaContext } from '../../../../../contexts/kibana';
interface GetDataFrameAnalyticsResponse {
count: number;
@ -31,6 +35,21 @@ export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
</EuiTitle>
);
const jobConfigErrorTitle = i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError',
{
defaultMessage:
'Unable to fetch results. An error occurred loading the job configuration data.',
}
);
const jobCapsErrorTitle = i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError',
{
defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.",
}
);
interface Props {
jobId: string;
jobStatus: DATA_FRAME_TASK_STATE;
@ -39,8 +58,13 @@ interface Props {
export const RegressionExploration: FC<Props> = ({ jobId, jobStatus }) => {
const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined);
const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState<undefined | string>(
undefined
);
const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery);
const kibanaContext = useKibanaContext();
const loadJobConfig = async () => {
setIsLoadingJobConfig(true);
@ -69,23 +93,41 @@ export const RegressionExploration: FC<Props> = ({ jobId, jobStatus }) => {
loadJobConfig();
}, []);
if (jobConfigErrorMessage !== undefined) {
const initializeJobCapsService = async () => {
if (jobConfig !== undefined) {
try {
const sourceIndex = jobConfig.source.index[0];
const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId);
if (indexPattern !== undefined) {
await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false);
}
setIsInitialized(true);
} catch (e) {
if (e.message !== undefined) {
setJobCapsServiceErrorMessage(e.message);
} else {
setJobCapsServiceErrorMessage(JSON.stringify(e));
}
}
}
};
useEffect(() => {
initializeJobCapsService();
}, [JSON.stringify(jobConfig)]);
if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) {
return (
<EuiPanel grow={false}>
<ExplorationTitle jobId={jobId} />
<EuiSpacer />
<EuiCallOut
title={i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError',
{
defaultMessage:
'Unable to fetch results. An error occurred loading the job configuration data.',
}
)}
title={jobConfigErrorMessage ? jobConfigErrorTitle : jobCapsErrorTitle}
color="danger"
iconType="cross"
>
<p>{jobConfigErrorMessage}</p>
<p>{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}</p>
</EuiCallOut>
</EuiPanel>
);
@ -94,12 +136,12 @@ export const RegressionExploration: FC<Props> = ({ jobId, jobStatus }) => {
return (
<Fragment>
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
{isLoadingJobConfig === false && jobConfig !== undefined && (
{isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
<EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} />
)}
<EuiSpacer />
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
{isLoadingJobConfig === false && jobConfig !== undefined && (
{isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && (
<ResultsTable
jobConfig={jobConfig}
jobStatus={jobStatus}

View file

@ -27,6 +27,7 @@ import {
} from '@elastic/eui';
import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common';
import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import {
ColumnType,
@ -37,12 +38,16 @@ import {
} from '../../../../../components/ml_in_memory_table';
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
import { Field } from '../../../../../../../common/types/fields';
import { SavedSearchQuery } from '../../../../../contexts/kibana';
import {
BASIC_NUMERICAL_TYPES,
EXTENDED_NUMERICAL_TYPES,
toggleSelectedField,
isKeywordAndTextType,
} from '../../../../common/fields';
import {
sortRegressionResultsColumns,
sortRegressionResultsFields,
toggleSelectedField,
DataFrameAnalyticsConfig,
EsFieldName,
EsDoc,
@ -51,6 +56,7 @@ import {
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
getDependentVar,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
@ -72,12 +78,20 @@ export const ResultsTable: FC<Props> = React.memo(
({ jobConfig, jobStatus, setEvaluateSearchQuery }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(25);
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
const [selectedFields, setSelectedFields] = useState([] as Field[]);
const [docFields, setDocFields] = useState([] as Field[]);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
const [searchError, setSearchError] = useState<any>(undefined);
const [searchString, setSearchString] = useState<string | undefined>(undefined);
const predictedFieldName = getPredictedFieldName(
jobConfig.dest.results_field,
jobConfig.analysis
);
const dependentVariable = getDependentVar(jobConfig.analysis);
function toggleColumnsPopover() {
setColumnsPopoverVisible(!isColumnsPopoverVisible);
}
@ -100,147 +114,140 @@ export const ResultsTable: FC<Props> = React.memo(
sortDirection,
status,
tableItems,
} = useExploreData(jobConfig, selectedFields, setSelectedFields);
} = useExploreData(jobConfig, selectedFields, setSelectedFields, setDocFields);
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: Array<ColumnType<TableItem>> = selectedFields.map(field => {
const { type } = field;
const isNumber =
type !== undefined &&
(BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
const columns: Array<ColumnType<TableItem>> = [];
const column: ColumnType<TableItem> = {
field: field.name,
name: field.name,
sortable: true,
truncateText: true,
};
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
columns.push(
...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
const column: ColumnType<TableItem> = {
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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent',
{
defaultMessage:
'The full content of this array based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
{
defaultMessage: 'array',
}
)}
</EuiBadge>
</EuiToolTip>
);
}
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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent',
{
defaultMessage:
'The full content of this array based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
{
defaultMessage: 'array',
}
)}
</EuiBadge>
</EuiToolTip>
);
} 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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.indexObjectToolTipContent',
{
defaultMessage:
'The full content of this object based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent',
{
defaultMessage: 'object',
}
)}
</EuiBadge>
</EuiToolTip>
);
}
return d;
};
return d;
};
let columnType;
if (tableItems.length > 0) {
columnType = typeof tableItems[0][k];
}
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 {
if (isNumber) {
column.dataType = 'number';
column.render = render;
} else if (typeof type !== 'undefined') {
switch (type) {
case ES_FIELD_TYPES.BOOLEAN:
column.dataType = ES_FIELD_TYPES.BOOLEAN;
break;
case ES_FIELD_TYPES.DATE:
column.align = 'right';
column.render = (d: any) => {
if (d !== undefined) {
return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
}
return d;
};
break;
default:
column.render = render;
}
break;
}
} else {
column.render = render;
}
return column;
})
);
}
return column;
});
const docFieldsCount = docFields.length;
useEffect(() => {
if (jobConfig !== undefined) {
const predictedFieldName = getPredictedFieldName(
jobConfig.dest.results_field,
jobConfig.analysis
);
const predictedFieldSelected = selectedFields.includes(predictedFieldName);
if (
jobConfig !== undefined &&
columns.length > 0 &&
selectedFields.length > 0 &&
sortField !== undefined &&
sortDirection !== undefined &&
selectedFields.some(field => field.name === sortField)
) {
let field = sortField;
// If sorting by predictedField use dependentVar type
if (predictedFieldName === sortField) {
field = dependentVariable;
}
const requiresKeyword = isKeywordAndTextType(field);
const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field, direction, searchQuery });
loadExploreData({
field: sortField,
direction: sortDirection,
searchQuery,
requiresKeyword,
});
}
}, [JSON.stringify(searchQuery)]);
useEffect(() => {
// by default set the sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_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
// By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`).
// if that's not available sort ascending on the first column. Check if the current sorting field is still available.
if (
jobConfig !== undefined &&
columns.length > 0 &&
selectedFields.length > 0 &&
!selectedFields.some(field => field.name === sortField)
) {
const predictedFieldSelected = selectedFields.some(
field => field.name === predictedFieldName
);
const predictedFieldSelected = selectedFields.includes(predictedFieldName);
const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
// CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type)
let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name;
const requiresKeyword = isKeywordAndTextType(sortByField);
sortByField = predictedFieldSelected ? predictedFieldName : sortByField;
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field, direction, searchQuery });
loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword });
}
}, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
}, [
jobConfig,
columns.length,
selectedFields.length,
sortField,
sortDirection,
tableItems.length,
]);
let sorting: SortingPropType = false;
let onTableChange;
@ -262,7 +269,17 @@ export const ResultsTable: FC<Props> = React.memo(
setPageSize(size);
if (sort.field !== sortField || sort.direction !== sortDirection) {
loadExploreData({ ...sort, searchQuery });
let field = sort.field;
// If sorting by predictedField use depVar for type check
if (predictedFieldName === sort.field) {
field = dependentVariable;
}
loadExploreData({
...sort,
searchQuery,
requiresKeyword: isKeywordAndTextType(field),
});
}
};
}
@ -423,14 +440,16 @@ export const ResultsTable: FC<Props> = React.memo(
)}
</EuiPopoverTitle>
<div style={{ maxHeight: '400px', overflowY: 'scroll' }}>
{docFields.map(d => (
{docFields.map(({ name }) => (
<EuiCheckbox
key={d}
id={d}
label={d}
checked={selectedFields.includes(d)}
onChange={() => toggleColumn(d)}
disabled={selectedFields.includes(d) && selectedFields.length === 1}
id={name}
label={name}
checked={selectedFields.some(field => field.name === name)}
onChange={() => toggleColumn(name)}
disabled={
selectedFields.some(field => field.name === name) &&
selectedFields.length === 1
}
/>
))}
</div>

View file

@ -12,27 +12,22 @@ import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_m
import { ml } from '../../../../../services/ml_api_service';
import { getNestedProperty } from '../../../../../util/object_utils';
import { SavedSearchQuery } from '../../../../../contexts/kibana';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import {
getDefaultRegressionFields,
getDefaultFieldsFromJobCaps,
getFlattenedFields,
DataFrameAnalyticsConfig,
EsFieldName,
getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
SearchQuery,
} from '../../../../common';
import { Field } from '../../../../../../../common/types/fields';
import { LoadExploreDataArg } from '../../../../common/analytics';
export type TableItem = Record<string, any>;
interface LoadExploreDataArg {
field: string;
direction: SortDirection;
searchQuery: SavedSearchQuery;
}
export interface UseExploreDataReturnType {
errorMessage: string;
loadExploreData: (arg: LoadExploreDataArg) => void;
@ -44,8 +39,9 @@ export interface UseExploreDataReturnType {
export const useExploreData = (
jobConfig: DataFrameAnalyticsConfig | undefined,
selectedFields: EsFieldName[],
setSelectedFields: React.Dispatch<React.SetStateAction<EsFieldName[]>>
selectedFields: Field[],
setSelectedFields: React.Dispatch<React.SetStateAction<Field[]>>,
setDocFields: React.Dispatch<React.SetStateAction<Field[]>>
): UseExploreDataReturnType => {
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
@ -53,7 +49,26 @@ export const useExploreData = (
const [sortField, setSortField] = useState<string>('');
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => {
const getDefaultSelectedFields = () => {
const { fields } = newJobCapsService;
if (selectedFields.length === 0 && jobConfig !== undefined) {
const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps(
fields,
jobConfig
);
setSelectedFields(defaultSelected);
setDocFields(docFields);
}
};
const loadExploreData = async ({
field,
direction,
searchQuery,
requiresKeyword,
}: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
@ -67,7 +82,7 @@ export const useExploreData = (
if (field !== undefined) {
body.sort = [
{
[field]: {
[`${field}${requiresKeyword ? '.keyword' : ''}`]: {
order: direction,
},
},
@ -91,11 +106,6 @@ export const useExploreData = (
return;
}
if (selectedFields.length === 0) {
const newSelectedFields = getDefaultRegressionFields(docs, jobConfig);
setSelectedFields(newSelectedFields);
}
// Create a version of the doc's source with flattened field names.
// This avoids confusion later on if a field name has dots in its name
// or is a nested fields when displaying it via EuiInMemoryTable.
@ -139,11 +149,7 @@ export const useExploreData = (
useEffect(() => {
if (jobConfig !== undefined) {
loadExploreData({
field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis),
direction: SORT_DIRECTION.DESC,
searchQuery: defaultSearchQuery,
});
getDefaultSelectedFields();
}
}, [jobConfig && jobConfig.id]);

View file

@ -7,20 +7,7 @@
import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public';
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields';
import { JOB_TYPES, AnalyticsJobType } from '../../hooks/use_create_analytics_form/state';
const BASIC_NUMERICAL_TYPES = new Set([
ES_FIELD_TYPES.LONG,
ES_FIELD_TYPES.INTEGER,
ES_FIELD_TYPES.SHORT,
ES_FIELD_TYPES.BYTE,
]);
const EXTENDED_NUMERICAL_TYPES = new Set([
ES_FIELD_TYPES.DOUBLE,
ES_FIELD_TYPES.FLOAT,
ES_FIELD_TYPES.HALF_FLOAT,
ES_FIELD_TYPES.SCALED_FLOAT,
]);
import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields';
const CATEGORICAL_TYPES = new Set(['ip', 'keyword', 'text']);

View file

@ -16,6 +16,17 @@ describe('object_utils', () => {
},
};
const falseyObj = {
the: {
nested: {
value: false,
},
other_nested: {
value: 0,
},
},
};
const test1 = getNestedProperty(testObj, 'the');
expect(typeof test1).toBe('object');
expect(Object.keys(test1)).toStrictEqual(['nested']);
@ -47,5 +58,13 @@ describe('object_utils', () => {
const test9 = getNestedProperty(testObj, 'the.nested.value.doesntExist', 'the-default-value');
expect(typeof test9).toBe('string');
expect(test9).toBe('the-default-value');
const test10 = getNestedProperty(falseyObj, 'the.nested.value');
expect(typeof test10).toBe('boolean');
expect(test10).toBe(false);
const test11 = getNestedProperty(falseyObj, 'the.other_nested.value');
expect(typeof test11).toBe('number');
expect(test11).toBe(0);
});
});

View file

@ -11,5 +11,9 @@ export const getNestedProperty = (
accessor: string,
defaultValue?: any
) => {
return accessor.split('.').reduce((o, i) => o?.[i], obj) || defaultValue;
const value = accessor.split('.').reduce((o, i) => o?.[i], obj);
if (value === undefined) return defaultValue;
return value;
};