From cd83f7a22f20f6941e07993b4d4cf090c62298e3 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 23 Jun 2020 14:28:48 -0400 Subject: [PATCH 01/20] init commit: component template wizard --- .../lib/component_template_serialization.ts | 14 ++ .../index_management/common/lib/index.ts | 1 + .../public/application/app.tsx | 2 + .../component_template_create.tsx | 65 ++++++ .../component_template_create/index.ts | 7 + .../component_template_list/empty_prompt.tsx | 11 +- .../component_template_list/table.tsx | 11 + .../components/component_templates/index.ts | 2 + .../component_template_form.tsx | 201 +++++++++++++++++ .../component_template_form/index.ts | 7 + .../component_template_form/steps/index.ts | 8 + .../steps/step_logistics.tsx | 151 +++++++++++++ .../steps/step_logistics_container.tsx} | 17 +- .../steps/step_review.tsx | 205 ++++++++++++++++++ .../steps/step_review_container.tsx | 26 +++ .../shared/components/index.ts | 7 + .../component_templates/shared/index.ts | 7 + .../component_templates/shared_imports.ts | 23 ++ .../components/shared/components/index.ts | 7 +- .../shared/components/wizard_steps/index.ts | 8 +- .../wizard_steps/step_aliases_container.tsx | 22 ++ .../wizard_steps}/step_mappings_container.tsx | 17 +- .../wizard_steps/step_settings_container.tsx | 22 ++ .../shared/components/wizard_steps/types.ts | 13 ++ .../application/components/shared/index.ts | 7 +- .../components/template_form/steps/index.ts | 3 - .../steps/step_settings_container.tsx | 23 -- .../template_form/template_form.tsx | 18 +- 28 files changed, 845 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx rename x-pack/plugins/index_management/public/application/components/{template_form/steps/step_aliases_container.tsx => component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx} (59%) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/step_mappings_container.tsx (57%) create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts delete mode 100644 x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts index 0db81bf81d3002..03ddacd473c5e0 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts @@ -8,6 +8,7 @@ import { ComponentTemplateFromEs, ComponentTemplateDeserialized, ComponentTemplateListItem, + ComponentTemplateSerialized, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; @@ -84,3 +85,16 @@ export function deserializeComponenTemplateList( return componentTemplateListItem; } + +// TODO add test +export function serializeComponentTemplate( + componentTemplateDeserialized: ComponentTemplateDeserialized +): ComponentTemplateSerialized { + const { version, template, _meta } = componentTemplateDeserialized; + + return { + version, + template, + _meta, + }; +} diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index fce4d8ccc2502b..efd79bf2afcf23 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -18,4 +18,5 @@ export { getTemplateParameter } from './utils'; export { deserializeComponentTemplate, deserializeComponenTemplateList, + serializeComponentTemplate, } from './component_template_serialization'; diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index 92197bee30c88f..3bb8343ea77101 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -16,6 +16,7 @@ import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { useServices } from './app_context'; +import { ComponentTemplateCreate } from './components'; export const App = ({ history }: { history: ScopedHistory }) => { const { uiMetricService } = useServices(); @@ -32,6 +33,7 @@ export const App = ({ history }: { history: ScopedHistory }) => { export const AppWithoutRouter = () => ( + diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx new file mode 100644 index 00000000000000..21a3c95c214019 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx @@ -0,0 +1,65 @@ +/* + * 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, { useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { ComponentTemplateDeserialized } from '../shared_imports'; +import { ComponentTemplateForm } from '../shared'; + +export const ComponentTemplateCreate: React.FunctionComponent = ({ + history, +}) => { + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + // TODO implement + const onSave = async (template: ComponentTemplateDeserialized) => { + // const { name } = template; + // setIsSaving(true); + // setSaveError(null); + // const { error } = await saveTemplate(template); + // setIsSaving(false); + // if (error) { + // setSaveError(error); + // return; + // } + // history.push(getTemplateDetailsLink(name, template._kbnMeta.isLegacy)); + }; + + const clearSaveError = () => { + setSaveError(null); + }; + + // TODO implement breadcrumb + // useEffect(() => { + // breadcrumbService.setBreadcrumbs('templateCreate'); + // }, []); + + return ( + + + +

+ +

+
+ + + + +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/index.ts new file mode 100644 index 00000000000000..6b0e02317888b5 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/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 { ComponentTemplateCreate } from './component_template_create'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx index edd9f77cbf635d..c4db16cd16f715 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx @@ -6,8 +6,9 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; +// import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { useComponentTemplatesContext } from '../component_templates_context'; export const EmptyPrompt: FunctionComponent = () => { @@ -38,6 +39,14 @@ export const EmptyPrompt: FunctionComponent = () => {

} + // TODO implement + // actions={ + // + // {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptButtonLabel', { + // defaultMessage: 'Create a component template', + // })} + // + // } /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index b67a249ae69765..bbe510f217f1f4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -85,6 +85,17 @@ export const ComponentTable: FunctionComponent = ({ defaultMessage: 'Reload', })} , + + {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.createButtonLabel', { + defaultMessage: 'Create a component template', + })} + , ], box: { incremental: true, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index 72e79a57ae4136..80570f4a443cde 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -9,3 +9,5 @@ export { ComponentTemplatesProvider } from './component_templates_context'; export { ComponentTemplateList } from './component_template_list'; export { ComponentTemplateDetailsFlyout } from './component_template_details'; + +export { ComponentTemplateCreate } from './component_template_create'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx new file mode 100644 index 00000000000000..8ec405fc5d23db --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx @@ -0,0 +1,201 @@ +/* + * 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, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer } from '@elastic/eui'; + +import { serializers, Forms, ComponentTemplateDeserialized } from '../../../shared_imports'; +// import { SectionError } from '../section_error'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; +import { + CommonWizardSteps, + StepSettingsContainer, + StepMappingsContainer, + StepAliasesContainer, +} from '../../../../shared'; + +const { stripEmptyFields } = serializers; +const { FormWizard, FormWizardStep } = Forms; + +interface Props { + onSave: (componentTemplate: ComponentTemplateDeserialized) => void; + clearSaveError: () => void; + isSaving: boolean; + saveError: any; + defaultValue?: ComponentTemplateDeserialized; + isEditing?: boolean; +} + +export interface WizardContent extends CommonWizardSteps { + logistics: Omit; +} + +export type WizardSection = keyof WizardContent | 'review'; + +const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { + logistics: { + id: 'logistics', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.logisticsStepName', { + defaultMessage: 'Logistics', + }), + }, + settings: { + id: 'settings', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.settingsStepName', { + defaultMessage: 'Index settings', + }), + }, + mappings: { + id: 'mappings', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.mappingsStepName', { + defaultMessage: 'Mappings', + }), + }, + aliases: { + id: 'aliases', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.aliasesStepName', { + defaultMessage: 'Aliases', + }), + }, + review: { + id: 'review', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.summaryStepName', { + defaultMessage: 'Review component template', + }), + }, +}; + +export const ComponentTemplateForm = ({ + defaultValue = { + name: '', + _meta: {}, + template: { + settings: {}, + mappings: {}, + aliases: {}, + }, + _kbnMeta: { + // TODO + usedBy: [], + }, + }, + isEditing, + isSaving, + saveError, + clearSaveError, + onSave, +}: Props) => { + const { + template: { settings, mappings, aliases }, + ...logistics + } = defaultValue; + + const wizardDefaultValue: WizardContent = { + logistics, + settings, + mappings, + aliases, + }; + + const i18nTexts = { + save: isEditing ? ( + + ) : ( + + ), + }; + + // TODO implement + const apiError = saveError ? ( + <> + {/* + } + error={saveError} + data-test-subj="saveComponentTemplateError" + /> */} + + + ) : null; + + const buildTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => ( + wizardData: WizardContent + ): ComponentTemplateDeserialized => ({ + ...initialTemplate, + ...wizardData.logistics, + template: { + settings: wizardData.settings, + mappings: wizardData.mappings, + aliases: wizardData.aliases, + }, + }); + + const onSaveTemplate = useCallback( + async (wizardData: WizardContent) => { + const template = buildTemplateObject(defaultValue)(wizardData); + + // We need to strip empty strings, otherwise if the "version" is not set, + // it will be an empty string and ES expects a number. + onSave( + stripEmptyFields(template, { + types: ['string'], + }) as ComponentTemplateDeserialized + ); + + clearSaveError(); + }, + [defaultValue, onSave, clearSaveError] + ); + + return ( + + defaultValue={wizardDefaultValue} + onSave={onSaveTemplate} + isEditing={isEditing} + isSaving={isSaving} + apiError={apiError} + texts={i18nTexts} + > + + + + + {/* TODO fix doc link */} + + + + + {/* TODO fix doc link */} + + + + + {/* TODO fix doc link */} + + + + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/index.ts new file mode 100644 index 00000000000000..84d9a2795ee2c0 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/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 { ComponentTemplateForm } from './component_template_form'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/index.ts new file mode 100644 index 00000000000000..b7e3e36e61814d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { StepLogisticsContainer } from './step_logistics_container'; +export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx new file mode 100644 index 00000000000000..e0287054b0c474 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx @@ -0,0 +1,151 @@ +/* + * 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 } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + useForm, + Form, + getUseField, + getFormRow, + Field, + Forms, + FieldConfig, + FIELD_TYPES, + fieldValidators, +} from '../../../../shared_imports'; + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +const { emptyField } = fieldValidators; + +const fieldsMeta = { + name: { + title: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameTitle', { + defaultMessage: 'Name', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.nameDescription', + { + defaultMessage: 'A unique identifier for this component template.', + } + ), + testSubject: 'nameField', + }, + version: { + title: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionTitle', { + defaultMessage: 'Version', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.versionDescription', + { + defaultMessage: 'A number that identifies the template to external management systems.', + } + ), + testSubject: 'versionField', + }, +}; + +const nameConfig: FieldConfig = { + defaultValue: undefined, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameFieldLabel', { + defaultMessage: 'Name', + }), + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.idxMgmt.componentTemplateForm.validation.nameRequiredError', { + defaultMessage: 'A component template name is required.', + }) + ), + }, + ], +}; + +interface Props { + defaultValue: { [key: string]: any }; + onChange: (content: Forms.Content) => void; + isEditing?: boolean; +} + +export const StepLogistics: React.FunctionComponent = React.memo( + ({ defaultValue, isEditing, onChange }) => { + const { form } = useForm({ + defaultValue, + options: { stripEmptyFields: false }, + }); + + useEffect(() => { + const validate = async () => { + return (await form.submit()).isValid; + }; + onChange({ + isValid: form.isValid, + validate, + getData: form.getFormData, + }); + }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps + + const { name, version } = fieldsMeta; + + return ( +
+ + + +

+ +

