diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx
new file mode 100644
index 000000000000..aa575cd64944
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx
@@ -0,0 +1,166 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useMemo,
+ useEffect,
+ useRef,
+} from 'react';
+import { EuiFlyout } from '@elastic/eui';
+
+interface Context {
+ addContent:
(content: Content
) => void;
+ removeContent: (contentId: string) => void;
+ closeFlyout: () => void;
+}
+
+interface Content
{
+ id: string;
+ Component: React.FunctionComponent
;
+ props?: P;
+ flyoutProps?: { [key: string]: any };
+ cleanUpFunc?: () => void;
+}
+
+const FlyoutMultiContentContext = createContext(undefined);
+
+const DEFAULT_FLYOUT_PROPS = {
+ 'data-test-subj': 'flyout',
+ size: 'm' as 'm',
+ maxWidth: 500,
+};
+
+export const GlobalFlyoutProvider: React.FC = ({ children }) => {
+ const [showFlyout, setShowFlyout] = useState(false);
+ const [activeContent, setActiveContent] = useState | undefined>(undefined);
+
+ const { id, Component, props, flyoutProps } = activeContent ?? {};
+
+ const addContent: Context['addContent'] = useCallback((content) => {
+ setActiveContent((prev) => {
+ if (prev !== undefined) {
+ if (prev.id !== content.id && prev.cleanUpFunc) {
+ // Clean up anything from the content about to be removed
+ prev.cleanUpFunc();
+ }
+ }
+ return content;
+ });
+
+ setShowFlyout(true);
+ }, []);
+
+ const closeFlyout: Context['closeFlyout'] = useCallback(() => {
+ setActiveContent(undefined);
+ setShowFlyout(false);
+ }, []);
+
+ const removeContent: Context['removeContent'] = useCallback(
+ (contentId: string) => {
+ if (contentId === id) {
+ closeFlyout();
+ }
+ },
+ [id, closeFlyout]
+ );
+
+ const mergedFlyoutProps = useMemo(() => {
+ return {
+ ...DEFAULT_FLYOUT_PROPS,
+ onClose: closeFlyout,
+ ...flyoutProps,
+ };
+ }, [flyoutProps, closeFlyout]);
+
+ const context: Context = {
+ addContent,
+ removeContent,
+ closeFlyout,
+ };
+
+ const ContentFlyout = showFlyout && Component !== undefined ? Component : null;
+
+ return (
+
+ <>
+ {children}
+ {ContentFlyout && (
+
+
+
+ )}
+ >
+
+ );
+};
+
+export const useGlobalFlyout = () => {
+ const ctx = useContext(FlyoutMultiContentContext);
+
+ if (ctx === undefined) {
+ throw new Error('useGlobalFlyout must be used within a ');
+ }
+
+ const isMounted = useRef(false);
+ /**
+ * A component can add one or multiple content to the flyout
+ * during its lifecycle. When it unmounts, we will remove
+ * all those content added to the flyout.
+ */
+ const contents = useRef | undefined>(undefined);
+ const { removeContent, addContent: addContentToContext } = ctx;
+
+ useEffect(() => {
+ isMounted.current = true;
+
+ return () => {
+ isMounted.current = false;
+ };
+ }, []);
+
+ const getContents = useCallback(() => {
+ if (contents.current === undefined) {
+ contents.current = new Set();
+ }
+ return contents.current;
+ }, []);
+
+ const addContent: Context['addContent'] = useCallback(
+ (content) => {
+ getContents().add(content.id);
+ return addContentToContext(content);
+ },
+ [getContents, addContentToContext]
+ );
+
+ useEffect(() => {
+ return () => {
+ if (!isMounted.current) {
+ // When the component unmounts, remove all the content it has added to the flyout
+ Array.from(getContents()).forEach(removeContent);
+ }
+ };
+ }, [removeContent]);
+
+ return { ...ctx, addContent };
+};
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts
new file mode 100644
index 000000000000..c49692547fb2
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { GlobalFlyoutProvider, useGlobalFlyout } from './global_flyout';
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx
index cdb332e9e913..642a21eae50e 100644
--- a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx
@@ -27,13 +27,14 @@ import {
} from './form_wizard_context';
import { FormWizardNav, NavTexts } from './form_wizard_nav';
-interface Props extends ProviderProps {
+interface Props extends ProviderProps {
isSaving?: boolean;
apiError: JSX.Element | null;
texts?: Partial;
+ rightContentNav?: JSX.Element | null | ((stepId: S) => JSX.Element | null);
}
-export function FormWizard({
+export function FormWizard({
texts,
defaultActiveStep,
defaultValue,
@@ -43,7 +44,8 @@ export function FormWizard({
onSave,
onChange,
children,
-}: Props) {
+ rightContentNav,
+}: Props) {
return (
defaultValue={defaultValue}
@@ -53,7 +55,14 @@ export function FormWizard({
defaultActiveStep={defaultActiveStep}
>
- {({ activeStepIndex, lastStep, steps, isCurrentStepValid, navigateToStep }) => {
+ {({
+ activeStepIndex,
+ lastStep,
+ steps,
+ isCurrentStepValid,
+ navigateToStep,
+ activeStepId,
+ }) => {
const stepsRequiredArray = Object.values(steps).map(
(step) => Boolean(step.isRequired) && step.isComplete === false
);
@@ -95,6 +104,13 @@ export function FormWizard({
};
});
+ const getRightContentNav = () => {
+ if (typeof rightContentNav === 'function') {
+ return rightContentNav(activeStepId);
+ }
+ return rightContentNav;
+ };
+
const onBack = () => {
const prevStep = activeStepIndex - 1;
navigateToStep(prevStep);
@@ -129,6 +145,7 @@ export function FormWizard({
onBack={onBack}
onNext={onNext}
texts={texts}
+ getRightContent={getRightContentNav}
/>
>
);
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx
index 3e0e9cf897b5..0af99e8bce35 100644
--- a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx
@@ -29,6 +29,7 @@ interface Props {
isSaving?: boolean;
isStepValid?: boolean;
texts?: Partial;
+ getRightContent?: () => JSX.Element | null | undefined;
}
export interface NavTexts {
@@ -53,6 +54,7 @@ export const FormWizardNav = ({
onBack,
onNext,
texts,
+ getRightContent,
}: Props) => {
const isLastStep = activeStepIndex === lastStep;
const labels = {
@@ -66,6 +68,8 @@ export const FormWizardNav = ({
: labels.save
: labels.next;
+ const rightContent = getRightContent !== undefined ? getRightContent() : undefined;
+
return (
@@ -100,6 +104,8 @@ export const FormWizardNav = ({
+
+ {rightContent && {rightContent}}
);
};
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 8d470f6454b0..2e7c91a26e1f 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
@@ -94,7 +94,7 @@ export function useMultiContent({
const activeContentData: Partial = {};
for (const [id, _content] of Object.entries(contents.current)) {
- if (validation.contents[id as keyof T]) {
+ if (validation.contents[id as keyof T] !== false) {
const contentData = (_content as Content).getData();
// Replace the getData() handler with the cached value
@@ -161,7 +161,7 @@ export function useMultiContent({
);
/**
- * Validate the multi-content active content(s) in the DOM
+ * Validate the content(s) currently in the DOM
*/
const validate = useCallback(async () => {
if (Object.keys(contents.current).length === 0) {
diff --git a/src/plugins/es_ui_shared/public/global_flyout/index.ts b/src/plugins/es_ui_shared/public/global_flyout/index.ts
new file mode 100644
index 000000000000..e876594337c1
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/global_flyout/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export {
+ GlobalFlyoutProvider,
+ useGlobalFlyout,
+} from '../../__packages_do_not_import__/global_flyout';
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index 98a305fe68f0..bdea5ccf5fe2 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -24,6 +24,7 @@
import * as Forms from './forms';
import * as Monaco from './monaco';
import * as ace from './ace';
+import * as GlobalFlyout from './global_flyout';
export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor';
@@ -65,7 +66,7 @@ export {
useAuthorizationContext,
} from './authorization';
-export { Monaco, Forms, ace };
+export { Monaco, Forms, ace, GlobalFlyout };
export { extractQueryParams } from './url';
diff --git a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts
index 98287f6bac35..733a60f1f86f 100644
--- a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts
+++ b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts
@@ -64,9 +64,13 @@ interface StripEmptyFieldsOptions {
* @param options An optional configuration object. By default recursive it turned on.
*/
export const stripEmptyFields = (
- object: { [key: string]: any },
+ object?: { [key: string]: any },
options?: StripEmptyFieldsOptions
): { [key: string]: any } => {
+ if (object === undefined) {
+ return {};
+ }
+
const { types = ['string', 'object'], recursive = false } = options || {};
return Object.entries(object).reduce((acc, [key, value]) => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
index 907c749f8ec0..12cf7ccac6c5 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
@@ -92,6 +92,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
+ const setSimulateTemplateResponse = (response?: HttpResponse, error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
+
+ server.respondWith('POST', `${API_BASE_PATH}/index_templates/simulate`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ body,
+ ]);
+ };
+
return {
setLoadTemplatesResponse,
setLoadIndicesResponse,
@@ -102,6 +113,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setLoadTemplateResponse,
setCreateTemplateResponse,
setUpdateTemplateResponse,
+ setSimulateTemplateResponse,
};
};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
index ad445f75f047..e40cdc026210 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
@@ -14,6 +14,8 @@ import {
notificationServiceMock,
docLinksServiceMock,
} from '../../../../../../src/core/public/mocks';
+import { GlobalFlyout } from '../../../../../../src/plugins/es_ui_shared/public';
+
import { AppContextProvider } from '../../../public/application/app_context';
import { httpService } from '../../../public/application/services/http';
import { breadcrumbService } from '../../../public/application/services/breadcrumbs';
@@ -23,9 +25,11 @@ import { ExtensionsService } from '../../../public/services';
import { UiMetricService } from '../../../public/application/services/ui_metric';
import { setUiMetricService } from '../../../public/application/services/api';
import { setExtensionsService } from '../../../public/application/store/selectors';
+import { MappingsEditorProvider } from '../../../public/application/components';
import { init as initHttpRequests } from './http_requests';
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
+const { GlobalFlyoutProvider } = GlobalFlyout;
export const services = {
extensionsService: new ExtensionsService(),
@@ -62,7 +66,11 @@ export const WithAppDependencies = (Comp: any, overridingDependencies: any = {})
const mergedDependencies = merge({}, appDependencies, overridingDependencies);
return (
-
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
index 9889ebe16ba1..ecedf819e618 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
@@ -28,6 +28,7 @@ export type TestSubjects =
| 'legacyTemplateTable'
| 'manageTemplateButton'
| 'mappingsTabContent'
+ | 'previewTabContent'
| 'noAliasesCallout'
| 'noMappingsCallout'
| 'noSettingsCallout'
@@ -48,4 +49,5 @@ export type TestSubjects =
| 'templateList'
| 'templatesTab'
| 'templateTable'
- | 'viewButton';
+ | 'viewButton'
+ | 'simulateTemplatePreview';
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
index a39741905335..23b40f4cbd3d 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
@@ -40,10 +40,15 @@ const createActions = (testBed: TestBed) => {
/**
* User Actions
*/
- const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => {
- const tabs = ['summary', 'settings', 'mappings', 'aliases'];
+ const selectDetailsTab = async (
+ tab: 'summary' | 'settings' | 'mappings' | 'aliases' | 'preview'
+ ) => {
+ const tabs = ['summary', 'settings', 'mappings', 'aliases', 'preview'];
- testBed.find('templateDetails.tab').at(tabs.indexOf(tab)).simulate('click');
+ await act(async () => {
+ testBed.find('templateDetails.tab').at(tabs.indexOf(tab)).simulate('click');
+ });
+ testBed.component.update();
};
const clickReloadButton = () => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index f7ebc0bcf632..06f57896d490 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -493,7 +493,7 @@ describe('Index Templates tab', () => {
});
describe('tabs', () => {
- test('should have 4 tabs', async () => {
+ test('should have 5 tabs', async () => {
const template = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
@@ -524,35 +524,48 @@ describe('Index Templates tab', () => {
const { find, actions, exists } = testBed;
httpRequestsMockHelpers.setLoadTemplateResponse(template);
+ httpRequestsMockHelpers.setSimulateTemplateResponse({ simulateTemplate: 'response' });
await actions.clickTemplateAt(0);
- expect(find('templateDetails.tab').length).toBe(4);
+ expect(find('templateDetails.tab').length).toBe(5);
expect(find('templateDetails.tab').map((t) => t.text())).toEqual([
'Summary',
'Settings',
'Mappings',
'Aliases',
+ 'Preview',
]);
// Summary tab should be initial active tab
expect(exists('summaryTab')).toBe(true);
// Navigate and verify all tabs
- actions.selectDetailsTab('settings');
+ await actions.selectDetailsTab('settings');
expect(exists('summaryTab')).toBe(false);
expect(exists('settingsTabContent')).toBe(true);
- actions.selectDetailsTab('aliases');
+ await actions.selectDetailsTab('aliases');
expect(exists('summaryTab')).toBe(false);
expect(exists('settingsTabContent')).toBe(false);
expect(exists('aliasesTabContent')).toBe(true);
- actions.selectDetailsTab('mappings');
+ await actions.selectDetailsTab('mappings');
expect(exists('summaryTab')).toBe(false);
expect(exists('settingsTabContent')).toBe(false);
expect(exists('aliasesTabContent')).toBe(false);
expect(exists('mappingsTabContent')).toBe(true);
+
+ await actions.selectDetailsTab('preview');
+ expect(exists('summaryTab')).toBe(false);
+ expect(exists('settingsTabContent')).toBe(false);
+ expect(exists('aliasesTabContent')).toBe(false);
+ expect(exists('mappingsTabContent')).toBe(false);
+ expect(exists('previewTabContent')).toBe(true);
+
+ expect(find('simulateTemplatePreview').text().replace(/\s/g, '')).toEqual(
+ JSON.stringify({ simulateTemplate: 'response' })
+ );
});
test('should show an info callout if data is not present', async () => {
@@ -568,17 +581,17 @@ describe('Index Templates tab', () => {
await actions.clickTemplateAt(0);
- expect(find('templateDetails.tab').length).toBe(4);
+ expect(find('templateDetails.tab').length).toBe(5);
expect(exists('summaryTab')).toBe(true);
// Navigate and verify callout message per tab
- actions.selectDetailsTab('settings');
+ await actions.selectDetailsTab('settings');
expect(exists('noSettingsCallout')).toBe(true);
- actions.selectDetailsTab('mappings');
+ await actions.selectDetailsTab('mappings');
expect(exists('noMappingsCallout')).toBe(true);
- actions.selectDetailsTab('aliases');
+ await actions.selectDetailsTab('aliases');
expect(exists('noAliasesCallout')).toBe(true);
});
});
diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts
index d1700f0e611c..11240271503e 100644
--- a/x-pack/plugins/index_management/common/constants/index.ts
+++ b/x-pack/plugins/index_management/common/constants/index.ts
@@ -47,7 +47,9 @@ export {
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
+ UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB,
UIM_TEMPLATE_CREATE,
UIM_TEMPLATE_UPDATE,
UIM_TEMPLATE_CLONE,
+ UIM_TEMPLATE_SIMULATE,
} from './ui_metric';
diff --git a/x-pack/plugins/index_management/common/constants/ui_metric.ts b/x-pack/plugins/index_management/common/constants/ui_metric.ts
index 5fda812c704d..545555b92f35 100644
--- a/x-pack/plugins/index_management/common/constants/ui_metric.ts
+++ b/x-pack/plugins/index_management/common/constants/ui_metric.ts
@@ -41,6 +41,8 @@ export const UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB = 'template_details_summary_t
export const UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB = 'template_details_settings_tab';
export const UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB = 'template_details_mappings_tab';
export const UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB = 'template_details_aliases_tab';
+export const UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB = 'template_details_preview_tab';
export const UIM_TEMPLATE_CREATE = 'template_create';
export const UIM_TEMPLATE_UPDATE = 'template_update';
export const UIM_TEMPLATE_CLONE = 'template_clone';
+export const UIM_TEMPLATE_SIMULATE = 'template_simulate';
diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts
index 069d6ac29fbc..1803d89a4001 100644
--- a/x-pack/plugins/index_management/common/lib/template_serialization.ts
+++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts
@@ -109,7 +109,7 @@ export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyT
version,
order,
indexPatterns,
- template: { settings, aliases, mappings },
+ template: { settings, aliases, mappings } = {},
} = template;
return {
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts
index 3d496d68cc66..a112d73230b8 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts
@@ -61,11 +61,10 @@ describe('', () => {
const { exists, find, actions, component } = testBed;
// Verify flyout exists with correct title
- expect(exists('componentTemplateDetails')).toBe(true);
- expect(find('componentTemplateDetails.title').text()).toBe(COMPONENT_TEMPLATE.name);
+ expect(find('title').text()).toBe(COMPONENT_TEMPLATE.name);
// Verify footer does not display since "actions" prop was not provided
- expect(exists('componentTemplateDetails.footer')).toBe(false);
+ expect(exists('footer')).toBe(false);
// Verify tabs exist
expect(exists('settingsTab')).toBe(true);
@@ -185,7 +184,7 @@ describe('', () => {
const { exists, actions, component, find } = testBed;
// Verify footer exists
- expect(exists('componentTemplateDetails.footer')).toBe(true);
+ expect(exists('footer')).toBe(true);
expect(exists('manageComponentTemplateButton')).toBe(true);
// Click manage button and verify actions
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts
index 25c2d654fd90..fe81e8dcfe12 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts
@@ -6,7 +6,7 @@
import { registerTestBed, TestBed } from '../../../../../../../../../test_utils';
import { WithAppDependencies } from './setup_environment';
-import { ComponentTemplateDetailsFlyout } from '../../../component_template_details';
+import { ComponentTemplateDetailsFlyoutContent } from '../../../component_template_details';
export type ComponentTemplateDetailsTestBed = TestBed & {
actions: ReturnType;
@@ -44,7 +44,7 @@ const createActions = (testBed: TestBed) =
export const setup = (props: any): ComponentTemplateDetailsTestBed => {
const setupTestBed = registerTestBed(
- WithAppDependencies(ComponentTemplateDetailsFlyout),
+ WithAppDependencies(ComponentTemplateDetailsFlyoutContent),
{
memoryRouter: {
wrapComponent: false,
@@ -65,6 +65,8 @@ export type ComponentTemplateDetailsTestSubjects =
| 'componentTemplateDetails'
| 'componentTemplateDetails.title'
| 'componentTemplateDetails.footer'
+ | 'title'
+ | 'footer'
| 'summaryTab'
| 'mappingsTab'
| 'settingsTab'
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 7e460d3855cb..2f7317e3e656 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
@@ -15,12 +15,15 @@ import {
applicationServiceMock,
} from '../../../../../../../../../../src/core/public/mocks';
+import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public';
+import { MappingsEditorProvider } from '../../../../mappings_editor';
import { ComponentTemplatesProvider } from '../../../component_templates_context';
import { init as initHttpRequests } from './http_requests';
import { API_BASE_PATH } from './constants';
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
+const { GlobalFlyoutProvider } = GlobalFlyout;
const appDependencies = {
httpClient: (mockHttpClient as unknown) as HttpSetup,
@@ -42,7 +45,11 @@ export const setupEnvironment = () => {
};
export const WithAppDependencies = (Comp: any) => (props: any) => (
-
-
-
+
+
+
+
+
+
+
);
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 60f1fff3cc9d..0f5bc64c358b 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
@@ -8,7 +8,6 @@ import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
@@ -28,14 +27,19 @@ import { ComponentTemplateTabs, TabType } from './tabs';
import { ManageButton, ManageAction } from './manage_button';
import { attemptToDecodeURI } from '../lib';
-interface Props {
+export interface Props {
componentTemplateName: string;
onClose: () => void;
actions?: ManageAction[];
showSummaryCallToAction?: boolean;
}
-export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({
+export const defaultFlyoutProps = {
+ 'data-test-subj': 'componentTemplateDetails',
+ 'aria-labelledby': 'componentTemplateDetailsFlyoutTitle',
+};
+
+export const ComponentTemplateDetailsFlyoutContent: React.FunctionComponent = ({
componentTemplateName,
onClose,
actions,
@@ -109,13 +113,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({
}
return (
-
+ <>
@@ -172,6 +170,6 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({
)}
-
+ >
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts
index 11aac200a2f1..8687a1f5b89c 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts
@@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { ComponentTemplateDetailsFlyout } from './component_template_details';
+export {
+ ComponentTemplateDetailsFlyoutContent,
+ defaultFlyoutProps,
+ Props as ComponentTemplateDetailsProps,
+} from './component_template_details';
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 efc8b649ef87..8ba7409a9ac5 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
@@ -4,18 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
-import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports';
+import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } 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 {
+ ComponentTemplateDetailsFlyoutContent,
+ defaultFlyoutProps,
+ ComponentTemplateDetailsProps,
+} from '../component_template_details';
import { EmptyPrompt } from './empty_prompt';
import { ComponentTable } from './table';
import { LoadError } from './error';
@@ -26,39 +30,112 @@ interface Props {
history: RouteComponentProps['history'];
}
+const { useGlobalFlyout } = GlobalFlyout;
+
export const ComponentTemplateList: React.FunctionComponent = ({
componentTemplateName,
history,
}) => {
+ const {
+ addContent: addContentToGlobalFlyout,
+ removeContent: removeContentFromGlobalFlyout,
+ } = useGlobalFlyout();
const { api, trackMetric, documentation } = useComponentTemplatesContext();
const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates();
const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]);
- const goToComponentTemplateList = () => {
+ const goToComponentTemplateList = useCallback(() => {
return history.push({
pathname: 'component_templates',
});
- };
+ }, [history]);
- const goToEditComponentTemplate = (name: string) => {
- return history.push({
- pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`),
- });
- };
+ const goToEditComponentTemplate = useCallback(
+ (name: string) => {
+ return history.push({
+ pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`),
+ });
+ },
+ [history]
+ );
- const goToCloneComponentTemplate = (name: string) => {
- return history.push({
- pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`),
- });
- };
+ const goToCloneComponentTemplate = useCallback(
+ (name: string) => {
+ return history.push({
+ pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`),
+ });
+ },
+ [history]
+ );
// Track component loaded
useEffect(() => {
trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD);
}, [trackMetric]);
+ useEffect(() => {
+ if (componentTemplateName) {
+ const actions = [
+ {
+ name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.editButtonLabel', {
+ defaultMessage: 'Edit',
+ }),
+ icon: 'pencil',
+ handleActionClick: () =>
+ goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)),
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', {
+ defaultMessage: 'Clone',
+ }),
+ icon: 'copy',
+ handleActionClick: () =>
+ goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)),
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', {
+ defaultMessage: 'Delete',
+ }),
+ icon: 'trash',
+ getIsDisabled: (details: ComponentTemplateDeserialized) =>
+ details._kbnMeta.usedBy.length > 0,
+ closePopoverOnClick: true,
+ handleActionClick: () => {
+ setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]);
+ },
+ },
+ ];
+
+ // Open the flyout with the Component Template Details content
+ addContentToGlobalFlyout({
+ id: 'componentTemplateDetails',
+ Component: ComponentTemplateDetailsFlyoutContent,
+ props: {
+ onClose: goToComponentTemplateList,
+ componentTemplateName,
+ showSummaryCallToAction: true,
+ actions,
+ },
+ flyoutProps: { ...defaultFlyoutProps, onClose: goToComponentTemplateList },
+ });
+ }
+ }, [
+ componentTemplateName,
+ goToComponentTemplateList,
+ goToEditComponentTemplate,
+ goToCloneComponentTemplate,
+ addContentToGlobalFlyout,
+ history,
+ ]);
+
+ useEffect(() => {
+ if (!componentTemplateName) {
+ removeContentFromGlobalFlyout('componentTemplateDetails');
+ }
+ }, [componentTemplateName, removeContentFromGlobalFlyout]);
+
let content: React.ReactNode;
if (isLoading) {
@@ -126,45 +203,6 @@ export const ComponentTemplateList: React.FunctionComponent = ({
componentTemplatesToDelete={componentTemplatesToDelete}
/>
) : null}
-
- {/* details flyout */}
- {componentTemplateName && (
-
- goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)),
- },
- {
- name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', {
- defaultMessage: 'Clone',
- }),
- icon: 'copy',
- handleActionClick: () =>
- goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)),
- },
- {
- name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', {
- defaultMessage: 'Delete',
- }),
- icon: 'trash',
- getIsDisabled: (details: ComponentTemplateDeserialized) =>
- details._kbnMeta.usedBy.length > 0,
- closePopoverOnClick: true,
- handleActionClick: () => {
- setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]);
- },
- },
- ]}
- />
- )}
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx
index 8795c08fd2be..ed570579d4e4 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx
@@ -11,8 +11,12 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { ComponentTemplateListItem } from '../../../../../common';
-import { SectionError, SectionLoading } from '../shared_imports';
-import { ComponentTemplateDetailsFlyout } from '../component_template_details';
+import { SectionError, SectionLoading, GlobalFlyout } from '../shared_imports';
+import {
+ ComponentTemplateDetailsFlyoutContent,
+ defaultFlyoutProps,
+ ComponentTemplateDetailsProps,
+} from '../component_template_details';
import { CreateButtonPopOver } from './components';
import { ComponentTemplates } from './component_templates';
import { ComponentTemplatesSelection } from './component_templates_selection';
@@ -20,10 +24,12 @@ import { useApi } from '../component_templates_context';
import './component_templates_selector.scss';
+const { useGlobalFlyout } = GlobalFlyout;
+
interface Props {
onChange: (components: string[]) => void;
onComponentsLoaded: (components: ComponentTemplateListItem[]) => void;
- defaultValue: string[];
+ defaultValue?: string[];
docUri: string;
emptyPrompt?: {
text?: string | JSX.Element;
@@ -53,6 +59,10 @@ export const ComponentTemplatesSelector = ({
emptyPrompt: { text, showCreateButton } = {},
}: Props) => {
const { data: components, isLoading, error } = useApi().useLoadComponentTemplates();
+ const {
+ addContent: addContentToGlobalFlyout,
+ removeContent: removeContentFromGlobalFlyout,
+ } = useGlobalFlyout();
const [selectedComponent, setSelectedComponent] = useState(null);
const [componentsSelected, setComponentsSelected] = useState([]);
const isInitialized = useRef(false);
@@ -60,15 +70,20 @@ export const ComponentTemplatesSelector = ({
const hasSelection = Object.keys(componentsSelected).length > 0;
const hasComponents = components && components.length > 0 ? true : false;
+ const closeComponentTemplateDetails = () => {
+ setSelectedComponent(null);
+ };
+
useEffect(() => {
if (components) {
if (
+ defaultValue &&
defaultValue.length > 0 &&
componentsSelected.length === 0 &&
isInitialized.current === false
) {
- // Once the components are loaded we check the ones selected
- // from the defaultValue provided
+ // Once the components are fetched, we check the ones previously selected
+ // from the prop "defaultValue" passed.
const nextComponentsSelected = defaultValue
.map((name) => components.find((comp) => comp.name === name))
.filter(Boolean) as ComponentTemplateListItem[];
@@ -88,6 +103,30 @@ export const ComponentTemplatesSelector = ({
}
}, [isLoading, error, components, onComponentsLoaded]);
+ useEffect(() => {
+ if (selectedComponent) {
+ // Open the flyout with the Component Template Details content
+ addContentToGlobalFlyout({
+ id: 'componentTemplateDetails',
+ Component: ComponentTemplateDetailsFlyoutContent,
+ props: {
+ onClose: closeComponentTemplateDetails,
+ componentTemplateName: selectedComponent,
+ },
+ flyoutProps: { ...defaultFlyoutProps, onClose: closeComponentTemplateDetails },
+ cleanUpFunc: () => {
+ setSelectedComponent(null);
+ },
+ });
+ }
+ }, [selectedComponent, addContentToGlobalFlyout]);
+
+ useEffect(() => {
+ if (!selectedComponent) {
+ removeContentFromGlobalFlyout('componentTemplateDetails');
+ }
+ }, [selectedComponent, removeContentFromGlobalFlyout]);
+
const onSelectionReorder = (reorderedComponents: ComponentTemplateListItem[]) => {
setComponentsSelected(reorderedComponents);
};
@@ -198,30 +237,12 @@ export const ComponentTemplatesSelector = ({
);
- const renderComponentDetails = () => {
- if (!selectedComponent) {
- return null;
- }
-
- return (
- setSelectedComponent(null)}
- componentTemplateName={selectedComponent}
- />
- );
- };
-
if (isLoading) {
return renderLoading();
} else if (error) {
return renderError();
} else if (hasComponents) {
- return (
- <>
- {renderSelector()}
- {renderComponentDetails()}
- >
- );
+ return renderSelector();
}
// No components: render empty prompt
@@ -244,6 +265,7 @@ export const ComponentTemplatesSelector = ({
);
+
return (
{
+ const [templatePreview, setTemplatePreview] = useState('{}');
+
+ const updatePreview = useCallback(async () => {
+ if (!template || Object.keys(template).length === 0) {
+ return;
+ }
+
+ const indexTemplate = serializeTemplate(stripEmptyFields(template) as TemplateDeserialized);
+
+ // Until ES fixes a bug on their side we will send a random index pattern to the simulate API.
+ // Issue: https://github.com/elastic/elasticsearch/issues/59152
+ indexTemplate.index_patterns = [uuid.v4()];
+
+ const { data, error } = await simulateIndexTemplate(indexTemplate);
+
+ if (data) {
+ // "Overlapping" info is only useful when simulating against an index
+ // which we don't do here.
+ delete data.overlapping;
+ }
+
+ setTemplatePreview(JSON.stringify(data ?? error, null, 2));
+ }, [template]);
+
+ useEffect(() => {
+ updatePreview();
+ }, [updatePreview]);
+
+ return templatePreview === '{}' ? null : (
+
+ {templatePreview}
+
+ );
+});
diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx
new file mode 100644
index 000000000000..63bfe7854604
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template_flyout.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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, useCallback, useEffect, useRef } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiTextColor,
+ EuiText,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { SimulateTemplate } from './simulate_template';
+
+export interface Props {
+ onClose(): void;
+ getTemplate: () => { [key: string]: any };
+}
+
+export const defaultFlyoutProps = {
+ 'data-test-subj': 'simulateTemplateFlyout',
+ 'aria-labelledby': 'simulateTemplateFlyoutTitle',
+};
+
+export const SimulateTemplateFlyoutContent = ({ onClose, getTemplate }: Props) => {
+ const isMounted = useRef(false);
+ const [heightCodeBlock, setHeightCodeBlock] = useState(0);
+ const [template, setTemplate] = useState<{ [key: string]: any }>({});
+
+ useEffect(() => {
+ setHeightCodeBlock(
+ document.getElementsByClassName('euiFlyoutBody__overflow')[0].clientHeight - 96
+ );
+ }, []);
+
+ const updatePreview = useCallback(async () => {
+ const indexTemplate = await getTemplate();
+ setTemplate(indexTemplate);
+ }, [getTemplate]);
+
+ useEffect(() => {
+ if (isMounted.current === false) {
+ updatePreview();
+ }
+ isMounted.current = true;
+ }, [updatePreview]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx
index 311cb37d0b47..64347d19e9b4 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx
@@ -36,8 +36,6 @@ describe('Mappings editor: shape datatype', () => {
test('initial view and default parameters values', async () => {
const defaultMappings = {
- _meta: {},
- _source: {},
properties: {
myField: {
type: 'shape',
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx
index ed60414d198f..c03aa4805d27 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/text_datatype.test.tsx
@@ -47,8 +47,6 @@ describe.skip('Mappings editor: text datatype', () => {
test('initial view and default parameters values', async () => {
const defaultMappings = {
- _meta: {},
- _source: {},
properties: {
myField: {
type: 'text',
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx
index 4f9d8a960a1a..c146c7704911 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/edit_field.test.tsx
@@ -65,8 +65,6 @@ describe('Mappings editor: edit field', () => {
test('should update form parameters when changing the field datatype', async () => {
const defaultMappings = {
- _meta: {},
- _source: {},
properties: {
userName: {
...defaultTextParameters,
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx
index 638bbfd925ff..a6558b28a127 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx
@@ -7,9 +7,11 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
+import { GlobalFlyout } from '../../../../../../../../../../src/plugins/es_ui_shared/public';
import { registerTestBed, TestBed } from '../../../../../../../../../test_utils';
import { getChildFieldsName } from '../../../lib';
import { MappingsEditor } from '../../../mappings_editor';
+import { MappingsEditorProvider } from '../../../mappings_editor_context';
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
@@ -51,6 +53,8 @@ jest.mock('@elastic/eui', () => {
};
});
+const { GlobalFlyoutProvider } = GlobalFlyout;
+
export interface DomFields {
[key: string]: {
type: string;
@@ -247,7 +251,15 @@ const createActions = (testBed: TestBed) => {
};
export const setup = (props: any = { onUpdate() {} }): MappingsEditorTestBed => {
- const setupTestBed = registerTestBed(MappingsEditor, {
+ const ComponentToTest = (propsOverride: { [key: string]: any }) => (
+
+
+
+
+
+ );
+
+ const setupTestBed = registerTestBed(ComponentToTest, {
memoryRouter: {
wrapComponent: false,
},
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx
index 86bcc796a88e..20b2e1185502 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx
@@ -7,16 +7,14 @@ import React, { useEffect, useRef } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { useForm, Form, SerializerFunc } from '../../shared_imports';
-import { GenericObject } from '../../types';
-import { Types, useDispatch } from '../../mappings_state';
+import { GenericObject, MappingsConfiguration } from '../../types';
+import { useDispatch } from '../../mappings_state_context';
import { DynamicMappingSection } from './dynamic_mapping_section';
import { SourceFieldSection } from './source_field_section';
import { MetaFieldSection } from './meta_field_section';
import { RoutingSection } from './routing_section';
import { configurationFormSchema } from './configuration_form_schema';
-type MappingsConfiguration = Types['MappingsConfiguration'];
-
interface Props {
value?: MappingsConfiguration;
}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
index 6e80f8b813ec..8742dfc91692 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
@@ -11,8 +11,7 @@ import { EuiLink, EuiCode } from '@elastic/eui';
import { documentationService } from '../../../../services/documentation';
import { FormSchema, FIELD_TYPES, VALIDATION_TYPES, fieldValidators } from '../../shared_imports';
-import { MappingsConfiguration } from '../../reducer';
-import { ComboBoxOption } from '../../types';
+import { ComboBoxOption, MappingsConfiguration } from '../../types';
const { containsCharsField, isJsonField } = fieldValidators;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx
index 400de4052afa..4b19b6f7ae5c 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx
@@ -6,7 +6,7 @@
import React, { useMemo, useCallback } from 'react';
import { EuiSpacer } from '@elastic/eui';
-import { useMappingsState, useDispatch } from '../../mappings_state';
+import { useMappingsState, useDispatch } from '../../mappings_state_context';
import { deNormalize } from '../../lib';
import { EditFieldContainer } from './fields';
import { DocumentFieldsHeader } from './document_fields_header';
@@ -18,7 +18,7 @@ export const DocumentFields = React.memo(() => {
const { fields, search, documentFields } = useMappingsState();
const dispatch = useDispatch();
- const { status, fieldToEdit, editor: editorType } = documentFields;
+ const { editor: editorType } = documentFields;
const jsonEditorDefaultValue = useMemo(() => {
if (editorType === 'json') {
@@ -33,14 +33,6 @@ export const DocumentFields = React.memo(() => {
);
- const renderEditField = () => {
- if (status !== 'editingField') {
- return null;
- }
- const field = fields.byId[fieldToEdit!];
- return ;
- };
-
const onSearchChange = useCallback(
(value: string) => {
dispatch({ type: 'search:update', value });
@@ -59,7 +51,7 @@ export const DocumentFields = React.memo(() => {
) : (
editor
)}
- {renderEditField()}
+
);
});
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
index 51f9ca63be40..ad283a3fe47b 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { EuiButton, EuiText } from '@elastic/eui';
-import { useDispatch, useMappingsState } from '../../mappings_state';
+import { useDispatch, useMappingsState } from '../../mappings_state_context';
import { FieldsEditor } from '../../types';
import { canUseMappingsEditor, normalize } from '../../lib';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx
index 01cca7e249a2..0320f2ff51da 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { TextField, UseField, FieldConfig } from '../../../shared_imports';
import { validateUniqueName } from '../../../lib';
import { PARAMETERS_DEFINITION } from '../../../constants';
-import { useMappingsState } from '../../../mappings_state';
+import { useMappingsState } from '../../../mappings_state_context';
export const NameParameter = () => {
const {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx
index 46e70bf8e56b..31ae37c82a43 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx
@@ -70,7 +70,13 @@ export const TypeParameter = ({ isMultiField, isRootLevelField, showDocLink = fa
: filterTypesForNonRootFields(FIELD_TYPES_OPTIONS)
}
selectedOptions={typeField.value}
- onChange={typeField.setValue}
+ onChange={(value) => {
+ if (value.length === 0) {
+ // Don't allow clearing the type. One must always be selected
+ return;
+ }
+ typeField.setValue(value);
+ }}
isClearable={false}
data-test-subj="fieldType"
/>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx
index 57a765c38dd2..dc631b7dbf32 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx
@@ -18,7 +18,7 @@ import {
import { useForm, Form, FormDataProvider } from '../../../../shared_imports';
import { EUI_SIZE } from '../../../../constants';
-import { useDispatch } from '../../../../mappings_state';
+import { useDispatch } from '../../../../mappings_state_context';
import { fieldSerializer } from '../../../../lib';
import { Field, NormalizedFields } from '../../../../types';
import { NameParameter, TypeParameter, SubTypeParameter } from '../../field_parameters';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx
index 80e3e9bec605..2a98b5948e5a 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx
@@ -7,7 +7,7 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { useMappingsState, useDispatch } from '../../../mappings_state';
+import { useMappingsState, useDispatch } from '../../../mappings_state_context';
import { NormalizedField } from '../../../types';
import { getAllDescendantAliases } from '../../../lib';
import { ModalConfirmationDeleteFields } from './modal_confirmation_delete_fields';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx
index e8e41955a5e8..e6950ccfe253 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx
@@ -6,7 +6,6 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
- EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
@@ -25,7 +24,7 @@ import { TYPE_DEFINITION } from '../../../../constants';
import { Field, NormalizedField, NormalizedFields, MainType, SubType } from '../../../../types';
import { CodeBlock } from '../../../code_block';
import { getParametersFormForType } from '../field_types';
-import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider';
+import { UpdateFieldFunc } from './use_update_field';
import { EditFieldHeaderForm } from './edit_field_header_form';
const limitStringLength = (text: string, limit = 18): string => {
@@ -36,19 +35,28 @@ const limitStringLength = (text: string, limit = 18): string => {
return `...${text.substr(limit * -1)}`;
};
-interface Props {
+export interface Props {
form: FormHook;
field: NormalizedField;
allFields: NormalizedFields['byId'];
exitEdit(): void;
+ updateField: UpdateFieldFunc;
}
-export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props) => {
- const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => {
- if (e) {
- e.preventDefault();
- }
+export const defaultFlyoutProps = {
+ 'data-test-subj': 'mappingsEditorFieldEdit',
+ 'aria-labelledby': 'mappingsEditorFieldEditTitle',
+ className: 'mappingsEditor__editField',
+ maxWidth: 720,
+};
+// The default FormWrapper is the , which wrapps the form with
+// a . We can't have a div as first child of the Flyout as it breaks
+// the height calculaction and does not render the footer position correctly.
+const FormWrapper: React.FC = ({ children }) => <>{children}>;
+
+export const EditField = React.memo(({ form, field, allFields, exitEdit, updateField }: Props) => {
+ const submitForm = async () => {
const { isValid, data } = await form.submit();
if (isValid) {
@@ -56,174 +64,152 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props
}
};
- const cancel = () => {
- exitEdit();
- };
-
const { isMultiField } = field;
return (
-
- {(updateField) => (
-
- )}
-
+ );
+ }}
+
+
+
+ {/* Field path */}
+
+
+ {field.path.join(' > ')}
+
+
+
+
+
+
+
+
+ {({ type, subType }) => {
+ const ParametersForm = getParametersFormForType(type, subType);
+
+ if (!ParametersForm) {
+ return null;
+ }
+
+ return (
+
+ );
+ }}
+
+
+
+
+ {form.isSubmitted && !form.isValid && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldCancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', {
+ defaultMessage: 'Update',
+ })}
+
+
+
+
+
);
});
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx
index 5105a2a157a6..4996f59105c0 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx
@@ -3,24 +3,38 @@
* 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, useCallback } from 'react';
+import React, { useEffect, useCallback, useMemo } from 'react';
-import { useForm } from '../../../../shared_imports';
-import { useDispatch } from '../../../../mappings_state';
-import { Field, NormalizedField, NormalizedFields } from '../../../../types';
+import { useForm, GlobalFlyout } from '../../../../shared_imports';
+import { useDispatch, useMappingsState } from '../../../../mappings_state_context';
+import { Field } from '../../../../types';
import { fieldSerializer, fieldDeserializer } from '../../../../lib';
-import { EditField } from './edit_field';
+import { ModalConfirmationDeleteFields } from '../modal_confirmation_delete_fields';
+import { EditField, defaultFlyoutProps, Props as EditFieldProps } from './edit_field';
+import { useUpdateField } from './use_update_field';
-interface Props {
- field: NormalizedField;
- allFields: NormalizedFields['byId'];
-}
+const { useGlobalFlyout } = GlobalFlyout;
-export const EditFieldContainer = React.memo(({ field, allFields }: Props) => {
+export const EditFieldContainer = React.memo(() => {
+ const { fields, documentFields } = useMappingsState();
const dispatch = useDispatch();
+ const {
+ addContent: addContentToGlobalFlyout,
+ removeContent: removeContentFromGlobalFlyout,
+ } = useGlobalFlyout();
+ const { updateField, modal } = useUpdateField();
+
+ const { status, fieldToEdit } = documentFields;
+ const isEditing = status === 'editingField';
+
+ const field = isEditing ? fields.byId[fieldToEdit!] : undefined;
+
+ const formDefaultValue = useMemo(() => {
+ return { ...field?.source };
+ }, [field?.source]);
const { form } = useForm
({
- defaultValue: { ...field.source },
+ defaultValue: formDefaultValue,
serializer: fieldSerializer,
deserializer: fieldDeserializer,
options: { stripEmptyFields: false },
@@ -40,5 +54,48 @@ export const EditFieldContainer = React.memo(({ field, allFields }: Props) => {
dispatch({ type: 'documentField.changeStatus', value: 'idle' });
}, [dispatch]);
- return ;
+ useEffect(() => {
+ if (isEditing) {
+ // Open the flyout with the content
+ addContentToGlobalFlyout({
+ id: 'mappingsEditField',
+ Component: EditField,
+ props: {
+ form,
+ field: field!,
+ exitEdit,
+ allFields: fields.byId,
+ updateField,
+ },
+ flyoutProps: { ...defaultFlyoutProps, onClose: exitEdit },
+ cleanUpFunc: exitEdit,
+ });
+ }
+ }, [
+ isEditing,
+ field,
+ form,
+ addContentToGlobalFlyout,
+ fields.byId,
+ fieldToEdit,
+ exitEdit,
+ updateField,
+ ]);
+
+ useEffect(() => {
+ if (!isEditing) {
+ removeContentFromGlobalFlyout('mappingsEditField');
+ }
+ }, [isEditing, removeContentFromGlobalFlyout]);
+
+ useEffect(() => {
+ return () => {
+ if (isEditing) {
+ // When the component unmounts, exit edit mode.
+ exitEdit();
+ }
+ };
+ }, [isEditing, exitEdit]);
+
+ return modal.isOpen ? : null;
});
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
deleted file mode 100644
index e31d12689e7e..000000000000
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
+++ /dev/null
@@ -1,147 +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, { useState } from 'react';
-import { i18n } from '@kbn/i18n';
-
-import { useMappingsState, useDispatch } from '../../../../mappings_state';
-import { shouldDeleteChildFieldsAfterTypeChange, getAllDescendantAliases } from '../../../../lib';
-import { NormalizedField, DataType } from '../../../../types';
-import { PARAMETERS_DEFINITION } from '../../../../constants';
-import { ModalConfirmationDeleteFields } from '../modal_confirmation_delete_fields';
-
-export type UpdateFieldFunc = (field: NormalizedField) => void;
-
-interface Props {
- children: (saveProperty: UpdateFieldFunc) => React.ReactNode;
-}
-
-interface State {
- isModalOpen: boolean;
- field?: NormalizedField;
- aliases?: string[];
-}
-
-export const UpdateFieldProvider = ({ children }: Props) => {
- const [state, setState] = useState({
- isModalOpen: false,
- });
- const dispatch = useDispatch();
-
- const { fields } = useMappingsState();
- const { byId, aliases } = fields;
-
- const confirmButtonText = i18n.translate(
- 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.confirmDescription',
- {
- defaultMessage: 'Confirm type change',
- }
- );
-
- let modalTitle: string | undefined;
-
- if (state.field) {
- const { source } = state.field;
-
- modalTitle = i18n.translate(
- 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.title',
- {
- defaultMessage: "Confirm change '{fieldName}' type to '{fieldType}'.",
- values: {
- fieldName: source.name,
- fieldType: source.type,
- },
- }
- );
- }
-
- const closeModal = () => {
- setState({ isModalOpen: false });
- };
-
- const updateField: UpdateFieldFunc = (field) => {
- const previousField = byId[field.id];
-
- const willDeleteChildFields = (oldType: DataType, newType: DataType): boolean => {
- const { hasChildFields, hasMultiFields } = field;
-
- if (!hasChildFields && !hasMultiFields) {
- // No child or multi-fields will be deleted, no confirmation needed.
- return false;
- }
-
- return shouldDeleteChildFieldsAfterTypeChange(oldType, newType);
- };
-
- if (field.source.type !== previousField.source.type) {
- // Array of all the aliases pointing to the current field beeing updated
- const aliasesOnField = aliases[field.id] || [];
-
- // Array of all the aliases pointing to the current field + all its possible children
- const aliasesOnFieldAndDescendants = getAllDescendantAliases(field, fields);
-
- const isReferencedByAlias = aliasesOnField && Boolean(aliasesOnField.length);
- const nextTypeCanHaveAlias = !PARAMETERS_DEFINITION.path.targetTypesNotAllowed.includes(
- field.source.type
- );
-
- // We need to check if, by changing the type, we will also
- // delete possible child properties ("fields" or "properties").
- // If we will, we need to warn the user about it.
- let requiresConfirmation: boolean;
- let aliasesToDelete: string[] = [];
-
- if (isReferencedByAlias && !nextTypeCanHaveAlias) {
- aliasesToDelete = aliasesOnFieldAndDescendants;
- requiresConfirmation = true;
- } else {
- requiresConfirmation = willDeleteChildFields(previousField.source.type, field.source.type);
- if (requiresConfirmation) {
- aliasesToDelete = aliasesOnFieldAndDescendants.filter(
- // We will only delete aliases that points to possible children, *NOT* the field itself
- (id) => aliasesOnField.includes(id) === false
- );
- }
- }
-
- if (requiresConfirmation) {
- setState({
- isModalOpen: true,
- field,
- aliases: Boolean(aliasesToDelete.length)
- ? aliasesToDelete.map((id) => byId[id].path.join(' > ')).sort()
- : undefined,
- });
- return;
- }
- }
-
- dispatch({ type: 'field.edit', value: field.source });
- };
-
- const confirmTypeUpdate = () => {
- dispatch({ type: 'field.edit', value: state.field!.source });
- closeModal();
- };
-
- return (
- <>
- {children(updateField)}
-
- {state.isModalOpen && (
-
- )}
- >
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts
new file mode 100644
index 000000000000..ed659cd05b06
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/use_update_field.ts
@@ -0,0 +1,146 @@
+/*
+ * 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 { useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { useMappingsState, useDispatch } from '../../../../mappings_state_context';
+import { shouldDeleteChildFieldsAfterTypeChange, getAllDescendantAliases } from '../../../../lib';
+import { NormalizedField, DataType } from '../../../../types';
+import { PARAMETERS_DEFINITION } from '../../../../constants';
+
+export type UpdateFieldFunc = (field: NormalizedField) => void;
+
+interface State {
+ isModalOpen: boolean;
+ field?: NormalizedField;
+ aliases?: string[];
+}
+
+export const useUpdateField = () => {
+ const [state, setState] = useState({
+ isModalOpen: false,
+ });
+ const dispatch = useDispatch();
+
+ const { fields } = useMappingsState();
+ const { byId, aliases } = fields;
+
+ const confirmButtonText = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.confirmDescription',
+ {
+ defaultMessage: 'Confirm type change',
+ }
+ );
+
+ let modalTitle = '';
+
+ if (state.field) {
+ const { source } = state.field;
+
+ modalTitle = i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.updateField.confirmationModal.title',
+ {
+ defaultMessage: "Confirm change '{fieldName}' type to '{fieldType}'.",
+ values: {
+ fieldName: source.name,
+ fieldType: source.type,
+ },
+ }
+ );
+ }
+
+ const closeModal = () => {
+ setState({ isModalOpen: false });
+ };
+
+ const updateField: UpdateFieldFunc = useCallback(
+ (field) => {
+ const previousField = byId[field.id];
+
+ const willDeleteChildFields = (oldType: DataType, newType: DataType): boolean => {
+ const { hasChildFields, hasMultiFields } = field;
+
+ if (!hasChildFields && !hasMultiFields) {
+ // No child or multi-fields will be deleted, no confirmation needed.
+ return false;
+ }
+
+ return shouldDeleteChildFieldsAfterTypeChange(oldType, newType);
+ };
+
+ if (field.source.type !== previousField.source.type) {
+ // Array of all the aliases pointing to the current field beeing updated
+ const aliasesOnField = aliases[field.id] || [];
+
+ // Array of all the aliases pointing to the current field + all its possible children
+ const aliasesOnFieldAndDescendants = getAllDescendantAliases(field, fields);
+
+ const isReferencedByAlias = aliasesOnField && Boolean(aliasesOnField.length);
+ const nextTypeCanHaveAlias = !PARAMETERS_DEFINITION.path.targetTypesNotAllowed.includes(
+ field.source.type
+ );
+
+ // We need to check if, by changing the type, we will also
+ // delete possible child properties ("fields" or "properties").
+ // If we will, we need to warn the user about it.
+ let requiresConfirmation: boolean;
+ let aliasesToDelete: string[] = [];
+
+ if (isReferencedByAlias && !nextTypeCanHaveAlias) {
+ aliasesToDelete = aliasesOnFieldAndDescendants;
+ requiresConfirmation = true;
+ } else {
+ requiresConfirmation = willDeleteChildFields(
+ previousField.source.type,
+ field.source.type
+ );
+ if (requiresConfirmation) {
+ aliasesToDelete = aliasesOnFieldAndDescendants.filter(
+ // We will only delete aliases that points to possible children, *NOT* the field itself
+ (id) => aliasesOnField.includes(id) === false
+ );
+ }
+ }
+
+ if (requiresConfirmation) {
+ setState({
+ isModalOpen: true,
+ field,
+ aliases: Boolean(aliasesToDelete.length)
+ ? aliasesToDelete.map((id) => byId[id].path.join(' > ')).sort()
+ : undefined,
+ });
+ return;
+ }
+ }
+
+ dispatch({ type: 'field.edit', value: field.source });
+ },
+ [dispatch, aliases, fields, byId]
+ );
+
+ const confirmTypeUpdate = () => {
+ dispatch({ type: 'field.edit', value: state.field!.source });
+ closeModal();
+ };
+
+ return {
+ updateField,
+ modal: {
+ isOpen: state.isModalOpen,
+ props: {
+ childFields: state.field && state.field.childFields,
+ title: modalTitle,
+ aliases: state.aliases,
+ byId,
+ confirmButtonText,
+ onConfirm: confirmTypeUpdate,
+ onCancel: closeModal,
+ },
+ },
+ };
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
index 55093e606cfa..7d9ad3bc6aae 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
@@ -5,7 +5,7 @@
*/
import React, { useMemo, useCallback, useRef } from 'react';
-import { useMappingsState, useDispatch } from '../../../mappings_state';
+import { useMappingsState, useDispatch } from '../../../mappings_state_context';
import { NormalizedField } from '../../../types';
import { FieldsListItem } from './fields_list_item';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx
index 5954f6f285f1..d750c0e604c5 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx
@@ -6,7 +6,7 @@
import React, { useRef, useCallback } from 'react';
-import { useDispatch } from '../../mappings_state';
+import { useDispatch } from '../../mappings_state_context';
import { JsonEditor } from '../../shared_imports';
export interface Props {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx
index 9d9df38ef4e2..7a0b72ae647d 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx
@@ -8,7 +8,7 @@ import React, { useMemo, useCallback } from 'react';
import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { useMappingsState, useDispatch } from '../../mappings_state';
+import { useMappingsState, useDispatch } from '../../mappings_state_context';
import { FieldsList, CreateField } from './fields';
export const DocumentFieldsTreeEditor = () => {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx
index 9077781b7fb4..f3602a800eee 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx
@@ -8,9 +8,8 @@ import VirtualList from 'react-tiny-virtual-list';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { SearchResult as SearchResultType } from '../../../types';
-import { useDispatch } from '../../../mappings_state';
-import { State } from '../../../reducer';
+import { SearchResult as SearchResultType, State } from '../../../types';
+import { useDispatch } from '../../../mappings_state_context';
import { SearchResultItem } from './search_result_item';
interface Props {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx
index ab8b90b6be3b..73d3e078f6ff 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx
@@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { SearchResult } from '../../../types';
import { TYPE_DEFINITION } from '../../../constants';
-import { useDispatch } from '../../../mappings_state';
+import { useDispatch } from '../../../mappings_state_context';
import { getTypeLabelFromType } from '../../../lib';
import { DeleteFieldProvider } from '../fields/delete_field_provider';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts
index 34c410f06e52..dc7f20f4d026 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts
@@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './load_from_json_button';
-export * from './load_mappings_provider';
+export { LoadMappingsFromJsonButton } from './load_from_json_button';
+export { LoadMappingsProvider } from './load_mappings_provider';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx
index a95579a8a141..44a809a7a01b 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx
@@ -9,12 +9,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui';
import { useForm, Form, SerializerFunc, UseField, JsonEditorField } from '../../shared_imports';
-import { Types, useDispatch } from '../../mappings_state';
+import { MappingsTemplates } from '../../types';
+import { useDispatch } from '../../mappings_state_context';
import { templatesFormSchema } from './templates_form_schema';
import { documentationService } from '../../../../services/documentation';
-type MappingsTemplates = Types['MappingsTemplates'];
-
interface Props {
value?: MappingsTemplates;
}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts
index 667b5685723d..daca85f95b0b 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { FormSchema, fieldValidators } from '../../shared_imports';
-import { MappingsTemplates } from '../../reducer';
+import { MappingsTemplates } from '../../types';
const { isJsonField } = fieldValidators;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts
index 29cfaf99c655..00bb41663dd9 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './mappings_editor';
+export { MappingsEditor } from './mappings_editor';
// We export both the button & the load mappings provider
// to give flexibility to the consumer
-export * from './components/load_mappings';
+export { LoadMappingsFromJsonButton, LoadMappingsProvider } from './components/load_mappings';
-export { OnUpdateHandler, Types } from './mappings_state';
+export { MappingsEditorProvider } from './mappings_editor_context';
-export { IndexSettings } from './types';
+export { IndexSettings, OnUpdateHandler } from './types';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx
index 9e3637f97029..411193f10b24 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { createContext, useContext } from 'react';
+
import { IndexSettings } from './types';
const IndexSettingsContext = createContext(undefined);
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx
index e8fda9073770..292882f1c5b4 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx
@@ -14,24 +14,40 @@ import {
TemplatesForm,
MultipleMappingsWarning,
} from './components';
-import { IndexSettings } from './types';
+import {
+ OnUpdateHandler,
+ IndexSettings,
+ Field,
+ Mappings,
+ MappingsConfiguration,
+ MappingsTemplates,
+} from './types';
import { extractMappingsDefinition } from './lib';
-import { State } from './reducer';
-import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state';
+import { useMappingsState } from './mappings_state_context';
+import { useMappingsStateListener } from './use_state_listener';
import { IndexSettingsProvider } from './index_settings_context';
+type TabName = 'fields' | 'advanced' | 'templates';
+
+interface MappingsEditorParsedMetadata {
+ parsedDefaultValue?: {
+ configuration: MappingsConfiguration;
+ fields: { [key: string]: Field };
+ templates: MappingsTemplates;
+ };
+ multipleMappingsDeclared: boolean;
+}
+
interface Props {
- onChange: MappingsStateProps['onChange'];
+ onChange: OnUpdateHandler;
value?: { [key: string]: any };
indexSettings?: IndexSettings;
}
-type TabName = 'fields' | 'advanced' | 'templates';
-
export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Props) => {
- const [selectedTab, selectTab] = useState('fields');
-
- const { parsedDefaultValue, multipleMappingsDeclared } = useMemo(() => {
+ const { parsedDefaultValue, multipleMappingsDeclared } = useMemo<
+ MappingsEditorParsedMetadata
+ >(() => {
const mappingsDefinition = extractMappingsDefinition(value);
if (mappingsDefinition === null) {
@@ -69,18 +85,28 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr
return { parsedDefaultValue: parsed, multipleMappingsDeclared: false };
}, [value]);
+ /**
+ * Hook that will listen to:
+ * 1. "value" prop changes in order to reset the mappings editor
+ * 2. "state" changes in order to communicate any updates to the consumer
+ */
+ useMappingsStateListener({ onChange, value: parsedDefaultValue });
+
+ const state = useMappingsState();
+ const [selectedTab, selectTab] = useState('fields');
+
useEffect(() => {
if (multipleMappingsDeclared) {
// We set the data getter here as the user won't be able to make any changes
onChange({
- getData: () => value! as Types['Mappings'],
+ getData: () => value! as Mappings,
validate: () => Promise.resolve(true),
isValid: true,
});
}
}, [multipleMappingsDeclared, onChange, value]);
- const changeTab = async (tab: TabName, state: State) => {
+ const changeTab = async (tab: TabName) => {
if (selectedTab === 'advanced') {
// When we navigate away we need to submit the form to validate if there are any errors.
const { isValid: isConfigurationFormValid } = await state.configuration.submitForm!();
@@ -102,59 +128,53 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr
selectTab(tab);
};
+ const tabToContentMap = {
+ fields: ,
+ templates: ,
+ advanced: ,
+ };
+
return (
{multipleMappingsDeclared ? (
) : (
-
- {({ state }) => {
- const tabToContentMap = {
- fields: ,
- templates: ,
- advanced: ,
- };
+
+
+ changeTab('fields')}
+ isSelected={selectedTab === 'fields'}
+ data-test-subj="formTab"
+ >
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
+ defaultMessage: 'Mapped fields',
+ })}
+
+ changeTab('templates')}
+ isSelected={selectedTab === 'templates'}
+ data-test-subj="formTab"
+ >
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
+ defaultMessage: 'Dynamic templates',
+ })}
+
+ changeTab('advanced')}
+ isSelected={selectedTab === 'advanced'}
+ data-test-subj="formTab"
+ >
+ {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
+ defaultMessage: 'Advanced options',
+ })}
+
+
- return (
-
-
- changeTab('fields', state)}
- isSelected={selectedTab === 'fields'}
- data-test-subj="formTab"
- >
- {i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
- defaultMessage: 'Mapped fields',
- })}
-
- changeTab('templates', state)}
- isSelected={selectedTab === 'templates'}
- data-test-subj="formTab"
- >
- {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
- defaultMessage: 'Dynamic templates',
- })}
-
- changeTab('advanced', state)}
- isSelected={selectedTab === 'advanced'}
- data-test-subj="formTab"
- >
- {i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
- defaultMessage: 'Advanced options',
- })}
-
-
+
-
-
- {tabToContentMap[selectedTab]}
-
- );
- }}
-
+ {tabToContentMap[selectedTab]}
+
)}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx
new file mode 100644
index 000000000000..596b49cc89ee
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor_context.tsx
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { StateProvider } from './mappings_state_context';
+
+export const MappingsEditorProvider: React.FC = ({ children }) => {
+ return {children};
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx
new file mode 100644
index 000000000000..a402dec25005
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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, { useReducer, createContext, useContext } from 'react';
+
+import { reducer } from './reducer';
+import { State, Dispatch } from './types';
+
+const StateContext = createContext(undefined);
+const DispatchContext = createContext(undefined);
+
+export const StateProvider: React.FC = ({ children }) => {
+ const initialState: State = {
+ isValid: true,
+ configuration: {
+ defaultValue: {},
+ data: {
+ raw: {},
+ format: () => ({}),
+ },
+ validate: () => Promise.resolve(true),
+ },
+ templates: {
+ defaultValue: {},
+ data: {
+ raw: {},
+ format: () => ({}),
+ },
+ validate: () => Promise.resolve(true),
+ },
+ fields: {
+ byId: {},
+ rootLevelFields: [],
+ aliases: {},
+ maxNestedDepth: 0,
+ },
+ documentFields: {
+ status: 'idle',
+ editor: 'default',
+ },
+ fieldsJsonEditor: {
+ format: () => ({}),
+ isValid: true,
+ },
+ search: {
+ term: '',
+ result: [],
+ },
+ };
+
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useMappingsState = () => {
+ const ctx = useContext(StateContext);
+ if (ctx === undefined) {
+ throw new Error('useMappingsState must be used within a ');
+ }
+ return ctx;
+};
+
+export const useDispatch = () => {
+ const ctx = useContext(DispatchContext);
+ if (ctx === undefined) {
+ throw new Error('useDispatch must be used within a ');
+ }
+ return ctx;
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
index 27f8b1249300..18a8270117ea 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
@@ -3,8 +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 { OnFormUpdateArg, FormHook } from './shared_imports';
-import { Field, NormalizedFields, NormalizedField, FieldsEditor, SearchResult } from './types';
+import { Field, NormalizedFields, NormalizedField, State, Action } from './types';
import {
getFieldMeta,
getUniqueId,
@@ -17,99 +16,6 @@ import {
} from './lib';
import { PARAMETERS_DEFINITION } from './constants';
-export interface MappingsConfiguration {
- enabled?: boolean;
- throwErrorsForUnmappedFields?: boolean;
- date_detection: boolean;
- numeric_detection: boolean;
- dynamic_date_formats: string[];
- _source: {
- enabled?: boolean;
- includes?: string[];
- excludes?: string[];
- };
- _meta?: string;
-}
-
-export interface MappingsTemplates {
- dynamic_templates: DynamicTemplate[];
-}
-
-interface DynamicTemplate {
- [key: string]: {
- mapping: {
- [key: string]: any;
- };
- match_mapping_type?: string;
- match?: string;
- unmatch?: string;
- match_pattern?: string;
- path_match?: string;
- path_unmatch?: string;
- };
-}
-
-export interface MappingsFields {
- [key: string]: any;
-}
-
-type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField';
-
-interface DocumentFieldsState {
- status: DocumentFieldsStatus;
- editor: FieldsEditor;
- fieldToEdit?: string;
- fieldToAddFieldTo?: string;
-}
-
-interface ConfigurationFormState extends OnFormUpdateArg {
- defaultValue: MappingsConfiguration;
- submitForm?: FormHook['submit'];
-}
-
-interface TemplatesFormState extends OnFormUpdateArg {
- defaultValue: MappingsTemplates;
- submitForm?: FormHook['submit'];
-}
-
-export interface State {
- isValid: boolean | undefined;
- configuration: ConfigurationFormState;
- documentFields: DocumentFieldsState;
- fields: NormalizedFields;
- fieldForm?: OnFormUpdateArg;
- fieldsJsonEditor: {
- format(): MappingsFields;
- isValid: boolean;
- };
- search: {
- term: string;
- result: SearchResult[];
- };
- templates: TemplatesFormState;
-}
-
-export type Action =
- | { type: 'editor.replaceMappings'; value: { [key: string]: any } }
- | { type: 'configuration.update'; value: Partial }
- | { type: 'configuration.save'; value: MappingsConfiguration }
- | { type: 'templates.update'; value: Partial }
- | { type: 'templates.save'; value: MappingsTemplates }
- | { type: 'fieldForm.update'; value: OnFormUpdateArg }
- | { type: 'field.add'; value: Field }
- | { type: 'field.remove'; value: string }
- | { type: 'field.edit'; value: Field }
- | { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } }
- | { type: 'documentField.createField'; value?: string }
- | { type: 'documentField.editField'; value: string }
- | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }
- | { type: 'documentField.changeEditor'; value: FieldsEditor }
- | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } }
- | { type: 'search:update'; value: string }
- | { type: 'validity:update'; value: boolean };
-
-export type Dispatch = (action: Action) => void;
-
export const addFieldToState = (field: Field, state: State): State => {
const updatedFields = { ...state.fields };
const id = getUniqueId();
@@ -277,7 +183,7 @@ export const reducer = (state: State, action: Action): State => {
},
documentFields: {
...state.documentFields,
- status: 'idle',
+ ...action.value.documentFields,
fieldToAddFieldTo: undefined,
fieldToEdit: undefined,
},
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts
index 2979015c0745..097d03952795 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts
@@ -49,4 +49,5 @@ export {
export {
JsonEditor,
OnJsonEditorUpdateHandler,
+ GlobalFlyout,
} from '../../../../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
similarity index 65%
rename from x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts
rename to x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
index 5b18af68ed55..a9f6d2ea03bd 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
@@ -3,10 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { ReactNode, OptionHTMLAttributes } from 'react';
+import { ReactNode } from 'react';
-import { FieldConfig } from './shared_imports';
-import { PARAMETERS_DEFINITION } from './constants';
+import { GenericObject } from './mappings_editor';
+
+import { FieldConfig } from '../shared_imports';
+import { PARAMETERS_DEFINITION } from '../constants';
export interface DataTypeDefinition {
label: string;
@@ -203,100 +205,7 @@ export interface NormalizedField extends FieldMeta {
export type ChildFieldName = 'properties' | 'fields';
-export type FieldsEditor = 'default' | 'json';
-
-export type SelectOption = {
- value: unknown;
- text: T | ReactNode;
-} & OptionHTMLAttributes;
-
-export interface SuperSelectOption {
- value: unknown;
- inputDisplay?: ReactNode;
- dropdownDisplay?: ReactNode;
- disabled?: boolean;
- 'data-test-subj'?: string;
-}
-
export interface AliasOption {
id: string;
label: string;
}
-
-export interface IndexSettingsInterface {
- analysis?: {
- analyzer: {
- [key: string]: {
- type: string;
- tokenizer: string;
- char_filter?: string[];
- filter?: string[];
- position_increment_gap?: number;
- };
- };
- };
-}
-
-/**
- * When we define the index settings we can skip
- * the "index" property and directly add the "analysis".
- * ES always returns the settings wrapped under "index".
- */
-export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface };
-
-export interface ComboBoxOption {
- label: string;
- value?: unknown;
-}
-
-export interface SearchResult {
- display: JSX.Element;
- field: NormalizedField;
-}
-
-export interface SearchMetadata {
- /**
- * Whether or not the search term match some part of the field path.
- */
- matchPath: boolean;
- /**
- * If the search term matches the field type we will give it a higher score.
- */
- matchType: boolean;
- /**
- * If the last word of the search terms matches the field name
- */
- matchFieldName: boolean;
- /**
- * If the search term matches the beginning of the path we will give it a higher score
- */
- matchStartOfPath: boolean;
- /**
- * If the last word of the search terms fully matches the field name
- */
- fullyMatchFieldName: boolean;
- /**
- * If the search term exactly matches the field type
- */
- fullyMatchType: boolean;
- /**
- * If the search term matches the full field path
- */
- fullyMatchPath: boolean;
- /**
- * The score of the result that will allow us to sort the list
- */
- score: number;
- /**
- * The JSX with tag wrapping the matched string
- */
- display: JSX.Element;
- /**
- * The field path substring that matches the search
- */
- stringMatch: string | null;
-}
-
-export interface GenericObject {
- [key: string]: any;
-}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/index.ts
new file mode 100644
index 000000000000..cce2d550a68c
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 * from './mappings_editor';
+
+export * from './document_fields';
+
+export * from './state';
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts
new file mode 100644
index 000000000000..1ca944024ae2
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/mappings_editor.ts
@@ -0,0 +1,110 @@
+/*
+ * 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 { ReactNode, OptionHTMLAttributes } from 'react';
+
+import { NormalizedField } from './document_fields';
+import { Mappings } from './state';
+
+export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void;
+
+export interface OnUpdateHandlerArg {
+ isValid?: boolean;
+ getData: () => Mappings | undefined;
+ validate: () => Promise;
+}
+
+export type FieldsEditor = 'default' | 'json';
+
+export interface IndexSettingsInterface {
+ analysis?: {
+ analyzer: {
+ [key: string]: {
+ type: string;
+ tokenizer: string;
+ char_filter?: string[];
+ filter?: string[];
+ position_increment_gap?: number;
+ };
+ };
+ };
+}
+
+/**
+ * When we define the index settings we can skip
+ * the "index" property and directly add the "analysis".
+ * ES always returns the settings wrapped under "index".
+ */
+export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface };
+
+export type SelectOption = {
+ value: unknown;
+ text: T | ReactNode;
+} & OptionHTMLAttributes;
+
+export interface ComboBoxOption {
+ label: string;
+ value?: unknown;
+}
+
+export interface SuperSelectOption {
+ value: unknown;
+ inputDisplay?: ReactNode;
+ dropdownDisplay?: ReactNode;
+ disabled?: boolean;
+ 'data-test-subj'?: string;
+}
+
+export interface SearchResult {
+ display: JSX.Element;
+ field: NormalizedField;
+}
+
+export interface SearchMetadata {
+ /**
+ * Whether or not the search term match some part of the field path.
+ */
+ matchPath: boolean;
+ /**
+ * If the search term matches the field type we will give it a higher score.
+ */
+ matchType: boolean;
+ /**
+ * If the last word of the search terms matches the field name
+ */
+ matchFieldName: boolean;
+ /**
+ * If the search term matches the beginning of the path we will give it a higher score
+ */
+ matchStartOfPath: boolean;
+ /**
+ * If the last word of the search terms fully matches the field name
+ */
+ fullyMatchFieldName: boolean;
+ /**
+ * If the search term exactly matches the field type
+ */
+ fullyMatchType: boolean;
+ /**
+ * If the search term matches the full field path
+ */
+ fullyMatchPath: boolean;
+ /**
+ * The score of the result that will allow us to sort the list
+ */
+ score: number;
+ /**
+ * The JSX with tag wrapping the matched string
+ */
+ display: JSX.Element;
+ /**
+ * The field path substring that matches the search
+ */
+ stringMatch: string | null;
+}
+
+export interface GenericObject {
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts
new file mode 100644
index 000000000000..34df70374aa8
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 { FormHook, OnFormUpdateArg } from '../shared_imports';
+import { Field, NormalizedFields } from './document_fields';
+import { FieldsEditor, SearchResult } from './mappings_editor';
+
+export type Mappings = MappingsTemplates &
+ MappingsConfiguration & {
+ properties?: MappingsFields;
+ };
+
+export interface MappingsConfiguration {
+ enabled?: boolean;
+ throwErrorsForUnmappedFields?: boolean;
+ date_detection?: boolean;
+ numeric_detection?: boolean;
+ dynamic_date_formats?: string[];
+ _source?: {
+ enabled?: boolean;
+ includes?: string[];
+ excludes?: string[];
+ };
+ _meta?: string;
+}
+
+export interface MappingsTemplates {
+ dynamic_templates?: DynamicTemplate[];
+}
+
+export interface DynamicTemplate {
+ [key: string]: {
+ mapping: {
+ [key: string]: any;
+ };
+ match_mapping_type?: string;
+ match?: string;
+ unmatch?: string;
+ match_pattern?: string;
+ path_match?: string;
+ path_unmatch?: string;
+ };
+}
+
+export interface MappingsFields {
+ [key: string]: any;
+}
+
+export type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField';
+
+export interface DocumentFieldsState {
+ status: DocumentFieldsStatus;
+ editor: FieldsEditor;
+ fieldToEdit?: string;
+ fieldToAddFieldTo?: string;
+}
+
+export interface ConfigurationFormState extends OnFormUpdateArg {
+ defaultValue: MappingsConfiguration;
+ submitForm?: FormHook['submit'];
+}
+
+interface TemplatesFormState extends OnFormUpdateArg {
+ defaultValue: MappingsTemplates;
+ submitForm?: FormHook['submit'];
+}
+
+export interface State {
+ isValid: boolean | undefined;
+ configuration: ConfigurationFormState;
+ documentFields: DocumentFieldsState;
+ fields: NormalizedFields;
+ fieldForm?: OnFormUpdateArg;
+ fieldsJsonEditor: {
+ format(): MappingsFields;
+ isValid: boolean;
+ };
+ search: {
+ term: string;
+ result: SearchResult[];
+ };
+ templates: TemplatesFormState;
+}
+
+export type Action =
+ | { type: 'editor.replaceMappings'; value: { [key: string]: any } }
+ | { type: 'configuration.update'; value: Partial }
+ | { type: 'configuration.save'; value: MappingsConfiguration }
+ | { type: 'templates.update'; value: Partial }
+ | { type: 'templates.save'; value: MappingsTemplates }
+ | { type: 'fieldForm.update'; value: OnFormUpdateArg }
+ | { type: 'field.add'; value: Field }
+ | { type: 'field.remove'; value: string }
+ | { type: 'field.edit'; value: Field }
+ | { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } }
+ | { type: 'documentField.createField'; value?: string }
+ | { type: 'documentField.editField'; value: string }
+ | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }
+ | { type: 'documentField.changeEditor'; value: FieldsEditor }
+ | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } }
+ | { type: 'search:update'; value: string }
+ | { type: 'validity:update'; value: boolean };
+
+export type Dispatch = (action: Action) => void;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx
similarity index 53%
rename from x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
rename to x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx
index ad5056fa73ce..f1ffd5356c97 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx
@@ -3,92 +3,32 @@
* 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, { useReducer, useEffect, createContext, useContext, useMemo, useRef } from 'react';
+import { useEffect, useMemo } from 'react';
import {
- reducer,
+ Field,
+ Mappings,
MappingsConfiguration,
- MappingsFields,
MappingsTemplates,
- State,
- Dispatch,
-} from './reducer';
-import { Field } from './types';
+ OnUpdateHandler,
+} from './types';
import { normalize, deNormalize, stripUndefinedValues } from './lib';
+import { useMappingsState, useDispatch } from './mappings_state_context';
-type Mappings = MappingsTemplates &
- MappingsConfiguration & {
- properties?: MappingsFields;
- };
-
-export interface Types {
- Mappings: Mappings;
- MappingsConfiguration: MappingsConfiguration;
- MappingsFields: MappingsFields;
- MappingsTemplates: MappingsTemplates;
-}
-
-export interface OnUpdateHandlerArg {
- isValid?: boolean;
- getData: () => Mappings | undefined;
- validate: () => Promise;
-}
-
-export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void;
-
-const StateContext = createContext(undefined);
-const DispatchContext = createContext(undefined);
-
-export interface Props {
- children: (params: { state: State }) => React.ReactNode;
- value: {
+interface Args {
+ onChange: OnUpdateHandler;
+ value?: {
templates: MappingsTemplates;
configuration: MappingsConfiguration;
fields: { [key: string]: Field };
};
- onChange: OnUpdateHandler;
}
-export const MappingsState = React.memo(({ children, onChange, value }: Props) => {
- const didMountRef = useRef(false);
+export const useMappingsStateListener = ({ onChange, value }: Args) => {
+ const state = useMappingsState();
+ const dispatch = useDispatch();
- const parsedFieldsDefaultValue = useMemo(() => normalize(value.fields), [value.fields]);
-
- const initialState: State = {
- isValid: true,
- configuration: {
- defaultValue: value.configuration,
- data: {
- raw: value.configuration,
- format: () => value.configuration,
- },
- validate: () => Promise.resolve(true),
- },
- templates: {
- defaultValue: value.templates,
- data: {
- raw: value.templates,
- format: () => value.templates,
- },
- validate: () => Promise.resolve(true),
- },
- fields: parsedFieldsDefaultValue,
- documentFields: {
- status: parsedFieldsDefaultValue.rootLevelFields.length === 0 ? 'creatingField' : 'idle',
- editor: 'default',
- },
- fieldsJsonEditor: {
- format: () => ({}),
- isValid: true,
- },
- search: {
- term: '',
- result: [],
- },
- };
-
- const [state, dispatch] = useReducer(reducer, initialState);
+ const parsedFieldsDefaultValue = useMemo(() => normalize(value?.fields), [value?.fields]);
useEffect(() => {
// If we are creating a new field, but haven't entered any name
@@ -158,46 +98,28 @@ export const MappingsState = React.memo(({ children, onChange, value }: Props) =
},
isValid: state.isValid,
});
- }, [state, onChange]);
+ }, [state, onChange, dispatch]);
useEffect(() => {
/**
* If the value has changed that probably means that we have loaded
* new data from JSON. We need to update our state with the new mappings.
*/
- if (didMountRef.current) {
- dispatch({
- type: 'editor.replaceMappings',
- value: {
- configuration: value.configuration,
- templates: value.templates,
- fields: parsedFieldsDefaultValue,
- },
- });
- } else {
- didMountRef.current = true;
+ if (value === undefined) {
+ return;
}
- }, [value, parsedFieldsDefaultValue]);
- return (
-
- {children({ state })}
-
- );
-});
-
-export const useMappingsState = () => {
- const ctx = useContext(StateContext);
- if (ctx === undefined) {
- throw new Error('useMappingsState must be used within a ');
- }
- return ctx;
-};
-
-export const useDispatch = () => {
- const ctx = useContext(DispatchContext);
- if (ctx === undefined) {
- throw new Error('useDispatch must be used within a ');
- }
- return ctx;
+ dispatch({
+ type: 'editor.replaceMappings',
+ value: {
+ configuration: value.configuration,
+ templates: value.templates,
+ fields: parsedFieldsDefaultValue,
+ documentFields: {
+ status: parsedFieldsDefaultValue.rootLevelFields.length === 0 ? 'creatingField' : 'idle',
+ editor: 'default',
+ },
+ },
+ });
+ }, [value, parsedFieldsDefaultValue, dispatch]);
};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx
index df0cc791384f..ae831f4acf7e 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx
@@ -39,7 +39,7 @@ const i18nTexts = {
),
};
-export const StepComponents = ({ defaultValue = [], onChange, esDocsBase }: Props) => {
+export const StepComponents = ({ defaultValue, onChange, esDocsBase }: Props) => {
const [state, setState] = useState<{
isLoadingComponents: boolean;
components: ComponentTemplateListItem[];
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
index f3d05ac38108..fcc9795617eb 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.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, { useEffect } from 'react';
+import React, { useEffect, useCallback } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -153,25 +153,18 @@ export const StepLogistics: React.FunctionComponent = React.memo(
serializer: formSerializer,
deserializer: formDeserializer,
});
+ const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form;
/**
* When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state
* and we can display the form errors on top of the forms if there are any.
*/
- const validate = async () => {
- return (await form.submit()).isValid;
- };
+ const validate = useCallback(async () => {
+ return (await submit()).isValid;
+ }, [submit]);
useEffect(() => {
- onChange({
- isValid: form.isValid,
- validate,
- getData: form.getFormData,
- });
- }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps
-
- useEffect(() => {
- const subscription = form.subscribe(({ data, isValid }) => {
+ const subscription = subscribe(({ data, isValid }) => {
onChange({
isValid,
validate,
@@ -179,7 +172,7 @@ export const StepLogistics: React.FunctionComponent = React.memo(
});
});
return subscription.unsubscribe;
- }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [onChange, validate, subscribe]);
const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta(
documentationService.getEsDocsBase()
@@ -204,7 +197,7 @@ export const StepLogistics: React.FunctionComponent = React.memo(
@@ -220,8 +213,8 @@ export const StepLogistics: React.FunctionComponent = React.memo(