diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc deleted file mode 100644 index c32b8d69d3c0d..0000000000000 --- a/docs/api/using-api.asciidoc +++ /dev/null @@ -1,86 +0,0 @@ -[[using-api]] -== Using the APIs - -Interact with the {kib} APIs through the `curl` command and HTTP and HTTPs protocols. - -It is recommended that you use HTTPs on port 5601 because it is more secure. - -NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to interact with the {kib} APIs with the Console and must use `curl` or another HTTP tool instead. For more information, refer to <>. - -[float] -[[api-authentication]] -=== Authentication - -The {kib} APIs support key- and token-based authentication. - -[float] -[[token-api-authentication]] -==== Token-based authentication - -To use token-based authentication, you use the same username and password that you use to log into Elastic. -In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, -which is where the username and password are stored in order to be passed as part of the call. - -[float] -[[key-authentication]] -==== Key-based authentication - -To use key-based authentication, you create an API key using the Elastic Console, then specify the key in the header of your API calls. - -For information about API keys, refer to <>. - -[float] -[[api-calls]] -=== API calls -API calls are stateless. Each request that you make happens in isolation from other calls and must include all of the necessary information for {kib} to fulfill the request. API requests return JSON output, which is a format that is machine-readable and works well for automation. - -Calls to the API endpoints require different operations. To interact with the {kib} APIs, use the following operations: - -* *GET* - Fetches the information. - -* *POST* - Adds new information. - -* *PUT* - Updates the existing information. - -* *DELETE* - Removes the information. - -For example, the following `curl` command exports a dashboard: - -[source,sh] --- -curl -X POST api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c --- -// KIBANA - -[float] -[[api-request-headers]] -=== Request headers - -For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsrf` and `Content-Type` headers. - -`kbn-xsrf: true`:: - By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: - -* The API endpoint uses the `GET` or `HEAD` operations -* The path is allowed using the <> setting -* XSRF protections are disabled using the <> setting - -`Content-Type: application/json`:: - Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. - -Request header example: - -[source,sh] --- -curl -X POST \ - http://localhost:5601/api/spaces/space \ - -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: true' \ - -d '{ - "id": "sales", - "name": "Sales", - "description": "This is your Sales Space!", - "disabledFeatures": [] -} -' --- diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md index 5cfd5e1bc9929..80f4832ba5643 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md @@ -12,5 +12,6 @@ export declare type IndexPatternSelectProps = Required void; + maxIndexPatterns?: number; }; ``` diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 9916ab42186dc..459dbbdd34b27 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -1,37 +1,99 @@ [[api]] = REST API -[partintro] --- Some {kib} features are provided via a REST API, which is ideal for creating an integration with {kib}, or automating certain aspects of configuring and deploying {kib}. -Each API is experimental and can include breaking changes in any version of -{kib}, or might be entirely removed from {kib}. +[float] +[[using-apis]] +== Using the APIs -//// -Each API has one of the following labels: +Interact with the {kib} APIs through the `curl` command and HTTP and HTTPs protocols. -* *Stable* APIs should be safe to use extensively in production. Any breaking -changes to these APIs should only occur in major versions and will be -clearly documented in the breaking changes documentation for that release. +It is recommended that you use HTTPs on port 5601 because it is more secure. -* *Beta* APIs are on track to become stable, permanent features of {kib}. -Caution should be exercised in their use since it is possible we'd have to make -a breaking change to these APIs in a minor version, but we'll avoid this -wherever possible. +NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to interact with the {kib} APIs with the Console and must use `curl` or another HTTP tool instead. For more information, refer to <>. -* *Experimental* APIs are just that - an experiment. An experimental API might -have breaking changes in any version of {kib}, or it might even be removed -entirely. +[float] +[[api-authentication]] +=== Authentication +The {kib} APIs support key- and token-based authentication. -If a label is missing from an API, it is considered `experimental`. -//// +[float] +[[token-api-authentication]] +==== Token-based authentication + +To use token-based authentication, you use the same username and password that you use to log into Elastic. +In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, +which is where the username and password are stored in order to be passed as part of the call. + +[float] +[[key-authentication]] +==== Key-based authentication + +To use key-based authentication, you create an API key using the Elastic Console, then specify the key in the header of your API calls. + +For information about API keys, refer to <>. + +[float] +[[api-calls]] +=== API calls +API calls are stateless. Each request that you make happens in isolation from other calls and must include all of the necessary information for {kib} to fulfill the request. API requests return JSON output, which is a format that is machine-readable and works well for automation. + +Calls to the API endpoints require different operations. To interact with the {kib} APIs, use the following operations: + +* *GET* - Fetches the information. + +* *POST* - Adds new information. + +* *PUT* - Updates the existing information. + +* *DELETE* - Removes the information. + +For example, the following `curl` command exports a dashboard: + +[source,sh] +-------------------------------------------- +curl -X POST api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c +-------------------------------------------- +// KIBANA + +[float] +[[api-request-headers]] +=== Request headers + +For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsrf` and `Content-Type` headers. + +`kbn-xsrf: true`:: + By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: + +* The API endpoint uses the `GET` or `HEAD` operations +* The path is allowed using the <> setting +* XSRF protections are disabled using the <> setting + +`Content-Type: application/json`:: + Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. + Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. + +Request header example: + +[source,sh] +-------------------------------------------- +curl -X POST \ + http://localhost:5601/api/spaces/space \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: true' \ + -d '{ + "id": "sales", + "name": "Sales", + "description": "This is your Sales Space!", + "disabledFeatures": [] +} +' +-------------------------------------------- --- -include::{kib-repo-dir}/api/using-api.asciidoc[] include::{kib-repo-dir}/api/features.asciidoc[] include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 40c6705e1d7d0..af7442b5e742f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1537,6 +1537,7 @@ export type IndexPatternSelectProps = Required, 'isLo indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; + maxIndexPatterns?: number; }; // Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 96e7a6d83d2d2..aa36323d11bcc 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -25,6 +25,7 @@ export type IndexPatternSelectProps = Required< indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; + maxIndexPatterns?: number; }; export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { @@ -41,6 +42,10 @@ interface IndexPatternSelectState { // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default class IndexPatternSelect extends Component { + static defaultProps: { + maxIndexPatterns: 1000; + }; + private isMounted: boolean = false; state: IndexPatternSelectState; @@ -103,7 +108,10 @@ export default class IndexPatternSelect extends Component { const { fieldTypes, onNoIndexPatterns, indexPatternService } = this.props; - const indexPatterns = await indexPatternService.find(`${searchValue}*`, 100); + const indexPatterns = await indexPatternService.find( + `${searchValue}*`, + this.props.maxIndexPatterns + ); // We need this check to handle the case where search results come back in a different // order than they were sent out. Only load results for the most recent search. diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index a7a5b8626914a..349e024f31c31 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -9,7 +9,9 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectMetaData, OnSaveProps } from 'src/plugins/saved_objects/public'; import { first } from 'rxjs/operators'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { SavedObjectAttributes } from '../../../../core/public'; +import { extractSearchSourceReferences } from '../../../data/public'; import { EmbeddableFactoryDefinition, EmbeddableOutput, @@ -236,4 +238,42 @@ export class VisualizeEmbeddableFactory } ); } + + public extract(_state: EmbeddableStateWithType) { + const state = (_state as unknown) as VisualizeInput; + const references = []; + + if (state.savedVis?.data.searchSource) { + const [, searchSourceReferences] = extractSearchSourceReferences( + state.savedVis.data.searchSource + ); + + references.push(...searchSourceReferences); + } + + if (state.savedVis?.data.savedSearchId) { + references.push({ + name: 'search_0', + type: 'search', + id: String(state.savedVis.data.savedSearchId), + }); + } + + if (state.savedVis?.params.controls) { + const controls = state.savedVis.params.controls; + controls.forEach((control: Record, i: number) => { + if (!control.indexPattern) { + return; + } + control.indexPatternRefName = `control_${i}_index_pattern`; + references.push({ + name: control.indexPatternRefName, + type: 'index-pattern', + id: control.indexPattern, + }); + }); + } + + return { state: _state, references }; + } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 59b64de369745..1d75e873f9b18 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -187,272 +187,275 @@ export function LayerPanel( ]); return ( -
- - - - - - - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, + <> +
+ + + + - )} - - - - - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}} - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
- ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; - return ( - -
- { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
-
- ); - })} -
- {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - -
- ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } - } - setActiveDimension(initialActiveDimensionState); - }} - panel={ - <> - {activeGroup && activeId && ( + {layerDatasource && ( + { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ - layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ - layerId, - groupId: activeGroup.groupId, - columnId: activeId, - prevState: props.visualizationState, - }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, + layerId, + state: layerDatasourceState, + activeData: props.framePublicAPI.activeData, + setState: (updater: unknown) => { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); }); + + props.updateAll(datasourceId, newState, nextVisState); }, }} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- + )} + + + + + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
} + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', + })} +
+ ) : ( + [] + ) + } + > + <> + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
+ { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: id, + prevState: props.visualizationState, + }) + ); + removeButtonRef(id); + }} + > + + +
+
+ ); + })} +
+ {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - - )} - - } - /> + ) : null} + + + ); + })} - + - - - - - -
-
+ + + + + +
+
+ + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } + } + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ +
+ )} + + } + /> + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 4c40282012d6d..a676b7283671c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { Capabilities, HttpSetup } from 'kibana/public'; +import { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/target/common'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { IndexPatternsContract, TimefilterContract, @@ -105,4 +106,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { parent ); } + + extract(state: EmbeddableStateWithType) { + let references: SavedObjectReference[] = []; + const typedState = (state as unknown) as LensEmbeddableInput; + + if ('attributes' in typedState && typedState.attributes !== undefined) { + references = typedState.attributes.references; + } + + return { state, references }; + } } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index b039076305498..7e15bfa9a340e 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { EmbeddableFactoryDefinition, IContainer, @@ -13,8 +14,10 @@ import { import '../index.scss'; import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; -import { MapByReferenceInput, MapEmbeddableInput } from './types'; +import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; +// @ts-expect-error +import { extractReferences } from '../../common/migrations/references'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; @@ -61,4 +64,16 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { parent ); }; + + extract(state: EmbeddableStateWithType) { + const maybeMapByValueInput = state as EmbeddableStateWithType | MapByValueInput; + + if ((maybeMapByValueInput as MapByValueInput).attributes !== undefined) { + const { references } = extractReferences(maybeMapByValueInput); + + return { state, references }; + } + + return { state, references: [] }; + } } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx index d82f0769c8b74..fb846d041bd17 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx @@ -123,6 +123,7 @@ export const DeleteActionModal: FC = ({ return ( = ({ closeModal, items, startAndC return ( = ({ }) => { return ( <> - + - +

{stepName}

- + = ({ - + = (item) => { - return {item.name}; + return ( + + {item.name} + + ); }; interface Props { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx index c746a5cc63a9b..9a66b586d1d56 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx @@ -9,18 +9,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartProps } from './waterfall_chart'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; interface LegendProps { items: Required['legendItems']; render: Required['renderLegendItem']; } +const StyledFlexItem = euiStyled(EuiFlexItem)` + margin-right: ${(props) => props.theme.eui.paddingSizes.m}; + max-width: 7%; + min-width: 160px; +`; + export const Legend: React.FC = ({ items, render }) => { return ( - - {items.map((item, index) => { - return {render(item, index)}; - })} + + {items.map((item, index) => ( + {render(item, index)} + ))} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 59990b29db5db..119c907f76ca1 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -120,8 +120,12 @@ export const WaterfallChart = ({ - - + + {shouldRenderSidebar && } { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createAndRunTransform( + testData.originalConfig.id, + testData.originalConfig + ); + } + + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + await transform.api.cleanTransformIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + it('delete transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + if (testData.expected.row.mode === 'continuous') { + await transform.testExecution.logTestStep('should have the delete action disabled'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Delete', + false + ); + + await transform.testExecution.logTestStep('should stop the transform'); + await transform.table.clickTransformRowActionWithRetry( + testData.originalConfig.id, + 'Stop' + ); + } + + await transform.testExecution.logTestStep('should display the stopped transform'); + await transform.table.assertTransformRowFields(testData.originalConfig.id, { + id: testData.originalConfig.id, + description: testData.originalConfig.description, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + + await transform.testExecution.logTestStep('should show the delete modal'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Delete', + true + ); + await transform.table.clickTransformRowActionWithRetry( + testData.originalConfig.id, + 'Delete' + ); + await transform.table.assertTransformDeleteModalExists(); + + await transform.testExecution.logTestStep('should delete the transform'); + await transform.table.confirmDeleteTransform(); + await transform.table.assertTransformRowNotExists(testData.originalConfig.id); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 71a7cf02df1fd..1f0bb058bdc38 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.filterWithSearchString(testData.originalConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); - await transform.table.assertTransformRowActions(false); + await transform.table.assertTransformRowActions(testData.originalConfig.id, false); await transform.testExecution.logTestStep('should show the edit flyout'); await transform.table.clickTransformRowAction('Edit'); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 63d8d0b51bc8c..1440f0a3f9a09 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -6,7 +6,10 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformLatestConfig } from '../../../../plugins/transform/common/types/transform'; +import { + TransformLatestConfig, + TransformPivotConfig, +} from '../../../../plugins/transform/common/types/transform'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -41,6 +44,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./cloning')); loadTestFile(require.resolve('./editing')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./deleting')); + loadTestFile(require.resolve('./starting')); }); } export interface ComboboxOption { @@ -80,20 +85,46 @@ export function isLatestTransformTestData(arg: any): arg is LatestTransformTestD return arg.type === 'latest'; } -export function getLatestTransformConfig(): TransformLatestConfig { +export function getPivotTransformConfig( + prefix: string, + continuous?: boolean +): TransformPivotConfig { const timestamp = Date.now(); return { - id: `ec_cloning_2_${timestamp}`, + id: `ec_${prefix}_pivot_${timestamp}_${continuous ? 'cont' : 'batch'}`, + source: { index: ['ft_ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: `ecommerce ${ + continuous ? 'continuous' : 'batch' + } transform with avg(products.base_price) grouped by terms(category.keyword)`, + dest: { index: `user-ec_2_${timestamp}` }, + ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}), + }; +} + +export function getLatestTransformConfig( + prefix: string, + continuous?: boolean +): TransformLatestConfig { + const timestamp = Date.now(); + return { + id: `ec_${prefix}_latest_${timestamp}_${continuous ? 'cont' : 'batch'}`, source: { index: ['ft_ecommerce'] }, latest: { unique_key: ['category.keyword'], sort: 'order_date', }, - description: 'ecommerce batch transform with category unique key and sorted by order date', + description: `ecommerce ${ + continuous ? 'continuous' : 'batch' + } transform with category unique key and sorted by order date`, frequency: '3s', settings: { max_page_search_size: 250, }, dest: { index: `user-ec_3_${timestamp}` }, + ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}), }; } diff --git a/x-pack/test/functional/apps/transform/starting.ts b/x-pack/test/functional/apps/transform/starting.ts new file mode 100644 index 0000000000000..4b0b6f8dade66 --- /dev/null +++ b/x-pack/test/functional/apps/transform/starting.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import { getLatestTransformConfig, getPivotTransformConfig } from './index'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('starting', function () { + const PREFIX = 'starting'; + const testDataList = [ + { + suiteTitle: 'batch transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, false), + mode: 'batch', + }, + { + suiteTitle: 'continuous transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, true), + mode: 'continuous', + }, + { + suiteTitle: 'batch transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX, false), + mode: 'batch', + }, + { + suiteTitle: 'continuous transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX, true), + mode: 'continuous', + }, + ]; + + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createTransform(testData.originalConfig.id, testData.originalConfig); + } + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + + await transform.api.cleanTransformIndices(); + }); + + for (const testData of testDataList) { + const transformId = testData.originalConfig.id; + + describe(`${testData.suiteTitle}`, function () { + it('start transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + await transform.testExecution.logTestStep( + 'should display the original transform in the transform list' + ); + await transform.table.filterWithSearchString(transformId, 1); + + await transform.testExecution.logTestStep('should start the transform'); + await transform.table.assertTransformRowActionEnabled(transformId, 'Start', true); + await transform.table.clickTransformRowActionWithRetry(transformId, 'Start'); + await transform.table.confirmStartTransform(); + await transform.table.clearSearchString(testDataList.length); + + if (testData.mode === 'continuous') { + await transform.testExecution.logTestStep('should display the started transform'); + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.STOPPED + ); + } else { + await transform.table.assertTransformRowProgressGreaterThan(transformId, 0); + } + + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.FAILED + ); + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.ABORTING + ); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/services/transform/management.ts b/x-pack/test/functional/services/transform/management.ts index fdfd1d1d9b40f..807c3d49e344c 100644 --- a/x-pack/test/functional/services/transform/management.ts +++ b/x-pack/test/functional/services/transform/management.ts @@ -5,8 +5,11 @@ * 2.0. */ +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +export type TransformManagement = ProvidedType; + export function TransformManagementProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 72626580e9461..ce2625677e479 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -12,6 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformTableProvider({ getService }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return new (class TransformTable { public async parseTransformTable() { @@ -129,21 +130,63 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { const filteredRows = rows.filter((row) => row.id === filter); expect(filteredRows).to.have.length( expectedRowCount, - `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + `Filtered Transform table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` ); } - public async assertTransformRowFields(transformId: string, expectedRow: object) { + public async clearSearchString(expectedRowCount: number = 1) { + await this.waitForTransformsToLoad(); + const tableListContainer = await testSubjects.find('transformListTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); const rows = await this.parseTransformTable(); - const transformRow = rows.filter((row) => row.id === transformId)[0]; - expect(transformRow).to.eql( - expectedRow, - `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( - transformRow - )}')` + expect(rows).to.have.length( + expectedRowCount, + `Transform table should have ${expectedRowCount} row(s) after clearing search' (got '${rows.length}')` ); } + public async assertTransformRowFields(transformId: string, expectedRow: object) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow).to.eql( + expectedRow, + `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( + transformRow + )}')` + ); + }); + } + + public async assertTransformRowProgressGreaterThan( + transformId: string, + expectedProgress: number + ) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow.progress).to.greaterThan( + 0, + `Expected transform row progress to be greater than '${expectedProgress}' (got '${transformRow.progress}')` + ); + }); + } + + public async assertTransformRowStatusNotEql(transformId: string, status: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow.status).to.not.eql( + status, + `Expected transform row status to not be '${status}' (got '${transformRow.status}')` + ); + }); + } + public async assertTransformExpandedRow() { await testSubjects.click('transformListRowDetailsToggle'); @@ -185,8 +228,13 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } - public async assertTransformRowActions(isTransformRunning = false) { - await testSubjects.click('euiCollapsedItemActionsButton'); + public rowSelector(transformId: string, subSelector?: string) { + const row = `~transformListTable > ~row-${transformId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async assertTransformRowActions(transformId: string, isTransformRunning = false) { + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); await testSubjects.existOrFail('transformActionClone'); await testSubjects.existOrFail('transformActionDelete'); @@ -201,6 +249,42 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { } } + public async assertTransformRowActionEnabled( + transformId: string, + action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit', + expectedValue: boolean + ) { + const selector = `transformAction${action}`; + await retry.tryForTime(60 * 1000, async () => { + await this.refreshTransformList(); + + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); + + await testSubjects.existOrFail(selector); + const isEnabled = await testSubjects.isEnabled(selector); + expect(isEnabled).to.eql( + expectedValue, + `Expected '${action}' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }); + } + + public async clickTransformRowActionWithRetry( + transformId: string, + action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit' + ) { + await retry.tryForTime(30 * 1000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); + await testSubjects.existOrFail(`transformAction${action}`); + await testSubjects.click(`transformAction${action}`); + await testSubjects.missingOrFail(`transformAction${action}`); + }); + } + public async clickTransformRowAction(action: string) { await testSubjects.click(`transformAction${action}`); } @@ -214,5 +298,53 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await this.waitForTransformsExpandedRowPreviewTabToLoad(); await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values); } + + public async assertTransformDeleteModalExists() { + await testSubjects.existOrFail('transformDeleteModal', { timeout: 60 * 1000 }); + } + + public async assertTransformDeleteModalNotExists() { + await testSubjects.missingOrFail('transformDeleteModal', { timeout: 60 * 1000 }); + } + + public async assertTransformStartModalExists() { + await testSubjects.existOrFail('transformStartModal', { timeout: 60 * 1000 }); + } + + public async assertTransformStartModalNotExists() { + await testSubjects.missingOrFail('transformStartModal', { timeout: 60 * 1000 }); + } + + public async confirmDeleteTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformDeleteModalExists(); + await testSubjects.click('transformDeleteModal > confirmModalConfirmButton'); + await this.assertTransformDeleteModalNotExists(); + }); + } + + public async assertTransformRowNotExists(transformId: string) { + await retry.tryForTime(30 * 1000, async () => { + // If after deletion, and there's no transform left + const noTransformsFoundMessageExists = await testSubjects.exists( + 'transformNoTransformsFound' + ); + + if (noTransformsFoundMessageExists) { + return true; + } else { + // Checks that the tranform was deleted + await this.filterWithSearchString(transformId, 0); + } + }); + } + + public async confirmStartTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformStartModalExists(); + await testSubjects.click('transformStartModal > confirmModalConfirmButton'); + await this.assertTransformStartModalNotExists(); + }); + } })(); }