[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:
parent
45f8ca90a3
commit
69730cef73
|
@ -23,7 +23,7 @@ export interface Field {
|
|||
id: FieldId;
|
||||
name: string;
|
||||
type: ES_FIELD_TYPES;
|
||||
aggregatable: boolean;
|
||||
aggregatable?: boolean;
|
||||
aggIds?: AggId[];
|
||||
aggs?: Aggregation[];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -33,11 +33,13 @@ export {
|
|||
getDefaultSelectableFields,
|
||||
getDefaultRegressionFields,
|
||||
getDefaultClassificationFields,
|
||||
getDefaultFieldsFromJobCaps,
|
||||
getFlattenedFields,
|
||||
sortColumns,
|
||||
sortRegressionResultsColumns,
|
||||
sortRegressionResultsFields,
|
||||
toggleSelectedField,
|
||||
toggleSelectedFieldSimple,
|
||||
EsId,
|
||||
EsDoc,
|
||||
EsDocSource,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue