[ML] Transform: Fix use of saved search in pivot wizard. (#51079) (#51555)

Fixes applying saved searches in the transform wizard. Previously, upon initializing the transform wizard's state, we would miss passing on the initialized data from kibanaContext. The resulting bug was that saved search were not applied in the generated transform config and source preview table.
This commit is contained in:
Walter Rafelsberger 2019-11-24 10:13:51 +01:00 committed by GitHub
parent 65a60c9b64
commit 9eed629e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 850 additions and 349 deletions

View file

@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ProgressBar, MlInMemoryTable } from './ml_in_memory_table';
export { ProgressBar, mlInMemoryTableFactory } from './ml_in_memory_table';
export * from './types';

View file

@ -71,34 +71,38 @@ const getInitialSorting = (columns: any, sorting: any) => {
};
};
import { MlInMemoryTableBasic } from './types';
import { mlInMemoryTableBasicFactory } from './types';
export class MlInMemoryTable extends MlInMemoryTableBasic {
static getDerivedStateFromProps(nextProps: any, prevState: any) {
const derivedState = {
...prevState.prevProps,
pageIndex: nextProps.pagination.initialPageIndex,
pageSize: nextProps.pagination.initialPageSize,
};
export function mlInMemoryTableFactory<T>() {
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<T>();
if (nextProps.items !== prevState.prevProps.items) {
Object.assign(derivedState, {
prevProps: {
items: nextProps.items,
},
});
return class MlInMemoryTable extends MlInMemoryTableBasic {
static getDerivedStateFromProps(nextProps: any, prevState: any) {
const derivedState = {
...prevState.prevProps,
pageIndex: nextProps.pagination.initialPageIndex,
pageSize: nextProps.pagination.initialPageSize,
};
if (nextProps.items !== prevState.prevProps.items) {
Object.assign(derivedState, {
prevProps: {
items: nextProps.items,
},
});
}
const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting);
if (
sortName !== prevState.prevProps.sortName ||
sortDirection !== prevState.prevProps.sortDirection
) {
Object.assign(derivedState, {
sortName,
sortDirection,
});
}
return derivedState;
}
const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting);
if (
sortName !== prevState.prevProps.sortName ||
sortDirection !== prevState.prevProps.sortDirection
) {
Object.assign(derivedState, {
sortName,
sortDirection,
});
}
return derivedState;
}
};
}

View file