+
+
+ + + + + + +
+ + {/* Name */} + + + + {/* Version */} + {/* + + */} + + ); + } +); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx similarity index 59% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx index a0e0c59be6622e..867ecff799858d 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx @@ -6,18 +6,17 @@ import React from 'react'; import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepAliases } from '../../shared'; import { WizardContent } from '../template_form'; +import { StepLogistics } from './step_logistics'; -export const StepAliasesContainer = () => { - const { defaultValue, updateContent } = Forms.useContent('aliases'); +interface Props { + isEditing?: boolean; +} + +export const StepLogisticsContainer = ({ isEditing = false }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('logistics'); return ( - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review.tsx new file mode 100644 index 00000000000000..2e18edd6783711 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review.tsx @@ -0,0 +1,205 @@ +/* + * 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 from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiTitle, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiText, + EuiCodeBlock, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { WizardSection } from '../component_template_form'; +import { + ComponentTemplateDeserialized, + serializers, + serializeComponentTemplate, +} from '../../../../shared_imports'; + +const { stripEmptyFields } = serializers; + +const NoneDescriptionText = () => ( + +); + +const getDescriptionText = (data: any) => { + const hasEntries = data && Object.entries(data).length > 0; + + return hasEntries ? ( + + ) : ( + + ); +}; + +interface Props { + template: ComponentTemplateDeserialized; + navigateToStep: (stepId: WizardSection) => void; +} + +export const StepReview: React.FunctionComponent = React.memo( + ({ template, navigateToStep }) => { + const { name, version } = template!; + + const serializedComponentTemplate = serializeComponentTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as ComponentTemplateDeserialized + ); + + const { + template: { + mappings: serializedMappings, + settings: serializedSettings, + aliases: serializedAliases, + }, + } = serializedComponentTemplate; + + const SummaryTab = () => ( +
+ + + + + + {/* Version */} + + + + + {version ? version : } + + + + + + + + + + + {getDescriptionText(serializedSettings)} + + + + + + {getDescriptionText(serializedMappings)} + + + + + + {getDescriptionText(serializedAliases)} + + + + +
+ ); + + const RequestTab = () => { + const endpoint = `PUT _component_template/${name || ''}`; + const templateString = JSON.stringify(serializedComponentTemplate, null, 2); + const request = `${endpoint}\n${templateString}`; + + // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable + // levels. This way we prevent that happening for very large requests. + const language = request.length < 60000 ? 'json' : undefined; + + return ( +
+ + + +

+ +

+
+ + + + + {request} + +
+ ); + }; + + return ( +
+ +

+ +

+
+ + + + , + }, + { + id: 'request', + name: i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepReview.requestTabTitle', + { + defaultMessage: 'Request', + } + ), + content: , + }, + ]} + /> +
+ ); + } +); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx new file mode 100644 index 00000000000000..cafa8660b11503 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx @@ -0,0 +1,26 @@ +/* + * 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 from 'react'; + +import { TemplateDeserialized } from '../../../../../common'; +import { Forms } from '../../../../shared_imports'; +import { WizardContent, WizardSection } from '../template_form'; +import { StepReview } from './step_review'; + +interface Props { + getTemplateData: (wizardContent: WizardContent) => TemplateDeserialized; +} + +export const StepReviewContainer = React.memo(({ getTemplateData }: Props) => { + const { navigateToStep } = Forms.useFormWizardContext(); + const { getData } = Forms.useMultiContentContext(); + + const wizardContent = getData(); + // Build the final template object, providing the wizard content data + const template = getTemplateData(wizardContent); + + return ; +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/index.ts new file mode 100644 index 00000000000000..84d9a2795ee2c0 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/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 { ComponentTemplateForm } from './component_template_form'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts new file mode 100644 index 00000000000000..a81d0dcd900a0b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/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 { ComponentTemplateForm } from './components/component_template_form'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index 4e56f4a8c98189..f57a432f003e71 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -18,8 +18,29 @@ export { Error, useAuthorizationContext, NotAuthorizedSection, + Forms, } from '../../../../../../../src/plugins/es_ui_shared/public'; +export { + serializers, + fieldValidators, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + FormSchema, + FIELD_TYPES, + VALIDATION_TYPES, + FieldConfig, + useForm, + Form, + getUseField, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + getFormRow, + Field, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/components'; + export { TabMappings, TabSettings, TabAliases } from '../shared'; export { @@ -27,3 +48,5 @@ export { ComponentTemplateDeserialized, ComponentTemplateListItem, } from '../../../../common'; + +export { serializeComponentTemplate } from '../../../../common/lib'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts index e1700ad6a632d2..b67a9c355e723d 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts @@ -6,4 +6,9 @@ export { TabAliases, TabMappings, TabSettings } from './details_panel'; -export { StepAliases, StepMappings, StepSettings } from './wizard_steps'; +export { + StepAliasesContainer, + StepMappingsContainer, + StepSettingsContainer, + CommonWizardSteps, +} from './wizard_steps'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts index 90ce6227c09c88..ea554ca269d8bd 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { StepAliases } from './step_aliases'; -export { StepMappings } from './step_mappings'; -export { StepSettings } from './step_settings'; +export { StepAliasesContainer } from './step_aliases_container'; +export { StepMappingsContainer } from './step_mappings_container'; +export { StepSettingsContainer } from './step_settings_container'; + +export { CommonWizardSteps } from './types'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx new file mode 100644 index 00000000000000..a5953ea00a1063 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx @@ -0,0 +1,22 @@ +/* + * 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 from 'react'; + +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepAliases } from './step_aliases'; + +interface Props { + esDocsBase: string; +} + +export const StepAliasesContainer: React.FunctionComponent = ({ esDocsBase }) => { + const { defaultValue, updateContent } = Forms.useContent('aliases'); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx similarity index 57% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 80c0d1d4df4890..34e05d88c651d7 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -5,20 +5,23 @@ */ import React from 'react'; -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepMappings } from '../../shared'; -import { WizardContent } from '../template_form'; +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepMappings } from './step_mappings'; -export const StepMappingsContainer = () => { - const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); +interface Props { + esDocsBase: string; +} + +export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { + const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); return ( ); }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx new file mode 100644 index 00000000000000..c540ddceb95c2f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx @@ -0,0 +1,22 @@ +/* + * 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 from 'react'; + +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepSettings } from './step_settings'; + +interface Props { + esDocsBase: string; +} + +export const StepSettingsContainer = React.memo(({ esDocsBase }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('settings'); + + return ( + + ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts new file mode 100644 index 00000000000000..f8088e2b6e058b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts @@ -0,0 +1,13 @@ +/* + * 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 { Mappings, IndexSettings, Aliases } from '../../../../../../common'; + +export interface CommonWizardSteps { + settings?: IndexSettings; + mappings?: Mappings; + aliases?: Aliases; +} diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts index 5ec1f717102709..897e86c99eca0b 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts @@ -8,7 +8,8 @@ export { TabAliases, TabMappings, TabSettings, - StepAliases, - StepMappings, - StepSettings, + StepAliasesContainer, + StepMappingsContainer, + StepSettingsContainer, + CommonWizardSteps, } from './components'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts index 95d1222ad2cc9a..b7e3e36e61814d 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts @@ -5,7 +5,4 @@ */ export { StepLogisticsContainer } from './step_logistics_container'; -export { StepAliasesContainer } from './step_aliases_container'; -export { StepMappingsContainer } from './step_mappings_container'; -export { StepSettingsContainer } from './step_settings_container'; export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx deleted file mode 100644 index b79c6804d382b3..00000000000000 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 from 'react'; - -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepSettings } from '../../shared'; -import { WizardContent } from '../template_form'; - -export const StepSettingsContainer = React.memo(() => { - const { defaultValue, updateContent } = Forms.useContent('settings'); - - return ( - - ); -}); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 9e6d49faac5630..8a2c991aea8d07 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -11,13 +11,14 @@ import { EuiSpacer } from '@elastic/eui'; import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; import { serializers, Forms } from '../../../shared_imports'; import { SectionError } from '../section_error'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; import { - StepLogisticsContainer, + CommonWizardSteps, StepSettingsContainer, StepMappingsContainer, StepAliasesContainer, - StepReviewContainer, -} from './steps'; +} from '../shared'; +import { documentationService } from '../../services/documentation'; const { stripEmptyFields } = serializers; const { FormWizard, FormWizardStep } = Forms; @@ -31,11 +32,8 @@ interface Props { isEditing?: boolean; } -export interface WizardContent { +export interface WizardContent extends CommonWizardSteps { logistics: Omit; - settings: TemplateDeserialized['template']['settings']; - mappings: TemplateDeserialized['template']['mappings']; - aliases: TemplateDeserialized['template']['aliases']; } export type WizardSection = keyof WizardContent | 'review'; @@ -183,15 +181,15 @@ export const TemplateForm = ({ - + - + - + From 6ea26f56b39f97ed36ae86bac68bbaaa294097d1 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 23 Jun 2020 21:36:33 -0400 Subject: [PATCH 02/20] build out logistics form --- .../component_templates/lib/documentation.ts | 1 + .../steps/step_logistics.tsx | 201 ++++++++++++------ .../steps/step_logistics_container.tsx | 2 +- .../steps/step_logistics_schema.tsx | 111 ++++++++++ .../steps/step_review_container.tsx | 7 +- .../component_templates/shared_imports.ts | 4 + 6 files changed, 252 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts index dc27dadf0b8073..b2a6c94d339ef7 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts @@ -12,5 +12,6 @@ export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocL return { componentTemplates: `${esDocsBase}/indices-component-template.html`, + componentTemplatesMetadata: `${esDocsBase}/indices-component-template.html#component-templates-metadata`, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx index e0287054b0c474..f72dc0519cb444 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx @@ -3,8 +3,16 @@ * 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 } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiSwitch, + EuiLink, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -14,60 +22,15 @@ import { getFormRow, Field, Forms, - FieldConfig, - FIELD_TYPES, - fieldValidators, + JsonEditorField, } from '../../../../shared_imports'; +import { logisticsFormSchema } from './step_logistics_schema'; +import { useComponentTemplatesContext } from '../../../../component_templates_context'; + const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); -const { emptyField } = fieldValidators; - -const fieldsMeta = { - name: { - title: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameTitle', { - defaultMessage: 'Name', - }), - description: i18n.translate( - 'xpack.idxMgmt.componentTemplateForm.stepLogistics.nameDescription', - { - defaultMessage: 'A unique identifier for this component template.', - } - ), - testSubject: 'nameField', - }, - version: { - title: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionTitle', { - defaultMessage: 'Version', - }), - description: i18n.translate( - 'xpack.idxMgmt.componentTemplateForm.stepLogistics.versionDescription', - { - defaultMessage: 'A number that identifies the template to external management systems.', - } - ), - testSubject: 'versionField', - }, -}; - -const nameConfig: FieldConfig = { - defaultValue: undefined, - label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameFieldLabel', { - defaultMessage: 'Name', - }), - type: FIELD_TYPES.TEXT, - validations: [ - { - validator: emptyField( - i18n.translate('xpack.idxMgmt.componentTemplateForm.validation.nameRequiredError', { - defaultMessage: 'A component template name is required.', - }) - ), - }, - ], -}; - interface Props { defaultValue: { [key: string]: any }; onChange: (content: Forms.Content) => void; @@ -77,10 +40,21 @@ interface Props { export const StepLogistics: React.FunctionComponent = React.memo( ({ defaultValue, isEditing, onChange }) => { const { form } = useForm({ + schema: logisticsFormSchema, defaultValue, options: { stripEmptyFields: false }, }); + const { documentation } = useComponentTemplatesContext(); + + const [isVersionVisible, setIsVersionVisible] = useState( + Boolean(defaultValue.version) + ); + + const [isMetaVisible, setIsMetaVisible] = useState( + Boolean(Object.keys(defaultValue._meta).length) + ); + useEffect(() => { const validate = async () => { return (await form.submit()).isValid; @@ -92,8 +66,6 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps - const { name, version } = fieldsMeta; - return (
@@ -112,39 +84,130 @@ export const StepLogistics: React.FunctionComponent = React.memo( - {/* Name */} - + {/* Name with optional version field */} + + } + description={ + <> + + + + } + checked={isVersionVisible} + onChange={(e) => setIsVersionVisible(e.target.checked)} + data-test-subj="versionToggle" + /> + + } + > + + {isVersionVisible && ( + + )} + + + {/* _meta field */} + + } + description={ + <> + + {i18n.translate('xpack.ingestPipelines.form.metaDocumentionLink', { + defaultMessage: 'Learn more', + })} + + ), + }} + /> + + + } + checked={isMetaVisible} + onChange={(e) => setIsMetaVisible(e.target.checked)} + data-test-subj="metaToggle" + /> + + } + > + {isMetaVisible ? ( + + ) : ( + // requires children or a field + // For now, we return an empty
if the editor is not visible +
+ )} - {/* Version */} - {/* - - */} ); } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx index 867ecff799858d..03d5f6f8aebfdd 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_container.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { Forms } from '../../../../shared_imports'; -import { WizardContent } from '../template_form'; +import { WizardContent } from '../component_template_form'; import { StepLogistics } from './step_logistics'; interface Props { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx new file mode 100644 index 00000000000000..a3ec69a2357e31 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx @@ -0,0 +1,111 @@ +/* + * 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 from 'react'; +import { EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + fieldFormatters, + FormSchema, +} from '../../../../shared_imports'; + +const { emptyField, containsCharsField, isJsonField } = fieldValidators; +const { toInt } = fieldFormatters; + +const stringifyJson = (json: any): string => + Array.isArray(json) ? JSON.stringify(json, null, 2) : '{\n\n}'; + +const parseJson = (jsonString: string): object => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + } catch { + parsedJSON = {}; + } + + return parsedJSON; +}; + +export const logisticsFormSchema: FormSchema = { + name: { + defaultValue: undefined, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameFieldLabel', { + defaultMessage: 'Name', + }), + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.idxMgmt.componentTemplateForm.validation.nameRequiredError', { + defaultMessage: 'A component template name is required.', + }) + ), + }, + { + validator: containsCharsField({ + chars: ' ', + message: i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.nameSpacesError', + { + defaultMessage: 'Spaces are not allowed in a component template name.', + } + ), + }), + }, + ], + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, + _meta: { + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', { + defaultMessage: 'Metadata (optional)', + }), + helpText: ( + {JSON.stringify({ arbitrary_data: 'anything_goes' })}, + }} + /> + ), + serializer: (value) => { + const result = parseJson(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(result).length) { + return undefined; + } + return result; + }, + deserializer: stringifyJson, + validations: [ + { + validator: (validationArg) => { + if (!validationArg.value) { + return; + } + return isJsonField( + i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.metaJsonError', + { + defaultMessage: 'The input is not valid.', + } + ) + )(validationArg); + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx index cafa8660b11503..90edc9d0ebfb95 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx @@ -5,13 +5,12 @@ */ import React from 'react'; -import { TemplateDeserialized } from '../../../../../common'; -import { Forms } from '../../../../shared_imports'; -import { WizardContent, WizardSection } from '../template_form'; +import { Forms, ComponentTemplateDeserialized } from '../../../../shared_imports'; +import { WizardContent, WizardSection } from '../component_template_form'; import { StepReview } from './step_review'; interface Props { - getTemplateData: (wizardContent: WizardContent) => TemplateDeserialized; + getTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized; } export const StepReviewContainer = React.memo(({ getTemplateData }: Props) => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index f57a432f003e71..bfe0c76497da6e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -24,6 +24,7 @@ export { export { serializers, fieldValidators, + fieldFormatters, } from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; export { @@ -39,8 +40,11 @@ export { export { getFormRow, Field, + JsonEditorField, } from '../../../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { isJSON } from '../../../../../../../src/plugins/es_ui_shared/static/validators/string'; + export { TabMappings, TabSettings, TabAliases } from '../shared'; export { From 5ec33800130ef4b1cfa3ac029396db902f5fafb9 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 24 Jun 2020 14:05:46 -0400 Subject: [PATCH 03/20] implement save + cleanup --- .../component_template_serialization.test.ts | 203 +++++++++---- .../lib/component_template_serialization.ts | 1 - .../component_template_create.tsx | 31 +- .../component_template_list.tsx | 2 +- .../component_template_list/empty_prompt.tsx | 28 +- .../component_template_list/table.tsx | 2 +- .../component_templates/constants.ts | 1 + .../components/component_templates/lib/api.ts | 25 +- .../component_templates/lib/documentation.ts | 1 + .../component_template_form.tsx | 35 ++- .../steps/step_logistics.tsx | 9 +- .../steps/step_review.tsx | 282 +++++++++--------- .../steps/step_review_container.tsx | 11 +- .../routes/api/component_templates/create.ts | 7 +- .../component_templates/schema_validation.ts | 3 + 15 files changed, 385 insertions(+), 256 deletions(-) diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts index eaa7f24017a2f8..83682f45918e3e 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts @@ -4,91 +4,164 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserializeComponentTemplate } from './component_template_serialization'; +import { + deserializeComponentTemplate, + serializeComponentTemplate, +} from './component_template_serialization'; -describe('deserializeComponentTemplate', () => { - test('deserializes a component template', () => { - expect( - deserializeComponentTemplate( - { - name: 'my_component_template', - component_template: { - version: 1, - _meta: { - serialization: { - id: 10, - class: 'MyComponentTemplate', - }, - description: 'set number of shards to one', - }, - template: { - settings: { - number_of_shards: 1, +describe('Component template serialization', () => { + describe('deserializeComponentTemplate()', () => { + test('deserializes a component template', () => { + expect( + deserializeComponentTemplate( + { + name: 'my_component_template', + component_template: { + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', }, - mappings: { - _source: { - enabled: false, + template: { + settings: { + number_of_shards: 1, }, - properties: { - host_name: { - type: 'keyword', + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, }, }, }, - }, - [ - { - name: 'my_index_template', - index_template: { - index_patterns: ['foo'], - template: { - settings: { - number_of_replicas: 2, + [ + { + name: 'my_index_template', + index_template: { + index_patterns: ['foo'], + template: { + settings: { + number_of_replicas: 2, + }, }, + composed_of: ['my_component_template'], + }, + }, + ] + ) + ).toEqual({ + name: 'my_component_template', + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', }, - composed_of: ['my_component_template'], }, }, - ] - ) - ).toEqual({ - name: 'my_component_template', - version: 1, - _meta: { - serialization: { - id: 10, - class: 'MyComponentTemplate', }, - description: 'set number of shards to one', - }, - template: { - settings: { - number_of_shards: 1, + _kbnMeta: { + usedBy: ['my_index_template'], }, - mappings: { - _source: { - enabled: false, + }); + }); + }); + + describe('serializeComponentTemplate()', () => { + test('serialize a component template', () => { + expect( + serializeComponentTemplate({ + name: 'my_component_template', + version: 1, + _kbnMeta: { + usedBy: [], + }, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }) + ).toEqual({ + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', }, - properties: { - host_name: { - type: 'keyword', + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, }, - }, - _kbnMeta: { - usedBy: ['my_index_template'], - }, + }); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts index 03ddacd473c5e0..672b8140f79fb5 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts @@ -86,7 +86,6 @@ export function deserializeComponenTemplateList( return componentTemplateListItem; } -// TODO add test export function serializeComponentTemplate( componentTemplateDeserialized: ComponentTemplateDeserialized ): ComponentTemplateSerialized { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx index 21a3c95c214019..698db969967122 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx @@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { ComponentTemplateDeserialized } from '../shared_imports'; import { ComponentTemplateForm } from '../shared'; +import { useComponentTemplatesContext } from '../component_templates_context'; export const ComponentTemplateCreate: React.FunctionComponent = ({ history, @@ -16,18 +17,24 @@ export const ComponentTemplateCreate: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); - // TODO implement - const onSave = async (template: ComponentTemplateDeserialized) => { - // const { name } = template; - // setIsSaving(true); - // setSaveError(null); - // const { error } = await saveTemplate(template); - // setIsSaving(false); - // if (error) { - // setSaveError(error); - // return; - // } - // history.push(getTemplateDetailsLink(name, template._kbnMeta.isLegacy)); + const { api } = useComponentTemplatesContext(); + + const onSave = async (componentTemplate: ComponentTemplateDeserialized) => { + const { name } = componentTemplate; + + setIsSaving(true); + setSaveError(null); + + const { error } = await api.createComponentTemplate(componentTemplate); + + setIsSaving(false); + + if (error) { + setSaveError(error); + return; + } + + history.push(`/component_templates/${encodeURIComponent(name)}`); }; const clearSaveError = () => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 05a5ed462d8f72..52ced41c5f95f2 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -44,7 +44,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ }, [trackMetric]); if (data && data.length === 0) { - return ; + return ; } let content: React.ReactNode; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx index c4db16cd16f715..fbb1968491ff65 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx @@ -6,12 +6,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { RouteComponentProps } from 'react-router-dom'; import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; -// import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { useComponentTemplatesContext } from '../component_templates_context'; -export const EmptyPrompt: FunctionComponent = () => { +interface Props { + history: RouteComponentProps['history']; +} + +export const EmptyPrompt: FunctionComponent = ({ history }) => { const { documentation } = useComponentTemplatesContext(); return ( @@ -39,14 +44,17 @@ export const EmptyPrompt: FunctionComponent = () => {

} - // TODO implement - // actions={ - // - // {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptButtonLabel', { - // defaultMessage: 'Create a component template', - // })} - // - // } + actions={ + + {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptButtonLabel', { + defaultMessage: 'Create a component template', + })} + + } /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index bbe510f217f1f4..fa10f80423bd2c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -90,7 +90,7 @@ export const ComponentTable: FunctionComponent = ({ iconType="plusInCircle" data-test-subj="createPipelineButton" key="createPipelineButton" - // {...reactRouterNavigate(history, '/create')} + {...reactRouterNavigate(history, '/create_component_template')} > {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.createButtonLabel', { defaultMessage: 'Create a component template', diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index e9acfa8dcc56d1..514d24026456bf 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -9,6 +9,7 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load'; export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details'; +export const UIM_COMPONENT_TEMPLATE_CREATE = 'component_template_create'; // privileges export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 4a8cf965adfb92..e6701c83a64e06 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -4,8 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../shared_imports'; -import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants'; +import { + ComponentTemplateListItem, + ComponentTemplateDeserialized, + ComponentTemplateSerialized, +} from '../shared_imports'; +import { + UIM_COMPONENT_TEMPLATE_DELETE_MANY, + UIM_COMPONENT_TEMPLATE_DELETE, + UIM_COMPONENT_TEMPLATE_CREATE, +} from '../constants'; import { UseRequestHook, SendRequestHook } from './request'; export const getApi = ( @@ -44,9 +52,22 @@ export const getApi = ( }); } + async function createComponentTemplate(componentTemplate: ComponentTemplateSerialized) { + const result = await sendRequest({ + path: `${apiBasePath}/component_templates`, + method: 'post', + body: JSON.stringify(componentTemplate), + }); + + trackMetric('count', UIM_COMPONENT_TEMPLATE_CREATE); + + return result; + } + return { useLoadComponentTemplates, deleteComponentTemplates, useLoadComponentTemplate, + createComponentTemplate, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts index b2a6c94d339ef7..2c45c0beb2bd32 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts @@ -11,6 +11,7 @@ export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocL const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; return { + esDocsBase, componentTemplates: `${esDocsBase}/indices-component-template.html`, componentTemplatesMetadata: `${esDocsBase}/indices-component-template.html#component-templates-metadata`, }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx index 8ec405fc5d23db..0d615d3ede2e43 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx @@ -6,10 +6,9 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { serializers, Forms, ComponentTemplateDeserialized } from '../../../shared_imports'; -// import { SectionError } from '../section_error'; import { StepLogisticsContainer, StepReviewContainer } from './steps'; import { CommonWizardSteps, @@ -17,6 +16,7 @@ import { StepMappingsContainer, StepAliasesContainer, } from '../../../../shared'; +import { useComponentTemplatesContext } from '../../../component_templates_context'; const { stripEmptyFields } = serializers; const { FormWizard, FormWizardStep } = Forms; @@ -72,14 +72,13 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { export const ComponentTemplateForm = ({ defaultValue = { name: '', - _meta: {}, template: { settings: {}, mappings: {}, aliases: {}, }, + _meta: {}, _kbnMeta: { - // TODO usedBy: [], }, }, @@ -94,6 +93,8 @@ export const ComponentTemplateForm = ({ ...logistics } = defaultValue; + const { documentation } = useComponentTemplatesContext(); + const wizardDefaultValue: WizardContent = { logistics, settings, @@ -115,19 +116,21 @@ export const ComponentTemplateForm = ({ ), }; - // TODO implement const apiError = saveError ? ( <> - {/* } - error={saveError} + color="danger" + iconType="alert" data-test-subj="saveComponentTemplateError" - /> */} + > +
{saveError.message || saveError.statusText}
+ ) : null; @@ -148,11 +151,10 @@ export const ComponentTemplateForm = ({ async (wizardData: WizardContent) => { const template = buildTemplateObject(defaultValue)(wizardData); - // We need to strip empty strings, otherwise if the "version" is not set, - // it will be an empty string and ES expects a number. + // This will strip an empty string if "version" is not set, as well as an empty "_meta" object onSave( stripEmptyFields(template, { - types: ['string'], + types: ['string', 'object'], }) as ComponentTemplateDeserialized ); @@ -178,23 +180,20 @@ export const ComponentTemplateForm = ({ - {/* TODO fix doc link */} - + - {/* TODO fix doc link */} - + - {/* TODO fix doc link */} - + - + ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx index f72dc0519cb444..f75d687fbcbe1a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx @@ -52,13 +52,14 @@ export const StepLogistics: React.FunctionComponent = React.memo( ); const [isMetaVisible, setIsMetaVisible] = useState( - Boolean(Object.keys(defaultValue._meta).length) + Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length) ); useEffect(() => { const validate = async () => { return (await form.submit()).isValid; }; + onChange({ isValid: form.isValid, validate, @@ -95,7 +96,9 @@ export const StepLogistics: React.FunctionComponent = React.memo( + + {/* Name with optional version field */} = React.memo( id="xpack.idxMgmt.componentTemplateForm.stepLogistics.nameDescription" defaultMessage="A unique identifier for this component template." /> + + = React.memo( ), }} /> + + { }; interface Props { - template: ComponentTemplateDeserialized; - navigateToStep: (stepId: WizardSection) => void; + componentTemplate: ComponentTemplateDeserialized; } -export const StepReview: React.FunctionComponent = React.memo( - ({ template, navigateToStep }) => { - const { name, version } = template!; +export const StepReview: React.FunctionComponent = React.memo(({ componentTemplate }) => { + const { name, version } = componentTemplate!; - const serializedComponentTemplate = serializeComponentTemplate( - stripEmptyFields(template!, { - types: ['string'], - }) as ComponentTemplateDeserialized - ); - - const { - template: { - mappings: serializedMappings, - settings: serializedSettings, - aliases: serializedAliases, - }, - } = serializedComponentTemplate; - - const SummaryTab = () => ( -
- - - - - - {/* Version */} - - - - - {version ? version : } - - - - - - - - - - - {getDescriptionText(serializedSettings)} - - - - - - {getDescriptionText(serializedMappings)} - - - - - - {getDescriptionText(serializedAliases)} - - - - -
- ); - - const RequestTab = () => { - const endpoint = `PUT _component_template/${name || ''}`; - const templateString = JSON.stringify(serializedComponentTemplate, null, 2); - const request = `${endpoint}\n${templateString}`; - - // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable - // levels. This way we prevent that happening for very large requests. - const language = request.length < 60000 ? 'json' : undefined; + const serializedComponentTemplate = serializeComponentTemplate( + stripEmptyFields(componentTemplate!, { + types: ['string', 'object'], + }) as ComponentTemplateDeserialized + ); - return ( -
- + const { + template: { + mappings: serializedMappings, + settings: serializedSettings, + aliases: serializedAliases, + }, + _meta: serializedMeta, + } = serializedComponentTemplate; + + const SummaryTab = () => ( +
+ + + + + + {/* Version */} + + + + + {version ? version : } + - -

+ {/* Metadata */} + + + + + {serializedMeta ? ( + + {JSON.stringify(serializedMeta, null, 2)} + + ) : ( + + )} + + + + + + + + + + + {getDescriptionText(serializedSettings)} + + -

-
+ + + {getDescriptionText(serializedMappings)} + + + + + + {getDescriptionText(serializedAliases)} + +
+
+
+
+ ); - + const RequestTab = () => { + const endpoint = `PUT _component_template/${name || ''}`; + const templateString = JSON.stringify(serializedComponentTemplate, null, 2); + const request = `${endpoint}\n${templateString}`; - - {request} - -
- ); - }; + // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable + // levels. This way we prevent that happening for very large requests. + const language = request.length < 60000 ? 'json' : undefined; return ( -
- -

+
+ + + +

-

-
- - - - , - }, - { - id: 'request', - name: i18n.translate( - 'xpack.idxMgmt.componentTemplateForm.stepReview.requestTabTitle', - { - defaultMessage: 'Request', - } - ), - content: , - }, - ]} - /> +

+ + + + + + {request} +
); - } -); + }; + + return ( +
+ +

+ +

+
+ + + + , + }, + { + id: 'request', + name: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepReview.requestTabTitle', { + defaultMessage: 'Request', + }), + content: , + }, + ]} + /> +
+ ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx index 90edc9d0ebfb95..b532da4c8a96bc 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx @@ -6,20 +6,19 @@ import React from 'react'; import { Forms, ComponentTemplateDeserialized } from '../../../../shared_imports'; -import { WizardContent, WizardSection } from '../component_template_form'; +import { WizardContent } from '../component_template_form'; import { StepReview } from './step_review'; interface Props { - getTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized; + getComponentTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized; } -export const StepReviewContainer = React.memo(({ getTemplateData }: Props) => { - const { navigateToStep } = Forms.useFormWizardContext(); +export const StepReviewContainer = React.memo(({ getComponentTemplateData }: Props) => { const { getData } = Forms.useMultiContentContext(); const wizardContent = getData(); // Build the final template object, providing the wizard content data - const template = getTemplateData(wizardContent); + const componentTemplate = getComponentTemplateData(wizardContent); - return ; + return ; }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index 175254ca16e3d0..80e44ae2cd0aee 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; +import { serializeComponentTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; @@ -30,7 +31,9 @@ export const registerCreateRoute = ({ license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const { name, ...componentTemplateDefinition } = req.body; + const serializedComponentTemplate = serializeComponentTemplate(req.body); + + const { name } = req.body; try { // Check that a component template with the same name doesn't already exist @@ -60,7 +63,7 @@ export const registerCreateRoute = ({ try { const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { name, - body: componentTemplateDefinition, + body: serializedComponentTemplate, }); return res.ok({ body: response }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts index 7d32637c6b9779..0bdd102dba42fc 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -13,4 +13,7 @@ export const componentTemplateSchema = { }), version: schema.maybe(schema.number()), _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + _kbnMeta: schema.object({ + usedBy: schema.arrayOf(schema.string()), + }), }; From 7a97ce9030dbdc232faae78a1e2a45c2840d068a Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 25 Jun 2020 08:41:58 -0400 Subject: [PATCH 04/20] add edit flow --- .../public/application/app.tsx | 3 +- .../component_template_edit.tsx | 116 ++++++++++++++++++ .../component_template_edit/index.ts | 7 ++ .../component_template_list.tsx | 12 ++ .../component_template_list/table.tsx | 17 +++ .../components/component_templates/index.ts | 2 + .../components/component_templates/lib/api.ts | 14 +++ .../steps/step_logistics.tsx | 11 ++ .../routes/api/component_templates/create.ts | 8 +- .../component_templates/schema_validation.ts | 5 +- .../routes/api/component_templates/update.ts | 4 +- 11 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/index.ts diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index 3bb8343ea77101..d4a830c7c632b4 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -16,7 +16,7 @@ import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { useServices } from './app_context'; -import { ComponentTemplateCreate } from './components'; +import { ComponentTemplateCreate, ComponentTemplateEdit } from './components'; export const App = ({ history }: { history: ScopedHistory }) => { const { uiMetricService } = useServices(); @@ -36,6 +36,7 @@ export const AppWithoutRouter = () => ( + diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx new file mode 100644 index 00000000000000..69d20dabcd2610 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx @@ -0,0 +1,116 @@ +/* + * 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, { useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { useComponentTemplatesContext } from '../component_templates_context'; +import { ComponentTemplateDeserialized, SectionLoading } from '../shared_imports'; +import { ComponentTemplateForm } from '../shared'; + +interface MatchParams { + name: string; +} + +export const ComponentTemplateEdit: React.FunctionComponent> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { api } = useComponentTemplatesContext(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(name); + + // TODO breadcrumbs + // useEffect(() => { + // breadcrumbService.setBreadcrumbs('templateEdit'); + // }, []); + + const onSave = async (updatedComponentTemplate: ComponentTemplateDeserialized) => { + setIsSaving(true); + setSaveError(null); + + const { error: saveErrorObject } = await api.updateComponentTemplate(updatedComponentTemplate); + + setIsSaving(false); + + if (saveErrorObject) { + setSaveError(saveErrorObject); + return; + } + + history.push(`/component_templates/${encodeURIComponent(name)}`); + }; + + const clearSaveError = () => { + setSaveError(null); + }; + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="loadComponentTemplateError" + > +
{error.message}
+
+ + + ); + } else if (componentTemplate) { + content = ( + + ); + } + + return ( + + + +

+ +

+
+ + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/index.ts new file mode 100644 index 00000000000000..1f877bdae24f03 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/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 { ComponentTemplateEdit } from './component_template_edit'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 52ced41c5f95f2..d8c098c0036e5d 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -38,6 +38,10 @@ export const ComponentTemplateList: React.FunctionComponent = ({ return history.push('component_templates'); }; + const goToEditComponentTemplate = (name: string) => { + return history.push(`edit_component_template/${encodeURIComponent(name)}`); + }; + // Track component loaded useEffect(() => { trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD); @@ -64,6 +68,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ componentTemplates={data} onReloadClick={sendRequest} onDeleteClick={setComponentTemplatesToDelete} + onEditClick={goToEditComponentTemplate} history={history as ScopedHistory} /> ); @@ -97,6 +102,13 @@ export const ComponentTemplateList: React.FunctionComponent = ({ onClose={goToList} componentTemplateName={componentTemplateName} actions={[ + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.editButtonLabel', { + defaultMessage: 'Edit', + }), + icon: 'pencil', + handleActionClick: () => goToEditComponentTemplate(componentTemplateName), + }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { defaultMessage: 'Delete', diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index fa10f80423bd2c..724d1bafdeaefb 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -25,6 +25,7 @@ export interface Props { componentTemplates: ComponentTemplateListItem[]; onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; + onEditClick: (componentTemplateName: string) => void; history: ScopedHistory; } @@ -32,6 +33,7 @@ export const ComponentTable: FunctionComponent = ({ componentTemplates, onReloadClick, onDeleteClick, + onEditClick, history, }) => { const { trackMetric } = useComponentTemplatesContext(); @@ -214,6 +216,21 @@ export const ComponentTable: FunctionComponent = ({ /> ), actions: [ + { + name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionEditText', { + defaultMessage: 'Edit', + }), + isPrimary: true, + description: i18n.translate( + 'xpack.idxMgmt.componentTemplatesList.table.actionEditDecription', + { + defaultMessage: 'Edit this component template', + } + ), + icon: 'pencil', + type: 'icon', + onClick: ({ name }: ComponentTemplateListItem) => onEditClick(name), + }, { 'data-test-subj': 'deleteComponentTemplateButton', isPrimary: true, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index 80570f4a443cde..9663df7e46762b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -11,3 +11,5 @@ export { ComponentTemplateList } from './component_template_list'; export { ComponentTemplateDetailsFlyout } from './component_template_details'; export { ComponentTemplateCreate } from './component_template_create'; + +export { ComponentTemplateEdit } from './component_template_edit'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index e6701c83a64e06..37e2924dcad038 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -64,10 +64,24 @@ export const getApi = ( return result; } + async function updateComponentTemplate(componentTemplate: ComponentTemplateDeserialized) { + const { name } = componentTemplate; + const result = await sendRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(componentTemplate), + }); + + // trackMetric('count', UIM_TEMPLATE_UPDATE); + + return result; + } + return { useLoadComponentTemplates, deleteComponentTemplates, useLoadComponentTemplate, createComponentTemplate, + updateComponentTemplate, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx index f75d687fbcbe1a..a55df6dbadbd72 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx @@ -67,6 +67,17 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + const subscription = form.subscribe(({ data, validate, isValid }) => { + onChange({ + isValid, + validate, + getData: data.format, + }); + }); + return subscription.unsubscribe; + }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + return (
diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index 80e44ae2cd0aee..56ee9640d3d077 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import { serializeComponentTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -const bodySchema = schema.object({ - name: schema.string(), - ...componentTemplateSchema, -}); - export const registerCreateRoute = ({ router, license, @@ -25,7 +19,7 @@ export const registerCreateRoute = ({ { path: addBasePath('/component_templates'), validate: { - body: bodySchema, + body: componentTemplateSchema, }, }, license.guardApiRoute(async (ctx, req, res) => { diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts index 0bdd102dba42fc..a1fc2581272294 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -5,7 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -export const componentTemplateSchema = { +export const componentTemplateSchema = schema.object({ + name: schema.string(), template: schema.object({ settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), @@ -16,4 +17,4 @@ export const componentTemplateSchema = { _kbnMeta: schema.object({ usedBy: schema.arrayOf(schema.string()), }), -}; +}); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index 7e447bb110c67b..47834a2cf499d3 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -9,8 +9,6 @@ import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -const bodySchema = schema.object(componentTemplateSchema); - const paramsSchema = schema.object({ name: schema.string(), }); @@ -24,7 +22,7 @@ export const registerUpdateRoute = ({ { path: addBasePath('/component_templates/{name}'), validate: { - body: bodySchema, + body: componentTemplateSchema, params: paramsSchema, }, }, From 45e910bcd39b3ae2400355f63df96639bf1d9b5b Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 25 Jun 2020 13:59:17 -0400 Subject: [PATCH 05/20] add clone flow --- .../public/application/app.tsx | 13 +++- .../component_template_clone.tsx | 61 +++++++++++++++++++ .../component_template_clone/index.ts | 7 +++ .../component_template_create.tsx | 11 +++- .../component_template_list.tsx | 18 +++++- .../component_template_list/table.tsx | 30 +++++++-- .../components/component_templates/index.ts | 2 + .../steps/step_logistics.tsx | 2 +- .../steps/step_logistics_schema.tsx | 4 +- 9 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/index.ts diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index d4a830c7c632b4..8d78995a94e2f5 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -16,7 +16,11 @@ import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { useServices } from './app_context'; -import { ComponentTemplateCreate, ComponentTemplateEdit } from './components'; +import { + ComponentTemplateCreate, + ComponentTemplateEdit, + ComponentTemplateClone, +} from './components'; export const App = ({ history }: { history: ScopedHistory }) => { const { uiMetricService } = useServices(); @@ -33,9 +37,14 @@ export const App = ({ history }: { history: ScopedHistory }) => { export const AppWithoutRouter = () => ( - + + diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx new file mode 100644 index 00000000000000..13bc7f05f4a7b5 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx @@ -0,0 +1,61 @@ +/* + * 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, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading } from '../shared_imports'; +import { useComponentTemplatesContext } from '../component_templates_context'; +import { ComponentTemplateCreate } from '../component_template_create'; + +export interface Params { + sourceComponentTemplateName: string; +} + +export const ComponentTemplateClone: FunctionComponent> = (props) => { + const { sourceComponentTemplateName } = props.match.params; + const { toasts, api } = useComponentTemplatesContext(); + + const { + error, + data: componentTemplateToClone, + isLoading, + isInitialRequest, + } = api.useLoadComponentTemplate(sourceComponentTemplateName); + + useEffect(() => { + if (error && !isLoading) { + toasts.addError(error, { + title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', { + defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`, + values: { sourceComponentTemplateName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading && isInitialRequest) { + return ( + + + + ); + } else { + // We still show the create form (unpopulated) even if we were not able to load the + // selected component template data. + const sourceComponentTemplate = componentTemplateToClone + ? { ...componentTemplateToClone, name: `${componentTemplateToClone.name}-copy` } + : undefined; + + return ; + } +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/index.ts new file mode 100644 index 00000000000000..b7165919644f44 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/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 { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx index 698db969967122..17b86faa15a61e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx @@ -11,8 +11,16 @@ import { ComponentTemplateDeserialized } from '../shared_imports'; import { ComponentTemplateForm } from '../shared'; import { useComponentTemplatesContext } from '../component_templates_context'; -export const ComponentTemplateCreate: React.FunctionComponent = ({ +interface Props { + /** + * This value may be passed in to prepopulate the creation form (e.g., to clone a template) + */ + sourceComponentTemplate?: any; +} + +export const ComponentTemplateCreate: React.FunctionComponent = ({ history, + sourceComponentTemplate, }) => { const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); @@ -61,6 +69,7 @@ export const ComponentTemplateCreate: React.FunctionComponent = ({ const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); - const goToList = () => { + const goToComponentTemplateList = () => { return history.push('component_templates'); }; @@ -42,6 +42,10 @@ export const ComponentTemplateList: React.FunctionComponent = ({ return history.push(`edit_component_template/${encodeURIComponent(name)}`); }; + const goToCloneComponentTemplate = (name: string) => { + return history.push(`create_component_template/${encodeURIComponent(name)}`); + }; + // Track component loaded useEffect(() => { trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD); @@ -69,6 +73,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ onReloadClick={sendRequest} onDeleteClick={setComponentTemplatesToDelete} onEditClick={goToEditComponentTemplate} + onCloneClick={goToCloneComponentTemplate} history={history as ScopedHistory} /> ); @@ -88,7 +93,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ // refetch the component templates sendRequest(); // go back to list view (if deleted from details flyout) - goToList(); + goToComponentTemplateList(); } setComponentTemplatesToDelete([]); }} @@ -99,7 +104,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ {/* details flyout */} {componentTemplateName && ( = ({ icon: 'pencil', handleActionClick: () => goToEditComponentTemplate(componentTemplateName), }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: 'copy', + handleActionClick: () => goToCloneComponentTemplate(componentTemplateName), + }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { defaultMessage: 'Delete', diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index 724d1bafdeaefb..9984025da1a22a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -26,6 +26,7 @@ export interface Props { onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; onEditClick: (componentTemplateName: string) => void; + onCloneClick: (componentTemplateName: string) => void; history: ScopedHistory; } @@ -34,6 +35,7 @@ export const ComponentTable: FunctionComponent = ({ onReloadClick, onDeleteClick, onEditClick, + onCloneClick, history, }) => { const { trackMetric } = useComponentTemplatesContext(); @@ -220,20 +222,34 @@ export const ComponentTable: FunctionComponent = ({ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionEditText', { defaultMessage: 'Edit', }), - isPrimary: true, description: i18n.translate( 'xpack.idxMgmt.componentTemplatesList.table.actionEditDecription', { defaultMessage: 'Edit this component template', } ), + onClick: ({ name }: ComponentTemplateListItem) => onEditClick(name), + isPrimary: true, icon: 'pencil', type: 'icon', - onClick: ({ name }: ComponentTemplateListItem) => onEditClick(name), + 'data-test-subj': 'editComponentTemplateButton', + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionCloneText', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplatesList.table.actionCloneDecription', + { + defaultMessage: 'Clone this component template', + } + ), + onClick: ({ name }: ComponentTemplateListItem) => onCloneClick(name), + icon: 'copy', + type: 'icon', + 'data-test-subj': 'cloneComponentTemplateButton', }, { - 'data-test-subj': 'deleteComponentTemplateButton', - isPrimary: true, name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.deleteActionLabel', { defaultMessage: 'Delete', }), @@ -241,11 +257,13 @@ export const ComponentTable: FunctionComponent = ({ 'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription', { defaultMessage: 'Delete this component template' } ), + onClick: ({ name }) => onDeleteClick([name]), + enabled: ({ usedBy }) => usedBy.length === 0, + isPrimary: true, type: 'icon', icon: 'trash', color: 'danger', - onClick: ({ name }) => onDeleteClick([name]), - enabled: ({ usedBy }) => usedBy.length === 0, + 'data-test-subj': 'deleteComponentTemplateButton', }, ], }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index 9663df7e46762b..fd2741cb51248a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -13,3 +13,5 @@ export { ComponentTemplateDetailsFlyout } from './component_template_details'; export { ComponentTemplateCreate } from './component_template_create'; export { ComponentTemplateEdit } from './component_template_edit'; + +export { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx index a55df6dbadbd72..9db02b5f7a5619 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx @@ -210,7 +210,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( componentProps={{ euiCodeEditorProps: { ['data-test-subj']: 'metaEditor', - height: '300px', + height: '200px', 'aria-label': i18n.translate( 'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaAriaLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx index a3ec69a2357e31..04ef8884c9b3f5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics_schema.tsx @@ -18,8 +18,8 @@ import { const { emptyField, containsCharsField, isJsonField } = fieldValidators; const { toInt } = fieldFormatters; -const stringifyJson = (json: any): string => - Array.isArray(json) ? JSON.stringify(json, null, 2) : '{\n\n}'; +const stringifyJson = (json: { [key: string]: any }): string => + Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; const parseJson = (jsonString: string): object => { let parsedJSON: any; From dc0ff0b8fb2aee7f31b657cac4f502529a81816b Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 25 Jun 2020 21:17:32 -0400 Subject: [PATCH 06/20] add support for breadcrumbs --- .../public/application/app_context.tsx | 2 + .../helpers/setup_environment.tsx | 1 + .../component_template_create.tsx | 11 ++-- .../component_template_edit.tsx | 11 ++-- .../component_templates_context.tsx | 10 ++- .../component_templates/lib/breadcrumbs.ts | 61 +++++++++++++++++++ .../component_templates/lib/index.ts | 2 + .../public/application/index.tsx | 3 +- .../application/mount_management_section.ts | 1 + 9 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 84938de4169417..37de2bb78f3481 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -6,6 +6,7 @@ import React, { createContext, useContext } from 'react'; import { ScopedHistory } from 'kibana/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; import { CoreStart } from '../../../../../src/core/public'; import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; @@ -30,6 +31,7 @@ export interface AppDependencies { notificationService: NotificationService; }; history: ScopedHistory; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index a2194bbfa0186b..70634a226c67b6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -27,6 +27,7 @@ const appDependencies = { trackMetric: () => {}, docLinks: docLinksServiceMock.createStartContract(), toasts: notificationServiceMock.createSetupContract().toasts, + setBreadcrumbs: () => {}, }; export const setupEnvironment = () => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx index 17b86faa15a61e..b96af32ddeaea4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx @@ -3,7 +3,7 @@ * 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, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; @@ -25,7 +25,7 @@ export const ComponentTemplateCreate: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); - const { api } = useComponentTemplatesContext(); + const { api, breadcrumbs } = useComponentTemplatesContext(); const onSave = async (componentTemplate: ComponentTemplateDeserialized) => { const { name } = componentTemplate; @@ -49,10 +49,9 @@ export const ComponentTemplateCreate: React.FunctionComponent { - // breadcrumbService.setBreadcrumbs('templateCreate'); - // }, []); + useEffect(() => { + breadcrumbs.setCreateBreadcrumbs(); + }, [breadcrumbs]); return ( diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx index 69d20dabcd2610..ed4e98e17b5678 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx @@ -3,7 +3,7 @@ * 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, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; @@ -21,17 +21,16 @@ export const ComponentTemplateEdit: React.FunctionComponent { - const { api } = useComponentTemplatesContext(); + const { api, breadcrumbs } = useComponentTemplatesContext(); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(name); - // TODO breadcrumbs - // useEffect(() => { - // breadcrumbService.setBreadcrumbs('templateEdit'); - // }, []); + useEffect(() => { + breadcrumbs.setEditBreadcrumbs(); + }, [breadcrumbs]); const onSave = async (updatedComponentTemplate: ComponentTemplateDeserialized) => { setIsSaving(true); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index c78d24f126e297..6fa8bd62b43dc6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -7,7 +7,8 @@ import React, { createContext, useContext } from 'react'; import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public'; -import { getApi, getUseRequest, getSendRequest, getDocumentation } from './lib'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; +import { getApi, getUseRequest, getSendRequest, getDocumentation, getBreadcrumbs } from './lib'; const ComponentTemplatesContext = createContext(undefined); @@ -17,6 +18,7 @@ interface Props { trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; docLinks: DocLinksStart; toasts: NotificationsSetup['toasts']; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } interface Context { @@ -24,6 +26,7 @@ interface Context { apiBasePath: string; api: ReturnType; documentation: ReturnType; + breadcrumbs: ReturnType; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; toasts: NotificationsSetup['toasts']; } @@ -35,17 +38,18 @@ export const ComponentTemplatesProvider = ({ value: Props; children: React.ReactNode; }) => { - const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value; + const { httpClient, apiBasePath, trackMetric, docLinks, toasts, setBreadcrumbs } = value; const useRequest = getUseRequest(httpClient); const sendRequest = getSendRequest(httpClient); const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric); const documentation = getDocumentation(docLinks); + const breadcrumbs = getBreadcrumbs(setBreadcrumbs); return ( {children} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts new file mode 100644 index 00000000000000..033df5a9562ed7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts @@ -0,0 +1,61 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; + +export const getBreadcrumbs = (setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']) => { + const baseBreadcrumbs = [ + { + text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.homeLabel', { + defaultMessage: 'Index Management', + }), + href: '/', + }, + { + text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.componentTemplatesLabel', { + defaultMessage: 'Component templates', + }), + href: '/component_templates', + }, + ]; + + const setCreateBreadcrumbs = () => { + const createBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate( + 'xpack.idxMgmt.componentTemplate.breadcrumb.createComponentTemplateLabel', + { + defaultMessage: 'Create component template', + } + ), + }, + ]; + + return setBreadcrumbs(createBreadcrumbs); + }; + + const setEditBreadcrumbs = () => { + const editBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate( + 'xpack.idxMgmt.componentTemplate.breadcrumb.editComponentTemplateLabel', + { + defaultMessage: 'Edit component template', + } + ), + }, + ]; + + return setBreadcrumbs(editBreadcrumbs); + }; + + return { + setCreateBreadcrumbs, + setEditBreadcrumbs, + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts index 9a91312f832948..bd7af97e71a4d2 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts @@ -9,3 +9,5 @@ export * from './api'; export * from './request'; export * from './documentation'; + +export * from './breadcrumbs'; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index ff54b4b1bfe350..7b053a15b26d02 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -27,7 +27,7 @@ export const renderApp = ( const { i18n, docLinks, notifications } = core; const { Context: I18nContext } = i18n; - const { services, history } = dependencies; + const { services, history, setBreadcrumbs } = dependencies; const componentTemplateProviderValues = { httpClient: services.httpService.httpClient, @@ -35,6 +35,7 @@ export const renderApp = ( trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService), docLinks, toasts: notifications.toasts, + setBreadcrumbs, }; render( diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index e8b6f200fb349f..16e21cb0852684 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -47,6 +47,7 @@ export async function mountManagementSection( }, services, history, + setBreadcrumbs, }; return renderApp(element, { core, dependencies: appDependencies }); From a6ccd7aaf555857e7972d7a822b1bf5e6e62722b Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 25 Jun 2020 22:12:21 -0400 Subject: [PATCH 07/20] fix api integration tests --- .../index_management/component_templates.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts index 56b4ec45b42b71..1a00eaba35aa15 100644 --- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -146,6 +146,9 @@ export default function ({ getService }: FtrProviderContext) { id: 10, }, }, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -162,6 +165,9 @@ export default function ({ getService }: FtrProviderContext) { .send({ name: REQUIRED_FIELDS_COMPONENT_NAME, template: {}, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -177,6 +183,9 @@ export default function ({ getService }: FtrProviderContext) { .send({ name: COMPONENT_NAME, template: {}, + _kbnMeta: { + usedBy: [], + }, }) .expect(409); @@ -233,7 +242,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ ...COMPONENT, + name: COMPONENT_NAME, version: 1, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -250,7 +263,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ ...COMPONENT, + name: 'component_does_not_exist', version: 1, + _kbnMeta: { + usedBy: [], + }, }) .expect(404); From ea27f11807ff910a861a04538e4df6e47ab4b6fe Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 26 Jun 2020 08:37:47 -0400 Subject: [PATCH 08/20] i18n fix --- .../component_template_form/steps/step_logistics.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx index 9db02b5f7a5619..16f82e4efd8291 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx @@ -179,9 +179,12 @@ export const StepLogistics: React.FunctionComponent = React.memo( target="_blank" external > - {i18n.translate('xpack.ingestPipelines.form.metaDocumentionLink', { - defaultMessage: 'Learn more', - })} + {i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaDocumentionLink', + { + defaultMessage: 'Learn more', + } + )} ), }} From 18655a10d61774e97cd469cc97b9d7ec3ed95416 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 26 Jun 2020 11:20:12 -0400 Subject: [PATCH 09/20] properly encode/decode component template name --- .../public/application/app_context.tsx | 1 - .../component_template_clone.tsx | 5 ++++- .../component_template_details.tsx | 7 +++++-- .../component_template_edit.tsx | 7 +++++-- .../component_template_list/table.tsx | 2 +- .../components/component_templates/lib/index.ts | 2 ++ .../components/component_templates/lib/utils.ts | 17 +++++++++++++++++ 7 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 332a8d488ca9e1..6fbe177d24e066 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -10,7 +10,6 @@ import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { CoreStart } from '../../../../../src/core/public'; -import { CoreStart } from '../../../../../src/core/public'; import { IngestManagerSetup } from '../../../ingest_manager/public'; import { IndexMgmtMetricsType } from '../types'; import { UiMetricService, NotificationService, HttpService } from './services'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx index 13bc7f05f4a7b5..3b026324c0be16 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx @@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { SectionLoading } from '../shared_imports'; import { useComponentTemplatesContext } from '../component_templates_context'; import { ComponentTemplateCreate } from '../component_template_create'; +import { attemptToDecodeURI } from '../lib'; export interface Params { sourceComponentTemplateName: string; @@ -19,6 +20,8 @@ export interface Params { export const ComponentTemplateClone: FunctionComponent> = (props) => { const { sourceComponentTemplateName } = props.match.params; + const decodedSourceComponentTemplateName = attemptToDecodeURI(sourceComponentTemplateName); + const { toasts, api } = useComponentTemplatesContext(); const { @@ -26,7 +29,7 @@ export const ComponentTemplateClone: FunctionComponent { if (error && !isLoading) { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx index a8007c6363584a..f94c5c38f23ddf 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -24,6 +24,7 @@ import { useComponentTemplatesContext } from '../component_templates_context'; import { TabSummary } from './tab_summary'; import { ComponentTemplateTabs, TabType } from './tabs'; import { ManageButton, ManageAction } from './manage_button'; +import { attemptToDecodeURI } from '../lib'; interface Props { componentTemplateName: string; @@ -39,8 +40,10 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ }) => { const { api } = useComponentTemplatesContext(); + const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName); + const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate( - componentTemplateName + decodedComponentTemplateName ); const [activeTab, setActiveTab] = useState('summary'); @@ -108,7 +111,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({

- {componentTemplateName} + {decodedComponentTemplateName}

diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx index ed4e98e17b5678..d8acce639556c5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx @@ -10,6 +10,7 @@ import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@e import { useComponentTemplatesContext } from '../component_templates_context'; import { ComponentTemplateDeserialized, SectionLoading } from '../shared_imports'; import { ComponentTemplateForm } from '../shared'; +import { attemptToDecodeURI } from '../lib'; interface MatchParams { name: string; @@ -26,7 +27,9 @@ export const ComponentTemplateEdit: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); - const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(name); + const decodedName = attemptToDecodeURI(name); + + const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(decodedName); useEffect(() => { breadcrumbs.setEditBreadcrumbs(); @@ -103,7 +106,7 @@ export const ComponentTemplateEdit: React.FunctionComponent diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index 9984025da1a22a..f922bff08205de 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -150,7 +150,7 @@ export const ComponentTable: FunctionComponent = ({ {...reactRouterNavigate( history, { - pathname: `/component_templates/${name}`, + pathname: `/component_templates/${encodeURIComponent(name)}`, }, () => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS) )} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts index bd7af97e71a4d2..29273bd946e10e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts @@ -11,3 +11,5 @@ export * from './request'; export * from './documentation'; export * from './breadcrumbs'; + +export { attemptToDecodeURI } from './utils'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts new file mode 100644 index 00000000000000..88592f3e4f5174 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const attemptToDecodeURI = (value: string) => { + let result: string; + + try { + result = decodeURIComponent(value); + } catch (e) { + result = value; + } + + return result; +}; From 87b0155d042e1353f67a223a233b52fee6bbccb6 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Sat, 27 Jun 2020 12:08:06 -0400 Subject: [PATCH 10/20] cleanup --- .../component_template_clone.tsx | 6 +- .../component_template_clone/index.ts | 0 .../component_template_create.tsx | 7 +- .../component_template_create/index.ts | 0 .../component_template_edit.tsx | 9 ++- .../component_template_edit/index.ts | 0 .../component_template_form.tsx | 12 +-- .../component_template_form/index.ts | 0 .../component_template_form/steps/index.ts | 0 .../steps/step_logistics.tsx | 8 +- .../steps/step_logistics_container.tsx | 2 +- .../steps/step_logistics_schema.tsx | 7 +- .../steps/step_review.tsx | 76 +++++++++---------- .../steps/step_review_container.tsx | 2 +- .../index.ts | 6 +- .../components/component_templates/index.ts | 10 +-- .../component_templates/shared/index.ts | 7 -- .../component_templates/shared_imports.ts | 10 ++- 18 files changed, 82 insertions(+), 80 deletions(-) rename x-pack/plugins/index_management/public/application/components/component_templates/{ => component_template_wizard}/component_template_clone/component_template_clone.tsx (92%) rename x-pack/plugins/index_management/public/application/components/component_templates/{ => component_template_wizard}/component_template_clone/index.ts (100%) rename x-pack/plugins/index_management/public/application/components/component_templates/{ => component_template_wizard}/component_template_create/component_template_create.tsx (90%) rename x-pack/plugins/index_management/public/application/components/component_templates/{ => component_template_wizard}/component_template_create/index.ts (100%) rename x-pack/plugins/index_management/public/application/components/component_templates/{ => component_template_wizard}/component_template_edit/component_template_edit.tsx (93%) rename x-pack/plugins/index_management/public/application/components/component_templates/{ => component_template_wizard}/component_template_edit/index.ts (100%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/component_template_form.tsx (95%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/index.ts (100%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/steps/index.ts (100%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/steps/step_logistics.tsx (96%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/steps/step_logistics_container.tsx (93%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/steps/step_logistics_schema.tsx (96%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/steps/step_review.tsx (82%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/component_template_form/steps/step_review_container.tsx (91%) rename x-pack/plugins/index_management/public/application/components/component_templates/{shared/components => component_template_wizard}/index.ts (54%) delete mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx similarity index 92% rename from x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx index 3b026324c0be16..1761b1bb72c300 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -9,10 +9,10 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SectionLoading } from '../shared_imports'; -import { useComponentTemplatesContext } from '../component_templates_context'; +import { SectionLoading } from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { attemptToDecodeURI } from '../../lib'; import { ComponentTemplateCreate } from '../component_template_create'; -import { attemptToDecodeURI } from '../lib'; export interface Params { sourceComponentTemplateName: string; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/components/component_templates/component_template_clone/index.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx similarity index 90% rename from x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx index b96af32ddeaea4..747da5e1ffcbd9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -7,9 +7,10 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { ComponentTemplateDeserialized } from '../shared_imports'; -import { ComponentTemplateForm } from '../shared'; -import { useComponentTemplatesContext } from '../component_templates_context'; + +import { ComponentTemplateDeserialized } from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { ComponentTemplateForm } from '../component_template_form'; interface Props { /** diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/components/component_templates/component_template_create/index.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx similarity index 93% rename from x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index d8acce639556c5..96bc45de80803e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -7,10 +7,11 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { useComponentTemplatesContext } from '../component_templates_context'; -import { ComponentTemplateDeserialized, SectionLoading } from '../shared_imports'; -import { ComponentTemplateForm } from '../shared'; -import { attemptToDecodeURI } from '../lib'; + +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { ComponentTemplateDeserialized, SectionLoading } from '../../shared_imports'; +import { attemptToDecodeURI } from '../../lib'; +import { ComponentTemplateForm } from '../component_template_form'; interface MatchParams { name: string; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/components/component_templates/component_template_edit/index.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx similarity index 95% rename from x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx index 0d615d3ede2e43..121125e7d63d0d 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/component_template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx @@ -8,15 +8,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { serializers, Forms, ComponentTemplateDeserialized } from '../../../shared_imports'; -import { StepLogisticsContainer, StepReviewContainer } from './steps'; import { + serializers, + Forms, + ComponentTemplateDeserialized, CommonWizardSteps, StepSettingsContainer, StepMappingsContainer, StepAliasesContainer, -} from '../../../../shared'; -import { useComponentTemplatesContext } from '../../../component_templates_context'; +} from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; const { stripEmptyFields } = serializers; const { FormWizard, FormWizardStep } = Forms; @@ -64,7 +66,7 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { review: { id: 'review', label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.summaryStepName', { - defaultMessage: 'Review component template', + defaultMessage: 'Review', }), }, }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/index.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/index.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx similarity index 96% rename from x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx index 16f82e4efd8291..56f95e17acf908 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx @@ -23,10 +23,9 @@ import { Field, Forms, JsonEditorField, -} from '../../../../shared_imports'; - +} from '../../../shared_imports'; +import { useComponentTemplatesContext } from '../../../component_templates_context'; import { logisticsFormSchema } from './step_logistics_schema'; -import { useComponentTemplatesContext } from '../../../../component_templates_context'; const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); @@ -99,6 +98,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( href={documentation.componentTemplates} target="_blank" iconType="help" + data-test-subj="documentationLink" > = React.memo( <> ( - -); - const getDescriptionText = (data: any) => { const hasEntries = data && Object.entries(data).length > 0; @@ -80,37 +73,19 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen {/* Version */} - - - - - {version ? version : } - - - {/* Metadata */} - - - - - {serializedMeta ? ( - - {JSON.stringify(serializedMeta, null, 2)} - - ) : ( - - )} - - - - - - + {version && ( + <> + + + + {version} + + )} + + {/* Index settings */} = React.memo(({ componen {getDescriptionText(serializedSettings)} + + {/* Mappings */} = React.memo(({ componen {getDescriptionText(serializedMappings)} + + {/* Aliases */} = React.memo(({ componen + + + {/* Metadata */} + {serializedMeta && ( + + + + + + + {JSON.stringify(serializedMeta, null, 2)} + + + + )} +
); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx similarity index 91% rename from x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx index b532da4c8a96bc..10698afc5bc238 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/component_template_form/steps/step_review_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Forms, ComponentTemplateDeserialized } from '../../../../shared_imports'; +import { Forms, ComponentTemplateDeserialized } from '../../../shared_imports'; import { WizardContent } from '../component_template_form'; import { StepReview } from './step_review'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts similarity index 54% rename from x-pack/plugins/index_management/public/application/components/component_templates/shared/components/index.ts rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts index 84d9a2795ee2c0..59168785b77b2e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ComponentTemplateForm } from './component_template_form'; +export { ComponentTemplateCreate } from './component_template_create'; + +export { ComponentTemplateEdit } from './component_template_edit'; + +export { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index fd2741cb51248a..51fa63ba1b66c8 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -10,8 +10,8 @@ export { ComponentTemplateList } from './component_template_list'; export { ComponentTemplateDetailsFlyout } from './component_template_details'; -export { ComponentTemplateCreate } from './component_template_create'; - -export { ComponentTemplateEdit } from './component_template_edit'; - -export { ComponentTemplateClone } from './component_template_clone'; +export { + ComponentTemplateCreate, + ComponentTemplateEdit, + ComponentTemplateClone, +} from './component_template_wizard'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts deleted file mode 100644 index a81d0dcd900a0b..00000000000000 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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 { ComponentTemplateForm } from './components/component_template_form'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index bfe0c76497da6e..80e222f4f77064 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -45,7 +45,15 @@ export { export { isJSON } from '../../../../../../../src/plugins/es_ui_shared/static/validators/string'; -export { TabMappings, TabSettings, TabAliases } from '../shared'; +export { + TabMappings, + TabSettings, + TabAliases, + CommonWizardSteps, + StepSettingsContainer, + StepMappingsContainer, + StepAliasesContainer, +} from '../shared'; export { ComponentTemplateSerialized, From 332a0096a3bfe0776fd01ef744ca3b0dda8b3be5 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Sat, 27 Jun 2020 20:37:28 -0400 Subject: [PATCH 11/20] uncomment code --- .../application/components/component_templates/constants.ts | 1 + .../application/components/component_templates/lib/api.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index 514d24026456bf..897440feedf705 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -10,6 +10,7 @@ export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details'; export const UIM_COMPONENT_TEMPLATE_CREATE = 'component_template_create'; +export const UIM_COMPONENT_TEMPLATE_UPDATE = 'component_template_update'; // privileges export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 37e2924dcad038..ca15b82f83acf8 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -13,6 +13,7 @@ import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE, UIM_COMPONENT_TEMPLATE_CREATE, + UIM_COMPONENT_TEMPLATE_UPDATE, } from '../constants'; import { UseRequestHook, SendRequestHook } from './request'; @@ -72,7 +73,7 @@ export const getApi = ( body: JSON.stringify(componentTemplate), }); - // trackMetric('count', UIM_TEMPLATE_UPDATE); + trackMetric('count', UIM_COMPONENT_TEMPLATE_UPDATE); return result; } From 5fb083b381dfd81dafcf7276b8a0b13f0e8da41f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 30 Jun 2020 13:29:20 -0400 Subject: [PATCH 12/20] fix bug when editing component template --- .../component_template_form.tsx | 33 +++++++++++-------- .../steps/step_logistics.tsx | 10 +++--- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx index 121125e7d63d0d..c937733ecd4a56 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx @@ -137,25 +137,30 @@ export const ComponentTemplateForm = ({ ) : null; - const buildTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => ( + const buildComponentTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => ( wizardData: WizardContent - ): ComponentTemplateDeserialized => ({ - ...initialTemplate, - ...wizardData.logistics, - template: { - settings: wizardData.settings, - mappings: wizardData.mappings, - aliases: wizardData.aliases, - }, - }); + ): ComponentTemplateDeserialized => { + const componentTemplate = { + ...initialTemplate, + name: wizardData.logistics.name, + version: wizardData.logistics.version, + _meta: wizardData.logistics._meta, + template: { + settings: wizardData.settings, + mappings: wizardData.mappings, + aliases: wizardData.aliases, + }, + }; + return componentTemplate; + }; const onSaveTemplate = useCallback( async (wizardData: WizardContent) => { - const template = buildTemplateObject(defaultValue)(wizardData); + const componentTemplate = buildComponentTemplateObject(defaultValue)(wizardData); // This will strip an empty string if "version" is not set, as well as an empty "_meta" object onSave( - stripEmptyFields(template, { + stripEmptyFields(componentTemplate, { types: ['string', 'object'], }) as ComponentTemplateDeserialized ); @@ -195,7 +200,9 @@ export const ComponentTemplateForm = ({ - + ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx index 56f95e17acf908..99bb862a7bc9c6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx @@ -54,11 +54,11 @@ export const StepLogistics: React.FunctionComponent = React.memo( Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length) ); - useEffect(() => { - const validate = async () => { - return (await form.submit()).isValid; - }; + const validate = async () => { + return (await form.submit()).isValid; + }; + useEffect(() => { onChange({ isValid: form.isValid, validate, @@ -67,7 +67,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { - const subscription = form.subscribe(({ data, validate, isValid }) => { + const subscription = form.subscribe(({ data, isValid }) => { onChange({ isValid, validate, From be67388262faf76712eed21770d78c505f28905b Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 30 Jun 2020 22:30:17 -0400 Subject: [PATCH 13/20] component integration tests --- .../forms/multi_content/use_multi_content.ts | 4 + .../component_template_create.test.tsx | 233 ++++++++++++++++++ .../component_template_edit.test.tsx | 134 ++++++++++ .../component_template_create.helpers.ts | 38 +++ .../component_template_edit.helpers.ts | 38 +++ .../component_template_form.helpers.ts | 168 +++++++++++++ .../helpers/http_requests.ts | 21 +- .../component_template_form.tsx | 4 +- .../steps/step_review.tsx | 6 +- 9 files changed, 640 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts index 0a2c7bb6519593..adc68a39a4a5b6 100644 --- a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts +++ b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts @@ -150,6 +150,10 @@ export function useMultiContent({ * Validate the multi-content active content(s) in the DOM */ const validate = useCallback(async () => { + if (Object.keys(contents.current).length === 0) { + return Boolean(validation.isValid); + } + const updatedValidation = {} as { [key in keyof T]?: boolean | undefined }; for (const [id, _content] of Object.entries(contents.current)) { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx new file mode 100644 index 00000000000000..0c88cc1512804f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx @@ -0,0 +1,233 @@ +/* + * 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 from 'react'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from './helpers'; +import { setup, ComponentTemplateCreateTestBed } from './helpers/component_template_create.helpers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +describe('', () => { + let testBed: ComponentTemplateCreateTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('On component mount', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('should set the correct page header', async () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create component template'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Component Templates docs'); + }); + + describe('Step: Logistics', () => { + test('should toggle the version field', async () => { + const { exists, component, actions } = testBed; + + // Version field should be hidden by default + expect(exists('versionField')).toBe(false); + + await act(async () => { + actions.toggleVersionSwitch(); + }); + + component.update(); + + expect(exists('versionField')).toBe(true); + }); + + test('should toggle the metadata field', async () => { + const { exists, component, actions } = testBed; + + // Meta editor should be hidden by default + // Since the editor itself is mocked, we checked for the mocked element + expect(exists('mockCodeEditor')).toBe(false); + + await act(async () => { + actions.toggleMetaSwitch(); + }); + + component.update(); + + expect(exists('mockCodeEditor')).toBe(true); + }); + + describe('Validation', () => { + test('should require a name', async () => { + const { form, actions, component, find } = testBed; + + await act(async () => { + // Submit logistics step without any values + actions.clickNextButton(); + }); + + component.update(); + + // Verify name is required + expect(form.getErrorsMessages()).toEqual(['A component template name is required.']); + expect(find('nextButton').props().disabled).toEqual(true); + }); + }); + }); + + describe('Step: Review and submit', () => { + const COMPONENT_TEMPLATE_NAME = 'comp-1'; + const SETTINGS = { number_of_shards: 1 }; + const ALIASES = { my_alias: {} }; + + const BOOLEAN_MAPPING_FIELD = { + name: 'boolean_datatype', + type: 'boolean', + }; + + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + const { actions, component } = testBed; + + component.update(); + + // Complete step 1 (logistics) + await actions.completeStepLogistics({ name: COMPONENT_TEMPLATE_NAME }); + + // Complete step 2 (index settings) + await actions.completeStepSettings(SETTINGS); + + // Complete step 3 (mappings) + await actions.completeStepMappings([BOOLEAN_MAPPING_FIELD]); + + // Complete step 4 (aliases) + await actions.completeStepAliases(ALIASES); + }); + + test('should render the review content', () => { + const { find, exists, actions } = testBed; + // Verify page header + expect(exists('stepReview')).toBe(true); + expect(find('stepReview.title').text()).toEqual( + `Review details for '${COMPONENT_TEMPLATE_NAME}'` + ); + + // Verify 2 tabs exist + expect(find('stepReview.content').find('.euiTab').length).toBe(2); + expect( + find('stepReview.content') + .find('.euiTab') + .map((t) => t.text()) + ).toEqual(['Summary', 'Request']); + + // Summary tab should render by default + expect(exists('stepReview.summaryTab')).toBe(true); + expect(exists('stepReview.requestTab')).toBe(false); + + // Navigate to request tab and verify content + actions.selectReviewTab('request'); + + expect(exists('stepReview.summaryTab')).toBe(false); + expect(exists('stepReview.requestTab')).toBe(true); + }); + + test('should send the correct payload when submitting the form', async () => { + const { actions, component } = testBed; + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: COMPONENT_TEMPLATE_NAME, + template: { + settings: SETTINGS, + mappings: { + _source: {}, + _meta: {}, + properties: { + [BOOLEAN_MAPPING_FIELD.name]: { + type: BOOLEAN_MAPPING_FIELD.type, + }, + }, + }, + aliases: ALIASES, + }, + _kbnMeta: { usedBy: [] }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + + test('should surface API errors if the request is unsuccessful', async () => { + const { component, actions, find, exists } = testBed; + + const error = { + status: 409, + error: 'Conflict', + message: `There is already a template with name '${COMPONENT_TEMPLATE_NAME}'`, + }; + + httpRequestsMockHelpers.setCreateComponentTemplateResponse(undefined, { body: error }); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + expect(exists('saveComponentTemplateError')).toBe(true); + expect(find('saveComponentTemplateError').text()).toContain(error.message); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx new file mode 100644 index 00000000000000..cabc2a5c53bb1c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 from 'react'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from './helpers'; +import { setup, ComponentTemplateEditTestBed } from './helpers/component_template_edit.helpers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +describe('', () => { + let testBed: ComponentTemplateEditTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + const COMPONENT_TEMPLATE_NAME = 'comp-1'; + const COMPONENT_TEMPLATE_TO_EDIT = { + name: COMPONENT_TEMPLATE_NAME, + template: { + settings: { number_of_shards: 1 }, + }, + _kbnMeta: { usedBy: [] }, + }; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE_TO_EDIT); + + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('should set the correct page title', () => { + const { exists, find } = testBed; + + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual( + `Edit component template '${COMPONENT_TEMPLATE_NAME}'` + ); + }); + + it('should set the name field to read only', () => { + const { find } = testBed; + + const nameInput = find('nameField.input'); + expect(nameInput.props().disabled).toEqual(true); + }); + + describe('form payload', () => { + it('should send the correct payload with changed values', async () => { + const { actions, component, form } = testBed; + + await act(async () => { + actions.toggleVersionSwitch(); + }); + + component.update(); + + await act(async () => { + form.setInputValue('versionField.input', '1'); + }); + + component.update(); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + await actions.completeStepSettings(); + await actions.completeStepMappings(); + await actions.completeStepAliases(); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + version: 1, + ...COMPONENT_TEMPLATE_TO_EDIT, + template: { + ...COMPONENT_TEMPLATE_TO_EDIT.template, + mappings: { + _meta: {}, + _source: {}, + properties: {}, + }, + }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts new file mode 100644 index 00000000000000..e6ced2fcc309a6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts @@ -0,0 +1,38 @@ +/* + * 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils'; +import { BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateCreate } from '../../../component_template_wizard'; + +import { WithAppDependencies } from './setup_environment'; +import { + getFormActions, + ComponentTemplateFormTestSubjects, +} from './component_template_form.helpers'; + +export type ComponentTemplateCreateTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/create_component_template`], + componentRoutePath: `${BASE_PATH}/create_component_template`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateCreate), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts new file mode 100644 index 00000000000000..3c0cbb19577a9b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts @@ -0,0 +1,38 @@ +/* + * 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils'; +import { BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateEdit } from '../../../component_template_wizard'; + +import { WithAppDependencies } from './setup_environment'; +import { + getFormActions, + ComponentTemplateFormTestSubjects, +} from './component_template_form.helpers'; + +export type ComponentTemplateEditTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/edit_component_template/comp-1`], + componentRoutePath: `${BASE_PATH}/edit_component_template/:name`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateEdit), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts new file mode 100644 index 00000000000000..c9a4ff680aad5b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -0,0 +1,168 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { TestBed } from '../../../../../../../../../test_utils'; + +interface MappingField { + name: string; + type: string; +} + +export const getFormActions = (testBed: TestBed) => { + // User actions + const toggleVersionSwitch = () => { + testBed.form.toggleEuiSwitch('versionToggle'); + }; + + const toggleMetaSwitch = () => { + testBed.form.toggleEuiSwitch('metaToggle'); + }; + + const clickNextButton = () => { + testBed.find('nextButton').simulate('click'); + }; + + const clickBackButton = () => { + testBed.find('backButton').simulate('click'); + }; + + const clickSubmitButton = () => { + testBed.find('submitButton').simulate('click'); + }; + + const setMetaField = (jsonString: string) => { + testBed.find('mockCodeEditor').simulate('change', { + jsonString, + }); + }; + + const selectReviewTab = (tab: 'summary' | 'request') => { + const tabs = ['summary', 'request']; + + testBed.find('stepReview.content').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); + }; + + const completeStepLogistics = async ({ name }: { name: string }) => { + const { form, component } = testBed; + // Add name field + form.setInputValue('nameField.input', name); + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepSettings = async (settings?: { [key: string]: any }) => { + const { find, component } = testBed; + + await act(async () => { + if (settings) { + find('mockCodeEditor').simulate('change', { + jsonString: JSON.stringify(settings), + }); // Using mocked EuiCodeEditor + } + }); + + component.update(); + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const addMappingField = async (name: string, type: string) => { + const { find, form, component } = testBed; + + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.mockComboBox').simulate('change', [ + { + label: type, + value: type, + }, + ]); + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); + }; + + const completeStepMappings = async (mappingFields?: MappingField[]) => { + const { component } = testBed; + + if (mappingFields) { + for (const field of mappingFields) { + const { name, type } = field; + await addMappingField(name, type); + } + } + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepAliases = async (aliases?: { [key: string]: any }) => { + const { find, component } = testBed; + + await act(async () => { + if (aliases) { + find('mockCodeEditor').simulate('change', { + jsonString: JSON.stringify(aliases), + }); // Using mocked EuiCodeEditor + } + }); + + component.update(); + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + return { + toggleVersionSwitch, + toggleMetaSwitch, + clickNextButton, + clickBackButton, + clickSubmitButton, + setMetaField, + selectReviewTab, + completeStepSettings, + completeStepAliases, + completeStepLogistics, + completeStepMappings, + }; +}; + +export type ComponentTemplateFormTestSubjects = + | 'backButton' + | 'documentationLink' + | 'metaToggle' + | 'metaEditor' + | 'mockCodeEditor' + | 'nameField.input' + | 'nextButton' + | 'pageTitle' + | 'saveComponentTemplateError' + | 'submitButton' + | 'stepReview' + | 'stepReview.title' + | 'stepReview.content' + | 'stepReview.summaryTab' + | 'stepReview.requestTab' + | 'versionField' + | 'versionField.input' + | 'versionToggle'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts index b7b674292dd98e..a4e532ba5d3d3a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts @@ -5,7 +5,11 @@ */ import sinon, { SinonFakeServer } from 'sinon'; -import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { + ComponentTemplateListItem, + ComponentTemplateDeserialized, + ComponentTemplateSerialized, +} from '../../../shared_imports'; import { API_BASE_PATH } from './constants'; // Register helpers to mock HTTP Requests @@ -46,10 +50,25 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setCreateComponentTemplateResponse = ( + response?: ComponentTemplateSerialized, + error?: any + ) => { + const status = error ? error.body.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', `${API_BASE_PATH}/component_templates`, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + return { setLoadComponentTemplatesResponse, setDeleteComponentTemplateResponse, setLoadComponentTemplateResponse, + setCreateComponentTemplateResponse, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx index c937733ecd4a56..6e35fbad31d4e4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx @@ -154,7 +154,7 @@ export const ComponentTemplateForm = ({ return componentTemplate; }; - const onSaveTemplate = useCallback( + const onSaveComponentTemplate = useCallback( async (wizardData: WizardContent) => { const componentTemplate = buildComponentTemplateObject(defaultValue)(wizardData); @@ -173,7 +173,7 @@ export const ComponentTemplateForm = ({ return ( defaultValue={wizardDefaultValue} - onSave={onSaveTemplate} + onSave={onSaveComponentTemplate} isEditing={isEditing} isSaving={isSaving} apiError={apiError} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx index 71aa8cdb1ece4c..c994e55f54acb2 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx @@ -174,9 +174,9 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen }; return ( -
+
-

+

= React.memo(({ componen Date: Thu, 2 Jul 2020 12:21:26 -0400 Subject: [PATCH 14/20] fix version field --- .../component_template_details/tab_summary.tsx | 2 +- .../component_template_form/steps/step_logistics.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx index 401186f6c962e0..ed42b5bb5988d4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent = ({ componentTemplateDe )} {/* Version (optional) */} - {version && ( + {isNaN(version) === false && ( <> = React.memo( const { documentation } = useComponentTemplatesContext(); const [isVersionVisible, setIsVersionVisible] = useState( - Boolean(defaultValue.version) + isNaN(defaultValue.version) === false ); const [isMetaVisible, setIsMetaVisible] = useState( From 2116798e4a0cb05ca1c7a0a3e6329751f5d319c0 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 2 Jul 2020 12:43:42 -0400 Subject: [PATCH 15/20] clean up tests --- .../client_integration/component_template_edit.test.tsx | 5 ----- .../helpers/component_template_form.helpers.ts | 8 -------- 2 files changed, 13 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index cabc2a5c53bb1c..30eef428260498 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -93,11 +93,6 @@ describe('', () => { await act(async () => { form.setInputValue('versionField.input', '1'); - }); - - component.update(); - - await act(async () => { actions.clickNextButton(); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts index c9a4ff680aad5b..74b0d3db9ca3a6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -67,11 +67,7 @@ export const getFormActions = (testBed: TestBed) => { jsonString: JSON.stringify(settings), }); // Using mocked EuiCodeEditor } - }); - - component.update(); - await act(async () => { clickNextButton(); }); @@ -121,11 +117,7 @@ export const getFormActions = (testBed: TestBed) => { jsonString: JSON.stringify(aliases), }); // Using mocked EuiCodeEditor } - }); - - component.update(); - await act(async () => { clickNextButton(); }); From b0b68e0d53095fb1c084e5ca7f7c9da8ded28685 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 2 Jul 2020 13:14:13 -0400 Subject: [PATCH 16/20] remove version field toggle --- .../component_template_create.test.tsx | 15 ----- .../component_template_edit.test.tsx | 6 -- .../component_template_form.helpers.ts | 3 +- .../tab_summary.tsx | 2 +- .../steps/step_logistics.tsx | 58 +++++++++---------- 5 files changed, 28 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx index 0c88cc1512804f..6c8da4684f019a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx @@ -68,21 +68,6 @@ describe('', () => { }); describe('Step: Logistics', () => { - test('should toggle the version field', async () => { - const { exists, component, actions } = testBed; - - // Version field should be hidden by default - expect(exists('versionField')).toBe(false); - - await act(async () => { - actions.toggleVersionSwitch(); - }); - - component.update(); - - expect(exists('versionField')).toBe(true); - }); - test('should toggle the metadata field', async () => { const { exists, component, actions } = testBed; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index 30eef428260498..f237605756d5c3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -85,12 +85,6 @@ describe('', () => { it('should send the correct payload with changed values', async () => { const { actions, component, form } = testBed; - await act(async () => { - actions.toggleVersionSwitch(); - }); - - component.update(); - await act(async () => { form.setInputValue('versionField.input', '1'); actions.clickNextButton(); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts index 74b0d3db9ca3a6..f92f46d71e7c79 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -156,5 +156,4 @@ export type ComponentTemplateFormTestSubjects = | 'stepReview.summaryTab' | 'stepReview.requestTab' | 'versionField' - | 'versionField.input' - | 'versionToggle'; + | 'versionField.input'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx index ed42b5bb5988d4..67d668194e4234 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent = ({ componentTemplateDe )} {/* Version (optional) */} - {isNaN(version) === false && ( + {typeof version !== undefined && ( <> = React.memo( const { documentation } = useComponentTemplatesContext(); - const [isVersionVisible, setIsVersionVisible] = useState( - isNaN(defaultValue.version) === false - ); - const [isMetaVisible, setIsMetaVisible] = useState( Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length) ); @@ -110,7 +106,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( - {/* Name with optional version field */} + {/* Name field */} = React.memo( /> } description={ - <> - - - - - - } - checked={isVersionVisible} - onChange={(e) => setIsVersionVisible(e.target.checked)} - data-test-subj="versionToggle" - /> - + } > = React.memo( euiFieldProps: { disabled: isEditing }, }} /> + - {isVersionVisible && ( - - )} + } + description={ + + } + > + {/* _meta field */} From 2a4c47dcc7cc2c992393c34ef6e1a9ec71e2870d Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 3 Jul 2020 21:08:47 -0400 Subject: [PATCH 17/20] fix test --- .../component_template_details/tab_summary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx index 67d668194e4234..80f28f23c9f91e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent = ({ componentTemplateDe )} {/* Version (optional) */} - {typeof version !== undefined && ( + {typeof version !== 'undefined' && ( <> Date: Sat, 4 Jul 2020 21:29:37 -0400 Subject: [PATCH 18/20] fix TS and tests --- .../component_template_list/component_template_list.tsx | 2 +- .../component_template_form/steps/step_logistics_container.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index ec23c9428a51bd..2c54be6e7313ac 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -74,7 +74,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ /> ); } else if (data && data.length === 0) { - content = ; + content = ; } else if (error) { content = ; } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx index 3c443b45ac6ca6..d71e36c0d997f4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx @@ -14,7 +14,7 @@ interface Props { } export const StepLogisticsContainer = ({ isEditing = false }: Props) => { - const { defaultValue, updateContent } = Forms.useContent('logistics'); + const { defaultValue, updateContent } = Forms.useContent('logistics'); return ( From ce140d5f37ec1caab8d26b782e9f06654dbf6d82 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Jul 2020 14:15:18 -0400 Subject: [PATCH 19/20] address review feedback --- .../component_template_list.tsx | 21 +++++++++++++----- .../component_template_list/table.tsx | 2 +- .../component_template_clone.tsx | 13 +++++------ .../component_template_create.tsx | 4 +++- .../component_template_edit.tsx | 4 +++- .../steps/step_logistics_schema.tsx | 22 ++++++++----------- .../steps/step_review.tsx | 6 ++--- .../component_templates/lib/utils.ts | 5 +++-- 8 files changed, 42 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 2c54be6e7313ac..d356eabc7997df 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -12,6 +12,7 @@ import { ScopedHistory } from 'kibana/public'; import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; +import { attemptToDecodeURI } from '../lib'; import { useComponentTemplatesContext } from '../component_templates_context'; import { ComponentTemplateDetailsFlyout } from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; @@ -35,15 +36,21 @@ export const ComponentTemplateList: React.FunctionComponent = ({ const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); const goToComponentTemplateList = () => { - return history.push('component_templates'); + return history.push({ + pathname: 'component_templates', + }); }; const goToEditComponentTemplate = (name: string) => { - return history.push(`edit_component_template/${encodeURIComponent(name)}`); + return history.push({ + pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`), + }); }; const goToCloneComponentTemplate = (name: string) => { - return history.push(`create_component_template/${encodeURIComponent(name)}`); + return history.push({ + pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`), + }); }; // Track component loaded @@ -110,14 +117,16 @@ export const ComponentTemplateList: React.FunctionComponent = ({ defaultMessage: 'Edit', }), icon: 'pencil', - handleActionClick: () => goToEditComponentTemplate(componentTemplateName), + handleActionClick: () => + goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)), }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { defaultMessage: 'Clone', }), icon: 'copy', - handleActionClick: () => goToCloneComponentTemplate(componentTemplateName), + handleActionClick: () => + goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)), }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { @@ -128,7 +137,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ details._kbnMeta.usedBy.length > 0, closePopoverOnClick: true, handleActionClick: () => { - setComponentTemplatesToDelete([componentTemplateName]); + setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]); }, }, ]} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index f922bff08205de..089c2f889e726e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -150,7 +150,7 @@ export const ComponentTable: FunctionComponent = ({ {...reactRouterNavigate( history, { - pathname: `/component_templates/${encodeURIComponent(name)}`, + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), }, () => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS) )} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx index 1761b1bb72c300..94db623f313c79 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -20,16 +20,13 @@ export interface Params { export const ComponentTemplateClone: FunctionComponent> = (props) => { const { sourceComponentTemplateName } = props.match.params; - const decodedSourceComponentTemplateName = attemptToDecodeURI(sourceComponentTemplateName); + const decodedSourceName = attemptToDecodeURI(sourceComponentTemplateName); const { toasts, api } = useComponentTemplatesContext(); - const { - error, - data: componentTemplateToClone, - isLoading, - isInitialRequest, - } = api.useLoadComponentTemplate(decodedSourceComponentTemplateName); + const { error, data: componentTemplateToClone, isLoading } = api.useLoadComponentTemplate( + decodedSourceName + ); useEffect(() => { if (error && !isLoading) { @@ -43,7 +40,7 @@ export const ComponentTemplateClone: FunctionComponent { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 96bc45de80803e..2bd3dfb34acb9a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -49,7 +49,9 @@ export const ComponentTemplateEdit: React.FunctionComponent { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx index 745ac8a31a83c2..0c52037abde459 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx @@ -87,19 +87,15 @@ export const logisticsFormSchema: FormSchema = { deserializer: stringifyJson, validations: [ { - validator: (validationArg) => { - if (!validationArg.value) { - return; - } - return isJsonField( - i18n.translate( - 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.metaJsonError', - { - defaultMessage: 'The input is not valid.', - } - ) - )(validationArg); - }, + validator: isJsonField( + i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.metaJsonError', + { + defaultMessage: 'The input is not valid.', + } + ), + { allowEmptyString: true } + ), }, ], }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx index c994e55f54acb2..c5e667c6a4877e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx @@ -48,10 +48,10 @@ interface Props { } export const StepReview: React.FunctionComponent = React.memo(({ componentTemplate }) => { - const { name, version } = componentTemplate!; + const { name, version } = componentTemplate; const serializedComponentTemplate = serializeComponentTemplate( - stripEmptyFields(componentTemplate!, { + stripEmptyFields(componentTemplate, { types: ['string', 'object'], }) as ComponentTemplateDeserialized ); @@ -73,7 +73,7 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen {/* Version */} - {version && ( + {version !== '' && ( <> { let result: string; try { - result = decodeURIComponent(value); + result = decodeURI(value); + result = decodeURIComponent(result); } catch (e) { - result = value; + result = decodeURIComponent(value); } return result; From 5d22378b94cd993e0b039a145f760ad42bce84a0 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Jul 2020 15:50:25 -0400 Subject: [PATCH 20/20] fix TS --- .../component_template_form/steps/step_review.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx index c5e667c6a4877e..ce85854dc79ab6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx @@ -48,7 +48,7 @@ interface Props { } export const StepReview: React.FunctionComponent = React.memo(({ componentTemplate }) => { - const { name, version } = componentTemplate; + const { name } = componentTemplate; const serializedComponentTemplate = serializeComponentTemplate( stripEmptyFields(componentTemplate, { @@ -63,6 +63,7 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen aliases: serializedAliases, }, _meta: serializedMeta, + version: serializedVersion, } = serializedComponentTemplate; const SummaryTab = () => ( @@ -73,7 +74,7 @@ export const StepReview: React.FunctionComponent = React.memo(({ componen {/* Version */} - {version !== '' && ( + {typeof serializedVersion !== 'undefined' && ( <> = React.memo(({ componen defaultMessage="Version" /> - {version} + {serializedVersion} )}