[ML] Data frames: Fixes table sorting. (#43859)

- EuiInMemoryTable will not correctly reflect prop updates like sorting. So for example, when the component gets mounted with sorting={false} it will never consider a later update to make sorting available after all data is loaded. This PR fixes it by mounting the component only once sorting was set properly. This affected all data frame analytics/transform tables.
- This consolidates code where we had multiple custom type definitions for EuiInMemoryTable because it's not based on TypeScript itself yet. The PR adds TypeScript Prop definitions for
the component in ml/common/types/eui/in_memory_table.ts based on React propTypes and exposes a MlInMemoryTable component that wraps EuiInMemoryTable. I'll be in contact with the EUI team so they can make use of this for EUI itself.
This commit is contained in:
Walter Rafelsberger 2019-08-27 02:22:53 -07:00 committed by GitHub
parent b7bba213db
commit 0772e19346
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 84 deletions

View file

@ -0,0 +1,184 @@
/*
* 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 { Component, HTMLAttributes, ReactElement, ReactNode } from 'react';
import { CommonProps, EuiInMemoryTable } from '@elastic/eui';
// At some point this could maybe solved with a generic <T>.
type Item = any;
// Not using an enum here because the original HorizontalAlignment is also a union type of string.
type HorizontalAlignment = 'left' | 'center' | 'right';
type SortableFunc = (item: Item) => any;
type Sortable = boolean | SortableFunc;
type DATA_TYPES = any;
type FooterFunc = (payload: { items: Item[]; pagination: any }) => ReactNode;
type RenderFunc = (value: any, record?: any) => ReactNode;
export interface FieldDataColumnType {
field: string;
name: ReactNode;
description?: string;
dataType?: DATA_TYPES;
width?: string;
sortable?: Sortable;
align?: HorizontalAlignment;
truncateText?: boolean;
render?: RenderFunc;
footer?: string | ReactElement | FooterFunc;
}
export interface ComputedColumnType {
render: RenderFunc;
name?: ReactNode;
description?: string;
sortable?: (item: Item) => any;
width?: string;
truncateText?: boolean;
}
type ICON_TYPES = any;
type IconTypesFunc = (item: Item) => ICON_TYPES; // (item) => oneOf(ICON_TYPES)
type BUTTON_ICON_COLORS = any;
type ButtonIconColorsFunc = (item: Item) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS)
interface DefaultItemActionType {
type?: 'icon' | 'button';
name: string;
description: string;
onClick?(item: Item): void;
href?: string;
target?: string;
available?(item: Item): boolean;
enabled?(item: Item): boolean;
isPrimary?: boolean;
icon?: ICON_TYPES | IconTypesFunc; // required when type is 'icon'
color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc;
}
interface CustomItemActionType {
render(item: Item, enabled: boolean): ReactNode;
available?(item: Item): boolean;
enabled?(item: Item): boolean;
isPrimary?: boolean;
}
export interface ExpanderColumnType {
align?: HorizontalAlignment;
width?: string;
isExpander: boolean;
render: RenderFunc;
}
type SupportedItemActionType = DefaultItemActionType | CustomItemActionType;
export interface ActionsColumnType {
actions: SupportedItemActionType[];
name?: ReactNode;
description?: string;
width?: string;
}
export type ColumnType =
| ActionsColumnType
| ComputedColumnType
| ExpanderColumnType
| FieldDataColumnType;
type QueryType = any;
interface Schema {
strict?: boolean;
fields?: Record<string, any>;
flags?: string[];
}
interface SearchBoxConfigPropTypes {
placeholder?: string;
incremental?: boolean;
schema?: Schema;
}
interface Box {
placeholder?: string;
incremental?: boolean;
// here we enable the user to just assign 'true' to the schema, in which case
// we will auto-generate it out of the columns configuration
schema?: boolean | SearchBoxConfigPropTypes['schema'];
}
type SearchFiltersFiltersType = any;
interface ExecuteQueryOptions {
defaultFields: string[];
isClauseMatcher: () => void;
explain: boolean;
}
type SearchType =
| boolean
| {
toolsLeft?: ReactNode;
toolsRight?: ReactNode;
defaultQuery?: QueryType;
box?: Box;
filters?: SearchFiltersFiltersType;
onChange?: (arg: any) => void;
executeQueryOptions?: ExecuteQueryOptions;
};
interface PageSizeOptions {
pageSizeOptions: number[];
}
interface InitialPageOptions extends PageSizeOptions {
initialPageIndex: number;
initialPageSize: number;
}
type Pagination = boolean | PageSizeOptions | InitialPageOptions;
type PropertySortType = any;
type Sorting = boolean | { sort: PropertySortType };
type SelectionType = any;
type ItemIdTypeFunc = (item: Item) => string;
type ItemIdType =
| string // the name of the item id property
| ItemIdTypeFunc;
export type EuiInMemoryTableProps = CommonProps & {
columns: ColumnType[];
hasActions?: boolean;
isExpandable?: boolean;
isSelectable?: boolean;
items?: Item[];
loading?: boolean;
message?: HTMLAttributes<HTMLDivElement>;
error?: string;
compressed?: boolean;
search?: SearchType;
pagination?: Pagination;
sorting?: Sorting;
// Set `allowNeutralSort` to false to force column sorting. Defaults to true.
allowNeutralSort?: boolean;
selection?: SelectionType;
itemId?: ItemIdType;
itemIdToExpandedRowMap?: Record<string, Item>;
rowProps?: () => void | Record<string, any>;
cellProps?: () => void | Record<string, any>;
onTableChange?: (arg: {
page: { index: number; size: number };
sort: { field: string; direction: string };
}) => void;
};
interface ComponentWithConstructor<T> extends Component {
new (): Component<T>;
}
export const MlInMemoryTable = (EuiInMemoryTable as any) as ComponentWithConstructor<
EuiInMemoryTableProps
>;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent, useState } from 'react';
import React, { useState } from 'react';
import moment from 'moment-timezone';
import { i18n } from '@kbn/i18n';
@ -18,8 +18,6 @@ import {
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiInMemoryTableProps,
EuiPanel,
EuiPopover,
EuiPopoverTitle,
@ -30,14 +28,7 @@ import {
RIGHT_ALIGNMENT,
} from '@elastic/eui';
// TODO EUI's types for EuiInMemoryTable is missing these props
interface ExpandableTableProps extends EuiInMemoryTableProps {
compressed: boolean;
itemIdToExpandedRowMap: ItemIdToExpandedRowMap;
isExpandable: boolean;
}
const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<ExpandableTableProps>;
import { ColumnType, MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
import { KBN_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { Dictionary } from '../../../../../../common/types/common';
@ -204,13 +195,13 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
docFieldsCount = docFields.length;
}
const columns = selectedFields.map(k => {
const column = {
const columns: ColumnType[] = selectedFields.map(k => {
const column: ColumnType = {
field: `_source["${k}"]`,
name: k,
sortable: true,
truncateText: true,
} as Dictionary<any>;
};
const field = indexPattern.fields.find(f => f.name === k);
@ -315,7 +306,7 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
if (columns.length > 0) {
sorting = {
sort: {
field: columns[0].field,
field: `_source["${selectedFields[0]}"]`,
direction: SORT_DIRECTON.ASC,
},
};
@ -425,8 +416,9 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
{status !== SOURCE_INDEX_STATUS.LOADING && (
<EuiProgress size="xs" color="accent" max={1} value={0} />
)}
{clearTable === false && (
<ExpandableTable
{clearTable === false && columns.length > 0 && sorting !== false && (
<MlInMemoryTable
allowNeutralSort={false}
compressed
items={tableItems}
columns={columns}

View file

@ -15,8 +15,6 @@ import {
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiInMemoryTableProps,
EuiPanel,
EuiProgress,
EuiText,
@ -24,7 +22,8 @@ import {
SortDirection,
} from '@elastic/eui';
import { Dictionary, dictionaryToArray } from '../../../../../../common/types/common';
import { ColumnType, MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
import { dictionaryToArray } from '../../../../../../common/types/common';
import { ES_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
@ -42,13 +41,6 @@ import {
import { getPivotPreviewDevConsoleStatement } from './common';
import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data';
// TODO EUI's types for EuiInMemoryTable is missing these props
interface CompressedTableProps extends EuiInMemoryTableProps {
compressed: boolean;
}
const CompressedTable = (EuiInMemoryTable as any) as SFC<CompressedTableProps>;
function sortColumns(groupByArr: PivotGroupByConfig[]) {
return (a: string, b: string) => {
// make sure groupBy fields are always most left columns
@ -237,7 +229,7 @@ export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
const columns = columnKeys
.filter(k => typeof dataFramePreviewMappings.properties[k] !== 'undefined')
.map(k => {
const column: Dictionary<any> = {
const column: ColumnType = {
field: k,
name: k,
sortable: true,
@ -290,8 +282,9 @@ export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
{status !== PIVOT_PREVIEW_STATUS.LOADING && (
<EuiProgress size="xs" color="accent" max={1} value={0} />
)}
{dataFramePreviewData.length > 0 && clearTable === false && (
<CompressedTable
{dataFramePreviewData.length > 0 && clearTable === false && columns.length > 0 && (
<MlInMemoryTable
allowNeutralSort={false}
compressed
items={dataFramePreviewData}
columns={columns}

View file

@ -17,6 +17,13 @@ import {
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import {
ActionsColumnType,
ComputedColumnType,
ExpanderColumnType,
FieldDataColumnType,
} from '../../../../../../common/types/eui/in_memory_table';
import { DataFrameTransformId } from '../../../../common';
import {
getTransformProgress,
@ -79,7 +86,17 @@ export const getColumns = (
setExpandedRowItemIds([...expandedRowItemIds]);
}
return [
const columns: [
ExpanderColumnType,
FieldDataColumnType,
FieldDataColumnType,
FieldDataColumnType,
FieldDataColumnType,
ComputedColumnType,
ComputedColumnType,
ComputedColumnType,
ActionsColumnType
] = [
{
align: RIGHT_ALIGNMENT,
width: '40px',
@ -205,4 +222,6 @@ export const getColumns = (
width: '200px',
},
];
return columns;
};

View file

@ -164,11 +164,12 @@ export const ExpandedRowPreviewPane: FC<Props> = ({ transformConfig }) => {
return (
<TransformTable
allowNeutralSort={false}
loading={dataFramePreviewData.length === 0 && isLoading === true}
compressed
items={dataFramePreviewData}
columns={columns}
onChange={onTableChange}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
error={errorMessage}

View file

@ -370,6 +370,7 @@ export const DataFrameTransformList: SFC = () => {
<Fragment>
<ProgressBar isLoading={isLoading} />
<TransformTable
allowNeutralSort={false}
className="mlTransformTable"
columns={columns}
error={searchError}
@ -379,7 +380,7 @@ export const DataFrameTransformList: SFC = () => {
items={filterActive ? filteredTransforms : transforms}
itemId={DataFrameTransformListColumn.id}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onTableChange}
onTableChange={onTableChange}
pagination={pagination}
selection={selection}
sorting={sorting}

View file

@ -7,11 +7,11 @@
// This component extends EuiInMemoryTable with some
// fixes and TS specs until the changes become available upstream.
import React, { Component, Fragment } from 'react';
import React, { Fragment } from 'react';
import { EuiInMemoryTable, EuiInMemoryTableProps, EuiProgress } from '@elastic/eui';
import { EuiProgress } from '@elastic/eui';
import { ItemIdToExpandedRowMap } from './common';
import { MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement
// of the table and doesn't play well with auto-refreshing. That's why we're displaying
@ -73,21 +73,7 @@ const getInitialSorting = (columns: any, sorting: any) => {
};
};
// TODO EUI's types for EuiInMemoryTable is missing these props
interface ExpandableTableProps extends EuiInMemoryTableProps {
itemIdToExpandedRowMap?: ItemIdToExpandedRowMap;
isExpandable?: boolean;
onChange({ page }: { page?: {} | undefined }): void;
loading?: boolean;
compressed?: boolean;
error?: string;
}
interface ComponentWithConstructor<T> extends Component {
new (): Component<T>;
}
const ExpandableTable = (EuiInMemoryTable as any) as ComponentWithConstructor<ExpandableTableProps>;
export class TransformTable extends ExpandableTable {
export class TransformTable extends MlInMemoryTable {
static getDerivedStateFromProps(nextProps: any, prevState: any) {
const derivedState = {
...prevState.prevProps,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, FunctionComponent, useEffect, useState } from 'react';
import React, { FC, useEffect, useState } from 'react';
import moment from 'moment-timezone';
import { i18n } from '@kbn/i18n';
@ -18,8 +18,6 @@ import {
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiInMemoryTableProps,
EuiPanel,
EuiPopover,
EuiPopoverTitle,
@ -32,12 +30,7 @@ import {
import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';
import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json';
// TODO EUI's types for EuiInMemoryTable is missing these props
interface ExpandableTableProps extends EuiInMemoryTableProps {
compressed: boolean;
}
const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<ExpandableTableProps>;
import { ColumnType, MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context';
@ -218,7 +211,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId }) => {
docFieldsCount = docFields.length;
}
const columns = [];
const columns: ColumnType[] = [];
if (selectedFields.length > 0 && tableItems.length > 0) {
// table cell color coding takes into account:
@ -241,12 +234,12 @@ export const Exploration: FC<Props> = React.memo(({ jobId }) => {
...selectedFields
.sort(sortColumns(tableItems[0]._source, jobConfig.dest.results_field))
.map(k => {
const column = {
const column: ColumnType = {
field: `_source["${k}"]`,
name: k,
sortable: true,
truncateText: true,
} as Record<string, any>;
};
const render = (d: any, fullItem: EsDoc) => {
if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
@ -367,7 +360,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId }) => {
if (columns.length > 0) {
sorting = {
sort: {
field: columns[0].field,
field: `_source["${selectedFields[0]}"]`,
direction: SORT_DIRECTON.ASC,
},
};
@ -443,7 +436,8 @@ export const Exploration: FC<Props> = React.memo(({ jobId }) => {
<EuiProgress size="xs" color="accent" max={1} value={0} />
)}
{clearTable === false && columns.length > 0 && (
<ExpandableTable
<MlInMemoryTable
allowNeutralSort={false}
className="mlDataFrameAnalyticsExploration"
columns={columns}
compressed

View file

@ -310,6 +310,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
<Fragment>
<ProgressBar isLoading={isLoading} />
<AnalyticsTable
allowNeutralSort={false}
className="mlAnalyticsTable"
columns={columns}
error={searchError}
@ -319,7 +320,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
items={filterActive ? filteredAnalytics : analytics}
itemId={DataFrameAnalyticsListColumn.id}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onTableChange}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
search={search}

View file

@ -7,11 +7,9 @@
// This component extends EuiInMemoryTable with some
// fixes and TS specs until the changes become available upstream.
import React, { Component, Fragment } from 'react';
import React, { Fragment } from 'react';
import { EuiInMemoryTable, EuiInMemoryTableProps, EuiProgress } from '@elastic/eui';
import { ItemIdToExpandedRowMap } from './common';
import { EuiProgress } from '@elastic/eui';
// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement
// of the table and doesn't play well with auto-refreshing. That's why we're displaying
@ -73,21 +71,9 @@ const getInitialSorting = (columns: any, sorting: any) => {
};
};
// TODO EUI's types for EuiInMemoryTable is missing these props
interface ExpandableTableProps extends EuiInMemoryTableProps {
itemIdToExpandedRowMap?: ItemIdToExpandedRowMap;
isExpandable?: boolean;
onChange({ page }: { page?: {} | undefined }): void;
loading?: boolean;
compressed?: boolean;
error?: string;
}
interface ComponentWithConstructor<T> extends Component {
new (): Component<T>;
}
const ExpandableTable = (EuiInMemoryTable as any) as ComponentWithConstructor<ExpandableTableProps>;
import { MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
export class AnalyticsTable extends ExpandableTable {
export class AnalyticsTable extends MlInMemoryTable {
static getDerivedStateFromProps(nextProps: any, prevState: any) {
const derivedState = {
...prevState.prevProps,