From 96000fa425932618211b76643ef6914335beb28e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Feb 2020 15:31:57 +0100 Subject: [PATCH] [ML] Transforms: Adds clone feature to transforms list. (#57837) Adds a Clone action to the transform management list. The action opens the transform wizard with prepopulated configurations based on the selected transform. The fields for the transform name and target index will not be populated to avoid the obvious "already exists" warnings. --- .../plugins/transform/public/app/app.tsx | 5 + .../transform/public/app/common/index.ts | 3 + .../public/app/common/request.test.ts | 11 +- .../transform/public/app/common/request.ts | 6 + .../transform/public/app/constants/index.ts | 1 + .../transform/public/app/lib/kibana/common.ts | 34 ++- .../transform/public/app/lib/kibana/index.ts | 1 + .../public/app/lib/kibana/kibana_context.tsx | 18 +- .../public/app/lib/kibana/kibana_provider.tsx | 20 +- .../clone_transform_section.tsx | 194 ++++++++++++++++++ .../app/sections/clone_transform/index.ts | 7 + .../use_source_index_data.ts | 3 +- .../components/step_define/index.ts | 3 +- .../step_define/step_define_form.tsx | 72 ++++++- .../components/step_details/index.ts | 6 +- .../step_details/step_details_form.tsx | 16 ++ .../components/wizard/wizard.tsx | 26 ++- .../transform_list/action_clone.tsx | 60 ++++++ .../transform_list/actions.test.tsx | 3 +- .../components/transform_list/actions.tsx | 6 + .../app/services/navigation/breadcrumb.ts | 8 + .../public/app/services/text/text.ts | 3 + 22 files changed, 454 insertions(+), 52 deletions(-) create mode 100644 x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx create mode 100644 x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts create mode 100644 x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/app.tsx b/x-pack/legacy/plugins/transform/public/app/app.tsx index 825c1761bf619..0f21afbcccca8 100644 --- a/x-pack/legacy/plugins/transform/public/app/app.tsx +++ b/x-pack/legacy/plugins/transform/public/app/app.tsx @@ -16,6 +16,7 @@ import { getAppProviders } from './app_dependencies'; import { AuthorizationContext } from './lib/authorization'; import { AppDependencies } from '../shim'; +import { CloneTransformSection } from './sections/clone_transform'; import { CreateTransformSection } from './sections/create_transform'; import { TransformManagementSection } from './sections/transform_management'; @@ -39,6 +40,10 @@ export const App: FC = () => { return (
+ { + test('isMatchAllQuery()', () => { + expect(isMatchAllQuery(defaultQuery)).toBe(false); + expect(isMatchAllQuery(matchAllQuery)).toBe(true); + expect(isMatchAllQuery(simpleQuery)).toBe(false); + }); + test('isSimpleQuery()', () => { expect(isSimpleQuery(defaultQuery)).toBe(true); expect(isSimpleQuery(matchAllQuery)).toBe(false); diff --git a/x-pack/legacy/plugins/transform/public/app/common/request.ts b/x-pack/legacy/plugins/transform/public/app/common/request.ts index 5d508f3d245d3..3b740de177ef8 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/request.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/request.ts @@ -53,6 +53,12 @@ export function isSimpleQuery(arg: any): arg is SimpleQuery { return arg.query_string !== undefined; } +export const matchAllQuery = { match_all: {} }; +export function isMatchAllQuery(query: any): boolean { + return query.match_all !== undefined && Object.keys(query.match_all).length === 0; +} + +export const defaultQuery: PivotQuery = { query_string: { query: '*' } }; export function isDefaultQuery(query: PivotQuery): boolean { return isSimpleQuery(query) && query.query_string.query === '*'; } diff --git a/x-pack/legacy/plugins/transform/public/app/constants/index.ts b/x-pack/legacy/plugins/transform/public/app/constants/index.ts index 85ffc222f59a2..78b5f018dd782 100644 --- a/x-pack/legacy/plugins/transform/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/constants/index.ts @@ -8,6 +8,7 @@ export const CLIENT_BASE_PATH = '/management/elasticsearch/transform'; export enum SECTION_SLUG { HOME = 'transform_management', + CLONE_TRANSFORM = 'clone_transform', CREATE_TRANSFORM = 'create_transform', } diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index 3e55d509a94ab..aba61766b5d2b 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/public'; +import { SavedObjectsClientContract, SimpleSavedObject, IUiSettingsClient } from 'src/core/public'; import { IndexPattern, esQuery, IndexPatternsContract, } from '../../../../../../../../src/plugins/data/public'; +import { matchAllQuery } from '../../common'; + type IndexPatternId = string; type SavedSearchId = string; -let indexPatternCache = []; +let indexPatternCache: Array>> = []; let fullIndexPatterns; let currentIndexPattern = null; let currentSavedSearch = null; @@ -53,6 +55,10 @@ export function loadIndexPatterns( }); } +export function getIndexPatternIdByTitle(indexPatternTitle: string): string | undefined { + return indexPatternCache.find(d => d?.attributes?.title === indexPatternTitle)?.id; +} + type CombinedQuery = Record<'bool', any> | unknown; export function loadCurrentIndexPattern( @@ -69,12 +75,20 @@ export function loadCurrentSavedSearch(savedSearches: any, savedSearchId: SavedS return currentSavedSearch; } +function isIndexPattern(arg: any): arg is IndexPattern { + return arg !== undefined; +} // Helper for creating the items used for searching and job creation. export function createSearchItems( indexPattern: IndexPattern | undefined, savedSearch: any, config: IUiSettingsClient -) { +): { + indexPattern: IndexPattern; + savedSearch: any; + query: any; + combinedQuery: CombinedQuery; +} { // query is only used by the data visualizer as it needs // a lucene query_string. // Using a blank query will cause match_all:{} to be used @@ -86,17 +100,13 @@ export function createSearchItems( let combinedQuery: CombinedQuery = { bool: { - must: [ - { - match_all: {}, - }, - ], + must: [matchAllQuery], }, }; - if (indexPattern === undefined && savedSearch !== null && savedSearch.id !== undefined) { + if (!isIndexPattern(indexPattern) && savedSearch !== null && savedSearch.id !== undefined) { const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index'); + indexPattern = searchSource.getField('index') as IndexPattern; query = searchSource.getField('query'); const fs = searchSource.getField('filter'); @@ -107,6 +117,10 @@ export function createSearchItems( combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); } + if (!isIndexPattern(indexPattern)) { + throw new Error('Index Pattern is not defined.'); + } + return { indexPattern, savedSearch, diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts index 82d5362e21c02..62107cb37ff2c 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { getIndexPatternIdByTitle, loadIndexPatterns } from './common'; export { useKibanaContext, InitializedKibanaContextValue, diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index 5b7702a0193ec..b0a0371d2de86 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -6,30 +6,26 @@ import React, { createContext, useContext, FC } from 'react'; +import { IUiSettingsClient } from 'kibana/public'; + import { SavedSearch } from '../../../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/types'; import { IndexPattern, IndexPatternsContract, } from '../../../../../../../../src/plugins/data/public'; -import { KibanaConfig } from '../../../../../../../../src/legacy/server/kbn_server'; - -// set() method is missing in original d.ts -interface KibanaConfigTypeFix extends KibanaConfig { - set(key: string, value: any): void; -} interface UninitializedKibanaContextValue { - initialized: boolean; + initialized: false; } export interface InitializedKibanaContextValue { combinedQuery: any; - currentIndexPattern: IndexPattern; - currentSavedSearch: SavedSearch; indexPatterns: IndexPatternsContract; - initialized: boolean; + initialized: true; kbnBaseUrl: string; - kibanaConfig: KibanaConfigTypeFix; + kibanaConfig: IUiSettingsClient; + currentIndexPattern: IndexPattern; + currentSavedSearch?: SavedSearch; } export type KibanaContextValue = UninitializedKibanaContextValue | InitializedKibanaContextValue; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx index 0a9de49168ad4..d2cf5f2b32910 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx @@ -17,7 +17,7 @@ import { loadCurrentSavedSearch, } from './common'; -import { KibanaContext, KibanaContextValue } from './kibana_context'; +import { InitializedKibanaContextValue, KibanaContext, KibanaContextValue } from './kibana_context'; const indexPatterns = npStart.plugins.data.indexPatterns; const savedObjectsClient = npStart.core.savedObjects.client; @@ -52,20 +52,20 @@ export const KibanaProvider: FC = ({ savedObjectId, children }) => { const kibanaConfig = npStart.core.uiSettings; - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - fetchedIndexPattern, - fetchedSavedSearch, - kibanaConfig - ); - - const kibanaContext = { + const { + indexPattern: currentIndexPattern, + savedSearch: currentSavedSearch, combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, + } = createSearchItems(fetchedIndexPattern, fetchedSavedSearch, kibanaConfig); + + const kibanaContext: InitializedKibanaContextValue = { indexPatterns, initialized: true, kbnBaseUrl: npStart.core.injectedMetadata.getBasePath(), kibanaConfig, + combinedQuery, + currentIndexPattern, + currentSavedSearch, }; setContextValue(kibanaContext); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx new file mode 100644 index 0000000000000..de96a4de32962 --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, FC } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiBetaBadge, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPageContent, + EuiPageContentBody, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { npStart } from 'ui/new_platform'; + +import { useApi } from '../../hooks/use_api'; + +import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { TransformPivotConfig } from '../../common'; +import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; +import { documentationLinksService } from '../../services/documentation'; +import { PrivilegesWrapper } from '../../lib/authorization'; +import { + getIndexPatternIdByTitle, + loadIndexPatterns, + KibanaProvider, + RenderOnlyWithInitializedKibanaContext, +} from '../../lib/kibana'; + +import { Wizard } from '../create_transform/components/wizard'; + +const indexPatterns = npStart.plugins.data.indexPatterns; +const savedObjectsClient = npStart.core.savedObjects.client; + +interface GetTransformsResponseOk { + count: number; + transforms: TransformPivotConfig[]; +} + +interface GetTransformsResponseError { + error: { + msg: string; + path: string; + query: any; + statusCode: number; + response: string; + }; +} + +function isGetTransformsResponseError(arg: any): arg is GetTransformsResponseError { + return arg.error !== undefined; +} + +type GetTransformsResponse = GetTransformsResponseOk | GetTransformsResponseError; + +type Props = RouteComponentProps<{ transformId: string }>; +export const CloneTransformSection: FC = ({ match }) => { + // Set breadcrumb and page title + useEffect(() => { + breadcrumbService.setBreadcrumbs(BREADCRUMB_SECTION.CLONE_TRANSFORM); + docTitleService.setTitle('createTransform'); + }, []); + + const api = useApi(); + + const transformId = match.params.transformId; + + const [transformConfig, setTransformConfig] = useState(); + const [errorMessage, setErrorMessage] = useState(); + const [isInitialized, setIsInitialized] = useState(false); + const [savedObjectId, setSavedObjectId] = useState(undefined); + + const fetchTransformConfig = async () => { + try { + const transformConfigs: GetTransformsResponse = await api.getTransforms(transformId); + if (isGetTransformsResponseError(transformConfigs)) { + setTransformConfig(undefined); + setErrorMessage(transformConfigs.error.msg); + setIsInitialized(true); + return; + } + + await loadIndexPatterns(savedObjectsClient, indexPatterns); + const indexPatternTitle = Array.isArray(transformConfigs.transforms[0].source.index) + ? transformConfigs.transforms[0].source.index.join(',') + : transformConfigs.transforms[0].source.index; + const indexPatternId = getIndexPatternIdByTitle(indexPatternTitle); + + if (indexPatternId === undefined) { + throw new Error( + i18n.translate('xpack.transform.clone.errorPromptText', { + defaultMessage: 'Could not fetch the Kibana index pattern ID.', + }) + ); + } + + setSavedObjectId(indexPatternId); + + setTransformConfig(transformConfigs.transforms[0]); + setErrorMessage(undefined); + setIsInitialized(true); + } catch (e) { + setTransformConfig(undefined); + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e, null, 2)); + } + setIsInitialized(true); + } + }; + + useEffect(() => { + fetchTransformConfig(); + // The effect should only be called once. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + + + +

+ +   + +

+
+ + + + + +
+
+ + + {typeof errorMessage !== 'undefined' && ( + +
{JSON.stringify(errorMessage)}
+
+ )} + {savedObjectId !== undefined && isInitialized === true && transformConfig !== undefined && ( + + + + + + )} +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts new file mode 100644 index 0000000000000..fef33d50130a7 --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CloneTransformSection } from './clone_transform_section'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts index 3fcc3cc15803b..e5c6783db1022 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts @@ -17,6 +17,7 @@ import { getDefaultSelectableFields, getFlattenedFields, isDefaultQuery, + matchAllQuery, EsDoc, EsDocSource, EsFieldName, @@ -75,7 +76,7 @@ export const useSourceIndexData = ( index: indexPattern.title, size: SEARCH_SIZE, // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - body: { query: isDefaultQuery(query) ? { match_all: {} } : query }, + body: { query: isDefaultQuery(query) ? matchAllQuery : query }, }); if (isErrorResponse(resp)) { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts index 7c5b60715961b..881e8c6b26658 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts @@ -5,8 +5,9 @@ */ export { + applyTransformConfigToDefineState, + getDefaultStepDefineState, StepDefineExposedState, StepDefineForm, - getDefaultStepDefineState, } from './step_define_form'; export { StepDefineSummary } from './step_define_summary'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index b8f63ef697e78..675386be8e2a5 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEqual } from 'lodash'; import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -27,7 +28,8 @@ import { EuiSwitch, } from '@elastic/eui'; -import { dictionaryToArray } from '../../../../../../common/types/common'; +import { TransformPivotConfig } from '../../../../common'; +import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; import { DropDown } from '../aggregation_dropdown'; import { AggListForm } from '../aggregation_list'; import { GroupByListForm } from '../group_by_list'; @@ -43,10 +45,12 @@ import { } from '../../../../lib/kibana'; import { - AggName, - DropDownLabel, getPivotQuery, getPreviewRequestBody, + isMatchAllQuery, + matchAllQuery, + AggName, + DropDownLabel, PivotAggDict, PivotAggsConfig, PivotAggsConfigDict, @@ -55,6 +59,7 @@ import { PivotGroupByConfigDict, PivotSupportedGroupByAggs, PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; import { getPivotDropdownOptions } from './common'; @@ -89,6 +94,58 @@ export function getDefaultStepDefineState( valid: false, }; } + +export function applyTransformConfigToDefineState( + state: StepDefineExposedState, + transformConfig?: TransformPivotConfig +): StepDefineExposedState { + // apply the transform configuration to wizard DEFINE state + if (transformConfig !== undefined) { + // transform aggregations config to wizard state + state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => { + const aggConfig = transformConfig.pivot.aggregations[aggName] as Dictionary; + const agg = Object.keys(aggConfig)[0]; + aggList[aggName] = { + ...aggConfig[agg], + agg: agg as PIVOT_SUPPORTED_AGGS, + aggName, + dropDownName: aggName, + } as PivotAggsConfig; + return aggList; + }, {} as PivotAggsConfigDict); + + // transform group by config to wizard state + state.groupByList = Object.keys(transformConfig.pivot.group_by).reduce( + (groupByList, groupByName) => { + const groupByConfig = transformConfig.pivot.group_by[groupByName] as Dictionary; + const groupBy = Object.keys(groupByConfig)[0]; + groupByList[groupByName] = { + agg: groupBy as PIVOT_SUPPORTED_GROUP_BY_AGGS, + aggName: groupByName, + dropDownName: groupByName, + ...groupByConfig[groupBy], + } as PivotGroupByConfig; + return groupByList; + }, + {} as PivotGroupByConfigDict + ); + + // only apply the query from the transform config to wizard state if it's not the default query + const query = transformConfig.source.query; + if (query !== undefined && !isEqual(query, matchAllQuery)) { + state.isAdvancedSourceEditorEnabled = true; + state.searchString = ''; + state.searchQuery = query; + state.sourceConfigUpdated = true; + } + + // applying a transform config to wizard state will always result in a valid configuration + state.valid = true; + } + + return state; +} + export function isAggNameConflict( aggName: AggName, aggList: PivotAggsConfigDict, @@ -208,10 +265,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const searchHandler = (d: Record) => { const { filterQuery, queryString } = d; const newSearch = queryString === emptySearch ? defaultSearch : queryString; - const newSearchQuery = - filterQuery.match_all && Object.keys(filterQuery.match_all).length === 0 - ? defaultSearch - : filterQuery; + const newSearchQuery = isMatchAllQuery(filterQuery) ? defaultSearch : filterQuery; setSearchString(newSearch); setSearchQuery(newSearchQuery); }; @@ -363,10 +417,10 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const aggConfigKeys = Object.keys(aggConfig); const agg = aggConfigKeys[0] as PivotSupportedGroupByAggs; newGroupByList[aggName] = { + ...aggConfig[agg], agg, aggName, dropDownName: '', - ...aggConfig[agg], }; }); } @@ -380,10 +434,10 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const aggConfigKeys = Object.keys(aggConfig); const agg = aggConfigKeys[0] as PIVOT_SUPPORTED_AGGS; newAggList[aggName] = { + ...aggConfig[agg], agg, aggName, dropDownName: '', - ...aggConfig[agg], }; }); } diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts index e454ea32d76ed..5cbdf4500e3c3 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts @@ -4,5 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { StepDetailsForm, getDefaultStepDetailsState } from './step_details_form'; +export { + applyTransformConfigToDetailsState, + getDefaultStepDetailsState, + StepDetailsForm, +} from './step_details_form'; export { StepDetailsSummary } from './step_details_summary'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index a01481fde343c..220923f88ed36 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -49,6 +49,22 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState { }; } +export function applyTransformConfigToDetailsState( + state: StepDetailsExposedState, + transformConfig?: TransformPivotConfig +): StepDetailsExposedState { + // apply the transform configuration to wizard DETAILS state + if (transformConfig !== undefined) { + const time = transformConfig.sync?.time; + if (time !== undefined) { + state.continuousModeDateField = time.field; + state.continuousModeDelay = time.delay; + state.isContinuousModeEnabled = true; + } + } + return state; +} + interface Props { overrides?: StepDetailsExposedState; onChange(s: StepDetailsExposedState): void; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 109cf81da6caa..f1861755d9742 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -12,16 +12,22 @@ import { EuiSteps, EuiStepStatus } from '@elastic/eui'; import { useKibanaContext } from '../../../../lib/kibana'; -import { getCreateRequestBody } from '../../../../common'; +import { getCreateRequestBody, TransformPivotConfig } from '../../../../common'; import { + applyTransformConfigToDefineState, + getDefaultStepDefineState, StepDefineExposedState, StepDefineForm, StepDefineSummary, - getDefaultStepDefineState, } from '../step_define'; import { getDefaultStepCreateState, StepCreateForm, StepCreateSummary } from '../step_create'; -import { getDefaultStepDetailsState, StepDetailsForm, StepDetailsSummary } from '../step_details'; +import { + applyTransformConfigToDetailsState, + getDefaultStepDetailsState, + StepDetailsForm, + StepDetailsSummary, +} from '../step_details'; import { WizardNav } from '../wizard_nav'; enum KBN_MANAGEMENT_PAGE_CLASSNAME { @@ -67,17 +73,25 @@ const StepDefine: FC = ({ ); }; -export const Wizard: FC = React.memo(() => { +interface WizardProps { + cloneConfig?: TransformPivotConfig; +} + +export const Wizard: FC = React.memo(({ cloneConfig }) => { const kibanaContext = useKibanaContext(); // The current WIZARD_STEP const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE); // The DEFINE state - const [stepDefineState, setStepDefineState] = useState(getDefaultStepDefineState(kibanaContext)); + const [stepDefineState, setStepDefineState] = useState( + applyTransformConfigToDefineState(getDefaultStepDefineState(kibanaContext), cloneConfig) + ); // The DETAILS state - const [stepDetailsState, setStepDetailsState] = useState(getDefaultStepDetailsState()); + const [stepDetailsState, setStepDetailsState] = useState( + applyTransformConfigToDetailsState(getDefaultStepDetailsState(), cloneConfig) + ); const stepDetails = currentStep === WIZARD_STEPS.DETAILS ? ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx new file mode 100644 index 0000000000000..40098ac7ef72a --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; + +import { + createCapabilityFailureMessage, + AuthorizationContext, +} from '../../../../lib/authorization'; + +import { CLIENT_BASE_PATH, SECTION_SLUG } from '../../../../constants'; + +interface CloneActionProps { + itemId: string; +} + +export const CloneAction: FC = ({ itemId }) => { + const history = useHistory(); + + const { canCreateTransform } = useContext(AuthorizationContext).capabilities; + + const buttonCloneText = i18n.translate('xpack.transform.transformList.cloneActionName', { + defaultMessage: 'Clone', + }); + + function clickHandler() { + history.push(`${CLIENT_BASE_PATH}/${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`); + } + + const cloneButton = ( + + {buttonCloneText} + + ); + + if (!canCreateTransform) { + const content = createCapabilityFailureMessage('canStartStopTransform'); + + return ( + + {cloneButton} + + ); + } + + return <>{cloneButton}; +}; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx index 3d847890b2bd5..ef92a5e3859d7 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx @@ -12,9 +12,10 @@ describe('Transform: Transform List Actions', () => { test('getActions()', () => { const actions = getActions({ forceDisable: false }); - expect(actions).toHaveLength(2); + expect(actions).toHaveLength(3); expect(actions[0].isPrimary).toBeTruthy(); expect(typeof actions[0].render).toBe('function'); expect(typeof actions[1].render).toBe('function'); + expect(typeof actions[2].render).toBe('function'); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx index 1773405e36e39..3e3829973e328 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { TransformListRow, TRANSFORM_STATE } from '../../../../common'; +import { CloneAction } from './action_clone'; import { StartAction } from './action_start'; import { StopAction } from './action_stop'; import { DeleteAction } from './action_delete'; @@ -21,6 +22,11 @@ export const getActions = ({ forceDisable }: { forceDisable: boolean }) => { return ; }, }, + { + render: (item: TransformListRow) => { + return ; + }, + }, { render: (item: TransformListRow) => { return ; diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts index 0e0b174f28f99..5a2f698b35154 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts @@ -10,6 +10,7 @@ import { linkToHome } from './links'; export enum BREADCRUMB_SECTION { MANAGEMENT = 'management', HOME = 'home', + CLONE_TRANSFORM = 'cloneTransform', CREATE_TRANSFORM = 'createTransform', } @@ -27,6 +28,7 @@ class BreadcrumbService { private breadcrumbs: Breadcrumbs = { management: [], home: [], + cloneTransform: [], createTransform: [], }; @@ -42,6 +44,12 @@ class BreadcrumbService { href: linkToHome(), }, ]; + this.breadcrumbs.cloneTransform = [ + ...this.breadcrumbs.home, + { + text: textService.breadcrumbs.cloneTransform, + }, + ]; this.breadcrumbs.createTransform = [ ...this.breadcrumbs.home, { diff --git a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts b/x-pack/legacy/plugins/transform/public/app/services/text/text.ts index df1b07e171c62..af4aea7e8db4e 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/text/text.ts @@ -14,6 +14,9 @@ class TextService { home: i18n.translate('xpack.transform.home.breadcrumbTitle', { defaultMessage: 'Transforms', }), + cloneTransform: i18n.translate('xpack.transform.cloneTransform.breadcrumbTitle', { + defaultMessage: 'Clone transform', + }), createTransform: i18n.translate('xpack.transform.createTransform.breadcrumbTitle', { defaultMessage: 'Create transform', }),