[ML] Transforms: Data grid fixes. (#59538)
- Fixes data grid schemas to avoid crashing the page. - Fixes date formatting. - Uses data grid for preview table in transform list.
This commit is contained in:
parent
b56cd41412
commit
ec1f46bdbd
70
x-pack/plugins/transform/common/utils/object_utils.test.ts
Normal file
70
x-pack/plugins/transform/common/utils/object_utils.test.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getNestedProperty } from './object_utils';
|
||||
|
||||
describe('object_utils', () => {
|
||||
test('getNestedProperty()', () => {
|
||||
const testObj = {
|
||||
the: {
|
||||
nested: {
|
||||
value: 'the-nested-value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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']);
|
||||
|
||||
const test2 = getNestedProperty(testObj, 'the$');
|
||||
expect(typeof test2).toBe('undefined');
|
||||
|
||||
const test3 = getNestedProperty(testObj, 'the$', 'the-default-value');
|
||||
expect(typeof test3).toBe('string');
|
||||
expect(test3).toBe('the-default-value');
|
||||
|
||||
const test4 = getNestedProperty(testObj, 'the.neSted');
|
||||
expect(typeof test4).toBe('undefined');
|
||||
|
||||
const test5 = getNestedProperty(testObj, 'the.nested');
|
||||
expect(typeof test5).toBe('object');
|
||||
expect(Object.keys(test5)).toStrictEqual(['value']);
|
||||
|
||||
const test6 = getNestedProperty(testObj, 'the.nested.vaLue');
|
||||
expect(typeof test6).toBe('undefined');
|
||||
|
||||
const test7 = getNestedProperty(testObj, 'the.nested.value');
|
||||
expect(typeof test7).toBe('string');
|
||||
expect(test7).toBe('the-nested-value');
|
||||
|
||||
const test8 = getNestedProperty(testObj, 'the.nested.value.doesntExist');
|
||||
expect(typeof test8).toBe('undefined');
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDataGridSorting } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
getPreviewRequestBody,
|
||||
PivotAggsConfig,
|
||||
PivotGroupByConfig,
|
||||
PIVOT_SUPPORTED_AGGS,
|
||||
PIVOT_SUPPORTED_GROUP_BY_AGGS,
|
||||
SimpleQuery,
|
||||
} from '../../common';
|
||||
|
||||
import { multiColumnSortFactory, getPivotPreviewDevConsoleStatement } from './common';
|
||||
|
||||
describe('Transform: Define Pivot Common', () => {
|
||||
test('multiColumnSortFactory()', () => {
|
||||
const data = [
|
||||
{ s: 'a', n: 1 },
|
||||
{ s: 'a', n: 2 },
|
||||
{ s: 'b', n: 3 },
|
||||
{ s: 'b', n: 4 },
|
||||
];
|
||||
|
||||
const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }];
|
||||
const multiColumnSort1 = multiColumnSortFactory(sortingColumns1);
|
||||
data.sort(multiColumnSort1);
|
||||
|
||||
expect(data).toStrictEqual([
|
||||
{ s: 'b', n: 3 },
|
||||
{ s: 'b', n: 4 },
|
||||
{ s: 'a', n: 1 },
|
||||
{ s: 'a', n: 2 },
|
||||
]);
|
||||
|
||||
const sortingColumns2: EuiDataGridSorting['columns'] = [
|
||||
{ id: 's', direction: 'asc' },
|
||||
{ id: 'n', direction: 'desc' },
|
||||
];
|
||||
const multiColumnSort2 = multiColumnSortFactory(sortingColumns2);
|
||||
data.sort(multiColumnSort2);
|
||||
|
||||
expect(data).toStrictEqual([
|
||||
{ s: 'a', n: 2 },
|
||||
{ s: 'a', n: 1 },
|
||||
{ s: 'b', n: 4 },
|
||||
{ s: 'b', n: 3 },
|
||||
]);
|
||||
|
||||
const sortingColumns3: EuiDataGridSorting['columns'] = [
|
||||
{ id: 'n', direction: 'desc' },
|
||||
{ id: 's', direction: 'desc' },
|
||||
];
|
||||
const multiColumnSort3 = multiColumnSortFactory(sortingColumns3);
|
||||
data.sort(multiColumnSort3);
|
||||
|
||||
expect(data).toStrictEqual([
|
||||
{ s: 'b', n: 4 },
|
||||
{ s: 'b', n: 3 },
|
||||
{ s: 'a', n: 2 },
|
||||
{ s: 'a', n: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('getPivotPreviewDevConsoleStatement()', () => {
|
||||
const query: SimpleQuery = {
|
||||
query_string: {
|
||||
query: '*',
|
||||
default_operator: 'AND',
|
||||
},
|
||||
};
|
||||
const groupBy: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
aggName: 'the-group-by-agg-name',
|
||||
dropDownName: 'the-group-by-drop-down-name',
|
||||
};
|
||||
const agg: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-agg-field',
|
||||
aggName: 'the-agg-agg-name',
|
||||
dropDownName: 'the-agg-drop-down-name',
|
||||
};
|
||||
const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]);
|
||||
const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request);
|
||||
|
||||
expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview
|
||||
{
|
||||
"source": {
|
||||
"index": [
|
||||
"the-index-pattern-title"
|
||||
]
|
||||
},
|
||||
"pivot": {
|
||||
"group_by": {
|
||||
"the-group-by-agg-name": {
|
||||
"terms": {
|
||||
"field": "the-group-by-field"
|
||||
}
|
||||
}
|
||||
},
|
||||
"aggregations": {
|
||||
"the-agg-agg-name": {
|
||||
"avg": {
|
||||
"field": "the-agg-field"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDataGridSorting } from '@elastic/eui';
|
||||
|
||||
import { getNestedProperty } from '../../../../common/utils/object_utils';
|
||||
|
||||
import { PreviewRequestBody } from '../../common';
|
||||
|
||||
/**
|
||||
* Helper to sort an array of objects based on an EuiDataGrid sorting configuration.
|
||||
* `sortFn()` is recursive to support sorting on multiple columns.
|
||||
*
|
||||
* @param sortingColumns - The EUI data grid sorting configuration
|
||||
* @returns The sorting function which can be used with an array's sort() function.
|
||||
*/
|
||||
export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => {
|
||||
const isString = (arg: any): arg is string => {
|
||||
return typeof arg === 'string';
|
||||
};
|
||||
|
||||
const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => {
|
||||
const sort = sortingColumns[sortingColumnIndex];
|
||||
const aValue = getNestedProperty(a, sort.id, null);
|
||||
const bValue = getNestedProperty(b, sort.id, null);
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
if (aValue < bValue) {
|
||||
return sort.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sort.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(aValue) && isString(bValue)) {
|
||||
if (aValue.localeCompare(bValue) === -1) {
|
||||
return sort.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue.localeCompare(bValue) === 1) {
|
||||
return sort.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (sortingColumnIndex + 1 < sortingColumns.length) {
|
||||
return sortFn(a, b, sortingColumnIndex + 1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
return sortFn;
|
||||
};
|
||||
|
||||
export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => {
|
||||
return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { PivotPreview } from './pivot_preview';
|
|
@ -8,20 +8,19 @@ import React from 'react';
|
|||
import { render, wait } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
import { Providers } from '../../../../app_dependencies.mock';
|
||||
import { Providers } from '../../app_dependencies.mock';
|
||||
import {
|
||||
getPivotQuery,
|
||||
PivotAggsConfig,
|
||||
PivotGroupByConfig,
|
||||
PIVOT_SUPPORTED_AGGS,
|
||||
PIVOT_SUPPORTED_GROUP_BY_AGGS,
|
||||
} from '../../../../common';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
} from '../../common';
|
||||
|
||||
import { PivotPreview } from './pivot_preview';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../../../../shared_imports');
|
||||
jest.mock('../../../shared_imports');
|
||||
|
||||
describe('Transform: <PivotPreview />', () => {
|
||||
// Using the async/await wait()/done() pattern to avoid act() errors.
|
||||
|
@ -42,10 +41,7 @@ describe('Transform: <PivotPreview />', () => {
|
|||
const props = {
|
||||
aggs: { 'the-agg-name': agg },
|
||||
groupBy: { 'the-group-by-name': groupBy },
|
||||
indexPattern: {
|
||||
title: 'the-index-pattern-title',
|
||||
fields: [] as any[],
|
||||
} as SearchItems['indexPattern'],
|
||||
indexPatternTitle: 'the-index-pattern-title',
|
||||
query: getPivotQuery('the-query'),
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -21,8 +22,11 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { dictionaryToArray } from '../../../../../../common/types/common';
|
||||
import { getNestedProperty } from '../../../../../../common/utils/object_utils';
|
||||
import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
import { dictionaryToArray } from '../../../../common/types/common';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils';
|
||||
import { getNestedProperty } from '../../../../common/utils/object_utils';
|
||||
|
||||
import {
|
||||
euiDataGridStyle,
|
||||
|
@ -33,8 +37,8 @@ import {
|
|||
PivotGroupByConfig,
|
||||
PivotGroupByConfigDict,
|
||||
PivotQuery,
|
||||
} from '../../../../common';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
} from '../../common';
|
||||
import { SearchItems } from '../../hooks/use_search_items';
|
||||
|
||||
import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common';
|
||||
import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data';
|
||||
|
@ -102,21 +106,22 @@ const ErrorMessage: FC<ErrorMessageProps> = ({ message }) => (
|
|||
interface PivotPreviewProps {
|
||||
aggs: PivotAggsConfigDict;
|
||||
groupBy: PivotGroupByConfigDict;
|
||||
indexPattern: SearchItems['indexPattern'];
|
||||
indexPatternTitle: SearchItems['indexPattern']['title'];
|
||||
query: PivotQuery;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
const defaultPagination = { pageIndex: 0, pageSize: 5 };
|
||||
|
||||
export const PivotPreview: FC<PivotPreviewProps> = React.memo(
|
||||
({ aggs, groupBy, indexPattern, query }) => {
|
||||
({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => {
|
||||
const {
|
||||
previewData: data,
|
||||
previewMappings,
|
||||
errorMessage,
|
||||
previewRequest,
|
||||
status,
|
||||
} = usePivotPreviewData(indexPattern, query, aggs, groupBy);
|
||||
} = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy);
|
||||
const groupByArr = dictionaryToArray(groupBy);
|
||||
|
||||
// Filters mapping properties of type `object`, which get returned for nested field parents.
|
||||
|
@ -142,7 +147,42 @@ export const PivotPreview: FC<PivotPreviewProps> = React.memo(
|
|||
}, [data.length]);
|
||||
|
||||
// EuiDataGrid State
|
||||
const dataGridColumns = columnKeys.map(id => ({ id }));
|
||||
const dataGridColumns = columnKeys.map(id => {
|
||||
const field = previewMappings.properties[id];
|
||||
|
||||
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
|
||||
// To fall back to the default string schema it needs to be undefined.
|
||||
let schema;
|
||||
|
||||
switch (field?.type) {
|
||||
case ES_FIELD_TYPES.GEO_POINT:
|
||||
case ES_FIELD_TYPES.GEO_SHAPE:
|
||||
schema = 'json';
|
||||
break;
|
||||
case ES_FIELD_TYPES.BOOLEAN:
|
||||
schema = 'boolean';
|
||||
break;
|
||||
case ES_FIELD_TYPES.DATE:
|
||||
schema = 'datetime';
|
||||
break;
|
||||
case ES_FIELD_TYPES.BYTE:
|
||||
case ES_FIELD_TYPES.DOUBLE:
|
||||
case ES_FIELD_TYPES.FLOAT:
|
||||
case ES_FIELD_TYPES.HALF_FLOAT:
|
||||
case ES_FIELD_TYPES.INTEGER:
|
||||
case ES_FIELD_TYPES.LONG:
|
||||
case ES_FIELD_TYPES.SCALED_FLOAT:
|
||||
case ES_FIELD_TYPES.SHORT:
|
||||
schema = 'numeric';
|
||||
break;
|
||||
// keep schema undefined for text based columns
|
||||
case ES_FIELD_TYPES.KEYWORD:
|
||||
case ES_FIELD_TYPES.TEXT:
|
||||
break;
|
||||
}
|
||||
|
||||
return { id, schema };
|
||||
});
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
pageSize => {
|
||||
|
@ -191,13 +231,17 @@ export const PivotPreview: FC<PivotPreviewProps> = React.memo(
|
|||
return JSON.stringify(cellValue);
|
||||
}
|
||||
|
||||
if (cellValue === undefined) {
|
||||
if (cellValue === undefined || cellValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.DATE) {
|
||||
return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000);
|
||||
}
|
||||
|
||||
return cellValue;
|
||||
};
|
||||
}, [pageData, pagination.pageIndex, pagination.pageSize]);
|
||||
}, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]);
|
||||
|
||||
if (status === PIVOT_PREVIEW_STATUS.ERROR) {
|
||||
return (
|
||||
|
@ -256,13 +300,17 @@ export const PivotPreview: FC<PivotPreviewProps> = React.memo(
|
|||
|
||||
return (
|
||||
<div data-test-subj="transformPivotPreview loaded">
|
||||
<PreviewTitle previewRequest={previewRequest} />
|
||||
<div className="transform__progress">
|
||||
{status === PIVOT_PREVIEW_STATUS.LOADING && <EuiProgress size="xs" color="accent" />}
|
||||
{status !== PIVOT_PREVIEW_STATUS.LOADING && (
|
||||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
</div>
|
||||
{showHeader && (
|
||||
<>
|
||||
<PreviewTitle previewRequest={previewRequest} />
|
||||
<div className="transform__progress">
|
||||
{status === PIVOT_PREVIEW_STATUS.LOADING && <EuiProgress size="xs" color="accent" />}
|
||||
{status !== PIVOT_PREVIEW_STATUS.LOADING && (
|
||||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{dataGridColumns.length > 0 && data.length > 0 && (
|
||||
<EuiDataGrid
|
||||
aria-label="Source index preview"
|
|
@ -7,16 +7,14 @@
|
|||
import React, { FC } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { SimpleQuery } from '../../../../common';
|
||||
import { SimpleQuery } from '../../common';
|
||||
import {
|
||||
PIVOT_PREVIEW_STATUS,
|
||||
usePivotPreviewData,
|
||||
UsePivotPreviewDataReturnType,
|
||||
} from './use_pivot_preview_data';
|
||||
|
||||
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
|
||||
|
||||
jest.mock('../../../../hooks/use_api');
|
||||
jest.mock('../../hooks/use_api');
|
||||
|
||||
type Callback = () => void;
|
||||
interface TestHookProps {
|
||||
|
@ -46,12 +44,7 @@ let pivotPreviewObj: UsePivotPreviewDataReturnType;
|
|||
describe('usePivotPreviewData', () => {
|
||||
test('indexPattern not defined', () => {
|
||||
testHook(() => {
|
||||
pivotPreviewObj = usePivotPreviewData(
|
||||
({ id: 'the-id', title: 'the-title', fields: [] } as unknown) as IndexPattern,
|
||||
query,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {});
|
||||
});
|
||||
|
||||
expect(pivotPreviewObj.errorMessage).toBe('');
|
||||
|
@ -61,12 +54,7 @@ describe('usePivotPreviewData', () => {
|
|||
|
||||
test('indexPattern set triggers loading', () => {
|
||||
testHook(() => {
|
||||
pivotPreviewObj = usePivotPreviewData(
|
||||
({ id: 'the-id', title: 'the-title', fields: [] } as unknown) as IndexPattern,
|
||||
query,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {});
|
||||
});
|
||||
|
||||
expect(pivotPreviewObj.errorMessage).toBe('');
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { dictionaryToArray } from '../../../../../../common/types/common';
|
||||
import { useApi } from '../../../../hooks/use_api';
|
||||
import { dictionaryToArray } from '../../../../common/types/common';
|
||||
import { useApi } from '../../hooks/use_api';
|
||||
|
||||
import { Dictionary } from '../../../../../../common/types/common';
|
||||
import { IndexPattern, ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public';
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
import { IndexPattern, ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
import {
|
||||
getPreviewRequestBody,
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
PivotAggsConfigDict,
|
||||
PivotGroupByConfigDict,
|
||||
PivotQuery,
|
||||
} from '../../../../common';
|
||||
} from '../../common';
|
||||
|
||||
export enum PIVOT_PREVIEW_STATUS {
|
||||
UNUSED,
|
||||
|
@ -51,7 +51,7 @@ export interface GetTransformsResponse {
|
|||
}
|
||||
|
||||
export const usePivotPreviewData = (
|
||||
indexPattern: IndexPattern,
|
||||
indexPatternTitle: IndexPattern['title'],
|
||||
query: PivotQuery,
|
||||
aggs: PivotAggsConfigDict,
|
||||
groupBy: PivotGroupByConfigDict
|
||||
|
@ -65,7 +65,7 @@ export const usePivotPreviewData = (
|
|||
const aggsArr = dictionaryToArray(aggs);
|
||||
const groupByArr = dictionaryToArray(groupBy);
|
||||
|
||||
const previewRequest = getPreviewRequestBody(indexPattern.title, query, groupByArr, aggsArr);
|
||||
const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr);
|
||||
|
||||
const getPreviewData = async () => {
|
||||
if (aggsArr.length === 0 || groupByArr.length === 0) {
|
||||
|
@ -94,7 +94,7 @@ export const usePivotPreviewData = (
|
|||
// custom comparison
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [
|
||||
indexPattern.title,
|
||||
indexPatternTitle,
|
||||
JSON.stringify(aggsArr),
|
||||
JSON.stringify(groupByArr),
|
||||
JSON.stringify(query),
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -17,9 +18,11 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils';
|
||||
import { getNestedProperty } from '../../../../../../common/utils/object_utils';
|
||||
|
||||
import {
|
||||
|
@ -29,6 +32,7 @@ import {
|
|||
PivotQuery,
|
||||
} from '../../../../common';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
import { useToastNotifications } from '../../../../app_dependencies';
|
||||
|
||||
import { getSourceIndexDevConsoleStatement } from './common';
|
||||
import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data';
|
||||
|
@ -52,9 +56,8 @@ interface Props {
|
|||
query: PivotQuery;
|
||||
}
|
||||
|
||||
const defaultPagination = { pageIndex: 0, pageSize: 5 };
|
||||
|
||||
export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, query }) => {
|
||||
const toastNotifications = useToastNotifications();
|
||||
const allFields = indexPattern.fields.map(f => f.name);
|
||||
const indexPatternFields: string[] = allFields.filter(f => {
|
||||
if (indexPattern.metaFields.includes(f)) {
|
||||
|
@ -73,38 +76,67 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, q
|
|||
// Column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState<EsFieldName[]>(indexPatternFields);
|
||||
|
||||
const [pagination, setPagination] = useState(defaultPagination);
|
||||
|
||||
useEffect(() => {
|
||||
setPagination(defaultPagination);
|
||||
}, [query]);
|
||||
|
||||
const { errorMessage, status, rowCount, tableItems: data } = useSourceIndexData(
|
||||
indexPattern,
|
||||
query,
|
||||
pagination
|
||||
);
|
||||
const {
|
||||
errorMessage,
|
||||
pagination,
|
||||
setPagination,
|
||||
setSortingColumns,
|
||||
rowCount,
|
||||
sortingColumns,
|
||||
status,
|
||||
tableItems: data,
|
||||
} = useSourceIndexData(indexPattern, query);
|
||||
|
||||
// EuiDataGrid State
|
||||
const dataGridColumns = indexPatternFields.map(id => {
|
||||
const field = indexPattern.fields.getByName(id);
|
||||
const dataGridColumns = [
|
||||
...indexPatternFields.map(id => {
|
||||
const field = indexPattern.fields.getByName(id);
|
||||
|
||||
let schema = 'string';
|
||||
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
|
||||
// To fall back to the default string schema it needs to be undefined.
|
||||
let schema;
|
||||
|
||||
switch (field?.type) {
|
||||
case 'date':
|
||||
schema = 'datetime';
|
||||
break;
|
||||
case 'geo_point':
|
||||
schema = 'json';
|
||||
break;
|
||||
case 'number':
|
||||
schema = 'numeric';
|
||||
break;
|
||||
}
|
||||
switch (field?.type) {
|
||||
case 'date':
|
||||
schema = 'datetime';
|
||||
break;
|
||||
case 'geo_point':
|
||||
schema = 'json';
|
||||
break;
|
||||
case 'number':
|
||||
schema = 'numeric';
|
||||
break;
|
||||
}
|
||||
|
||||
return { id, schema };
|
||||
});
|
||||
return { id, schema };
|
||||
}),
|
||||
];
|
||||
|
||||
const onSort = useCallback(
|
||||
(sc: Array<{ id: string; direction: 'asc' | 'desc' }>) => {
|
||||
// Check if an unsupported column type for sorting was selected.
|
||||
const invalidSortingColumnns = sc.reduce<string[]>((arr, current) => {
|
||||
const columnType = dataGridColumns.find(dgc => dgc.id === current.id);
|
||||
if (columnType?.schema === 'json') {
|
||||
arr.push(current.id);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
if (invalidSortingColumnns.length === 0) {
|
||||
setSortingColumns(sc);
|
||||
} else {
|
||||
invalidSortingColumnns.forEach(columnId => {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.transform.sourceIndexPreview.invalidSortingColumnError', {
|
||||
defaultMessage: `The column '{columnId}' cannot be used for sorting.`,
|
||||
values: { columnId },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
[dataGridColumns, setSortingColumns, toastNotifications]
|
||||
);
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
pageSize => {
|
||||
|
@ -120,10 +152,6 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, q
|
|||
setPagination,
|
||||
]);
|
||||
|
||||
// ** Sorting config
|
||||
const [sortingColumns, setSortingColumns] = useState([]);
|
||||
const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]);
|
||||
|
||||
const renderCellValue = useMemo(() => {
|
||||
return ({
|
||||
rowIndex,
|
||||
|
@ -144,32 +172,18 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, q
|
|||
return JSON.stringify(cellValue);
|
||||
}
|
||||
|
||||
if (cellValue === undefined) {
|
||||
if (cellValue === undefined || cellValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const field = indexPattern.fields.getByName(columnId);
|
||||
if (field?.type === 'date') {
|
||||
return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000);
|
||||
}
|
||||
|
||||
return cellValue;
|
||||
};
|
||||
}, [data, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
if (status === SOURCE_INDEX_STATUS.ERROR) {
|
||||
return (
|
||||
<div data-test-subj="transformSourceIndexPreview error">
|
||||
<SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} />
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternError', {
|
||||
defaultMessage: 'An error occurred loading the source index data.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<EuiCodeBlock language="json" fontSize="s" paddingSize="s" isCopyable>
|
||||
{errorMessage}
|
||||
</EuiCodeBlock>
|
||||
</EuiCallOut>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) {
|
||||
return (
|
||||
|
@ -200,7 +214,11 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, q
|
|||
});
|
||||
|
||||
return (
|
||||
<div data-test-subj="transformSourceIndexPreview loaded">
|
||||
<div
|
||||
data-test-subj={`transformSourceIndexPreview ${
|
||||
status === SOURCE_INDEX_STATUS.ERROR ? 'error' : 'loaded'
|
||||
}`}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} />
|
||||
|
@ -222,24 +240,38 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, q
|
|||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
</div>
|
||||
{dataGridColumns.length > 0 && data.length > 0 && (
|
||||
<EuiDataGrid
|
||||
aria-label="Source index preview"
|
||||
columns={dataGridColumns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
gridStyle={euiDataGridStyle}
|
||||
rowCount={rowCount}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
toolbarVisibility={euiDataGridToolbarSettings}
|
||||
pagination={{
|
||||
...pagination,
|
||||
pageSizeOptions: [5, 10, 25],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{status === SOURCE_INDEX_STATUS.ERROR && (
|
||||
<div data-test-subj="transformSourceIndexPreview error">
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternError', {
|
||||
defaultMessage: 'An error occurred loading the source index data.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<EuiCodeBlock language="json" fontSize="s" paddingSize="s" isCopyable>
|
||||
{errorMessage}
|
||||
</EuiCodeBlock>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</div>
|
||||
)}
|
||||
<EuiDataGrid
|
||||
aria-label="Source index preview"
|
||||
columns={dataGridColumns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
gridStyle={euiDataGridStyle}
|
||||
rowCount={rowCount}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
toolbarVisibility={euiDataGridToolbarSettings}
|
||||
pagination={{
|
||||
...pagination,
|
||||
pageSizeOptions: [5, 10, 25],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -26,10 +26,7 @@ const query: SimpleQuery = {
|
|||
describe('useSourceIndexData', () => {
|
||||
test('indexPattern set triggers loading', async done => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query, {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query)
|
||||
);
|
||||
const sourceIndexObj: UseSourceIndexDataReturnType = result.current;
|
||||
|
||||
|
|
|
@ -4,15 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
|
||||
import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui';
|
||||
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
import { useApi } from '../../../../hooks/use_api';
|
||||
import { Dictionary } from '../../../../../../common/types/common';
|
||||
|
||||
import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common';
|
||||
import { useApi } from '../../../../hooks/use_api';
|
||||
|
||||
export enum SOURCE_INDEX_STATUS {
|
||||
UNUSED,
|
||||
|
@ -21,19 +24,25 @@ export enum SOURCE_INDEX_STATUS {
|
|||
ERROR,
|
||||
}
|
||||
|
||||
type EsSorting = Dictionary<{
|
||||
order: 'asc' | 'desc';
|
||||
}>;
|
||||
|
||||
interface ErrorResponse {
|
||||
error: {
|
||||
body: string;
|
||||
msg: string;
|
||||
path: string;
|
||||
query: any;
|
||||
response: string;
|
||||
request: Dictionary<any>;
|
||||
response: Dictionary<any>;
|
||||
body: {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
};
|
||||
name: string;
|
||||
req: Dictionary<any>;
|
||||
res: Dictionary<any>;
|
||||
}
|
||||
|
||||
const isErrorResponse = (arg: any): arg is ErrorResponse => {
|
||||
return arg.error !== undefined;
|
||||
return arg?.body?.error !== undefined && arg?.body?.message !== undefined;
|
||||
};
|
||||
|
||||
// The types specified in `@types/elasticsearch` are out of date and still have `total: number`.
|
||||
|
@ -46,42 +55,60 @@ interface SearchResponse7 extends SearchResponse<any> {
|
|||
};
|
||||
}
|
||||
|
||||
type SourceIndexSearchResponse = ErrorResponse | SearchResponse7;
|
||||
type SourceIndexSearchResponse = SearchResponse7;
|
||||
|
||||
type SourceIndexPagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>;
|
||||
const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 };
|
||||
|
||||
export interface UseSourceIndexDataReturnType {
|
||||
errorMessage: string;
|
||||
status: SOURCE_INDEX_STATUS;
|
||||
pagination: SourceIndexPagination;
|
||||
setPagination: Dispatch<SetStateAction<SourceIndexPagination>>;
|
||||
setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>;
|
||||
rowCount: number;
|
||||
sortingColumns: EuiDataGridSorting['columns'];
|
||||
status: SOURCE_INDEX_STATUS;
|
||||
tableItems: EsDocSource[];
|
||||
}
|
||||
|
||||
export const useSourceIndexData = (
|
||||
indexPattern: IIndexPattern,
|
||||
query: PivotQuery,
|
||||
pagination: { pageIndex: number; pageSize: number }
|
||||
query: PivotQuery
|
||||
): UseSourceIndexDataReturnType => {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED);
|
||||
const [pagination, setPagination] = useState(defaultPagination);
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
const [tableItems, setTableItems] = useState<EsDocSource[]>([]);
|
||||
const api = useApi();
|
||||
|
||||
useEffect(() => {
|
||||
setPagination(defaultPagination);
|
||||
}, [query]);
|
||||
|
||||
const getSourceIndexData = async function() {
|
||||
setErrorMessage('');
|
||||
setStatus(SOURCE_INDEX_STATUS.LOADING);
|
||||
|
||||
try {
|
||||
const resp: SourceIndexSearchResponse = await api.esSearch({
|
||||
index: indexPattern.title,
|
||||
const sort: EsSorting = sortingColumns.reduce((s, column) => {
|
||||
s[column.id] = { order: column.direction };
|
||||
return s;
|
||||
}, {} as EsSorting);
|
||||
|
||||
const esSearchRequest = {
|
||||
index: indexPattern.title,
|
||||
body: {
|
||||
// Instead of using the default query (`*`), fall back to a more efficient `match_all` query.
|
||||
query: isDefaultQuery(query) ? matchAllQuery : query,
|
||||
from: pagination.pageIndex * pagination.pageSize,
|
||||
size: pagination.pageSize,
|
||||
// Instead of using the default query (`*`), fall back to a more efficient `match_all` query.
|
||||
body: { query: isDefaultQuery(query) ? matchAllQuery : query },
|
||||
});
|
||||
...(Object.keys(sort).length > 0 ? { sort } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
if (isErrorResponse(resp)) {
|
||||
throw resp.error;
|
||||
}
|
||||
try {
|
||||
const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest);
|
||||
|
||||
const docs = resp.hits.hits.map(d => d._source);
|
||||
|
||||
|
@ -89,12 +116,11 @@ export const useSourceIndexData = (
|
|||
setTableItems(docs);
|
||||
setStatus(SOURCE_INDEX_STATUS.LOADED);
|
||||
} catch (e) {
|
||||
if (e.message !== undefined) {
|
||||
setErrorMessage(e.message);
|
||||
if (isErrorResponse(e)) {
|
||||
setErrorMessage(`${e.body.error}: ${e.body.message}`);
|
||||
} else {
|
||||
setErrorMessage(JSON.stringify(e, null, 2));
|
||||
}
|
||||
setTableItems([]);
|
||||
setStatus(SOURCE_INDEX_STATUS.ERROR);
|
||||
}
|
||||
};
|
||||
|
@ -103,6 +129,15 @@ export const useSourceIndexData = (
|
|||
getSourceIndexData();
|
||||
// custom comparison
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [indexPattern.title, JSON.stringify(query), JSON.stringify(pagination)]);
|
||||
return { errorMessage, status, rowCount, tableItems };
|
||||
}, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]);
|
||||
return {
|
||||
errorMessage,
|
||||
pagination,
|
||||
setPagination,
|
||||
setSortingColumns,
|
||||
rowCount,
|
||||
sortingColumns,
|
||||
status,
|
||||
tableItems,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,73 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDataGridSorting } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
getPreviewRequestBody,
|
||||
PivotAggsConfig,
|
||||
PivotGroupByConfig,
|
||||
PIVOT_SUPPORTED_AGGS,
|
||||
PIVOT_SUPPORTED_GROUP_BY_AGGS,
|
||||
SimpleQuery,
|
||||
} from '../../../../common';
|
||||
|
||||
import {
|
||||
multiColumnSortFactory,
|
||||
getPivotPreviewDevConsoleStatement,
|
||||
getPivotDropdownOptions,
|
||||
} from './common';
|
||||
import { getPivotDropdownOptions } from './common';
|
||||
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
|
||||
|
||||
describe('Transform: Define Pivot Common', () => {
|
||||
test('customSortFactory()', () => {
|
||||
const data = [
|
||||
{ s: 'a', n: 1 },
|
||||
{ s: 'a', n: 2 },
|
||||
{ s: 'b', n: 3 },
|
||||
{ s: 'b', n: 4 },
|
||||
];
|
||||
|
||||
const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }];
|
||||
const multiColumnSort1 = multiColumnSortFactory(sortingColumns1);
|
||||
data.sort(multiColumnSort1);
|
||||
|
||||
expect(data).toStrictEqual([
|
||||
{ s: 'b', n: 3 },
|
||||
{ s: 'b', n: 4 },
|
||||
{ s: 'a', n: 1 },
|
||||
{ s: 'a', n: 2 },
|
||||
]);
|
||||
|
||||
const sortingColumns2: EuiDataGridSorting['columns'] = [
|
||||
{ id: 's', direction: 'asc' },
|
||||
{ id: 'n', direction: 'desc' },
|
||||
];
|
||||
const multiColumnSort2 = multiColumnSortFactory(sortingColumns2);
|
||||
data.sort(multiColumnSort2);
|
||||
|
||||
expect(data).toStrictEqual([
|
||||
{ s: 'a', n: 2 },
|
||||
{ s: 'a', n: 1 },
|
||||
{ s: 'b', n: 4 },
|
||||
{ s: 'b', n: 3 },
|
||||
]);
|
||||
|
||||
const sortingColumns3: EuiDataGridSorting['columns'] = [
|
||||
{ id: 'n', direction: 'desc' },
|
||||
{ id: 's', direction: 'desc' },
|
||||
];
|
||||
const multiColumnSort3 = multiColumnSortFactory(sortingColumns3);
|
||||
data.sort(multiColumnSort3);
|
||||
|
||||
expect(data).toStrictEqual([
|
||||
{ s: 'b', n: 4 },
|
||||
{ s: 'b', n: 3 },
|
||||
{ s: 'a', n: 2 },
|
||||
{ s: 'a', n: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('getPivotDropdownOptions()', () => {
|
||||
// The field name includes the characters []> as well as a leading and ending space charcter
|
||||
// which cannot be used for aggregation names. The test results verifies that the characters
|
||||
|
@ -155,53 +92,4 @@ describe('Transform: Define Pivot Common', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('getPivotPreviewDevConsoleStatement()', () => {
|
||||
const query: SimpleQuery = {
|
||||
query_string: {
|
||||
query: '*',
|
||||
default_operator: 'AND',
|
||||
},
|
||||
};
|
||||
const groupBy: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
aggName: 'the-group-by-agg-name',
|
||||
dropDownName: 'the-group-by-drop-down-name',
|
||||
};
|
||||
const agg: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-agg-field',
|
||||
aggName: 'the-agg-agg-name',
|
||||
dropDownName: 'the-agg-drop-down-name',
|
||||
};
|
||||
const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]);
|
||||
const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request);
|
||||
|
||||
expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview
|
||||
{
|
||||
"source": {
|
||||
"index": [
|
||||
"the-index-pattern-title"
|
||||
]
|
||||
},
|
||||
"pivot": {
|
||||
"group_by": {
|
||||
"the-group-by-agg-name": {
|
||||
"terms": {
|
||||
"field": "the-group-by-field"
|
||||
}
|
||||
}
|
||||
},
|
||||
"aggregations": {
|
||||
"the-agg-agg-name": {
|
||||
"avg": {
|
||||
"field": "the-agg-field"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,13 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
import { EuiComboBoxOptionOption, EuiDataGridSorting } from '@elastic/eui';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { IndexPattern, KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public';
|
||||
|
||||
import { getNestedProperty } from '../../../../../../common/utils/object_utils';
|
||||
|
||||
import {
|
||||
PreviewRequestBody,
|
||||
DropDownLabel,
|
||||
DropDownOption,
|
||||
EsFieldName,
|
||||
|
@ -27,51 +24,6 @@ export interface Field {
|
|||
type: KBN_FIELD_TYPES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to sort an array of objects based on an EuiDataGrid sorting configuration.
|
||||
* `sortFn()` is recursive to support sorting on multiple columns.
|
||||
*
|
||||
* @param sortingColumns - The EUI data grid sorting configuration
|
||||
* @returns The sorting function which can be used with an array's sort() function.
|
||||
*/
|
||||
export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => {
|
||||
const isString = (arg: any): arg is string => {
|
||||
return typeof arg === 'string';
|
||||
};
|
||||
|
||||
const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => {
|
||||
const sort = sortingColumns[sortingColumnIndex];
|
||||
const aValue = getNestedProperty(a, sort.id, null);
|
||||
const bValue = getNestedProperty(b, sort.id, null);
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
if (aValue < bValue) {
|
||||
return sort.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sort.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(aValue) && isString(bValue)) {
|
||||
if (aValue.localeCompare(bValue) === -1) {
|
||||
return sort.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue.localeCompare(bValue) === 1) {
|
||||
return sort.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (sortingColumnIndex + 1 < sortingColumns.length) {
|
||||
return sortFn(a, b, sortingColumnIndex + 1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
return sortFn;
|
||||
};
|
||||
|
||||
function getDefaultGroupByConfig(
|
||||
aggName: string,
|
||||
dropDownName: string,
|
||||
|
@ -166,7 +118,3 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) {
|
|||
aggOptionsData,
|
||||
};
|
||||
}
|
||||
|
||||
export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => {
|
||||
return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { PivotPreview } from '../../../../components/pivot_preview';
|
||||
import { useDocumentationLinks } from '../../../../hooks/use_documentation_links';
|
||||
import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items';
|
||||
import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode';
|
||||
|
@ -36,7 +37,6 @@ import { DropDown } from '../aggregation_dropdown';
|
|||
import { AggListForm } from '../aggregation_list';
|
||||
import { GroupByListForm } from '../group_by_list';
|
||||
import { SourceIndexPreview } from '../source_index_preview';
|
||||
import { PivotPreview } from './pivot_preview';
|
||||
import { KqlFilterBar } from '../../../../../shared_imports';
|
||||
import { SwitchModal } from './switch_modal';
|
||||
|
||||
|
@ -899,7 +899,7 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange,
|
|||
<PivotPreview
|
||||
aggs={aggList}
|
||||
groupBy={groupByList}
|
||||
indexPattern={searchItems.indexPattern}
|
||||
indexPatternTitle={searchItems.indexPattern.title}
|
||||
query={pivotQuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -18,12 +18,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { getPivotQuery } from '../../../../common';
|
||||
import { PivotPreview } from '../../../../components/pivot_preview';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
|
||||
import { AggListSummary } from '../aggregation_list';
|
||||
import { GroupByListSummary } from '../group_by_list';
|
||||
|
||||
import { PivotPreview } from './pivot_preview';
|
||||
import { StepDefineExposedState } from './step_define_form';
|
||||
|
||||
const defaultSearch = '*';
|
||||
|
@ -134,7 +134,7 @@ export const StepDefineSummary: FC<Props> = ({
|
|||
<PivotPreview
|
||||
aggs={aggList}
|
||||
groupBy={groupByList}
|
||||
indexPattern={searchItems.indexPattern}
|
||||
indexPatternTitle={searchItems.indexPattern.title}
|
||||
query={pivotQuery}
|
||||
/>
|
||||
</EuiText>
|
||||
|
|
|
@ -4,218 +4,39 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment-timezone';
|
||||
import { Direction } from '@elastic/eui';
|
||||
import { SortDirection, SORT_DIRECTION, FieldDataColumnType } from '../../../../../shared_imports';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { useApi } from '../../../../hooks/use_api';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
|
||||
import { getPivotQuery, TransformPivotConfig } from '../../../../common';
|
||||
|
||||
import {
|
||||
getFlattenedFields,
|
||||
useRefreshTransformList,
|
||||
EsDoc,
|
||||
PreviewRequestBody,
|
||||
TransformPivotConfig,
|
||||
} from '../../../../common';
|
||||
import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils';
|
||||
import { transformTableFactory } from './transform_table';
|
||||
|
||||
const TransformTable = transformTableFactory<EsDoc>();
|
||||
applyTransformConfigToDefineState,
|
||||
getDefaultStepDefineState,
|
||||
} from '../../../create_transform/components/step_define/';
|
||||
import { PivotPreview } from '../../../../components/pivot_preview';
|
||||
|
||||
interface Props {
|
||||
transformConfig: TransformPivotConfig;
|
||||
}
|
||||
|
||||
export function sortColumns(groupByArr: string[]) {
|
||||
return (a: string, b: string) => {
|
||||
// make sure groupBy fields are always most left columns
|
||||
if (groupByArr.some(aggName => aggName === a) && groupByArr.some(aggName => aggName === b)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
if (groupByArr.some(aggName => aggName === a)) {
|
||||
return -1;
|
||||
}
|
||||
if (groupByArr.some(aggName => aggName === b)) {
|
||||
return 1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
};
|
||||
}
|
||||
|
||||
function getDataFromTransform(
|
||||
transformConfig: TransformPivotConfig
|
||||
): { previewRequest: PreviewRequestBody; groupByArr: string[] | [] } {
|
||||
const index = transformConfig.source.index;
|
||||
const query = transformConfig.source.query;
|
||||
const pivot = transformConfig.pivot;
|
||||
const groupByArr = [];
|
||||
|
||||
const previewRequest: PreviewRequestBody = {
|
||||
source: {
|
||||
index,
|
||||
query,
|
||||
},
|
||||
pivot,
|
||||
};
|
||||
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
|
||||
if (pivot.group_by !== undefined) {
|
||||
for (const key in pivot.group_by) {
|
||||
if (pivot.group_by.hasOwnProperty(key)) {
|
||||
groupByArr.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { groupByArr, previewRequest };
|
||||
}
|
||||
|
||||
export const ExpandedRowPreviewPane: FC<Props> = ({ transformConfig }) => {
|
||||
const [previewData, setPreviewData] = useState<EsDoc[]>([]);
|
||||
const [columns, setColumns] = useState<Array<FieldDataColumnType<EsDoc>> | []>([]);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection | Direction>(SORT_DIRECTION.ASC);
|
||||
const [sortField, setSortField] = useState<keyof typeof previewData[number] | ''>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const api = useApi();
|
||||
const previewConfig = applyTransformConfigToDefineState(
|
||||
getDefaultStepDefineState({} as SearchItems),
|
||||
transformConfig
|
||||
);
|
||||
|
||||
const getPreviewFactory = () => {
|
||||
let concurrentLoads = 0;
|
||||
|
||||
return async function getPreview() {
|
||||
try {
|
||||
concurrentLoads++;
|
||||
|
||||
if (concurrentLoads > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { previewRequest, groupByArr } = getDataFromTransform(transformConfig);
|
||||
setIsLoading(true);
|
||||
const resp: any = await api.getTransformsPreview(previewRequest);
|
||||
setIsLoading(false);
|
||||
|
||||
if (resp.preview.length > 0) {
|
||||
const columnKeys = getFlattenedFields(resp.preview[0]);
|
||||
columnKeys.sort(sortColumns(groupByArr));
|
||||
|
||||
const tableColumns: Array<FieldDataColumnType<EsDoc>> = columnKeys.map(k => {
|
||||
const column: FieldDataColumnType<EsDoc> = {
|
||||
field: k,
|
||||
name: k,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
};
|
||||
|
||||
if (typeof resp.mappings.properties[k] !== 'undefined') {
|
||||
const esFieldType = resp.mappings.properties[k].type;
|
||||
switch (esFieldType) {
|
||||
case ES_FIELD_TYPES.BOOLEAN:
|
||||
column.dataType = 'boolean';
|
||||
break;
|
||||
case ES_FIELD_TYPES.DATE:
|
||||
column.align = 'right';
|
||||
column.render = (d: any) =>
|
||||
formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
|
||||
break;
|
||||
case ES_FIELD_TYPES.BYTE:
|
||||
case ES_FIELD_TYPES.DOUBLE:
|
||||
case ES_FIELD_TYPES.FLOAT:
|
||||
case ES_FIELD_TYPES.HALF_FLOAT:
|
||||
case ES_FIELD_TYPES.INTEGER:
|
||||
case ES_FIELD_TYPES.LONG:
|
||||
case ES_FIELD_TYPES.SCALED_FLOAT:
|
||||
case ES_FIELD_TYPES.SHORT:
|
||||
column.dataType = 'number';
|
||||
break;
|
||||
case ES_FIELD_TYPES.KEYWORD:
|
||||
case ES_FIELD_TYPES.TEXT:
|
||||
column.dataType = 'string';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return column;
|
||||
});
|
||||
|
||||
setPreviewData(resp.preview);
|
||||
setColumns(tableColumns);
|
||||
setSortField(sortField);
|
||||
setSortDirection(sortDirection);
|
||||
}
|
||||
concurrentLoads--;
|
||||
|
||||
if (concurrentLoads > 0) {
|
||||
concurrentLoads = 0;
|
||||
getPreview();
|
||||
}
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
setErrorMessage(
|
||||
i18n.translate('xpack.transform.transformList.stepDetails.previewPane.errorMessage', {
|
||||
defaultMessage: 'Preview could not be loaded',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
useRefreshTransformList({ onRefresh: getPreviewFactory() });
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: previewData.length,
|
||||
pageSizeOptions: [10, 20],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField as string,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
const onTableChange = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: columns[0].field, direction: SORT_DIRECTION.ASC },
|
||||
}: {
|
||||
page?: { index: number; size: number };
|
||||
sort?: { field: keyof typeof previewData[number]; direction: SortDirection | Direction };
|
||||
}) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
|
||||
const transformTableLoading = previewData.length === 0 && isLoading === true;
|
||||
const dataTestSubj = `transformPreviewTabContent${!transformTableLoading ? ' loaded' : ''}`;
|
||||
const indexPatternTitle = Array.isArray(transformConfig.source.index)
|
||||
? transformConfig.source.index.join(',')
|
||||
: transformConfig.source.index;
|
||||
|
||||
return (
|
||||
<div data-test-subj={dataTestSubj}>
|
||||
<TransformTable
|
||||
allowNeutralSort={false}
|
||||
loading={transformTableLoading}
|
||||
compressed
|
||||
items={previewData}
|
||||
columns={columns}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'transformPreviewTabContentRow',
|
||||
})}
|
||||
sorting={sorting}
|
||||
error={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<PivotPreview
|
||||
aggs={previewConfig.aggList}
|
||||
groupBy={previewConfig.groupByList}
|
||||
indexPatternTitle={indexPatternTitle}
|
||||
query={getPivotQuery(previewConfig.searchQuery)}
|
||||
showHeader={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12500,7 +12500,6 @@
|
|||
"xpack.transform.transformList.startModalTitle": "{transformId} を開始",
|
||||
"xpack.transform.transformList.startTransformErrorMessage": "変換 {transformId} の開始中にエラーが発生しました",
|
||||
"xpack.transform.transformList.startTransformSuccessMessage": "変換 {transformId} の開始リクエストが受け付けられました。",
|
||||
"xpack.transform.transformList.stepDetails.previewPane.errorMessage": "プレビューを読み込めませんでした",
|
||||
"xpack.transform.transformList.stopActionName": "停止",
|
||||
"xpack.transform.transformList.stoppedTransformBulkToolTip": "1 つまたは複数の変換が既に開始済みです。",
|
||||
"xpack.transform.transformList.stoppedTransformToolTip": "{transformId} は既に停止済みです。",
|
||||
|
|
|
@ -12500,7 +12500,6 @@
|
|||
"xpack.transform.transformList.startModalTitle": "启动 {transformId}",
|
||||
"xpack.transform.transformList.startTransformErrorMessage": "启动转换 {transformId} 时发生错误",
|
||||
"xpack.transform.transformList.startTransformSuccessMessage": "启动转换 {transformId} 的请求已确认。",
|
||||
"xpack.transform.transformList.stepDetails.previewPane.errorMessage": "无法加载预览",
|
||||
"xpack.transform.transformList.stopActionName": "停止",
|
||||
"xpack.transform.transformList.stoppedTransformBulkToolTip": "一个或多个选定数据帧转换已停止。",
|
||||
"xpack.transform.transformList.stoppedTransformToolTip": "{transformId} 已停止。",
|
||||
|
|
|
@ -61,17 +61,17 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
|
|||
return rows;
|
||||
}
|
||||
|
||||
async parseEuiInMemoryTable(tableSubj: string) {
|
||||
async parseEuiDataGrid(tableSubj: string) {
|
||||
const table = await testSubjects.find(`~${tableSubj}`);
|
||||
const $ = await table.parseDomContent();
|
||||
const rows = [];
|
||||
|
||||
// For each row, get the content of each cell and
|
||||
// add its values as an array to each row.
|
||||
for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) {
|
||||
for (const tr of $.findTestSubjects(`~dataGridRow`).toArray()) {
|
||||
rows.push(
|
||||
$(tr)
|
||||
.find('.euiTableCellContent')
|
||||
.find('.euiDataGridRowCell__truncate')
|
||||
.toArray()
|
||||
.map(cell =>
|
||||
$(cell)
|
||||
|
@ -84,14 +84,14 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
|
|||
return rows;
|
||||
}
|
||||
|
||||
async assertEuiInMemoryTableColumnValues(
|
||||
async assertEuiDataGridColumnValues(
|
||||
tableSubj: string,
|
||||
column: number,
|
||||
expectedColumnValues: string[]
|
||||
) {
|
||||
await retry.tryForTime(2000, async () => {
|
||||
// get a 2D array of rows and cell values
|
||||
const rows = await this.parseEuiInMemoryTable(tableSubj);
|
||||
const rows = await this.parseEuiDataGrid(tableSubj);
|
||||
|
||||
// reduce the rows data to an array of unique values in the specified column
|
||||
const uniqueColumnValues = rows
|
||||
|
@ -148,17 +148,17 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
|
|||
|
||||
await testSubjects.existOrFail('transformPreviewTab');
|
||||
await testSubjects.click('transformPreviewTab');
|
||||
await testSubjects.existOrFail('~transformPreviewTabContent');
|
||||
await testSubjects.existOrFail('~transformPivotPreview');
|
||||
}
|
||||
|
||||
public async waitForTransformsExpandedRowPreviewTabToLoad() {
|
||||
await testSubjects.existOrFail('~transformPreviewTabContent', { timeout: 60 * 1000 });
|
||||
await testSubjects.existOrFail('transformPreviewTabContent loaded', { timeout: 30 * 1000 });
|
||||
await testSubjects.existOrFail('~transformPivotPreview', { timeout: 60 * 1000 });
|
||||
await testSubjects.existOrFail('transformPivotPreview loaded', { timeout: 30 * 1000 });
|
||||
}
|
||||
|
||||
async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) {
|
||||
await this.waitForTransformsExpandedRowPreviewTabToLoad();
|
||||
await this.assertEuiInMemoryTableColumnValues('transformPreviewTabContent', column, values);
|
||||
await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue