[Composable template] Preview composite template (#72598)
Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
c2ad4bf048
commit
1329b683de
|
@ -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: <P extends object = { [key: string]: any }>(content: Content<P>) => void;
|
||||
removeContent: (contentId: string) => void;
|
||||
closeFlyout: () => void;
|
||||
}
|
||||
|
||||
interface Content<P extends object = { [key: string]: any }> {
|
||||
id: string;
|
||||
Component: React.FunctionComponent<P>;
|
||||
props?: P;
|
||||
flyoutProps?: { [key: string]: any };
|
||||
cleanUpFunc?: () => void;
|
||||
}
|
||||
|
||||
const FlyoutMultiContentContext = createContext<Context | undefined>(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<Content<any> | 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 (
|
||||
<FlyoutMultiContentContext.Provider value={context}>
|
||||
<>
|
||||
{children}
|
||||
{ContentFlyout && (
|
||||
<EuiFlyout {...mergedFlyoutProps}>
|
||||
<ContentFlyout {...props} />
|
||||
</EuiFlyout>
|
||||
)}
|
||||
</>
|
||||
</FlyoutMultiContentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useGlobalFlyout = () => {
|
||||
const ctx = useContext(FlyoutMultiContentContext);
|
||||
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useGlobalFlyout must be used within a <GlobalFlyoutProvider />');
|
||||
}
|
||||
|
||||
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<Set<string> | 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 };
|
||||
};
|
|
@ -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';
|
|
@ -27,13 +27,14 @@ import {
|
|||
} from './form_wizard_context';
|
||||
import { FormWizardNav, NavTexts } from './form_wizard_nav';
|
||||
|
||||
interface Props<T extends object> extends ProviderProps<T> {
|
||||
interface Props<T extends object, S extends string> extends ProviderProps<T> {
|
||||
isSaving?: boolean;
|
||||
apiError: JSX.Element | null;
|
||||
texts?: Partial<NavTexts>;
|
||||
rightContentNav?: JSX.Element | null | ((stepId: S) => JSX.Element | null);
|
||||
}
|
||||
|
||||
export function FormWizard<T extends object = { [key: string]: any }>({
|
||||
export function FormWizard<T extends object = { [key: string]: any }, S extends string = any>({
|
||||
texts,
|
||||
defaultActiveStep,
|
||||
defaultValue,
|
||||
|
@ -43,7 +44,8 @@ export function FormWizard<T extends object = { [key: string]: any }>({
|
|||
onSave,
|
||||
onChange,
|
||||
children,
|
||||
}: Props<T>) {
|
||||
rightContentNav,
|
||||
}: Props<T, S>) {
|
||||
return (
|
||||
<FormWizardProvider<T>
|
||||
defaultValue={defaultValue}
|
||||
|
@ -53,7 +55,14 @@ export function FormWizard<T extends object = { [key: string]: any }>({
|
|||
defaultActiveStep={defaultActiveStep}
|
||||
>
|
||||
<FormWizardConsumer>
|
||||
{({ 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<T extends object = { [key: string]: any }>({
|
|||
};
|
||||
});
|
||||
|
||||
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<T extends object = { [key: string]: any }>({
|
|||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
texts={texts}
|
||||
getRightContent={getRightContentNav}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -29,6 +29,7 @@ interface Props {
|
|||
isSaving?: boolean;
|
||||
isStepValid?: boolean;
|
||||
texts?: Partial<NavTexts>;
|
||||
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 (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -100,6 +104,8 @@ export const FormWizardNav = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
{rightContent && <EuiFlexItem grow={false}>{rightContent}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -94,7 +94,7 @@ export function useMultiContent<T extends object>({
|
|||
const activeContentData: Partial<T> = {};
|
||||
|
||||
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<T extends object>({
|
|||
);
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
|
23
src/plugins/es_ui_shared/public/global_flyout/index.ts
Normal file
23
src/plugins/es_ui_shared/public/global_flyout/index.ts
Normal file
|
@ -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';
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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]) => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<AppContextProvider value={mergedDependencies}>
|
||||
<Comp {...props} />
|
||||
<MappingsEditorProvider>
|
||||
<GlobalFlyoutProvider>
|
||||
<Comp {...props} />
|
||||
</GlobalFlyoutProvider>
|
||||
</MappingsEditorProvider>
|
||||
</AppContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -40,10 +40,15 @@ const createActions = (testBed: TestBed<TestSubjects>) => {
|
|||
/**
|
||||
* 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 = () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -109,7 +109,7 @@ export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyT
|
|||
version,
|
||||
order,
|
||||
indexPatterns,
|
||||
template: { settings, aliases, mappings },
|
||||
template: { settings, aliases, mappings } = {},
|
||||
} = template;
|
||||
|
||||
return {
|
||||
|
|
|
@ -61,11 +61,10 @@ describe('<ComponentTemplateDetails />', () => {
|
|||
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('<ComponentTemplateDetails />', () => {
|
|||
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
|
||||
|
|
|
@ -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<ComponentTemplateDetailsTestSubjects> & {
|
||||
actions: ReturnType<typeof createActions>;
|
||||
|
@ -44,7 +44,7 @@ const createActions = (testBed: TestBed<ComponentTemplateDetailsTestSubjects>) =
|
|||
|
||||
export const setup = (props: any): ComponentTemplateDetailsTestBed => {
|
||||
const setupTestBed = registerTestBed<ComponentTemplateDetailsTestSubjects>(
|
||||
WithAppDependencies(ComponentTemplateDetailsFlyout),
|
||||
WithAppDependencies(ComponentTemplateDetailsFlyoutContent),
|
||||
{
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
|
@ -65,6 +65,8 @@ export type ComponentTemplateDetailsTestSubjects =
|
|||
| 'componentTemplateDetails'
|
||||
| 'componentTemplateDetails.title'
|
||||
| 'componentTemplateDetails.footer'
|
||||
| 'title'
|
||||
| 'footer'
|
||||
| 'summaryTab'
|
||||
| 'mappingsTab'
|
||||
| 'settingsTab'
|
||||
|
|
|
@ -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) => (
|
||||
<ComponentTemplatesProvider value={appDependencies}>
|
||||
<Comp {...props} />
|
||||
</ComponentTemplatesProvider>
|
||||
<MappingsEditorProvider>
|
||||
<ComponentTemplatesProvider value={appDependencies}>
|
||||
<GlobalFlyoutProvider>
|
||||
<Comp {...props} />
|
||||
</GlobalFlyoutProvider>
|
||||
</ComponentTemplatesProvider>
|
||||
</MappingsEditorProvider>
|
||||
);
|
||||
|
|
|
@ -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<Props> = ({
|
||||
export const defaultFlyoutProps = {
|
||||
'data-test-subj': 'componentTemplateDetails',
|
||||
'aria-labelledby': 'componentTemplateDetailsFlyoutTitle',
|
||||
};
|
||||
|
||||
export const ComponentTemplateDetailsFlyoutContent: React.FunctionComponent<Props> = ({
|
||||
componentTemplateName,
|
||||
onClose,
|
||||
actions,
|
||||
|
@ -109,13 +113,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent<Props> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="componentTemplateDetails"
|
||||
aria-labelledby="componentTemplateDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={500}
|
||||
>
|
||||
<>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -172,6 +170,6 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent<Props> = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<Props> = ({
|
||||
componentTemplateName,
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
addContent: addContentToGlobalFlyout,
|
||||
removeContent: removeContentFromGlobalFlyout,
|
||||
} = useGlobalFlyout();
|
||||
const { api, trackMetric, documentation } = useComponentTemplatesContext();
|
||||
|
||||
const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates();
|
||||
|
||||
const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState<string[]>([]);
|
||||
|
||||
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<ComponentTemplateDetailsProps>({
|
||||
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<Props> = ({
|
|||
componentTemplatesToDelete={componentTemplatesToDelete}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* details flyout */}
|
||||
{componentTemplateName && (
|
||||
<ComponentTemplateDetailsFlyout
|
||||
onClose={goToComponentTemplateList}
|
||||
componentTemplateName={componentTemplateName}
|
||||
showSummaryCallToAction={true}
|
||||
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)]);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<string | null>(null);
|
||||
const [componentsSelected, setComponentsSelected] = useState<ComponentTemplateListItem[]>([]);
|
||||
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<ComponentTemplateDetailsProps>({
|
||||
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 = ({
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const renderComponentDetails = () => {
|
||||
if (!selectedComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentTemplateDetailsFlyout
|
||||
onClose={() => 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 = ({
|
|||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
|
|
|
@ -8,7 +8,10 @@ export { ComponentTemplatesProvider } from './component_templates_context';
|
|||
|
||||
export { ComponentTemplateList } from './component_template_list';
|
||||
|
||||
export { ComponentTemplateDetailsFlyout } from './component_template_details';
|
||||
export {
|
||||
ComponentTemplateDetailsFlyoutContent,
|
||||
defaultFlyoutProps as componentDetailsFlyoutProps,
|
||||
} from './component_template_details';
|
||||
|
||||
export {
|
||||
ComponentTemplateCreate,
|
||||
|
|
|
@ -19,6 +19,7 @@ export {
|
|||
useAuthorizationContext,
|
||||
NotAuthorizedSection,
|
||||
Forms,
|
||||
GlobalFlyout,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
||||
|
||||
export {
|
||||
|
|
|
@ -12,3 +12,4 @@ export { TemplateDeleteModal } from './template_delete_modal';
|
|||
export { TemplateForm } from './template_form';
|
||||
export * from './mappings_editor';
|
||||
export * from './component_templates';
|
||||
export * from './index_templates';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './simulate_template';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
SimulateTemplateFlyoutContent,
|
||||
defaultFlyoutProps as simulateTemplateFlyoutProps,
|
||||
Props as SimulateTemplateProps,
|
||||
} from './simulate_template_flyout';
|
||||
|
||||
export { SimulateTemplate } from './simulate_template';
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
|
||||
import { serializers } from '../../../../shared_imports';
|
||||
import { TemplateDeserialized } from '../../../../../common';
|
||||
import { serializeTemplate } from '../../../../../common/lib/template_serialization';
|
||||
import { simulateIndexTemplate } from '../../../services';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
|
||||
interface Props {
|
||||
template: { [key: string]: any };
|
||||
minHeightCodeBlock?: string;
|
||||
}
|
||||
|
||||
export const SimulateTemplate = React.memo(({ template, minHeightCodeBlock }: Props) => {
|
||||
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 : (
|
||||
<EuiCodeBlock
|
||||
style={{ minHeight: minHeightCodeBlock }}
|
||||
lang="json"
|
||||
data-test-subj="simulateTemplatePreview"
|
||||
>
|
||||
{templatePreview}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
});
|
|
@ -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 (
|
||||
<>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="componentTemplatesFlyoutTitle" data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.simulateTemplate.title"
|
||||
defaultMessage="Preview index template"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiTextColor color="subdued">
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.simulateTemplate.descriptionText"
|
||||
defaultMessage="This is the final template that will be applied to your indices based on the
|
||||
components templates you have selected and any overrides you've added."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiTextColor>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody data-test-subj="content">
|
||||
<SimulateTemplate template={template} minHeightCodeBlock={`${heightCodeBlock}px`} />
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onClose}
|
||||
data-test-subj="closeDetailsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.simulateTemplate.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="refresh"
|
||||
onClick={updatePreview}
|
||||
data-test-subj="updateSimulationButton"
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.simulateTemplate.updateButtonLabel"
|
||||
defaultMessage="Update"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<TestSubjects>) => {
|
|||
};
|
||||
|
||||
export const setup = (props: any = { onUpdate() {} }): MappingsEditorTestBed => {
|
||||
const setupTestBed = registerTestBed<TestSubjects>(MappingsEditor, {
|
||||
const ComponentToTest = (propsOverride: { [key: string]: any }) => (
|
||||
<MappingsEditorProvider>
|
||||
<GlobalFlyoutProvider>
|
||||
<MappingsEditor {...props} {...propsOverride} />
|
||||
</GlobalFlyoutProvider>
|
||||
</MappingsEditorProvider>
|
||||
);
|
||||
|
||||
const setupTestBed = registerTestBed<TestSubjects>(ComponentToTest, {
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(() => {
|
|||
<DocumentFieldsTreeEditor />
|
||||
);
|
||||
|
||||
const renderEditField = () => {
|
||||
if (status !== 'editingField') {
|
||||
return null;
|
||||
}
|
||||
const field = fields.byId[fieldToEdit!];
|
||||
return <EditFieldContainer field={field} allFields={fields.byId} />;
|
||||
};
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
dispatch({ type: 'search:update', value });
|
||||
|
@ -59,7 +51,7 @@ export const DocumentFields = React.memo(() => {
|
|||
) : (
|
||||
editor
|
||||
)}
|
||||
{renderEditField()}
|
||||
<EditFieldContainer />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
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 <EuiForm />, which wrapps the form with
|
||||
// a <div>. 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 (
|
||||
<UpdateFieldProvider>
|
||||
{(updateField) => (
|
||||
<Form form={form} onSubmit={getSubmitForm(updateField)}>
|
||||
<EuiFlyout
|
||||
data-test-subj="mappingsEditorFieldEdit"
|
||||
onClose={exitEdit}
|
||||
aria-labelledby="mappingsEditorFieldEditTitle"
|
||||
size="m"
|
||||
className="mappingsEditor__editField"
|
||||
maxWidth={720}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
{/* We need an extra div to get out of flex grow */}
|
||||
<div>
|
||||
{/* Title */}
|
||||
<EuiTitle size="m">
|
||||
<h2 data-test-subj="flyoutTitle">
|
||||
{isMultiField
|
||||
? i18n.translate('xpack.idxMgmt.mappingsEditor.editMultiFieldTitle', {
|
||||
defaultMessage: "Edit multi-field '{fieldName}'",
|
||||
values: {
|
||||
fieldName: limitStringLength(field.source.name),
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldTitle', {
|
||||
defaultMessage: "Edit field '{fieldName}'",
|
||||
values: {
|
||||
fieldName: limitStringLength(field.source.name),
|
||||
},
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<Form form={form} FormWrapper={FormWrapper}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
{/* We need an extra div to get out of flex grow */}
|
||||
<div>
|
||||
{/* Title */}
|
||||
<EuiTitle size="m">
|
||||
<h2 data-test-subj="flyoutTitle">
|
||||
{isMultiField
|
||||
? i18n.translate('xpack.idxMgmt.mappingsEditor.editMultiFieldTitle', {
|
||||
defaultMessage: "Edit multi-field '{fieldName}'",
|
||||
values: {
|
||||
fieldName: limitStringLength(field.source.name),
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldTitle', {
|
||||
defaultMessage: "Edit field '{fieldName}'",
|
||||
values: {
|
||||
fieldName: limitStringLength(field.source.name),
|
||||
},
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Documentation link */}
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const linkDocumentation =
|
||||
documentationService.getTypeDocLink(subType) ||
|
||||
documentationService.getTypeDocLink(type);
|
||||
{/* Documentation link */}
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const linkDocumentation =
|
||||
documentationService.getTypeDocLink(subType) ||
|
||||
documentationService.getTypeDocLink(type);
|
||||
|
||||
if (!linkDocumentation) {
|
||||
return null;
|
||||
}
|
||||
if (!linkDocumentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeDefinition = TYPE_DEFINITION[type as MainType];
|
||||
const subTypeDefinition = TYPE_DEFINITION[subType as SubType];
|
||||
const typeDefinition = TYPE_DEFINITION[type as MainType];
|
||||
const subTypeDefinition = TYPE_DEFINITION[subType as SubType];
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={linkDocumentation}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation',
|
||||
{
|
||||
defaultMessage: '{type} documentation',
|
||||
values: {
|
||||
type: subTypeDefinition
|
||||
? subTypeDefinition.label
|
||||
: typeDefinition.label,
|
||||
},
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{/* Field path */}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPath">
|
||||
<CodeBlock padding="small">{field.path.join(' > ')}</CodeBlock>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EditFieldHeaderForm
|
||||
defaultValue={field.source}
|
||||
isRootLevelField={field.parentId === undefined}
|
||||
isMultiField={isMultiField}
|
||||
/>
|
||||
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const ParametersForm = getParametersFormForType(type, subType);
|
||||
|
||||
if (!ParametersForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ParametersForm
|
||||
// As the component "ParametersForm" does not change when switching type, and all the props
|
||||
// also remain the same (===), adding a key give us *a new instance* each time we change the type or subType.
|
||||
// This will trigger an unmount of all the previous form fields and then mount the new ones.
|
||||
key={subType ?? type}
|
||||
field={field}
|
||||
allFields={allFields}
|
||||
isMultiField={isMultiField}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
{form.isSubmitted && !form.isValid && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.editFieldFlyout.validationErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Fix errors in form before continuing.',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
data-test-subj="formError"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={cancel}>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldCancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={linkDocumentation}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', {
|
||||
defaultMessage: '{type} documentation',
|
||||
values: {
|
||||
type: subTypeDefinition ? subTypeDefinition.label : typeDefinition.label,
|
||||
},
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={getSubmitForm(updateField)}
|
||||
type="submit"
|
||||
disabled={form.isSubmitted && !form.isValid}
|
||||
data-test-subj="editFieldUpdateButton"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', {
|
||||
defaultMessage: 'Update',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</Form>
|
||||
)}
|
||||
</UpdateFieldProvider>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{/* Field path */}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPath">
|
||||
<CodeBlock padding="small">{field.path.join(' > ')}</CodeBlock>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EditFieldHeaderForm
|
||||
defaultValue={field.source}
|
||||
isRootLevelField={field.parentId === undefined}
|
||||
isMultiField={isMultiField}
|
||||
/>
|
||||
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const ParametersForm = getParametersFormForType(type, subType);
|
||||
|
||||
if (!ParametersForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ParametersForm
|
||||
// As the component "ParametersForm" does not change when switching type, and all the props
|
||||
// also remain the same (===), adding a key give us *a new instance* each time we change the type or subType.
|
||||
// This will trigger an unmount of all the previous form fields and then mount the new ones.
|
||||
key={subType ?? type}
|
||||
field={field}
|
||||
allFields={allFields}
|
||||
isMultiField={isMultiField}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
{form.isSubmitted && !form.isValid && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.editFieldFlyout.validationErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Fix errors in form before continuing.',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
data-test-subj="formError"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={exitEdit}>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldCancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={submitForm}
|
||||
type="submit"
|
||||
disabled={form.isSubmitted && !form.isValid}
|
||||
data-test-subj="editFieldUpdateButton"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', {
|
||||
defaultMessage: 'Update',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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<Field>({
|
||||
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 <EditField form={form} field={field} allFields={allFields} exitEdit={exitEdit} />;
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
// Open the flyout with the <EditField /> content
|
||||
addContentToGlobalFlyout<EditFieldProps>({
|
||||
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 ? <ModalConfirmationDeleteFields {...modal.props} /> : 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<State>({
|
||||
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 && (
|
||||
<ModalConfirmationDeleteFields
|
||||
title={modalTitle!}
|
||||
childFields={state.field && state.field.childFields}
|
||||
aliases={state.aliases}
|
||||
byId={byId}
|
||||
confirmButtonText={confirmButtonText}
|
||||
onConfirm={confirmTypeUpdate}
|
||||
onCancel={closeModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<State>({
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<IndexSettings | undefined>(undefined);
|
||||
|
|
|
@ -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<TabName>('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<TabName>('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: <DocumentFields />,
|
||||
templates: <TemplatesForm value={state.templates.defaultValue} />,
|
||||
advanced: <ConfigurationForm value={state.configuration.defaultValue} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test-subj="mappingsEditor">
|
||||
{multipleMappingsDeclared ? (
|
||||
<MultipleMappingsWarning />
|
||||
) : (
|
||||
<IndexSettingsProvider indexSettings={indexSettings}>
|
||||
<MappingsState onChange={onChange} value={parsedDefaultValue!}>
|
||||
{({ state }) => {
|
||||
const tabToContentMap = {
|
||||
fields: <DocumentFields />,
|
||||
templates: <TemplatesForm value={state.templates.defaultValue} />,
|
||||
advanced: <ConfigurationForm value={state.configuration.defaultValue} />,
|
||||
};
|
||||
<div className="mappingsEditor">
|
||||
<EuiTabs>
|
||||
<EuiTab
|
||||
onClick={() => changeTab('fields')}
|
||||
isSelected={selectedTab === 'fields'}
|
||||
data-test-subj="formTab"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
|
||||
defaultMessage: 'Mapped fields',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={() => changeTab('templates')}
|
||||
isSelected={selectedTab === 'templates'}
|
||||
data-test-subj="formTab"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
|
||||
defaultMessage: 'Dynamic templates',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={() => changeTab('advanced')}
|
||||
isSelected={selectedTab === 'advanced'}
|
||||
data-test-subj="formTab"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
|
||||
defaultMessage: 'Advanced options',
|
||||
})}
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
|
||||
return (
|
||||
<div className="mappingsEditor">
|
||||
<EuiTabs>
|
||||
<EuiTab
|
||||
onClick={() => changeTab('fields', state)}
|
||||
isSelected={selectedTab === 'fields'}
|
||||
data-test-subj="formTab"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
|
||||
defaultMessage: 'Mapped fields',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={() => changeTab('templates', state)}
|
||||
isSelected={selectedTab === 'templates'}
|
||||
data-test-subj="formTab"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
|
||||
defaultMessage: 'Dynamic templates',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={() => changeTab('advanced', state)}
|
||||
isSelected={selectedTab === 'advanced'}
|
||||
data-test-subj="formTab"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
|
||||
defaultMessage: 'Advanced options',
|
||||
})}
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{tabToContentMap[selectedTab]}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</MappingsState>
|
||||
{tabToContentMap[selectedTab]}
|
||||
</div>
|
||||
</IndexSettingsProvider>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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 <StateProvider>{children}</StateProvider>;
|
||||
};
|
|
@ -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<State | undefined>(undefined);
|
||||
const DispatchContext = createContext<Dispatch | undefined>(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 (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMappingsState = () => {
|
||||
const ctx = useContext(StateContext);
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useMappingsState must be used within a <MappingsState>');
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const useDispatch = () => {
|
||||
const ctx = useContext(DispatchContext);
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useDispatch must be used within a <MappingsState>');
|
||||
}
|
||||
return ctx;
|
||||
};
|
|
@ -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<MappingsConfiguration> {
|
||||
defaultValue: MappingsConfiguration;
|
||||
submitForm?: FormHook<MappingsConfiguration>['submit'];
|
||||
}
|
||||
|
||||
interface TemplatesFormState extends OnFormUpdateArg<MappingsTemplates> {
|
||||
defaultValue: MappingsTemplates;
|
||||
submitForm?: FormHook<MappingsTemplates>['submit'];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isValid: boolean | undefined;
|
||||
configuration: ConfigurationFormState;
|
||||
documentFields: DocumentFieldsState;
|
||||
fields: NormalizedFields;
|
||||
fieldForm?: OnFormUpdateArg<any>;
|
||||
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<ConfigurationFormState> }
|
||||
| { type: 'configuration.save'; value: MappingsConfiguration }
|
||||
| { type: 'templates.update'; value: Partial<State['templates']> }
|
||||
| { type: 'templates.save'; value: MappingsTemplates }
|
||||
| { type: 'fieldForm.update'; value: OnFormUpdateArg<any> }
|
||||
| { 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,
|
||||
},
|
||||
|
|
|
@ -49,4 +49,5 @@ export {
|
|||
export {
|
||||
JsonEditor,
|
||||
OnJsonEditorUpdateHandler,
|
||||
GlobalFlyout,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
||||
|
|
|
@ -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<T extends string = string> = {
|
||||
value: unknown;
|
||||
text: T | ReactNode;
|
||||
} & OptionHTMLAttributes<HTMLOptionElement>;
|
||||
|
||||
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 <strong> 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;
|
||||
}
|
|
@ -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';
|
|
@ -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<boolean>;
|
||||
}
|
||||
|
||||
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<T extends string = string> = {
|
||||
value: unknown;
|
||||
text: T | ReactNode;
|
||||
} & OptionHTMLAttributes<HTMLOptionElement>;
|
||||
|
||||
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 <strong> 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;
|
||||
}
|
|
@ -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<MappingsConfiguration> {
|
||||
defaultValue: MappingsConfiguration;
|
||||
submitForm?: FormHook<MappingsConfiguration>['submit'];
|
||||
}
|
||||
|
||||
interface TemplatesFormState extends OnFormUpdateArg<MappingsTemplates> {
|
||||
defaultValue: MappingsTemplates;
|
||||
submitForm?: FormHook<MappingsTemplates>['submit'];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isValid: boolean | undefined;
|
||||
configuration: ConfigurationFormState;
|
||||
documentFields: DocumentFieldsState;
|
||||
fields: NormalizedFields;
|
||||
fieldForm?: OnFormUpdateArg<any>;
|
||||
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<ConfigurationFormState> }
|
||||
| { type: 'configuration.save'; value: MappingsConfiguration }
|
||||
| { type: 'templates.update'; value: Partial<State['templates']> }
|
||||
| { type: 'templates.save'; value: MappingsTemplates }
|
||||
| { type: 'fieldForm.update'; value: OnFormUpdateArg<any> }
|
||||
| { 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;
|
|
@ -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<boolean>;
|
||||
}
|
||||
|
||||
export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void;
|
||||
|
||||
const StateContext = createContext<State | undefined>(undefined);
|
||||
const DispatchContext = createContext<Dispatch | undefined>(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 (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>{children({ state })}</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export const useMappingsState = () => {
|
||||
const ctx = useContext(StateContext);
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useMappingsState must be used within a <MappingsState>');
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const useDispatch = () => {
|
||||
const ctx = useContext(DispatchContext);
|
||||
if (ctx === undefined) {
|
||||
throw new Error('useDispatch must be used within a <MappingsState>');
|
||||
}
|
||||
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]);
|
||||
};
|
|
@ -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[];
|
||||
|
|
|
@ -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<Props> = 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<Props> = 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<Props> = React.memo(
|
|||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationService.getTemplatesDocumentationLink()}
|
||||
href={documentationService.getTemplatesDocumentationLink(isLegacy)}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
|
@ -220,8 +213,8 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
|
||||
<Form
|
||||
form={form}
|
||||
isInvalid={form.isSubmitted && !form.isValid}
|
||||
error={form.getErrors()}
|
||||
isInvalid={isSubmitted && !isFormValid}
|
||||
error={getFormErrors()}
|
||||
data-test-subj="stepLogistics"
|
||||
>
|
||||
{/* Name */}
|
||||
|
|
|
@ -24,6 +24,7 @@ import { serializers } from '../../../../shared_imports';
|
|||
|
||||
import { serializeLegacyTemplate, serializeTemplate } from '../../../../../common/lib';
|
||||
import { TemplateDeserialized, getTemplateParameter } from '../../../../../common';
|
||||
import { SimulateTemplate } from '../../index_templates';
|
||||
import { WizardSection } from '../template_form';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
|
@ -56,6 +57,27 @@ interface Props {
|
|||
navigateToStep: (stepId: WizardSection) => void;
|
||||
}
|
||||
|
||||
const PreviewTab = ({ template }: { template: { [key: string]: any } }) => {
|
||||
return (
|
||||
<div data-test-subj="previewTab">
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepReview.previewTab.descriptionText"
|
||||
defaultMessage="This is the final template that will be applied to your indices."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<SimulateTemplate template={template} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StepReview: React.FunctionComponent<Props> = React.memo(
|
||||
({ template, navigateToStep }) => {
|
||||
const {
|
||||
|
@ -286,6 +308,33 @@ export const StepReview: React.FunctionComponent<Props> = React.memo(
|
|||
);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'summary',
|
||||
name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.summaryTabTitle', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
content: <SummaryTab />,
|
||||
},
|
||||
{
|
||||
id: 'request',
|
||||
name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.requestTabTitle', {
|
||||
defaultMessage: 'Request',
|
||||
}),
|
||||
content: <RequestTab />,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isLegacy) {
|
||||
tabs.splice(1, 0, {
|
||||
id: 'preview',
|
||||
name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.previewTabTitle', {
|
||||
defaultMessage: 'Preview',
|
||||
}),
|
||||
content: <PreviewTab template={template} />,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj="stepSummary">
|
||||
<EuiTitle>
|
||||
|
@ -331,25 +380,7 @@ export const StepReview: React.FunctionComponent<Props> = React.memo(
|
|||
</Fragment>
|
||||
) : null}
|
||||
|
||||
<EuiTabbedContent
|
||||
data-test-subj="summaryTabContent"
|
||||
tabs={[
|
||||
{
|
||||
id: 'summary',
|
||||
name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.summaryTabTitle', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
content: <SummaryTab />,
|
||||
},
|
||||
{
|
||||
id: 'request',
|
||||
name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.requestTabTitle', {
|
||||
defaultMessage: 'Request',
|
||||
}),
|
||||
content: <RequestTab />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiTabbedContent data-test-subj="summaryTabContent" tabs={tabs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,14 +3,19 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiButton } from '@elastic/eui';
|
||||
|
||||
import { TemplateDeserialized } from '../../../../common';
|
||||
import { serializers, Forms } from '../../../shared_imports';
|
||||
import { serializers, Forms, GlobalFlyout } from '../../../shared_imports';
|
||||
import { SectionError } from '../section_error';
|
||||
import {
|
||||
SimulateTemplateFlyoutContent,
|
||||
SimulateTemplateProps,
|
||||
simulateTemplateFlyoutProps,
|
||||
} from '../index_templates';
|
||||
import { StepLogisticsContainer, StepComponentContainer, StepReviewContainer } from './steps';
|
||||
import {
|
||||
CommonWizardSteps,
|
||||
|
@ -22,8 +27,10 @@ import { documentationService } from '../../services/documentation';
|
|||
|
||||
const { stripEmptyFields } = serializers;
|
||||
const { FormWizard, FormWizardStep } = Forms;
|
||||
const { useGlobalFlyout } = GlobalFlyout;
|
||||
|
||||
interface Props {
|
||||
title: string | JSX.Element;
|
||||
onSave: (template: TemplateDeserialized) => void;
|
||||
clearSaveError: () => void;
|
||||
isSaving: boolean;
|
||||
|
@ -80,6 +87,7 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = {
|
|||
};
|
||||
|
||||
export const TemplateForm = ({
|
||||
title,
|
||||
defaultValue,
|
||||
isEditing,
|
||||
isSaving,
|
||||
|
@ -88,6 +96,9 @@ export const TemplateForm = ({
|
|||
clearSaveError,
|
||||
onSave,
|
||||
}: Props) => {
|
||||
const [wizardContent, setWizardContent] = useState<Forms.Content<WizardContent> | null>(null);
|
||||
const { addContent: addContentToGlobalFlyout, closeFlyout } = useGlobalFlyout();
|
||||
|
||||
const indexTemplate = defaultValue ?? {
|
||||
name: '',
|
||||
indexPatterns: [],
|
||||
|
@ -189,6 +200,10 @@ export const TemplateForm = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const onWizardContentChange = useCallback((content: Forms.Content<WizardContent>) => {
|
||||
setWizardContent(content);
|
||||
}, []);
|
||||
|
||||
const onSaveTemplate = useCallback(
|
||||
async (wizardData: WizardContent) => {
|
||||
const template = buildTemplateObject(indexTemplate)(wizardData);
|
||||
|
@ -206,44 +221,101 @@ export const TemplateForm = ({
|
|||
[indexTemplate, buildTemplateObject, onSave, clearSaveError]
|
||||
);
|
||||
|
||||
const getSimulateTemplate = useCallback(async () => {
|
||||
if (!wizardContent) {
|
||||
return;
|
||||
}
|
||||
const isValid = await wizardContent.validate();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
const wizardData = wizardContent.getData();
|
||||
const template = buildTemplateObject(indexTemplate)(wizardData);
|
||||
return template;
|
||||
}, [buildTemplateObject, indexTemplate, wizardContent]);
|
||||
|
||||
const showPreviewFlyout = () => {
|
||||
addContentToGlobalFlyout<SimulateTemplateProps>({
|
||||
id: 'simulateTemplate',
|
||||
Component: SimulateTemplateFlyoutContent,
|
||||
props: {
|
||||
getTemplate: getSimulateTemplate,
|
||||
onClose: closeFlyout,
|
||||
},
|
||||
flyoutProps: simulateTemplateFlyoutProps,
|
||||
});
|
||||
};
|
||||
|
||||
const getRightContentWizardNav = (stepId: WizardSection) => {
|
||||
if (isLegacy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't show "Preview template" button on logistics and review steps
|
||||
if (stepId === 'logistics' || stepId === 'review') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButton size="s" onClick={showPreviewFlyout}>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.previewIndexTemplateButtonLabel"
|
||||
defaultMessage="Preview index template"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormWizard<WizardContent>
|
||||
defaultValue={wizardDefaultValue}
|
||||
onSave={onSaveTemplate}
|
||||
isEditing={isEditing}
|
||||
isSaving={isSaving}
|
||||
apiError={apiError}
|
||||
texts={i18nTexts}
|
||||
>
|
||||
<FormWizardStep
|
||||
id={wizardSections.logistics.id}
|
||||
label={wizardSections.logistics.label}
|
||||
isRequired
|
||||
<>
|
||||
{/* Form header */}
|
||||
{title}
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<FormWizard<WizardContent, WizardSection>
|
||||
defaultValue={wizardDefaultValue}
|
||||
onSave={onSaveTemplate}
|
||||
isEditing={isEditing}
|
||||
isSaving={isSaving}
|
||||
apiError={apiError}
|
||||
texts={i18nTexts}
|
||||
onChange={onWizardContentChange}
|
||||
rightContentNav={getRightContentWizardNav}
|
||||
>
|
||||
<StepLogisticsContainer isEditing={isEditing} isLegacy={indexTemplate._kbnMeta.isLegacy} />
|
||||
</FormWizardStep>
|
||||
|
||||
{indexTemplate._kbnMeta.isLegacy !== true && (
|
||||
<FormWizardStep id={wizardSections.components.id} label={wizardSections.components.label}>
|
||||
<StepComponentContainer />
|
||||
<FormWizardStep
|
||||
id={wizardSections.logistics.id}
|
||||
label={wizardSections.logistics.label}
|
||||
isRequired
|
||||
>
|
||||
<StepLogisticsContainer
|
||||
isEditing={isEditing}
|
||||
isLegacy={indexTemplate._kbnMeta.isLegacy}
|
||||
/>
|
||||
</FormWizardStep>
|
||||
)}
|
||||
|
||||
<FormWizardStep id={wizardSections.settings.id} label={wizardSections.settings.label}>
|
||||
<StepSettingsContainer esDocsBase={documentationService.getEsDocsBase()} />
|
||||
</FormWizardStep>
|
||||
{indexTemplate._kbnMeta.isLegacy !== true && (
|
||||
<FormWizardStep id={wizardSections.components.id} label={wizardSections.components.label}>
|
||||
<StepComponentContainer />
|
||||
</FormWizardStep>
|
||||
)}
|
||||
|
||||
<FormWizardStep id={wizardSections.mappings.id} label={wizardSections.mappings.label}>
|
||||
<StepMappingsContainer esDocsBase={documentationService.getEsDocsBase()} />
|
||||
</FormWizardStep>
|
||||
<FormWizardStep id={wizardSections.settings.id} label={wizardSections.settings.label}>
|
||||
<StepSettingsContainer esDocsBase={documentationService.getEsDocsBase()} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.aliases.id} label={wizardSections.aliases.label}>
|
||||
<StepAliasesContainer esDocsBase={documentationService.getEsDocsBase()} />
|
||||
</FormWizardStep>
|
||||
<FormWizardStep id={wizardSections.mappings.id} label={wizardSections.mappings.label}>
|
||||
<StepMappingsContainer esDocsBase={documentationService.getEsDocsBase()} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.review.id} label={wizardSections.review.label}>
|
||||
<StepReviewContainer getTemplateData={buildTemplateObject(indexTemplate)} />
|
||||
</FormWizardStep>
|
||||
</FormWizard>
|
||||
<FormWizardStep id={wizardSections.aliases.id} label={wizardSections.aliases.label}>
|
||||
<StepAliasesContainer esDocsBase={documentationService.getEsDocsBase()} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.review.id} label={wizardSections.review.label}>
|
||||
<StepReviewContainer getTemplateData={buildTemplateObject(indexTemplate)} />
|
||||
</FormWizardStep>
|
||||
</FormWizard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,11 +11,14 @@ import { render, unmountComponentAtNode } from 'react-dom';
|
|||
import { CoreStart } from '../../../../../src/core/public';
|
||||
|
||||
import { API_BASE_PATH } from '../../common';
|
||||
import { GlobalFlyout } from '../shared_imports';
|
||||
|
||||
import { AppContextProvider, AppDependencies } from './app_context';
|
||||
import { App } from './app';
|
||||
import { indexManagementStore } from './store';
|
||||
import { ComponentTemplatesProvider } from './components';
|
||||
import { ComponentTemplatesProvider, MappingsEditorProvider } from './components';
|
||||
|
||||
const { GlobalFlyoutProvider } = GlobalFlyout;
|
||||
|
||||
export const renderApp = (
|
||||
elem: HTMLElement | null,
|
||||
|
@ -43,9 +46,13 @@ export const renderApp = (
|
|||
<I18nContext>
|
||||
<Provider store={indexManagementStore(services)}>
|
||||
<AppContextProvider value={dependencies}>
|
||||
<ComponentTemplatesProvider value={componentTemplateProviderValues}>
|
||||
<App history={history} />
|
||||
</ComponentTemplatesProvider>
|
||||
<MappingsEditorProvider>
|
||||
<ComponentTemplatesProvider value={componentTemplateProviderValues}>
|
||||
<GlobalFlyoutProvider>
|
||||
<App history={history} />
|
||||
</GlobalFlyoutProvider>
|
||||
</ComponentTemplatesProvider>
|
||||
</MappingsEditorProvider>
|
||||
</AppContextProvider>
|
||||
</Provider>
|
||||
</I18nContext>,
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export { TabSummary } from './tab_summary';
|
||||
export { TabPreview } from './tab_preview';
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import { TemplateDeserialized } from '../../../../../../../common';
|
||||
import { SimulateTemplate } from '../../../../../components/index_templates';
|
||||
|
||||
interface Props {
|
||||
templateDetails: TemplateDeserialized;
|
||||
}
|
||||
|
||||
export const TabPreview = ({ templateDetails }: Props) => {
|
||||
return (
|
||||
<div data-test-subj="previewTabContent">
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateDetails.previewTab.descriptionText"
|
||||
defaultMessage="This is the final template that will be applied to your indices."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<SimulateTemplate template={templateDetails} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -15,8 +15,6 @@ export const TemplateDetails = (props: Props) => {
|
|||
onClose={props.onClose}
|
||||
data-test-subj="templateDetails"
|
||||
aria-labelledby="templateDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={500}
|
||||
>
|
||||
<TemplateDetailsContent {...props} />
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
|
||||
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
|
||||
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
|
||||
UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB,
|
||||
} from '../../../../../../common/constants';
|
||||
import { SendRequestResponse } from '../../../../../shared_imports';
|
||||
import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components';
|
||||
|
@ -37,12 +38,13 @@ import { decodePathFromReactRouter } from '../../../../services/routing';
|
|||
import { useServices } from '../../../../app_context';
|
||||
import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared';
|
||||
import { TemplateTypeIndicator } from '../components';
|
||||
import { TabSummary } from './tabs';
|
||||
import { TabSummary, TabPreview } from './tabs';
|
||||
|
||||
const SUMMARY_TAB_ID = 'summary';
|
||||
const MAPPINGS_TAB_ID = 'mappings';
|
||||
const ALIASES_TAB_ID = 'aliases';
|
||||
const SETTINGS_TAB_ID = 'settings';
|
||||
const PREVIEW_TAB_ID = 'preview';
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
|
@ -69,6 +71,12 @@ const TABS = [
|
|||
defaultMessage: 'Aliases',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: PREVIEW_TAB_ID,
|
||||
name: i18n.translate('xpack.idxMgmt.templateDetails.previewTabTitle', {
|
||||
defaultMessage: 'Preview',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const tabToUiMetricMap: { [key: string]: string } = {
|
||||
|
@ -76,6 +84,7 @@ const tabToUiMetricMap: { [key: string]: string } = {
|
|||
[SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
|
||||
[MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
|
||||
[ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
|
||||
[PREVIEW_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB,
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
|
@ -161,6 +170,7 @@ export const TemplateDetailsContent = ({
|
|||
[SETTINGS_TAB_ID]: <TabSettings settings={settings} />,
|
||||
[MAPPINGS_TAB_ID]: <TabMappings mappings={mappings} />,
|
||||
[ALIASES_TAB_ID]: <TabAliases aliases={aliases} />,
|
||||
[PREVIEW_TAB_ID]: <TabPreview templateDetails={templateDetails} />,
|
||||
};
|
||||
|
||||
const tabContent = tabToComponentMap[activeTab];
|
||||
|
@ -191,7 +201,13 @@ export const TemplateDetailsContent = ({
|
|||
{managedTemplateCallout}
|
||||
|
||||
<EuiTabs>
|
||||
{TABS.map((tab) => (
|
||||
{TABS.filter((tab) => {
|
||||
// Legacy index templates don't have the "simulate" template API
|
||||
if (isLegacy && tab.id === PREVIEW_TAB_ID) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).map((tab) => (
|
||||
<EuiTab
|
||||
onClick={() => {
|
||||
uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { TemplateDeserialized } from '../../../../common';
|
||||
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
|
||||
|
@ -94,30 +94,30 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
|
|||
|
||||
content = (
|
||||
<TemplateForm
|
||||
title={
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createTemplate.cloneTemplatePageTitle"
|
||||
defaultMessage="Clone template '{name}'"
|
||||
values={{ name: decodedTemplateName }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
}
|
||||
defaultValue={templateData}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
clearSaveError={clearSaveError}
|
||||
isLegacy={isLegacy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createTemplate.cloneTemplatePageTitle"
|
||||
defaultMessage="Clone template '{name}'"
|
||||
values={{ name: decodedTemplateName }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{content}
|
||||
</EuiPageContent>
|
||||
<EuiPageContent>{content}</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { parse } from 'query-string';
|
||||
|
||||
|
@ -51,23 +51,24 @@ export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ h
|
|||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
{isLegacy ? (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle"
|
||||
defaultMessage="Create legacy template"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createTemplate.createTemplatePageTitle"
|
||||
defaultMessage="Create template"
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
<TemplateForm
|
||||
title={
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
{isLegacy ? (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle"
|
||||
defaultMessage="Create legacy template"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createTemplate.createTemplatePageTitle"
|
||||
defaultMessage="Create template"
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
|
|
|
@ -133,12 +133,24 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</Fragment>
|
||||
)}
|
||||
<TemplateForm
|
||||
title={
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.editTemplate.editTemplatePageTitle"
|
||||
defaultMessage="Edit template '{name}'"
|
||||
values={{ name: decodedTemplateName }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
}
|
||||
defaultValue={template}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
clearSaveError={clearSaveError}
|
||||
isEditing={true}
|
||||
isLegacy={isLegacy}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -147,19 +159,7 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.editTemplate.editTemplatePageTitle"
|
||||
defaultMessage="Edit template '{name}'"
|
||||
values={{ name: decodedTemplateName }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{content}
|
||||
</EuiPageContent>
|
||||
<EuiPageContent>{content}</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
UIM_TEMPLATE_CREATE,
|
||||
UIM_TEMPLATE_UPDATE,
|
||||
UIM_TEMPLATE_CLONE,
|
||||
UIM_TEMPLATE_SIMULATE,
|
||||
} from '../../../common/constants';
|
||||
import { TemplateDeserialized, TemplateListItem, DataStream } from '../../../common';
|
||||
import { IndexMgmtMetricsType } from '../../types';
|
||||
|
@ -286,3 +287,14 @@ export async function updateTemplate(template: TemplateDeserialized) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function simulateIndexTemplate(template: { [key: string]: any }) {
|
||||
return sendRequest({
|
||||
path: `${API_BASE_PATH}/index_templates/simulate`,
|
||||
method: 'post',
|
||||
body: JSON.stringify(template),
|
||||
}).then((result) => {
|
||||
uiMetricService.trackMetric('count', UIM_TEMPLATE_SIMULATE);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -40,8 +40,10 @@ class DocumentationService {
|
|||
return `${this.esDocsBase}/data-streams.html`;
|
||||
}
|
||||
|
||||
public getTemplatesDocumentationLink() {
|
||||
return `${this.esDocsBase}/indices-templates.html`;
|
||||
public getTemplatesDocumentationLink(isLegacy = false) {
|
||||
return isLegacy
|
||||
? `${this.esDocsBase}/indices-templates-v1.html`
|
||||
: `${this.esDocsBase}/indices-templates.html`;
|
||||
}
|
||||
|
||||
public getIdxMgmtDocumentationLink() {
|
||||
|
|
|
@ -22,6 +22,7 @@ export {
|
|||
loadIndexMapping,
|
||||
loadIndexData,
|
||||
useLoadIndexTemplates,
|
||||
simulateIndexTemplate,
|
||||
} from './api';
|
||||
export { healthToColor } from './health_to_color';
|
||||
export { sortTable } from './sort_table';
|
||||
|
|
|
@ -12,6 +12,7 @@ export {
|
|||
useRequest,
|
||||
Forms,
|
||||
extractQueryParams,
|
||||
GlobalFlyout,
|
||||
} from '../../../../src/plugins/es_ui_shared/public/';
|
||||
|
||||
export {
|
||||
|
|
|
@ -182,4 +182,14 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
|
|||
],
|
||||
method: 'HEAD',
|
||||
});
|
||||
|
||||
dataManagement.simulateTemplate = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_index_template/_simulate',
|
||||
},
|
||||
],
|
||||
needBody: true,
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addBasePath } from '../index';
|
||||
|
||||
const bodySchema = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
export function registerSimulateRoute({ router, license, lib }: RouteDependencies) {
|
||||
router.post(
|
||||
{
|
||||
path: addBasePath('/index_templates/simulate'),
|
||||
validate: { body: bodySchema },
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.dataManagement!.client;
|
||||
const template = req.body as TypeOf<typeof bodySchema>;
|
||||
|
||||
try {
|
||||
const templatePreview = await callAsCurrentUser('dataManagement.simulateTemplate', {
|
||||
body: template,
|
||||
});
|
||||
|
||||
return res.ok({ body: templatePreview });
|
||||
} catch (e) {
|
||||
if (lib.isEsError(e)) {
|
||||
return res.customError({
|
||||
statusCode: e.statusCode,
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
// Case: default
|
||||
return res.internalError({ body: e });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import { registerGetAllRoute, registerGetOneRoute } from './register_get_routes'
|
|||
import { registerDeleteRoute } from './register_delete_route';
|
||||
import { registerCreateRoute } from './register_create_route';
|
||||
import { registerUpdateRoute } from './register_update_route';
|
||||
import { registerSimulateRoute } from './register_simulate_route';
|
||||
|
||||
export function registerTemplateRoutes(dependencies: RouteDependencies) {
|
||||
registerGetAllRoute(dependencies);
|
||||
|
@ -17,4 +18,5 @@ export function registerTemplateRoutes(dependencies: RouteDependencies) {
|
|||
registerDeleteRoute(dependencies);
|
||||
registerCreateRoute(dependencies);
|
||||
registerUpdateRoute(dependencies);
|
||||
registerSimulateRoute(dependencies);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue