[7.5] [ML] Add tests to create transforms (#49760) (#52080)

* [ML] Add tests to create transforms (#49760)

* Apply #51066 to backport

* Adjust eui switch handling for 7.5
This commit is contained in:
Robert Oskamp 2019-12-04 11:24:06 +01:00 committed by GitHub
parent aef43e72a7
commit 785807f676
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1715 additions and 705 deletions

View file

@ -178,6 +178,7 @@ export class FilterBar extends Component {
onClick={this.onClickInput}
autoComplete="off"
spellCheck={false}
data-test-subj={this.props.testSubj}
/>
{this.props.isLoading && (
@ -213,12 +214,14 @@ FilterBar.propTypes = {
placeholder: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
valueExternal: PropTypes.string,
suggestions: PropTypes.array.isRequired
suggestions: PropTypes.array.isRequired,
testSubj: PropTypes.string,
};
FilterBar.defaultProps = {
isLoading: false,
disabled: false,
placeholder: 'tag : engineering OR tag : marketing',
suggestions: []
suggestions: [],
testSubj: undefined,
};

View file

@ -90,7 +90,7 @@ export class KqlFilterBar extends Component {
render() {
const { error } = this.state;
const { initialValue, placeholder, valueExternal } = this.props;
const { initialValue, placeholder, valueExternal, testSubj } = this.props;
return (
<Fragment>
@ -103,6 +103,7 @@ export class KqlFilterBar extends Component {
onSubmit={this.onSubmit}
suggestions={this.state.suggestions}
valueExternal={valueExternal}
testSubj={testSubj}
/>
{ error &&
<EuiCallOut color="danger">
@ -118,6 +119,7 @@ KqlFilterBar.propTypes = {
initialValue: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
placeholder: PropTypes.string,
valueExternal: PropTypes.string
valueExternal: PropTypes.string,
testSubj: PropTypes.string,
};

View file

@ -31,6 +31,7 @@ export interface FieldDataColumnType {
render?: RenderFunc;
footer?: string | ReactElement | FooterFunc;
textOnly?: boolean;
'data-test-subj'?: string;
}
export interface ComputedColumnType {
@ -40,6 +41,7 @@ export interface ComputedColumnType {
sortable?: (item: Item) => any;
width?: string;
truncateText?: boolean;
'data-test-subj'?: string;
}
type ICON_TYPES = any;
@ -183,7 +185,7 @@ export type EuiInMemoryTableProps = CommonProps & {
selection?: SelectionType;
itemId?: ItemIdType;
itemIdToExpandedRowMap?: Record<string, Item>;
rowProps?: () => void | Record<string, any>;
rowProps?: (item: Item) => void | Record<string, any>;
cellProps?: () => void | Record<string, any>;
onTableChange?: (arg: OnTableChangeArg) => void;
};

View file

@ -12,12 +12,14 @@ interface Props {
options: EuiComboBoxOptionProps[];
placeholder?: string;
changeHandler(d: EuiComboBoxOptionProps[]): void;
testSubj?: string;
}
export const DropDown: React.SFC<Props> = ({
changeHandler,
options,
placeholder = 'Search ...',
testSubj,
}) => {
return (
<EuiComboBox
@ -27,6 +29,7 @@ export const DropDown: React.SFC<Props> = ({
selectedOptions={[]}
onChange={changeHandler}
isClearable={false}
data-test-subj={testSubj}
/>
);
};

View file

@ -11,6 +11,7 @@ exports[`Transform: <AggLabelForm /> Date histogram aggregation 1`] = `
>
<span
className="eui-textTruncate"
data-test-subj="transformAggregationEntryLabel"
>
the-group-by-agg-name
</span>
@ -24,6 +25,7 @@ exports[`Transform: <AggLabelForm /> Date histogram aggregation 1`] = `
button={
<EuiButtonIcon
aria-label="Edit aggregation"
data-test-subj="transformAggregationEntryEditButton"
iconType="pencil"
onClick={[Function]}
size="s"
@ -58,6 +60,7 @@ exports[`Transform: <AggLabelForm /> Date histogram aggregation 1`] = `
>
<EuiButtonIcon
aria-label="Delete item"
data-test-subj="transformAggregationEntryDeleteButton"
iconType="cross"
onClick={[Function]}
size="s"

View file

@ -3,6 +3,7 @@
exports[`Transform: <AggListForm /> Minimal initialization 1`] = `
<Fragment>
<EuiPanel
data-test-subj="transformAggregationEntry 0"
paddingSize="s"
>
<AggLabelForm

View file

@ -39,7 +39,9 @@ export const AggLabelForm: React.SFC<Props> = ({
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem className="transform__AggregationLabel--text">
<span className="eui-textTruncate">{item.aggName}</span>
<span className="eui-textTruncate" data-test-subj="transformAggregationEntryLabel">
{item.aggName}
</span>
</EuiFlexItem>
<EuiFlexItem grow={false} className="transform__GroupByLabel--button">
<EuiPopover
@ -53,6 +55,7 @@ export const AggLabelForm: React.SFC<Props> = ({
size="s"
iconType="pencil"
onClick={() => setPopoverVisibility(!isPopoverVisible)}
data-test-subj="transformAggregationEntryEditButton"
/>
}
isOpen={isPopoverVisible}
@ -74,6 +77,7 @@ export const AggLabelForm: React.SFC<Props> = ({
size="s"
iconType="cross"
onClick={() => deleteHandler(item.aggName)}
data-test-subj="transformAggregationEntryDeleteButton"
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -33,11 +33,11 @@ export const AggListForm: React.SFC<AggListProps> = ({
const listKeys = Object.keys(list);
return (
<Fragment>
{listKeys.map((aggName: AggName) => {
{listKeys.map((aggName: AggName, i) => {
const otherAggNames = listKeys.filter(k => k !== aggName);
return (
<Fragment key={aggName}>
<EuiPanel paddingSize="s">
<EuiPanel paddingSize="s" data-test-subj={`transformAggregationEntry ${i}`}>
<AggLabelForm
deleteHandler={deleteHandler}
item={list[aggName]}

View file

@ -11,6 +11,7 @@ exports[`Transform: <GroupByLabelForm /> Date histogram aggregation 1`] = `
>
<span
className="eui-textTruncate"
data-test-subj="transformGroupByEntryLabel"
>
the-group-by-agg-name
</span>
@ -22,6 +23,7 @@ exports[`Transform: <GroupByLabelForm /> Date histogram aggregation 1`] = `
<EuiTextColor
className="eui-textTruncate"
color="subdued"
data-test-subj="transformGroupByEntryIntervalLabel"
>
1m
</EuiTextColor>
@ -35,6 +37,7 @@ exports[`Transform: <GroupByLabelForm /> Date histogram aggregation 1`] = `
button={
<EuiButtonIcon
aria-label="Edit interval"
data-test-subj="transformGroupByEntryEditButton"
iconType="pencil"
onClick={[Function]}
size="s"
@ -70,6 +73,7 @@ exports[`Transform: <GroupByLabelForm /> Date histogram aggregation 1`] = `
>
<EuiButtonIcon
aria-label="Delete item"
data-test-subj="transformGroupByEntryDeleteButton"
iconType="cross"
onClick={[Function]}
size="s"
@ -89,6 +93,7 @@ exports[`Transform: <GroupByLabelForm /> Histogram aggregation 1`] = `
>
<span
className="eui-textTruncate"
data-test-subj="transformGroupByEntryLabel"
>
the-group-by-agg-name
</span>
@ -100,6 +105,7 @@ exports[`Transform: <GroupByLabelForm /> Histogram aggregation 1`] = `
<EuiTextColor
className="eui-textTruncate"
color="subdued"
data-test-subj="transformGroupByEntryIntervalLabel"
>
100
</EuiTextColor>
@ -113,6 +119,7 @@ exports[`Transform: <GroupByLabelForm /> Histogram aggregation 1`] = `
button={
<EuiButtonIcon
aria-label="Edit interval"
data-test-subj="transformGroupByEntryEditButton"
iconType="pencil"
onClick={[Function]}
size="s"
@ -148,6 +155,7 @@ exports[`Transform: <GroupByLabelForm /> Histogram aggregation 1`] = `
>
<EuiButtonIcon
aria-label="Delete item"
data-test-subj="transformGroupByEntryDeleteButton"
iconType="cross"
onClick={[Function]}
size="s"
@ -167,6 +175,7 @@ exports[`Transform: <GroupByLabelForm /> Terms aggregation 1`] = `
>
<span
className="eui-textTruncate"
data-test-subj="transformGroupByEntryLabel"
>
the-group-by-agg-name
</span>
@ -180,6 +189,7 @@ exports[`Transform: <GroupByLabelForm /> Terms aggregation 1`] = `
button={
<EuiButtonIcon
aria-label="Edit interval"
data-test-subj="transformGroupByEntryEditButton"
iconType="pencil"
onClick={[Function]}
size="s"
@ -214,6 +224,7 @@ exports[`Transform: <GroupByLabelForm /> Terms aggregation 1`] = `
>
<EuiButtonIcon
aria-label="Delete item"
data-test-subj="transformGroupByEntryDeleteButton"
iconType="cross"
onClick={[Function]}
size="s"

View file

@ -3,6 +3,7 @@
exports[`Transform: <GroupByListForm /> Minimal initialization 1`] = `
<Fragment>
<EuiPanel
data-test-subj="transformGroupByEntry 0"
paddingSize="s"
>
<GroupByLabelForm

View file

@ -53,14 +53,20 @@ export const GroupByLabelForm: React.SFC<Props> = ({
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem className="transform__GroupByLabel--text">
<span className="eui-textTruncate">{item.aggName}</span>
<span className="eui-textTruncate" data-test-subj="transformGroupByEntryLabel">
{item.aggName}
</span>
</EuiFlexItem>
{interval !== undefined && (
<EuiFlexItem
grow={false}
className="transform__GroupByLabel--text transform__GroupByLabel--interval"
>
<EuiTextColor color="subdued" className="eui-textTruncate">
<EuiTextColor
color="subdued"
className="eui-textTruncate"
data-test-subj="transformGroupByEntryIntervalLabel"
>
{interval}
</EuiTextColor>
</EuiFlexItem>
@ -77,6 +83,7 @@ export const GroupByLabelForm: React.SFC<Props> = ({
size="s"
iconType="pencil"
onClick={() => setPopoverVisibility(!isPopoverVisible)}
data-test-subj="transformGroupByEntryEditButton"
/>
}
isOpen={isPopoverVisible}
@ -98,6 +105,7 @@ export const GroupByLabelForm: React.SFC<Props> = ({
size="s"
iconType="cross"
onClick={() => deleteHandler(item.aggName)}
data-test-subj="transformGroupByEntryDeleteButton"
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -33,11 +33,11 @@ export const GroupByListForm: React.SFC<ListProps> = ({
const listKeys = Object.keys(list);
return (
<Fragment>
{listKeys.map((aggName: AggName) => {
{listKeys.map((aggName: AggName, i) => {
const otherAggNames = listKeys.filter(k => k !== aggName);
return (
<Fragment key={aggName}>
<EuiPanel paddingSize="s">
<EuiPanel paddingSize="s" data-test-subj={`transformGroupByEntry ${i}`}>
<GroupByLabelForm
deleteHandler={deleteHandler}
item={list[aggName]}

View file

@ -134,7 +134,7 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
if (status === SOURCE_INDEX_STATUS.ERROR) {
return (
<EuiPanel grow={false}>
<EuiPanel grow={false} data-test-subj="transformSourceIndexPreview error">
<SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} />
<EuiCallOut
title={i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternError', {
@ -153,7 +153,7 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
if (status === SOURCE_INDEX_STATUS.LOADED && tableItems.length === 0) {
return (
<EuiPanel grow={false}>
<EuiPanel grow={false} data-test-subj="transformSourceIndexPreview empty">
<SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} />
<EuiCallOut
title={i18n.translate(
@ -320,7 +320,7 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
});
return (
<EuiPanel grow={false}>
<EuiPanel grow={false} data-test-subj="transformSourceIndexPreview loaded">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} />

View file

@ -257,169 +257,197 @@ export const StepCreateForm: SFC<Props> = React.memo(
}
return (
<EuiForm>
{!created && (
<div data-test-subj="transformStepCreateForm">
<EuiForm>
{!created && (
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiButton
fill
isDisabled={created && started}
onClick={createAndStartTransform}
data-test-subj="transformWizardCreateAndStartButton"
>
{i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', {
defaultMessage: 'Create and start',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate(
'xpack.transform.stepCreateForm.createAndStartTransformDescription',
{
defaultMessage:
'Creates and starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
{created && (
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiButton
fill
isDisabled={created && started}
onClick={startTransform}
data-test-subj="transformWizardStartButton"
>
{i18n.translate('xpack.transform.stepCreateForm.startTransformButton', {
defaultMessage: 'Start',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.transform.stepCreateForm.startTransformDescription', {
defaultMessage:
'Starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiButton fill isDisabled={created && started} onClick={createAndStartTransform}>
{i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', {
defaultMessage: 'Create and start',
<EuiButton
isDisabled={created}
onClick={createTransform}
data-test-subj="transformWizardCreateButton"
>
{i18n.translate('xpack.transform.stepCreateForm.createTransformButton', {
defaultMessage: 'Create',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.transform.stepCreateForm.createTransformDescription', {
defaultMessage:
'Create the transform without starting it. You will be able to start the transform later by returning to the transforms list.',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiCopy textToCopy={getTransformConfigDevConsoleStatement()}>
{(copy: () => void) => (
<EuiButton
onClick={copy}
style={{ width: '100%' }}
data-test-subj="transformWizardCopyToClipboardButton"
>
{i18n.translate(
'xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton',
{
defaultMessage: 'Copy to clipboard',
}
)}
</EuiButton>
)}
</EuiCopy>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate(
'xpack.transform.stepCreateForm.createAndStartTransformDescription',
'xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription',
{
defaultMessage:
'Creates and starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.',
'Copies to the clipboard the Kibana Dev Console command for creating the transform.',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
{created && (
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiButton fill isDisabled={created && started} onClick={startTransform}>
{i18n.translate('xpack.transform.stepCreateForm.startTransformButton', {
defaultMessage: 'Start',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.transform.stepCreateForm.startTransformDescription', {
defaultMessage:
'Starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiButton isDisabled={created} onClick={createTransform}>
{i18n.translate('xpack.transform.stepCreateForm.createTransformButton', {
defaultMessage: 'Create',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.transform.stepCreateForm.createTransformDescription', {
defaultMessage:
'Create the transform without starting it. You will be able to start the transform later by returning to the transforms list.',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
<EuiCopy textToCopy={getTransformConfigDevConsoleStatement()}>
{(copy: () => void) => (
<EuiButton onClick={copy} style={{ width: '100%' }}>
{i18n.translate(
'xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton',
{
defaultMessage: 'Copy to clipboard',
}
)}
</EuiButton>
)}
</EuiCopy>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate(
'xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription',
{
defaultMessage:
'Copies to the clipboard the Kibana Dev Console command for creating the transform.',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
{progressPercentComplete !== undefined && isBatchTransform && (
<Fragment>
<EuiSpacer size="m" />
<EuiText size="xs">
<strong>
{i18n.translate('xpack.transform.stepCreateForm.progressTitle', {
defaultMessage: 'Progress',
})}
</strong>
</EuiText>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem style={{ width: '400px' }} grow={false}>
<EuiProgress size="l" color="primary" value={progressPercentComplete} max={100} />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="xs">{progressPercentComplete}%</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
)}
{created && (
<Fragment>
<EuiHorizontalRule />
<EuiFlexGrid gutterSize="l">
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiCard
icon={<EuiIcon size="xxl" type="list" />}
title={i18n.translate('xpack.transform.stepCreateForm.transformListCardTitle', {
defaultMessage: 'Transforms',
{progressPercentComplete !== undefined && isBatchTransform && (
<Fragment>
<EuiSpacer size="m" />
<EuiText size="xs">
<strong>
{i18n.translate('xpack.transform.stepCreateForm.progressTitle', {
defaultMessage: 'Progress',
})}
description={i18n.translate(
'xpack.transform.stepCreateForm.transformListCardDescription',
{
defaultMessage: 'Return to the transform management page.',
}
)}
onClick={() => setRedirectToTransformManagement(true)}
/>
</EuiFlexItem>
{started === true && createIndexPattern === true && indexPatternId === undefined && (
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiPanel style={{ position: 'relative' }}>
<EuiProgress size="xs" color="primary" position="absolute" />
<EuiText color="subdued" size="s">
<p>
{i18n.translate(
'xpack.transform.stepCreateForm.creatingIndexPatternMessage',
{
defaultMessage: 'Creating Kibana index pattern ...',
}
)}
</p>
</EuiText>
</EuiPanel>
</EuiFlexItem>
)}
{started === true && indexPatternId !== undefined && (
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiCard
icon={<EuiIcon size="xxl" type="discoverApp" />}
title={i18n.translate('xpack.transform.stepCreateForm.discoverCardTitle', {
defaultMessage: 'Discover',
})}
description={i18n.translate(
'xpack.transform.stepCreateForm.discoverCardDescription',
{
defaultMessage: 'Use Discover to explore the transform.',
}
)}
href={getDiscoverUrl(indexPatternId, kibanaContext.kbnBaseUrl)}
</strong>
</EuiText>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem style={{ width: '400px' }} grow={false}>
<EuiProgress
size="l"
color="primary"
value={progressPercentComplete}
max={100}
data-test-subj="transformWizardProgressBar"
/>
</EuiFlexItem>
)}
</EuiFlexGrid>
</Fragment>
)}
</EuiForm>
<EuiFlexItem>
<EuiText size="xs">{progressPercentComplete}%</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
)}
{created && (
<Fragment>
<EuiHorizontalRule />
<EuiFlexGrid gutterSize="l">
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiCard
icon={<EuiIcon size="xxl" type="list" />}
title={i18n.translate('xpack.transform.stepCreateForm.transformListCardTitle', {
defaultMessage: 'Transforms',
})}
description={i18n.translate(
'xpack.transform.stepCreateForm.transformListCardDescription',
{
defaultMessage: 'Return to the transform management page.',
}
)}
onClick={() => setRedirectToTransformManagement(true)}
data-test-subj="transformWizardCardManagement"
/>
</EuiFlexItem>
{started === true && createIndexPattern === true && indexPatternId === undefined && (
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiPanel style={{ position: 'relative' }}>
<EuiProgress size="xs" color="primary" position="absolute" />
<EuiText color="subdued" size="s">
<p>
{i18n.translate(
'xpack.transform.stepCreateForm.creatingIndexPatternMessage',
{
defaultMessage: 'Creating Kibana index pattern ...',
}
)}
</p>
</EuiText>
</EuiPanel>
</EuiFlexItem>
)}
{started === true && indexPatternId !== undefined && (
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiCard
icon={<EuiIcon size="xxl" type="discoverApp" />}
title={i18n.translate('xpack.transform.stepCreateForm.discoverCardTitle', {
defaultMessage: 'Discover',
})}
description={i18n.translate(
'xpack.transform.stepCreateForm.discoverCardDescription',
{
defaultMessage: 'Use Discover to explore the transform.',
}
)}
href={getDiscoverUrl(indexPatternId, kibanaContext.kbnBaseUrl)}
data-test-subj="transformWizardCardDiscover"
/>
</EuiFlexItem>
)}
</EuiFlexGrid>
</Fragment>
)}
</EuiForm>
</div>
);
}
);

View file

@ -158,7 +158,7 @@ export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
if (status === PIVOT_PREVIEW_STATUS.ERROR) {
return (
<EuiPanel grow={false}>
<EuiPanel grow={false} data-test-subj="transformPivotPreview error">
<PreviewTitle previewRequest={previewRequest} />
<EuiCallOut
title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewError', {
@ -192,7 +192,7 @@ export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
);
}
return (
<EuiPanel grow={false}>
<EuiPanel grow={false} data-test-subj="transformPivotPreview empty">
<PreviewTitle previewRequest={previewRequest} />
<EuiCallOut
title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle', {
@ -257,7 +257,7 @@ export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
};
return (
<EuiPanel>
<EuiPanel data-test-subj="transformPivotPreview loaded">
<PreviewTitle previewRequest={previewRequest} />
{status === PIVOT_PREVIEW_STATUS.LOADING && <EuiProgress size="xs" color="accent" />}
{status !== PIVOT_PREVIEW_STATUS.LOADING && (

View file

@ -505,145 +505,318 @@ export const StepDefineForm: SFC<Props> = React.memo(({ overrides = {}, onChange
return (
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: '420px' }}>
<EuiForm>
{kibanaContext.currentSavedSearch === undefined && typeof searchString === 'string' && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.indexPatternLabel', {
defaultMessage: 'Index pattern',
})}
helpText={
disabledQuery
? i18n.translate('xpack.transform.stepDefineForm.indexPatternHelpText', {
defaultMessage:
'An optional query for this index pattern is not supported. The number of supported index fields is {maxIndexFields} whereas this index has {numIndexFields} fields.',
values: {
maxIndexFields,
numIndexFields,
},
})
: ''
}
>
<span>{indexPattern.title}</span>
</EuiFormRow>
{!disabledQuery && (
<Fragment>
{!isAdvancedSourceEditorEnabled && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.queryLabel', {
defaultMessage: 'Query',
})}
helpText={i18n.translate('xpack.transform.stepDefineForm.queryHelpText', {
defaultMessage: 'Use a query to filter the source data (optional).',
})}
>
<KqlFilterBar
indexPattern={indexPattern}
onSubmit={searchHandler}
initialValue={searchString === defaultSearch ? emptySearch : searchString}
placeholder={i18n.translate(
'xpack.transform.stepDefineForm.queryPlaceholder',
{
defaultMessage: 'e.g. {example}',
values: { example: 'method : "GET" or status : "404"' },
}
)}
/>
</EuiFormRow>
)}
</Fragment>
)}
</Fragment>
)}
{isAdvancedSourceEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.advancedSourceEditorLabel', {
defaultMessage: 'Source query clause',
})}
helpText={advancedSourceEditorHelpText}
>
<EuiPanel grow={false} paddingSize="none">
<EuiCodeEditor
mode="json"
width="100%"
value={advancedEditorSourceConfig}
onChange={(d: string) => {
setAdvancedEditorSourceConfig(d);
// Disable the "Apply"-Button if the config hasn't changed.
if (advancedEditorSourceConfigLastApplied === d) {
setAdvancedSourceEditorApplyButtonEnabled(false);
return;
}
// Try to parse the string passed on from the editor.
// If parsing fails, the "Apply"-Button will be disabled
try {
JSON.parse(d);
setAdvancedSourceEditorApplyButtonEnabled(true);
} catch (e) {
setAdvancedSourceEditorApplyButtonEnabled(false);
}
}}
setOptions={{
fontSize: '12px',
}}
theme="textmate"
aria-label={i18n.translate(
'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel',
{
defaultMessage: 'Advanced query editor',
}
<div data-test-subj="transformStepDefineForm">
<EuiForm>
{kibanaContext.currentSavedSearch === undefined && typeof searchString === 'string' && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.indexPatternLabel', {
defaultMessage: 'Index pattern',
})}
helpText={
disabledQuery
? i18n.translate('xpack.transform.stepDefineForm.indexPatternHelpText', {
defaultMessage:
'An optional query for this index pattern is not supported. The number of supported index fields is {maxIndexFields} whereas this index has {numIndexFields} fields.',
values: {
maxIndexFields,
numIndexFields,
},
})
: ''
}
>
<span>{indexPattern.title}</span>
</EuiFormRow>
{!disabledQuery && (
<Fragment>
{!isAdvancedSourceEditorEnabled && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.queryLabel', {
defaultMessage: 'Query',
})}
helpText={i18n.translate('xpack.transform.stepDefineForm.queryHelpText', {
defaultMessage: 'Use a query to filter the source data (optional).',
})}
>
<KqlFilterBar
indexPattern={indexPattern}
onSubmit={searchHandler}
initialValue={searchString === defaultSearch ? emptySearch : searchString}
placeholder={i18n.translate(
'xpack.transform.stepDefineForm.queryPlaceholder',
{
defaultMessage: 'e.g. {example}',
values: { example: 'method : "GET" or status : "404"' },
}
)}
testSubj="tarnsformQueryInput"
/>
</EuiFormRow>
)}
/>
</EuiPanel>
</Fragment>
)}
</Fragment>
)}
{isAdvancedSourceEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate(
'xpack.transform.stepDefineForm.advancedSourceEditorLabel',
{
defaultMessage: 'Source query clause',
}
)}
helpText={advancedSourceEditorHelpText}
>
<EuiPanel grow={false} paddingSize="none">
<EuiCodeEditor
mode="json"
width="100%"
value={advancedEditorSourceConfig}
onChange={(d: string) => {
setAdvancedEditorSourceConfig(d);
// Disable the "Apply"-Button if the config hasn't changed.
if (advancedEditorSourceConfigLastApplied === d) {
setAdvancedSourceEditorApplyButtonEnabled(false);
return;
}
// Try to parse the string passed on from the editor.
// If parsing fails, the "Apply"-Button will be disabled
try {
JSON.parse(d);
setAdvancedSourceEditorApplyButtonEnabled(true);
} catch (e) {
setAdvancedSourceEditorApplyButtonEnabled(false);
}
}}
setOptions={{
fontSize: '12px',
}}
theme="textmate"
aria-label={i18n.translate(
'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel',
{
defaultMessage: 'Advanced query editor',
}
)}
/>
</EuiPanel>
</EuiFormRow>
</Fragment>
)}
{kibanaContext.currentSavedSearch === undefined && (
<EuiFormRow>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiSwitch
label={i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorSourceConfigSwitchLabel',
{
defaultMessage: 'Advanced query editor',
}
)}
checked={isAdvancedSourceEditorEnabled}
onChange={() => {
if (isAdvancedSourceEditorEnabled && sourceConfigUpdated) {
setAdvancedSourceEditorSwitchModalVisible(true);
return;
}
toggleAdvancedSourceEditor();
}}
data-test-subj="transformAdvancedQueryEditorSwitch"
/>
{isAdvancedSourceEditorSwitchModalVisible && (
<SwitchModal
onCancel={() => setAdvancedSourceEditorSwitchModalVisible(false)}
onConfirm={() => {
setAdvancedSourceEditorSwitchModalVisible(false);
toggleAdvancedSourceEditor(true);
}}
type={'source'}
/>
)}
</EuiFlexItem>
{isAdvancedSourceEditorEnabled && (
<EuiButton
size="s"
fill
onClick={applyAdvancedSourceEditorChanges}
disabled={!isAdvancedSourceEditorApplyButtonEnabled}
>
{i18n.translate(
'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText',
{
defaultMessage: 'Apply changes',
}
)}
</EuiButton>
)}
</EuiFlexGroup>
</EuiFormRow>
</Fragment>
)}
{kibanaContext.currentSavedSearch === undefined && (
)}
{kibanaContext.currentSavedSearch !== undefined &&
kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
)}
{!isAdvancedPivotEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.groupByLabel', {
defaultMessage: 'Group by',
})}
>
<Fragment>
<GroupByListForm
list={groupByList}
options={groupByOptionsData}
onChange={updateGroupBy}
deleteHandler={deleteGroupBy}
/>
<DropDown
changeHandler={addGroupBy}
options={groupByOptions}
placeholder={i18n.translate(
'xpack.transform.stepDefineForm.groupByPlaceholder',
{
defaultMessage: 'Add a group by field ...',
}
)}
testSubj="transformGroupBySelection"
/>
</Fragment>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.aggregationsLabel', {
defaultMessage: 'Aggregations',
})}
>
<Fragment>
<AggListForm
list={aggList}
options={aggOptionsData}
onChange={updateAggregation}
deleteHandler={deleteAggregation}
/>
<DropDown
changeHandler={addAggregation}
options={aggOptions}
placeholder={i18n.translate(
'xpack.transform.stepDefineForm.aggregationsPlaceholder',
{
defaultMessage: 'Add an aggregation ...',
}
)}
testSubj="transformAggregationSelection"
/>
</Fragment>
</EuiFormRow>
</Fragment>
)}
{isAdvancedPivotEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorLabel', {
defaultMessage: 'Pivot configuration object',
})}
helpText={advancedEditorHelpText}
>
<EuiPanel grow={false} paddingSize="none">
<EuiCodeEditor
mode="json"
width="100%"
value={advancedEditorConfig}
onChange={(d: string) => {
setAdvancedEditorConfig(d);
// Disable the "Apply"-Button if the config hasn't changed.
if (advancedEditorConfigLastApplied === d) {
setAdvancedPivotEditorApplyButtonEnabled(false);
return;
}
// Try to parse the string passed on from the editor.
// If parsing fails, the "Apply"-Button will be disabled
try {
JSON.parse(d);
setAdvancedPivotEditorApplyButtonEnabled(true);
} catch (e) {
setAdvancedPivotEditorApplyButtonEnabled(false);
}
}}
setOptions={{
fontSize: '12px',
}}
theme="textmate"
aria-label={i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorAriaLabel',
{
defaultMessage: 'Advanced pivot editor',
}
)}
/>
</EuiPanel>
</EuiFormRow>
</Fragment>
)}
<EuiFormRow>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiSwitch
label={i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorSourceConfigSwitchLabel',
'xpack.transform.stepDefineForm.advancedEditorSwitchLabel',
{
defaultMessage: 'Advanced query editor',
defaultMessage: 'Advanced pivot editor',
}
)}
checked={isAdvancedSourceEditorEnabled}
checked={isAdvancedPivotEditorEnabled}
onChange={() => {
if (isAdvancedSourceEditorEnabled && sourceConfigUpdated) {
setAdvancedSourceEditorSwitchModalVisible(true);
if (
isAdvancedPivotEditorEnabled &&
(isAdvancedPivotEditorApplyButtonEnabled ||
advancedEditorConfig !== advancedEditorConfigLastApplied)
) {
setAdvancedEditorSwitchModalVisible(true);
return;
}
toggleAdvancedSourceEditor();
toggleAdvancedEditor();
}}
data-test-subj="transformAdvancedPivotEditorSwitch"
/>
{isAdvancedSourceEditorSwitchModalVisible && (
{isAdvancedEditorSwitchModalVisible && (
<SwitchModal
onCancel={() => setAdvancedSourceEditorSwitchModalVisible(false)}
onCancel={() => setAdvancedEditorSwitchModalVisible(false)}
onConfirm={() => {
setAdvancedSourceEditorSwitchModalVisible(false);
toggleAdvancedSourceEditor(true);
setAdvancedEditorSwitchModalVisible(false);
toggleAdvancedEditor();
}}
type={'source'}
type={'pivot'}
/>
)}
</EuiFlexItem>
{isAdvancedSourceEditorEnabled && (
{isAdvancedPivotEditorEnabled && (
<EuiButton
size="s"
fill
onClick={applyAdvancedSourceEditorChanges}
disabled={!isAdvancedSourceEditorApplyButtonEnabled}
onClick={applyAdvancedPivotEditorChanges}
disabled={!isAdvancedPivotEditorApplyButtonEnabled}
>
{i18n.translate(
'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText',
'xpack.transform.stepDefineForm.advancedEditorApplyButtonText',
{
defaultMessage: 'Apply changes',
}
@ -652,179 +825,19 @@ export const StepDefineForm: SFC<Props> = React.memo(({ overrides = {}, onChange
)}
</EuiFlexGroup>
</EuiFormRow>
)}
{kibanaContext.currentSavedSearch !== undefined &&
kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
)}
{!isAdvancedPivotEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.groupByLabel', {
defaultMessage: 'Group by',
})}
>
<Fragment>
<GroupByListForm
list={groupByList}
options={groupByOptionsData}
onChange={updateGroupBy}
deleteHandler={deleteGroupBy}
/>
<DropDown
changeHandler={addGroupBy}
options={groupByOptions}
placeholder={i18n.translate(
'xpack.transform.stepDefineForm.groupByPlaceholder',
{
defaultMessage: 'Add a group by field ...',
}
)}
/>
</Fragment>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.aggregationsLabel', {
defaultMessage: 'Aggregations',
})}
>
<Fragment>
<AggListForm
list={aggList}
options={aggOptionsData}
onChange={updateAggregation}
deleteHandler={deleteAggregation}
/>
<DropDown
changeHandler={addAggregation}
options={aggOptions}
placeholder={i18n.translate(
'xpack.transform.stepDefineForm.aggregationsPlaceholder',
{
defaultMessage: 'Add an aggregation ...',
}
)}
/>
</Fragment>
</EuiFormRow>
</Fragment>
)}
{isAdvancedPivotEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorLabel', {
defaultMessage: 'Pivot configuration object',
})}
helpText={advancedEditorHelpText}
>
<EuiPanel grow={false} paddingSize="none">
<EuiCodeEditor
mode="json"
width="100%"
value={advancedEditorConfig}
onChange={(d: string) => {
setAdvancedEditorConfig(d);
// Disable the "Apply"-Button if the config hasn't changed.
if (advancedEditorConfigLastApplied === d) {
setAdvancedPivotEditorApplyButtonEnabled(false);
return;
}
// Try to parse the string passed on from the editor.
// If parsing fails, the "Apply"-Button will be disabled
try {
JSON.parse(d);
setAdvancedPivotEditorApplyButtonEnabled(true);
} catch (e) {
setAdvancedPivotEditorApplyButtonEnabled(false);
}
}}
setOptions={{
fontSize: '12px',
}}
theme="textmate"
aria-label={i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorAriaLabel',
{
defaultMessage: 'Advanced pivot editor',
}
)}
/>
</EuiPanel>
</EuiFormRow>
</Fragment>
)}
<EuiFormRow>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiSwitch
label={i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorSwitchLabel',
{
defaultMessage: 'Advanced pivot editor',
}
)}
checked={isAdvancedPivotEditorEnabled}
onChange={() => {
if (
isAdvancedPivotEditorEnabled &&
(isAdvancedPivotEditorApplyButtonEnabled ||
advancedEditorConfig !== advancedEditorConfigLastApplied)
) {
setAdvancedEditorSwitchModalVisible(true);
return;
}
toggleAdvancedEditor();
}}
/>
{isAdvancedEditorSwitchModalVisible && (
<SwitchModal
onCancel={() => setAdvancedEditorSwitchModalVisible(false)}
onConfirm={() => {
setAdvancedEditorSwitchModalVisible(false);
toggleAdvancedEditor();
}}
type={'pivot'}
/>
)}
</EuiFlexItem>
{isAdvancedPivotEditorEnabled && (
<EuiButton
size="s"
fill
onClick={applyAdvancedPivotEditorChanges}
disabled={!isAdvancedPivotEditorApplyButtonEnabled}
>
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorApplyButtonText', {
defaultMessage: 'Apply changes',
{!valid && (
<Fragment>
<EuiSpacer size="m" />
<EuiFormHelpText style={{ maxWidth: '320px' }}>
{i18n.translate('xpack.transform.stepDefineForm.formHelp', {
defaultMessage:
'Transforms are scalable and automated processes for pivoting. Choose at least one group-by and aggregation to get started.',
})}
</EuiButton>
)}
</EuiFlexGroup>
</EuiFormRow>
{!valid && (
<Fragment>
<EuiSpacer size="m" />
<EuiFormHelpText style={{ maxWidth: '320px' }}>
{i18n.translate('xpack.transform.stepDefineForm.formHelp', {
defaultMessage:
'Transforms are scalable and automated processes for pivoting. Choose at least one group-by and aggregation to get started.',
})}
</EuiFormHelpText>
</Fragment>
)}
</EuiForm>
</EuiFormHelpText>
</Fragment>
)}
</EuiForm>
</div>
</EuiFlexItem>
<EuiFlexItem>

View file

@ -57,75 +57,80 @@ export const StepDefineSummary: FC<StepDefineExposedState> = ({
return (
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: '420px' }}>
<EuiForm>
{kibanaContext.currentSavedSearch !== undefined &&
kibanaContext.currentSavedSearch.id === undefined &&
typeof searchString === 'string' && (
<Fragment>
<div data-test-subj="transformStepDefineSummary">
<EuiForm>
{kibanaContext.currentSavedSearch !== undefined &&
kibanaContext.currentSavedSearch.id === undefined &&
typeof searchString === 'string' && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.indexPatternLabel', {
defaultMessage: 'Index pattern',
})}
>
<span>{kibanaContext.currentIndexPattern.title}</span>
</EuiFormRow>
{useCodeBlock === false && displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.queryLabel', {
defaultMessage: 'Query',
})}
>
<span>{displaySearch}</span>
</EuiFormRow>
)}
{useCodeBlock === true && displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate(
'xpack.transform.stepDefineSummary.queryCodeBlockLabel',
{
defaultMessage: 'Query',
}
)}
>
<EuiCodeBlock
language="js"
fontSize="s"
paddingSize="s"
color="light"
overflowHeight={300}
isCopyable
>
{displaySearch}
</EuiCodeBlock>
</EuiFormRow>
)}
</Fragment>
)}
{kibanaContext.currentSavedSearch !== undefined &&
kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.indexPatternLabel', {
defaultMessage: 'Index pattern',
label={i18n.translate('xpack.transform.stepDefineSummary.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
>
<span>{kibanaContext.currentIndexPattern.title}</span>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
{useCodeBlock === false && displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.queryLabel', {
defaultMessage: 'Query',
})}
>
<span>{displaySearch}</span>
</EuiFormRow>
)}
{useCodeBlock === true && displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.queryCodeBlockLabel', {
defaultMessage: 'Query',
})}
>
<EuiCodeBlock
language="js"
fontSize="s"
paddingSize="s"
color="light"
overflowHeight={300}
isCopyable
>
{displaySearch}
</EuiCodeBlock>
</EuiFormRow>
)}
</Fragment>
)}
)}
{kibanaContext.currentSavedSearch !== undefined &&
kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.groupByLabel', {
defaultMessage: 'Group by',
})}
>
<GroupByListSummary list={groupByList} />
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.groupByLabel', {
defaultMessage: 'Group by',
})}
>
<GroupByListSummary list={groupByList} />
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.aggregationsLabel', {
defaultMessage: 'Aggregations',
})}
>
<AggListSummary list={aggList} />
</EuiFormRow>
</EuiForm>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.aggregationsLabel', {
defaultMessage: 'Aggregations',
})}
>
<AggListSummary list={aggList} />
</EuiFormRow>
</EuiForm>
</div>
</EuiFlexItem>
<EuiFlexItem>

View file

@ -181,200 +181,217 @@ export const StepDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChang
]);
return (
<EuiForm>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.transformIdLabel', {
defaultMessage: 'Transform ID',
})}
isInvalid={(!transformIdEmpty && !transformIdValid) || transformIdExists}
error={[
...(!transformIdEmpty && !transformIdValid
? [
i18n.translate('xpack.transform.stepDetailsForm.transformIdInvalidError', {
defaultMessage:
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
}),
]
: []),
...(transformIdExists
? [
i18n.translate('xpack.transform.stepDetailsForm.transformIdExistsError', {
defaultMessage: 'A transform with this ID already exists.',
}),
]
: []),
]}
>
<EuiFieldText
placeholder="transform ID"
value={transformId}
onChange={e => setTransformId(e.target.value)}
aria-label={i18n.translate('xpack.transform.stepDetailsForm.transformIdInputAriaLabel', {
defaultMessage: 'Choose a unique transform ID.',
<div data-test-subj="transformStepDetailsForm">
<EuiForm>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.transformIdLabel', {
defaultMessage: 'Transform ID',
})}
isInvalid={(!transformIdEmpty && !transformIdValid) || transformIdExists}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionLabel', {
defaultMessage: 'Transform description',
})}
helpText={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionHelpText', {
defaultMessage: 'Optional descriptive text.',
})}
>
<EuiFieldText
placeholder="transform description"
value={transformDescription}
onChange={e => setTransformDescription(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel',
{
defaultMessage: 'Choose an optional transform description.',
}
)}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.destinationIndexLabel', {
defaultMessage: 'Destination index',
})}
isInvalid={!indexNameEmpty && !indexNameValid}
helpText={
indexNameExists &&
i18n.translate('xpack.transform.stepDetailsForm.destinationIndexHelpText', {
defaultMessage:
'An index with this name already exists. Be aware that running this transform will modify this destination index.',
})
}
error={
!indexNameEmpty &&
!indexNameValid && [
<Fragment>
{i18n.translate('xpack.transform.stepDetailsForm.destinationIndexInvalidError', {
defaultMessage: 'Invalid destination index name.',
})}
<br />
<EuiLink
href={`https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/indices-create-index.html#indices-create-index`}
target="_blank"
>
{i18n.translate(
'xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink',
{
defaultMessage: 'Learn more about index name limitations.',
}
)}
</EuiLink>
</Fragment>,
]
}
>
<EuiFieldText
placeholder="destination index"
value={destinationIndex}
onChange={e => setDestinationIndex(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel',
{
defaultMessage: 'Choose a unique destination index name.',
}
)}
error={[
...(!transformIdEmpty && !transformIdValid
? [
i18n.translate('xpack.transform.stepDetailsForm.transformIdInvalidError', {
defaultMessage:
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
}),
]
: []),
...(transformIdExists
? [
i18n.translate('xpack.transform.stepDetailsForm.transformIdExistsError', {
defaultMessage: 'A transform with this ID already exists.',
}),
]
: []),
]}
>
<EuiFieldText
placeholder="transform ID"
value={transformId}
onChange={e => setTransformId(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.transformIdInputAriaLabel',
{
defaultMessage: 'Choose a unique transform ID.',
}
)}
isInvalid={(!transformIdEmpty && !transformIdValid) || transformIdExists}
data-test-subj="transformIdInput"
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionLabel', {
defaultMessage: 'Transform description',
})}
helpText={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionHelpText', {
defaultMessage: 'Optional descriptive text.',
})}
>
<EuiFieldText
placeholder="transform description"
value={transformDescription}
onChange={e => setTransformDescription(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel',
{
defaultMessage: 'Choose an optional transform description.',
}
)}
data-test-subj="transformDescriptionInput"
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.destinationIndexLabel', {
defaultMessage: 'Destination index',
})}
isInvalid={!indexNameEmpty && !indexNameValid}
/>
</EuiFormRow>
<EuiFormRow
isInvalid={createIndexPattern && indexPatternTitleExists}
error={
createIndexPattern &&
indexPatternTitleExists && [
i18n.translate('xpack.transform.stepDetailsForm.indexPatternTitleError', {
defaultMessage: 'An index pattern with this title already exists.',
}),
]
}
>
<EuiSwitch
name="transformCreateIndexPattern"
label={i18n.translate('xpack.transform.stepCreateForm.createIndexPatternLabel', {
defaultMessage: 'Create index pattern',
})}
checked={createIndexPattern === true}
onChange={() => setCreateIndexPattern(!createIndexPattern)}
/>
</EuiFormRow>
<EuiFormRow
helpText={
isContinuousModeAvailable === false
? i18n.translate('xpack.transform.stepDetailsForm.continuousModeError', {
defaultMessage: 'Continuous mode is not available for indices without date fields.',
})
: ''
}
>
<EuiSwitch
name="transformContinuousMode"
label={i18n.translate('xpack.transform.stepCreateForm.continuousModeLabel', {
defaultMessage: 'Continuous mode',
})}
checked={isContinuousModeEnabled === true}
onChange={() => setContinuousModeEnabled(!isContinuousModeEnabled)}
disabled={isContinuousModeAvailable === false}
/>
</EuiFormRow>
{isContinuousModeEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.continuousModeDateFieldLabel', {
defaultMessage: 'Date field',
})}
helpText={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText',
helpText={
indexNameExists &&
i18n.translate('xpack.transform.stepDetailsForm.destinationIndexHelpText', {
defaultMessage:
'An index with this name already exists. Be aware that running this transform will modify this destination index.',
})
}
error={
!indexNameEmpty &&
!indexNameValid && [
<Fragment>
{i18n.translate('xpack.transform.stepDetailsForm.destinationIndexInvalidError', {
defaultMessage: 'Invalid destination index name.',
})}
<br />
<EuiLink
href={`https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/indices-create-index.html#indices-create-index`}
target="_blank"
>
{i18n.translate(
'xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink',
{
defaultMessage: 'Learn more about index name limitations.',
}
)}
</EuiLink>
</Fragment>,
]
}
>
<EuiFieldText
placeholder="destination index"
value={destinationIndex}
onChange={e => setDestinationIndex(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel',
{
defaultMessage: 'Select the date field that can be used to identify new documents.',
defaultMessage: 'Choose a unique destination index name.',
}
)}
>
<EuiSelect
options={dateFieldNames.map(text => ({ text }))}
value={continuousModeDateField}
onChange={e => setContinuousModeDateField(e.target.value)}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.continuousModeDelayLabel', {
defaultMessage: 'Delay',
isInvalid={!indexNameEmpty && !indexNameValid}
data-test-subj="transformDestinationIndexInput"
/>
</EuiFormRow>
<EuiFormRow
isInvalid={createIndexPattern && indexPatternTitleExists}
error={
createIndexPattern &&
indexPatternTitleExists && [
i18n.translate('xpack.transform.stepDetailsForm.indexPatternTitleError', {
defaultMessage: 'An index pattern with this title already exists.',
}),
]
}
>
<EuiSwitch
name="transformCreateIndexPattern"
label={i18n.translate('xpack.transform.stepCreateForm.createIndexPatternLabel', {
defaultMessage: 'Create index pattern',
})}
isInvalid={!isContinuousModeDelayValid}
error={
!isContinuousModeDelayValid && [
i18n.translate('xpack.transform.stepDetailsForm.continuousModeDelayError', {
defaultMessage: 'Invalid delay format',
}),
]
}
helpText={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeDelayHelpText',
{
defaultMessage: 'Time delay between current time and latest input data time.',
}
)}
>
<EuiFieldText
placeholder="delay"
value={continuousModeDelay}
onChange={e => setContinuousModeDelay(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeAriaLabel',
checked={createIndexPattern === true}
onChange={() => setCreateIndexPattern(!createIndexPattern)}
data-test-subj="transformCreateIndexPatternSwitch"
/>
</EuiFormRow>
<EuiFormRow
helpText={
isContinuousModeAvailable === false
? i18n.translate('xpack.transform.stepDetailsForm.continuousModeError', {
defaultMessage:
'Continuous mode is not available for indices without date fields.',
})
: ''
}
>
<EuiSwitch
name="transformContinuousMode"
label={i18n.translate('xpack.transform.stepCreateForm.continuousModeLabel', {
defaultMessage: 'Continuous mode',
})}
checked={isContinuousModeEnabled === true}
onChange={() => setContinuousModeEnabled(!isContinuousModeEnabled)}
disabled={isContinuousModeAvailable === false}
data-test-subj="transformContinuousModeSwitch"
/>
</EuiFormRow>
{isContinuousModeEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeDateFieldLabel',
{
defaultMessage: 'Choose a delay.',
defaultMessage: 'Date field',
}
)}
helpText={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeDateFieldHelpText',
{
defaultMessage:
'Select the date field that can be used to identify new documents.',
}
)}
>
<EuiSelect
options={dateFieldNames.map(text => ({ text }))}
value={continuousModeDateField}
onChange={e => setContinuousModeDateField(e.target.value)}
data-test-subj="transformContinuousDateFieldSelect"
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsForm.continuousModeDelayLabel', {
defaultMessage: 'Delay',
})}
isInvalid={!isContinuousModeDelayValid}
/>
</EuiFormRow>
</Fragment>
)}
</EuiForm>
error={
!isContinuousModeDelayValid && [
i18n.translate('xpack.transform.stepDetailsForm.continuousModeDelayError', {
defaultMessage: 'Invalid delay format',
}),
]
}
helpText={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeDelayHelpText',
{
defaultMessage: 'Time delay between current time and latest input data time.',
}
)}
>
<EuiFieldText
placeholder="delay"
value={continuousModeDelay}
onChange={e => setContinuousModeDelay(e.target.value)}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.continuousModeAriaLabel',
{
defaultMessage: 'Choose a delay.',
}
)}
isInvalid={!isContinuousModeDelayValid}
data-test-subj="transformContinuousDelayInput"
/>
</EuiFormRow>
</Fragment>
)}
</EuiForm>
</div>
);
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, SFC } from 'react';
import React, { SFC } from 'react';
import { i18n } from '@kbn/i18n';
@ -33,7 +33,7 @@ export const StepDetailsSummary: SFC<StepDetailsExposedState> = React.memo(
: '';
return (
<Fragment>
<div data-test-subj="transformStepDetailsSummary">
<EuiFormRow
label={i18n.translate('xpack.transform.stepDetailsSummary.transformIdLabel', {
defaultMessage: 'Transform ID',
@ -68,7 +68,7 @@ export const StepDetailsSummary: SFC<StepDetailsExposedState> = React.memo(
<EuiFieldText defaultValue={continuousModeDateField} disabled={true} />
</EuiFormRow>
)}
</Fragment>
</div>
);
}
);

View file

@ -27,7 +27,13 @@ export const WizardNav: SFC<StepsNavProps> = ({
<EuiFlexItem />
{previous && (
<EuiFlexItem grow={false}>
<EuiButton disabled={!previousActive} onClick={previous} iconType="arrowLeft" size="s">
<EuiButton
disabled={!previousActive}
onClick={previous}
iconType="arrowLeft"
size="s"
data-test-subj="transformWizardNavButtonPrevious"
>
{i18n.translate('xpack.transform.wizard.previousStepButton', {
defaultMessage: 'Previous',
})}
@ -36,7 +42,13 @@ export const WizardNav: SFC<StepsNavProps> = ({
)}
{next && (
<EuiFlexItem grow={false}>
<EuiButton disabled={!nextActive} onClick={next} iconType="arrowRight" size="s">
<EuiButton
disabled={!nextActive}
onClick={next}
iconType="arrowRight"
size="s"
data-test-subj="transformWizardNavButtonNext"
>
{i18n.translate('xpack.transform.wizard.nextStepButton', {
defaultMessage: 'Next',
})}

View file

@ -9,6 +9,7 @@ exports[`Transform: Transform List <TransformList /> Minimal initialization 1`]
actions={
Array [
<EuiButtonEmpty
data-test-subj="transformCreateFirstButton"
isDisabled={true}
onClick={[MockFunction]}
>

View file

@ -116,29 +116,34 @@ export const getColumns = (
})
}
iconType={expandedRowItemIds.includes(item.config.id) ? 'arrowUp' : 'arrowDown'}
data-test-subj="transformListRowDetailsToggle"
/>
),
},
{
field: TRANSFORM_LIST_COLUMN.ID,
'data-test-subj': 'transformListColumnId',
name: 'ID',
sortable: true,
truncateText: true,
},
{
field: TRANSFORM_LIST_COLUMN.DESCRIPTION,
'data-test-subj': 'transformListColumnDescription',
name: i18n.translate('xpack.transform.description', { defaultMessage: 'Description' }),
sortable: true,
truncateText: true,
},
{
field: TRANSFORM_LIST_COLUMN.CONFIG_SOURCE_INDEX,
'data-test-subj': 'transformListColumnSourceIndex',
name: i18n.translate('xpack.transform.sourceIndex', { defaultMessage: 'Source index' }),
sortable: true,
truncateText: true,
},
{
field: TRANSFORM_LIST_COLUMN.CONFIG_DEST_INDEX,
'data-test-subj': 'transformListColumnDestinationIndex',
name: i18n.translate('xpack.transform.destinationIndex', {
defaultMessage: 'Destination index',
}),
@ -147,6 +152,7 @@ export const getColumns = (
},
{
name: i18n.translate('xpack.transform.status', { defaultMessage: 'Status' }),
'data-test-subj': 'transformListColumnStatus',
sortable: (item: TransformListRow) => item.stats.state,
truncateText: true,
render(item: TransformListRow) {
@ -156,6 +162,7 @@ export const getColumns = (
},
{
name: i18n.translate('xpack.transform.mode', { defaultMessage: 'Mode' }),
'data-test-subj': 'transformListColumnMode',
sortable: (item: TransformListRow) => item.mode,
truncateText: true,
render(item: TransformListRow) {
@ -167,6 +174,7 @@ export const getColumns = (
},
{
name: i18n.translate('xpack.transform.progress', { defaultMessage: 'Progress' }),
'data-test-subj': 'transformListColumnProgress',
sortable: (item: TransformListRow) => getTransformProgress(item) || 0,
truncateText: true,
render(item: TransformListRow) {
@ -183,7 +191,13 @@ export const getColumns = (
{isBatchTransform && (
<Fragment>
<EuiFlexItem style={{ width: '40px' }} grow={false}>
<EuiProgress value={progress} max={100} color="primary" size="m">
<EuiProgress
value={progress}
max={100}
color="primary"
size="m"
data-test-subj="transformListProgress"
>
{progress}%
</EuiProgress>
</EuiFlexItem>

View file

@ -219,7 +219,11 @@ export const TransformList: FC<Props> = ({
</h2>
}
actions={[
<EuiButtonEmpty onClick={onCreateTransform} isDisabled={disabled}>
<EuiButtonEmpty
onClick={onCreateTransform}
isDisabled={disabled}
data-test-subj="transformCreateFirstButton"
>
{i18n.translate('xpack.transform.list.emptyPromptButtonText', {
defaultMessage: 'Create your first transform',
})}
@ -374,7 +378,7 @@ export const TransformList: FC<Props> = ({
};
return (
<Fragment>
<div data-test-subj="transformListTableContainer">
<ProgressBar isLoading={isLoading || transformsLoading} />
<TransformTable
allowNeutralSort={false}
@ -389,11 +393,18 @@ export const TransformList: FC<Props> = ({
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onTableChange={onTableChange}
pagination={pagination}
rowProps={item => ({
'data-test-subj': `transformListRow row-${item.id}`,
})}
selection={selection}
sorting={sorting}
search={search}
data-test-subj="transformTableTransforms"
data-test-subj={
isLoading || transformsLoading
? 'transformListTable loading'
: 'transformListTable loaded'
}
/>
</Fragment>
</div>
);
};

View file

@ -138,7 +138,11 @@ export const TransformManagement: FC = () => {
</EuiPageBody>
{isSearchSelectionVisible && (
<EuiOverlayMask>
<EuiModal onClose={onCloseModal} className="transformCreateTransformSearchDialog">
<EuiModal
onClose={onCloseModal}
className="transformCreateTransformSearchDialog"
data-test-subj="transformSelectSourceModal"
>
<SearchSelection onSearchSelected={onSearchSelected} />
</EuiModal>
</EuiOverlayMask>

View file

@ -0,0 +1,229 @@
/*
* 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', function() {
this.tags(['smoke']);
before(async () => {
await esArchiver.load('ml/ecommerce');
});
after(async () => {
await esArchiver.unload('ml/ecommerce');
await transform.api.cleanTransformIndices();
});
const testDataList = [
{
suiteTitle: 'batch transform with terms+date_histogram groups and avg agg',
source: 'ecommerce',
groupByEntries: [
{
identifier: 'terms(category.keyword)',
label: 'category.keyword',
} as GroupByEntry,
{
identifier: 'date_histogram(order_date)',
label: 'order_date',
intervalLabel: '1m',
} as GroupByEntry,
],
aggregationEntries: [
{
identifier: 'avg(products.base_price)',
label: 'products.base_price.avg',
},
],
transformId: `ec_1_${Date.now()}`,
transformDescription:
'ecommerce batch transform with groups terms(category.keyword) + date_histogram(order_date) 1m and aggregation avg(products.base_price)',
get destinationIndex(): string {
return `dest_${this.transformId}`;
},
expected: {
row: {
status: 'stopped',
mode: 'batch',
progress: '100',
},
},
},
];
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('displays an empty pivot preview', async () => {
await transform.wizard.assertPivotPreviewEmpty();
});
it('displays the query input', async () => {
await transform.wizard.assertQueryInputExists();
await transform.wizard.assertQueryValue('');
});
it('displays the advanced query editor switch', async () => {
await transform.wizard.assertAdvancedQueryEditorSwitchExists();
await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false);
});
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('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.source,
destinationIndex: testData.destinationIndex,
status: testData.expected.row.status,
mode: testData.expected.row.mode,
progress: testData.expected.row.progress,
});
});
});
}
});
}

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default function({ loadTestFile }: FtrProviderContext) {
describe('transform', function() {
this.tags(['ciGroup9', 'transform']);
loadTestFile(require.resolve('./creation'));
});
}

View file

@ -55,6 +55,7 @@ export default async function ({ readConfigFile }) {
resolve(__dirname, './apps/snapshot_restore'),
resolve(__dirname, './apps/cross_cluster_replication'),
resolve(__dirname, './apps/remote_clusters'),
resolve(__dirname, './apps/transform'),
// This license_management file must be last because it is destructive.
resolve(__dirname, './apps/license_management'),
],
@ -193,6 +194,10 @@ export default async function ({ readConfigFile }) {
pathname: '/app/kibana',
hash: '/management/elasticsearch/watcher/watches/',
},
transform: {
pathname: '/app/kibana/',
hash: '/management/elasticsearch/transform'
}
},
// choose where esArchiver should load archives from

View file

@ -47,6 +47,7 @@ import { UptimeProvider } from './uptime';
import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form';
import { InfraLogStreamProvider } from './infra_log_stream';
import { MachineLearningProvider } from './ml';
import { TransformProvider } from './transform';
import { SecurityServiceProvider, SpacesServiceProvider } from '../../common/services';
@ -89,4 +90,5 @@ export const services = {
infraSourceConfigurationForm: InfraSourceConfigurationFormProvider,
infraLogStream: InfraLogStreamProvider,
ml: MachineLearningProvider,
transform: TransformProvider,
};

View file

@ -0,0 +1,34 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
import {
TransformAPIProvider,
TransformManagementProvider,
TransformNavigationProvider,
TransformSourceSelectionProvider,
TransformTableProvider,
TransformWizardProvider,
} from './transform_ui';
export function TransformProvider(context: FtrProviderContext) {
const api = TransformAPIProvider(context);
const management = TransformManagementProvider(context);
const navigation = TransformNavigationProvider(context);
const sourceSelection = TransformSourceSelectionProvider(context);
const table = TransformTableProvider(context);
const wizard = TransformWizardProvider(context);
return {
api,
management,
navigation,
sourceSelection,
table,
wizard,
};
}

View file

@ -0,0 +1,43 @@
/*
* 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';
export function TransformAPIProvider({ getService }: FtrProviderContext) {
const es = getService('legacyEs');
const log = getService('log');
const retry = getService('retry');
return {
async deleteIndices(indices: string) {
log.debug(`Deleting indices: '${indices}'...`);
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) {
log.debug(`Indices '${indices}' don't exist. Nothing to delete.`);
return;
}
const deleteResponse = await es.indices.delete({
index: indices,
});
expect(deleteResponse)
.to.have.property('acknowledged')
.eql(true, 'Response for delete request should be acknowledged');
await retry.waitForWithTimeout(`'${indices}' indices to be deleted`, 30 * 1000, async () => {
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) {
return true;
} else {
throw new Error(`expected indices '${indices}' to be deleted`);
}
});
},
async cleanTransformIndices() {
await this.deleteIndices('.transform-*');
},
};
}

View file

@ -0,0 +1,12 @@
/*
* 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 { TransformAPIProvider } from './api';
export { TransformManagementProvider } from './management';
export { TransformNavigationProvider } from './navigation';
export { TransformSourceSelectionProvider } from './source_selection';
export { TransformTableProvider } from './transform_table';
export { TransformWizardProvider } from './wizard';

View file

@ -0,0 +1,42 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export function TransformManagementProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
async assertTransformListPageExists() {
await testSubjects.existOrFail('transformPageTransformList');
},
async assertNoTransformsFoundMessageExists() {
await testSubjects.existOrFail('transformNoTransformsFound');
},
async assertTransformsTableExists() {
await testSubjects.existOrFail('~transformListTable');
},
async assertCreateNewTransformButtonExists() {
await testSubjects.existOrFail('transformButtonCreate');
},
async assertTransformStatsBarExists() {
await testSubjects.existOrFail('transformStatsBar');
},
async startTransformCreation() {
if (await testSubjects.exists('transformNoTransformsFound')) {
await testSubjects.click('transformCreateFirstButton');
} else {
await testSubjects.click('transformButtonCreate');
}
await testSubjects.existOrFail('transformSelectSourceModal');
},
};
}

View file

@ -0,0 +1,17 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export function TransformNavigationProvider({ getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common']);
return {
async navigateTo() {
return await PageObjects.common.navigateToApp('transform');
},
};
}

View file

@ -0,0 +1,30 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export function TransformSourceSelectionProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
async assertSourceListContainsEntry(sourceName: string) {
await testSubjects.existOrFail(`savedObjectTitle${sourceName}`);
},
async filterSourceSelection(sourceName: string) {
await testSubjects.setValue('savedObjectFinderSearchInput', sourceName, {
clearWithKeyboard: true,
});
await this.assertSourceListContainsEntry(sourceName);
},
async selectSource(sourceName: string) {
await this.filterSourceSelection(sourceName);
await testSubjects.clickWhenNotDisabled(`savedObjectTitle${sourceName}`);
await testSubjects.existOrFail('transformPageCreateTransform');
},
};
}

View file

@ -0,0 +1,87 @@
/*
* 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';
export function TransformTableProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return new (class TransformTable {
public async parseTransformTable() {
const table = await testSubjects.find('~transformListTable');
const $ = await table.parseDomContent();
const rows = [];
for (const tr of $.findTestSubjects('~transformListRow').toArray()) {
const $tr = $(tr);
rows.push({
id: $tr
.findTestSubject('transformListColumnId')
.find('.euiTableCellContent')
.text()
.trim(),
description: $tr
.findTestSubject('transformListColumnDescription')
.find('.euiTableCellContent')
.text()
.trim(),
sourceIndex: $tr
.findTestSubject('transformListColumnSourceIndex')
.find('.euiTableCellContent')
.text()
.trim(),
destinationIndex: $tr
.findTestSubject('transformListColumnDestinationIndex')
.find('.euiTableCellContent')
.text()
.trim(),
status: $tr
.findTestSubject('transformListColumnStatus')
.find('.euiTableCellContent')
.text()
.trim(),
mode: $tr
.findTestSubject('transformListColumnMode')
.find('.euiTableCellContent')
.text()
.trim(),
progress: $tr
.findTestSubject('transformListColumnProgress')
.findTestSubject('transformListProgress')
.attr('value'),
});
}
return rows;
}
public async refreshTransformList() {
await testSubjects.click('transformRefreshTransformListButton');
await this.waitForTransformsToLoad();
}
public async waitForTransformsToLoad() {
await testSubjects.existOrFail('~transformListTable', { timeout: 60 * 1000 });
await testSubjects.existOrFail('transformListTable loaded', { timeout: 30 * 1000 });
}
public async filterWithSearchString(filter: string) {
await this.waitForTransformsToLoad();
const tableListContainer = await testSubjects.find('transformListTableContainer');
const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch');
await searchBarInput.clearValueWithKeyboard();
await searchBarInput.type(filter);
}
public async assertTransformRowFields(transformId: string, expectedRow: object) {
const rows = await this.parseTransformTable();
const transformRow = rows.filter(row => row.id === transformId)[0];
expect(transformRow).to.eql(expectedRow);
}
})();
}

View file

@ -0,0 +1,352 @@
/*
* 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';
export function TransformWizardProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const retry = getService('retry');
return {
async clickNextButton() {
await testSubjects.existOrFail('transformWizardNavButtonNext');
await testSubjects.clickWhenNotDisabled('transformWizardNavButtonNext');
},
async assertDefineStepActive() {
await testSubjects.existOrFail('transformStepDefineForm');
},
async assertDefineSummaryExists() {
await testSubjects.existOrFail('transformStepDefineSummary');
},
async assertDetailsStepActive() {
await testSubjects.existOrFail('transformStepDetailsForm');
},
async assertDetailsSummaryExists() {
await testSubjects.existOrFail('transformStepDetailsSummary');
},
async assertCreateStepActive() {
await testSubjects.existOrFail('transformStepCreateForm');
},
async advanceToDetailsStep() {
await this.clickNextButton();
await this.assertDetailsStepActive();
await this.assertDefineSummaryExists();
},
async advanceToCreateStep() {
await this.clickNextButton();
await this.assertCreateStepActive();
await this.assertDefineSummaryExists();
await this.assertDetailsSummaryExists();
},
async assertSourceIndexPreviewExists(subSelector?: string) {
let selector = 'transformSourceIndexPreview';
if (subSelector !== undefined) {
selector = `${selector} ${subSelector}`;
} else {
selector = `~${selector}`;
}
await testSubjects.existOrFail(selector);
},
async assertSourceIndexPreviewLoaded() {
await this.assertSourceIndexPreviewExists('loaded');
},
async assertPivotPreviewExists(subSelector?: string) {
let selector = 'transformPivotPreview';
if (subSelector !== undefined) {
selector = `${selector} ${subSelector}`;
} else {
selector = `~${selector}`;
}
await testSubjects.existOrFail(selector);
},
async assertPivotPreviewLoaded() {
await this.assertPivotPreviewExists('loaded');
},
async assertPivotPreviewEmpty() {
await this.assertPivotPreviewExists('empty');
},
async assertQueryInputExists() {
await testSubjects.existOrFail('tarnsformQueryInput');
},
async assertQueryValue(expectedQuery: string) {
const actualQuery = await testSubjects.getVisibleText('tarnsformQueryInput');
expect(actualQuery).to.eql(
expectedQuery,
`Query input text should be '${expectedQuery}' (got ${actualQuery})`
);
},
async assertAdvancedQueryEditorSwitchExists() {
await testSubjects.existOrFail(`transformAdvancedQueryEditorSwitch`, { allowHidden: true });
},
async assertAdvancedQueryEditorSwitchCheckState(expectedCheckState: boolean) {
const actualCheckState = await testSubjects.isSelected('transformAdvancedQueryEditorSwitch');
expect(actualCheckState).to.eql(
expectedCheckState,
`Advanced query editor switch check state should be ${expectedCheckState} (got ${actualCheckState})`
);
},
async assertGroupByInputExists() {
await testSubjects.existOrFail('transformGroupBySelection > comboBoxInput');
},
async assertGroupByInputValue(expectedIdentifier: string[]) {
await retry.tryForTime(2000, async () => {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'transformGroupBySelection > comboBoxInput'
);
expect(comboBoxSelectedOptions).to.eql(expectedIdentifier);
});
},
async assertGroupByEntryExists(
index: number,
expectedLabel: string,
expectedIntervalLabel?: string
) {
await testSubjects.existOrFail(`transformGroupByEntry ${index}`);
const actualLabel = await testSubjects.getVisibleText(
`transformGroupByEntry ${index} > transformGroupByEntryLabel`
);
expect(actualLabel).to.eql(
expectedLabel,
`Label for group by entry ${index} should be '${expectedLabel}' (got '${actualLabel}')`
);
if (expectedIntervalLabel !== undefined) {
const actualIntervalLabel = await testSubjects.getVisibleText(
`transformGroupByEntry ${index} > transformGroupByEntryIntervalLabel`
);
expect(actualIntervalLabel).to.eql(
expectedIntervalLabel,
`Label for group by entry ${index} should be '${expectedIntervalLabel}' (got '${actualIntervalLabel}')`
);
}
},
async addGroupByEntry(
index: number,
identifier: string,
expectedLabel: string,
expectedIntervalLabel?: string
) {
await comboBox.set('transformGroupBySelection > comboBoxInput', identifier);
await this.assertGroupByInputValue([]);
await this.assertGroupByEntryExists(index, expectedLabel, expectedIntervalLabel);
},
async assertAggregationInputExists() {
await testSubjects.existOrFail('transformAggregationSelection > comboBoxInput');
},
async assertAggregationInputValue(expectedIdentifier: string[]) {
await retry.tryForTime(2000, async () => {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'transformAggregationSelection > comboBoxInput'
);
expect(comboBoxSelectedOptions).to.eql(expectedIdentifier);
});
},
async assertAggregationEntryExists(index: number, expectedLabel: string) {
await testSubjects.existOrFail(`transformAggregationEntry ${index}`);
const actualLabel = await testSubjects.getVisibleText(
`transformAggregationEntry ${index} > transformAggregationEntryLabel`
);
expect(actualLabel).to.eql(
expectedLabel,
`Label for aggregation entry ${index} should be '${expectedLabel}' (got '${actualLabel}')`
);
},
async addAggregationEntry(index: number, identifier: string, expectedLabel: string) {
await comboBox.set('transformAggregationSelection > comboBoxInput', identifier);
await this.assertAggregationInputValue([]);
await this.assertAggregationEntryExists(index, expectedLabel);
},
async assertAdvancedPivotEditorSwitchExists() {
await testSubjects.existOrFail(`transformAdvancedPivotEditorSwitch`, { allowHidden: true });
},
async assertAdvancedPivotEditorSwitchCheckState(expectedCheckState: boolean) {
const actualCheckState = await testSubjects.isSelected('transformAdvancedPivotEditorSwitch');
expect(actualCheckState).to.eql(
expectedCheckState,
`Advanced pivot editor switch check state should be ${expectedCheckState} (got ${actualCheckState})`
);
},
async assertTransformIdInputExists() {
await testSubjects.existOrFail('transformIdInput');
},
async assertTransformIdValue(expectedValue: string) {
const actualTransformId = await testSubjects.getAttribute('transformIdInput', 'value');
expect(actualTransformId).to.eql(
expectedValue,
`Transform id input text should be ${expectedValue} (got ${actualTransformId})`
);
},
async setTransformId(transformId: string) {
await testSubjects.setValue('transformIdInput', transformId, { clearWithKeyboard: true });
await this.assertTransformIdValue(transformId);
},
async assertTransformDescriptionInputExists() {
await testSubjects.existOrFail('transformDescriptionInput');
},
async assertTransformDescriptionValue(expectedValue: string) {
const actualTransformDescription = await testSubjects.getAttribute(
'transformDescriptionInput',
'value'
);
expect(actualTransformDescription).to.eql(
expectedValue,
`Transform description input text should be ${expectedValue} (got ${actualTransformDescription})`
);
},
async setTransformDescription(transformDescription: string) {
await testSubjects.setValue('transformDescriptionInput', transformDescription, {
clearWithKeyboard: true,
});
await this.assertTransformDescriptionValue(transformDescription);
},
async assertDestinationIndexInputExists() {
await testSubjects.existOrFail('transformDestinationIndexInput');
},
async assertDestinationIndexValue(expectedValue: string) {
const actualDestinationIndex = await testSubjects.getAttribute(
'transformDestinationIndexInput',
'value'
);
expect(actualDestinationIndex).to.eql(
expectedValue,
`Destination index input text should be ${expectedValue} (got ${actualDestinationIndex})`
);
},
async setDestinationIndex(destinationIndex: string) {
await testSubjects.setValue('transformDestinationIndexInput', destinationIndex, {
clearWithKeyboard: true,
});
await this.assertDestinationIndexValue(destinationIndex);
},
async assertCreateIndexPatternSwitchExists() {
await testSubjects.existOrFail(`transformCreateIndexPatternSwitch`, { allowHidden: true });
},
async assertCreateIndexPatternSwitchCheckState(expectedCheckState: boolean) {
const actualCheckState = await testSubjects.isSelected('transformCreateIndexPatternSwitch');
expect(actualCheckState).to.eql(
expectedCheckState,
`Create index pattern switch check state should be ${expectedCheckState} (got ${actualCheckState})`
);
},
async assertContinuousModeSwitchExists() {
await testSubjects.existOrFail(`transformContinuousModeSwitch`, { allowHidden: true });
},
async assertContinuousModeSwitchCheckState(expectedCheckState: boolean) {
const actualCheckState = await testSubjects.isSelected('transformContinuousModeSwitch');
expect(actualCheckState).to.eql(
expectedCheckState,
`Continuous mode switch check state should be ${expectedCheckState} (got ${actualCheckState})`
);
},
async assertCreateAndStartButtonExists() {
await testSubjects.existOrFail(`transformWizardCreateAndStartButton`);
},
async assertCreateButtonExists() {
await testSubjects.existOrFail(`transformWizardCreateButton`);
},
async assertCopyToClipboardButtonExists() {
await testSubjects.existOrFail(`transformWizardCopyToClipboardButton`);
},
async assertStartButtonExists() {
await testSubjects.existOrFail(`transformWizardStartButton`);
},
async assertManagementCardExists() {
await testSubjects.existOrFail(`transformWizardCardManagement`);
},
async returnToManagement() {
await testSubjects.click('transformWizardCardManagement');
await testSubjects.existOrFail('transformPageTransformList');
},
async assertDiscoverCardExists() {
await testSubjects.existOrFail(`transformWizardCardDiscover`);
},
async assertProgressbarExists() {
await testSubjects.existOrFail(`transformWizardProgressBar`);
},
async waitForProgressBarComplete() {
await retry.tryForTime(60 * 1000, async () => {
const actualValue = await testSubjects.getAttribute('transformWizardProgressBar', 'value');
if (actualValue === '100') {
return true;
} else {
throw new Error(`Expected progress bar value to be 100 (got ${actualValue})`);
}
});
},
async createTransform() {
await testSubjects.click('transformWizardCreateButton');
await this.assertStartButtonExists();
await this.assertManagementCardExists();
expect(await testSubjects.isEnabled('transformWizardCreateButton')).to.eql(
false,
'The create button should not be enabled any more'
);
},
async startTransform() {
await testSubjects.click('transformWizardStartButton');
await this.assertDiscoverCardExists();
expect(await testSubjects.isEnabled('transformWizardStartButton')).to.eql(
false,
'The start button should not be enabled any more'
);
await this.assertProgressbarExists();
},
};
}