@ -8,24 +8,21 @@ 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 SortableFunc<T> = <T>(item: T) => any;
type Sortable<T> = boolean | SortableFunc<T>;
type DATA_TYPES = any;
type FooterFunc = (payload: { items: Item[]; pagination: any }) => ReactNode;
type FooterFunc = <T>(payload: { items: T[]; pagination: any }) => ReactNode;
type RenderFunc = (value: any, record?: any) => ReactNode;
export interface FieldDataColumnType {
export interface FieldDataColumnType<T> {
field: string;
name: ReactNode;
description?: string;
dataType?: DATA_TYPES;
width?: string;
sortable?: Sortable;
sortable?: Sortable<T>;
align?: HorizontalAlignment;
truncateText?: boolean;
render?: RenderFunc;
@ -34,38 +31,38 @@ export interface FieldDataColumnType {
'data-test-subj'?: string;
}
export interface ComputedColumnType {
export interface ComputedColumnType<T> {
render: RenderFunc;
name?: ReactNode;
description?: string;
sortable?: (item: Item) => any;
sortable?: (item: T) => any;
width?: string;
truncateText?: boolean;
'data-test-subj'?: string;
}
type ICON_TYPES = any;
type IconTypesFunc = (item: Item) => ICON_TYPES; // (item) => oneOf(ICON_TYPES)
type IconTypesFunc<T> = (item: T) => 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 ButtonIconColorsFunc<T> = (item: T) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS)
interface DefaultItemActionType<T> {
type?: 'icon' | 'button';
name: string;
description: string;
onClick?(item: Item): void;
onClick?(item: T): void;
href?: string;
target?: string;
available?(item: Item): boolean;
enabled?(item: Item): boolean;
available?(item: T): boolean;
enabled?(item: T): boolean;
isPrimary?: boolean;
icon?: ICON_TYPES | IconTypesFunc; // required when type is 'icon'
color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc;
icon?: ICON_TYPES | IconTypesFunc<T>; // required when type is 'icon'
color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc<T>;
}
interface CustomItemActionType {
render(item: Item, enabled: boolean): ReactNode;
available?(item: Item): boolean;
enabled?(item: Item): boolean;
interface CustomItemActionType<T> {
render(item: T, enabled: boolean): ReactNode;
available?(item: T): boolean;
enabled?(item: T): boolean;
isPrimary?: boolean;
}
@ -76,20 +73,20 @@ export interface ExpanderColumnType {
render: RenderFunc;
}
type SupportedItemActionType = DefaultItemActionType | CustomItemActionType;
type SupportedItemActionType<T> = DefaultItemActionType<T> | CustomItemActionType<T>;
export interface ActionsColumnType {
actions: SupportedItemActionType[];
export interface ActionsColumnType<T> {
actions: Array<SupportedItemActionType<T>>;
name?: ReactNode;
description?: string;
width?: string;
}
export type ColumnType =
| ActionsColumnType
| ComputedColumnType
export type ColumnType<T> =
| ActionsColumnType<T>
| ComputedColumnType<T>
| ExpanderColumnType
| FieldDataColumnType;
| FieldDataColumnType<T>;
type QueryType = any;
@ -161,17 +158,17 @@ export interface OnTableChangeArg extends Sorting {
page: { index: number; size: number };
}
type ItemIdTypeFunc = (item: Item) => string;
type ItemIdTypeFunc = <T>(item: T) => string;
type ItemIdType =
| string // the name of the item id property
| ItemIdTypeFunc;
export type EuiInMemoryTableProps = CommonProps & {
columns: ColumnType[];
export type EuiInMemoryTableProps<T> = CommonProps & {
columns: Array<ColumnType<T>>;
hasActions?: boolean;
isExpandable?: boolean;
isSelectable?: boolean;
items?: Item[];
items?: T[];
loading?: boolean;
message?: HTMLAttributes<HTMLDivElement>;
error?: string;
@ -184,16 +181,18 @@ export type EuiInMemoryTableProps = CommonProps & {
responsive?: boolean;
selection?: SelectionType;
itemId?: ItemIdType;
itemIdToExpandedRowMap?: Record<string, Item>;
rowProps?: (item: Item) => void | Record<string, any>;
itemIdToExpandedRowMap?: Record<string, JSX.Element>;
rowProps?: (item: T) => void | Record<string, any>;
cellProps?: () => void | Record<string, any>;
onTableChange?: (arg: OnTableChangeArg) => void;
};
interface ComponentWithConstructor<T> extends Component {
type EuiInMemoryTableType = typeof EuiInMemoryTable;
interface ComponentWithConstructor<T> extends EuiInMemoryTableType {
new (): Component<T>;
}
export const MlInMemoryTableBasic = (EuiInMemoryTable as any) as ComponentWithConstructor<
EuiInMemoryTableProps
>;
export function mlInMemoryTableBasicFactory<T>() {
return EuiInMemoryTable as ComponentWithConstructor<EuiInMemoryTableProps<T>>;
}

View file

@ -35,7 +35,7 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json';
import {
ColumnType,
MlInMemoryTableBasic,
mlInMemoryTableBasicFactory,
OnTableChangeArg,
SortingPropType,
SORT_DIRECTION,
@ -59,7 +59,7 @@ import {
} from '../../../../common';
import { getOutlierScoreFieldName } from './common';
import { useExploreData } from './use_explore_data';
import { useExploreData, TableItem } from './use_explore_data';
import {
DATA_FRAME_TASK_STATE,
Query as QueryType,
@ -167,7 +167,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
docFieldsCount = docFields.length;
}
const columns: ColumnType[] = [];
const columns: Array<ColumnType<TableItem>> = [];
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
// table cell color coding takes into account:
@ -188,7 +188,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
columns.push(
...selectedFields.sort(sortColumns(tableItems[0], jobConfig.dest.results_field)).map(k => {
const column: ColumnType = {
const column: ColumnType<TableItem> = {
field: k,
name: k,
sortable: true,
@ -425,6 +425,8 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
});
}
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>();
return (
<EuiPanel grow={false}>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>

View file

@ -27,7 +27,7 @@ import {
import { getOutlierScoreFieldName } from './common';
import { SavedSearchQuery } from '../../../../../contexts/kibana';
type TableItem = Record<string, any>;
export type TableItem = Record<string, any>;
interface LoadExploreDataArg {
field: string;

View file

@ -30,7 +30,7 @@ import { Query as QueryType } from '../../../analytics_management/components/ana
import {
ColumnType,
MlInMemoryTableBasic,
mlInMemoryTableBasicFactory,
OnTableChangeArg,
SortingPropType,
SORT_DIRECTION,
@ -55,7 +55,7 @@ import {
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';
import { useExploreData, TableItem } from './use_explore_data';
import { ExplorationTitle } from './regression_exploration';
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
@ -108,12 +108,12 @@ export const ResultsTable: FC<Props> = React.memo(
docFieldsCount = docFields.length;
}
const columns: ColumnType[] = [];
const columns: Array<ColumnType<TableItem>> = [];
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
columns.push(
...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
const column: ColumnType = {
const column: ColumnType<TableItem> = {
field: k,
name: k,
sortable: true,
@ -363,6 +363,8 @@ export const ResultsTable: FC<Props> = React.memo(
? errorMessage
: searchError;
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>();
return (
<EuiPanel grow={false}>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>

View file

@ -31,7 +31,7 @@ import {
SearchQuery,
} from '../../../../common';
type TableItem = Record<string, any>;
export type TableItem = Record<string, any>;
interface LoadExploreDataArg {
field: string;

View file

@ -34,7 +34,7 @@ import { getColumns } from './columns';
import { ExpandedRow } from './expanded_row';
import {
ProgressBar,
MlInMemoryTable,
mlInMemoryTableFactory,
OnTableChangeArg,
SortDirection,
SORT_DIRECTION,
@ -326,6 +326,8 @@ export const DataFrameAnalyticsList: FC<Props> = ({
setSortDirection(direction);
};
const MlInMemoryTable = mlInMemoryTableFactory<DataFrameAnalyticsListRow>();
return (
<Fragment>
<EuiFlexGroup justifyContent="spaceBetween">

View file

@ -8,7 +8,7 @@ import React, { FC, useState } from 'react';
import { EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
MlInMemoryTable,
mlInMemoryTableFactory,
SortDirection,
SORT_DIRECTION,
OnTableChangeArg,
@ -27,7 +27,7 @@ import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analyti
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
interface Props {
items: any[];
items: DataFrameAnalyticsListRow[];
}
export const AnalyticsTable: FC<Props> = ({ items }) => {
const [pageIndex, setPageIndex] = useState(0);
@ -37,7 +37,7 @@ export const AnalyticsTable: FC<Props> = ({ items }) => {
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
// id, type, status, progress, created time, view icon
const columns: ColumnType[] = [
const columns: Array<ColumnType<DataFrameAnalyticsListRow>> = [
{
field: DataFrameAnalyticsListColumn.id,
name: i18n.translate('xpack.ml.overview.analyticsList.id', { defaultMessage: 'ID' }),
@ -113,6 +113,8 @@ export const AnalyticsTable: FC<Props> = ({ items }) => {
},
};
const MlInMemoryTable = mlInMemoryTableFactory<DataFrameAnalyticsListRow>();
return (
<MlInMemoryTable
allowNeutralSort={false}

View file

@ -17,7 +17,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
MlInMemoryTable,
mlInMemoryTableFactory,
SortDirection,
SORT_DIRECTION,
OnTableChangeArg,
@ -59,7 +59,7 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, jobsList, statsBarData
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
// columns: group, max anomaly, jobs in group, latest timestamp, docs processed, action to explorer
const columns: ColumnType[] = [
const columns: Array<ColumnType<Group>> = [
{
field: AnomalyDetectionListColumns.id,
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableId', {
@ -195,6 +195,8 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, jobsList, statsBarData
},
};
const MlInMemoryTable = mlInMemoryTableFactory<Group>();
return (
<Fragment>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">

View file

@ -5,10 +5,12 @@
*/
export {
isKibanaContextInitialized,
useKibanaContext,
InitializedKibanaContextValue,
KibanaContext,
KibanaContextValue,
SavedSearchQuery,
RenderOnlyWithInitializedKibanaContext,
} from './kibana_context';
export { KibanaProvider } from './kibana_provider';
export { useCurrentIndexPattern } from './use_current_index_pattern';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createContext } from 'react';
import React, { createContext, useContext, FC } from 'react';
import {
IndexPattern as IndexPatternType,
@ -15,14 +15,15 @@ import { SavedSearch } from '../../../../../../../../src/legacy/core_plugins/kib
import { KibanaConfig } from '../../../../../../../../src/legacy/server/kbn_server';
// set() method is missing in original d.ts
export interface KibanaConfigTypeFix extends KibanaConfig {
interface KibanaConfigTypeFix extends KibanaConfig {
set(key: string, value: any): void;
}
interface UninitializedKibanaContextValue {
initialized: boolean;
}
interface InitializedKibanaContextValue {
export interface InitializedKibanaContextValue {
combinedQuery: any;
currentIndexPattern: IndexPatternType;
currentSavedSearch: SavedSearch;
@ -41,3 +42,37 @@ export function isKibanaContextInitialized(arg: any): arg is InitializedKibanaCo
export type SavedSearchQuery = object;
export const KibanaContext = createContext<KibanaContextValue>({ initialized: false });
/**
* Custom hook to get the current kibanaContext.
*
* @remarks
* This hook should only be used in components wrapped in `RenderOnlyWithInitializedKibanaContext`,
* otherwise it will throw an error when KibanaContext hasn't been initialized yet.
* In return you get the benefit of not having to check if it's been initialized in the component
* where it's used.
*
* @returns `kibanaContext`
*/
export const useKibanaContext = () => {
const kibanaContext = useContext(KibanaContext);
if (!isKibanaContextInitialized(kibanaContext)) {
throw new Error('useKibanaContext: kibanaContext not initialized');
}
return kibanaContext;
};
/**
* Wrapper component to render children only if `kibanaContext` has been initialized.
* In combination with `useKibanaContext` this avoids having to check for the initialization
* in consuming components.
*
* @returns `children` or `null` depending on whether `kibanaContext` is initialized or not.
*/
export const RenderOnlyWithInitializedKibanaContext: FC = ({ children }) => {
const kibanaContext = useContext(KibanaContext);
return isKibanaContextInitialized(kibanaContext) ? <>{children}</> : null;
};

View file

@ -12,7 +12,7 @@ export const useCurrentIndexPattern = () => {
const context = useContext(KibanaContext);
if (!isKibanaContextInitialized(context)) {
throw new Error('currentIndexPattern is undefined');
throw new Error('useCurrentIndexPattern: kibanaContext not initialized');
}
return context.currentIndexPattern;

View file

@ -31,7 +31,7 @@ import {
import {
ColumnType,
MlInMemoryTableBasic,
mlInMemoryTableBasicFactory,
SortingPropType,
SORT_DIRECTION,
} from '../../../../../shared_imports';
@ -183,8 +183,8 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ cellClick, quer
docFieldsCount = docFields.length;
}
const columns: ColumnType[] = selectedFields.map(k => {
const column: ColumnType = {
const columns: Array<ColumnType<EsDoc>> = selectedFields.map(k => {
const column: ColumnType<EsDoc> = {
field: `_source["${k}"]`,
name: k,
sortable: true,
@ -319,6 +319,8 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ cellClick, quer
defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.',
});
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<EsDoc>();
return (
<EuiPanel grow={false} data-test-subj="transformSourceIndexPreview loaded">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
@ -410,6 +412,9 @@ export const SourceIndexPreview: React.FC<Props> = React.memo(({ cellClick, quer
itemId="_id"
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isExpandable={true}
rowProps={item => ({
'data-test-subj': `transformSourceIndexPreviewRow row-${item._id}`,
})}
sorting={sorting}
/>
)}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useContext, useEffect, useState } from 'react';
import React, { Fragment, FC, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
@ -32,7 +32,7 @@ import {
import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { ToastNotificationText } from '../../../../components';
import { useApi } from '../../../../hooks/use_api';
import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana';
import { useKibanaContext } from '../../../../lib/kibana';
import { RedirectToTransformManagement } from '../../../../common/navigation';
import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants';
@ -73,7 +73,7 @@ export const StepCreateForm: FC<Props> = React.memo(
undefined
);
const kibanaContext = useContext(KibanaContext);
const kibanaContext = useKibanaContext();
useEffect(() => {
onChange({ created, started, indexPatternId });
@ -83,10 +83,6 @@ export const StepCreateForm: FC<Props> = React.memo(
const api = useApi();
if (!isKibanaContextInitialized(kibanaContext)) {
return null;
}
async function createTransform() {
setCreated(true);
@ -151,8 +147,8 @@ export const StepCreateForm: FC<Props> = React.memo(
}
async function createAndStartTransform() {
const success = await createTransform();
if (success) {
const acknowledged = await createTransform();
if (acknowledged) {
await startTransform();
}
}

View file

@ -21,7 +21,11 @@ import {
EuiTitle,
} from '@elastic/eui';
import { ColumnType, MlInMemoryTableBasic, SORT_DIRECTION } from '../../../../../shared_imports';
import {
ColumnType,
mlInMemoryTableBasicFactory,
SORT_DIRECTION,
} from '../../../../../shared_imports';
import { dictionaryToArray } from '../../../../../../common/types/common';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils';
@ -38,7 +42,7 @@ import {
} from '../../../../common';
import { getPivotPreviewDevConsoleStatement } from './common';
import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data';
import { PreviewItem, PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data';
function sortColumns(groupByArr: PivotGroupByConfig[]) {
return (a: string, b: string) => {
@ -210,7 +214,7 @@ export const PivotPreview: FC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
columnKeys.sort(sortColumns(groupByArr));
const columns = columnKeys.map(k => {
const column: ColumnType = {
const column: ColumnType<PreviewItem> = {
field: k,
name: k,
sortable: true,
@ -256,6 +260,8 @@ export const PivotPreview: FC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
},
};
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<PreviewItem>();
return (
<EuiPanel data-test-subj="transformPivotPreview loaded">
<PreviewTitle previewRequest={previewRequest} />
@ -273,6 +279,9 @@ export const PivotPreview: FC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
initialPageSize: 5,
pageSizeOptions: [5, 10, 25],
}}
rowProps={() => ({
'data-test-subj': 'transformPivotPreviewRow',
})}
sorting={sorting}
/>
)}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useContext, useEffect, useState } from 'react';
import React, { Fragment, FC, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
@ -37,9 +37,8 @@ import { KqlFilterBar } from '../../../../../shared_imports';
import { SwitchModal } from './switch_modal';
import {
isKibanaContextInitialized,
KibanaContext,
KibanaContextValue,
useKibanaContext,
InitializedKibanaContextValue,
SavedSearchQuery,
} from '../../../../lib/kibana';
@ -75,7 +74,7 @@ const defaultSearch = '*';
const emptySearch = '';
export function getDefaultStepDefineState(
kibanaContext: KibanaContextValue
kibanaContext: InitializedKibanaContextValue
): StepDefineExposedState {
return {
aggList: {} as PivotAggsConfigDict,
@ -83,13 +82,9 @@ export function getDefaultStepDefineState(
isAdvancedPivotEditorEnabled: false,
isAdvancedSourceEditorEnabled: false,
searchString:
isKibanaContextInitialized(kibanaContext) && kibanaContext.currentSavedSearch !== undefined
? kibanaContext.combinedQuery
: defaultSearch,
kibanaContext.currentSavedSearch !== undefined ? kibanaContext.combinedQuery : defaultSearch,
searchQuery:
isKibanaContextInitialized(kibanaContext) && kibanaContext.currentSavedSearch !== undefined
? kibanaContext.combinedQuery
: defaultSearch,
kibanaContext.currentSavedSearch !== undefined ? kibanaContext.combinedQuery : defaultSearch,
sourceConfigUpdated: false,
valid: false,
};
@ -196,7 +191,7 @@ interface Props {
}
export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange }) => {
const kibanaContext = useContext(KibanaContext);
const kibanaContext = useKibanaContext();
const defaults = { ...getDefaultStepDefineState(kibanaContext), ...overrides };
@ -224,10 +219,6 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange
// The list of selected group by fields
const [groupByList, setGroupByList] = useState(defaults.groupByList);
if (!isKibanaContextInitialized(kibanaContext)) {
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
const {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext, Fragment, FC } from 'react';
import React, { Fragment, FC } from 'react';
import { i18n } from '@kbn/i18n';
@ -17,7 +17,7 @@ import {
EuiText,
} from '@elastic/eui';
import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana';
import { useKibanaContext } from '../../../../lib/kibana';
import { AggListSummary } from '../aggregation_list';
import { GroupByListSummary } from '../group_by_list';
@ -35,11 +35,7 @@ export const StepDefineSummary: FC<StepDefineExposedState> = ({
groupByList,
aggList,
}) => {
const kibanaContext = useContext(KibanaContext);
if (!isKibanaContextInitialized(kibanaContext)) {
return null;
}
const kibanaContext = useKibanaContext();
const pivotQuery = getPivotQuery(searchQuery);
let useCodeBlock = false;

View file

@ -32,7 +32,8 @@ interface EsMappingType {
type: ES_FIELD_TYPES;
}
type PreviewData = Array<Dictionary<any>>;
export type PreviewItem = Dictionary<any>;
type PreviewData = PreviewItem[];
interface PreviewMappings {
properties: Dictionary<EsMappingType>;
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useContext, useEffect, useState } from 'react';
import React, { Fragment, FC, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { metadata } from 'ui/metadata';
@ -13,7 +13,7 @@ import { toastNotifications } from 'ui/notify';
import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana';
import { useKibanaContext } from '../../../../lib/kibana';
import { isValidIndexName } from '../../../../../../common/utils/es_utils';
import { ToastNotificationText } from '../../../../components';
@ -55,7 +55,7 @@ interface Props {
}
export const StepDetailsForm: FC<Props> = React.memo(({ overrides = {}, onChange }) => {
const kibanaContext = useContext(KibanaContext);
const kibanaContext = useKibanaContext();
const defaults = { ...getDefaultStepDetailsState(), ...overrides };
@ -80,56 +80,47 @@ export const StepDetailsForm: FC<Props> = React.memo(({ overrides = {}, onChange
useEffect(() => {
// use an IIFE to avoid returning a Promise to useEffect.
(async function() {
if (isKibanaContextInitialized(kibanaContext)) {
try {
setTransformIds(
(await api.getTransforms()).transforms.map(
(transform: TransformPivotConfig) => transform.id
)
);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', {
defaultMessage: 'An error occurred getting the existing transform IDs:',
}),
text: toMountPoint(<ToastNotificationText text={e} />),
});
}
try {
setTransformIds(
(await api.getTransforms()).transforms.map(
(transform: TransformPivotConfig) => transform.id
)
);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', {
defaultMessage: 'An error occurred getting the existing transform IDs:',
}),
text: toMountPoint(<ToastNotificationText text={e} />),
});
}
try {
setIndexNames((await api.getIndices()).map(index => index.name));
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
defaultMessage: 'An error occurred getting the existing index names:',
}),
text: toMountPoint(<ToastNotificationText text={e} />),
});
}
try {
setIndexNames((await api.getIndices()).map(index => index.name));
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
defaultMessage: 'An error occurred getting the existing index names:',
}),
text: toMountPoint(<ToastNotificationText text={e} />),
});
}
try {
setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles());
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles',
{
defaultMessage: 'An error occurred getting the existing index pattern titles:',
}
),
text: toMountPoint(<ToastNotificationText text={e} />),
});
}
try {
setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles());
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles', {
defaultMessage: 'An error occurred getting the existing index pattern titles:',
}),
text: toMountPoint(<ToastNotificationText text={e} />),
});
}
})();
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [kibanaContext.initialized]);
if (!isKibanaContextInitialized(kibanaContext)) {
return null;
}
const dateFieldNames = kibanaContext.currentIndexPattern.fields
.filter(f => f.type === 'date')
.map(f => f.name)

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useContext, useEffect, useRef, useState } from 'react';
import React, { Fragment, FC, useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSteps, EuiStepStatus } from '@elastic/eui';
import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana';
import { useKibanaContext } from '../../../../lib/kibana';
import { getCreateRequestBody } from '../../../../common';
@ -68,7 +68,7 @@ const StepDefine: FC<DefinePivotStepProps> = ({
};
export const Wizard: FC = React.memo(() => {
const kibanaContext = useContext(KibanaContext);
const kibanaContext = useKibanaContext();
// The current WIZARD_STEP
const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE);
@ -108,11 +108,6 @@ export const Wizard: FC = React.memo(() => {
}
}, []);
if (!isKibanaContextInitialized(kibanaContext)) {
// TODO proper loading indicator
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
const transformConfig = getCreateRequestBody(
@ -134,18 +129,6 @@ export const Wizard: FC = React.memo(() => {
<StepCreateSummary />
);
// scroll to the currently selected wizard step
/*
function scrollToRef() {
if (definePivotRef !== null && definePivotRef.current !== null) {
// TODO Fix types
const dummy = definePivotRef as any;
const headerOffset = 70;
window.scrollTo(0, dummy.current.offsetTop - headerOffset);
}
}
*/
const stepsConfig = [
{
title: i18n.translate('xpack.transform.transformsWizard.stepDefineTitle', {
@ -171,7 +154,6 @@ export const Wizard: FC = React.memo(() => {
<WizardNav
previous={() => {
setCurrentStep(WIZARD_STEPS.DEFINE);
// scrollToRef();
}}
next={() => setCurrentStep(WIZARD_STEPS.CREATE)}
nextActive={stepDetailsState.valid}

View file

@ -26,7 +26,7 @@ import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/cons
import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation';
import { documentationLinksService } from '../../services/documentation';
import { PrivilegesWrapper } from '../../lib/authorization';
import { KibanaProvider } from '../../lib/kibana';
import { KibanaProvider, RenderOnlyWithInitializedKibanaContext } from '../../lib/kibana';
import { Wizard } from './components/wizard';
@ -82,7 +82,9 @@ export const CreateTransformSection: FC<Props> = ({ match }) => {
</EuiTitle>
<EuiPageContentBody>
<EuiSpacer size="l" />
<Wizard />
<RenderOnlyWithInitializedKibanaContext>
<Wizard />
</RenderOnlyWithInitializedKibanaContext>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>

View file

@ -90,7 +90,8 @@ exports[`Transform: Transform List <ExpandedRow /> Minimal initialization 1`] =
]
}
/>,
"id": "transform-details",
"data-test-subj": "transformDetailsTab",
"id": "transform-details-tab-fq_date_histogram_1m_1441",
"name": "Transform details",
}
}
@ -188,7 +189,8 @@ exports[`Transform: Transform List <ExpandedRow /> Minimal initialization 1`] =
]
}
/>,
"id": "transform-details",
"data-test-subj": "transformDetailsTab",
"id": "transform-details-tab-fq_date_histogram_1m_1441",
"name": "Transform details",
},
Object {
@ -229,14 +231,16 @@ exports[`Transform: Transform List <ExpandedRow /> Minimal initialization 1`] =
}
}
/>,
"id": "transform-json",
"data-test-subj": "transformJsonTab",
"id": "transform-json-tab-fq_date_histogram_1m_1441",
"name": "JSON",
},
Object {
"content": <ExpandedRowMessagesPane
transformId="fq_date_histogram_1m_1441"
/>,
"id": "transform-messages",
"data-test-subj": "transformMessagesTab",
"id": "transform-messages-tab-fq_date_histogram_1m_1441",
"name": "Messages",
},
Object {
@ -277,7 +281,8 @@ exports[`Transform: Transform List <ExpandedRow /> Minimal initialization 1`] =
}
}
/>,
"id": "transform-preview",
"data-test-subj": "transformPreviewTab",
"id": "transform-preview-tab-fq_date_histogram_1m_1441",
"name": "Preview",
},
]

View file

@ -1,40 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transform: Job List Expanded Row <ExpandedRowDetailsPane /> Minimal initialization 1`] = `
<EuiFlexGroup>
<EuiFlexItem
style={
Object {
"width": "50%",
}
}
>
<EuiSpacer
size="s"
/>
<Section
section={
<div
data-test-subj="transformDetailsTabContent"
>
<EuiFlexGroup>
<EuiFlexItem
style={
Object {
"items": Array [
Object {
"description": "the-item-description",
"title": "the-item-title",
},
],
"position": "left",
"title": "the-section-title",
"width": "50%",
}
}
>
<EuiSpacer
size="s"
/>
<Section
section={
Object {
"items": Array [
Object {
"description": "the-item-description",
"title": "the-item-title",
},
],
"position": "left",
"title": "the-section-title",
}
}
/>
</EuiFlexItem>
<EuiFlexItem
style={
Object {
"width": "50%",
}
}
/>
</EuiFlexItem>
<EuiFlexItem
style={
Object {
"width": "50%",
}
}
/>
</EuiFlexGroup>
</EuiFlexGroup>
</div>
`;
exports[`Transform: Job List Expanded Row <Section /> Minimal initialization 1`] = `

View file

@ -1,22 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transform: Transform List Expanded Row <ExpandedRowJsonPane /> Minimal initialization 1`] = `
<EuiFlexGroup>
<EuiFlexItem>
<EuiSpacer
size="s"
/>
<EuiCodeEditor
mode="json"
readOnly={true}
setOptions={Object {}}
style={
Object {
"width": "100%",
<div
data-test-subj="transformJsonTabContent"
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiSpacer
size="s"
/>
<EuiCodeEditor
mode="json"
readOnly={true}
setOptions={Object {}}
style={
Object {
"width": "100%",
}
}
}
theme="textmate"
value="{
theme="textmate"
value="{
\\"id\\": \\"fq_date_histogram_1m_1441\\",
\\"source\\": {
\\"index\\": [
@ -49,12 +52,13 @@ exports[`Transform: Transform List Expanded Row <ExpandedRowJsonPane /> Minimal
\\"version\\": \\"8.0.0\\",
\\"create_time\\": 1564388146667
}"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
 
</EuiFlexItem>
</EuiFlexGroup>
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
 
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;

View file

@ -88,14 +88,14 @@ export const getColumns = (
const columns: [
ExpanderColumnType,
FieldDataColumnType,
FieldDataColumnType,
FieldDataColumnType,
FieldDataColumnType,
ComputedColumnType,
ComputedColumnType,
ComputedColumnType,
ActionsColumnType
FieldDataColumnType<TransformListRow>,
FieldDataColumnType<TransformListRow>,
FieldDataColumnType<TransformListRow>,
FieldDataColumnType<TransformListRow>,
ComputedColumnType<TransformListRow>,
ComputedColumnType<TransformListRow>,
ComputedColumnType<TransformListRow>,
ActionsColumnType<TransformListRow>
] = [
{
align: RIGHT_ALIGNMENT,

View file

@ -121,7 +121,8 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
const tabs = [
{
id: 'transform-details',
id: `transform-details-tab-${item.id}`,
'data-test-subj': 'transformDetailsTab',
name: i18n.translate(
'xpack.transform.transformList.transformDetails.tabs.transformSettingsLabel',
{
@ -131,12 +132,14 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
content: <ExpandedRowDetailsPane sections={[state, checkpointing, stats]} />,
},
{
id: 'transform-json',
id: `transform-json-tab-${item.id}`,
'data-test-subj': 'transformJsonTab',
name: 'JSON',
content: <ExpandedRowJsonPane json={item.config} />,
},
{
id: 'transform-messages',
id: `transform-messages-tab-${item.id}`,
'data-test-subj': 'transformMessagesTab',
name: i18n.translate(
'xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel',
{
@ -146,7 +149,8 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
content: <ExpandedRowMessagesPane transformId={item.id} />,
},
{
id: 'transform-preview',
id: `transform-preview-tab-${item.id}`,
'data-test-subj': 'transformPreviewTab',
name: i18n.translate(
'xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel',
{

View file

@ -50,27 +50,29 @@ interface ExpandedRowDetailsPaneProps {
export const ExpandedRowDetailsPane: FC<ExpandedRowDetailsPaneProps> = ({ sections }) => {
return (
<EuiFlexGroup>
<EuiFlexItem style={{ width: '50%' }}>
{sections
.filter(s => s.position === 'left')
.map(s => (
<Fragment key={s.title}>
<EuiSpacer size="s" />
<Section section={s} />
</Fragment>
))}
</EuiFlexItem>
<EuiFlexItem style={{ width: '50%' }}>
{sections
.filter(s => s.position === 'right')
.map(s => (
<Fragment key={s.title}>
<EuiSpacer size="s" />
<Section section={s} />
</Fragment>
))}
</EuiFlexItem>
</EuiFlexGroup>
<div data-test-subj="transformDetailsTabContent">
<EuiFlexGroup>
<EuiFlexItem style={{ width: '50%' }}>
{sections
.filter(s => s.position === 'left')
.map(s => (
<Fragment key={s.title}>
<EuiSpacer size="s" />
<Section section={s} />
</Fragment>
))}
</EuiFlexItem>
<EuiFlexItem style={{ width: '50%' }}>
{sections
.filter(s => s.position === 'right')
.map(s => (
<Fragment key={s.title}>
<EuiSpacer size="s" />
<Section section={s} />
</Fragment>
))}
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -20,18 +20,20 @@ interface Props {
export const ExpandedRowJsonPane: FC<Props> = ({ json }) => {
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiCodeEditor
value={JSON.stringify(json, null, 2)}
readOnly={true}
mode="json"
style={{ width: '100%' }}
theme="textmate"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>&nbsp;</EuiFlexItem>
</EuiFlexGroup>
<div data-test-subj="transformJsonTabContent">
<EuiFlexGroup>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiCodeEditor
value={JSON.stringify(json, null, 2)}
readOnly={true}
mode="json"
style={{ width: '100%' }}
theme="textmate"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>&nbsp;</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useState } from 'react';
import React, { useState } from 'react';
import { EuiSpacer, EuiBasicTable } from '@elastic/eui';
// @ts-ignore
@ -143,7 +143,7 @@ export const ExpandedRowMessagesPane: React.FC<Props> = ({ transformId }) => {
};
return (
<Fragment>
<div data-test-subj="transformMessagesTabContent">
<EuiSpacer size="s" />
<EuiBasicTable
className="transform__TransformTable__messagesPaneTable"
@ -155,6 +155,6 @@ export const ExpandedRowMessagesPane: React.FC<Props> = ({ transformId }) => {
pagination={pagination}
onChange={onChange}
/>
</Fragment>
</div>
);
};

View file

@ -14,12 +14,13 @@ import { useApi } from '../../../../hooks/use_api';
import {
getFlattenedFields,
useRefreshTransformList,
EsDoc,
PreviewRequestBody,
TransformPivotConfig,
} from '../../../../common';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils';
import { TransformTable } from './transform_table';
import { transformTableFactory } from './transform_table';
interface Props {
transformConfig: TransformPivotConfig;
@ -45,12 +46,14 @@ function getDataFromTransform(
transformConfig: TransformPivotConfig
): { previewRequest: PreviewRequestBody; groupByArr: string[] | [] } {
const index = transformConfig.source.index;
const query = transformConfig.source.query;
const pivot = transformConfig.pivot;
const groupByArr = [];
const previewRequest: PreviewRequestBody = {
source: {
index,
query,
},
pivot,
};
@ -67,8 +70,8 @@ function getDataFromTransform(
}
export const ExpandedRowPreviewPane: FC<Props> = ({ transformConfig }) => {
const [previewData, setPreviewData] = useState([]);
const [columns, setColumns] = useState<FieldDataColumnType[] | []>([]);
const [previewData, setPreviewData] = useState<EsDoc[]>([]);
const [columns, setColumns] = useState<Array<FieldDataColumnType<EsDoc>> | []>([]);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState<string>('');
@ -97,8 +100,8 @@ export const ExpandedRowPreviewPane: FC<Props> = ({ transformConfig }) => {
const columnKeys = getFlattenedFields(resp.preview[0]);
columnKeys.sort(sortColumns(groupByArr));
const tableColumns: FieldDataColumnType[] = columnKeys.map(k => {
const column: FieldDataColumnType = {
const tableColumns: Array<FieldDataColumnType<EsDoc>> = columnKeys.map(k => {
const column: FieldDataColumnType<EsDoc> = {
field: k,
name: k,
sortable: true,
@ -191,17 +194,27 @@ export const ExpandedRowPreviewPane: FC<Props> = ({ transformConfig }) => {
setSortDirection(direction);
};
const transformTableLoading = previewData.length === 0 && isLoading === true;
const dataTestSubj = `transformPreviewTabContent${!transformTableLoading ? ' loaded' : ''}`;
const TransformTable = transformTableFactory<EsDoc>();
return (
<TransformTable
allowNeutralSort={false}
loading={previewData.length === 0 && isLoading === true}
compressed
items={previewData}
columns={columns}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
error={errorMessage}
/>
<div data-test-subj={dataTestSubj}>
<TransformTable
allowNeutralSort={false}
loading={transformTableLoading}
compressed
items={previewData}
columns={columns}
onTableChange={onTableChange}
pagination={pagination}
rowProps={() => ({
'data-test-subj': 'transformPreviewTabContentRow',
})}
sorting={sorting}
error={errorMessage}
/>
</div>
);
};

View file

@ -42,7 +42,7 @@ import { StopAction } from './action_stop';
import { ItemIdToExpandedRowMap, Query, Clause } from './common';
import { getColumns } from './columns';
import { ExpandedRow } from './expanded_row';
import { ProgressBar, TransformTable } from './transform_table';
import { ProgressBar, transformTableFactory } from './transform_table';
function getItemIdToExpandedRowMap(
itemIds: TransformId[],
@ -374,6 +374,8 @@ export const TransformList: FC<Props> = ({
onSelectionChange: (selected: TransformListRow[]) => setTransformSelection(selected),
};
const TransformTable = transformTableFactory<TransformListRow>();
return (
<div data-test-subj="transformListTableContainer">
<ProgressBar isLoading={isLoading || transformsLoading} />

View file

@ -11,7 +11,7 @@ import React, { Fragment } from 'react';
import { EuiProgress } from '@elastic/eui';
import { MlInMemoryTableBasic } from '../../../../../shared_imports';
import { mlInMemoryTableBasicFactory } from '../../../../../shared_imports';
// 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,32 +73,35 @@ const getInitialSorting = (columns: any, sorting: any) => {
};
};
export class TransformTable extends MlInMemoryTableBasic {
static getDerivedStateFromProps(nextProps: any, prevState: any) {
const derivedState = {
...prevState.prevProps,
pageIndex: nextProps.pagination.initialPageIndex,
pageSize: nextProps.pagination.initialPageSize,
};
export function transformTableFactory<T>() {
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<T>();
return class TransformTable extends MlInMemoryTableBasic {
static getDerivedStateFromProps(nextProps: any, prevState: any) {
const derivedState = {
...prevState.prevProps,
pageIndex: nextProps.pagination.initialPageIndex,
pageSize: nextProps.pagination.initialPageSize,
};
if (nextProps.items !== prevState.prevProps.items) {
Object.assign(derivedState, {
prevProps: {
items: nextProps.items,
},
});
}
if (nextProps.items !== prevState.prevProps.items) {
Object.assign(derivedState, {
prevProps: {
items: nextProps.items,
},
});
}
const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting);
if (
sortName !== prevState.prevProps.sortName ||
sortDirection !== prevState.prevProps.sortDirection
) {
Object.assign(derivedState, {
sortName,
sortDirection,
});
const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting);
if (
sortName !== prevState.prevProps.sortName ||
sortDirection !== prevState.prevProps.sortDirection
) {
Object.assign(derivedState, {
sortName,
sortDirection,
});
}
return derivedState;
}
return derivedState;
}
};
}

View file

@ -25,7 +25,7 @@ export {
ExpanderColumnType,
FieldDataColumnType,
ColumnType,
MlInMemoryTableBasic,
mlInMemoryTableBasicFactory,
OnTableChangeArg,
SortingPropType,
SortDirection,

View file

@ -17,7 +17,7 @@ export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const transform = getService('transform');
describe('creation', function() {
describe('creation_index_pattern', function() {
this.tags(['smoke']);
before(async () => {
await esArchiver.load('ml/ecommerce');
@ -56,11 +56,19 @@ export default function({ getService }: FtrProviderContext) {
return `dest_${this.transformId}`;
},
expected: {
pivotPreview: {
column: 0,
values: [`Men's Accessories`],
},
row: {
status: 'stopped',
mode: 'batch',
progress: '100',
},
sourcePreview: {
columns: 6,
rows: 5,
},
},
},
];
@ -96,6 +104,13 @@ export default function({ getService }: FtrProviderContext) {
await transform.wizard.assertSourceIndexPreviewLoaded();
});
it('shows the source index preview', async () => {
await transform.wizard.assertSourceIndexPreview(
testData.expected.sourcePreview.columns,
testData.expected.sourcePreview.rows
);
});
it('displays an empty pivot preview', async () => {
await transform.wizard.assertPivotPreviewEmpty();
});
@ -140,6 +155,13 @@ export default function({ getService }: FtrProviderContext) {
await transform.wizard.assertPivotPreviewLoaded();
});
it('shows the pivot preview', async () => {
await transform.wizard.assertPivotPreviewColumnValues(
testData.expected.pivotPreview.column,
testData.expected.pivotPreview.values
);
});
it('loads the details step', async () => {
await transform.wizard.advanceToDetailsStep();
});

View file

@ -0,0 +1,256 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
interface GroupByEntry {
identifier: string;
label: string;
intervalLabel?: string;
}
export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const transform = getService('transform');
describe('creation_saved_search', function() {
this.tags(['smoke']);
before(async () => {
await esArchiver.load('ml/farequote');
});
// after(async () => {
// await esArchiver.unload('ml/farequote');
// await transform.api.cleanTransformIndices();
// });
const testDataList = [
{
suiteTitle: 'batch transform with terms groups and avg agg with saved search filter',
source: 'farequote_filter',
groupByEntries: [
{
identifier: 'terms(airline)',
label: 'airline',
} as GroupByEntry,
],
aggregationEntries: [
{
identifier: 'avg(responsetime)',
label: 'responsetime.avg',
},
],
transformId: `fq_1_${Date.now()}`,
transformDescription:
'farequote batch transform with groups terms(airline) and aggregation avg(responsetime.avg) with saved search filter',
get destinationIndex(): string {
return `dest_${this.transformId}`;
},
expected: {
pivotPreview: {
column: 0,
values: ['ASA'],
},
row: {
status: 'stopped',
mode: 'batch',
progress: '100',
},
sourceIndex: 'farequote',
sourcePreview: {
column: 3,
values: ['ASA'],
},
},
},
];
for (const testData of testDataList) {
describe(`${testData.suiteTitle}`, function() {
after(async () => {
await transform.api.deleteIndices(testData.destinationIndex);
});
it('loads the home page', async () => {
await transform.navigation.navigateTo();
await transform.management.assertTransformListPageExists();
});
it('displays the stats bar', async () => {
await transform.management.assertTransformStatsBarExists();
});
it('loads the source selection modal', async () => {
await transform.management.startTransformCreation();
});
it('selects the source data', async () => {
await transform.sourceSelection.selectSource(testData.source);
});
it('displays the define pivot step', async () => {
await transform.wizard.assertDefineStepActive();
});
it('loads the source index preview', async () => {
await transform.wizard.assertSourceIndexPreviewLoaded();
});
it('shows the filtered source index preview', async () => {
await transform.wizard.assertSourceIndexPreviewColumnValues(
testData.expected.sourcePreview.column,
testData.expected.sourcePreview.values
);
});
it('displays an empty pivot preview', async () => {
await transform.wizard.assertPivotPreviewEmpty();
});
it('hides the query input', async () => {
await transform.wizard.assertQueryInputMissing();
});
it('hides the advanced query editor switch', async () => {
await transform.wizard.assertAdvancedQueryEditorSwitchMissing();
});
it('adds the group by entries', async () => {
for (const [index, entry] of testData.groupByEntries.entries()) {
await transform.wizard.assertGroupByInputExists();
await transform.wizard.assertGroupByInputValue([]);
await transform.wizard.addGroupByEntry(
index,
entry.identifier,
entry.label,
entry.intervalLabel
);
}
});
it('adds the aggregation entries', async () => {
for (const [index, agg] of testData.aggregationEntries.entries()) {
await transform.wizard.assertAggregationInputExists();
await transform.wizard.assertAggregationInputValue([]);
await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label);
}
});
it('displays the advanced pivot editor switch', async () => {
await transform.wizard.assertAdvancedPivotEditorSwitchExists();
await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false);
});
it('loads the pivot preview', async () => {
await transform.wizard.assertPivotPreviewLoaded();
});
it('shows the pivot preview', async () => {
await transform.wizard.assertPivotPreviewColumnValues(
testData.expected.pivotPreview.column,
testData.expected.pivotPreview.values
);
});
it('loads the details step', async () => {
await transform.wizard.advanceToDetailsStep();
});
it('inputs the transform id', async () => {
await transform.wizard.assertTransformIdInputExists();
await transform.wizard.assertTransformIdValue('');
await transform.wizard.setTransformId(testData.transformId);
});
it('inputs the transform description', async () => {
await transform.wizard.assertTransformDescriptionInputExists();
await transform.wizard.assertTransformDescriptionValue('');
await transform.wizard.setTransformDescription(testData.transformDescription);
});
it('inputs the destination index', async () => {
await transform.wizard.assertDestinationIndexInputExists();
await transform.wizard.assertDestinationIndexValue('');
await transform.wizard.setDestinationIndex(testData.destinationIndex);
});
it('displays the create index pattern switch', async () => {
await transform.wizard.assertCreateIndexPatternSwitchExists();
await transform.wizard.assertCreateIndexPatternSwitchCheckState(true);
});
it('displays the continuous mode switch', async () => {
await transform.wizard.assertContinuousModeSwitchExists();
await transform.wizard.assertContinuousModeSwitchCheckState(false);
});
it('loads the create step', async () => {
await transform.wizard.advanceToCreateStep();
});
it('displays the create and start button', async () => {
await transform.wizard.assertCreateAndStartButtonExists();
});
it('displays the create button', async () => {
await transform.wizard.assertCreateButtonExists();
});
it('displays the copy to clipboard button', async () => {
await transform.wizard.assertCreateAndStartButtonExists();
});
it('creates the transform', async () => {
await transform.wizard.createTransform();
});
it('starts the transform and finishes processing', async () => {
await transform.wizard.startTransform();
await transform.wizard.waitForProgressBarComplete();
});
it('returns to the management page', async () => {
await transform.wizard.returnToManagement();
});
it('displays the transforms table', async () => {
await transform.management.assertTransformsTableExists();
});
it('displays the created transform in the transform list', async () => {
await transform.table.refreshTransformList();
await transform.table.filterWithSearchString(testData.transformId);
const rows = await transform.table.parseTransformTable();
expect(rows.filter(row => row.id === testData.transformId)).to.have.length(1);
});
it('job creation displays details for the created job in the job list', async () => {
await transform.table.assertTransformRowFields(testData.transformId, {
id: testData.transformId,
description: testData.transformDescription,
sourceIndex: testData.expected.sourceIndex,
destinationIndex: testData.destinationIndex,
status: testData.expected.row.status,
mode: testData.expected.row.mode,
progress: testData.expected.row.progress,
});
});
it('expands the transform management table row and walks through available tabs', async () => {
await transform.table.assertTransformExpandedRow();
});
it('displays the transform preview in the expanded row', async () => {
await transform.table.assertTransformsExpandedRowPreviewColumnValues(
testData.expected.pivotPreview.column,
testData.expected.pivotPreview.values
);
});
});
}
});
}

View file

@ -9,6 +9,7 @@ export default function({ loadTestFile }: FtrProviderContext) {
describe('transform', function() {
this.tags(['ciGroup9', 'transform']);
loadTestFile(require.resolve('./creation'));
loadTestFile(require.resolve('./creation_index_pattern'));
loadTestFile(require.resolve('./creation_saved_search'));
});
}

View file

@ -8,6 +8,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export function TransformTableProvider({ getService }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
return new (class TransformTable {
@ -60,6 +61,51 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
return rows;
}
async parseEuiInMemoryTable(tableSubj: string) {
const table = await testSubjects.find(`~${tableSubj}`);
const $ = await table.parseDomContent();
const rows = [];
// For each row, get the content of each cell and
// add its values as an array to each row.
for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) {
rows.push(
$(tr)
.find('.euiTableCellContent')
.toArray()
.map(cell =>
$(cell)
.text()
.trim()
)
);
}
return rows;
}
async assertEuiInMemoryTableColumnValues(
tableSubj: string,
column: number,
expectedColumnValues: string[]
) {
await retry.tryForTime(2000, async () => {
// get a 2D array of rows and cell values
const rows = await this.parseEuiInMemoryTable(tableSubj);
// reduce the rows data to an array of unique values in the specified column
const uniqueColumnValues = rows
.map(row => row[column])
.flat()
.filter((v, i, a) => a.indexOf(v) === i);
uniqueColumnValues.sort();
// check if the returned unique value matches the supplied filter value
expect(uniqueColumnValues).to.eql(expectedColumnValues);
});
}
public async refreshTransformList() {
await testSubjects.click('transformRefreshTransformListButton');
await this.waitForTransformsToLoad();
@ -83,5 +129,36 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
const transformRow = rows.filter(row => row.id === transformId)[0];
expect(transformRow).to.eql(expectedRow);
}
public async assertTransformExpandedRow() {
await testSubjects.click('transformListRowDetailsToggle');
// The expanded row should show the details tab content by default
await testSubjects.existOrFail('transformDetailsTab');
await testSubjects.existOrFail('~transformDetailsTabContent');
// Walk through the rest of the tabs and check if the corresponding content shows up
await testSubjects.existOrFail('transformJsonTab');
await testSubjects.click('transformJsonTab');
await testSubjects.existOrFail('~transformJsonTabContent');
await testSubjects.existOrFail('transformMessagesTab');
await testSubjects.click('transformMessagesTab');
await testSubjects.existOrFail('~transformMessagesTabContent');
await testSubjects.existOrFail('transformPreviewTab');
await testSubjects.click('transformPreviewTab');
await testSubjects.existOrFail('~transformPreviewTabContent');
}
public async waitForTransformsExpandedRowPreviewTabToLoad() {
await testSubjects.existOrFail('~transformPreviewTabContent', { timeout: 60 * 1000 });
await testSubjects.existOrFail('transformPreviewTabContent loaded', { timeout: 30 * 1000 });
}
async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) {
await this.waitForTransformsExpandedRowPreviewTabToLoad();
await this.assertEuiInMemoryTableColumnValues('transformPreviewTabContent', column, values);
}
})();
}

View file

@ -75,6 +75,81 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) {
await testSubjects.existOrFail(selector);
},
async parseEuiInMemoryTable(tableSubj: string) {
const table = await testSubjects.find(`~${tableSubj}`);
const $ = await table.parseDomContent();
const rows = [];
// For each row, get the content of each cell and
// add its values as an array to each row.
for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) {
rows.push(
$(tr)
.find('.euiTableCellContent')
.toArray()
.map(cell =>
$(cell)
.text()
.trim()
)
);
}
return rows;
},
async assertEuiInMemoryTableColumnValues(
tableSubj: string,
column: number,
expectedColumnValues: string[]
) {
await retry.tryForTime(2000, async () => {
// get a 2D array of rows and cell values
const rows = await this.parseEuiInMemoryTable(tableSubj);
// reduce the rows data to an array of unique values in the specified column
const uniqueColumnValues = rows
.map(row => row[column])
.flat()
.filter((v, i, a) => a.indexOf(v) === i);
uniqueColumnValues.sort();
// check if the returned unique value matches the supplied filter value
expect(uniqueColumnValues).to.eql(
expectedColumnValues,
`Unique EuiInMemoryTable column values should be '${expectedColumnValues.join()}' (got ${uniqueColumnValues.join()})`
);
});
},
async assertSourceIndexPreview(columns: number, rows: number) {
await retry.tryForTime(2000, async () => {
// get a 2D array of rows and cell values
const rowsData = await this.parseEuiInMemoryTable('transformSourceIndexPreview');
expect(rowsData).to.length(
rows,
`EuiInMemoryTable rows should be ${rows} (got ${rowsData.length})`
);
rowsData.map((r, i) =>
expect(r).to.length(
columns,
`EuiInMemoryTable row #${i + 1} column count should be ${columns} (got ${r.length})`
)
);
});
},
async assertSourceIndexPreviewColumnValues(column: number, values: string[]) {
await this.assertEuiInMemoryTableColumnValues('transformSourceIndexPreview', column, values);
},
async assertPivotPreviewColumnValues(column: number, values: string[]) {
await this.assertEuiInMemoryTableColumnValues('transformPivotPreview', column, values);
},
async assertPivotPreviewLoaded() {
await this.assertPivotPreviewExists('loaded');
},
@ -87,6 +162,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) {
await testSubjects.existOrFail('tarnsformQueryInput');
},
async assertQueryInputMissing() {
await testSubjects.missingOrFail('tarnsformQueryInput');
},
async assertQueryValue(expectedQuery: string) {
const actualQuery = await testSubjects.getVisibleText('tarnsformQueryInput');
expect(actualQuery).to.eql(
@ -99,6 +178,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) {
await testSubjects.existOrFail(`transformAdvancedQueryEditorSwitch`, { allowHidden: true });
},
async assertAdvancedQueryEditorSwitchMissing() {
await testSubjects.missingOrFail(`transformAdvancedQueryEditorSwitch`);
},
async assertAdvancedQueryEditorSwitchCheckState(expectedCheckState: boolean) {
const actualCheckState =
(await testSubjects.getAttribute('transformAdvancedQueryEditorSwitch', 'aria-checked')) ===