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', }),