From 09cd2a552437da3c6c2744ecfa35dac2a6499753 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 10 Apr 2020 13:22:15 +0200 Subject: [PATCH 01/34] [ML] Move SourceIndexPreview to more generic IndexPreview. --- .../__snapshots__/expanded_row.test.tsx.snap | 0 .../index_preview}/common.test.ts | 12 +- .../index_preview}/common.ts | 4 +- .../index_preview}/expanded_row.test.tsx | 0 .../index_preview}/expanded_row.tsx | 0 .../index_preview}/index.ts | 2 +- .../index_preview/index_preview.test.tsx} | 11 +- .../index_preview/index_preview.tsx | 154 +++++++++ .../index_preview/index_preview_title.tsx | 18 ++ .../index_preview/use_index_data.test.tsx} | 26 +- .../index_preview/use_index_data.ts | 294 ++++++++++++++++++ .../source_index_preview.tsx | 293 ----------------- .../use_source_index_data.ts | 143 --------- .../step_define/step_define_form.tsx | 11 +- .../apps/transform/creation_index_pattern.ts | 16 +- .../apps/transform/creation_saved_search.ts | 14 +- .../services/transform_ui/wizard.ts | 16 +- 17 files changed, 528 insertions(+), 486 deletions(-) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview => components/index_preview}/__snapshots__/expanded_row.test.tsx.snap (100%) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview => components/index_preview}/common.test.ts (57%) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview => components/index_preview}/common.ts (70%) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview => components/index_preview}/expanded_row.test.tsx (100%) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview => components/index_preview}/expanded_row.tsx (100%) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview => components/index_preview}/index.ts (79%) rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview/source_index_preview.test.tsx => components/index_preview/index_preview.test.tsx} (75%) create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/index_preview_title.tsx rename x-pack/plugins/transform/public/app/{sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx => components/index_preview/use_index_data.test.tsx} (59%) create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/components/index_preview/__snapshots__/expanded_row.test.tsx.snap similarity index 100% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap rename to x-pack/plugins/transform/public/app/components/index_preview/__snapshots__/expanded_row.test.tsx.snap diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts b/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts similarity index 57% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts rename to x-pack/plugins/transform/public/app/components/index_preview/common.test.ts index d3bf81bba2e56d..a3a811085f1508 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts @@ -4,24 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SimpleQuery } from '../../../../common'; +import { SimpleQuery } from '../../common'; -import { getSourceIndexDevConsoleStatement } from './common'; +import { getIndexDevConsoleStatement } from './common'; -describe('Transform: Source Index Preview Common', () => { - test('getSourceIndexDevConsoleStatement()', () => { +describe('Transform: Index Preview Common', () => { + test('getIndexDevConsoleStatement()', () => { const query: SimpleQuery = { query_string: { query: '*', default_operator: 'AND', }, }; - const sourceIndexPreviewDevConsoleStatement = getSourceIndexDevConsoleStatement( + const indexPreviewDevConsoleStatement = getIndexDevConsoleStatement( query, 'the-index-pattern-title' ); - expect(sourceIndexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search + expect(indexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search { "query": { "query_string": { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts b/x-pack/plugins/transform/public/app/components/index_preview/common.ts similarity index 70% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts rename to x-pack/plugins/transform/public/app/components/index_preview/common.ts index c34675463bf8b5..90f30ec2fef881 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/common.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PivotQuery } from '../../../../common'; +import { PivotQuery } from '../../common'; -export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { +export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { return `GET ${indexPatternTitle}/_search\n${JSON.stringify( { query, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.test.tsx similarity index 100% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx rename to x-pack/plugins/transform/public/app/components/index_preview/expanded_row.test.tsx diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.tsx similarity index 100% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx rename to x-pack/plugins/transform/public/app/components/index_preview/expanded_row.tsx diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts b/x-pack/plugins/transform/public/app/components/index_preview/index.ts similarity index 79% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts rename to x-pack/plugins/transform/public/app/components/index_preview/index.ts index a13e678813a00a..960cc0b313acc2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SourceIndexPreview } from './source_index_preview'; +export { IndexPreview } from './index_preview'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx similarity index 75% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx rename to x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx index 32f6ff9490a0f4..fcdfbc99f5096d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx @@ -8,15 +8,15 @@ import React from 'react'; import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { getPivotQuery } from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; +import { getPivotQuery } from '../../common'; +import { SearchItems } from '../../hooks/use_search_items'; -import { SourceIndexPreview } from './source_index_preview'; +import { IndexPreview } from './index_preview'; jest.mock('../../../../../shared_imports'); jest.mock('../../../../../app/app_dependencies'); -describe('Transform: ', () => { +describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async done => { // Arrange @@ -26,8 +26,9 @@ describe('Transform: ', () => { fields: [] as any[], } as SearchItems['indexPattern'], query: getPivotQuery('the-query'), + title: 'the-index-preview-title', }; - const { getByText } = render(); + const { getByText } = render(); // Act // Assert diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx new file mode 100644 index 00000000000000..af783ec6345456 --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiCodeBlock, + EuiCopy, + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, +} from '@elastic/eui'; + +import { useToastNotifications } from '../../app_dependencies'; +import { euiDataGridStyle, euiDataGridToolbarSettings, PivotQuery } from '../../common'; +import { SearchItems } from '../../hooks/use_search_items'; + +import { getIndexDevConsoleStatement } from './common'; +import { IndexPreviewTitle } from './index_preview_title'; +import { INDEX_STATUS, useIndexData } from './use_index_data'; + +interface Props { + indexPattern: SearchItems['indexPattern']; + query: PivotQuery; + title: string; +} + +export const IndexPreview: React.FC = React.memo(({ indexPattern, query, title }) => { + const toastNotifications = useToastNotifications(); + + const { + columns, + errorMessage, + invalidSortingColumnns, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems: data, + visibleColumns, + } = useIndexData(indexPattern, query); + + useEffect(() => { + if (invalidSortingColumnns.length > 0) { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.transform.indexPreview.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, [invalidSortingColumnns, toastNotifications]); + + if (status === INDEX_STATUS.LOADED && data.length === 0) { + return ( +
+ + +

+ {i18n.translate('xpack.transform.indexPreview.IndexNoDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', + })} +

+
+
+ ); + } + + const euiCopyText = i18n.translate('xpack.transform.indexPreview.copyClipboardTooltip', { + defaultMessage: 'Copy Dev Console statement of the index preview to the clipboard.', + }); + + return ( +
+ + + + + + + {(copy: () => void) => ( + + )} + + + +
+ {status === INDEX_STATUS.LOADING && } + {status !== INDEX_STATUS.LOADING && ( + + )} +
+ {status === INDEX_STATUS.ERROR && ( +
+ + + {errorMessage} + + + +
+ )} + +
+ ); +}); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview_title.tsx b/x-pack/plugins/transform/public/app/components/index_preview/index_preview_title.tsx new file mode 100644 index 00000000000000..caeb66c51d96e7 --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/index_preview_title.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTitle } from '@elastic/eui'; + +interface IndexPreviewTitle { + indexPreviewTitle: string; +} +export const IndexPreviewTitle: React.FC = ({ indexPreviewTitle }) => ( + + {indexPreviewTitle} + +); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx similarity index 59% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx rename to x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx index 5a1d8a8db5b421..d8e7d141971d1c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx @@ -7,12 +7,9 @@ import { renderHook } from '@testing-library/react-hooks'; import '@testing-library/jest-dom/extend-expect'; -import { SimpleQuery } from '../../../../common'; -import { - SOURCE_INDEX_STATUS, - useSourceIndexData, - UseSourceIndexDataReturnType, -} from './use_source_index_data'; +import { SimpleQuery } from '../../common'; +import { SearchItems } from '../../hooks/use_search_items'; +import { INDEX_STATUS, useIndexData, UseIndexDataReturnType } from './use_index_data'; jest.mock('../../../../hooks/use_api'); @@ -26,15 +23,22 @@ 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) + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ) ); - const sourceIndexObj: UseSourceIndexDataReturnType = result.current; + const IndexObj: UseIndexDataReturnType = result.current; await waitForNextUpdate(); - expect(sourceIndexObj.errorMessage).toBe(''); - expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); - expect(sourceIndexObj.tableItems).toEqual([]); + expect(IndexObj.errorMessage).toBe(''); + expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.tableItems).toEqual([]); done(); }); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts new file mode 100644 index 00000000000000..5715fecff70c43 --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts @@ -0,0 +1,294 @@ +/* + * 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 moment from 'moment-timezone'; +import { useCallback, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react'; + +import { SearchResponse } from 'elasticsearch'; + +import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; + +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; + +import { Dictionary } from '../../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../../common/utils/object_utils'; + +import { getErrorMessage } from '../../../shared_imports'; + +import { + isDefaultQuery, + matchAllQuery, + EsDocSource, + EsFieldName, + PivotQuery, + INIT_MAX_COLUMNS, +} from '../../common'; +import { SearchItems } from '../../hooks/use_search_items'; +import { useApi } from '../../hooks/use_api'; + +export enum INDEX_STATUS { + UNUSED, + LOADING, + LOADED, + ERROR, +} + +type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + +type IndexSearchResponse = SearchResponse7; + +type IndexPagination = Pick; +const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; + +type OnChangeItemsPerPage = (pageSize: any) => void; +type OnChangePage = (pageIndex: any) => void; +type OnSort = ( + sc: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> +) => void; +type RenderCellValue = ({ + rowIndex, + columnId, + setCellProps, +}: { + rowIndex: number; + columnId: string; + setCellProps: any; +}) => any; + +export interface UseIndexDataReturnType { + columns: Array<{ id: string; schema: string | undefined }>; + errorMessage: string; + invalidSortingColumnns: EsFieldName[]; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + setPagination: Dispatch>; + setVisibleColumns: Dispatch>; + renderCellValue: RenderCellValue; + rowCount: number; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: EsDocSource[]; + visibleColumns: EsFieldName[]; +} + +export const useIndexData = ( + indexPattern: SearchItems['indexPattern'], + query: PivotQuery +): UseIndexDataReturnType => { + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); + const api = useApi(); + + useEffect(() => { + setPagination(defaultPagination); + }, [query]); + + const getIndexData = async function() { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + 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, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map(d => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } + + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } + + return true; + }); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(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 KBN_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case KBN_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case KBN_FIELD_TYPES.NUMBER: + schema = 'numeric'; + break; + } + + return { id, schema }; + }), + ]; + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + const defaultVisibleColumns = indexPatternFields.splice(0, INIT_MAX_COLUMNS); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] + ); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [setPagination] + ); + + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + const field = indexPattern.fields.getByName(columnId); + if (field?.type === KBN_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { + return cellValue ? 'true' : 'false'; + } + + return cellValue; + }; + }, [indexPattern.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + + return { + columns, + errorMessage, + invalidSortingColumnns, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setPagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx deleted file mode 100644 index bcdeb7ddb0d365..00000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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 moment from 'moment-timezone'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PivotQuery, - INIT_MAX_COLUMNS, -} 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'; - -interface SourceIndexPreviewTitle { - indexPatternTitle: string; -} -const SourceIndexPreviewTitle: React.FC = ({ indexPatternTitle }) => ( - - - {i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternTitle', { - defaultMessage: 'Source index {indexPatternTitle}', - values: { indexPatternTitle }, - })} - - -); - -interface Props { - indexPattern: SearchItems['indexPattern']; - query: PivotQuery; -} - -export const SourceIndexPreview: React.FC = 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)) { - return false; - } - - const fieldParts = f.split('.'); - const lastPart = fieldParts.pop(); - if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { - return false; - } - - return true; - }); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(indexPatternFields.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPatternFields.join()]); - - 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); - - // 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 KBN_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - schema = 'datetime'; - break; - case KBN_FIELD_TYPES.GEO_POINT: - case KBN_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case KBN_FIELD_TYPES.NUMBER: - schema = 'numeric'; - break; - } - - 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((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 => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = data.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(data[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - const field = indexPattern.fields.getByName(columnId); - if (field?.type === KBN_FIELD_TYPES.DATE) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]); - - if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { - return ( -
- - -

- {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', { - defaultMessage: - 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', - })} -

-
-
- ); - } - - const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', - }); - - return ( -
- - - - - - - {(copy: () => void) => ( - - )} - - - -
- {status === SOURCE_INDEX_STATUS.LOADING && } - {status !== SOURCE_INDEX_STATUS.LOADING && ( - - )} -
- {status === SOURCE_INDEX_STATUS.ERROR && ( -
- - - {errorMessage} - - - -
- )} - -
- ); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts deleted file mode 100644 index 5301a3c168a512..00000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IIndexPattern } from 'src/plugins/data/public'; - -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, - LOADING, - LOADED, - ERROR, -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -interface ErrorResponse { - request: Dictionary; - response: Dictionary; - body: { - statusCode: number; - error: string; - message: string; - }; - name: string; - req: Dictionary; - res: Dictionary; -} - -const isErrorResponse = (arg: any): arg is ErrorResponse => { - return arg?.body?.error !== undefined && arg?.body?.message !== undefined; -}; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -type SourceIndexSearchResponse = SearchResponse7; - -type SourceIndexPagination = Pick; -const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 }; - -export interface UseSourceIndexDataReturnType { - errorMessage: string; - pagination: SourceIndexPagination; - setPagination: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: SOURCE_INDEX_STATUS; - tableItems: EsDocSource[]; -} - -export const useSourceIndexData = ( - indexPattern: IIndexPattern, - query: PivotQuery -): UseSourceIndexDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); - const api = useApi(); - - useEffect(() => { - setPagination(defaultPagination); - }, [query]); - - const getSourceIndexData = async function() { - setErrorMessage(''); - setStatus(SOURCE_INDEX_STATUS.LOADING); - - 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, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }; - - try { - const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map(d => d._source); - - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(SOURCE_INDEX_STATUS.LOADED); - } catch (e) { - if (isErrorResponse(e)) { - setErrorMessage(`${e.body.error}: ${e.body.message}`); - } else { - setErrorMessage(JSON.stringify(e, null, 2)); - } - setStatus(SOURCE_INDEX_STATUS.ERROR); - } - }; - - useEffect(() => { - getSourceIndexData(); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - return { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems, - }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 320e405b5d4371..73cbafd5eede73 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -45,7 +45,7 @@ import { dictionaryToArray, Dictionary } from '../../../../../../common/types/co import { DropDown } from '../aggregation_dropdown'; import { AggListForm } from '../aggregation_list'; import { GroupByListForm } from '../group_by_list'; -import { SourceIndexPreview } from '../source_index_preview'; +import { IndexPreview } from '../../../../components/index_preview'; import { SwitchModal } from './switch_modal'; import { @@ -973,7 +973,14 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, - + { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the source index preview', async () => { - await transform.wizard.assertSourceIndexPreview( - testData.expected.sourcePreview.columns, - testData.expected.sourcePreview.rows + it('shows the index preview', async () => { + await transform.wizard.assertIndexPreview( + testData.expected.indexPreview.columns, + testData.expected.indexPreview.rows ); }); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 993bd3a79abbc1..fd5673de0d7a7c 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -65,7 +65,7 @@ export default function({ getService }: FtrProviderContext) { progress: '100', }, sourceIndex: 'ft_farequote', - sourcePreview: { + indexPreview: { column: 2, values: ['ASA'], }, @@ -101,14 +101,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the filtered source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewColumnValues( - testData.expected.sourcePreview.column, - testData.expected.sourcePreview.values + it('shows the filtered index preview', async () => { + await transform.wizard.assertIndexPreviewColumnValues( + testData.expected.indexPreview.column, + testData.expected.indexPreview.values ); }); diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index ba9096a372d9a4..e63af493438d67 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -52,8 +52,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertDetailsSummaryExists(); }, - async assertSourceIndexPreviewExists(subSelector?: string) { - let selector = 'transformSourceIndexPreview'; + async assertIndexPreviewExists(subSelector?: string) { + let selector = 'transformIndexPreview'; if (subSelector !== undefined) { selector = `${selector} ${subSelector}`; } else { @@ -62,8 +62,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, - async assertSourceIndexPreviewLoaded() { - await this.assertSourceIndexPreviewExists('loaded'); + async assertIndexPreviewLoaded() { + await this.assertIndexPreviewExists('loaded'); }, async assertPivotPreviewExists(subSelector?: string) { @@ -124,10 +124,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreview(columns: number, rows: number) { + async assertIndexPreview(columns: number, rows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rowsData = await this.parseEuiDataGrid('transformSourceIndexPreview'); + const rowsData = await this.parseEuiDataGrid('transformIndexPreview'); expect(rowsData).to.length( rows, @@ -143,8 +143,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiDataGridColumnValues('transformSourceIndexPreview', column, values); + async assertIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiDataGridColumnValues('transformIndexPreview', column, values); }, async assertPivotPreviewColumnValues(column: number, values: string[]) { From 0322ac1b9aa1baa527e3e02814c2f79bffd13137 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 14 Apr 2020 11:31:04 +0200 Subject: [PATCH 02/34] [ML] Consolidate code, source and preview table use same component. --- .../public/__mocks__/shared_imports.ts | 1 + .../__snapshots__/expanded_row.test.tsx.snap | 71 ---- .../components/index_preview/common.test.ts | 116 +++++- .../app/components/index_preview/common.ts | 55 ++- .../index_preview/expanded_row.test.tsx | 46 --- .../components/index_preview/expanded_row.tsx | 22 -- .../app/components/index_preview/index.ts | 4 + .../index_preview/index_preview.test.tsx | 32 +- .../index_preview/index_preview.tsx | 103 ++++-- .../app/components/index_preview/types.ts | 57 +++ .../index_preview/use_index_data.test.tsx | 6 +- .../index_preview/use_index_data.ts | 61 +--- .../index_preview/use_pivot_data.ts | 301 +++++++++++++++ .../components/pivot_preview/common.test.ts | 117 ------ .../app/components/pivot_preview/common.ts | 60 --- .../app/components/pivot_preview/index.ts | 7 - .../pivot_preview/pivot_preview.test.tsx | 55 --- .../pivot_preview/pivot_preview.tsx | 345 ------------------ .../use_pivot_preview_data.test.tsx | 69 ---- .../pivot_preview/use_pivot_preview_data.ts | 91 ----- .../step_define/step_define_form.tsx | 44 ++- .../step_define/step_define_summary.tsx | 49 ++- .../expanded_row_preview_pane.tsx | 21 +- x-pack/run_functional_tests.sh | 3 + 24 files changed, 722 insertions(+), 1014 deletions(-) delete mode 100644 x-pack/plugins/transform/public/app/components/index_preview/__snapshots__/expanded_row.test.tsx.snap delete mode 100644 x-pack/plugins/transform/public/app/components/index_preview/expanded_row.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/index_preview/expanded_row.tsx create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/types.ts create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/common.ts delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/index.ts delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx delete mode 100644 x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts create mode 100755 x-pack/run_functional_tests.sh diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 0db11ffa061c0a..68925401a0bd2b 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -13,3 +13,4 @@ export const useRequest = jest.fn(() => ({ })); export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; export const SORT_DIRECTION = { ASC: 'asc' }; +export { getErrorMessage } from '../../../ml/common/util/errors'; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/components/index_preview/__snapshots__/expanded_row.test.tsx.snap deleted file mode 100644 index b668c7d8e4a698..00000000000000 --- a/x-pack/plugins/transform/public/app/components/index_preview/__snapshots__/expanded_row.test.tsx.snap +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Test against strings, objects and arrays. 1`] = ` - - - - name - : - - - - the-name -    - - - - - nested.inner1 - : - - - - the-inner-1 -    - - - - - nested.inner2 - : - - - - the-inner-2 -    - - - - - arrayString - : - - - - ["the-array-string-1","the-array-string-2"] -    - - - - - arrayObject - : - - - - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] -    - - - -`; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts b/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts index a3a811085f1508..f82c5ba99890d4 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts @@ -4,9 +4,121 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SimpleQuery } from '../../common'; +import { EuiDataGridSorting } from '@elastic/eui'; -import { getIndexDevConsoleStatement } from './common'; +import { + getPreviewRequestBody, + PivotAggsConfig, + PivotGroupByConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, + SimpleQuery, +} from '../../common'; + +import { + getIndexDevConsoleStatement, + 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" + } + } + } + } +} +`); + }); +}); describe('Transform: Index Preview Common', () => { test('getIndexDevConsoleStatement()', () => { diff --git a/x-pack/plugins/transform/public/app/components/index_preview/common.ts b/x-pack/plugins/transform/public/app/components/index_preview/common.ts index 90f30ec2fef881..a560c1d2461f83 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/common.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/common.ts @@ -4,7 +4,60 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PivotQuery } from '../../common'; +import { EuiDataGridSorting } from '@elastic/eui'; + +import { getNestedProperty } from '../../../../common/utils/object_utils'; + +import { PivotQuery, 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`; +}; export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { return `GET ${indexPatternTitle}/_search\n${JSON.stringify( diff --git a/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.test.tsx deleted file mode 100644 index ddd1a1482fd357..00000000000000 --- a/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import React from 'react'; - -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; -import { getFlattenedFields } from '../../../../common'; - -import { ExpandedRow } from './expanded_row'; - -describe('Transform: ', () => { - test('Test against strings, objects and arrays.', () => { - const source = { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - } as Record; - - const flattenedSource = getFlattenedFields(source).reduce((p, c) => { - p[c] = getNestedProperty(source, c); - if (p[c] === undefined) { - p[c] = source[`"${c}"`]; - } - return p; - }, {} as Record); - - const props = { - item: { - _id: 'the-id', - _source: flattenedSource, - }, - }; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.tsx deleted file mode 100644 index 9b83a3e5da8a8c..00000000000000 --- a/x-pack/plugins/transform/public/app/components/index_preview/expanded_row.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { EuiBadge, EuiText } from '@elastic/eui'; - -import { EsDoc } from '../../../../common'; - -export const ExpandedRow: React.FC<{ item: EsDoc }> = ({ item }) => ( - - {Object.entries(item._source).map(([k, value]) => ( - - {k}: - {typeof value === 'string' ? value : JSON.stringify(value)}   - - ))} - -); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index.ts b/x-pack/plugins/transform/public/app/components/index_preview/index.ts index 960cc0b313acc2..401e8e202b83e5 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './common'; +export { useIndexData } from './use_index_data'; +export { usePivotData } from './use_pivot_data'; export { IndexPreview } from './index_preview'; +export { INDEX_STATUS } from './types'; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx index fcdfbc99f5096d..e70f5bf08299d0 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx +++ b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx @@ -8,31 +8,39 @@ import React from 'react'; import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { getPivotQuery } from '../../common'; import { SearchItems } from '../../hooks/use_search_items'; +import { useIndexData } from './use_index_data'; import { IndexPreview } from './index_preview'; -jest.mock('../../../../../shared_imports'); -jest.mock('../../../../../app/app_dependencies'); +jest.mock('../../../shared_imports'); +jest.mock('../../app_dependencies'); describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async done => { // Arrange - const props = { - indexPattern: { - title: 'the-index-pattern-title', - fields: [] as any[], - } as SearchItems['indexPattern'], - query: getPivotQuery('the-query'), - title: 'the-index-preview-title', + const indexPattern = { + title: 'the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const Wrapper = () => { + const props = { + ...useIndexData(indexPattern, { match_all: {} }), + title: 'the-index-preview-title', + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + }; + + return ; }; - const { getByText } = render(); + const { getByText } = render(); // Act // Assert - expect(getByText(`Source index ${props.indexPattern.title}`)).toBeInTheDocument(); + expect(getByText('the-index-preview-title')).toBeInTheDocument(); await wait(); done(); }); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx index af783ec6345456..4f05c90df5284a 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx +++ b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -21,26 +21,35 @@ import { } from '@elastic/eui'; import { useToastNotifications } from '../../app_dependencies'; -import { euiDataGridStyle, euiDataGridToolbarSettings, PivotQuery } from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; +import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../common'; -import { getIndexDevConsoleStatement } from './common'; import { IndexPreviewTitle } from './index_preview_title'; -import { INDEX_STATUS, useIndexData } from './use_index_data'; +import { INDEX_STATUS, UseIndexDataReturnType } from './types'; -interface Props { - indexPattern: SearchItems['indexPattern']; - query: PivotQuery; +interface PropsWithHeader extends UseIndexDataReturnType { + copyToClipboard: string; + copyToClipboardDescription: string; + dataTestSubj: string; title: string; } -export const IndexPreview: React.FC = React.memo(({ indexPattern, query, title }) => { - const toastNotifications = useToastNotifications(); +interface PropsWithoutHeader extends UseIndexDataReturnType { + dataTestSubj: string; +} + +function isWithHeader(arg: any): arg is PropsWithHeader { + return typeof arg?.title === 'string' && arg?.title !== ''; +} + +type Props = PropsWithHeader | PropsWithoutHeader; +export const IndexPreview: FC = props => { const { columns, + dataTestSubj, errorMessage, invalidSortingColumnns, + noDataMessage, onChangeItemsPerPage, onChangePage, onSort, @@ -52,7 +61,9 @@ export const IndexPreview: React.FC = React.memo(({ indexPattern, query, status, tableItems: data, visibleColumns, - } = useIndexData(indexPattern, query); + } = props; + + const toastNotifications = useToastNotifications(); useEffect(() => { if (invalidSortingColumnns.length > 0) { @@ -69,8 +80,8 @@ export const IndexPreview: React.FC = React.memo(({ indexPattern, query, if (status === INDEX_STATUS.LOADED && data.length === 0) { return ( -
- +
+ {isWithHeader(props) && } = React.memo(({ indexPattern, query, ); } - const euiCopyText = i18n.translate('xpack.transform.indexPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the index preview to the clipboard.', - }); + if (noDataMessage !== '') { + return ( +
+ {isWithHeader(props) && } + +

{noDataMessage}

+
+
+ ); + } return ( -
- - - - - - - {(copy: () => void) => ( - - )} - - - +
+ {isWithHeader(props) && ( + + + + + + + {(copy: () => void) => ( + + )} + + + + )}
{status === INDEX_STATUS.LOADING && } {status !== INDEX_STATUS.LOADING && ( @@ -118,9 +145,9 @@ export const IndexPreview: React.FC = React.memo(({ indexPattern, query, )}
{status === INDEX_STATUS.ERROR && ( -
+
= React.memo(({ indexPattern, query,
)} = React.memo(({ indexPattern, query, />
); -}); +}; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/types.ts b/x-pack/plugins/transform/public/app/components/index_preview/types.ts new file mode 100644 index 00000000000000..ab126f8f0c8aac --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/types.ts @@ -0,0 +1,57 @@ +/* + * 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 { Dispatch, SetStateAction } from 'react'; + +import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; + +import { EsDocSource, EsFieldName } from '../../common'; + +export enum INDEX_STATUS { + UNUSED, + LOADING, + LOADED, + ERROR, +} + +export type IndexPagination = Pick; + +export type OnChangeItemsPerPage = (pageSize: any) => void; +export type OnChangePage = (pageIndex: any) => void; +export type OnSort = ( + sc: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> +) => void; +export type RenderCellValue = ({ + rowIndex, + columnId, + setCellProps, +}: { + rowIndex: number; + columnId: string; + setCellProps: any; +}) => any; + +export interface UseIndexDataReturnType { + columns: Array<{ id: string; schema: string | undefined }>; + errorMessage: string; + invalidSortingColumnns: EsFieldName[]; + noDataMessage: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + setPagination: Dispatch>; + setVisibleColumns: Dispatch>; + renderCellValue: RenderCellValue; + rowCount: number; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: EsDocSource[]; + visibleColumns: EsFieldName[]; +} diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx index d8e7d141971d1c..89d3cc69317bb8 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx @@ -9,9 +9,11 @@ import '@testing-library/jest-dom/extend-expect'; import { SimpleQuery } from '../../common'; import { SearchItems } from '../../hooks/use_search_items'; -import { INDEX_STATUS, useIndexData, UseIndexDataReturnType } from './use_index_data'; +import { useIndexData } from './use_index_data'; +import { INDEX_STATUS, UseIndexDataReturnType } from './types'; -jest.mock('../../../../hooks/use_api'); +jest.mock('../../../shared_imports'); +jest.mock('../../hooks/use_api'); const query: SimpleQuery = { query_string: { diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts index 5715fecff70c43..a0f3ceac3039d6 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts @@ -5,11 +5,11 @@ */ import moment from 'moment-timezone'; -import { useCallback, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; +import { EuiDataGridSorting } from '@elastic/eui'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; @@ -30,12 +30,15 @@ import { import { SearchItems } from '../../hooks/use_search_items'; import { useApi } from '../../hooks/use_api'; -export enum INDEX_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} +import { + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from './types'; type EsSorting = Dictionary<{ order: 'asc' | 'desc'; @@ -53,45 +56,8 @@ interface SearchResponse7 extends SearchResponse { type IndexSearchResponse = SearchResponse7; -type IndexPagination = Pick; const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; -type OnChangeItemsPerPage = (pageSize: any) => void; -type OnChangePage = (pageIndex: any) => void; -type OnSort = ( - sc: Array<{ - id: string; - direction: 'asc' | 'desc'; - }> -) => void; -type RenderCellValue = ({ - rowIndex, - columnId, - setCellProps, -}: { - rowIndex: number; - columnId: string; - setCellProps: any; -}) => any; - -export interface UseIndexDataReturnType { - columns: Array<{ id: string; schema: string | undefined }>; - errorMessage: string; - invalidSortingColumnns: EsFieldName[]; - onChangeItemsPerPage: OnChangeItemsPerPage; - onChangePage: OnChangePage; - onSort: OnSort; - pagination: IndexPagination; - setPagination: Dispatch>; - setVisibleColumns: Dispatch>; - renderCellValue: RenderCellValue; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableItems: EsDocSource[]; - visibleColumns: EsFieldName[]; -} - export const useIndexData = ( indexPattern: SearchItems['indexPattern'], query: PivotQuery @@ -106,7 +72,9 @@ export const useIndexData = ( useEffect(() => { setPagination(defaultPagination); - }, [query]); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); const getIndexData = async function() { setErrorMessage(''); @@ -281,6 +249,7 @@ export const useIndexData = ( onChangeItemsPerPage, onChangePage, onSort, + noDataMessage: '', pagination, setPagination, setVisibleColumns, diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts b/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts new file mode 100644 index 00000000000000..552dfdcfaa8023 --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts @@ -0,0 +1,301 @@ +/* + * 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 moment from 'moment-timezone'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { EuiDataGridSorting } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +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 { getErrorMessage } from '../../../shared_imports'; + +import { + getPreviewRequestBody, + EsDocSource, + EsFieldName, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PivotGroupByConfig, + PivotQuery, + PreviewMappings, + INIT_MAX_COLUMNS, +} from '../../common'; +import { SearchItems } from '../../hooks/use_search_items'; +import { useApi } from '../../hooks/use_api'; + +import { multiColumnSortFactory } from './common'; +import { + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from './types'; + +const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; + +function sortColumns(groupByArr: PivotGroupByConfig[]) { + return (a: string, b: string) => { + // make sure groupBy fields are always most left columns + if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { + return a.localeCompare(b); + } + if (groupByArr.some(d => d.aggName === a)) { + return -1; + } + if (groupByArr.some(d => d.aggName === b)) { + return 1; + } + return a.localeCompare(b); + }; +} + +export const usePivotData = ( + indexPatternTitle: SearchItems['indexPattern']['title'], + query: PivotQuery, + aggs: PivotAggsConfigDict, + groupBy: PivotGroupByConfigDict +): UseIndexDataReturnType => { + const [noDataMessage, setNoDataMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); + const [previewMappings, setPreviewMappings] = useState({ properties: {} }); + const api = useApi(); + + const aggsArr = dictionaryToArray(aggs); + const groupByArr = dictionaryToArray(groupBy); + + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); + + const getPreviewData = async () => { + if (aggsArr.length === 0 || groupByArr.length === 0) { + setTableItems([]); + setRowCount(0); + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', { + defaultMessage: 'Please choose at least one group-by field and aggregation.', + }) + ); + return; + } + + setErrorMessage(''); + setNoDataMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resp = await api.getTransformsPreview(previewRequest); + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappings(resp.generated_dest_index.mappings); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); + } + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setTableItems([]); + setRowCount(0); + setPreviewMappings({ properties: {} }); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + setPagination(defaultPagination); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + useEffect(() => { + getPreviewData(); + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + indexPatternTitle, + JSON.stringify(aggsArr), + JSON.stringify(groupByArr), + JSON.stringify(query), + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // EuiDataGrid State + const columns = 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: + case ES_FIELD_TYPES.DATE_NANOS: + 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 }; + }); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + const defaultVisibleColumns = columnKeys.splice(0, INIT_MAX_COLUMNS); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + if (sortingColumns.length > 0) { + tableItems.sort(multiColumnSortFactory(sortingColumns)); + } + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] + ); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [setPagination] + ); + + const pageData = tableItems.slice( + pagination.pageIndex * pagination.pageSize, + (pagination.pageIndex + 1) * pagination.pageSize + ); + + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = pageData.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if ( + [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( + previewMappings.properties[columnId].type + ) + ) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + return cellValue ? 'true' : 'false'; + } + + return cellValue; + }; + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + + return { + columns, + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setPagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts deleted file mode 100644 index 172256ddb5ceed..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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" - } - } - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts deleted file mode 100644 index 498c3a3ac60af6..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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`; -}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts deleted file mode 100644 index 049e73d6309fc3..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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'; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx deleted file mode 100644 index 5ed50eaab46ba7..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { - getPivotQuery, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../common'; - -import { PivotPreview } from './pivot_preview'; - -jest.mock('../../../shared_imports'); -jest.mock('../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - 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-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const props = { - aggs: { 'the-agg-name': agg }, - groupBy: { 'the-group-by-name': groupBy }, - indexPatternTitle: 'the-index-pattern-title', - query: getPivotQuery('the-query'), - }; - - const { getByText } = render(); - - // Act - // Assert - expect(getByText('Transform pivot preview')).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx deleted file mode 100644 index c50df0366d6987..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx +++ /dev/null @@ -1,345 +0,0 @@ -/* - * 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 moment from 'moment-timezone'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiDataGridSorting, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiTitle, -} from '@elastic/eui'; - -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, - euiDataGridToolbarSettings, - EsFieldName, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfig, - PivotGroupByConfigDict, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; - -import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; - -function sortColumns(groupByArr: PivotGroupByConfig[]) { - return (a: string, b: string) => { - // make sure groupBy fields are always most left columns - if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { - return a.localeCompare(b); - } - if (groupByArr.some(d => d.aggName === a)) { - return -1; - } - if (groupByArr.some(d => d.aggName === b)) { - return 1; - } - return a.localeCompare(b); - }; -} - -interface PreviewTitleProps { - previewRequest: PreviewRequestBody; -} - -const PreviewTitle: FC = ({ previewRequest }) => { - const euiCopyText = i18n.translate('xpack.transform.pivotPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', - }); - - return ( - - - - - {i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { - defaultMessage: 'Transform pivot preview', - })} - - - - - - {(copy: () => void) => ( - - )} - - - - ); -}; - -interface ErrorMessageProps { - message: string; -} - -const ErrorMessage: FC = ({ message }) => ( - - {message} - -); - -interface PivotPreviewProps { - aggs: PivotAggsConfigDict; - groupBy: PivotGroupByConfigDict; - indexPatternTitle: SearchItems['indexPattern']['title']; - query: PivotQuery; - showHeader?: boolean; -} - -const defaultPagination = { pageIndex: 0, pageSize: 5 }; - -export const PivotPreview: FC = React.memo( - ({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => { - const { - previewData: data, - previewMappings, - errorMessage, - previewRequest, - status, - } = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy); - const groupByArr = dictionaryToArray(groupBy); - - // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - key => previewMappings.properties[key].type !== 'object' - ); - columnKeys.sort(sortColumns(groupByArr)); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(columnKeys.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columnKeys.join()]); - - const [pagination, setPagination] = useState(defaultPagination); - - // Reset pagination if data changes. This is to avoid ending up with an empty table - // when for example the user selected a page that is not available with the updated data. - useEffect(() => { - setPagination(defaultPagination); - }, [data.length]); - - // EuiDataGrid State - 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: - case ES_FIELD_TYPES.DATE_NANOS: - 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 => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - // Sorting config - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - if (sortingColumns.length > 0) { - data.sort(multiColumnSortFactory(sortingColumns)); - } - - const pageData = data.slice( - pagination.pageIndex * pagination.pageSize, - (pagination.pageIndex + 1) * pagination.pageSize - ); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = pageData.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - if ( - [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type - ) - ) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); - - if (status === PIVOT_PREVIEW_STATUS.ERROR) { - return ( -
- - - - -
- ); - } - - if (data.length === 0) { - let noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', - { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - } - ); - - const aggsArr = dictionaryToArray(aggs); - if (aggsArr.length === 0 || groupByArr.length === 0) { - noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', - { - defaultMessage: 'Please choose at least one group-by field and aggregation.', - } - ); - } - - return ( -
- - -

{noDataMessage}

-
-
- ); - } - - if (columnKeys.length === 0) { - return null; - } - - return ( -
- {showHeader && ( - <> - -
- {status === PIVOT_PREVIEW_STATUS.LOADING && } - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - - )} -
- - )} - {dataGridColumns.length > 0 && data.length > 0 && ( - - )} -
- ); - } -); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx deleted file mode 100644 index 8d09d06b1c7315..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import ReactDOM from 'react-dom'; - -import { SimpleQuery } from '../../common'; -import { - PIVOT_PREVIEW_STATUS, - usePivotPreviewData, - UsePivotPreviewDataReturnType, -} from './use_pivot_preview_data'; - -jest.mock('../../hooks/use_api'); - -type Callback = () => void; -interface TestHookProps { - callback: Callback; -} - -const TestHook: FC = ({ callback }) => { - callback(); - return null; -}; - -const testHook = (callback: Callback) => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(, container); -}; - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -let pivotPreviewObj: UsePivotPreviewDataReturnType; - -describe('usePivotPreviewData', () => { - test('indexPattern not defined', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - test('indexPattern set triggers loading', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - // ideally this should be LOADING instead of UNUSED but jest/enzyme/hooks doesn't - // trigger that state upate yet. - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest/enzyme's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts deleted file mode 100644 index 83fa7ba189ff0d..00000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { useEffect, useState } from 'react'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { useApi } from '../../hooks/use_api'; - -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; - -import { - getPreviewRequestBody, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfigDict, - PivotQuery, - PreviewData, - PreviewMappings, -} from '../../common'; - -export enum PIVOT_PREVIEW_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -export interface UsePivotPreviewDataReturnType { - errorMessage: string; - status: PIVOT_PREVIEW_STATUS; - previewData: PreviewData; - previewMappings: PreviewMappings; - previewRequest: PreviewRequestBody; -} - -export const usePivotPreviewData = ( - indexPatternTitle: IndexPattern['title'], - query: PivotQuery, - aggs: PivotAggsConfigDict, - groupBy: PivotGroupByConfigDict -): UsePivotPreviewDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(PIVOT_PREVIEW_STATUS.UNUSED); - const [previewData, setPreviewData] = useState([]); - const [previewMappings, setPreviewMappings] = useState({ properties: {} }); - const api = useApi(); - - const aggsArr = dictionaryToArray(aggs); - const groupByArr = dictionaryToArray(groupBy); - - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - - const getPreviewData = async () => { - if (aggsArr.length === 0 || groupByArr.length === 0) { - setPreviewData([]); - return; - } - - setErrorMessage(''); - setStatus(PIVOT_PREVIEW_STATUS.LOADING); - - try { - const resp = await api.getTransformsPreview(previewRequest); - setPreviewData(resp.preview); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(PIVOT_PREVIEW_STATUS.LOADED); - } catch (e) { - setErrorMessage(JSON.stringify(e, null, 2)); - setPreviewData([]); - setPreviewMappings({ properties: {} }); - setStatus(PIVOT_PREVIEW_STATUS.ERROR); - } - }; - - useEffect(() => { - getPreviewData(); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - indexPatternTitle, - JSON.stringify(aggsArr), - JSON.stringify(groupByArr), - JSON.stringify(query), - /* eslint-enable react-hooks/exhaustive-deps */ - ]); - - return { errorMessage, status, previewData, previewMappings, previewRequest }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 73cbafd5eede73..6fac8838cdbc8f 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,8 +35,6 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PivotPreview } from '../../../../components/pivot_preview'; - import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; import { useToastNotifications } from '../../../../app_dependencies'; @@ -45,7 +43,14 @@ import { dictionaryToArray, Dictionary } from '../../../../../../common/types/co import { DropDown } from '../aggregation_dropdown'; import { AggListForm } from '../aggregation_list'; import { GroupByListForm } from '../group_by_list'; -import { IndexPreview } from '../../../../components/index_preview'; +import { + getIndexDevConsoleStatement, + useIndexData, + usePivotData, + IndexPreview, +} from '../../../../components/index_preview'; +import { getPivotPreviewDevConsoleStatement } from '../../../../components/index_preview'; + import { SwitchModal } from './switch_modal'; import { @@ -296,7 +301,6 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, return; } } catch (e) { - console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console setErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -593,6 +597,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, /* eslint-enable react-hooks/exhaustive-deps */ ]); + const indexPreviewProps = useIndexData(indexPattern, pivotQuery); + const pivotPreviewProps = usePivotData(indexPattern.title, pivotQuery, aggList, groupByList); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` const maxIndexFields = 1024; const numIndexFields = indexPattern.fields.length; @@ -974,19 +981,34 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, - diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index f31514e67003b2..824ef6b7698c9f 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -17,8 +17,19 @@ import { EuiText, } from '@elastic/eui'; -import { getPivotQuery, isDefaultQuery, isMatchAllQuery } from '../../../../common'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { dictionaryToArray } from '../../../../../../common/types/common'; + +import { + getPivotQuery, + getPreviewRequestBody, + isDefaultQuery, + isMatchAllQuery, +} from '../../../../common'; +import { + getPivotPreviewDevConsoleStatement, + usePivotData, + IndexPreview, +} from '../../../../components/index_preview'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -35,8 +46,24 @@ export const StepDefineSummary: FC = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const pivotPreviewProps = usePivotData( + searchItems.indexPattern.title, + pivotQuery, + aggList, + groupByList + ); + return ( @@ -117,11 +144,19 @@ export const StepDefineSummary: FC = ({ - diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index eaaedc2eb77ce6..e33c5ae95cfc2e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,15 +6,14 @@ import React, { FC } from 'react'; -import { SearchItems } from '../../../../hooks/use_search_items'; - import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { usePivotData, IndexPreview } from '../../../../components/index_preview'; +import { SearchItems } from '../../../../hooks/use_search_items'; import { applyTransformConfigToDefineState, getDefaultStepDefineState, } from '../../../create_transform/components/step_define/'; -import { PivotPreview } from '../../../../components/pivot_preview'; interface Props { transformConfig: TransformPivotConfig; @@ -26,17 +25,15 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { transformConfig ); + const { aggList, groupByList, searchQuery } = previewConfig; + + const pivotQuery = getPivotQuery(searchQuery); + const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; - return ( - - ); + const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); + + return ; }; diff --git a/x-pack/run_functional_tests.sh b/x-pack/run_functional_tests.sh new file mode 100755 index 00000000000000..e94f283ea03942 --- /dev/null +++ b/x-pack/run_functional_tests.sh @@ -0,0 +1,3 @@ +export TEST_KIBANA_URL="http://elastic:mlqa_admin@localhost:5601" +export TEST_ES_URL="http://elastic:mlqa_admin@localhost:9200" +node ../scripts/functional_test_runner --include-tag walterra From 91c5b412a99fb39bfb48a5ad6caab4ff95940828 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 15 Apr 2020 09:49:15 +0200 Subject: [PATCH 03/34] [ML] Fix translations. --- x-pack/plugins/translations/translations/ja-JP.json | 7 ------- x-pack/plugins/translations/translations/zh-CN.json | 7 ------- 2 files changed, 14 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8a606e230dc36f..e0bf0a10a1d25f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15596,19 +15596,12 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.pivotPreview.PivotPreviewError": "ピボットプレビューの読み込み中にエラーが発生しました。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "ピボットプレビューを利用できません", "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列「{columnId}」は並べ替えに使用できません。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "ソースインデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "ソースインデックスクエリの結果がありません", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "ソースインデックスデータの読み込み中にエラーが発生しました。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "ソースインデックス {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "一斉", "xpack.transform.statsBar.continuousTransformsLabel": "連続", "xpack.transform.statsBar.failedTransformsLabel": "失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index faee95b8172b72..24725b05b081c0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15600,19 +15600,12 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", - "xpack.transform.pivotPreview.PivotPreviewError": "加载数据透视表预览时出错。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "数据透视表预览不可用", "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列“{columnId}”无法用于排序。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "源索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "源索引查询结果为空。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "加载源索引数据时出错。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "源索引 {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "批量", "xpack.transform.statsBar.continuousTransformsLabel": "连续", "xpack.transform.statsBar.failedTransformsLabel": "失败", From ff96a6f647cfedddfa0ff8dac2ca4cbc7ecd2876 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 15 Apr 2020 09:50:25 +0200 Subject: [PATCH 04/34] [ML] Remove progress bar, we now have the global one. Remove top border without header. --- .../app/components/index_preview/_index_preview.scss | 7 +++++++ .../app/components/index_preview/index_preview.tsx | 10 +++------- 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/_index_preview.scss diff --git a/x-pack/plugins/transform/public/app/components/index_preview/_index_preview.scss b/x-pack/plugins/transform/public/app/components/index_preview/_index_preview.scss new file mode 100644 index 00000000000000..cd7060018f7e9f --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/_index_preview.scss @@ -0,0 +1,7 @@ +/* override to hide the top border of EuiDataGrid on the transform list preview + to avoid a 2px line where EuiDataGrid and EuiTabs clash. */ +.mlDataGrid-noHeader { + .euiDataGrid__controls { + border-top: 0; + } +} diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx index 4f05c90df5284a..aff72578b18a34 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx +++ b/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './_index_preview.scss'; + import React, { useEffect, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -16,7 +18,6 @@ import { EuiDataGrid, EuiFlexGroup, EuiFlexItem, - EuiProgress, EuiSpacer, } from '@elastic/eui'; @@ -138,12 +139,6 @@ export const IndexPreview: FC = props => { )} -
- {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} -
{status === INDEX_STATUS.ERROR && (
= props => { )} Date: Wed, 15 Apr 2020 13:12:49 +0200 Subject: [PATCH 05/34] [ML] useDataGrid() --- .../app/components/index_preview/types.ts | 4 +- .../components/index_preview/use_data_grid.ts | 93 ++++++++ .../index_preview/use_index_data.ts | 192 ++++++----------- .../index_preview/use_pivot_data.ts | 200 +++++++----------- 4 files changed, 237 insertions(+), 252 deletions(-) create mode 100644 x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts diff --git a/x-pack/plugins/transform/public/app/components/index_preview/types.ts b/x-pack/plugins/transform/public/app/components/index_preview/types.ts index ab126f8f0c8aac..97e7980a113e43 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/types.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/types.ts @@ -6,7 +6,7 @@ import { Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; +import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; import { EsDocSource, EsFieldName } from '../../common'; @@ -38,7 +38,7 @@ export type RenderCellValue = ({ }) => any; export interface UseIndexDataReturnType { - columns: Array<{ id: string; schema: string | undefined }>; + columns: EuiDataGridColumn[]; errorMessage: string; invalidSortingColumnns: EsFieldName[]; noDataMessage: string; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts b/x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts new file mode 100644 index 00000000000000..eda7c515503d6a --- /dev/null +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts @@ -0,0 +1,93 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; + +import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { EsDocSource, EsFieldName, INIT_MAX_COLUMNS } from '../../common'; + +import { IndexPagination, OnChangeItemsPerPage, OnChangePage, OnSort, INDEX_STATUS } from './types'; + +const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; + +export const useDataGrid = (columns: EuiDataGridColumn[]) => { + const [noDataMessage, setNoDataMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, []); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [] + ); + + const resetPagination = () => setPagination(defaultPagination); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + const columnIds = columns.map(c => c.id); + const defaultVisibleColumns = columnIds.splice(0, INIT_MAX_COLUMNS); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + return { + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + resetPagination, + rowCount, + status, + setErrorMessage, + setNoDataMessage, + setPagination, + setRowCount, + setStatus, + setTableItems, + setVisibleColumns, + sortingColumns, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts index a0f3ceac3039d6..dad542fc15c901 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts @@ -5,12 +5,10 @@ */ import moment from 'moment-timezone'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { EuiDataGridSorting } from '@elastic/eui'; - import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { Dictionary } from '../../../../common/types/common'; @@ -19,26 +17,12 @@ import { getNestedProperty } from '../../../../common/utils/object_utils'; import { getErrorMessage } from '../../../shared_imports'; -import { - isDefaultQuery, - matchAllQuery, - EsDocSource, - EsFieldName, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../common'; +import { isDefaultQuery, matchAllQuery, PivotQuery } from '../../common'; import { SearchItems } from '../../hooks/use_search_items'; import { useApi } from '../../hooks/use_api'; -import { - IndexPagination, - OnChangeItemsPerPage, - OnChangePage, - OnSort, - RenderCellValue, - UseIndexDataReturnType, - INDEX_STATUS, -} from './types'; +import { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; +import { useDataGrid } from './use_data_grid'; type EsSorting = Dictionary<{ order: 'asc' | 'desc'; @@ -56,66 +40,12 @@ interface SearchResponse7 extends SearchResponse { type IndexSearchResponse = SearchResponse7; -const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; - export const useIndexData = ( indexPattern: SearchItems['indexPattern'], query: PivotQuery ): UseIndexDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); const api = useApi(); - useEffect(() => { - setPagination(defaultPagination); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(query)]); - - const getIndexData = async function() { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - 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, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }; - - try { - const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map(d => d._source); - - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - setErrorMessage(getErrorMessage(e)); - setStatus(INDEX_STATUS.ERROR); - } - }; - - useEffect(() => { - getIndexData(); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - const allFields = indexPattern.fields.map(f => f.name); const indexPatternFields: string[] = allFields.filter(f => { if (indexPattern.metaFields.includes(f)) { @@ -160,50 +90,64 @@ export const useIndexData = ( }), ]; - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); + const dataGrid = useDataGrid(columns); - const defaultVisibleColumns = indexPatternFields.splice(0, INIT_MAX_COLUMNS); + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; useEffect(() => { - setVisibleColumns(defaultVisibleColumns); + resetPagination(); + // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultVisibleColumns.join()]); - - const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); - - const onSort: OnSort = useCallback( - sc => { - // Check if an unsupported column type for sorting was selected. - const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { - const columnType = columns.find(dgc => dgc.id === current.id); - if (columnType?.schema === 'json') { - arr.push(current.id); - } - return arr; - }, []); - setInvalidSortingColumnns(updatedInvalidSortingColumnns); - if (updatedInvalidSortingColumnns.length === 0) { - setSortingColumns(sc); - } - }, - [columns] - ); - - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage: OnChangePage = useCallback( - pageIndex => setPagination(p => ({ ...p, pageIndex })), - [setPagination] - ); + }, [JSON.stringify(query)]); + + const getIndexData = async function() { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + 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, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map(d => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); const renderCellValue: RenderCellValue = useMemo(() => { return ({ @@ -244,20 +188,20 @@ export const useIndexData = ( return { columns, - errorMessage, - invalidSortingColumnns, - onChangeItemsPerPage, - onChangePage, - onSort, + errorMessage: dataGrid.errorMessage, + invalidSortingColumnns: dataGrid.invalidSortingColumnns, + onChangeItemsPerPage: dataGrid.onChangeItemsPerPage, + onChangePage: dataGrid.onChangePage, + onSort: dataGrid.onSort, noDataMessage: '', pagination, - setPagination, - setVisibleColumns, + setPagination: dataGrid.setPagination, + setVisibleColumns: dataGrid.setVisibleColumns, renderCellValue, - rowCount, + rowCount: dataGrid.rowCount, sortingColumns, - status, + status: dataGrid.status, tableItems, - visibleColumns, + visibleColumns: dataGrid.visibleColumns, }; }; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts b/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts index 552dfdcfaa8023..c95ed3f0167f30 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts @@ -5,9 +5,7 @@ */ import moment from 'moment-timezone'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -import { EuiDataGridSorting } from '@elastic/eui'; +import { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -21,30 +19,18 @@ import { getErrorMessage } from '../../../shared_imports'; import { getPreviewRequestBody, - EsDocSource, - EsFieldName, PivotAggsConfigDict, PivotGroupByConfigDict, PivotGroupByConfig, PivotQuery, PreviewMappings, - INIT_MAX_COLUMNS, } from '../../common'; import { SearchItems } from '../../hooks/use_search_items'; import { useApi } from '../../hooks/use_api'; import { multiColumnSortFactory } from './common'; -import { - IndexPagination, - OnChangeItemsPerPage, - OnChangePage, - OnSort, - RenderCellValue, - UseIndexDataReturnType, - INDEX_STATUS, -} from './types'; - -const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; +import { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; +import { useDataGrid } from './use_data_grid'; function sortColumns(groupByArr: PivotGroupByConfig[]) { return (a: string, b: string) => { @@ -68,19 +54,71 @@ export const usePivotData = ( aggs: PivotAggsConfigDict, groupBy: PivotGroupByConfigDict ): UseIndexDataReturnType => { - const [noDataMessage, setNoDataMessage] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); const [previewMappings, setPreviewMappings] = useState({ properties: {} }); const api = useApi(); const aggsArr = dictionaryToArray(aggs); const groupByArr = dictionaryToArray(groupBy); + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // EuiDataGrid State + const columns = 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: + case ES_FIELD_TYPES.DATE_NANOS: + 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 dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setNoDataMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); const getPreviewData = async () => { @@ -124,7 +162,7 @@ export const usePivotData = ( }; useEffect(() => { - setPagination(defaultPagination); + resetPagination(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(query)]); @@ -141,100 +179,10 @@ export const usePivotData = ( /* eslint-enable react-hooks/exhaustive-deps */ ]); - // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - key => previewMappings.properties[key].type !== 'object' - ); - columnKeys.sort(sortColumns(groupByArr)); - - // EuiDataGrid State - const columns = 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: - case ES_FIELD_TYPES.DATE_NANOS: - 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 }; - }); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - const defaultVisibleColumns = columnKeys.splice(0, INIT_MAX_COLUMNS); - - useEffect(() => { - setVisibleColumns(defaultVisibleColumns); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultVisibleColumns.join()]); - - const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); - - const onSort: OnSort = useCallback( - sc => { - // Check if an unsupported column type for sorting was selected. - const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { - const columnType = columns.find(dgc => dgc.id === current.id); - if (columnType?.schema === 'json') { - arr.push(current.id); - } - return arr; - }, []); - setInvalidSortingColumnns(updatedInvalidSortingColumnns); - if (updatedInvalidSortingColumnns.length === 0) { - setSortingColumns(sc); - } - }, - [columns] - ); - if (sortingColumns.length > 0) { tableItems.sort(multiColumnSortFactory(sortingColumns)); } - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage: OnChangePage = useCallback( - pageIndex => setPagination(p => ({ ...p, pageIndex })), - [setPagination] - ); - const pageData = tableItems.slice( pagination.pageIndex * pagination.pageSize, (pagination.pageIndex + 1) * pagination.pageSize @@ -282,20 +230,20 @@ export const usePivotData = ( return { columns, - errorMessage, - invalidSortingColumnns, - noDataMessage, - onChangeItemsPerPage, - onChangePage, - onSort, + errorMessage: dataGrid.errorMessage, + invalidSortingColumnns: dataGrid.invalidSortingColumnns, + onChangeItemsPerPage: dataGrid.onChangeItemsPerPage, + onChangePage: dataGrid.onChangePage, + onSort: dataGrid.onSort, + noDataMessage: dataGrid.noDataMessage, pagination, - setPagination, - setVisibleColumns, + setPagination: dataGrid.setPagination, + setVisibleColumns: dataGrid.setVisibleColumns, renderCellValue, - rowCount, + rowCount: dataGrid.rowCount, sortingColumns, - status, + status: dataGrid.status, tableItems, - visibleColumns, + visibleColumns: dataGrid.visibleColumns, }; }; From 0cc06639d4189c0fb88b1c1f2c342afbbe4c351b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 15 Apr 2020 16:55:14 +0200 Subject: [PATCH 06/34] [ML] Move IndexPreview to ML plugin. --- .../index_preview/_index_preview.scss | 0 .../components/index_preview/common.test.ts | 59 ++++++++++++++++++ .../components/index_preview/common.ts | 28 +++++---- .../components/index_preview/index.ts | 7 +-- .../index_preview/index_preview.tsx | 31 +++++----- .../index_preview/index_preview_title.tsx | 0 .../components/index_preview/types.ts | 4 +- .../components/index_preview/use_data_grid.ts | 13 +++- .../public/__mocks__/shared_imports.ts | 8 +++ .../data_grid.test.ts} | 60 +------------------ .../transform/public/app/common/data_grid.ts | 17 ++++++ .../transform/public/app/common/index.ts | 8 ++- .../index_preview/use_index_data.test.tsx | 49 --------------- .../use_index_data.test.tsx} | 50 ++++++++++++++-- .../index_preview => hooks}/use_index_data.ts | 24 ++++---- .../index_preview => hooks}/use_pivot_data.ts | 26 ++++---- .../step_define/step_define_form.tsx | 31 +++++----- .../step_define/step_define_summary.tsx | 12 ++-- .../expanded_row_preview_pane.tsx | 14 ++++- .../transform/public/shared_imports.ts | 9 +++ .../translations/translations/ja-JP.json | 40 ++++++++++++- .../translations/translations/zh-CN.json | 40 ++++++++++++- 22 files changed, 336 insertions(+), 194 deletions(-) rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/_index_preview.scss (100%) create mode 100644 x-pack/plugins/ml/public/application/components/index_preview/common.test.ts rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/common.ts (74%) rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/index.ts (55%) rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/index_preview.tsx (85%) rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/index_preview_title.tsx (100%) rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/types.ts (89%) rename x-pack/plugins/{transform/public/app => ml/public/application}/components/index_preview/use_data_grid.ts (93%) rename x-pack/plugins/transform/public/app/{components/index_preview/common.test.ts => common/data_grid.test.ts} (58%) delete mode 100644 x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx rename x-pack/plugins/transform/public/app/{components/index_preview/index_preview.test.tsx => hooks/use_index_data.test.tsx} (50%) rename x-pack/plugins/transform/public/app/{components/index_preview => hooks}/use_index_data.ts (88%) rename x-pack/plugins/transform/public/app/{components/index_preview => hooks}/use_pivot_data.ts (91%) diff --git a/x-pack/plugins/transform/public/app/components/index_preview/_index_preview.scss b/x-pack/plugins/ml/public/application/components/index_preview/_index_preview.scss similarity index 100% rename from x-pack/plugins/transform/public/app/components/index_preview/_index_preview.scss rename to x-pack/plugins/ml/public/application/components/index_preview/_index_preview.scss diff --git a/x-pack/plugins/ml/public/application/components/index_preview/common.test.ts b/x-pack/plugins/ml/public/application/components/index_preview/common.test.ts new file mode 100644 index 00000000000000..4bb670ad02dfc2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/index_preview/common.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { multiColumnSortFactory } 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 }, + ]); + }); +}); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/common.ts b/x-pack/plugins/ml/public/application/components/index_preview/common.ts similarity index 74% rename from x-pack/plugins/transform/public/app/components/index_preview/common.ts rename to x-pack/plugins/ml/public/application/components/index_preview/common.ts index a560c1d2461f83..04788335a67f69 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/common.ts +++ b/x-pack/plugins/ml/public/application/components/index_preview/common.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridSorting } from '@elastic/eui'; +import { EuiDataGridSorting, EuiDataGridStyle } from '@elastic/eui'; -import { getNestedProperty } from '../../../../common/utils/object_utils'; +import { getNestedProperty } from '../../util/object_utils'; -import { PivotQuery, PreviewRequestBody } from '../../common'; +export const INIT_MAX_COLUMNS = 20; /** * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. @@ -55,16 +55,18 @@ export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['colum return sortFn; }; -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { - return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'none', + header: 'shade', }; -export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { - return `GET ${indexPatternTitle}/_search\n${JSON.stringify( - { - query, - }, - null, - 2 - )}\n`; +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, }; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index.ts b/x-pack/plugins/ml/public/application/components/index_preview/index.ts similarity index 55% rename from x-pack/plugins/transform/public/app/components/index_preview/index.ts rename to x-pack/plugins/ml/public/application/components/index_preview/index.ts index 401e8e202b83e5..a03b79028a8c8e 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index.ts +++ b/x-pack/plugins/ml/public/application/components/index_preview/index.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './common'; -export { useIndexData } from './use_index_data'; -export { usePivotData } from './use_pivot_data'; +export { multiColumnSortFactory } from './common'; +export { useDataGrid } from './use_data_grid'; export { IndexPreview } from './index_preview'; -export { INDEX_STATUS } from './types'; +export { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx b/x-pack/plugins/ml/public/application/components/index_preview/index_preview.tsx similarity index 85% rename from x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx rename to x-pack/plugins/ml/public/application/components/index_preview/index_preview.tsx index aff72578b18a34..734795232e1dc9 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.tsx +++ b/x-pack/plugins/ml/public/application/components/index_preview/index_preview.tsx @@ -21,21 +21,21 @@ import { EuiSpacer, } from '@elastic/eui'; -import { useToastNotifications } from '../../app_dependencies'; -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../common'; +import { CoreSetup } from 'src/core/public'; +import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; import { IndexPreviewTitle } from './index_preview_title'; import { INDEX_STATUS, UseIndexDataReturnType } from './types'; -interface PropsWithHeader extends UseIndexDataReturnType { - copyToClipboard: string; - copyToClipboardDescription: string; +interface PropsWithoutHeader extends UseIndexDataReturnType { dataTestSubj: string; - title: string; + toastNotifications: CoreSetup['notifications']['toasts']; } -interface PropsWithoutHeader extends UseIndexDataReturnType { - dataTestSubj: string; +interface PropsWithHeader extends PropsWithoutHeader { + copyToClipboard: string; + copyToClipboardDescription: string; + title: string; } function isWithHeader(arg: any): arg is PropsWithHeader { @@ -61,16 +61,15 @@ export const IndexPreview: FC = props => { sortingColumns, status, tableItems: data, + toastNotifications, visibleColumns, } = props; - const toastNotifications = useToastNotifications(); - useEffect(() => { if (invalidSortingColumnns.length > 0) { invalidSortingColumnns.forEach(columnId => { toastNotifications.addDanger( - i18n.translate('xpack.transform.indexPreview.invalidSortingColumnError', { + i18n.translate('xpack.ml.indexPreview.invalidSortingColumnError', { defaultMessage: `The column '{columnId}' cannot be used for sorting.`, values: { columnId }, }) @@ -84,13 +83,13 @@ export const IndexPreview: FC = props => {
{isWithHeader(props) && }

- {i18n.translate('xpack.transform.indexPreview.IndexNoDataCalloutBody', { + {i18n.translate('xpack.ml.indexPreview.IndexNoDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', })} @@ -105,8 +104,8 @@ export const IndexPreview: FC = props => {

{isWithHeader(props) && } @@ -142,7 +141,7 @@ export const IndexPreview: FC = props => { {status === INDEX_STATUS.ERROR && (
; +export type EsFieldName = string; export enum INDEX_STATUS { UNUSED, diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/index_preview/use_data_grid.ts similarity index 93% rename from x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts rename to x-pack/plugins/ml/public/application/components/index_preview/use_data_grid.ts index eda7c515503d6a..c218e3b492b1bd 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_data_grid.ts +++ b/x-pack/plugins/ml/public/application/components/index_preview/use_data_grid.ts @@ -8,9 +8,16 @@ import { useCallback, useEffect, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; -import { EsDocSource, EsFieldName, INIT_MAX_COLUMNS } from '../../common'; - -import { IndexPagination, OnChangeItemsPerPage, OnChangePage, OnSort, INDEX_STATUS } from './types'; +import { INIT_MAX_COLUMNS } from './common'; +import { + EsDocSource, + EsFieldName, + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + INDEX_STATUS, +} from './types'; const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 68925401a0bd2b..edbdb7bb50d15e 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -14,3 +14,11 @@ export const useRequest = jest.fn(() => ({ export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; export const SORT_DIRECTION = { ASC: 'asc' }; export { getErrorMessage } from '../../../ml/common/util/errors'; +export { + multiColumnSortFactory, + useDataGrid, + IndexPreview, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../../ml/public/application/components/index_preview'; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts similarity index 58% rename from x-pack/plugins/transform/public/app/components/index_preview/common.test.ts rename to x-pack/plugins/transform/public/app/common/data_grid.test.ts index f82c5ba99890d4..0e5ecb5d3b214c 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/common.test.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridSorting } from '@elastic/eui'; - import { getPreviewRequestBody, PivotAggsConfig, @@ -13,63 +11,11 @@ import { PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, SimpleQuery, -} from '../../common'; - -import { - getIndexDevConsoleStatement, - 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); +} from '../common'; - 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 }, - ]); - }); +import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; +describe('Transform: Data Grid', () => { test('getPivotPreviewDevConsoleStatement()', () => { const query: SimpleQuery = { query_string: { diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 0e9cceefb3156f..f2448c6b785ae6 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -6,6 +6,9 @@ import { EuiDataGridStyle } from '@elastic/eui'; +import { PivotQuery } from './request'; +import { PreviewRequestBody } from './transform'; + export const INIT_MAX_COLUMNS = 20; export const euiDataGridStyle: EuiDataGridStyle = { @@ -23,3 +26,17 @@ export const euiDataGridToolbarSettings = { showSortSelector: true, showFullScreenSelector: false, }; + +export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { + return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; +}; + +export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { + return `GET ${indexPatternTitle}/_search\n${JSON.stringify( + { + query, + }, + null, + 2 + )}\n`; +}; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index daeddaa8018285..5b223cc862255d 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -5,7 +5,13 @@ */ export { AggName, isAggName } from './aggregations'; -export { euiDataGridStyle, euiDataGridToolbarSettings, INIT_MAX_COLUMNS } from './data_grid'; +export { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, + euiDataGridStyle, + euiDataGridToolbarSettings, + INIT_MAX_COLUMNS, +} from './data_grid'; export { getDefaultSelectableFields, getFlattenedFields, diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx deleted file mode 100644 index 89d3cc69317bb8..00000000000000 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { renderHook } from '@testing-library/react-hooks'; -import '@testing-library/jest-dom/extend-expect'; - -import { SimpleQuery } from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; -import { useIndexData } from './use_index_data'; -import { INDEX_STATUS, UseIndexDataReturnType } from './types'; - -jest.mock('../../../shared_imports'); -jest.mock('../../hooks/use_api'); - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -describe('useSourceIndexData', () => { - test('indexPattern set triggers loading', async done => { - const { result, waitForNextUpdate } = renderHook(() => - useIndexData( - ({ - id: 'the-id', - title: 'the-title', - fields: [], - } as unknown) as SearchItems['indexPattern'], - query - ) - ); - const IndexObj: UseIndexDataReturnType = result.current; - - await waitForNextUpdate(); - - expect(IndexObj.errorMessage).toBe(''); - expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); - expect(IndexObj.tableItems).toEqual([]); - done(); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx similarity index 50% rename from x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx rename to x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index e70f5bf08299d0..b9f10d7486cf05 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/index_preview.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -5,18 +5,55 @@ */ import React from 'react'; + import { render, wait } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; import '@testing-library/jest-dom/extend-expect'; -import { SearchItems } from '../../hooks/use_search_items'; +import { CoreSetup } from 'src/core/public'; + +import { IndexPreview, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; + +import { SimpleQuery } from '../common'; +import { SearchItems } from './use_search_items'; import { useIndexData } from './use_index_data'; -import { IndexPreview } from './index_preview'; -jest.mock('../../../shared_imports'); -jest.mock('../../app_dependencies'); +jest.mock('../../shared_imports'); +jest.mock('../app_dependencies'); +jest.mock('./use_api'); + +const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, +}; + +describe('Transform: useIndexData()', () => { + test('indexPattern set triggers loading', async done => { + const { result, waitForNextUpdate } = renderHook(() => + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ) + ); + const IndexObj: UseIndexDataReturnType = result.current; + + await waitForNextUpdate(); -describe('Transform: ', () => { + expect(IndexObj.errorMessage).toBe(''); + expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.tableItems).toEqual([]); + done(); + }); +}); + +describe('Transform: with useIndexData()', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async done => { // Arrange @@ -28,10 +65,11 @@ describe('Transform: ', () => { const Wrapper = () => { const props = { ...useIndexData(indexPattern, { match_all: {} }), - title: 'the-index-preview-title', copyToClipboard: 'the-copy-to-clipboard-code', copyToClipboardDescription: 'the-copy-to-clipboard-description', dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], }; return ; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts similarity index 88% rename from x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts rename to x-pack/plugins/transform/public/app/hooks/use_index_data.ts index dad542fc15c901..5706802b8de4e3 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -9,20 +9,24 @@ import { useEffect, useMemo } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; +import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { Dictionary } from '../../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../common/utils/object_utils'; +import { Dictionary } from '../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../common/utils/object_utils'; -import { getErrorMessage } from '../../../shared_imports'; +import { + getErrorMessage, + useDataGrid, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; -import { isDefaultQuery, matchAllQuery, PivotQuery } from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; -import { useApi } from '../../hooks/use_api'; +import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; -import { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; -import { useDataGrid } from './use_data_grid'; +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; type EsSorting = Dictionary<{ order: 'asc' | 'desc'; diff --git a/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts similarity index 91% rename from x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts rename to x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index c95ed3f0167f30..b26adc6a3f1d4f 100644 --- a/x-pack/plugins/transform/public/app/components/index_preview/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -9,13 +9,20 @@ import { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; +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 { dictionaryToArray } from '../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../common/utils/object_utils'; -import { getErrorMessage } from '../../../shared_imports'; +import { + getErrorMessage, + multiColumnSortFactory, + useDataGrid, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; import { getPreviewRequestBody, @@ -24,13 +31,10 @@ import { PivotGroupByConfig, PivotQuery, PreviewMappings, -} from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; -import { useApi } from '../../hooks/use_api'; +} from '../common'; -import { multiColumnSortFactory } from './common'; -import { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; -import { useDataGrid } from './use_data_grid'; +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; function sortColumns(groupByArr: PivotGroupByConfig[]) { return (a: string, b: string) => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 6fac8838cdbc8f..b85339139ff8cc 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,24 +35,19 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; +import { IndexPreview } from '../../../../../shared_imports'; + +import { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, +} from '../../../../common/data_grid'; + import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; +import { useIndexData } from '../../../../hooks/use_index_data'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { useToastNotifications } from '../../../../app_dependencies'; -import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; -import { DropDown } from '../aggregation_dropdown'; -import { AggListForm } from '../aggregation_list'; -import { GroupByListForm } from '../group_by_list'; -import { - getIndexDevConsoleStatement, - useIndexData, - usePivotData, - IndexPreview, -} from '../../../../components/index_preview'; -import { getPivotPreviewDevConsoleStatement } from '../../../../components/index_preview'; - -import { SwitchModal } from './switch_modal'; - import { getPivotQuery, getPreviewRequestBody, @@ -66,11 +61,17 @@ import { PivotGroupByConfig, PivotGroupByConfigDict, PivotSupportedGroupByAggs, + TransformPivotConfig, PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; +import { DropDown } from '../aggregation_dropdown'; +import { AggListForm } from '../aggregation_list'; +import { GroupByListForm } from '../group_by_list'; + import { getPivotDropdownOptions } from './common'; +import { SwitchModal } from './switch_modal'; export interface StepDefineExposedState { aggList: PivotAggsConfigDict; @@ -994,6 +995,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, defaultMessage: 'Index {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title }, })} + toastNotifications={toastNotifications} /> = React.memo(({ overrides = {}, onChange, title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { defaultMessage: 'Transform pivot preview', })} + toastNotifications={toastNotifications} /> diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 824ef6b7698c9f..9435fcc039798e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -19,17 +19,17 @@ import { import { dictionaryToArray } from '../../../../../../common/types/common'; +import { IndexPreview } from '../../../../../shared_imports'; + +import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, + getPivotPreviewDevConsoleStatement, getPreviewRequestBody, isDefaultQuery, isMatchAllQuery, } from '../../../../common'; -import { - getPivotPreviewDevConsoleStatement, - usePivotData, - IndexPreview, -} from '../../../../components/index_preview'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -46,6 +46,7 @@ export const StepDefineSummary: FC = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const toastNotifications = useToastNotifications(); const pivotAggsArr = dictionaryToArray(aggList); const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); @@ -157,6 +158,7 @@ export const StepDefineSummary: FC = ({ title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { defaultMessage: 'Transform pivot preview', })} + toastNotifications={toastNotifications} /> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index e33c5ae95cfc2e..7a0ad91c5111f4 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,8 +6,11 @@ import React, { FC } from 'react'; +import { IndexPreview } from '../../../../../shared_imports'; + +import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, TransformPivotConfig } from '../../../../common'; -import { usePivotData, IndexPreview } from '../../../../components/index_preview'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { @@ -20,6 +23,7 @@ interface Props { } export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { + const toastNotifications = useToastNotifications(); const previewConfig = applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), transformConfig @@ -35,5 +39,11 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 494b6db6aafe0a..0b69938659ad1d 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -17,3 +17,12 @@ export { } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { getErrorMessage } from '../../ml/common/util/errors'; + +export { + multiColumnSortFactory, + useDataGrid, + IndexPreview, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../ml/public/application/components/index_preview'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e0bf0a10a1d25f..eeb6f9ad437efc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2295,6 +2295,45 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", + "kbn.topNavMenu.openInspectorButtonLabel": "検査", + "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", + "kbn.topNavMenu.shareVisualizationButtonLabel": "共有", + "kbn.visualize.badge.readOnly.text": "読み込み専用", + "kbn.visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", + "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", + "kbn.visualize.editor.createBreadcrumb": "作成", + "kbn.visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", + "kbn.visualize.helpMenu.appName": "可視化", + "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", + "kbn.visualize.listing.betaTitle": "ベータ", + "kbn.visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", + "kbn.visualize.listing.breadcrumb": "可視化", + "kbn.visualize.listing.createNew.createButtonLabel": "新規ビジュアライゼーションを追加", + "kbn.visualize.listing.createNew.description": "データに基づき異なるビジュアライゼーションを作成できます。", + "kbn.visualize.listing.createNew.title": "最初のビジュアライゼーションの作成", + "kbn.visualize.listing.experimentalTitle": "実験的", + "kbn.visualize.listing.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", + "kbn.visualize.listing.noItemsMessage": "ビジュアライゼーションがないようです。", + "kbn.visualize.listing.table.entityName": "ビジュアライゼーション", + "kbn.visualize.listing.table.entityNamePlural": "ビジュアライゼーション", + "kbn.visualize.listing.table.listTitle": "ビジュアライゼーション", + "kbn.visualize.listing.table.titleColumnName": "タイトル", + "kbn.visualize.listing.table.typeColumnName": "タイプ", + "kbn.visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション", + "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加", + "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", + "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", + "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました", + "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "「{visTitle}」が保存されました", + "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", + "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", + "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", + "kbn.visualize.visualizationTypeInvalidNotificationMessage": "無効なビジュアライゼーションタイプ", + "kbn.visualize.visualizeDescription": "ビジュアライゼーションを作成して Elasticsearch インデックスに保存されたデータを集約します。", + "kbn.visualize.visualizeListingBreadcrumbsTitle": "可視化", + "kbn.visualize.visualizeListingDeleteErrorTitle": "ビジュアライゼーションの削除中にエラーが発生", + "kbn.visualize.wizard.step1Breadcrumb": "作成", + "kbn.visualize.wizard.step2Breadcrumb": "作成", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", @@ -15598,7 +15637,6 @@ "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "ピボットプレビューを利用できません", "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 24725b05b081c0..551254d1b02531 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2296,6 +2296,45 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", + "kbn.topNavMenu.openInspectorButtonLabel": "检查", + "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", + "kbn.topNavMenu.shareVisualizationButtonLabel": "共享", + "kbn.visualize.badge.readOnly.text": "只读", + "kbn.visualize.badge.readOnly.tooltip": "无法保存可视化", + "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", + "kbn.visualize.editor.createBreadcrumb": "创建", + "kbn.visualize.experimentalVisInfoText": "此可视化标记为“实验”。", + "kbn.visualize.helpMenu.appName": "Visualize", + "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", + "kbn.visualize.listing.betaTitle": "公测版", + "kbn.visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", + "kbn.visualize.listing.breadcrumb": "可视化", + "kbn.visualize.listing.createNew.createButtonLabel": "新建可视化", + "kbn.visualize.listing.createNew.description": "可以根据您的数据创建不同的可视化。", + "kbn.visualize.listing.createNew.title": "创建首个可视化", + "kbn.visualize.listing.experimentalTitle": "实验性", + "kbn.visualize.listing.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", + "kbn.visualize.listing.noItemsMessage": "看起来您还没有任何可视化。", + "kbn.visualize.listing.table.entityName": "可视化", + "kbn.visualize.listing.table.entityNamePlural": "可视化", + "kbn.visualize.listing.table.listTitle": "可视化", + "kbn.visualize.listing.table.titleColumnName": "标题", + "kbn.visualize.listing.table.typeColumnName": "类型", + "kbn.visualize.pageHeading": "{chartName} {chartType}可视化", + "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板", + "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", + "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", + "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错", + "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "已保存“{visTitle}”", + "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "保存可视化", + "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "应用或放弃所做更改,然后保存", + "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "共享可视化", + "kbn.visualize.visualizationTypeInvalidNotificationMessage": "无效的可视化类型", + "kbn.visualize.visualizeDescription": "创建可视化并聚合存储在 Elasticsearch 索引中的数据。", + "kbn.visualize.visualizeListingBreadcrumbsTitle": "可视化", + "kbn.visualize.visualizeListingDeleteErrorTitle": "删除可视化时出错", + "kbn.visualize.wizard.step1Breadcrumb": "创建", + "kbn.visualize.wizard.step2Breadcrumb": "创建", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -15602,7 +15641,6 @@ "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "数据透视表预览不可用", "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", From a5d214c9fcb080d002eb5dbb6bfac3fe44d1923f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 15 Apr 2020 17:36:41 +0200 Subject: [PATCH 07/34] [ML] Rename IndexPreview to Data Grid. --- .../_data_grid.scss} | 0 .../common.test.ts | 0 .../{index_preview => data_grid}/common.ts | 0 .../data_grid.tsx} | 22 +++++++++---------- .../data_grid_title.tsx} | 8 +++---- .../{index_preview => data_grid}/index.ts | 2 +- .../{index_preview => data_grid}/types.ts | 0 .../use_data_grid.ts | 0 .../public/__mocks__/shared_imports.ts | 4 ++-- .../public/app/hooks/use_index_data.test.tsx | 6 ++--- .../step_define/step_define_form.tsx | 6 ++--- .../step_define/step_define_summary.tsx | 4 ++-- .../expanded_row_preview_pane.tsx | 4 ++-- .../transform/public/shared_imports.ts | 4 ++-- 14 files changed, 30 insertions(+), 30 deletions(-) rename x-pack/plugins/ml/public/application/components/{index_preview/_index_preview.scss => data_grid/_data_grid.scss} (100%) rename x-pack/plugins/ml/public/application/components/{index_preview => data_grid}/common.test.ts (100%) rename x-pack/plugins/ml/public/application/components/{index_preview => data_grid}/common.ts (100%) rename x-pack/plugins/ml/public/application/components/{index_preview/index_preview.tsx => data_grid/data_grid.tsx} (85%) rename x-pack/plugins/ml/public/application/components/{index_preview/index_preview_title.tsx => data_grid/data_grid_title.tsx} (65%) rename x-pack/plugins/ml/public/application/components/{index_preview => data_grid}/index.ts (89%) rename x-pack/plugins/ml/public/application/components/{index_preview => data_grid}/types.ts (100%) rename x-pack/plugins/ml/public/application/components/{index_preview => data_grid}/use_data_grid.ts (100%) diff --git a/x-pack/plugins/ml/public/application/components/index_preview/_index_preview.scss b/x-pack/plugins/ml/public/application/components/data_grid/_data_grid.scss similarity index 100% rename from x-pack/plugins/ml/public/application/components/index_preview/_index_preview.scss rename to x-pack/plugins/ml/public/application/components/data_grid/_data_grid.scss diff --git a/x-pack/plugins/ml/public/application/components/index_preview/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/index_preview/common.test.ts rename to x-pack/plugins/ml/public/application/components/data_grid/common.test.ts diff --git a/x-pack/plugins/ml/public/application/components/index_preview/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/index_preview/common.ts rename to x-pack/plugins/ml/public/application/components/data_grid/common.ts diff --git a/x-pack/plugins/ml/public/application/components/index_preview/index_preview.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx similarity index 85% rename from x-pack/plugins/ml/public/application/components/index_preview/index_preview.tsx rename to x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 734795232e1dc9..41a39db9057601 100644 --- a/x-pack/plugins/ml/public/application/components/index_preview/index_preview.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import './_index_preview.scss'; +import './_data_grid.scss'; import React, { useEffect, FC } from 'react'; @@ -24,7 +24,7 @@ import { import { CoreSetup } from 'src/core/public'; import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; -import { IndexPreviewTitle } from './index_preview_title'; +import { DataGridTitle } from './data_grid_title'; import { INDEX_STATUS, UseIndexDataReturnType } from './types'; interface PropsWithoutHeader extends UseIndexDataReturnType { @@ -44,7 +44,7 @@ function isWithHeader(arg: any): arg is PropsWithHeader { type Props = PropsWithHeader | PropsWithoutHeader; -export const IndexPreview: FC = props => { +export const DataGrid: FC = props => { const { columns, dataTestSubj, @@ -69,7 +69,7 @@ export const IndexPreview: FC = props => { if (invalidSortingColumnns.length > 0) { invalidSortingColumnns.forEach(columnId => { toastNotifications.addDanger( - i18n.translate('xpack.ml.indexPreview.invalidSortingColumnError', { + i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { defaultMessage: `The column '{columnId}' cannot be used for sorting.`, values: { columnId }, }) @@ -81,15 +81,15 @@ export const IndexPreview: FC = props => { if (status === INDEX_STATUS.LOADED && data.length === 0) { return (
- {isWithHeader(props) && } + {isWithHeader(props) && }

- {i18n.translate('xpack.ml.indexPreview.IndexNoDataCalloutBody', { + {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', })} @@ -102,9 +102,9 @@ export const IndexPreview: FC = props => { if (noDataMessage !== '') { return (

- {isWithHeader(props) && } + {isWithHeader(props) && } = props => { {isWithHeader(props) && ( - + = props => { {status === INDEX_STATUS.ERROR && (
= ({ indexPreviewTitle }) => ( +export const DataGridTitle: React.FC = ({ dataGridTitle }) => ( - {indexPreviewTitle} + {dataGridTitle} ); diff --git a/x-pack/plugins/ml/public/application/components/index_preview/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts similarity index 89% rename from x-pack/plugins/ml/public/application/components/index_preview/index.ts rename to x-pack/plugins/ml/public/application/components/data_grid/index.ts index a03b79028a8c8e..a00d9c032a7ed1 100644 --- a/x-pack/plugins/ml/public/application/components/index_preview/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -6,5 +6,5 @@ export { multiColumnSortFactory } from './common'; export { useDataGrid } from './use_data_grid'; -export { IndexPreview } from './index_preview'; +export { DataGrid } from './data_grid'; export { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; diff --git a/x-pack/plugins/ml/public/application/components/index_preview/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/index_preview/types.ts rename to x-pack/plugins/ml/public/application/components/data_grid/types.ts diff --git a/x-pack/plugins/ml/public/application/components/index_preview/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/index_preview/use_data_grid.ts rename to x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index edbdb7bb50d15e..1dffacff2c84d0 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -17,8 +17,8 @@ export { getErrorMessage } from '../../../ml/common/util/errors'; export { multiColumnSortFactory, useDataGrid, - IndexPreview, + DataGrid, RenderCellValue, UseIndexDataReturnType, INDEX_STATUS, -} from '../../../ml/public/application/components/index_preview'; +} from '../../../ml/public/application/components/data_grid'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index b9f10d7486cf05..4ca536e3c115d0 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -12,7 +12,7 @@ import '@testing-library/jest-dom/extend-expect'; import { CoreSetup } from 'src/core/public'; -import { IndexPreview, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; +import { DataGrid, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; import { SimpleQuery } from '../common'; @@ -53,7 +53,7 @@ describe('Transform: useIndexData()', () => { }); }); -describe('Transform: with useIndexData()', () => { +describe('Transform: with useIndexData()', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async done => { // Arrange @@ -72,7 +72,7 @@ describe('Transform: with useIndexData()', () => { toastNotifications: {} as CoreSetup['notifications']['toasts'], }; - return ; + return ; }; const { getByText } = render(); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index b85339139ff8cc..0e6e2c1a38d0ef 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,7 +35,7 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { IndexPreview } from '../../../../../shared_imports'; +import { DataGrid } from '../../../../../shared_imports'; import { getIndexDevConsoleStatement, @@ -981,7 +981,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, - = React.memo(({ overrides = {}, onChange, toastNotifications={toastNotifications} /> - = ({ - = ({ transformConfig }) => { const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( - Date: Thu, 16 Apr 2020 00:42:36 +0200 Subject: [PATCH 08/34] [ML] Refactor outlier detection to use DataGrid. --- .../components/data_grid/use_data_grid.ts | 1 + .../exploration_data_grid.tsx | 161 ----------- .../components/exploration_data_grid/index.ts | 7 - .../outlier_exploration}/common.test.ts | 0 .../outlier_exploration}/common.ts | 2 +- .../outlier_exploration.tsx | 201 ++++++------- .../use_outlier_data.test.ts | 33 +++ .../outlier_exploration/use_outlier_data.ts} | 272 +++++++++++------- .../hooks/use_explore_data/index.ts | 7 - 9 files changed, 303 insertions(+), 381 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/{hooks/use_explore_data => components/outlier_exploration}/common.test.ts (100%) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/{hooks/use_explore_data => components/outlier_exploration}/common.ts (90%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/{hooks/use_explore_data/use_explore_data.ts => components/outlier_exploration/use_outlier_data.ts} (51%) delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts index c218e3b492b1bd..df927804ef9fbb 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -90,6 +90,7 @@ export const useDataGrid = (columns: EuiDataGridColumn[]) => { setNoDataMessage, setPagination, setRowCount, + setSortingColumns, setStatus, setTableItems, setVisibleColumns, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx deleted file mode 100644 index e88bc1cd06f95a..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const FEATURE_INFLUENCE = 'feature_influence'; -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ExplorationDataGrid: FC = ({ - colorRange, - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - const split = columnId.split('.'); - let backgroundColor; - - // column with feature values get color coded by its corresponding influencer value - if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { - backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); - } - - // column with influencer values get color coded by its own value - if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { - backgroundColor = colorRange(cellValue); - } - - if (backgroundColor !== undefined) { - setCellProps({ - style: { backgroundColor }, - }); - } - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts deleted file mode 100644 index ea89e91de5046d..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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 { ExplorationDataGrid } from './exploration_data_grid'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts similarity index 90% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index 48db8e34c934e6..f9b3e0ef5150e4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -6,7 +6,7 @@ import { DataFrameAnalyticsConfig } from '../../../../common'; -const OUTLIER_SCORE = 'outlier_score'; +export const OUTLIER_SCORE = 'outlier_score'; export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index fdcb7d9d237e3c..8f88f052f4fea9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useEffect, useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -18,24 +18,46 @@ import { EuiTitle, } from '@elastic/eui'; +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + import { useColorRange, - ColorRangeLegend, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; +import { ml } from '../../../../../services/ml_api_service'; +import { ColorRangeLegend } from '../../../../../components/color_range_legend'; +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; +import { useMlContext } from '../../../../../contexts/ml'; -import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; +import { defaultSearchQuery, DataFrameAnalyticsConfig, INDEX_STATUS } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData, TableItem } from '../../hooks/use_explore_data'; - -import { ExplorationDataGrid } from '../exploration_data_grid'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { useOutlierData } from './use_outlier_data'; + +export type TableItem = Record; + const FEATURE_INFLUENCE = 'feature_influence'; +const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; + const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( @@ -51,91 +73,79 @@ interface ExplorationProps { jobId: string; } -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; - export const OutlierExploration: FC = React.memo(({ jobId }) => { - const { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - searchQuery, - selectedFields, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - rowCount, - status, - tableFields, - tableItems, - } = useExploreData(jobId); - - const columns = []; - - if ( - jobConfig !== undefined && - indexPattern !== undefined && - selectedFields.length > 0 && - tableItems.length > 0 - ) { - const resultsField = jobConfig.dest.results_field; - const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); - columns.push( - ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { - const idWithoutPrefix = id.replace(removePrefix, ''); - const field = indexPattern.fields.getByName(idWithoutPrefix); - - // 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; - } - - if (id === `${resultsField}.outlier_score`) { - schema = 'numeric'; + const mlContext = useMlContext(); + const [indexPattern, setIndexPattern] = useState(undefined); + const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + // get analytics configuration + useEffect(() => { + (async function() { + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + } + })(); + }, []); + + // get index pattern and field caps + useEffect(() => { + (async () => { + if (jobConfig !== undefined) { + try { + const destIndex = Array.isArray(jobConfig.dest.index) + ? jobConfig.dest.index[0] + : jobConfig.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; + + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfig.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } + + if (indexP !== undefined) { + setIndexPattern(indexP); + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + } + } catch (e) { + // eslint-disable-next-line + console.log('Error loading index field data', e); } + } + })(); + }, [jobConfig && jobConfig.id]); - return { id, schema }; - }) - ); - } - - const colorRange = useColorRange( - COLOR_RANGE.BLUE, - COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 - ); + const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - if (jobConfig === undefined || indexPattern === undefined) { - return null; - } + const { columns, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( - + = React.memo(({ jobId }) = }); } + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + ); + return ( = React.memo(({ jobId }) = gutterSize="s" > - + {jobStatus !== undefined && ( @@ -179,7 +195,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && indexPattern !== undefined && ( <> @@ -200,19 +216,10 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = {columns.length > 0 && tableItems.length > 0 && ( - )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts new file mode 100644 index 00000000000000..91a5ba2db6908e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { DataFrameAnalyticsConfig } from '../../../../common'; + +import { getOutlierScoreFieldName } from './common'; + +describe('Data Frame Analytics: common utils', () => { + test('getOutlierScoreFieldName()', () => { + const jobConfig: DataFrameAnalyticsConfig = { + id: 'the-id', + analysis: { outlier_detection: {} }, + dest: { + index: 'the-dest-index', + results_field: 'the-results-field', + }, + source: { + index: 'the-source-index', + }, + analyzed_fields: { includes: [], excludes: [] }, + model_memory_limit: '50mb', + create_time: 1234, + version: '1.0.0', + }; + + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + + expect(outlierScoreFieldName).toMatch('the-results-field.outlier_score'); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts similarity index 51% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index a0a9eb83124995..496511b754284f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -4,22 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { Dictionary } from '../../../../../../../common/types/common'; +import { + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; +import { useDataGrid, UseIndexDataReturnType } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { mlFieldFormatService } from '../../../../../services/field_format_service'; import { ml } from '../../../../../services/ml_api_service'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { getNestedProperty } from '../../../../../util/object_utils'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; import { getDefaultSelectableFields, @@ -28,36 +29,15 @@ import { EsFieldName, INDEX_STATUS, MAX_COLUMNS, - defaultSearchQuery, + sortColumns, } from '../../../../common'; import { isKeywordAndTextType } from '../../../../common/fields'; -import { getOutlierScoreFieldName } from './common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { getOutlierScoreFieldName, OUTLIER_SCORE } from './common'; -export type TableItem = Record; +const FEATURE_INFLUENCE = 'feature_influence'; -type Pagination = Pick; - -interface UseExploreDataReturnType { - errorMessage: string; - indexPattern: IndexPattern | undefined; - jobConfig: DataFrameAnalyticsConfig | undefined; - jobStatus: DATA_FRAME_TASK_STATE | undefined; - pagination: Pagination; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setJobConfig: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} +export type TableItem = Record; type EsSorting = Dictionary<{ order: 'asc' | 'desc'; @@ -73,80 +53,81 @@ interface SearchResponse7 extends SearchResponse { }; } -export const useExploreData = (jobId: string): UseExploreDataReturnType => { - const mlContext = useMlContext(); +const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } - const [indexPattern, setIndexPattern] = useState(undefined); - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; +export const useOutlierData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); const [tableFields, setTableFields] = useState([]); const [tableItems, setTableItems] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [sortingColumns, setSortingColumns] = useState([]); + const columns = []; + + if ( + jobConfig !== undefined && + indexPattern !== undefined && + selectedFields.length > 0 && + tableItems.length > 0 + ) { + const resultsField = jobConfig.dest.results_field; + const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); + columns.push( + ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { + const idWithoutPrefix = id.replace(removePrefix, ''); + const field = indexPattern.fields.getByName(idWithoutPrefix); + + // 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; + } - // get analytics configuration - useEffect(() => { - (async function() { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } + if (id === `${resultsField}.${OUTLIER_SCORE}`) { + schema = 'numeric'; + } - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - } - })(); - }, []); + return { id, schema }; + }) + ); + } - // get index pattern and field caps - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } + const dataGrid = useDataGrid(columns); - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } + const { + pagination, + rowCount, + setErrorMessage, + setRowCount, + setSortingColumns, + setStatus, + sortingColumns, + tableItems: dataGridTableItems, + } = dataGrid; - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - } catch (e) { - // eslint-disable-next-line - console.log('Error loading index field data', e); - } - } - })(); - }, [jobConfig && jobConfig.id]); + useEffect(() => { + setTableItems(dataGridTableItems); + }, [dataGridTableItems]); // initialize sorting: reverse sort on outlier score column useEffect(() => { @@ -245,23 +226,98 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { })(); }, [jobConfig && jobConfig.id, pagination, searchQuery, selectedFields, sortingColumns]); + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + ); + + const renderCellValue = useMemo(() => { + const resultsField = jobConfig?.dest.results_field ?? ''; + + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + const cellValue = + fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined + ? fullItem[columnId] + : null; + + const split = columnId.split('.'); + let backgroundColor; + + // column with feature values get color coded by its corresponding influencer value + if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { + backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); + } + + // column with influencer values get color coded by its own value + if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { + backgroundColor = colorRange(cellValue); + } + + if (backgroundColor !== undefined) { + setCellProps({ + style: { backgroundColor }, + }); + } + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [jobConfig, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); + return { - errorMessage, - indexPattern, - jobConfig, - jobStatus, + columns, + errorMessage: dataGrid.errorMessage, + invalidSortingColumnns: dataGrid.invalidSortingColumnns, + onChangeItemsPerPage: dataGrid.onChangeItemsPerPage, + onChangePage: dataGrid.onChangePage, + onSort: dataGrid.onSort, + noDataMessage: '', pagination, + setPagination: dataGrid.setPagination, + setVisibleColumns: dataGrid.setVisibleColumns, + renderCellValue, rowCount, - searchQuery, - selectedFields, - setJobConfig, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, sortingColumns, - status, - tableFields, + status: dataGrid.status, tableItems, + visibleColumns: dataGrid.visibleColumns, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts deleted file mode 100644 index dd896ca02f7f77..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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 { useExploreData, TableItem } from './use_explore_data'; From 05fc87d77888a33c6f730bdde0d3653883012985 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 16 Apr 2020 09:47:36 +0200 Subject: [PATCH 09/34] [ML] Fix translations. --- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index eeb6f9ad437efc..c45bce1a2154c6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9592,7 +9592,6 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の開始リクエストが受け付けられました。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "トレーニングパーセンテージ", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "外れ値検出結果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 551254d1b02531..b96bb3e27adf97 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9595,7 +9595,6 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 启动请求已确认。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "离群值检测结果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", From 3c1e1d237458749021143f969e74e25c756b508b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 16 Apr 2020 14:44:06 +0200 Subject: [PATCH 10/34] [ML] Code consolidation. --- .../components/data_grid/common.ts | 81 ++++-- .../application/components/data_grid/index.ts | 15 +- .../application/components/data_grid/types.ts | 20 ++ .../components/outlier_exploration/common.ts | 13 + .../outlier_exploration.tsx | 25 +- .../outlier_exploration/use_outlier_data.ts | 231 ++++++------------ .../public/app/hooks/use_index_data.ts | 58 +---- .../transform/public/shared_imports.ts | 4 + 8 files changed, 191 insertions(+), 256 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 04788335a67f69..a4b6a0deeb05fc 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -6,10 +6,75 @@ import { EuiDataGridSorting, EuiDataGridStyle } from '@elastic/eui'; +import { + IndexPattern, + IFieldType, + KBN_FIELD_TYPES, +} from '../../../../../../../src/plugins/data/public'; + import { getNestedProperty } from '../../util/object_utils'; export const INIT_MAX_COLUMNS = 20; +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'none', + header: 'shade', +}; + +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, +}; + +export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): string[] => { + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } + + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } + + return true; + }); + + return indexPatternFields; +}; + +export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { + // 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 KBN_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case KBN_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case KBN_FIELD_TYPES.NUMBER: + schema = 'numeric'; + break; + } + + return schema; +}; + /** * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. * `sortFn()` is recursive to support sorting on multiple columns. @@ -54,19 +119,3 @@ export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['colum return sortFn; }; - -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'none', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index a00d9c032a7ed1..6a62ec8a37f3d2 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -4,7 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export { multiColumnSortFactory } from './common'; +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, +} from './common'; export { useDataGrid } from './use_data_grid'; export { DataGrid } from './data_grid'; -export { RenderCellValue, UseIndexDataReturnType, INDEX_STATUS } from './types'; +export { + DataGridItem, + EsSorting, + RenderCellValue, + SearchResponse7, + UseIndexDataReturnType, + INDEX_STATUS, +} from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index 591c1796614238..be4b8ad3b49c2a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -5,9 +5,12 @@ */ import { Dispatch, SetStateAction } from 'react'; +import { SearchResponse } from 'elasticsearch'; import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; +import { Dictionary } from '../../../../common/types/common'; + // TODO move to common location and share with application/data_frame_analytics/common/fields.ts export type EsDocSource = Record; export type EsFieldName = string; @@ -29,6 +32,7 @@ export type OnSort = ( direction: 'asc' | 'desc'; }> ) => void; + export type RenderCellValue = ({ rowIndex, columnId, @@ -39,6 +43,22 @@ export type RenderCellValue = ({ setCellProps: any; }) => any; +export type DataGridItem = Record; + +export type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +export interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + export interface UseIndexDataReturnType { columns: EuiDataGridColumn[]; errorMessage: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index f9b3e0ef5150e4..bfb01b95419a41 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -4,9 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DataGridItem } from '../../../../../components/data_grid'; + import { DataFrameAnalyticsConfig } from '../../../../common'; +export const FEATURE_INFLUENCE = 'feature_influence'; export const OUTLIER_SCORE = 'outlier_score'; export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; + +export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 8f88f052f4fea9..47958c1ce1972d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -42,22 +42,11 @@ import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/ import { ExplorationQueryBar } from '../exploration_query_bar'; +import { getFeatureCount } from './common'; import { useOutlierData } from './use_outlier_data'; export type TableItem = Record; -const FEATURE_INFLUENCE = 'feature_influence'; - -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; - const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( @@ -159,18 +148,6 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = ); } - let tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : undefined; - - if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { - tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - }); - } - const colorRange = useColorRange( COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 496511b754284f..6675bacae9e647 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -4,104 +4,56 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo, useState } from 'react'; -import { SearchResponse } from 'elasticsearch'; +import { useEffect, useMemo } from 'react'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { Dictionary } from '../../../../../../../common/types/common'; +import { getErrorMessage } from '../../../../../../../common/util/errors'; import { useColorRange, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; -import { useDataGrid, UseIndexDataReturnType } from '../../../../../components/data_grid'; +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + useDataGrid, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; import { mlFieldFormatService } from '../../../../../services/field_format_service'; import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { - getDefaultSelectableFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - MAX_COLUMNS, - sortColumns, -} from '../../../../common'; +import { DataFrameAnalyticsConfig, INDEX_STATUS } from '../../../../common'; import { isKeywordAndTextType } from '../../../../common/fields'; -import { getOutlierScoreFieldName, OUTLIER_SCORE } from './common'; - -const FEATURE_INFLUENCE = 'feature_influence'; - -export type TableItem = Record; - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; +import { + getFeatureCount, + getOutlierScoreFieldName, + FEATURE_INFLUENCE, + OUTLIER_SCORE, +} from './common'; export const useOutlierData = ( indexPattern: IndexPattern | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, searchQuery: SavedSearchQuery ): UseIndexDataReturnType => { - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - + // EuiDataGrid State const columns = []; - if ( - jobConfig !== undefined && - indexPattern !== undefined && - selectedFields.length > 0 && - tableItems.length > 0 - ) { + if (jobConfig !== undefined && indexPattern !== undefined) { + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); const resultsField = jobConfig.dest.results_field; const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); columns.push( - ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { + ...indexPatternFields.map(id => { const idWithoutPrefix = id.replace(removePrefix, ''); const field = indexPattern.fields.getByName(idWithoutPrefix); - - // 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; - } + let schema = getDataGridSchemaFromKibanaFieldType(field); if (id === `${resultsField}.${OUTLIER_SCORE}`) { schema = 'numeric'; @@ -121,14 +73,11 @@ export const useOutlierData = ( setRowCount, setSortingColumns, setStatus, + setTableItems, sortingColumns, - tableItems: dataGridTableItems, + tableItems, } = dataGrid; - useEffect(() => { - setTableItems(dataGridTableItems); - }, [dataGridTableItems]); - // initialize sorting: reverse sort on outlier score column useEffect(() => { if (jobConfig !== undefined) { @@ -137,94 +86,52 @@ export const useOutlierData = ( }, [jobConfig && jobConfig.id]); // update data grid data - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const { pageIndex, pageSize } = pagination; - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query: searchQuery, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs, resultsField); - setSelectedFields(newSelectedFields.sort().splice(0, MAX_COLUMNS)); - } - - // 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. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableFields(flattenedFields); - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } + const getIndexData = async () => { + if (jobConfig !== undefined) { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { pageIndex, pageSize } = pagination; + const resp: SearchResponse7 = await ml.esSearch({ + index: jobConfig.dest.index, + body: { + query: searchQuery, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }); + + setRowCount(resp.hits.total.value); + + const docs = resp.hits.hits.map(d => d._source); + + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); } - })(); - }, [jobConfig && jobConfig.id, pagination, searchQuery, selectedFields, sortingColumns]); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, pagination, searchQuery, sortingColumns]); const colorRange = useColorRange( COLOR_RANGE.BLUE, diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 5706802b8de4e3..3cb6e30c767c1f 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -7,18 +7,19 @@ import moment from 'moment-timezone'; import { useEffect, useMemo } from 'react'; -import { SearchResponse } from 'elasticsearch'; - import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { Dictionary } from '../../../common/types/common'; import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; import { getNestedProperty } from '../../../common/utils/object_utils'; import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, getErrorMessage, useDataGrid, + EsSorting, RenderCellValue, + SearchResponse7, UseIndexDataReturnType, INDEX_STATUS, } from '../../shared_imports'; @@ -28,20 +29,6 @@ import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - type IndexSearchResponse = SearchResponse7; export const useIndexData = ( @@ -50,46 +37,13 @@ export const useIndexData = ( ): UseIndexDataReturnType => { const api = useApi(); - const allFields = indexPattern.fields.map(f => f.name); - const indexPatternFields: string[] = allFields.filter(f => { - if (indexPattern.metaFields.includes(f)) { - return false; - } - - const fieldParts = f.split('.'); - const lastPart = fieldParts.pop(); - if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { - return false; - } - - return true; - }); + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); // EuiDataGrid State const columns = [ ...indexPatternFields.map(id => { const field = indexPattern.fields.getByName(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 KBN_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - schema = 'datetime'; - break; - case KBN_FIELD_TYPES.GEO_POINT: - case KBN_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case KBN_FIELD_TYPES.NUMBER: - schema = 'numeric'; - break; - } - + const schema = getDataGridSchemaFromKibanaFieldType(field); return { id, schema }; }), ]; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 4d3a5ece7b59ec..2bd60febdb004b 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -19,10 +19,14 @@ export { export { getErrorMessage } from '../../ml/common/util/errors'; export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, multiColumnSortFactory, useDataGrid, DataGrid, + EsSorting, RenderCellValue, + SearchResponse7, UseIndexDataReturnType, INDEX_STATUS, } from '../../ml/public/application/components/data_grid'; From 311eb05593b3296dc0949059f8f951e8183ee96b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 16 Apr 2020 15:31:23 +0200 Subject: [PATCH 11/34] [ML] Consolidate renderCellValue. --- .../components/data_grid/common.ts | 89 +++++++++++++++++++ .../application/components/data_grid/index.ts | 1 + .../application/components/data_grid/types.ts | 15 ++-- .../components/data_grid/use_data_grid.ts | 12 +-- .../outlier_exploration/use_outlier_data.ts | 64 +++---------- .../public/app/hooks/use_index_data.ts | 47 +--------- .../transform/public/shared_imports.ts | 1 + 7 files changed, 117 insertions(+), 112 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index a4b6a0deeb05fc..1cd52afa184b96 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment-timezone'; +import { useMemo } from 'react'; + import { EuiDataGridSorting, EuiDataGridStyle } from '@elastic/eui'; import { @@ -12,7 +15,11 @@ import { KBN_FIELD_TYPES, } from '../../../../../../../src/plugins/data/public'; +import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; import { getNestedProperty } from '../../util/object_utils'; +import { mlFieldFormatService } from '../../services/field_format_service'; + +import { DataGridItem, IndexPagination, RenderCellValue } from './types'; export const INIT_MAX_COLUMNS = 20; @@ -75,6 +82,88 @@ export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefin return schema; }; +export const useRenderCellValue = ( + indexPattern: IndexPattern | undefined, + pagination: IndexPagination, + tableItems: DataGridItem[], + cellPropsCallback?: ( + columnId: string, + cellValue: any, + fullItem: Record, + setCellProps: any + ) => void +): RenderCellValue => { + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + if (indexPattern === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + const cellValue = tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + const field = indexPattern.fields.getByName(columnId); + if (field?.type === KBN_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (typeof cellPropsCallback === 'function') { + cellPropsCallback(columnId, cellValue, fullItem, setCellProps); + } + + return cellValue; + }; + }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + return renderCellValue; +}; + /** * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. * `sortFn()` is recursive to support sorting on multiple columns. diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index 6a62ec8a37f3d2..608e2bee277bbc 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -8,6 +8,7 @@ export { getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, + useRenderCellValue, } from './common'; export { useDataGrid } from './use_data_grid'; export { DataGrid } from './data_grid'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index be4b8ad3b49c2a..a7232cc1242bcf 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -11,9 +11,8 @@ import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } fro import { Dictionary } from '../../../../common/types/common'; -// TODO move to common location and share with application/data_frame_analytics/common/fields.ts -export type EsDocSource = Record; -export type EsFieldName = string; +export type ColumnId = string; +export type DataGridItem = Record; export enum INDEX_STATUS { UNUSED, @@ -43,8 +42,6 @@ export type RenderCellValue = ({ setCellProps: any; }) => any; -export type DataGridItem = Record; - export type EsSorting = Dictionary<{ order: 'asc' | 'desc'; }>; @@ -62,18 +59,18 @@ export interface SearchResponse7 extends SearchResponse { export interface UseIndexDataReturnType { columns: EuiDataGridColumn[]; errorMessage: string; - invalidSortingColumnns: EsFieldName[]; + invalidSortingColumnns: ColumnId[]; noDataMessage: string; onChangeItemsPerPage: OnChangeItemsPerPage; onChangePage: OnChangePage; onSort: OnSort; pagination: IndexPagination; setPagination: Dispatch>; - setVisibleColumns: Dispatch>; + setVisibleColumns: Dispatch>; renderCellValue: RenderCellValue; rowCount: number; sortingColumns: EuiDataGridSorting['columns']; status: INDEX_STATUS; - tableItems: EsDocSource[]; - visibleColumns: EsFieldName[]; + tableItems: DataGridItem[]; + visibleColumns: ColumnId[]; } diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts index df927804ef9fbb..8bad8b00b79e8d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -10,8 +10,8 @@ import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; import { INIT_MAX_COLUMNS } from './common'; import { - EsDocSource, - EsFieldName, + ColumnId, + DataGridItem, IndexPagination, OnChangeItemsPerPage, OnChangePage, @@ -19,14 +19,14 @@ import { INDEX_STATUS, } from './types'; -const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: 5 }; +export const useDataGrid = (columns: EuiDataGridColumn[], defaultPageSize = 5) => { + const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; -export const useDataGrid = (columns: EuiDataGridColumn[]) => { const [noDataMessage, setNoDataMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); + const [tableItems, setTableItems] = useState([]); const [pagination, setPagination] = useState(defaultPagination); const [sortingColumns, setSortingColumns] = useState([]); @@ -45,7 +45,7 @@ export const useDataGrid = (columns: EuiDataGridColumn[]) => { const resetPagination = () => setPagination(defaultPagination); // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); + const [visibleColumns, setVisibleColumns] = useState([]); const columnIds = columns.map(c => c.id); const defaultVisibleColumns = columnIds.splice(0, INIT_MAX_COLUMNS); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 6675bacae9e647..7276c501006442 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; @@ -19,12 +19,12 @@ import { getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, useDataGrid, + useRenderCellValue, EsSorting, SearchResponse7, UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { mlFieldFormatService } from '../../../../../services/field_format_service'; import { ml } from '../../../../../services/ml_api_service'; import { DataFrameAnalyticsConfig, INDEX_STATUS } from '../../../../common'; @@ -64,7 +64,7 @@ export const useOutlierData = ( ); } - const dataGrid = useDataGrid(columns); + const dataGrid = useDataGrid(columns, 25); const { pagination, @@ -139,36 +139,12 @@ export const useOutlierData = ( jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 ); - const renderCellValue = useMemo(() => { - const resultsField = jobConfig?.dest.results_field ?? ''; - - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; + const renderCellValue = useRenderCellValue( + indexPattern, + pagination, + tableItems, + (columnId, cellValue, fullItem, setCellProps) => { + const resultsField = jobConfig?.dest.results_field ?? ''; const split = columnId.split('.'); let backgroundColor; @@ -188,26 +164,8 @@ export const useOutlierData = ( style: { backgroundColor }, }); } - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [jobConfig, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); + } + ); return { columns, diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 3cb6e30c767c1f..35d3168041976b 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -4,21 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment-timezone'; -import { useEffect, useMemo } from 'react'; - -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; - -import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../common/utils/object_utils'; +import { useEffect } from 'react'; import { getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, getErrorMessage, useDataGrid, + useRenderCellValue, EsSorting, - RenderCellValue, SearchResponse7, UseIndexDataReturnType, INDEX_STATUS, @@ -107,42 +101,7 @@ export const useIndexData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - const renderCellValue: RenderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = tableItems.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(tableItems[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - const field = indexPattern.fields.getByName(columnId); - if (field?.type === KBN_FIELD_TYPES.DATE) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [indexPattern.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); return { columns, diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 2bd60febdb004b..831e12354b8627 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -23,6 +23,7 @@ export { getFieldsFromKibanaIndexPattern, multiColumnSortFactory, useDataGrid, + useRenderCellValue, DataGrid, EsSorting, RenderCellValue, From 46acaf55c4bd0b95fba946fbb811c7742edbf30e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 16 Apr 2020 17:19:59 +0200 Subject: [PATCH 12/34] [ML] Tweak hook return values. --- .../outlier_exploration/use_outlier_data.ts | 16 +--------------- .../transform/public/app/hooks/use_index_data.ts | 15 +-------------- .../transform/public/app/hooks/use_pivot_data.ts | 15 +-------------- 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 7276c501006442..23c17cd8eea77e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -68,7 +68,6 @@ export const useOutlierData = ( const { pagination, - rowCount, setErrorMessage, setRowCount, setSortingColumns, @@ -168,21 +167,8 @@ export const useOutlierData = ( ); return { + ...dataGrid, columns, - errorMessage: dataGrid.errorMessage, - invalidSortingColumnns: dataGrid.invalidSortingColumnns, - onChangeItemsPerPage: dataGrid.onChangeItemsPerPage, - onChangePage: dataGrid.onChangePage, - onSort: dataGrid.onSort, - noDataMessage: '', - pagination, - setPagination: dataGrid.setPagination, - setVisibleColumns: dataGrid.setVisibleColumns, renderCellValue, - rowCount, - sortingColumns, - status: dataGrid.status, - tableItems, - visibleColumns: dataGrid.visibleColumns, }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 35d3168041976b..ec5a4d244c152a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -104,21 +104,8 @@ export const useIndexData = ( const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); return { + ...dataGrid, columns, - errorMessage: dataGrid.errorMessage, - invalidSortingColumnns: dataGrid.invalidSortingColumnns, - onChangeItemsPerPage: dataGrid.onChangeItemsPerPage, - onChangePage: dataGrid.onChangePage, - onSort: dataGrid.onSort, - noDataMessage: '', - pagination, - setPagination: dataGrid.setPagination, - setVisibleColumns: dataGrid.setVisibleColumns, renderCellValue, - rowCount: dataGrid.rowCount, - sortingColumns, - status: dataGrid.status, - tableItems, - visibleColumns: dataGrid.visibleColumns, }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index b26adc6a3f1d4f..ff7ca5d42b5f7f 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -233,21 +233,8 @@ export const usePivotData = ( }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); return { + ...dataGrid, columns, - errorMessage: dataGrid.errorMessage, - invalidSortingColumnns: dataGrid.invalidSortingColumnns, - onChangeItemsPerPage: dataGrid.onChangeItemsPerPage, - onChangePage: dataGrid.onChangePage, - onSort: dataGrid.onSort, - noDataMessage: dataGrid.noDataMessage, - pagination, - setPagination: dataGrid.setPagination, - setVisibleColumns: dataGrid.setVisibleColumns, renderCellValue, - rowCount: dataGrid.rowCount, - sortingColumns, - status: dataGrid.status, - tableItems, - visibleColumns: dataGrid.visibleColumns, }; }; From 76dadc83a2d28e79904625e160a570d2a7df0e7f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 17 Apr 2020 00:49:07 +0200 Subject: [PATCH 13/34] Fix shared_imports.ts mock. --- .../transform/public/__mocks__/shared_imports.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 1dffacff2c84d0..5d391ee32a8ab7 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// actual mocks export const expandLiteralStrings = jest.fn(); export const XJsonMode = jest.fn(); export const useRequest = jest.fn(() => ({ @@ -11,14 +12,19 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; -export const SORT_DIRECTION = { ASC: 'asc' }; + +// just passing through the reimports export { getErrorMessage } from '../../../ml/common/util/errors'; export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, multiColumnSortFactory, useDataGrid, + useRenderCellValue, DataGrid, + EsSorting, RenderCellValue, + SearchResponse7, UseIndexDataReturnType, INDEX_STATUS, } from '../../../ml/public/application/components/data_grid'; From 3d4a8ae84804b93506d12cdae441f29aa5950a5c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 17 Apr 2020 10:00:28 +0200 Subject: [PATCH 14/34] [ML] Fix translations. --- .../translations/translations/ja-JP.json | 45 ++----------------- .../translations/translations/zh-CN.json | 45 ++----------------- 2 files changed, 6 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c45bce1a2154c6..786d1fa6c517cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2295,45 +2295,6 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", - "kbn.topNavMenu.openInspectorButtonLabel": "検査", - "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", - "kbn.topNavMenu.shareVisualizationButtonLabel": "共有", - "kbn.visualize.badge.readOnly.text": "読み込み専用", - "kbn.visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", - "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", - "kbn.visualize.editor.createBreadcrumb": "作成", - "kbn.visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", - "kbn.visualize.helpMenu.appName": "可視化", - "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", - "kbn.visualize.listing.betaTitle": "ベータ", - "kbn.visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", - "kbn.visualize.listing.breadcrumb": "可視化", - "kbn.visualize.listing.createNew.createButtonLabel": "新規ビジュアライゼーションを追加", - "kbn.visualize.listing.createNew.description": "データに基づき異なるビジュアライゼーションを作成できます。", - "kbn.visualize.listing.createNew.title": "最初のビジュアライゼーションの作成", - "kbn.visualize.listing.experimentalTitle": "実験的", - "kbn.visualize.listing.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", - "kbn.visualize.listing.noItemsMessage": "ビジュアライゼーションがないようです。", - "kbn.visualize.listing.table.entityName": "ビジュアライゼーション", - "kbn.visualize.listing.table.entityNamePlural": "ビジュアライゼーション", - "kbn.visualize.listing.table.listTitle": "ビジュアライゼーション", - "kbn.visualize.listing.table.titleColumnName": "タイトル", - "kbn.visualize.listing.table.typeColumnName": "タイプ", - "kbn.visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション", - "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加", - "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", - "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", - "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました", - "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "「{visTitle}」が保存されました", - "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", - "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", - "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", - "kbn.visualize.visualizationTypeInvalidNotificationMessage": "無効なビジュアライゼーションタイプ", - "kbn.visualize.visualizeDescription": "ビジュアライゼーションを作成して Elasticsearch インデックスに保存されたデータを集約します。", - "kbn.visualize.visualizeListingBreadcrumbsTitle": "可視化", - "kbn.visualize.visualizeListingDeleteErrorTitle": "ビジュアライゼーションの削除中にエラーが発生", - "kbn.visualize.wizard.step1Breadcrumb": "作成", - "kbn.visualize.wizard.step2Breadcrumb": "作成", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", @@ -2500,13 +2461,13 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "データに接続", "management.displayName": "管理", - "management.nav.label": "管理", - "management.nav.menu": "管理メニュー", - "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", + "management.nav.label": "管理", + "management.nav.menu": "管理メニュー", + "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "newsfeed.emptyPrompt.noNewsText": "Kibanaインスタンスがインターネットにアクセスできない場合、管理者にこの機能を無効にするように依頼してください。そうでない場合は、ニュースを取り込み続けます。", "newsfeed.emptyPrompt.noNewsTitle": "ニュースがない場合", "newsfeed.flyoutList.closeButtonLabel": "閉じる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b96bb3e27adf97..dbd2e0d3bf85c6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2296,45 +2296,6 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", - "kbn.topNavMenu.openInspectorButtonLabel": "检查", - "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", - "kbn.topNavMenu.shareVisualizationButtonLabel": "共享", - "kbn.visualize.badge.readOnly.text": "只读", - "kbn.visualize.badge.readOnly.tooltip": "无法保存可视化", - "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", - "kbn.visualize.editor.createBreadcrumb": "创建", - "kbn.visualize.experimentalVisInfoText": "此可视化标记为“实验”。", - "kbn.visualize.helpMenu.appName": "Visualize", - "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", - "kbn.visualize.listing.betaTitle": "公测版", - "kbn.visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", - "kbn.visualize.listing.breadcrumb": "可视化", - "kbn.visualize.listing.createNew.createButtonLabel": "新建可视化", - "kbn.visualize.listing.createNew.description": "可以根据您的数据创建不同的可视化。", - "kbn.visualize.listing.createNew.title": "创建首个可视化", - "kbn.visualize.listing.experimentalTitle": "实验性", - "kbn.visualize.listing.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", - "kbn.visualize.listing.noItemsMessage": "看起来您还没有任何可视化。", - "kbn.visualize.listing.table.entityName": "可视化", - "kbn.visualize.listing.table.entityNamePlural": "可视化", - "kbn.visualize.listing.table.listTitle": "可视化", - "kbn.visualize.listing.table.titleColumnName": "标题", - "kbn.visualize.listing.table.typeColumnName": "类型", - "kbn.visualize.pageHeading": "{chartName} {chartType}可视化", - "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板", - "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", - "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", - "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错", - "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "已保存“{visTitle}”", - "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "保存可视化", - "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "应用或放弃所做更改,然后保存", - "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "共享可视化", - "kbn.visualize.visualizationTypeInvalidNotificationMessage": "无效的可视化类型", - "kbn.visualize.visualizeDescription": "创建可视化并聚合存储在 Elasticsearch 索引中的数据。", - "kbn.visualize.visualizeListingBreadcrumbsTitle": "可视化", - "kbn.visualize.visualizeListingDeleteErrorTitle": "删除可视化时出错", - "kbn.visualize.wizard.step1Breadcrumb": "创建", - "kbn.visualize.wizard.step2Breadcrumb": "创建", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -2501,13 +2462,13 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "连接数据", "management.displayName": "管理", - "management.nav.label": "管理", - "management.nav.menu": "管理菜单", - "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "索引模式", "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "默认值", + "management.nav.label": "管理", + "management.nav.menu": "管理菜单", + "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "newsfeed.emptyPrompt.noNewsText": "如果您的 Kibana 实例没有 Internet 连接,请让您的管理员禁用此功能。否则,我们将不断尝试获取新闻。", "newsfeed.emptyPrompt.noNewsTitle": "无新闻?", "newsfeed.flyoutList.closeButtonLabel": "鍏抽棴", From 691c4c9eb006e27f94cd8155154f93464d777cc5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 17 Apr 2020 10:12:49 +0200 Subject: [PATCH 15/34] [ML] Delete helper file. --- x-pack/run_functional_tests.sh | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 x-pack/run_functional_tests.sh diff --git a/x-pack/run_functional_tests.sh b/x-pack/run_functional_tests.sh deleted file mode 100755 index e94f283ea03942..00000000000000 --- a/x-pack/run_functional_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -export TEST_KIBANA_URL="http://elastic:mlqa_admin@localhost:5601" -export TEST_ES_URL="http://elastic:mlqa_admin@localhost:9200" -node ../scripts/functional_test_runner --include-tag walterra From 8e14ff09a47a9271fce449138af9fc6b6c6a9ef0 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 17 Apr 2020 10:14:09 +0200 Subject: [PATCH 16/34] [ML] Cleanup DataGridTitle. --- .../components/data_grid/data_grid.tsx | 14 ++++++++++---- .../components/data_grid/data_grid_title.tsx | 18 ------------------ .../expanded_row_preview_pane.tsx | 11 +++-------- 3 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/data_grid/data_grid_title.tsx diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 41a39db9057601..4d36b4d85c018f 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -19,14 +19,20 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, + EuiTitle, } from '@elastic/eui'; import { CoreSetup } from 'src/core/public'; import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; -import { DataGridTitle } from './data_grid_title'; import { INDEX_STATUS, UseIndexDataReturnType } from './types'; +export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); + interface PropsWithoutHeader extends UseIndexDataReturnType { dataTestSubj: string; toastNotifications: CoreSetup['notifications']['toasts']; @@ -81,7 +87,7 @@ export const DataGrid: FC = props => { if (status === INDEX_STATUS.LOADED && data.length === 0) { return (
- {isWithHeader(props) && } + {isWithHeader(props) && } = props => { if (noDataMessage !== '') { return (
- {isWithHeader(props) && } + {isWithHeader(props) && } = props => { {isWithHeader(props) && ( - + = ({ dataGridTitle }) => ( - - {dataGridTitle} - -); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 6e434de6477892..e183712b390cf3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -18,25 +18,20 @@ import { getDefaultStepDefineState, } from '../../../create_transform/components/step_define/'; -interface Props { +interface ExpandedRowPreviewPaneProps { transformConfig: TransformPivotConfig; } -export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { +export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const toastNotifications = useToastNotifications(); - const previewConfig = applyTransformConfigToDefineState( + const { aggList, groupByList, searchQuery } = applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), transformConfig ); - - const { aggList, groupByList, searchQuery } = previewConfig; - const pivotQuery = getPivotQuery(searchQuery); - const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; - const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( From 9d75bbb9d2727444dd6ca52c245adeb5f7342bc4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 17 Apr 2020 13:30:07 +0200 Subject: [PATCH 17/34] [ML] Share code between outlier and regression analysis results pages. --- .../components/data_grid/common.ts | 18 +- .../application/components/data_grid/index.ts | 1 + .../application/components/data_grid/types.ts | 24 ++ .../components/data_grid/use_data_grid.ts | 16 +- .../data_frame_analytics/common/fields.ts | 22 +- .../common/get_index_data.ts | 68 ++++ .../data_frame_analytics/common/index.ts | 2 + .../outlier_exploration/use_outlier_data.ts | 82 +---- .../regression_exploration_data_grid.tsx | 135 -------- .../regression_exploration/results_table.tsx | 96 ++---- .../use_explore_data.ts | 291 ------------------ .../use_regression_data.ts | 143 +++++++++ .../public/__mocks__/shared_imports.ts | 1 + .../transform/public/shared_imports.ts | 1 + 14 files changed, 313 insertions(+), 587 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 1cd52afa184b96..569a9e94dc027a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -21,6 +21,8 @@ import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; +export const FEATURE_IMPORTANCE = 'feature_importance'; +export const FEATURE_INFLUENCE = 'feature_influence'; export const INIT_MAX_COLUMNS = 20; export const euiDataGridStyle: EuiDataGridStyle = { @@ -121,9 +123,19 @@ export const useRenderCellValue = ( format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); } - const cellValue = tableItems.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(tableItems[adjustedRowIndex], columnId, null) - : null; + function getCellValue(cId: string) { + if (cId.includes(`.${FEATURE_IMPORTANCE}.`)) { + const results = getNestedProperty(tableItems[adjustedRowIndex], 'ml', null); + const featureImportanceField = cId.replace('ml.', ''); + return results[featureImportanceField]; + } + + return tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) + : null; + } + + const cellValue = getCellValue(columnId); if (typeof cellValue === 'object' && cellValue !== null) { return JSON.stringify(cellValue); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index 608e2bee277bbc..ae224435b932bb 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -17,6 +17,7 @@ export { EsSorting, RenderCellValue, SearchResponse7, + UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, } from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index a7232cc1242bcf..41c521f2500539 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -74,3 +74,27 @@ export interface UseIndexDataReturnType { tableItems: DataGridItem[]; visibleColumns: ColumnId[]; } + +export interface UseDataGridReturnType { + errorMessage: string; + invalidSortingColumnns: ColumnId[]; + noDataMessage: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + resetPagination: () => void; + rowCount: number; + setErrorMessage: Dispatch>; + setNoDataMessage: Dispatch>; + setPagination: Dispatch>; + setRowCount: Dispatch>; + setSortingColumns: Dispatch>; + setStatus: Dispatch>; + setTableItems: Dispatch>; + setVisibleColumns: Dispatch>; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: DataGridItem[]; + visibleColumns: ColumnId[]; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts index 8bad8b00b79e8d..f2433095605f1a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -16,10 +16,16 @@ import { OnChangeItemsPerPage, OnChangePage, OnSort, + UseDataGridReturnType, INDEX_STATUS, } from './types'; -export const useDataGrid = (columns: EuiDataGridColumn[], defaultPageSize = 5) => { +export const useDataGrid = ( + columns: EuiDataGridColumn[], + defaultPageSize = 5, + defaultVisibleColumnsCount = INIT_MAX_COLUMNS, + defaultVisibleColumnsFilter?: (id: string) => boolean +): UseDataGridReturnType => { const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; const [noDataMessage, setNoDataMessage] = useState(''); @@ -48,7 +54,11 @@ export const useDataGrid = (columns: EuiDataGridColumn[], defaultPageSize = 5) = const [visibleColumns, setVisibleColumns] = useState([]); const columnIds = columns.map(c => c.id); - const defaultVisibleColumns = columnIds.splice(0, INIT_MAX_COLUMNS); + const filteredColumnIds = + defaultVisibleColumnsFilter !== undefined + ? columnIds.filter(defaultVisibleColumnsFilter) + : columnIds; + const defaultVisibleColumns = filteredColumnIds.splice(0, defaultVisibleColumnsCount); useEffect(() => { setVisibleColumns(defaultVisibleColumns); @@ -85,7 +95,6 @@ export const useDataGrid = (columns: EuiDataGridColumn[], defaultPageSize = 5) = pagination, resetPagination, rowCount, - status, setErrorMessage, setNoDataMessage, setPagination, @@ -95,6 +104,7 @@ export const useDataGrid = (columns: EuiDataGridColumn[], defaultPageSize = 5) = setTableItems, setVisibleColumns, sortingColumns, + status, tableItems, visibleColumns, }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index f165669bdd6745..688a9f9a243e26 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -273,15 +273,17 @@ export const getDefaultFieldsFromJobCaps = ( const featureImportanceFields = []; - if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push({ - id: `${resultsField}.feature_importance`, - name: `${resultsField}.feature_importance`, - type: KBN_FIELD_TYPES.NUMBER, - }); + if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { + featureImportanceFields.push( + ...fields.map(d => ({ + id: `${resultsField}.feature_importance.${d.id}`, + name: `${resultsField}.feature_importance.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); } - let allFields: any = []; + const allFields: any = []; // Only need to add these fields if we didn't use dest index pattern to get the fields if (needsDestIndexFields === true) { allFields.push( @@ -299,9 +301,9 @@ export const getDefaultFieldsFromJobCaps = ( sortRegressionResultsFields(a, b, jobConfig) ); // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid - if (needsDestIndexFields === false) { - allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); - } + // if (needsDestIndexFields === false) { + // allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); + // } let selectedFields = allFields.filter( (field: any) => field.name === predictedField || !field.name.includes('.keyword') diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts new file mode 100644 index 00000000000000..87b8c15aeaa784 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -0,0 +1,68 @@ +/* + * 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 { getErrorMessage } from '../../../../common/util/errors'; + +import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { ml } from '../../services/ml_api_service'; + +import { isKeywordAndTextType } from '../common/fields'; +import { SavedSearchQuery } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig, INDEX_STATUS } from './analytics'; + +export const getIndexData = async ( + jobConfig: DataFrameAnalyticsConfig | undefined, + dataGrid: UseDataGridReturnType, + searchQuery: SavedSearchQuery +) => { + if (jobConfig !== undefined) { + const { + pagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + } = dataGrid; + + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { pageIndex, pageSize } = pagination; + const resp: SearchResponse7 = await ml.esSearch({ + index: jobConfig.dest.index, + body: { + query: searchQuery, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }); + + setRowCount(resp.hits.total.value); + + const docs = resp.hits.hits.map(d => d._source); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 7b76faf613ce8c..16c8dd5b0f7260 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -48,3 +48,5 @@ export { } from './fields'; export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; + +export { getIndexData } from './get_index_data'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 23c17cd8eea77e..79f33ab54fea87 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -8,8 +8,6 @@ import { useEffect } from 'react'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { getErrorMessage } from '../../../../../../../common/util/errors'; - import { useColorRange, COLOR_RANGE, @@ -20,15 +18,11 @@ import { getFieldsFromKibanaIndexPattern, useDataGrid, useRenderCellValue, - EsSorting, - SearchResponse7, UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig, INDEX_STATUS } from '../../../../common'; -import { isKeywordAndTextType } from '../../../../common/fields'; +import { getIndexData, DataFrameAnalyticsConfig } from '../../../../common'; import { getFeatureCount, @@ -64,84 +58,38 @@ export const useOutlierData = ( ); } - const dataGrid = useDataGrid(columns, 25); - - const { - pagination, - setErrorMessage, - setRowCount, - setSortingColumns, - setStatus, - setTableItems, - sortingColumns, - tableItems, - } = dataGrid; + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-influence columns and the doc id copy + d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== 'ml__id_copy' + ); // initialize sorting: reverse sort on outlier score column useEffect(() => { if (jobConfig !== undefined) { - setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); + dataGrid.setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); } }, [jobConfig && jobConfig.id]); - // update data grid data - const getIndexData = async () => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const { pageIndex, pageSize } = pagination; - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query: searchQuery, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits.map(d => d._source); - - setTableItems(docs); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - setErrorMessage(getErrorMessage(e)); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - useEffect(() => { - getIndexData(); + getIndexData(jobConfig, dataGrid, searchQuery); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [jobConfig && jobConfig.id, pagination, searchQuery, sortingColumns]); + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); const colorRange = useColorRange( COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, dataGrid.tableItems) : 1 ); const renderCellValue = useRenderCellValue( indexPattern, - pagination, - tableItems, + dataGrid.pagination, + dataGrid.tableItems, (columnId, cellValue, fullItem, setCellProps) => { const resultsField = jobConfig?.dest.results_field ?? ''; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx deleted file mode 100644 index 0fcb1ed6007190..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const RegressionExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 43fa50b2e4df53..12187ac32cacdd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -18,12 +18,6 @@ import { EuiText, } from '@elastic/eui'; -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; - import { DataFrameAnalyticsConfig, MAX_COLUMNS, @@ -35,11 +29,15 @@ import { getTaskStateBadge } from '../../../analytics_management/components/anal import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { useExploreData } from './use_explore_data'; -import { ExplorationTitle } from './regression_exploration'; -import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; + import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from './regression_exploration'; +import { useRegressionData } from './use_regression_data'; + const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', { @@ -64,67 +62,17 @@ interface Props { export const ResultsTable: FC = React.memo( ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; + const regressionData = useRegressionData(indexPattern, jobConfig, searchQuery); + const docFieldsCount = regressionData.columns.length; + const { columns, errorMessage, status, tableItems, visibleColumns } = regressionData; - if (jobConfig === undefined) { + if (jobConfig === undefined || regressionData === undefined) { return null; } // if it's a searchBar syntax error leave the table visible so they can try again @@ -179,7 +127,7 @@ export const ResultsTable: FC = React.memo( { defaultMessage: '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, } )} @@ -211,18 +159,10 @@ export const ResultsTable: FC = React.memo( - diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts deleted file mode 100644 index 978aafd10de11f..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - LoadExploreDataArg, - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, -} from '../../../../common/analytics'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // 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. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts new file mode 100644 index 00000000000000..d09c3cde969289 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts @@ -0,0 +1,143 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { + IndexPattern, + ES_FIELD_TYPES, +} from '../../../../../../../../../../src/plugins/data/public'; + +import { + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; + +import { + getDefaultFieldsFromJobCaps, + getIndexData, + DataFrameAnalyticsConfig, +} from '../../../../common'; +import { + sortRegressionResultsFields, + BASIC_NUMERICAL_TYPES, + EXTENDED_NUMERICAL_TYPES, +} from '../../../../common/fields'; + +const FEATURE_IMPORTANCE = 'feature_importance'; + +export const useRegressionData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const getDefaultSelectedFields = () => { + const { fields } = newJobCapsService; + if (jobConfig !== undefined) { + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: { [key: string]: ES_FIELD_TYPES } = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + return { + defaultSelectedFields: defaultSelected.map(field => field.id), + fieldTypes: types, + tableFields: allFields, + }; + } else { + return { + defaultSelectedFields: [], + fieldTypes: {}, + tableFields: [], + }; + } + }; + + let columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes, tableFields } = getDefaultSelectedFields(); + columns = tableFields + .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) + .map((field: any) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + let isSortable = true; + const type = fieldTypes[field]; + const isNumber = + type !== undefined && + (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + + if (isNumber) { + schema = 'numeric'; + } + + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; + } + + if (field === `${resultsField}.${FEATURE_IMPORTANCE}`) { + isSortable = false; + } + + return { id: field, schema, isSortable }; + }); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-importance columns and the doc id copy + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== 'ml__id_copy' + ); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 5d391ee32a8ab7..1fb6bd8b92792d 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -25,6 +25,7 @@ export { EsSorting, RenderCellValue, SearchResponse7, + UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, } from '../../../ml/public/application/components/data_grid'; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 831e12354b8627..ad91054f834ac2 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -28,6 +28,7 @@ export { EsSorting, RenderCellValue, SearchResponse7, + UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, } from '../../ml/public/application/components/data_grid'; From 1481ac6b2cc98469e61d5a56aa6a3a7a6d3c9443 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 20 Apr 2020 16:45:48 +0200 Subject: [PATCH 18/34] Shared config for outliers and regression. --- .../components/data_grid/common.ts | 49 +++++++++ .../application/components/data_grid/index.ts | 1 + .../data_frame_analytics/common/fields.ts | 4 - .../common/get_index_fields.ts | 49 +++++++++ .../data_frame_analytics/common/index.ts | 3 + .../common/use_results_view_config.ts | 100 +++++++++++++++++ .../outlier_exploration.tsx | 75 +------------ .../outlier_exploration/use_outlier_data.ts | 40 +++---- .../regression_exploration.tsx | 104 ++---------------- .../regression_exploration/results_table.tsx | 5 - .../use_regression_data.ts | 93 ++-------------- 11 files changed, 243 insertions(+), 280 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 569a9e94dc027a..2787b49d3fc02a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -12,9 +12,15 @@ import { EuiDataGridSorting, EuiDataGridStyle } from '@elastic/eui'; import { IndexPattern, IFieldType, + ES_FIELD_TYPES, KBN_FIELD_TYPES, } from '../../../../../../../src/plugins/data/public'; +import { + BASIC_NUMERICAL_TYPES, + EXTENDED_NUMERICAL_TYPES, +} from '../../data_frame_analytics/common/fields'; + import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; @@ -23,6 +29,7 @@ import { DataGridItem, IndexPagination, RenderCellValue } from './types'; export const FEATURE_IMPORTANCE = 'feature_importance'; export const FEATURE_INFLUENCE = 'feature_influence'; +export const OUTLIER_SCORE = 'outlier_score'; export const INIT_MAX_COLUMNS = 20; export const euiDataGridStyle: EuiDataGridStyle = { @@ -60,6 +67,48 @@ export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): str return indexPatternFields; }; +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, resultsField: string) => { + return Object.keys(fieldTypes).map(field => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + const isSortable = true; + const type = fieldTypes[field]; + + const isNumber = + type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + if (isNumber) { + schema = 'numeric'; + } + + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; + } + + if ( + field === `${resultsField}.${OUTLIER_SCORE}` || + field === `${resultsField}.${FEATURE_INFLUENCE}` || + field === `${resultsField}.${FEATURE_IMPORTANCE}` + ) { + schema = 'numeric'; + } + + return { id: field, schema, isSortable }; + }); +}; + export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index ae224435b932bb..871d3f74209515 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -5,6 +5,7 @@ */ export { + getDataGridSchemasFromFieldTypes, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 688a9f9a243e26..55fa6267e407d4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -300,10 +300,6 @@ export const getDefaultFieldsFromJobCaps = ( allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => sortRegressionResultsFields(a, b, jobConfig) ); - // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid - // if (needsDestIndexFields === false) { - // allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); - // } let selectedFields = allFields.filter( (field: any) => field.name === predictedField || !field.name.includes('.keyword') diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts new file mode 100644 index 00000000000000..12ae4a586e9498 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts @@ -0,0 +1,49 @@ +/* + * 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 { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { newJobCapsService } from '../../services/new_job_capabilities_service'; + +import { getDefaultFieldsFromJobCaps, DataFrameAnalyticsConfig } from '../common'; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getIndexFields = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + needsDestIndexFields: boolean +) => { + const { fields } = newJobCapsService; + if (jobConfig !== undefined) { + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: FieldTypes = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + return { + defaultSelectedFields: defaultSelected.map(field => field.id), + fieldTypes: types, + tableFields: allFields, + }; + } else { + return { + defaultSelectedFields: [], + fieldTypes: {}, + tableFields: [], + }; + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 16c8dd5b0f7260..97c15da9bd5a39 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -50,3 +50,6 @@ export { export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; export { getIndexData } from './get_index_data'; +export { getIndexFields } from './get_index_fields'; + +export { useResultsViewConfig } from './use_results_view_config'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts new file mode 100644 index 00000000000000..f789b94e18f1a5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -0,0 +1,100 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; + +import { getIndexPatternIdFromName } from '../../util/index_utils'; +import { ml } from '../../services/ml_api_service'; +import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { useMlContext } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig } from '../common'; + +import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics'; +import { DATA_FRAME_TASK_STATE } from '../pages/analytics_management/components/analytics_list/common'; + +export const useResultsViewConfig = (jobId: string) => { + const mlContext = useMlContext(); + const [indexPattern, setIndexPattern] = useState(undefined); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); + const [jobConfig, setJobConfig] = useState(undefined); + const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( + undefined + ); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); + + // get analytics configuration, index pattern and field caps + useEffect(() => { + (async function() { + setIsLoadingJobConfig(false); + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; + + try { + const destIndex = Array.isArray(jobConfigUpdate.dest.index) + ? jobConfigUpdate.dest.index[0] + : jobConfigUpdate.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; + + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfigUpdate.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } + + if (indexP !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIndexPattern(indexP); + setIsInitialized(true); + setIsLoadingJobConfig(false); + } + } catch (e) { + if (e.message !== undefined) { + setJobCapsServiceErrorMessage(e.message); + } else { + setJobCapsServiceErrorMessage(JSON.stringify(e)); + } + setIsLoadingJobConfig(false); + } + } + })(); + }, []); + + return { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 47958c1ce1972d..46f1bab775e7af 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState, FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -18,27 +18,19 @@ import { EuiTitle, } from '@elastic/eui'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - import { useColorRange, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; -import { ml } from '../../../../../services/ml_api_service'; import { ColorRangeLegend } from '../../../../../components/color_range_legend'; import { DataGrid } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { useMlContext } from '../../../../../contexts/ml'; -import { defaultSearchQuery, DataFrameAnalyticsConfig, INDEX_STATUS } from '../../../../common'; +import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { ExplorationQueryBar } from '../exploration_query_bar'; @@ -63,69 +55,8 @@ interface ExplorationProps { } export const OutlierExploration: FC = React.memo(({ jobId }) => { - const mlContext = useMlContext(); - const [indexPattern, setIndexPattern] = useState(undefined); - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); + const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - - // get analytics configuration - useEffect(() => { - (async function() { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - } - })(); - }, []); - - // get index pattern and field caps - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - } catch (e) { - // eslint-disable-next-line - console.log('Error loading index field data', e); - } - } - })(); - }, [jobConfig && jobConfig.id]); - const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); const { columns, errorMessage, status, tableItems } = outlierData; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 79f33ab54fea87..7536d516b4f352 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -6,6 +6,8 @@ import { useEffect } from 'react'; +import { EuiDataGridColumn } from '@elastic/eui'; + import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { @@ -14,47 +16,37 @@ import { COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; import { - getDataGridSchemaFromKibanaFieldType, - getFieldsFromKibanaIndexPattern, + getDataGridSchemasFromFieldTypes, useDataGrid, useRenderCellValue, UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { getIndexData, DataFrameAnalyticsConfig } from '../../../../common'; +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; -import { - getFeatureCount, - getOutlierScoreFieldName, - FEATURE_INFLUENCE, - OUTLIER_SCORE, -} from './common'; +import { getFeatureCount, getOutlierScoreFieldName, FEATURE_INFLUENCE } from './common'; export const useOutlierData = ( indexPattern: IndexPattern | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, searchQuery: SavedSearchQuery ): UseIndexDataReturnType => { - // EuiDataGrid State - const columns = []; + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; if (jobConfig !== undefined && indexPattern !== undefined) { - const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); const resultsField = jobConfig.dest.results_field; - const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); columns.push( - ...indexPatternFields.map(id => { - const idWithoutPrefix = id.replace(removePrefix, ''); - const field = indexPattern.fields.getByName(idWithoutPrefix); - let schema = getDataGridSchemaFromKibanaFieldType(field); - - if (id === `${resultsField}.${OUTLIER_SCORE}`) { - schema = 'numeric'; - } - - return { id, schema }; - }) + ...getDataGridSchemasFromFieldTypes( + fieldTypes, + resultsField + ) /* .sort((a: any, b: any) => + sortRegressionResultsFields(a, b, jobConfig) + )*/ ); } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index bfeca76a2b1c75..3c975bbf7cd05d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -4,21 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; +import React, { FC, Fragment, useState } from 'react'; import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; +import { useResultsViewConfig } from '../../../../common'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { 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 { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -51,91 +44,16 @@ interface Props { } export const RegressionExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IIndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 12187ac32cacdd..17a32c9d35a3dc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -13,7 +13,6 @@ import { EuiFlexItem, EuiFormRow, EuiPanel, - EuiProgress, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -136,10 +135,6 @@ export const ResultsTable: FC = React.memo( - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts index d09c3cde969289..bd3d41cf4b1618 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts @@ -8,29 +8,18 @@ import { useEffect } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; -import { - IndexPattern, - ES_FIELD_TYPES, -} from '../../../../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { + getDataGridSchemasFromFieldTypes, useDataGrid, useRenderCellValue, UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { - getDefaultFieldsFromJobCaps, - getIndexData, - DataFrameAnalyticsConfig, -} from '../../../../common'; -import { - sortRegressionResultsFields, - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, -} from '../../../../common/fields'; +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { sortRegressionResultsFields } from '../../../../common/fields'; const FEATURE_IMPORTANCE = 'feature_importance'; @@ -42,76 +31,16 @@ export const useRegressionData = ( const needsDestIndexFields = indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - return { - defaultSelectedFields: defaultSelected.map(field => field.id), - fieldTypes: types, - tableFields: allFields, - }; - } else { - return { - defaultSelectedFields: [], - fieldTypes: {}, - tableFields: [], - }; - } - }; - - let columns: EuiDataGridColumn[] = []; + const columns: EuiDataGridColumn[] = []; if (jobConfig !== undefined) { const resultsField = jobConfig.dest.results_field; - const { fieldTypes, tableFields } = getDefaultSelectedFields(); - columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.${FEATURE_IMPORTANCE}`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortRegressionResultsFields(a.id, b.id, jobConfig) + ) + ); } const dataGrid = useDataGrid( From 58d00df732c447e06183a87bb5ba4b4d402b76d9 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 08:36:38 +0200 Subject: [PATCH 19/34] [ML] Use shared code for classication results page. --- .../classification_exploration.tsx | 122 ++------ .../classification_exploration_data_grid.tsx | 135 -------- .../results_table.tsx | 100 ++---- .../use_classification_data.ts | 72 +++++ .../use_explore_data.ts | 292 ------------------ .../regression_exploration.tsx | 9 +- 6 files changed, 117 insertions(+), 613 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 5c151166829ab9..a9f484b1496afd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -4,21 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; +import React, { FC, Fragment, useState } from 'react'; + import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { useResultsViewConfig } from '../../../../common'; import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; + +import { EvaluatePanel } from './evaluate_panel'; +import { ResultsTable } from './results_table'; export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -51,100 +49,16 @@ interface Props { } export const ClassificationExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } else { - setJobConfigErrorMessage( - i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', - { - defaultMessage: 'No results found.', - } - ) - ); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx deleted file mode 100644 index 424fc002795caa..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ClassificationExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx index bf63dfe68fe9e5..86efba7dd5a7bd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, @@ -12,17 +12,15 @@ import { EuiFlexItem, EuiFormRow, EuiPanel, - EuiProgress, EuiSpacer, EuiText, } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; + +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { DataFrameAnalyticsConfig, @@ -33,11 +31,11 @@ import { } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData } from './use_explore_data'; // TableItem import { ExplorationTitle } from './classification_exploration'; -import { ClassificationExplorationDataGrid } from './classification_exploration_data_grid'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { useClassificationData } from './use_classification_data'; + const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', { @@ -62,67 +60,21 @@ interface Props { export const ResultsTable: FC = React.memo( ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } + const classificationData = useClassificationData(indexPattern, jobConfig, searchQuery); + const docFieldsCount = classificationData.columns.length; + const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; + useEffect(() => { + setEvaluateSearchQuery(searchQuery); + }, [JSON.stringify(searchQuery)]); - if (jobConfig === undefined) { + if (jobConfig === undefined || classificationData === undefined) { return null; } // if it's a searchBar syntax error leave the table visible so they can try again @@ -181,7 +133,7 @@ export const ResultsTable: FC = React.memo( { defaultMessage: '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, } )} @@ -190,10 +142,6 @@ export const ResultsTable: FC = React.memo( - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( @@ -213,18 +161,10 @@ export const ResultsTable: FC = React.memo( - diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts new file mode 100644 index 00000000000000..339b672861847e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts @@ -0,0 +1,72 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { sortRegressionResultsFields } from '../../../../common/fields'; + +const FEATURE_IMPORTANCE = 'feature_importance'; + +export const useClassificationData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortRegressionResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-importance columns and the doc id copy + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== 'ml__id_copy' + ); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts deleted file mode 100644 index c8809ca5e471bd..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, - LoadExploreDataArg, -} from '../../../../common/analytics'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // 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. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 3c975bbf7cd05d..f794454393f140 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -5,14 +5,19 @@ */ import React, { FC, Fragment, useState } from 'react'; + import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; + import { useResultsViewConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + import { LoadingPanel } from '../loading_panel'; +import { EvaluatePanel } from './evaluate_panel'; +import { ResultsTable } from './results_table'; + export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( From e6d4ff9f33c3aef174ad16a182e63f1674057477 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 09:13:15 +0200 Subject: [PATCH 20/34] [ML] Consolidate exploration title and error callout. --- .../classification_exploration.tsx | 54 ++++++------------- .../results_table.tsx | 9 ++-- .../exploration_title/exploration_title.tsx | 15 ++++++ .../components/exploration_title/index.ts | 7 +++ .../job_config_error_callout/index.ts | 7 +++ .../job_config_error_callout.tsx | 54 +++++++++++++++++++ .../outlier_exploration.tsx | 22 +++----- .../regression_exploration.tsx | 54 ++++++------------- .../regression_exploration/results_table.tsx | 9 ++-- 9 files changed, 133 insertions(+), 98 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index a9f484b1496afd..66070e6049d570 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -6,7 +6,7 @@ import React, { FC, Fragment, useState } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -16,39 +16,22 @@ import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analy import { LoadingPanel } from '../loading_panel'; import { EvaluatePanel } from './evaluate_panel'; +import { JobConfigErrorCallout } from '../job_config_error_callout'; import { ResultsTable } from './results_table'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for classification job ID {jobId}', - values: { jobId }, - })} - - -); - -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; } export const ClassificationExploration: FC = ({ jobId }) => { + const explorationTitle = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', + { + defaultMessage: 'Destination index for classification job ID {jobId}', + values: { jobId }, + } + ); + const { indexPattern, isInitialized, @@ -62,17 +45,11 @@ export const ClassificationExploration: FC = ({ jobId }) => { if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
+ ); } @@ -93,6 +70,7 @@ export const ClassificationExploration: FC = ({ jobId }) => { indexPattern={indexPattern} jobStatus={jobStatus} setEvaluateSearchQuery={setSearchQuery} + title={explorationTitle} /> )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx index 86efba7dd5a7bd..c2b021af1648bc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -31,7 +31,7 @@ import { } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ExplorationTitle } from './classification_exploration'; +import { ExplorationTitle } from '../exploration_title'; import { ExplorationQueryBar } from '../exploration_query_bar'; import { useClassificationData } from './use_classification_data'; @@ -56,10 +56,11 @@ interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; setEvaluateSearchQuery: React.Dispatch>; + title: string; } export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { + ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { @@ -83,7 +84,7 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( @@ -114,7 +115,7 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx new file mode 100644 index 00000000000000..f06c88c73df711 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiTitle } from '@elastic/eui'; + +export const ExplorationTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts new file mode 100644 index 00000000000000..b34e61b3b5e766 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts @@ -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 { ExplorationTitle } from './exploration_title'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts new file mode 100644 index 00000000000000..a5991f4325d120 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts @@ -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 { JobConfigErrorCallout } from './job_config_error_callout'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx new file mode 100644 index 00000000000000..69aacb84053f6e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiCallOut, EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ExplorationTitle } from '../exploration_title'; + +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 { + jobCapsServiceErrorMessage: string | undefined; + jobConfigErrorMessage: string | undefined; + title: string; +} + +export const JobConfigErrorCallout: FC = ({ + jobCapsServiceErrorMessage, + jobConfigErrorMessage, + title, +}) => { + return ( + + + + +

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 46f1bab775e7af..0154f92576c4af 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -15,7 +15,6 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { @@ -33,28 +32,23 @@ import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../.. import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from '../exploration_title'; import { getFeatureCount } from './common'; import { useOutlierData } from './use_outlier_data'; export type TableItem = Record; -const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { - defaultMessage: 'Outlier detection job ID {jobId}', - values: { jobId }, - })} - - -); - interface ExplorationProps { jobId: string; } export const OutlierExploration: FC = React.memo(({ jobId }) => { + const explorationTitle = i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { + defaultMessage: 'Outlier detection job ID {jobId}', + values: { jobId }, + }); + const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); @@ -65,7 +59,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( - + = React.memo(({ jobId }) = gutterSize="s" > - + {jobStatus !== undefined && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index f794454393f140..43be9da9f333d6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -6,49 +6,32 @@ import React, { FC, Fragment, useState } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useResultsViewConfig } from '../../../../common'; import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { JobConfigErrorCallout } from '../job_config_error_callout'; import { LoadingPanel } from '../loading_panel'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for regression job ID {jobId}', - values: { jobId }, - })} - - -); - -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; } export const RegressionExploration: FC = ({ jobId }) => { + const explorationTitle = i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', + { + defaultMessage: 'Destination index for regression job ID {jobId}', + values: { jobId }, + } + ); + const { indexPattern, isInitialized, @@ -62,17 +45,11 @@ export const RegressionExploration: FC = ({ jobId }) => { if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
+ ); } @@ -93,6 +70,7 @@ export const RegressionExploration: FC = ({ jobId }) => { indexPattern={indexPattern} jobStatus={jobStatus} setEvaluateSearchQuery={setSearchQuery} + title={explorationTitle} /> )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 17a32c9d35a3dc..bef354befe4ef8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -33,8 +33,8 @@ import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from '../exploration_title'; -import { ExplorationTitle } from './regression_exploration'; import { useRegressionData } from './use_regression_data'; const showingDocs = i18n.translate( @@ -57,10 +57,11 @@ interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; setEvaluateSearchQuery: React.Dispatch>; + title: string; } export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { + ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { @@ -80,7 +81,7 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( @@ -107,7 +108,7 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( From f0bcb3d1a625524bd097edbd6de27b0aaff4240b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 09:40:27 +0200 Subject: [PATCH 21/34] [ML] ExplorationPageWrapper. --- .../classification_exploration.tsx | 69 ++++-------------- .../exploration_page_wrapper.tsx | 73 +++++++++++++++++++ .../exploration_page_wrapper/index.ts | 7 ++ .../regression_exploration.tsx | 68 +++-------------- 4 files changed, 104 insertions(+), 113 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 66070e6049d570..c3c1bdf05ec48a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState } from 'react'; - -import { EuiSpacer } from '@elastic/eui'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { useResultsViewConfig } from '../../../../common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; - -import { LoadingPanel } from '../loading_panel'; +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; import { EvaluatePanel } from './evaluate_panel'; -import { JobConfigErrorCallout } from '../job_config_error_callout'; import { ResultsTable } from './results_table'; interface Props { @@ -24,55 +18,18 @@ interface Props { } export const ClassificationExploration: FC = ({ jobId }) => { - const explorationTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', - { - defaultMessage: 'Destination index for classification job ID {jobId}', - values: { jobId }, - } - ); - - const { - indexPattern, - isInitialized, - isLoadingJobConfig, - jobCapsServiceErrorMessage, - jobConfig, - jobConfigErrorMessage, - jobStatus, - } = useResultsViewConfig(jobId); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - + - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + EvaluatePanel={EvaluatePanel} + ResultsTable={ResultsTable} + /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx new file mode 100644 index 00000000000000..0d6ddc99db4009 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { useResultsViewConfig } from '../../../../common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + +import { JobConfigErrorCallout } from '../job_config_error_callout'; +import { LoadingPanel } from '../loading_panel'; + +interface Props { + jobId: string; + title: string; + EvaluatePanel: any; + ResultsTable: any; +} + +export const ExplorationPageWrapper: FC = ({ + jobId, + title, + EvaluatePanel, + ResultsTable, +}) => { + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { + return ( + + ); + } + + return ( + <> + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( + + )} + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && + jobConfig !== undefined && + indexPattern !== undefined && + isInitialized === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts new file mode 100644 index 00000000000000..2fbad3c31b42b8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts @@ -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 { ExplorationPageWrapper } from './exploration_page_wrapper'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 43be9da9f333d6..59db5e1eb583f6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -4,17 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState } from 'react'; - -import { EuiSpacer } from '@elastic/eui'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { useResultsViewConfig } from '../../../../common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; - -import { JobConfigErrorCallout } from '../job_config_error_callout'; -import { LoadingPanel } from '../loading_panel'; +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; @@ -24,55 +18,15 @@ interface Props { } export const RegressionExploration: FC = ({ jobId }) => { - const explorationTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', - { - defaultMessage: 'Destination index for regression job ID {jobId}', - values: { jobId }, - } - ); - - const { - indexPattern, - isInitialized, - isLoadingJobConfig, - jobCapsServiceErrorMessage, - jobConfig, - jobConfigErrorMessage, - jobStatus, - } = useResultsViewConfig(jobId); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - - )} - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + ); }; From d06310dab0b8ad42b03996cb70125e23b4486e4a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 10:09:55 +0200 Subject: [PATCH 22/34] [ML] ExplorationResultsPage. --- .../data_frame_analytics/common/fields.ts | 8 +- .../classification_exploration.tsx | 2 - .../exploration_page_wrapper.tsx | 23 +-- .../exploration_page_wrapper/index.ts | 2 +- .../exploration_results_table.tsx} | 6 +- .../exploration_results_table/index.ts | 7 + .../use_exploration_results.ts} | 6 +- .../regression_exploration.tsx | 2 - .../regression_exploration/results_table.tsx | 169 ------------------ .../use_regression_data.ts | 72 -------- 10 files changed, 31 insertions(+), 266 deletions(-) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/{classification_exploration/results_table.tsx => exploration_results_table/exploration_results_table.tsx} (97%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/{classification_exploration/use_classification_data.ts => exploration_results_table/use_exploration_results.ts} (92%) delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 55fa6267e407d4..f03ae61482a405 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -112,7 +112,7 @@ export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: strin return a.localeCompare(b); }; -export const sortRegressionResultsFields = ( +export const sortExplorationResultsFields = ( a: string, b: string, jobConfig: DataFrameAnalyticsConfig @@ -298,7 +298,7 @@ export const getDefaultFieldsFromJobCaps = ( allFields.push(...fields, ...featureImportanceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => - sortRegressionResultsFields(a, b, jobConfig) + sortExplorationResultsFields(a, b, jobConfig) ); let selectedFields = allFields.filter( @@ -349,7 +349,7 @@ export const getDefaultClassificationFields = ( return docs.some(row => row._source[k] !== null); }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) + .sort((a, b) => sortExplorationResultsFields(a, b, jobConfig)) .slice(0, DEFAULT_REGRESSION_COLUMNS); }; @@ -382,7 +382,7 @@ export const getDefaultRegressionFields = ( return docs.some(row => row._source[k] !== null); }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) + .sort((a, b) => sortExplorationResultsFields(a, b, jobConfig)) .slice(0, DEFAULT_REGRESSION_COLUMNS); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index c3c1bdf05ec48a..ccac9a697210b0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { ExplorationPageWrapper } from '../exploration_page_wrapper'; import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; interface Props { jobId: string; @@ -29,7 +28,6 @@ export const ClassificationExploration: FC = ({ jobId }) => { } )} EvaluatePanel={EvaluatePanel} - ResultsTable={ResultsTable} /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 0d6ddc99db4009..1986c486974c95 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -8,25 +8,28 @@ import React, { FC, useState } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { useResultsViewConfig } from '../../../../common'; +import { useResultsViewConfig, DataFrameAnalyticsConfig } from '../../../../common'; import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { ExplorationResultsTable } from '../exploration_results_table'; import { JobConfigErrorCallout } from '../job_config_error_callout'; import { LoadingPanel } from '../loading_panel'; +export interface EvaluatePanelProps { + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + interface Props { jobId: string; title: string; - EvaluatePanel: any; - ResultsTable: any; + EvaluatePanel: FC; } -export const ExplorationPageWrapper: FC = ({ - jobId, - title, - EvaluatePanel, - ResultsTable, -}) => { +export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel }) => { const { indexPattern, isInitialized, @@ -60,7 +63,7 @@ export const ExplorationPageWrapper: FC = ({ jobConfig !== undefined && indexPattern !== undefined && isInitialized === true && ( - = React.memo( +export const ExplorationResultsTable: FC = React.memo( ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); @@ -67,7 +67,7 @@ export const ResultsTable: FC = React.memo( setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); - const classificationData = useClassificationData(indexPattern, jobConfig, searchQuery); + const classificationData = useExplorationResults(indexPattern, jobConfig, searchQuery); const docFieldsCount = classificationData.columns.length; const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts new file mode 100644 index 00000000000000..19308640c8b022 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts @@ -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 { ExplorationResultsTable } from './exploration_results_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts similarity index 92% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index 339b672861847e..0850e5298d74e0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_classification_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -19,11 +19,11 @@ import { import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; -import { sortRegressionResultsFields } from '../../../../common/fields'; +import { sortExplorationResultsFields } from '../../../../common/fields'; const FEATURE_IMPORTANCE = 'feature_importance'; -export const useClassificationData = ( +export const useExplorationResults = ( indexPattern: IndexPattern | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, searchQuery: SavedSearchQuery @@ -38,7 +38,7 @@ export const useClassificationData = ( const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); columns.push( ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => - sortRegressionResultsFields(a.id, b.id, jobConfig) + sortExplorationResultsFields(a.id, b.id, jobConfig) ) ); } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 59db5e1eb583f6..36d91f6f41d44b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { ExplorationPageWrapper } from '../exploration_page_wrapper'; import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; interface Props { jobId: string; @@ -26,7 +25,6 @@ export const RegressionExploration: FC = ({ jobId }) => { values: { jobId }, })} EvaluatePanel={EvaluatePanel} - ResultsTable={ResultsTable} /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx deleted file mode 100644 index bef354befe4ef8..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useEffect, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { - DataFrameAnalyticsConfig, - MAX_COLUMNS, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { DataGrid } from '../../../../../components/data_grid'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { getToastNotifications } from '../../../../../util/dependency_cache'; - -import { ExplorationQueryBar } from '../exploration_query_bar'; -import { ExplorationTitle } from '../exploration_title'; - -import { useRegressionData } from './use_regression_data'; - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch>; - title: string; -} - -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - - const regressionData = useRegressionData(indexPattern, jobConfig, searchQuery); - const docFieldsCount = regressionData.columns.length; - const { columns, errorMessage, status, tableItems, visibleColumns } = regressionData; - - if (jobConfig === undefined || regressionData === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - -

{errorMessage}

-
-
- ); - } - - return ( - - - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, - } - )} - - )} - - - - - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - - - - - - - - - - )} - - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts deleted file mode 100644 index bd3d41cf4b1618..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_regression_data.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 { useEffect } from 'react'; - -import { EuiDataGridColumn } from '@elastic/eui'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { - getDataGridSchemasFromFieldTypes, - useDataGrid, - useRenderCellValue, - UseIndexDataReturnType, -} from '../../../../../components/data_grid'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; -import { sortRegressionResultsFields } from '../../../../common/fields'; - -const FEATURE_IMPORTANCE = 'feature_importance'; - -export const useRegressionData = ( - indexPattern: IndexPattern | undefined, - jobConfig: DataFrameAnalyticsConfig | undefined, - searchQuery: SavedSearchQuery -): UseIndexDataReturnType => { - const needsDestIndexFields = - indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; - - const columns: EuiDataGridColumn[] = []; - - if (jobConfig !== undefined) { - const resultsField = jobConfig.dest.results_field; - const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); - columns.push( - ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => - sortRegressionResultsFields(a.id, b.id, jobConfig) - ) - ); - } - - const dataGrid = useDataGrid( - columns, - 25, - // reduce default selected rows from 20 to 8 for performance reasons. - 8, - // by default, hide feature-importance columns and the doc id copy - d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== 'ml__id_copy' - ); - - useEffect(() => { - getIndexData(jobConfig, dataGrid, searchQuery); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); - - const renderCellValue = useRenderCellValue( - indexPattern, - dataGrid.pagination, - dataGrid.tableItems - ); - - return { - ...dataGrid, - columns, - renderCellValue, - }; -}; From 246db60b92c0d9e924bb6a0c0cb579dbcaf5044d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 10:39:11 +0200 Subject: [PATCH 23/34] [ML] Consolidate constants. --- .../ml/public/application/components/data_grid/common.ts | 8 +++++--- .../application/data_frame_analytics/common/constants.ts | 9 +++++++++ .../exploration_results_table/use_exploration_results.ts | 3 +-- .../components/outlier_exploration/common.ts | 4 +--- 4 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 2787b49d3fc02a..70d1517697a0d4 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -21,15 +21,17 @@ import { EXTENDED_NUMERICAL_TYPES, } from '../../data_frame_analytics/common/fields'; +import { + FEATURE_IMPORTANCE, + FEATURE_INFLUENCE, + OUTLIER_SCORE, +} from '../../data_frame_analytics/common/constants'; import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; -export const FEATURE_IMPORTANCE = 'feature_importance'; -export const FEATURE_INFLUENCE = 'feature_influence'; -export const OUTLIER_SCORE = 'outlier_score'; export const INIT_MAX_COLUMNS = 20; export const euiDataGridStyle: EuiDataGridStyle = { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts new file mode 100644 index 00000000000000..27e45a9ca36f15 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -0,0 +1,9 @@ +/* + * 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 const FEATURE_IMPORTANCE = 'feature_importance'; +export const FEATURE_INFLUENCE = 'feature_influence'; +export const OUTLIER_SCORE = 'outlier_score'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index 0850e5298d74e0..f7e2cc9987e083 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -19,10 +19,9 @@ import { import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { FEATURE_IMPORTANCE } from '../../../../common/constants'; import { sortExplorationResultsFields } from '../../../../common/fields'; -const FEATURE_IMPORTANCE = 'feature_importance'; - export const useExplorationResults = ( indexPattern: IndexPattern | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index bfb01b95419a41..bfd3dd33995aac 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -7,9 +7,7 @@ import { DataGridItem } from '../../../../../components/data_grid'; import { DataFrameAnalyticsConfig } from '../../../../common'; - -export const FEATURE_INFLUENCE = 'feature_influence'; -export const OUTLIER_SCORE = 'outlier_score'; +import { FEATURE_INFLUENCE, OUTLIER_SCORE } from '../../../../common/constants'; export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; From c33299f23e0f2774466f871b7d2989a3d2322a3e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 10:46:18 +0200 Subject: [PATCH 24/34] [ML] Consolidiate data grid styles. --- .../data_frame_analytics/common/data_grid.ts | 23 ------------------- .../data_frame_analytics/common/index.ts | 4 +--- .../transform/public/app/common/data_grid.ts | 18 --------------- .../transform/public/app/common/index.ts | 2 -- 4 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts deleted file mode 100644 index 2b6d733837562b..00000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { EuiDataGridStyle } from '@elastic/eui'; - -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'none', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 97c15da9bd5a39..d58e37d7ffafd7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -36,8 +36,8 @@ export { getDefaultFieldsFromJobCaps, getFlattenedFields, sortColumns, + sortExplorationResultsFields, sortRegressionResultsColumns, - sortRegressionResultsFields, toggleSelectedField, toggleSelectedFieldSimple, EsId, @@ -47,8 +47,6 @@ export { MAX_COLUMNS, } from './fields'; -export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; - export { getIndexData } from './get_index_data'; export { getIndexFields } from './get_index_fields'; diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index f2448c6b785ae6..cf9ba5d6f58536 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,29 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridStyle } from '@elastic/eui'; - import { PivotQuery } from './request'; import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'highlight', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; - export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index 5b223cc862255d..009c8c7a2a9f52 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -8,8 +8,6 @@ export { AggName, isAggName } from './aggregations'; export { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement, - euiDataGridStyle, - euiDataGridToolbarSettings, INIT_MAX_COLUMNS, } from './data_grid'; export { From 49241285841c41e1c213e5049229473416eac089 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 11:00:49 +0200 Subject: [PATCH 25/34] [ML] Types cleanup. --- .../application/components/data_grid/types.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index 41c521f2500539..5fa038edf7815e 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -11,16 +11,11 @@ import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } fro import { Dictionary } from '../../../../common/types/common'; +import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; + export type ColumnId = string; export type DataGridItem = Record; -export enum INDEX_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - export type IndexPagination = Pick; export type OnChangeItemsPerPage = (pageSize: any) => void; @@ -56,23 +51,26 @@ export interface SearchResponse7 extends SearchResponse { }; } -export interface UseIndexDataReturnType { +export interface UseIndexDataReturnType + extends Pick< + UseDataGridReturnType, + | 'errorMessage' + | 'invalidSortingColumnns' + | 'noDataMessage' + | 'onChangeItemsPerPage' + | 'onChangePage' + | 'onSort' + | 'pagination' + | 'setPagination' + | 'setVisibleColumns' + | 'rowCount' + | 'sortingColumns' + | 'status' + | 'tableItems' + | 'visibleColumns' + > { columns: EuiDataGridColumn[]; - errorMessage: string; - invalidSortingColumnns: ColumnId[]; - noDataMessage: string; - onChangeItemsPerPage: OnChangeItemsPerPage; - onChangePage: OnChangePage; - onSort: OnSort; - pagination: IndexPagination; - setPagination: Dispatch>; - setVisibleColumns: Dispatch>; renderCellValue: RenderCellValue; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableItems: DataGridItem[]; - visibleColumns: ColumnId[]; } export interface UseDataGridReturnType { From 9235b2ef153af73f4746b4de001489ee2c075f17 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 12:39:04 +0200 Subject: [PATCH 26/34] [ML] Cleanup constants, SCSS. --- .../application/components/data_grid/_data_grid.scss | 7 ------- .../public/application/components/data_grid/data_grid.tsx | 7 +++---- .../ml/public/application/components/data_grid/index.ts | 1 - .../application/components/data_grid/use_data_grid.ts | 3 ++- .../components/outlier_exploration/use_outlier_data.ts | 3 ++- .../plugins/transform/public/__mocks__/shared_imports.ts | 2 +- x-pack/plugins/transform/public/shared_imports.ts | 2 +- 7 files changed, 9 insertions(+), 16 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/data_grid/_data_grid.scss diff --git a/x-pack/plugins/ml/public/application/components/data_grid/_data_grid.scss b/x-pack/plugins/ml/public/application/components/data_grid/_data_grid.scss deleted file mode 100644 index cd7060018f7e9f..00000000000000 --- a/x-pack/plugins/ml/public/application/components/data_grid/_data_grid.scss +++ /dev/null @@ -1,7 +0,0 @@ -/* override to hide the top border of EuiDataGrid on the transform list preview - to avoid a 2px line where EuiDataGrid and EuiTabs clash. */ -.mlDataGrid-noHeader { - .euiDataGrid__controls { - border-top: 0; - } -} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 4d36b4d85c018f..a5b301902cc75a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './_data_grid.scss'; - import React, { useEffect, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -24,8 +22,10 @@ import { import { CoreSetup } from 'src/core/public'; +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; -import { INDEX_STATUS, UseIndexDataReturnType } from './types'; +import { UseIndexDataReturnType } from './types'; export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( @@ -162,7 +162,6 @@ export const DataGrid: FC = props => { )} Date: Wed, 22 Apr 2020 12:39:55 +0200 Subject: [PATCH 27/34] [ML] Fix result view error handling. --- .../common/use_results_view_config.ts | 86 ++++++++++--------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index f789b94e18f1a5..0bc9e782075963 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -8,6 +8,8 @@ import { useEffect, useState } from 'react'; import { IndexPattern } from '../../../../../../../src/plugins/data/public'; +import { getErrorMessage } from '../../../../common/util/errors'; + import { getIndexPatternIdFromName } from '../../util/index_utils'; import { ml } from '../../services/ml_api_service'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; @@ -34,56 +36,58 @@ export const useResultsViewConfig = (jobId: string) => { useEffect(() => { (async function() { setIsLoadingJobConfig(false); - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; + try { + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; - try { - const destIndex = Array.isArray(jobConfigUpdate.dest.index) - ? jobConfigUpdate.dest.index[0] - : jobConfigUpdate.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } + const destIndex = Array.isArray(jobConfigUpdate.dest.index) + ? jobConfigUpdate.dest.index[0] + : jobConfigUpdate.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; - if (indexP === undefined) { - const sourceIndex = jobConfigUpdate.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfigUpdate.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } - if (indexP !== undefined) { - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIndexPattern(indexP); - setIsInitialized(true); + if (indexP !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIndexPattern(indexP); + setIsInitialized(true); + setIsLoadingJobConfig(false); + } + } catch (e) { + setJobCapsServiceErrorMessage(getErrorMessage(e)); setIsLoadingJobConfig(false); } - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); } + } catch (e) { + setJobConfigErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); } })(); }, []); From 0030e80fded15fe6b524b82a0a73110df6ed8d9d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Apr 2020 15:40:02 +0200 Subject: [PATCH 28/34] [ML] Fix translations. --- .../error_callout/error_callout.tsx | 37 +++++++------------ .../exploration_results_table.tsx | 6 +-- .../job_config_error_callout.tsx | 21 ++++------- .../regression_exploration/evaluate_panel.tsx | 2 +- .../translations/translations/ja-JP.json | 31 +++------------- .../translations/translations/zh-CN.json | 31 +++------------- 6 files changed, 37 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx index 9765192f0e4469..839587c47289a4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx @@ -15,7 +15,7 @@ interface Props { export const ErrorCallout: FC = ({ error }) => { let errorCallout = ( = ({ error }) => { if (error.includes('index_not_found')) { errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the destination index exists and contains documents.', })} @@ -46,16 +46,13 @@ export const ErrorCallout: FC = ({ error }) => { // Job was started but no results have been written yet errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the job has completed and the index contains documents.', })} @@ -66,22 +63,16 @@ export const ErrorCallout: FC = ({ error }) => { // query bar syntax is incorrect errorCallout = (

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

); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 1e81acb192bb4f..3b30665e1507c7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -37,14 +37,14 @@ import { ExplorationQueryBar } from '../exploration_query_bar'; import { useExplorationResults } from './use_exploration_results'; const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', + 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', { defaultMessage: 'Showing documents for which predictions exist', } ); const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText', + 'xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText', { defaultMessage: 'Showing first {searchSize} documents for which predictions exist', values: { searchSize: SEARCH_SIZE }, @@ -130,7 +130,7 @@ export const ExplorationResultsTable: FC = React.memo( {docFieldsCount > MAX_COLUMNS && ( {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.fieldSelection', + 'xpack.ml.dataframe.analytics.explorationResults.fieldSelection', { defaultMessage: '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx index 69aacb84053f6e..945d6654067c01 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx @@ -12,20 +12,13 @@ import { i18n } from '@kbn/i18n'; import { ExplorationTitle } from '../exploration_title'; -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.", - } -); +const jobConfigErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobConfig.errorTitle', { + defaultMessage: 'Unable to fetch results. An error occurred loading the job configuration data.', +}); + +const jobCapsErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobCaps.errorTitle', { + defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", +}); interface Props { jobCapsServiceErrorMessage: string | undefined; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 6ef6666be5ec6d..f6e8e0047671fc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -235,7 +235,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', + 'xpack.ml.dataframe.analytics.regressionExploration.regressionDocsLink', { defaultMessage: 'Regression evaluation docs ', } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 786d1fa6c517cc..38d622943d5d7c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2295,6 +2295,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可視化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", @@ -2416,8 +2418,6 @@ "savedObjectsManagement.breadcrumb.index": "保存されたオブジェクト", "savedObjectsManagement.field.offLabel": "オフ", "savedObjectsManagement.field.onLabel": "オン", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可視化", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -2461,13 +2461,13 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "データに接続", "management.displayName": "管理", + "management.nav.label": "管理", + "management.nav.menu": "管理メニュー", + "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "デフォルト", - "management.nav.label": "管理", - "management.nav.menu": "管理メニュー", - "management.stackManagement.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", "newsfeed.emptyPrompt.noNewsText": "Kibanaインスタンスがインターネットにアクセスできない場合、管理者にこの機能を無効にするように依頼してください。そうでない場合は、ニュースを取り込み続けます。", "newsfeed.emptyPrompt.noNewsTitle": "ニュースがない場合", "newsfeed.flyoutList.closeButtonLabel": "閉じる", @@ -9471,14 +9471,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分類混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "予測されたラベル", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回帰評価ドキュメント ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "アクションを表示", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "すべての列を表示", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分類ジョブID {jobId}のデスティネーションインデックス", @@ -9557,26 +9551,13 @@ "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "外れ値検出ジョブID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", "xpack.ml.dataframe.analytics.exploration.title": "分析の探索", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "予測があるドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回帰ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "{docFieldsCount, number} 件中 showing {selectedFieldsLength, number} 件の{docFieldsCount, plural, one {フィールド} other {フィールド}}", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "回帰分析モデルの実行の効果を測定します。真値と予測値の間の差異の二乗平均合計。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "インデックスのクエリが結果を返しませんでした。デスティネーションインデックスが存在し、ドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "クエリ構文が無効であり、結果を返しませんでした。クエリ構文を確認し、再試行してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス", @@ -16729,4 +16710,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dbd2e0d3bf85c6..1892bc472eb88b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2296,6 +2296,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可视化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -2417,8 +2419,6 @@ "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", "savedObjectsManagement.breadcrumb.index": "已保存对象", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -2462,13 +2462,13 @@ "management.breadcrumb": "管理", "management.connectDataDisplayName": "连接数据", "management.displayName": "管理", + "management.nav.label": "管理", + "management.nav.menu": "管理菜单", + "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", "indexPatternManagement.editIndexPattern.createIndex.defaultTypeName": "索引模式", "indexPatternManagement.editIndexPattern.list.defaultIndexPatternListName": "默认值", - "management.nav.label": "管理", - "management.nav.menu": "管理菜单", - "management.stackManagement.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", "newsfeed.emptyPrompt.noNewsText": "如果您的 Kibana 实例没有 Internet 连接,请让您的管理员禁用此功能。否则,我们将不断尝试获取新闻。", "newsfeed.emptyPrompt.noNewsTitle": "无新闻?", "newsfeed.flyoutList.closeButtonLabel": "鍏抽棴", @@ -9474,14 +9474,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分类混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "预测标签", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回归评估文档 ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "显示操作", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "显示所有列", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分类作业 ID {jobId} 的目标索引", @@ -9560,26 +9554,13 @@ "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "离群值检测作业 ID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回归作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "已选择 {docFieldsCount, number} 个{docFieldsCount, plural, one {字段} other {字段}}中的 {selectedFieldsLength, number} 个", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "度量回归分析模型的表现。真实值与预测值之差的平均平方和。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "该索引的查询未返回结果。请确保目标索引存在且包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "查询语法无效,未返回任何结果。请检查查询语法并重试。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引", @@ -16734,4 +16715,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} \ No newline at end of file +} From 773147ae72d68face15668f7d360c68f60809063 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Apr 2020 10:23:45 +0200 Subject: [PATCH 29/34] [ML] Fix functional test, adapts dataTestSubj. --- .../exploration_results_table/exploration_results_table.tsx | 2 +- .../services/machine_learning/data_frame_analytics.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 3b30665e1507c7..9188d43b3627ad 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -109,7 +109,7 @@ export const ExplorationResultsTable: FC = React.memo( diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index bada1d42b564a0..bd7d76e34b447c 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -41,7 +41,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertClassificationEvaluatePanelElementsExists() { @@ -50,7 +50,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertOutlierTablePanelExists() { From 8ba23f1406017d7e60ff560bf50ffe3dc7d1c2dd Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Apr 2020 11:56:56 +0200 Subject: [PATCH 30/34] [ML] Fix column sorting. Delete orphaned helper functions. --- .../data_frame_analytics/common/fields.ts | 424 +++++------------- .../data_frame_analytics/common/index.ts | 8 - .../use_exploration_results.ts | 4 +- .../outlier_exploration/use_outlier_data.ts | 12 +- 4 files changed, 119 insertions(+), 329 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index f03ae61482a405..bec27c90e53cef 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getNestedProperty } from '../../util/object_utils'; import { - DataFrameAnalyticsConfig, getNumTopFeatureImportanceValues, getPredictedFieldName, getDependentVar, getPredictionFieldName, + isClassificationAnalysis, + isOutlierAnalysis, + isRegressionAnalysis, + DataFrameAnalyticsConfig, } from './analytics'; import { Field } from '../../../../common/types/fields'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE } from './constants'; + export type EsId = string; export type EsDocSource = Record; export type EsFieldName = string; @@ -42,7 +46,7 @@ export const EXTENDED_NUMERICAL_TYPES = new Set([ ES_FIELD_TYPES.SCALED_FLOAT, ]); -const ML__ID_COPY = 'ml__id_copy'; +export const ML__ID_COPY = 'ml__id_copy'; export const isKeywordAndTextType = (fieldName: string): boolean => { const { fields } = newJobCapsService; @@ -64,32 +68,61 @@ export const isKeywordAndTextType = (fieldName: string): boolean => { }; // Used to sort columns: +// - Anchor on the left ml.outlier_score, ml.is_training, , // - string based columns are moved to the left -// - followed by the outlier_score column -// - feature_influence fields get moved next to the corresponding field column +// - feature_influence/feature_importance fields get moved next to the corresponding field column // - overall fields get sorted alphabetically -export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: string, b: string) => { - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; +export const sortExplorationResultsFields = ( + a: string, + b: string, + jobConfig: DataFrameAnalyticsConfig +) => { + const resultsField = jobConfig.dest.results_field; - if (typeofA !== 'string' && typeofB === 'string') { - return 1; - } - if (typeofA === 'string' && typeofB !== 'string') { - return -1; - } - if (typeofA === 'string' && typeofB === 'string') { - return a.localeCompare(b); - } + if (isOutlierAnalysis(jobConfig.analysis)) { + if (a === `${resultsField}.${OUTLIER_SCORE}`) { + return -1; + } - if (a === `${resultsField}.outlier_score`) { - return -1; + if (b === `${resultsField}.${OUTLIER_SCORE}`) { + return 1; + } } - if (b === `${resultsField}.outlier_score`) { - return 1; + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); + + if (a === `${resultsField}.is_training`) { + return -1; + } + if (b === `${resultsField}.is_training`) { + return 1; + } + if (a === predictedField) { + return -1; + } + if (b === predictedField) { + return 1; + } + if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { + return -1; + } + if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { + return 1; + } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } } + const typeofA = typeof a; + const typeofB = typeof b; + const tokensA = a.split('.'); const prefixA = tokensA[0]; const tokensB = b.split('.'); @@ -109,91 +142,6 @@ export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: strin return a.localeCompare(tokensB.join('.')); } - return a.localeCompare(b); -}; - -export const sortExplorationResultsFields = ( - a: string, - b: string, - jobConfig: DataFrameAnalyticsConfig -) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - if (a === `${resultsField}.is_training`) { - return -1; - } - if (b === `${resultsField}.is_training`) { - return 1; - } - if (a === predictedField) { - return -1; - } - if (b === predictedField) { - return 1; - } - if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { - return -1; - } - if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - - return a.localeCompare(b); -}; - -// Used to sort columns: -// Anchor on the left ml.is_training, , -export const sortRegressionResultsColumns = ( - obj: EsDocSource, - jobConfig: DataFrameAnalyticsConfig -) => (a: string, b: string) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; - - if (a === `${resultsField}.is_training`) { - return -1; - } - - if (b === `${resultsField}.is_training`) { - return 1; - } - - if (a === predictedField) { - return -1; - } - - if (b === predictedField) { - return 1; - } - - if (a === dependentVariable) { - return -1; - } - - if (b === dependentVariable) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -204,44 +152,9 @@ export const sortRegressionResultsColumns = ( return a.localeCompare(b); } - const tokensA = a.split('.'); - const prefixA = tokensA[0]; - const tokensB = b.split('.'); - const prefixB = tokensB[0]; - - if (prefixA === resultsField && tokensA.length > 1 && prefixB !== resultsField) { - tokensA.shift(); - tokensA.shift(); - if (tokensA.join('.') === b) return 1; - return tokensA.join('.').localeCompare(b); - } - - if (prefixB === resultsField && tokensB.length > 1 && prefixA !== resultsField) { - tokensB.shift(); - tokensB.shift(); - if (tokensB.join('.') === a) return -1; - return a.localeCompare(tokensB.join('.')); - } - return a.localeCompare(b); }; -export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFieldName[] { - const flatDocFields: EsFieldName[] = []; - const newDocFields = Object.keys(obj); - newDocFields.forEach(f => { - const fieldValue = getNestedProperty(obj, f); - if (typeof fieldValue !== 'object' || fieldValue === null || Array.isArray(fieldValue)) { - flatDocFields.push(f); - } else { - const innerFields = getFlattenedFields(fieldValue, resultsField); - const flattenedFields = innerFields.map(d => `${f}.${d}`); - flatDocFields.push(...flattenedFields); - } - }); - return flatDocFields.filter(f => f !== ML__ID_COPY); -} - export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, @@ -259,44 +172,73 @@ export const getDefaultFieldsFromJobCaps = ( return fieldsObj; } - const dependentVariable = getDependentVar(jobConfig.analysis); - const type = newJobCapsService.getFieldById(dependentVariable)?.type; - const predictionFieldName = getPredictionFieldName(jobConfig.analysis); - const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); // default is 'ml' const resultsField = jobConfig.dest.results_field; - const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ - predictionFieldName ? predictionFieldName : defaultPredictionField - }`; - const featureImportanceFields = []; - - if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { - featureImportanceFields.push( - ...fields.map(d => ({ - id: `${resultsField}.feature_importance.${d.id}`, - name: `${resultsField}.feature_importance.${d.name}`, + const featureInfluenceFields = []; + const allFields: any = []; + let type: ES_FIELD_TYPES | undefined; + let predictedField: string | undefined; + + if (isOutlierAnalysis(jobConfig.analysis)) { + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push({ + id: `${resultsField}.${OUTLIER_SCORE}`, + name: `${resultsField}.${OUTLIER_SCORE}`, type: KBN_FIELD_TYPES.NUMBER, - })) - ); + }); + + featureInfluenceFields.push( + ...fields + .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) + .map(d => ({ + id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`, + name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); + } } - const allFields: any = []; - // Only need to add these fields if we didn't use dest index pattern to get the fields - if (needsDestIndexFields === true) { - allFields.push( - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type } - ); + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + type = newJobCapsService.getFieldById(dependentVariable)?.type; + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); + + const defaultPredictionField = `${dependentVariable}_prediction`; + predictedField = `${resultsField}.${ + predictionFieldName ? predictionFieldName : defaultPredictionField + }`; + + if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { + featureImportanceFields.push( + ...fields + .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) + .map(d => ({ + id: `${resultsField}.${FEATURE_IMPORTANCE}.${d.id}`, + name: `${resultsField}.${FEATURE_IMPORTANCE}.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); + } + + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } } - allFields.push(...fields, ...featureImportanceFields); + allFields.push(...fields, ...featureImportanceFields, ...featureInfluenceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => sortExplorationResultsFields(a, b, jobConfig) ); @@ -315,145 +257,3 @@ export const getDefaultFieldsFromJobCaps = ( depVarType: type, }; }; - -export const getDefaultClassificationFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - const resultsField = jobConfig.dest.results_field; - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - - if (k === `${resultsField}.prediction_probability`) { - return true; - } - - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortExplorationResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultRegressionFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - const resultsField = jobConfig.dest.results_field; - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortExplorationResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields.filter(k => { - if (k === `${resultsField}.outlier_score`) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }); -}; - -export const toggleSelectedFieldSimple = ( - selectedFields: EsFieldName[], - column: EsFieldName -): EsFieldName[] => { - const index = selectedFields.indexOf(column); - - if (index === -1) { - selectedFields.push(column); - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; -// Fields starting with 'ml' or custom result name not included in newJobCapsService fields so -// need to recreate the field with correct type and add to selected fields -export const toggleSelectedField = ( - selectedFields: Field[], - column: EsFieldName, - resultsField: string, - depVarType?: ES_FIELD_TYPES -): 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 { - const resultFieldPattern = `^${resultsField}\.`; - const regex = new RegExp(resultFieldPattern); - const isResultField = column.match(regex) !== null; - let newField; - - if (isResultField && column.includes('is_training')) { - newField = { - id: column, - name: column, - type: ES_FIELD_TYPES.BOOLEAN, - }; - } else if (isResultField && depVarType !== undefined) { - newField = { - id: column, - name: column, - type: depVarType, - }; - } - - if (newField) selectedFields.push(newField); - } - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index d58e37d7ffafd7..400902c152c9e6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -30,16 +30,8 @@ export { } from './analytics'; export { - getDefaultSelectableFields, - getDefaultRegressionFields, - getDefaultClassificationFields, getDefaultFieldsFromJobCaps, - getFlattenedFields, - sortColumns, sortExplorationResultsFields, - sortRegressionResultsColumns, - toggleSelectedField, - toggleSelectedFieldSimple, EsId, EsDoc, EsDocSource, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index f7e2cc9987e083..c74b9f5a0613af 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -20,7 +20,7 @@ import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; import { FEATURE_IMPORTANCE } from '../../../../common/constants'; -import { sortExplorationResultsFields } from '../../../../common/fields'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; export const useExplorationResults = ( indexPattern: IndexPattern | undefined, @@ -48,7 +48,7 @@ export const useExplorationResults = ( // reduce default selected rows from 20 to 8 for performance reasons. 8, // by default, hide feature-importance columns and the doc id copy - d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== 'ml__id_copy' + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY ); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 5439356f81e91f..ad7613ae0e6f6e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -25,6 +25,7 @@ import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; import { FEATURE_INFLUENCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; import { getFeatureCount, getOutlierScoreFieldName } from './common'; @@ -42,12 +43,9 @@ export const useOutlierData = ( const resultsField = jobConfig.dest.results_field; const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); columns.push( - ...getDataGridSchemasFromFieldTypes( - fieldTypes, - resultsField - ) /* .sort((a: any, b: any) => - sortRegressionResultsFields(a, b, jobConfig) - )*/ + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) ); } @@ -57,7 +55,7 @@ export const useOutlierData = ( // reduce default selected rows from 20 to 8 for performance reasons. 8, // by default, hide feature-influence columns and the doc id copy - d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== 'ml__id_copy' + d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY ); // initialize sorting: reverse sort on outlier score column From 748cc886ed733f1e4e73cf007ee7b265b2d321a5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Apr 2020 13:33:11 +0200 Subject: [PATCH 31/34] [ML] Fix feature importance column. --- .../components/data_grid/common.ts | 42 ++++++++++++------- .../data_frame_analytics/common/constants.ts | 1 + .../data_frame_analytics/common/fields.ts | 14 +++---- .../use_exploration_results.ts | 5 ++- .../outlier_exploration/use_outlier_data.ts | 7 ++-- .../analytics_list/action_clone.tsx | 3 +- 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 70d1517697a0d4..bfc35be2edab00 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -5,9 +5,13 @@ */ import moment from 'moment-timezone'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; -import { EuiDataGridSorting, EuiDataGridStyle } from '@elastic/eui'; +import { + EuiDataGridCellValueElementProps, + EuiDataGridSorting, + EuiDataGridStyle, +} from '@elastic/eui'; import { IndexPattern, @@ -101,12 +105,15 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results if ( field === `${resultsField}.${OUTLIER_SCORE}` || - field === `${resultsField}.${FEATURE_INFLUENCE}` || - field === `${resultsField}.${FEATURE_IMPORTANCE}` + field.includes(`${resultsField}.${FEATURE_INFLUENCE}`) ) { schema = 'numeric'; } + if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) { + schema = 'json'; + } + return { id: field, schema, isSortable }; }); }; @@ -139,11 +146,12 @@ export const useRenderCellValue = ( indexPattern: IndexPattern | undefined, pagination: IndexPagination, tableItems: DataGridItem[], + resultsField: string, cellPropsCallback?: ( columnId: string, cellValue: any, fullItem: Record, - setCellProps: any + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] ) => void ): RenderCellValue => { const renderCellValue: RenderCellValue = useMemo(() => { @@ -154,7 +162,7 @@ export const useRenderCellValue = ( }: { rowIndex: number; columnId: string; - setCellProps: any; + setCellProps: EuiDataGridCellValueElementProps['setCellProps']; }) => { const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; @@ -175,10 +183,9 @@ export const useRenderCellValue = ( } function getCellValue(cId: string) { - if (cId.includes(`.${FEATURE_IMPORTANCE}.`)) { - const results = getNestedProperty(tableItems[adjustedRowIndex], 'ml', null); - const featureImportanceField = cId.replace('ml.', ''); - return results[featureImportanceField]; + if (cId.includes(`.${FEATURE_INFLUENCE}.`)) { + const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); + return results[cId.replace(`${resultsField}.`, '')]; } return tableItems.hasOwnProperty(adjustedRowIndex) @@ -188,6 +195,17 @@ export const useRenderCellValue = ( const cellValue = getCellValue(columnId); + // React by default doesn't all us to use a hook in a callback. + // However, this one will be passed on to EuiDataGrid and its docs + // recommend wrapping `setCellProps` in a `useEffect()` hook + // so we're ignoring the linting rule here. + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (typeof cellPropsCallback === 'function') { + cellPropsCallback(columnId, cellValue, fullItem, setCellProps); + } + }, [columnId, cellValue]); + if (typeof cellValue === 'object' && cellValue !== null) { return JSON.stringify(cellValue); } @@ -217,10 +235,6 @@ export const useRenderCellValue = ( return JSON.stringify(cellValue); } - if (typeof cellPropsCallback === 'function') { - cellPropsCallback(columnId, cellValue, fullItem, setCellProps); - } - return cellValue; }; }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts index 27e45a9ca36f15..51b2918012c8d9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const DEFAULT_RESULTS_FIELD = 'ml'; export const FEATURE_IMPORTANCE = 'feature_importance'; export const FEATURE_INFLUENCE = 'feature_influence'; export const OUTLIER_SCORE = 'outlier_score'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index bec27c90e53cef..8423bc1b94a096 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -214,15 +214,11 @@ export const getDefaultFieldsFromJobCaps = ( }`; if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { - featureImportanceFields.push( - ...fields - .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) - .map(d => ({ - id: `${resultsField}.${FEATURE_IMPORTANCE}.${d.id}`, - name: `${resultsField}.${FEATURE_IMPORTANCE}.${d.name}`, - type: KBN_FIELD_TYPES.NUMBER, - })) - ); + featureImportanceFields.push({ + id: `${resultsField}.${FEATURE_IMPORTANCE}`, + name: `${resultsField}.${FEATURE_IMPORTANCE}`, + type: KBN_FIELD_TYPES.UNKNOWN, + }); } // Only need to add these fields if we didn't use dest index pattern to get the fields diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index c74b9f5a0613af..6f9dc694d81724 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -19,7 +19,7 @@ import { import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; -import { FEATURE_IMPORTANCE } from '../../../../common/constants'; +import { DEFAULT_RESULTS_FIELD, FEATURE_IMPORTANCE } from '../../../../common/constants'; import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; export const useExplorationResults = ( @@ -60,7 +60,8 @@ export const useExplorationResults = ( const renderCellValue = useRenderCellValue( indexPattern, dataGrid.pagination, - dataGrid.tableItems + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD ); return { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index ad7613ae0e6f6e..fd316164dda97c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -24,7 +24,7 @@ import { import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; -import { FEATURE_INFLUENCE } from '../../../../common/constants'; +import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; import { getFeatureCount, getOutlierScoreFieldName } from './common'; @@ -81,6 +81,7 @@ export const useOutlierData = ( indexPattern, dataGrid.pagination, dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD, (columnId, cellValue, fullItem, setCellProps) => { const resultsField = jobConfig?.dest.results_field ?? ''; @@ -88,8 +89,8 @@ export const useOutlierData = ( let backgroundColor; // column with feature values get color coded by its corresponding influencer value - if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { - backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); + if (fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { + backgroundColor = colorRange(fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`]); } // column with influencer values get color coded by its own value diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index eb1871c98764b3..8c65af1d92959f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -214,7 +215,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, - defaultValue: 'ml', + defaultValue: DEFAULT_RESULTS_FIELD, }, }, model_memory_limit: { From da94eadfaced2f42edb9b3ded2a1c4eb7d89e098 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Apr 2020 15:13:50 +0200 Subject: [PATCH 32/34] [ML] Fix types. --- .../ml/public/application/components/data_grid/common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index bfc35be2edab00..d141b68b5d03fd 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -146,7 +146,7 @@ export const useRenderCellValue = ( indexPattern: IndexPattern | undefined, pagination: IndexPagination, tableItems: DataGridItem[], - resultsField: string, + resultsField?: string, cellPropsCallback?: ( columnId: string, cellValue: any, @@ -183,7 +183,7 @@ export const useRenderCellValue = ( } function getCellValue(cId: string) { - if (cId.includes(`.${FEATURE_INFLUENCE}.`)) { + if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) { const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); return results[cId.replace(`${resultsField}.`, '')]; } From 897ba46fa26d8032ba32f974a689cc4719f0f721 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Apr 2020 15:52:12 +0200 Subject: [PATCH 33/34] [ML] Fix attribute check. --- .../components/outlier_exploration/use_outlier_data.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index fd316164dda97c..0d06bc0d433079 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -89,7 +89,10 @@ export const useOutlierData = ( let backgroundColor; // column with feature values get color coded by its corresponding influencer value - if (fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { + if ( + fullItem[resultsField] !== undefined && + fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined + ) { backgroundColor = colorRange(fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`]); } From ae9eb8ff92728ff203a709bfaae186f40f25be3a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 24 Apr 2020 11:34:26 +0200 Subject: [PATCH 34/34] [ML] Delete duplicate code. --- .../exploration_results_table/exploration_results_table.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 9188d43b3627ad..24e5785c6e808e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -71,10 +71,6 @@ export const ExplorationResultsTable: FC = React.memo( const docFieldsCount = classificationData.columns.length; const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - if (jobConfig === undefined || classificationData === undefined) { return null; }