[Component templates] Form wizard (#69732)
This commit is contained in:
parent
2eb0896415
commit
e35a42aa07
47 changed files with 2203 additions and 111 deletions
|
@ -4,91 +4,164 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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', () => {
|
describe('Component template serialization', () => {
|
||||||
test('deserializes a component template', () => {
|
describe('deserializeComponentTemplate()', () => {
|
||||||
expect(
|
test('deserializes a component template', () => {
|
||||||
deserializeComponentTemplate(
|
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
name: 'my_index_template',
|
name: 'my_component_template',
|
||||||
index_template: {
|
component_template: {
|
||||||
index_patterns: ['foo'],
|
version: 1,
|
||||||
|
_meta: {
|
||||||
|
serialization: {
|
||||||
|
id: 10,
|
||||||
|
class: 'MyComponentTemplate',
|
||||||
|
},
|
||||||
|
description: 'set number of shards to one',
|
||||||
|
},
|
||||||
template: {
|
template: {
|
||||||
settings: {
|
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_index_template',
|
||||||
name: 'my_component_template',
|
index_template: {
|
||||||
version: 1,
|
index_patterns: ['foo'],
|
||||||
_meta: {
|
template: {
|
||||||
serialization: {
|
settings: {
|
||||||
id: 10,
|
number_of_replicas: 2,
|
||||||
class: 'MyComponentTemplate',
|
},
|
||||||
},
|
},
|
||||||
description: 'set number of shards to one',
|
composed_of: ['my_component_template'],
|
||||||
},
|
},
|
||||||
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({
|
||||||
|
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: {
|
||||||
_kbnMeta: {
|
usedBy: ['my_index_template'],
|
||||||
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,
|
ComponentTemplateFromEs,
|
||||||
ComponentTemplateDeserialized,
|
ComponentTemplateDeserialized,
|
||||||
ComponentTemplateListItem,
|
ComponentTemplateListItem,
|
||||||
|
ComponentTemplateSerialized,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
|
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
|
||||||
|
@ -84,3 +85,15 @@ export function deserializeComponenTemplateList(
|
||||||
|
|
||||||
return componentTemplateListItem;
|
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 {
|
export {
|
||||||
deserializeComponentTemplate,
|
deserializeComponentTemplate,
|
||||||
deserializeComponenTemplateList,
|
deserializeComponenTemplateList,
|
||||||
|
serializeComponentTemplate,
|
||||||
} from './component_template_serialization';
|
} from './component_template_serialization';
|
||||||
|
|
|
@ -16,6 +16,11 @@ import { TemplateClone } from './sections/template_clone';
|
||||||
import { TemplateEdit } from './sections/template_edit';
|
import { TemplateEdit } from './sections/template_edit';
|
||||||
|
|
||||||
import { useServices } from './app_context';
|
import { useServices } from './app_context';
|
||||||
|
import {
|
||||||
|
ComponentTemplateCreate,
|
||||||
|
ComponentTemplateEdit,
|
||||||
|
ComponentTemplateClone,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
export const App = ({ history }: { history: ScopedHistory }) => {
|
export const App = ({ history }: { history: ScopedHistory }) => {
|
||||||
const { uiMetricService } = useServices();
|
const { uiMetricService } = useServices();
|
||||||
|
@ -34,6 +39,13 @@ export const AppWithoutRouter = () => (
|
||||||
<Route exact path="/create_template" component={TemplateCreate} />
|
<Route exact path="/create_template" component={TemplateCreate} />
|
||||||
<Route exact path="/clone_template/:name*" component={TemplateClone} />
|
<Route exact path="/clone_template/:name*" component={TemplateClone} />
|
||||||
<Route exact path="/edit_template/:name*" component={TemplateEdit} />
|
<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} />
|
<Route path={`/:section(${homeSections.join('|')})`} component={IndexManagementHome} />
|
||||||
<Redirect from={`/`} to={`/indices`} />
|
<Redirect from={`/`} to={`/indices`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
|
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { ScopedHistory } from 'kibana/public';
|
import { ScopedHistory } from 'kibana/public';
|
||||||
|
import { ManagementAppMountParams } from 'src/plugins/management/public';
|
||||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||||
|
|
||||||
import { CoreStart } from '../../../../../src/core/public';
|
import { CoreStart } from '../../../../../src/core/public';
|
||||||
|
|
||||||
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
||||||
import { IndexMgmtMetricsType } from '../types';
|
import { IndexMgmtMetricsType } from '../types';
|
||||||
import { UiMetricService, NotificationService, HttpService } from './services';
|
import { UiMetricService, NotificationService, HttpService } from './services';
|
||||||
|
@ -32,6 +33,7 @@ export interface AppDependencies {
|
||||||
notificationService: NotificationService;
|
notificationService: NotificationService;
|
||||||
};
|
};
|
||||||
history: ScopedHistory;
|
history: ScopedHistory;
|
||||||
|
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContextProvider = ({
|
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 sinon, { SinonFakeServer } from 'sinon';
|
||||||
import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports';
|
import {
|
||||||
|
ComponentTemplateListItem,
|
||||||
|
ComponentTemplateDeserialized,
|
||||||
|
ComponentTemplateSerialized,
|
||||||
|
} from '../../../shared_imports';
|
||||||
import { API_BASE_PATH } from './constants';
|
import { API_BASE_PATH } from './constants';
|
||||||
|
|
||||||
// Register helpers to mock HTTP Requests
|
// 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 {
|
return {
|
||||||
setLoadComponentTemplatesResponse,
|
setLoadComponentTemplatesResponse,
|
||||||
setDeleteComponentTemplateResponse,
|
setDeleteComponentTemplateResponse,
|
||||||
setLoadComponentTemplateResponse,
|
setLoadComponentTemplateResponse,
|
||||||
|
setCreateComponentTemplateResponse,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ const appDependencies = {
|
||||||
trackMetric: () => {},
|
trackMetric: () => {},
|
||||||
docLinks: docLinksServiceMock.createStartContract(),
|
docLinks: docLinksServiceMock.createStartContract(),
|
||||||
toasts: notificationServiceMock.createSetupContract().toasts,
|
toasts: notificationServiceMock.createSetupContract().toasts,
|
||||||
|
setBreadcrumbs: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setupEnvironment = () => {
|
export const setupEnvironment = () => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { useComponentTemplatesContext } from '../component_templates_context';
|
||||||
import { TabSummary } from './tab_summary';
|
import { TabSummary } from './tab_summary';
|
||||||
import { ComponentTemplateTabs, TabType } from './tabs';
|
import { ComponentTemplateTabs, TabType } from './tabs';
|
||||||
import { ManageButton, ManageAction } from './manage_button';
|
import { ManageButton, ManageAction } from './manage_button';
|
||||||
|
import { attemptToDecodeURI } from '../lib';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
componentTemplateName: string;
|
componentTemplateName: string;
|
||||||
|
@ -39,8 +40,10 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent<Props> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { api } = useComponentTemplatesContext();
|
const { api } = useComponentTemplatesContext();
|
||||||
|
|
||||||
|
const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName);
|
||||||
|
|
||||||
const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate(
|
const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate(
|
||||||
componentTemplateName
|
decodedComponentTemplateName
|
||||||
);
|
);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('summary');
|
const [activeTab, setActiveTab] = useState<TabType>('summary');
|
||||||
|
@ -108,7 +111,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent<Props> = ({
|
||||||
<EuiFlyoutHeader>
|
<EuiFlyoutHeader>
|
||||||
<EuiTitle size="m">
|
<EuiTitle size="m">
|
||||||
<h2 id="componentTemplateDetailsFlyoutTitle" data-test-subj="title">
|
<h2 id="componentTemplateDetailsFlyoutTitle" data-test-subj="title">
|
||||||
{componentTemplateName}
|
{decodedComponentTemplateName}
|
||||||
</h2>
|
</h2>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
</EuiFlyoutHeader>
|
</EuiFlyoutHeader>
|
||||||
|
|
|
@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent<Props> = ({ componentTemplateDe
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Version (optional) */}
|
{/* Version (optional) */}
|
||||||
{version && (
|
{typeof version !== 'undefined' && (
|
||||||
<>
|
<>
|
||||||
<EuiDescriptionListTitle data-test-subj="versionTitle">
|
<EuiDescriptionListTitle data-test-subj="versionTitle">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { ScopedHistory } from 'kibana/public';
|
||||||
|
|
||||||
import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports';
|
import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports';
|
||||||
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
|
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
|
||||||
|
import { attemptToDecodeURI } from '../lib';
|
||||||
import { useComponentTemplatesContext } from '../component_templates_context';
|
import { useComponentTemplatesContext } from '../component_templates_context';
|
||||||
import { ComponentTemplateDetailsFlyout } from '../component_template_details';
|
import { ComponentTemplateDetailsFlyout } from '../component_template_details';
|
||||||
import { EmptyPrompt } from './empty_prompt';
|
import { EmptyPrompt } from './empty_prompt';
|
||||||
|
@ -34,8 +35,22 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
||||||
|
|
||||||
const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState<string[]>([]);
|
const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState<string[]>([]);
|
||||||
|
|
||||||
const goToList = () => {
|
const goToComponentTemplateList = () => {
|
||||||
return history.push('component_templates');
|
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
|
// Track component loaded
|
||||||
|
@ -60,11 +75,13 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
||||||
componentTemplates={data}
|
componentTemplates={data}
|
||||||
onReloadClick={sendRequest}
|
onReloadClick={sendRequest}
|
||||||
onDeleteClick={setComponentTemplatesToDelete}
|
onDeleteClick={setComponentTemplatesToDelete}
|
||||||
|
onEditClick={goToEditComponentTemplate}
|
||||||
|
onCloneClick={goToCloneComponentTemplate}
|
||||||
history={history as ScopedHistory}
|
history={history as ScopedHistory}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (data && data.length === 0) {
|
} else if (data && data.length === 0) {
|
||||||
content = <EmptyPrompt />;
|
content = <EmptyPrompt history={history} />;
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
content = <LoadError onReloadClick={sendRequest} />;
|
content = <LoadError onReloadClick={sendRequest} />;
|
||||||
}
|
}
|
||||||
|
@ -81,7 +98,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
||||||
// refetch the component templates
|
// refetch the component templates
|
||||||
sendRequest();
|
sendRequest();
|
||||||
// go back to list view (if deleted from details flyout)
|
// go back to list view (if deleted from details flyout)
|
||||||
goToList();
|
goToComponentTemplateList();
|
||||||
}
|
}
|
||||||
setComponentTemplatesToDelete([]);
|
setComponentTemplatesToDelete([]);
|
||||||
}}
|
}}
|
||||||
|
@ -92,9 +109,25 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
||||||
{/* details flyout */}
|
{/* details flyout */}
|
||||||
{componentTemplateName && (
|
{componentTemplateName && (
|
||||||
<ComponentTemplateDetailsFlyout
|
<ComponentTemplateDetailsFlyout
|
||||||
onClose={goToList}
|
onClose={goToComponentTemplateList}
|
||||||
componentTemplateName={componentTemplateName}
|
componentTemplateName={componentTemplateName}
|
||||||
actions={[
|
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', {
|
name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', {
|
||||||
defaultMessage: 'Delete',
|
defaultMessage: 'Delete',
|
||||||
|
@ -104,7 +137,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
||||||
details._kbnMeta.usedBy.length > 0,
|
details._kbnMeta.usedBy.length > 0,
|
||||||
closePopoverOnClick: true,
|
closePopoverOnClick: true,
|
||||||
handleActionClick: () => {
|
handleActionClick: () => {
|
||||||
setComponentTemplatesToDelete([componentTemplateName]);
|
setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -6,11 +6,17 @@
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
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';
|
import { useComponentTemplatesContext } from '../component_templates_context';
|
||||||
|
|
||||||
export const EmptyPrompt: FunctionComponent = () => {
|
interface Props {
|
||||||
|
history: RouteComponentProps['history'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyPrompt: FunctionComponent<Props> = ({ history }) => {
|
||||||
const { documentation } = useComponentTemplatesContext();
|
const { documentation } = useComponentTemplatesContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,6 +44,17 @@ export const EmptyPrompt: FunctionComponent = () => {
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
</p>
|
</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[];
|
componentTemplates: ComponentTemplateListItem[];
|
||||||
onReloadClick: () => void;
|
onReloadClick: () => void;
|
||||||
onDeleteClick: (componentTemplateName: string[]) => void;
|
onDeleteClick: (componentTemplateName: string[]) => void;
|
||||||
|
onEditClick: (componentTemplateName: string) => void;
|
||||||
|
onCloneClick: (componentTemplateName: string) => void;
|
||||||
history: ScopedHistory;
|
history: ScopedHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +34,8 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
||||||
componentTemplates,
|
componentTemplates,
|
||||||
onReloadClick,
|
onReloadClick,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
|
onEditClick,
|
||||||
|
onCloneClick,
|
||||||
history,
|
history,
|
||||||
}) => {
|
}) => {
|
||||||
const { trackMetric } = useComponentTemplatesContext();
|
const { trackMetric } = useComponentTemplatesContext();
|
||||||
|
@ -85,6 +89,17 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
||||||
defaultMessage: 'Reload',
|
defaultMessage: 'Reload',
|
||||||
})}
|
})}
|
||||||
</EuiButton>,
|
</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: {
|
box: {
|
||||||
incremental: true,
|
incremental: true,
|
||||||
|
@ -135,7 +150,7 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
||||||
{...reactRouterNavigate(
|
{...reactRouterNavigate(
|
||||||
history,
|
history,
|
||||||
{
|
{
|
||||||
pathname: `/component_templates/${name}`,
|
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
|
||||||
},
|
},
|
||||||
() => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS)
|
() => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS)
|
||||||
)}
|
)}
|
||||||
|
@ -204,8 +219,37 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
||||||
),
|
),
|
||||||
actions: [
|
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,
|
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', {
|
name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.deleteActionLabel', {
|
||||||
defaultMessage: 'Delete',
|
defaultMessage: 'Delete',
|
||||||
}),
|
}),
|
||||||
|
@ -213,11 +257,13 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
||||||
'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription',
|
'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription',
|
||||||
{ defaultMessage: 'Delete this component template' }
|
{ defaultMessage: 'Delete this component template' }
|
||||||
),
|
),
|
||||||
|
onClick: ({ name }) => onDeleteClick([name]),
|
||||||
|
enabled: ({ usedBy }) => usedBy.length === 0,
|
||||||
|
isPrimary: true,
|
||||||
type: 'icon',
|
type: 'icon',
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
onClick: ({ name }) => onDeleteClick([name]),
|
'data-test-subj': 'deleteComponentTemplateButton',
|
||||||
enabled: ({ usedBy }) => usedBy.length === 0,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 React, { createContext, useContext } from 'react';
|
||||||
import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public';
|
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);
|
const ComponentTemplatesContext = createContext<Context | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ interface Props {
|
||||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||||
docLinks: DocLinksStart;
|
docLinks: DocLinksStart;
|
||||||
toasts: NotificationsSetup['toasts'];
|
toasts: NotificationsSetup['toasts'];
|
||||||
|
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
|
@ -24,6 +26,7 @@ interface Context {
|
||||||
apiBasePath: string;
|
apiBasePath: string;
|
||||||
api: ReturnType<typeof getApi>;
|
api: ReturnType<typeof getApi>;
|
||||||
documentation: ReturnType<typeof getDocumentation>;
|
documentation: ReturnType<typeof getDocumentation>;
|
||||||
|
breadcrumbs: ReturnType<typeof getBreadcrumbs>;
|
||||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||||
toasts: NotificationsSetup['toasts'];
|
toasts: NotificationsSetup['toasts'];
|
||||||
}
|
}
|
||||||
|
@ -35,17 +38,18 @@ export const ComponentTemplatesProvider = ({
|
||||||
value: Props;
|
value: Props;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value;
|
const { httpClient, apiBasePath, trackMetric, docLinks, toasts, setBreadcrumbs } = value;
|
||||||
|
|
||||||
const useRequest = getUseRequest(httpClient);
|
const useRequest = getUseRequest(httpClient);
|
||||||
const sendRequest = getSendRequest(httpClient);
|
const sendRequest = getSendRequest(httpClient);
|
||||||
|
|
||||||
const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric);
|
const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric);
|
||||||
const documentation = getDocumentation(docLinks);
|
const documentation = getDocumentation(docLinks);
|
||||||
|
const breadcrumbs = getBreadcrumbs(setBreadcrumbs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComponentTemplatesContext.Provider
|
<ComponentTemplatesContext.Provider
|
||||||
value={{ api, documentation, trackMetric, toasts, httpClient, apiBasePath }}
|
value={{ api, documentation, trackMetric, toasts, httpClient, apiBasePath, breadcrumbs }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ComponentTemplatesContext.Provider>
|
</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 = 'component_template_delete';
|
||||||
export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many';
|
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_DETAILS = 'component_template_details';
|
||||||
|
export const UIM_COMPONENT_TEMPLATE_CREATE = 'component_template_create';
|
||||||
|
export const UIM_COMPONENT_TEMPLATE_UPDATE = 'component_template_update';
|
||||||
|
|
||||||
// privileges
|
// privileges
|
||||||
export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates'];
|
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 { ComponentTemplateDetailsFlyout } from './component_template_details';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ComponentTemplateCreate,
|
||||||
|
ComponentTemplateEdit,
|
||||||
|
ComponentTemplateClone,
|
||||||
|
} from './component_template_wizard';
|
||||||
|
|
||||||
export * from './component_template_selector';
|
export * from './component_template_selector';
|
||||||
|
|
|
@ -4,8 +4,18 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentTemplateListItem, ComponentTemplateDeserialized, Error } from '../shared_imports';
|
import {
|
||||||
import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants';
|
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';
|
import { UseRequestHook, SendRequestHook } from './request';
|
||||||
|
|
||||||
export const getApi = (
|
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 {
|
return {
|
||||||
useLoadComponentTemplates,
|
useLoadComponentTemplates,
|
||||||
deleteComponentTemplates,
|
deleteComponentTemplates,
|
||||||
useLoadComponentTemplate,
|
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}`;
|
const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
esDocsBase,
|
||||||
componentTemplates: `${esDocsBase}/indices-component-template.html`,
|
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 './request';
|
||||||
|
|
||||||
export * from './documentation';
|
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,
|
Forms,
|
||||||
} from '../../../../../../../src/plugins/es_ui_shared/public';
|
} 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 {
|
export {
|
||||||
ComponentTemplateSerialized,
|
ComponentTemplateSerialized,
|
||||||
ComponentTemplateDeserialized,
|
ComponentTemplateDeserialized,
|
||||||
ComponentTemplateListItem,
|
ComponentTemplateListItem,
|
||||||
} from '../../../../common';
|
} from '../../../../common';
|
||||||
|
|
||||||
|
export { serializeComponentTemplate } from '../../../../common/lib';
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const renderApp = (
|
||||||
|
|
||||||
const { i18n, docLinks, notifications } = core;
|
const { i18n, docLinks, notifications } = core;
|
||||||
const { Context: I18nContext } = i18n;
|
const { Context: I18nContext } = i18n;
|
||||||
const { services, history } = dependencies;
|
const { services, history, setBreadcrumbs } = dependencies;
|
||||||
|
|
||||||
const componentTemplateProviderValues = {
|
const componentTemplateProviderValues = {
|
||||||
httpClient: services.httpService.httpClient,
|
httpClient: services.httpService.httpClient,
|
||||||
|
@ -35,6 +35,7 @@ export const renderApp = (
|
||||||
trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService),
|
trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService),
|
||||||
docLinks,
|
docLinks,
|
||||||
toasts: notifications.toasts,
|
toasts: notifications.toasts,
|
||||||
|
setBreadcrumbs,
|
||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|
|
@ -50,6 +50,7 @@ export async function mountManagementSection(
|
||||||
},
|
},
|
||||||
services,
|
services,
|
||||||
history,
|
history,
|
||||||
|
setBreadcrumbs,
|
||||||
};
|
};
|
||||||
|
|
||||||
return renderApp(element, { core, dependencies: appDependencies });
|
return renderApp(element, { core, dependencies: appDependencies });
|
||||||
|
|
|
@ -4,17 +4,12 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { schema } from '@kbn/config-schema';
|
|
||||||
|
|
||||||
|
import { serializeComponentTemplate } from '../../../../common/lib';
|
||||||
import { RouteDependencies } from '../../../types';
|
import { RouteDependencies } from '../../../types';
|
||||||
import { addBasePath } from '../index';
|
import { addBasePath } from '../index';
|
||||||
import { componentTemplateSchema } from './schema_validation';
|
import { componentTemplateSchema } from './schema_validation';
|
||||||
|
|
||||||
const bodySchema = schema.object({
|
|
||||||
name: schema.string(),
|
|
||||||
...componentTemplateSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const registerCreateRoute = ({
|
export const registerCreateRoute = ({
|
||||||
router,
|
router,
|
||||||
license,
|
license,
|
||||||
|
@ -24,13 +19,15 @@ export const registerCreateRoute = ({
|
||||||
{
|
{
|
||||||
path: addBasePath('/component_templates'),
|
path: addBasePath('/component_templates'),
|
||||||
validate: {
|
validate: {
|
||||||
body: bodySchema,
|
body: componentTemplateSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
license.guardApiRoute(async (ctx, req, res) => {
|
license.guardApiRoute(async (ctx, req, res) => {
|
||||||
const { callAsCurrentUser } = ctx.dataManagement!.client;
|
const { callAsCurrentUser } = ctx.dataManagement!.client;
|
||||||
|
|
||||||
const { name, ...componentTemplateDefinition } = req.body;
|
const serializedComponentTemplate = serializeComponentTemplate(req.body);
|
||||||
|
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check that a component template with the same name doesn't already exist
|
// Check that a component template with the same name doesn't already exist
|
||||||
|
@ -60,7 +57,7 @@ export const registerCreateRoute = ({
|
||||||
try {
|
try {
|
||||||
const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', {
|
const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', {
|
||||||
name,
|
name,
|
||||||
body: componentTemplateDefinition,
|
body: serializedComponentTemplate,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.ok({ body: response });
|
return res.ok({ body: response });
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
*/
|
*/
|
||||||
import { schema } from '@kbn/config-schema';
|
import { schema } from '@kbn/config-schema';
|
||||||
|
|
||||||
export const componentTemplateSchema = {
|
export const componentTemplateSchema = schema.object({
|
||||||
|
name: schema.string(),
|
||||||
template: schema.object({
|
template: schema.object({
|
||||||
settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||||
aliases: 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()),
|
version: schema.maybe(schema.number()),
|
||||||
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
_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 { addBasePath } from '../index';
|
||||||
import { componentTemplateSchema } from './schema_validation';
|
import { componentTemplateSchema } from './schema_validation';
|
||||||
|
|
||||||
const bodySchema = schema.object(componentTemplateSchema);
|
|
||||||
|
|
||||||
const paramsSchema = schema.object({
|
const paramsSchema = schema.object({
|
||||||
name: schema.string(),
|
name: schema.string(),
|
||||||
});
|
});
|
||||||
|
@ -24,7 +22,7 @@ export const registerUpdateRoute = ({
|
||||||
{
|
{
|
||||||
path: addBasePath('/component_templates/{name}'),
|
path: addBasePath('/component_templates/{name}'),
|
||||||
validate: {
|
validate: {
|
||||||
body: bodySchema,
|
body: componentTemplateSchema,
|
||||||
params: paramsSchema,
|
params: paramsSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -146,6 +146,9 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
id: 10,
|
id: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
_kbnMeta: {
|
||||||
|
usedBy: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
@ -162,6 +165,9 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
.send({
|
.send({
|
||||||
name: REQUIRED_FIELDS_COMPONENT_NAME,
|
name: REQUIRED_FIELDS_COMPONENT_NAME,
|
||||||
template: {},
|
template: {},
|
||||||
|
_kbnMeta: {
|
||||||
|
usedBy: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
@ -177,6 +183,9 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
.send({
|
.send({
|
||||||
name: COMPONENT_NAME,
|
name: COMPONENT_NAME,
|
||||||
template: {},
|
template: {},
|
||||||
|
_kbnMeta: {
|
||||||
|
usedBy: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.expect(409);
|
.expect(409);
|
||||||
|
|
||||||
|
@ -233,7 +242,11 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send({
|
.send({
|
||||||
...COMPONENT,
|
...COMPONENT,
|
||||||
|
name: COMPONENT_NAME,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
_kbnMeta: {
|
||||||
|
usedBy: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
@ -250,7 +263,11 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send({
|
.send({
|
||||||
...COMPONENT,
|
...COMPONENT,
|
||||||
|
name: 'component_does_not_exist',
|
||||||
version: 1,
|
version: 1,
|
||||||
|
_kbnMeta: {
|
||||||
|
usedBy: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.expect(404);
|
.expect(404);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue