[Component templates] Form wizard (#69732)
This commit is contained in:
parent
2eb0896415
commit
e35a42aa07
|
@ -4,91 +4,164 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { deserializeComponentTemplate } from './component_template_serialization';
|
||||
import {
|
||||
deserializeComponentTemplate,
|
||||
serializeComponentTemplate,
|
||||
} from './component_template_serialization';
|
||||
|
||||
describe('deserializeComponentTemplate', () => {
|
||||
test('deserializes a component template', () => {
|
||||
expect(
|
||||
deserializeComponentTemplate(
|
||||
{
|
||||
name: 'my_component_template',
|
||||
component_template: {
|
||||
version: 1,
|
||||
_meta: {
|
||||
serialization: {
|
||||
id: 10,
|
||||
class: 'MyComponentTemplate',
|
||||
},
|
||||
description: 'set number of shards to one',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
describe('Component template serialization', () => {
|
||||
describe('deserializeComponentTemplate()', () => {
|
||||
test('deserializes a component template', () => {
|
||||
expect(
|
||||
deserializeComponentTemplate(
|
||||
{
|
||||
name: 'my_index_template',
|
||||
index_template: {
|
||||
index_patterns: ['foo'],
|
||||
name: 'my_component_template',
|
||||
component_template: {
|
||||
version: 1,
|
||||
_meta: {
|
||||
serialization: {
|
||||
id: 10,
|
||||
class: 'MyComponentTemplate',
|
||||
},
|
||||
description: 'set number of shards to one',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_replicas: 2,
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
composed_of: ['my_component_template'],
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
).toEqual({
|
||||
name: 'my_component_template',
|
||||
version: 1,
|
||||
_meta: {
|
||||
serialization: {
|
||||
id: 10,
|
||||
class: 'MyComponentTemplate',
|
||||
},
|
||||
description: 'set number of shards to one',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword',
|
||||
[
|
||||
{
|
||||
name: 'my_index_template',
|
||||
index_template: {
|
||||
index_patterns: ['foo'],
|
||||
template: {
|
||||
settings: {
|
||||
number_of_replicas: 2,
|
||||
},
|
||||
},
|
||||
composed_of: ['my_component_template'],
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
]
|
||||
)
|
||||
).toEqual({
|
||||
name: 'my_component_template',
|
||||
version: 1,
|
||||
_meta: {
|
||||
serialization: {
|
||||
id: 10,
|
||||
class: 'MyComponentTemplate',
|
||||
},
|
||||
description: 'set number of shards to one',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_kbnMeta: {
|
||||
usedBy: ['my_index_template'],
|
||||
},
|
||||
_kbnMeta: {
|
||||
usedBy: ['my_index_template'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeComponentTemplate()', () => {
|
||||
test('serialize a component template', () => {
|
||||
expect(
|
||||
serializeComponentTemplate({
|
||||
name: 'my_component_template',
|
||||
version: 1,
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
_meta: {
|
||||
serialization: {
|
||||
id: 10,
|
||||
class: 'MyComponentTemplate',
|
||||
},
|
||||
description: 'set number of shards to one',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
version: 1,
|
||||
_meta: {
|
||||
serialization: {
|
||||
id: 10,
|
||||
class: 'MyComponentTemplate',
|
||||
},
|
||||
description: 'set number of shards to one',
|
||||
},
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
properties: {
|
||||
host_name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
created_at: {
|
||||
type: 'date',
|
||||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
ComponentTemplateFromEs,
|
||||
ComponentTemplateDeserialized,
|
||||
ComponentTemplateListItem,
|
||||
ComponentTemplateSerialized,
|
||||
} from '../types';
|
||||
|
||||
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
|
||||
|
@ -84,3 +85,15 @@ export function deserializeComponenTemplateList(
|
|||
|
||||
return componentTemplateListItem;
|
||||
}
|
||||
|
||||
export function serializeComponentTemplate(
|
||||
componentTemplateDeserialized: ComponentTemplateDeserialized
|
||||
): ComponentTemplateSerialized {
|
||||
const { version, template, _meta } = componentTemplateDeserialized;
|
||||
|
||||
return {
|
||||
version,
|
||||
template,
|
||||
_meta,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,4 +20,5 @@ export { getTemplateParameter } from './utils';
|
|||
export {
|
||||
deserializeComponentTemplate,
|
||||
deserializeComponenTemplateList,
|
||||
serializeComponentTemplate,
|
||||
} from './component_template_serialization';
|
||||
|
|
|
@ -16,6 +16,11 @@ import { TemplateClone } from './sections/template_clone';
|
|||
import { TemplateEdit } from './sections/template_edit';
|
||||
|
||||
import { useServices } from './app_context';
|
||||
import {
|
||||
ComponentTemplateCreate,
|
||||
ComponentTemplateEdit,
|
||||
ComponentTemplateClone,
|
||||
} from './components';
|
||||
|
||||
export const App = ({ history }: { history: ScopedHistory }) => {
|
||||
const { uiMetricService } = useServices();
|
||||
|
@ -34,6 +39,13 @@ export const AppWithoutRouter = () => (
|
|||
<Route exact path="/create_template" component={TemplateCreate} />
|
||||
<Route exact path="/clone_template/:name*" component={TemplateClone} />
|
||||
<Route exact path="/edit_template/:name*" component={TemplateEdit} />
|
||||
<Route exact path="/create_component_template" component={ComponentTemplateCreate} />
|
||||
<Route
|
||||
exact
|
||||
path="/create_component_template/:sourceComponentTemplateName"
|
||||
component={ComponentTemplateClone}
|
||||
/>
|
||||
<Route exact path="/edit_component_template/:name*" component={ComponentTemplateEdit} />
|
||||
<Route path={`/:section(${homeSections.join('|')})`} component={IndexManagementHome} />
|
||||
<Redirect from={`/`} to={`/indices`} />
|
||||
</Switch>
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { ScopedHistory } from 'kibana/public';
|
||||
import { ManagementAppMountParams } from 'src/plugins/management/public';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
|
||||
import { CoreStart } from '../../../../../src/core/public';
|
||||
|
||||
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
||||
import { IndexMgmtMetricsType } from '../types';
|
||||
import { UiMetricService, NotificationService, HttpService } from './services';
|
||||
|
@ -32,6 +33,7 @@ export interface AppDependencies {
|
|||
notificationService: NotificationService;
|
||||
};
|
||||
history: ScopedHistory;
|
||||
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||
}
|
||||
|
||||
export const AppContextProvider = ({
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment } from './helpers';
|
||||
import { setup, ComponentTemplateCreateTestBed } from './helpers/component_template_create.helpers';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
|
||||
return {
|
||||
...original,
|
||||
// Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
|
||||
// which does not produce a valid component wrapper
|
||||
EuiComboBox: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockComboBox"
|
||||
onChange={(syntheticEvent: any) => {
|
||||
props.onChange([syntheticEvent['0']]);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
// Mocking EuiCodeEditor, which uses React Ace under the hood
|
||||
EuiCodeEditor: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockCodeEditor"
|
||||
onChange={(syntheticEvent: any) => {
|
||||
props.onChange(syntheticEvent.jsonString);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('<ComponentTemplateCreate />', () => {
|
||||
let testBed: ComponentTemplateCreateTestBed;
|
||||
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('On component mount', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('should set the correct page header', async () => {
|
||||
const { exists, find } = testBed;
|
||||
|
||||
// Verify page title
|
||||
expect(exists('pageTitle')).toBe(true);
|
||||
expect(find('pageTitle').text()).toEqual('Create component template');
|
||||
|
||||
// Verify documentation link
|
||||
expect(exists('documentationLink')).toBe(true);
|
||||
expect(find('documentationLink').text()).toBe('Component Templates docs');
|
||||
});
|
||||
|
||||
describe('Step: Logistics', () => {
|
||||
test('should toggle the metadata field', async () => {
|
||||
const { exists, component, actions } = testBed;
|
||||
|
||||
// Meta editor should be hidden by default
|
||||
// Since the editor itself is mocked, we checked for the mocked element
|
||||
expect(exists('mockCodeEditor')).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
actions.toggleMetaSwitch();
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(exists('mockCodeEditor')).toBe(true);
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
test('should require a name', async () => {
|
||||
const { form, actions, component, find } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
// Submit logistics step without any values
|
||||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
// Verify name is required
|
||||
expect(form.getErrorsMessages()).toEqual(['A component template name is required.']);
|
||||
expect(find('nextButton').props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step: Review and submit', () => {
|
||||
const COMPONENT_TEMPLATE_NAME = 'comp-1';
|
||||
const SETTINGS = { number_of_shards: 1 };
|
||||
const ALIASES = { my_alias: {} };
|
||||
|
||||
const BOOLEAN_MAPPING_FIELD = {
|
||||
name: 'boolean_datatype',
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
const { actions, component } = testBed;
|
||||
|
||||
component.update();
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepLogistics({ name: COMPONENT_TEMPLATE_NAME });
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepSettings(SETTINGS);
|
||||
|
||||
// Complete step 3 (mappings)
|
||||
await actions.completeStepMappings([BOOLEAN_MAPPING_FIELD]);
|
||||
|
||||
// Complete step 4 (aliases)
|
||||
await actions.completeStepAliases(ALIASES);
|
||||
});
|
||||
|
||||
test('should render the review content', () => {
|
||||
const { find, exists, actions } = testBed;
|
||||
// Verify page header
|
||||
expect(exists('stepReview')).toBe(true);
|
||||
expect(find('stepReview.title').text()).toEqual(
|
||||
`Review details for '${COMPONENT_TEMPLATE_NAME}'`
|
||||
);
|
||||
|
||||
// Verify 2 tabs exist
|
||||
expect(find('stepReview.content').find('.euiTab').length).toBe(2);
|
||||
expect(
|
||||
find('stepReview.content')
|
||||
.find('.euiTab')
|
||||
.map((t) => t.text())
|
||||
).toEqual(['Summary', 'Request']);
|
||||
|
||||
// Summary tab should render by default
|
||||
expect(exists('stepReview.summaryTab')).toBe(true);
|
||||
expect(exists('stepReview.requestTab')).toBe(false);
|
||||
|
||||
// Navigate to request tab and verify content
|
||||
actions.selectReviewTab('request');
|
||||
|
||||
expect(exists('stepReview.summaryTab')).toBe(false);
|
||||
expect(exists('stepReview.requestTab')).toBe(true);
|
||||
});
|
||||
|
||||
test('should send the correct payload when submitting the form', async () => {
|
||||
const { actions, component } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const expected = {
|
||||
name: COMPONENT_TEMPLATE_NAME,
|
||||
template: {
|
||||
settings: SETTINGS,
|
||||
mappings: {
|
||||
_source: {},
|
||||
_meta: {},
|
||||
properties: {
|
||||
[BOOLEAN_MAPPING_FIELD.name]: {
|
||||
type: BOOLEAN_MAPPING_FIELD.type,
|
||||
},
|
||||
},
|
||||
},
|
||||
aliases: ALIASES,
|
||||
},
|
||||
_kbnMeta: { usedBy: [] },
|
||||
};
|
||||
|
||||
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should surface API errors if the request is unsuccessful', async () => {
|
||||
const { component, actions, find, exists } = testBed;
|
||||
|
||||
const error = {
|
||||
status: 409,
|
||||
error: 'Conflict',
|
||||
message: `There is already a template with name '${COMPONENT_TEMPLATE_NAME}'`,
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setCreateComponentTemplateResponse(undefined, { body: error });
|
||||
|
||||
await act(async () => {
|
||||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(exists('saveComponentTemplateError')).toBe(true);
|
||||
expect(find('saveComponentTemplateError').text()).toContain(error.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment } from './helpers';
|
||||
import { setup, ComponentTemplateEditTestBed } from './helpers/component_template_edit.helpers';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
|
||||
return {
|
||||
...original,
|
||||
// Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
|
||||
// which does not produce a valid component wrapper
|
||||
EuiComboBox: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockComboBox"
|
||||
onChange={(syntheticEvent: any) => {
|
||||
props.onChange([syntheticEvent['0']]);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
// Mocking EuiCodeEditor, which uses React Ace under the hood
|
||||
EuiCodeEditor: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockCodeEditor"
|
||||
onChange={(syntheticEvent: any) => {
|
||||
props.onChange(syntheticEvent.jsonString);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('<ComponentTemplateEdit />', () => {
|
||||
let testBed: ComponentTemplateEditTestBed;
|
||||
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
const COMPONENT_TEMPLATE_NAME = 'comp-1';
|
||||
const COMPONENT_TEMPLATE_TO_EDIT = {
|
||||
name: COMPONENT_TEMPLATE_NAME,
|
||||
template: {
|
||||
settings: { number_of_shards: 1 },
|
||||
},
|
||||
_kbnMeta: { usedBy: [] },
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE_TO_EDIT);
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('should set the correct page title', () => {
|
||||
const { exists, find } = testBed;
|
||||
|
||||
expect(exists('pageTitle')).toBe(true);
|
||||
expect(find('pageTitle').text()).toEqual(
|
||||
`Edit component template '${COMPONENT_TEMPLATE_NAME}'`
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the name field to read only', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
const nameInput = find('nameField.input');
|
||||
expect(nameInput.props().disabled).toEqual(true);
|
||||
});
|
||||
|
||||
describe('form payload', () => {
|
||||
it('should send the correct payload with changed values', async () => {
|
||||
const { actions, component, form } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
form.setInputValue('versionField.input', '1');
|
||||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
await actions.completeStepSettings();
|
||||
await actions.completeStepMappings();
|
||||
await actions.completeStepAliases();
|
||||
|
||||
await act(async () => {
|
||||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const expected = {
|
||||
version: 1,
|
||||
...COMPONENT_TEMPLATE_TO_EDIT,
|
||||
template: {
|
||||
...COMPONENT_TEMPLATE_TO_EDIT.template,
|
||||
mappings: {
|
||||
_meta: {},
|
||||
_source: {},
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../../../../../common';
|
||||
import { ComponentTemplateCreate } from '../../../component_template_wizard';
|
||||
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
import {
|
||||
getFormActions,
|
||||
ComponentTemplateFormTestSubjects,
|
||||
} from './component_template_form.helpers';
|
||||
|
||||
export type ComponentTemplateCreateTestBed = TestBed<ComponentTemplateFormTestSubjects> & {
|
||||
actions: ReturnType<typeof getFormActions>;
|
||||
};
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}/create_component_template`],
|
||||
componentRoutePath: `${BASE_PATH}/create_component_template`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateCreate), testBedConfig);
|
||||
|
||||
export const setup = async (): Promise<ComponentTemplateCreateTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: getFormActions(testBed),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../../../../../common';
|
||||
import { ComponentTemplateEdit } from '../../../component_template_wizard';
|
||||
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
import {
|
||||
getFormActions,
|
||||
ComponentTemplateFormTestSubjects,
|
||||
} from './component_template_form.helpers';
|
||||
|
||||
export type ComponentTemplateEditTestBed = TestBed<ComponentTemplateFormTestSubjects> & {
|
||||
actions: ReturnType<typeof getFormActions>;
|
||||
};
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}/edit_component_template/comp-1`],
|
||||
componentRoutePath: `${BASE_PATH}/edit_component_template/:name`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateEdit), testBedConfig);
|
||||
|
||||
export const setup = async (): Promise<ComponentTemplateEditTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: getFormActions(testBed),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { TestBed } from '../../../../../../../../../test_utils';
|
||||
|
||||
interface MappingField {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const getFormActions = (testBed: TestBed) => {
|
||||
// User actions
|
||||
const toggleVersionSwitch = () => {
|
||||
testBed.form.toggleEuiSwitch('versionToggle');
|
||||
};
|
||||
|
||||
const toggleMetaSwitch = () => {
|
||||
testBed.form.toggleEuiSwitch('metaToggle');
|
||||
};
|
||||
|
||||
const clickNextButton = () => {
|
||||
testBed.find('nextButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickBackButton = () => {
|
||||
testBed.find('backButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickSubmitButton = () => {
|
||||
testBed.find('submitButton').simulate('click');
|
||||
};
|
||||
|
||||
const setMetaField = (jsonString: string) => {
|
||||
testBed.find('mockCodeEditor').simulate('change', {
|
||||
jsonString,
|
||||
});
|
||||
};
|
||||
|
||||
const selectReviewTab = (tab: 'summary' | 'request') => {
|
||||
const tabs = ['summary', 'request'];
|
||||
|
||||
testBed.find('stepReview.content').find('.euiTab').at(tabs.indexOf(tab)).simulate('click');
|
||||
};
|
||||
|
||||
const completeStepLogistics = async ({ name }: { name: string }) => {
|
||||
const { form, component } = testBed;
|
||||
// Add name field
|
||||
form.setInputValue('nameField.input', name);
|
||||
|
||||
await act(async () => {
|
||||
clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const completeStepSettings = async (settings?: { [key: string]: any }) => {
|
||||
const { find, component } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
if (settings) {
|
||||
find('mockCodeEditor').simulate('change', {
|
||||
jsonString: JSON.stringify(settings),
|
||||
}); // Using mocked EuiCodeEditor
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const addMappingField = async (name: string, type: string) => {
|
||||
const { find, form, component } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
form.setInputValue('nameParameterInput', name);
|
||||
find('createFieldForm.mockComboBox').simulate('change', [
|
||||
{
|
||||
label: type,
|
||||
value: type,
|
||||
},
|
||||
]);
|
||||
find('createFieldForm.addButton').simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const completeStepMappings = async (mappingFields?: MappingField[]) => {
|
||||
const { component } = testBed;
|
||||
|
||||
if (mappingFields) {
|
||||
for (const field of mappingFields) {
|
||||
const { name, type } = field;
|
||||
await addMappingField(name, type);
|
||||
}
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const completeStepAliases = async (aliases?: { [key: string]: any }) => {
|
||||
const { find, component } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
if (aliases) {
|
||||
find('mockCodeEditor').simulate('change', {
|
||||
jsonString: JSON.stringify(aliases),
|
||||
}); // Using mocked EuiCodeEditor
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
return {
|
||||
toggleVersionSwitch,
|
||||
toggleMetaSwitch,
|
||||
clickNextButton,
|
||||
clickBackButton,
|
||||
clickSubmitButton,
|
||||
setMetaField,
|
||||
selectReviewTab,
|
||||
completeStepSettings,
|
||||
completeStepAliases,
|
||||
completeStepLogistics,
|
||||
completeStepMappings,
|
||||
};
|
||||
};
|
||||
|
||||
export type ComponentTemplateFormTestSubjects =
|
||||
| 'backButton'
|
||||
| 'documentationLink'
|
||||
| 'metaToggle'
|
||||
| 'metaEditor'
|
||||
| 'mockCodeEditor'
|
||||
| 'nameField.input'
|
||||
| 'nextButton'
|
||||
| 'pageTitle'
|
||||
| 'saveComponentTemplateError'
|
||||
| 'submitButton'
|
||||
| 'stepReview'
|
||||
| 'stepReview.title'
|
||||
| 'stepReview.content'
|
||||
| 'stepReview.summaryTab'
|
||||
| 'stepReview.requestTab'
|
||||
| 'versionField'
|
||||
| 'versionField.input';
|
|
@ -5,7 +5,11 @@
|
|||
*/
|
||||
|
||||
import sinon, { SinonFakeServer } from 'sinon';
|
||||
import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports';
|
||||
import {
|
||||
ComponentTemplateListItem,
|
||||
ComponentTemplateDeserialized,
|
||||
ComponentTemplateSerialized,
|
||||
} from '../../../shared_imports';
|
||||
import { API_BASE_PATH } from './constants';
|
||||
|
||||
// Register helpers to mock HTTP Requests
|
||||
|
@ -46,10 +50,25 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
|||
]);
|
||||
};
|
||||
|
||||
const setCreateComponentTemplateResponse = (
|
||||
response?: ComponentTemplateSerialized,
|
||||
error?: any
|
||||
) => {
|
||||
const status = error ? error.body.status || 400 : 200;
|
||||
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
|
||||
|
||||
server.respondWith('POST', `${API_BASE_PATH}/component_templates`, [
|
||||
status,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
body,
|
||||
]);
|
||||
};
|
||||
|
||||
return {
|
||||
setLoadComponentTemplatesResponse,
|
||||
setDeleteComponentTemplateResponse,
|
||||
setLoadComponentTemplateResponse,
|
||||
setCreateComponentTemplateResponse,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ const appDependencies = {
|
|||
trackMetric: () => {},
|
||||
docLinks: docLinksServiceMock.createStartContract(),
|
||||
toasts: notificationServiceMock.createSetupContract().toasts,
|
||||
setBreadcrumbs: () => {},
|
||||
};
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { useComponentTemplatesContext } from '../component_templates_context';
|
|||
import { TabSummary } from './tab_summary';
|
||||
import { ComponentTemplateTabs, TabType } from './tabs';
|
||||
import { ManageButton, ManageAction } from './manage_button';
|
||||
import { attemptToDecodeURI } from '../lib';
|
||||
|
||||
interface Props {
|
||||
componentTemplateName: string;
|
||||
|
@ -39,8 +40,10 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent<Props> = ({
|
|||
}) => {
|
||||
const { api } = useComponentTemplatesContext();
|
||||
|
||||
const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName);
|
||||
|
||||
const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate(
|
||||
componentTemplateName
|
||||
decodedComponentTemplateName
|
||||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabType>('summary');
|
||||
|
@ -108,7 +111,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent<Props> = ({
|
|||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="componentTemplateDetailsFlyoutTitle" data-test-subj="title">
|
||||
{componentTemplateName}
|
||||
{decodedComponentTemplateName}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
|
|
@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent<Props> = ({ componentTemplateDe
|
|||
)}
|
||||
|
||||
{/* Version (optional) */}
|
||||
{version && (
|
||||
{typeof version !== 'undefined' && (
|
||||
<>
|
||||
<EuiDescriptionListTitle data-test-subj="versionTitle">
|
||||
<FormattedMessage
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ScopedHistory } from 'kibana/public';
|
|||
|
||||
import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports';
|
||||
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
|
||||
import { attemptToDecodeURI } from '../lib';
|
||||
import { useComponentTemplatesContext } from '../component_templates_context';
|
||||
import { ComponentTemplateDetailsFlyout } from '../component_template_details';
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
|
@ -34,8 +35,22 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
|||
|
||||
const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState<string[]>([]);
|
||||
|
||||
const goToList = () => {
|
||||
return history.push('component_templates');
|
||||
const goToComponentTemplateList = () => {
|
||||
return history.push({
|
||||
pathname: 'component_templates',
|
||||
});
|
||||
};
|
||||
|
||||
const goToEditComponentTemplate = (name: string) => {
|
||||
return history.push({
|
||||
pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`),
|
||||
});
|
||||
};
|
||||
|
||||
const goToCloneComponentTemplate = (name: string) => {
|
||||
return history.push({
|
||||
pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`),
|
||||
});
|
||||
};
|
||||
|
||||
// Track component loaded
|
||||
|
@ -60,11 +75,13 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
|||
componentTemplates={data}
|
||||
onReloadClick={sendRequest}
|
||||
onDeleteClick={setComponentTemplatesToDelete}
|
||||
onEditClick={goToEditComponentTemplate}
|
||||
onCloneClick={goToCloneComponentTemplate}
|
||||
history={history as ScopedHistory}
|
||||
/>
|
||||
);
|
||||
} else if (data && data.length === 0) {
|
||||
content = <EmptyPrompt />;
|
||||
content = <EmptyPrompt history={history} />;
|
||||
} else if (error) {
|
||||
content = <LoadError onReloadClick={sendRequest} />;
|
||||
}
|
||||
|
@ -81,7 +98,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
|||
// refetch the component templates
|
||||
sendRequest();
|
||||
// go back to list view (if deleted from details flyout)
|
||||
goToList();
|
||||
goToComponentTemplateList();
|
||||
}
|
||||
setComponentTemplatesToDelete([]);
|
||||
}}
|
||||
|
@ -92,9 +109,25 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
|||
{/* details flyout */}
|
||||
{componentTemplateName && (
|
||||
<ComponentTemplateDetailsFlyout
|
||||
onClose={goToList}
|
||||
onClose={goToComponentTemplateList}
|
||||
componentTemplateName={componentTemplateName}
|
||||
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',
|
||||
|
@ -104,7 +137,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
|||
details._kbnMeta.usedBy.length > 0,
|
||||
closePopoverOnClick: true,
|
||||
handleActionClick: () => {
|
||||
setComponentTemplatesToDelete([componentTemplateName]);
|
||||
setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]);
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -6,11 +6,17 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui';
|
||||
|
||||
import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { useComponentTemplatesContext } from '../component_templates_context';
|
||||
|
||||
export const EmptyPrompt: FunctionComponent = () => {
|
||||
interface Props {
|
||||
history: RouteComponentProps['history'];
|
||||
}
|
||||
|
||||
export const EmptyPrompt: FunctionComponent<Props> = ({ history }) => {
|
||||
const { documentation } = useComponentTemplatesContext();
|
||||
|
||||
return (
|
||||
|
@ -38,6 +44,17 @@ export const EmptyPrompt: FunctionComponent = () => {
|
|||
</EuiLink>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
{...reactRouterNavigate(history, '/create_component_template')}
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptButtonLabel', {
|
||||
defaultMessage: 'Create a component template',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,8 @@ export interface Props {
|
|||
componentTemplates: ComponentTemplateListItem[];
|
||||
onReloadClick: () => void;
|
||||
onDeleteClick: (componentTemplateName: string[]) => void;
|
||||
onEditClick: (componentTemplateName: string) => void;
|
||||
onCloneClick: (componentTemplateName: string) => void;
|
||||
history: ScopedHistory;
|
||||
}
|
||||
|
||||
|
@ -32,6 +34,8 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
|||
componentTemplates,
|
||||
onReloadClick,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
onCloneClick,
|
||||
history,
|
||||
}) => {
|
||||
const { trackMetric } = useComponentTemplatesContext();
|
||||
|
@ -85,6 +89,17 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
|||
defaultMessage: 'Reload',
|
||||
})}
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createPipelineButton"
|
||||
key="createPipelineButton"
|
||||
{...reactRouterNavigate(history, '/create_component_template')}
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.componentTemplatesList.table.createButtonLabel', {
|
||||
defaultMessage: 'Create a component template',
|
||||
})}
|
||||
</EuiButton>,
|
||||
],
|
||||
box: {
|
||||
incremental: true,
|
||||
|
@ -135,7 +150,7 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
|||
{...reactRouterNavigate(
|
||||
history,
|
||||
{
|
||||
pathname: `/component_templates/${name}`,
|
||||
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
|
||||
},
|
||||
() => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS)
|
||||
)}
|
||||
|
@ -204,8 +219,37 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
|||
),
|
||||
actions: [
|
||||
{
|
||||
'data-test-subj': 'deleteComponentTemplateButton',
|
||||
name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionEditText', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplatesList.table.actionEditDecription',
|
||||
{
|
||||
defaultMessage: 'Edit this component template',
|
||||
}
|
||||
),
|
||||
onClick: ({ name }: ComponentTemplateListItem) => onEditClick(name),
|
||||
isPrimary: true,
|
||||
icon: 'pencil',
|
||||
type: 'icon',
|
||||
'data-test-subj': 'editComponentTemplateButton',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionCloneText', {
|
||||
defaultMessage: 'Clone',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplatesList.table.actionCloneDecription',
|
||||
{
|
||||
defaultMessage: 'Clone this component template',
|
||||
}
|
||||
),
|
||||
onClick: ({ name }: ComponentTemplateListItem) => onCloneClick(name),
|
||||
icon: 'copy',
|
||||
type: 'icon',
|
||||
'data-test-subj': 'cloneComponentTemplateButton',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.deleteActionLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
|
@ -213,11 +257,13 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
|||
'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription',
|
||||
{ defaultMessage: 'Delete this component template' }
|
||||
),
|
||||
onClick: ({ name }) => onDeleteClick([name]),
|
||||
enabled: ({ usedBy }) => usedBy.length === 0,
|
||||
isPrimary: true,
|
||||
type: 'icon',
|
||||
icon: 'trash',
|
||||
color: 'danger',
|
||||
onClick: ({ name }) => onDeleteClick([name]),
|
||||
enabled: ({ usedBy }) => usedBy.length === 0,
|
||||
'data-test-subj': 'deleteComponentTemplateButton',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { SectionLoading } from '../../shared_imports';
|
||||
import { useComponentTemplatesContext } from '../../component_templates_context';
|
||||
import { attemptToDecodeURI } from '../../lib';
|
||||
import { ComponentTemplateCreate } from '../component_template_create';
|
||||
|
||||
export interface Params {
|
||||
sourceComponentTemplateName: string;
|
||||
}
|
||||
|
||||
export const ComponentTemplateClone: FunctionComponent<RouteComponentProps<Params>> = (props) => {
|
||||
const { sourceComponentTemplateName } = props.match.params;
|
||||
const decodedSourceName = attemptToDecodeURI(sourceComponentTemplateName);
|
||||
|
||||
const { toasts, api } = useComponentTemplatesContext();
|
||||
|
||||
const { error, data: componentTemplateToClone, isLoading } = api.useLoadComponentTemplate(
|
||||
decodedSourceName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error && !isLoading) {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', {
|
||||
defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`,
|
||||
values: { sourceComponentTemplateName },
|
||||
}),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error, isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateEdit.loadingDescription"
|
||||
defaultMessage="Loading component template…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else {
|
||||
// We still show the create form (unpopulated) even if we were not able to load the
|
||||
// selected component template data.
|
||||
const sourceComponentTemplate = componentTemplateToClone
|
||||
? { ...componentTemplateToClone, name: `${componentTemplateToClone.name}-copy` }
|
||||
: undefined;
|
||||
|
||||
return <ComponentTemplateCreate {...props} sourceComponentTemplate={sourceComponentTemplate} />;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ComponentTemplateClone } from './component_template_clone';
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { ComponentTemplateDeserialized } from '../../shared_imports';
|
||||
import { useComponentTemplatesContext } from '../../component_templates_context';
|
||||
import { ComponentTemplateForm } from '../component_template_form';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* This value may be passed in to prepopulate the creation form (e.g., to clone a template)
|
||||
*/
|
||||
sourceComponentTemplate?: any;
|
||||
}
|
||||
|
||||
export const ComponentTemplateCreate: React.FunctionComponent<RouteComponentProps & Props> = ({
|
||||
history,
|
||||
sourceComponentTemplate,
|
||||
}) => {
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
const { api, breadcrumbs } = useComponentTemplatesContext();
|
||||
|
||||
const onSave = async (componentTemplate: ComponentTemplateDeserialized) => {
|
||||
const { name } = componentTemplate;
|
||||
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
|
||||
const { error } = await api.createComponentTemplate(componentTemplate);
|
||||
|
||||
setIsSaving(false);
|
||||
|
||||
if (error) {
|
||||
setSaveError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
history.push({
|
||||
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
|
||||
});
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
breadcrumbs.setCreateBreadcrumbs();
|
||||
}, [breadcrumbs]);
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.createComponentTemplate.pageTitle"
|
||||
defaultMessage="Create component template"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<ComponentTemplateForm
|
||||
defaultValue={sourceComponentTemplate}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
clearSaveError={clearSaveError}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ComponentTemplateCreate } from './component_template_create';
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import { useComponentTemplatesContext } from '../../component_templates_context';
|
||||
import { ComponentTemplateDeserialized, SectionLoading } from '../../shared_imports';
|
||||
import { attemptToDecodeURI } from '../../lib';
|
||||
import { ComponentTemplateForm } from '../component_template_form';
|
||||
|
||||
interface MatchParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const ComponentTemplateEdit: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { name },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const { api, breadcrumbs } = useComponentTemplatesContext();
|
||||
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
const decodedName = attemptToDecodeURI(name);
|
||||
|
||||
const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(decodedName);
|
||||
|
||||
useEffect(() => {
|
||||
breadcrumbs.setEditBreadcrumbs();
|
||||
}, [breadcrumbs]);
|
||||
|
||||
const onSave = async (updatedComponentTemplate: ComponentTemplateDeserialized) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
|
||||
const { error: saveErrorObject } = await api.updateComponentTemplate(updatedComponentTemplate);
|
||||
|
||||
setIsSaving(false);
|
||||
|
||||
if (saveErrorObject) {
|
||||
setSaveError(saveErrorObject);
|
||||
return;
|
||||
}
|
||||
|
||||
history.push({
|
||||
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
|
||||
});
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
let content;
|
||||
|
||||
if (isLoading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateEdit.loadingDescription"
|
||||
defaultMessage="Loading component template…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateEdit.loadComponentTemplateError"
|
||||
defaultMessage="Error loading component template"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
data-test-subj="loadComponentTemplateError"
|
||||
>
|
||||
<div>{error.message}</div>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
} else if (componentTemplate) {
|
||||
content = (
|
||||
<ComponentTemplateForm
|
||||
defaultValue={componentTemplate}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
clearSaveError={clearSaveError}
|
||||
isEditing={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateEdit.editPageTitle"
|
||||
defaultMessage="Edit component template '{name}'"
|
||||
values={{ name: decodedName }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{content}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ComponentTemplateEdit } from './component_template_edit';
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiSpacer, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
serializers,
|
||||
Forms,
|
||||
ComponentTemplateDeserialized,
|
||||
CommonWizardSteps,
|
||||
StepSettingsContainer,
|
||||
StepMappingsContainer,
|
||||
StepAliasesContainer,
|
||||
} from '../../shared_imports';
|
||||
import { useComponentTemplatesContext } from '../../component_templates_context';
|
||||
import { StepLogisticsContainer, StepReviewContainer } from './steps';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
const { FormWizard, FormWizardStep } = Forms;
|
||||
|
||||
interface Props {
|
||||
onSave: (componentTemplate: ComponentTemplateDeserialized) => void;
|
||||
clearSaveError: () => void;
|
||||
isSaving: boolean;
|
||||
saveError: any;
|
||||
defaultValue?: ComponentTemplateDeserialized;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export interface WizardContent extends CommonWizardSteps {
|
||||
logistics: Omit<ComponentTemplateDeserialized, '_kbnMeta' | 'template'>;
|
||||
}
|
||||
|
||||
export type WizardSection = keyof WizardContent | 'review';
|
||||
|
||||
const wizardSections: { [id: string]: { id: WizardSection; label: string } } = {
|
||||
logistics: {
|
||||
id: 'logistics',
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.logisticsStepName', {
|
||||
defaultMessage: 'Logistics',
|
||||
}),
|
||||
},
|
||||
settings: {
|
||||
id: 'settings',
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.settingsStepName', {
|
||||
defaultMessage: 'Index settings',
|
||||
}),
|
||||
},
|
||||
mappings: {
|
||||
id: 'mappings',
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.mappingsStepName', {
|
||||
defaultMessage: 'Mappings',
|
||||
}),
|
||||
},
|
||||
aliases: {
|
||||
id: 'aliases',
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.aliasesStepName', {
|
||||
defaultMessage: 'Aliases',
|
||||
}),
|
||||
},
|
||||
review: {
|
||||
id: 'review',
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.summaryStepName', {
|
||||
defaultMessage: 'Review',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const ComponentTemplateForm = ({
|
||||
defaultValue = {
|
||||
name: '',
|
||||
template: {
|
||||
settings: {},
|
||||
mappings: {},
|
||||
aliases: {},
|
||||
},
|
||||
_meta: {},
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
},
|
||||
isEditing,
|
||||
isSaving,
|
||||
saveError,
|
||||
clearSaveError,
|
||||
onSave,
|
||||
}: Props) => {
|
||||
const {
|
||||
template: { settings, mappings, aliases },
|
||||
...logistics
|
||||
} = defaultValue;
|
||||
|
||||
const { documentation } = useComponentTemplatesContext();
|
||||
|
||||
const wizardDefaultValue: WizardContent = {
|
||||
logistics,
|
||||
settings,
|
||||
mappings,
|
||||
aliases,
|
||||
};
|
||||
|
||||
const i18nTexts = {
|
||||
save: isEditing ? (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.saveButtonLabel"
|
||||
defaultMessage="Save component template"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.createButtonLabel"
|
||||
defaultMessage="Create component template"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const apiError = saveError ? (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.saveTemplateError"
|
||||
defaultMessage="Unable to create component template"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
data-test-subj="saveComponentTemplateError"
|
||||
>
|
||||
<div>{saveError.message || saveError.statusText}</div>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const buildComponentTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => (
|
||||
wizardData: WizardContent
|
||||
): ComponentTemplateDeserialized => {
|
||||
const componentTemplate = {
|
||||
...initialTemplate,
|
||||
name: wizardData.logistics.name,
|
||||
version: wizardData.logistics.version,
|
||||
_meta: wizardData.logistics._meta,
|
||||
template: {
|
||||
settings: wizardData.settings,
|
||||
mappings: wizardData.mappings,
|
||||
aliases: wizardData.aliases,
|
||||
},
|
||||
};
|
||||
return componentTemplate;
|
||||
};
|
||||
|
||||
const onSaveComponentTemplate = useCallback(
|
||||
async (wizardData: WizardContent) => {
|
||||
const componentTemplate = buildComponentTemplateObject(defaultValue)(wizardData);
|
||||
|
||||
// This will strip an empty string if "version" is not set, as well as an empty "_meta" object
|
||||
onSave(
|
||||
stripEmptyFields(componentTemplate, {
|
||||
types: ['string', 'object'],
|
||||
}) as ComponentTemplateDeserialized
|
||||
);
|
||||
|
||||
clearSaveError();
|
||||
},
|
||||
[defaultValue, onSave, clearSaveError]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormWizard<WizardContent>
|
||||
defaultValue={wizardDefaultValue}
|
||||
onSave={onSaveComponentTemplate}
|
||||
isEditing={isEditing}
|
||||
isSaving={isSaving}
|
||||
apiError={apiError}
|
||||
texts={i18nTexts}
|
||||
>
|
||||
<FormWizardStep
|
||||
id={wizardSections.logistics.id}
|
||||
label={wizardSections.logistics.label}
|
||||
isRequired
|
||||
>
|
||||
<StepLogisticsContainer isEditing={isEditing} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.settings.id} label={wizardSections.settings.label}>
|
||||
<StepSettingsContainer esDocsBase={documentation.esDocsBase} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.mappings.id} label={wizardSections.mappings.label}>
|
||||
<StepMappingsContainer esDocsBase={documentation.esDocsBase} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.aliases.id} label={wizardSections.aliases.label}>
|
||||
<StepAliasesContainer esDocsBase={documentation.esDocsBase} />
|
||||
</FormWizardStep>
|
||||
|
||||
<FormWizardStep id={wizardSections.review.id} label={wizardSections.review.label}>
|
||||
<StepReviewContainer
|
||||
getComponentTemplateData={buildComponentTemplateObject(defaultValue)}
|
||||
/>
|
||||
</FormWizardStep>
|
||||
</FormWizard>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ComponentTemplateForm } from './component_template_form';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { StepLogisticsContainer } from './step_logistics_container';
|
||||
export { StepReviewContainer } from './step_review_container';
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
useForm,
|
||||
Form,
|
||||
getUseField,
|
||||
getFormRow,
|
||||
Field,
|
||||
Forms,
|
||||
JsonEditorField,
|
||||
} from '../../../shared_imports';
|
||||
import { useComponentTemplatesContext } from '../../../component_templates_context';
|
||||
import { logisticsFormSchema } from './step_logistics_schema';
|
||||
|
||||
const UseField = getUseField({ component: Field });
|
||||
const FormRow = getFormRow({ titleTag: 'h3' });
|
||||
|
||||
interface Props {
|
||||
defaultValue: { [key: string]: any };
|
||||
onChange: (content: Forms.Content) => void;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
||||
({ defaultValue, isEditing, onChange }) => {
|
||||
const { form } = useForm({
|
||||
schema: logisticsFormSchema,
|
||||
defaultValue,
|
||||
options: { stripEmptyFields: false },
|
||||
});
|
||||
|
||||
const { documentation } = useComponentTemplatesContext();
|
||||
|
||||
const [isMetaVisible, setIsMetaVisible] = useState<boolean>(
|
||||
Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length)
|
||||
);
|
||||
|
||||
const validate = async () => {
|
||||
return (await form.submit()).isValid;
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
onChange({
|
||||
isValid,
|
||||
validate,
|
||||
getData: data.format,
|
||||
});
|
||||
});
|
||||
return subscription.unsubscribe;
|
||||
}, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<Form form={form} data-test-subj="stepLogistics">
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.stepTitle"
|
||||
defaultMessage="Logistics"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentation.componentTemplates}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.docsButtonLabel"
|
||||
defaultMessage="Component Templates docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{/* Name field */}
|
||||
<FormRow
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.nameTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.nameDescription"
|
||||
defaultMessage="A unique identifier for this component template."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<UseField
|
||||
path="name"
|
||||
componentProps={{
|
||||
['data-test-subj']: 'nameField',
|
||||
euiFieldProps: { disabled: isEditing },
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
{/* version field */}
|
||||
<FormRow
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.versionTitle"
|
||||
defaultMessage="Version"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.versionDescription"
|
||||
defaultMessage="A number that identifies the component template to external management systems."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<UseField
|
||||
path="version"
|
||||
componentProps={{
|
||||
['data-test-subj']: 'versionField',
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
{/* _meta field */}
|
||||
<FormRow
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.metaTitle"
|
||||
defaultMessage="Metadata"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.metaDescription"
|
||||
defaultMessage="Arbitrary metadata that is stored in cluster state. {learnMoreLink}"
|
||||
values={{
|
||||
learnMoreLink: (
|
||||
<EuiLink
|
||||
href={documentation.componentTemplatesMetadata}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaDocumentionLink',
|
||||
{
|
||||
defaultMessage: 'Learn more',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.metadataDescription"
|
||||
defaultMessage="Add metadata"
|
||||
/>
|
||||
}
|
||||
checked={isMetaVisible}
|
||||
onChange={(e) => setIsMetaVisible(e.target.checked)}
|
||||
data-test-subj="metaToggle"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isMetaVisible ? (
|
||||
<UseField
|
||||
path="_meta"
|
||||
component={JsonEditorField}
|
||||
componentProps={{
|
||||
euiCodeEditorProps: {
|
||||
['data-test-subj']: 'metaEditor',
|
||||
height: '200px',
|
||||
'aria-label': i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Metadata JSON editor',
|
||||
}
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// <FormRow/> requires children or a field
|
||||
// For now, we return an empty <div> if the editor is not visible
|
||||
<div />
|
||||
)}
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { Forms } from '../../../shared_imports';
|
||||
import { WizardContent } from '../component_template_form';
|
||||
import { StepLogistics } from './step_logistics';
|
||||
|
||||
interface Props {
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export const StepLogisticsContainer = ({ isEditing = false }: Props) => {
|
||||
const { defaultValue, updateContent } = Forms.useContent<WizardContent, 'logistics'>('logistics');
|
||||
|
||||
return (
|
||||
<StepLogistics defaultValue={defaultValue} onChange={updateContent} isEditing={isEditing} />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiCode } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FIELD_TYPES, fieldValidators, fieldFormatters, FormSchema } from '../../../shared_imports';
|
||||
|
||||
const { emptyField, containsCharsField, isJsonField } = fieldValidators;
|
||||
const { toInt } = fieldFormatters;
|
||||
|
||||
const stringifyJson = (json: { [key: string]: any }): string =>
|
||||
Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}';
|
||||
|
||||
const parseJson = (jsonString: string): object => {
|
||||
let parsedJSON: any;
|
||||
|
||||
try {
|
||||
parsedJSON = JSON.parse(jsonString);
|
||||
} catch {
|
||||
parsedJSON = {};
|
||||
}
|
||||
|
||||
return parsedJSON;
|
||||
};
|
||||
|
||||
export const logisticsFormSchema: FormSchema = {
|
||||
name: {
|
||||
defaultValue: undefined,
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameFieldLabel', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
type: FIELD_TYPES.TEXT,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate('xpack.idxMgmt.componentTemplateForm.validation.nameRequiredError', {
|
||||
defaultMessage: 'A component template name is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: containsCharsField({
|
||||
chars: ' ',
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.nameSpacesError',
|
||||
{
|
||||
defaultMessage: 'Spaces are not allowed in a component template name.',
|
||||
}
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
version: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionFieldLabel', {
|
||||
defaultMessage: 'Version (optional)',
|
||||
}),
|
||||
formatters: [toInt],
|
||||
},
|
||||
_meta: {
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', {
|
||||
defaultMessage: 'Metadata (optional)',
|
||||
}),
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.metaHelpText"
|
||||
defaultMessage="Use JSON format: {code}"
|
||||
values={{
|
||||
code: <EuiCode>{JSON.stringify({ arbitrary_data: 'anything_goes' })}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
serializer: (value) => {
|
||||
const result = parseJson(value);
|
||||
// If an empty object was passed, strip out this value entirely.
|
||||
if (!Object.keys(result).length) {
|
||||
return undefined;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
deserializer: stringifyJson,
|
||||
validations: [
|
||||
{
|
||||
validator: isJsonField(
|
||||
i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.metaJsonError',
|
||||
{
|
||||
defaultMessage: 'The input is not valid.',
|
||||
}
|
||||
),
|
||||
{ allowEmptyString: true }
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiTitle,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiText,
|
||||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
ComponentTemplateDeserialized,
|
||||
serializers,
|
||||
serializeComponentTemplate,
|
||||
} from '../../../shared_imports';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
|
||||
const getDescriptionText = (data: any) => {
|
||||
const hasEntries = data && Object.entries(data).length > 0;
|
||||
|
||||
return hasEntries ? (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.summaryTab.yesDescriptionText"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.summaryTab.noDescriptionText"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
componentTemplate: ComponentTemplateDeserialized;
|
||||
}
|
||||
|
||||
export const StepReview: React.FunctionComponent<Props> = React.memo(({ componentTemplate }) => {
|
||||
const { name } = componentTemplate;
|
||||
|
||||
const serializedComponentTemplate = serializeComponentTemplate(
|
||||
stripEmptyFields(componentTemplate, {
|
||||
types: ['string', 'object'],
|
||||
}) as ComponentTemplateDeserialized
|
||||
);
|
||||
|
||||
const {
|
||||
template: {
|
||||
mappings: serializedMappings,
|
||||
settings: serializedSettings,
|
||||
aliases: serializedAliases,
|
||||
},
|
||||
_meta: serializedMeta,
|
||||
version: serializedVersion,
|
||||
} = serializedComponentTemplate;
|
||||
|
||||
const SummaryTab = () => (
|
||||
<div data-test-subj="summaryTab">
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
{/* Version */}
|
||||
{typeof serializedVersion !== 'undefined' && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepReview.summaryTab.versionLabel"
|
||||
defaultMessage="Version"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{serializedVersion}</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Index settings */}
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.summaryTab.settingsLabel"
|
||||
defaultMessage="Index settings"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{getDescriptionText(serializedSettings)}
|
||||
</EuiDescriptionListDescription>
|
||||
|
||||
{/* Mappings */}
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.summaryTab.mappingLabel"
|
||||
defaultMessage="Mappings"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{getDescriptionText(serializedMappings)}
|
||||
</EuiDescriptionListDescription>
|
||||
|
||||
{/* Aliases */}
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.summaryTab.aliasesLabel"
|
||||
defaultMessage="Aliases"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{getDescriptionText(serializedAliases)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
{/* Metadata */}
|
||||
{serializedMeta && (
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepReview.summaryTab.metaLabel"
|
||||
defaultMessage="Metadata"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiCodeBlock language="json">
|
||||
{JSON.stringify(serializedMeta, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RequestTab = () => {
|
||||
const endpoint = `PUT _component_template/${name || '<componentTemplateName>'}`;
|
||||
const templateString = JSON.stringify(serializedComponentTemplate, null, 2);
|
||||
const request = `${endpoint}\n${templateString}`;
|
||||
|
||||
// Beyond a certain point, highlighting the syntax will bog down performance to unacceptable
|
||||
// levels. This way we prevent that happening for very large requests.
|
||||
const language = request.length < 60000 ? 'json' : undefined;
|
||||
|
||||
return (
|
||||
<div data-test-subj="requestTab">
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.requestTab.descriptionText"
|
||||
defaultMessage="This request will create the following component template."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiCodeBlock language={language} isCopyable>
|
||||
{request}
|
||||
</EuiCodeBlock>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test-subj="stepReview">
|
||||
<EuiTitle>
|
||||
<h2 data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.stepTitle"
|
||||
defaultMessage="Review details for '{templateName}'"
|
||||
values={{ templateName: name }}
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiTabbedContent
|
||||
data-test-subj="content"
|
||||
tabs={[
|
||||
{
|
||||
id: 'summary',
|
||||
name: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepReview.summaryTabTitle', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
content: <SummaryTab />,
|
||||
},
|
||||
{
|
||||
id: 'request',
|
||||
name: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepReview.requestTabTitle', {
|
||||
defaultMessage: 'Request',
|
||||
}),
|
||||
content: <RequestTab />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { Forms, ComponentTemplateDeserialized } from '../../../shared_imports';
|
||||
import { WizardContent } from '../component_template_form';
|
||||
import { StepReview } from './step_review';
|
||||
|
||||
interface Props {
|
||||
getComponentTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized;
|
||||
}
|
||||
|
||||
export const StepReviewContainer = React.memo(({ getComponentTemplateData }: Props) => {
|
||||
const { getData } = Forms.useMultiContentContext<WizardContent>();
|
||||
|
||||
const wizardContent = getData();
|
||||
// Build the final template object, providing the wizard content data
|
||||
const componentTemplate = getComponentTemplateData(wizardContent);
|
||||
|
||||
return <StepReview componentTemplate={componentTemplate} />;
|
||||
});
|
|
@ -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 { ComponentTemplateCreate } from './component_template_create';
|
||||
|
||||
export { ComponentTemplateEdit } from './component_template_edit';
|
||||
|
||||
export { ComponentTemplateClone } from './component_template_clone';
|
|
@ -7,7 +7,8 @@
|
|||
import React, { createContext, useContext } from 'react';
|
||||
import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public';
|
||||
|
||||
import { getApi, getUseRequest, getSendRequest, getDocumentation } from './lib';
|
||||
import { ManagementAppMountParams } from 'src/plugins/management/public';
|
||||
import { getApi, getUseRequest, getSendRequest, getDocumentation, getBreadcrumbs } from './lib';
|
||||
|
||||
const ComponentTemplatesContext = createContext<Context | undefined>(undefined);
|
||||
|
||||
|
@ -17,6 +18,7 @@ interface Props {
|
|||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||
docLinks: DocLinksStart;
|
||||
toasts: NotificationsSetup['toasts'];
|
||||
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||
}
|
||||
|
||||
interface Context {
|
||||
|
@ -24,6 +26,7 @@ interface Context {
|
|||
apiBasePath: string;
|
||||
api: ReturnType<typeof getApi>;
|
||||
documentation: ReturnType<typeof getDocumentation>;
|
||||
breadcrumbs: ReturnType<typeof getBreadcrumbs>;
|
||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||
toasts: NotificationsSetup['toasts'];
|
||||
}
|
||||
|
@ -35,17 +38,18 @@ export const ComponentTemplatesProvider = ({
|
|||
value: Props;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value;
|
||||
const { httpClient, apiBasePath, trackMetric, docLinks, toasts, setBreadcrumbs } = value;
|
||||
|
||||
const useRequest = getUseRequest(httpClient);
|
||||
const sendRequest = getSendRequest(httpClient);
|
||||
|
||||
const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric);
|
||||
const documentation = getDocumentation(docLinks);
|
||||
const breadcrumbs = getBreadcrumbs(setBreadcrumbs);
|
||||
|
||||
return (
|
||||
<ComponentTemplatesContext.Provider
|
||||
value={{ api, documentation, trackMetric, toasts, httpClient, apiBasePath }}
|
||||
value={{ api, documentation, trackMetric, toasts, httpClient, apiBasePath, breadcrumbs }}
|
||||
>
|
||||
{children}
|
||||
</ComponentTemplatesContext.Provider>
|
||||
|
|
|
@ -9,6 +9,8 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load';
|
|||
export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete';
|
||||
export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many';
|
||||
export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details';
|
||||
export const UIM_COMPONENT_TEMPLATE_CREATE = 'component_template_create';
|
||||
export const UIM_COMPONENT_TEMPLATE_UPDATE = 'component_template_update';
|
||||
|
||||
// privileges
|
||||
export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates'];
|
||||
|
|
|
@ -10,4 +10,10 @@ export { ComponentTemplateList } from './component_template_list';
|
|||
|
||||
export { ComponentTemplateDetailsFlyout } from './component_template_details';
|
||||
|
||||
export {
|
||||
ComponentTemplateCreate,
|
||||
ComponentTemplateEdit,
|
||||
ComponentTemplateClone,
|
||||
} from './component_template_wizard';
|
||||
|
||||
export * from './component_template_selector';
|
||||
|
|
|
@ -4,8 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ComponentTemplateListItem, ComponentTemplateDeserialized, Error } from '../shared_imports';
|
||||
import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants';
|
||||
import {
|
||||
ComponentTemplateListItem,
|
||||
ComponentTemplateDeserialized,
|
||||
ComponentTemplateSerialized,
|
||||
Error,
|
||||
} from '../shared_imports';
|
||||
import {
|
||||
UIM_COMPONENT_TEMPLATE_DELETE_MANY,
|
||||
UIM_COMPONENT_TEMPLATE_DELETE,
|
||||
UIM_COMPONENT_TEMPLATE_CREATE,
|
||||
UIM_COMPONENT_TEMPLATE_UPDATE,
|
||||
} from '../constants';
|
||||
import { UseRequestHook, SendRequestHook } from './request';
|
||||
|
||||
export const getApi = (
|
||||
|
@ -44,9 +54,36 @@ export const getApi = (
|
|||
});
|
||||
}
|
||||
|
||||
async function createComponentTemplate(componentTemplate: ComponentTemplateSerialized) {
|
||||
const result = await sendRequest({
|
||||
path: `${apiBasePath}/component_templates`,
|
||||
method: 'post',
|
||||
body: JSON.stringify(componentTemplate),
|
||||
});
|
||||
|
||||
trackMetric('count', UIM_COMPONENT_TEMPLATE_CREATE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function updateComponentTemplate(componentTemplate: ComponentTemplateDeserialized) {
|
||||
const { name } = componentTemplate;
|
||||
const result = await sendRequest({
|
||||
path: `${apiBasePath}/component_templates/${encodeURIComponent(name)}`,
|
||||
method: 'put',
|
||||
body: JSON.stringify(componentTemplate),
|
||||
});
|
||||
|
||||
trackMetric('count', UIM_COMPONENT_TEMPLATE_UPDATE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
useLoadComponentTemplates,
|
||||
deleteComponentTemplates,
|
||||
useLoadComponentTemplate,
|
||||
createComponentTemplate,
|
||||
updateComponentTemplate,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ManagementAppMountParams } from 'src/plugins/management/public';
|
||||
|
||||
export const getBreadcrumbs = (setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']) => {
|
||||
const baseBreadcrumbs = [
|
||||
{
|
||||
text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.homeLabel', {
|
||||
defaultMessage: 'Index Management',
|
||||
}),
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.componentTemplatesLabel', {
|
||||
defaultMessage: 'Component templates',
|
||||
}),
|
||||
href: '/component_templates',
|
||||
},
|
||||
];
|
||||
|
||||
const setCreateBreadcrumbs = () => {
|
||||
const createBreadcrumbs = [
|
||||
...baseBreadcrumbs,
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplate.breadcrumb.createComponentTemplateLabel',
|
||||
{
|
||||
defaultMessage: 'Create component template',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return setBreadcrumbs(createBreadcrumbs);
|
||||
};
|
||||
|
||||
const setEditBreadcrumbs = () => {
|
||||
const editBreadcrumbs = [
|
||||
...baseBreadcrumbs,
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplate.breadcrumb.editComponentTemplateLabel',
|
||||
{
|
||||
defaultMessage: 'Edit component template',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return setBreadcrumbs(editBreadcrumbs);
|
||||
};
|
||||
|
||||
return {
|
||||
setCreateBreadcrumbs,
|
||||
setEditBreadcrumbs,
|
||||
};
|
||||
};
|
|
@ -11,6 +11,8 @@ export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocL
|
|||
const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`;
|
||||
|
||||
return {
|
||||
esDocsBase,
|
||||
componentTemplates: `${esDocsBase}/indices-component-template.html`,
|
||||
componentTemplatesMetadata: `${esDocsBase}/indices-component-template.html#component-templates-metadata`,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,3 +9,7 @@ export * from './api';
|
|||
export * from './request';
|
||||
|
||||
export * from './documentation';
|
||||
|
||||
export * from './breadcrumbs';
|
||||
|
||||
export { attemptToDecodeURI } from './utils';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const attemptToDecodeURI = (value: string) => {
|
||||
let result: string;
|
||||
|
||||
try {
|
||||
result = decodeURI(value);
|
||||
result = decodeURIComponent(result);
|
||||
} catch (e) {
|
||||
result = decodeURIComponent(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
|
@ -21,10 +21,44 @@ export {
|
|||
Forms,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
||||
|
||||
export { TabMappings, TabSettings, TabAliases } from '../shared';
|
||||
export {
|
||||
serializers,
|
||||
fieldValidators,
|
||||
fieldFormatters,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
|
||||
|
||||
export {
|
||||
FormSchema,
|
||||
FIELD_TYPES,
|
||||
VALIDATION_TYPES,
|
||||
FieldConfig,
|
||||
useForm,
|
||||
Form,
|
||||
getUseField,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
|
||||
export {
|
||||
getFormRow,
|
||||
Field,
|
||||
JsonEditorField,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/components';
|
||||
|
||||
export { isJSON } from '../../../../../../../src/plugins/es_ui_shared/static/validators/string';
|
||||
|
||||
export {
|
||||
TabMappings,
|
||||
TabSettings,
|
||||
TabAliases,
|
||||
CommonWizardSteps,
|
||||
StepSettingsContainer,
|
||||
StepMappingsContainer,
|
||||
StepAliasesContainer,
|
||||
} from '../shared';
|
||||
|
||||
export {
|
||||
ComponentTemplateSerialized,
|
||||
ComponentTemplateDeserialized,
|
||||
ComponentTemplateListItem,
|
||||
} from '../../../../common';
|
||||
|
||||
export { serializeComponentTemplate } from '../../../../common/lib';
|
||||
|
|
|
@ -27,7 +27,7 @@ export const renderApp = (
|
|||
|
||||
const { i18n, docLinks, notifications } = core;
|
||||
const { Context: I18nContext } = i18n;
|
||||
const { services, history } = dependencies;
|
||||
const { services, history, setBreadcrumbs } = dependencies;
|
||||
|
||||
const componentTemplateProviderValues = {
|
||||
httpClient: services.httpService.httpClient,
|
||||
|
@ -35,6 +35,7 @@ export const renderApp = (
|
|||
trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService),
|
||||
docLinks,
|
||||
toasts: notifications.toasts,
|
||||
setBreadcrumbs,
|
||||
};
|
||||
|
||||
render(
|
||||
|
|
|
@ -50,6 +50,7 @@ export async function mountManagementSection(
|
|||
},
|
||||
services,
|
||||
history,
|
||||
setBreadcrumbs,
|
||||
};
|
||||
|
||||
return renderApp(element, { core, dependencies: appDependencies });
|
||||
|
|
|
@ -4,17 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { serializeComponentTemplate } from '../../../../common/lib';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addBasePath } from '../index';
|
||||
import { componentTemplateSchema } from './schema_validation';
|
||||
|
||||
const bodySchema = schema.object({
|
||||
name: schema.string(),
|
||||
...componentTemplateSchema,
|
||||
});
|
||||
|
||||
export const registerCreateRoute = ({
|
||||
router,
|
||||
license,
|
||||
|
@ -24,13 +19,15 @@ export const registerCreateRoute = ({
|
|||
{
|
||||
path: addBasePath('/component_templates'),
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
body: componentTemplateSchema,
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const { callAsCurrentUser } = ctx.dataManagement!.client;
|
||||
|
||||
const { name, ...componentTemplateDefinition } = req.body;
|
||||
const serializedComponentTemplate = serializeComponentTemplate(req.body);
|
||||
|
||||
const { name } = req.body;
|
||||
|
||||
try {
|
||||
// Check that a component template with the same name doesn't already exist
|
||||
|
@ -60,7 +57,7 @@ export const registerCreateRoute = ({
|
|||
try {
|
||||
const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', {
|
||||
name,
|
||||
body: componentTemplateDefinition,
|
||||
body: serializedComponentTemplate,
|
||||
});
|
||||
|
||||
return res.ok({ body: response });
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const componentTemplateSchema = {
|
||||
export const componentTemplateSchema = schema.object({
|
||||
name: schema.string(),
|
||||
template: schema.object({
|
||||
settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
|
@ -13,4 +14,7 @@ export const componentTemplateSchema = {
|
|||
}),
|
||||
version: schema.maybe(schema.number()),
|
||||
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
};
|
||||
_kbnMeta: schema.object({
|
||||
usedBy: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -9,8 +9,6 @@ import { RouteDependencies } from '../../../types';
|
|||
import { addBasePath } from '../index';
|
||||
import { componentTemplateSchema } from './schema_validation';
|
||||
|
||||
const bodySchema = schema.object(componentTemplateSchema);
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
name: schema.string(),
|
||||
});
|
||||
|
@ -24,7 +22,7 @@ export const registerUpdateRoute = ({
|
|||
{
|
||||
path: addBasePath('/component_templates/{name}'),
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
body: componentTemplateSchema,
|
||||
params: paramsSchema,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -146,6 +146,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
id: 10,
|
||||
},
|
||||
},
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -162,6 +165,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send({
|
||||
name: REQUIRED_FIELDS_COMPONENT_NAME,
|
||||
template: {},
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -177,6 +183,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.send({
|
||||
name: COMPONENT_NAME,
|
||||
template: {},
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
})
|
||||
.expect(409);
|
||||
|
||||
|
@ -233,7 +242,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
...COMPONENT,
|
||||
name: COMPONENT_NAME,
|
||||
version: 1,
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -250,7 +263,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
...COMPONENT,
|
||||
name: 'component_does_not_exist',
|
||||
version: 1,
|
||||
_kbnMeta: {
|
||||
usedBy: [],
|
||||
},
|
||||
})
|
||||
.expect(404);
|
||||
|
||||
|
|
Loading…
Reference in a new issue