[ILM] Add index templates to "add policy" modal in policies table (#77077)

* [ILM] Add index templates to "add policy" modal in policies table

* [ILM] Change select to a combobox in 'add policy' modal in policies table

* [ILM] Add not found message to "add policy to template" modal

* [ILM] Fix api integration test

* [ILM] Fix type check error

* [ILM] Add PR review suggestions

* [ILM] Fix type check error

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2020-09-21 15:27:52 +02:00 committed by GitHub
parent 77bd7ea9cf
commit 337fe73d09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 344 additions and 214 deletions

View file

@ -4,13 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component, Fragment } from 'react';
import { get, find } from 'lodash';
import React, { Fragment, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiCallOut,
EuiSelect,
EuiComboBox,
EuiForm,
EuiFormRow,
EuiOverlayMask,
@ -18,82 +17,42 @@ import {
EuiFieldText,
EuiSpacer,
EuiText,
EuiSwitch,
EuiButton,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import { PolicyFromES } from '../../../../../common/types';
import { LearnMoreLink } from '../../edit_policy/components';
import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../services/api';
import { addLifecyclePolicyToTemplate, useLoadIndexTemplates } from '../../../services/api';
import { toasts } from '../../../services/notification';
import { showApiError } from '../../../services/api_errors';
import { LearnMoreLink } from '../../edit_policy/components';
interface Props {
policy: PolicyFromES;
onCancel: () => void;
}
interface State {
templates: Array<{ name: string }>;
templateName?: string;
aliasName?: string;
templateError?: string;
}
export class AddPolicyToTemplateConfirmModal extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
templates: [],
};
}
async componentDidMount() {
const templates = await loadIndexTemplates();
this.setState({ templates });
}
addPolicyToTemplate = async () => {
const { policy, onCancel } = this.props;
const { templateName, aliasName } = this.state;
const policyName = policy.name;
if (!templateName) {
this.setState({
templateError: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.noTemplateSelectedErrorMessage',
{ defaultMessage: 'You must select an index template.' }
),
});
return;
}
try {
await addLifecyclePolicyToTemplate({
policyName,
templateName,
aliasName,
});
const message = i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage',
{
defaultMessage: 'Added policy {policyName} to index template {templateName}',
values: { policyName, templateName },
}
);
toasts.addSuccess(message);
onCancel();
} catch (e) {
const title = i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorMessage',
{
defaultMessage: 'Error adding policy "{policyName}" to index template {templateName}',
values: { policyName, templateName },
}
);
showApiError(e, title);
}
};
renderTemplateHasPolicyWarning() {
const selectedTemplate = this.getSelectedTemplate();
const existingPolicyName = get(selectedTemplate, 'settings.index.lifecycle.name');
export const AddPolicyToTemplateConfirmModal: React.FunctionComponent<Props> = ({
policy,
onCancel,
}) => {
const [isLegacy, setIsLegacy] = useState<boolean>(false);
const [templateName, setTemplateName] = useState<string>('');
const [aliasName, setAliasName] = useState<string>('');
const [templateError, setTemplateError] = useState<string>('');
const { error, isLoading, data: templates, resendRequest } = useLoadIndexTemplates(isLegacy);
const renderTemplateHasPolicyWarning = () => {
const selectedTemplate = templates!.find((template) => template.name === templateName);
const existingPolicyName = selectedTemplate?.settings?.index?.lifecycle?.name;
if (!existingPolicyName) {
return;
}
return (
<Fragment>
<EuiSpacer size="s" />
<EuiCallOut
style={{ maxWidth: 400 }}
title={
@ -116,57 +75,40 @@ export class AddPolicyToTemplateConfirmModal extends Component<Props, State> {
<EuiSpacer size="s" />
</Fragment>
);
}
getSelectedTemplate() {
const { templates, templateName } = this.state;
return find(templates, (template) => template.name === templateName);
}
renderForm() {
const { templates, templateName, templateError } = this.state;
const options = templates.map(({ name }) => {
return {
value: name,
text: name,
};
});
options.unshift({
value: '',
text: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateMessage',
{
defaultMessage: 'Select an index template',
}
),
});
};
const renderUnableToLoadTemplatesCallout = () => {
const { statusCode = '', message = '' } = error!;
return (
<EuiForm>
{this.renderTemplateHasPolicyWarning()}
<EuiFormRow
isInvalid={!!templateError}
error={templateError}
label={
<Fragment>
<EuiSpacer size="s" />
<EuiCallOut
style={{ maxWidth: 400 }}
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateLabel"
defaultMessage="Index template"
id="xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorLoadingTemplatesTitle"
defaultMessage="Unable to load index templates"
/>
}
color="danger"
>
<EuiSelect
options={options}
value={templateName}
onChange={(e) => {
this.setState({ templateError: undefined, templateName: e.target.value });
}}
/>
</EuiFormRow>
{this.renderAliasFormElement()}
</EuiForm>
<p>
{message} ({statusCode})
</p>
<EuiButton isLoading={isLoading} color="danger" onClick={resendRequest}>
<FormattedMessage
id="xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyToTemplateConfirmModal.errorLoadingTemplatesButton"
defaultMessage="Try again"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer size="s" />
</Fragment>
);
}
renderAliasFormElement = () => {
const { aliasName } = this.state;
const { policy } = this.props;
const showAliasTextInput = policy && get(policy, 'policy.phases.hot.actions.rollover');
};
const renderAliasFormElement = () => {
const showAliasTextInput = policy.policy.phases.hot?.actions.rollover;
if (!showAliasTextInput) {
return null;
}
@ -182,62 +124,178 @@ export class AddPolicyToTemplateConfirmModal extends Component<Props, State> {
<EuiFieldText
value={aliasName}
onChange={(e) => {
this.setState({ aliasName: e.target.value });
setAliasName(e.target.value);
}}
/>
</EuiFormRow>
);
};
render() {
const { policy, onCancel } = this.props;
const title = i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title',
{
defaultMessage: 'Add policy "{name}" to index template',
values: { name: policy.name },
}
);
const renderForm = () => {
let options: EuiComboBoxOptionOption[] = [];
if (templates) {
options = templates.map(({ name }) => {
return {
label: name,
};
});
}
const onComboChange = (comboOptions: EuiComboBoxOptionOption[]) => {
setTemplateError('');
setTemplateName(comboOptions.length > 0 ? comboOptions[0].label : '');
};
return (
<EuiOverlayMask>
<EuiConfirmModal
title={title}
onCancel={onCancel}
onConfirm={this.addPolicyToTemplate}
cancelButtonText={i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.cancelButton',
{
defaultMessage: 'Cancel',
}
)}
confirmButtonText={i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.confirmButton',
{
defaultMessage: 'Add policy',
}
)}
>
<EuiText>
<p>
<EuiForm>
<EuiFormRow>
<EuiSwitch
label={
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.explanationText"
defaultMessage="This will apply the lifecycle policy to
all indices which match the index template."
/>{' '}
<LearnMoreLink
docPath="indices-templates.html"
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexTemplatesLink"
defaultMessage="Learn about index templates"
/>
}
id="xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.showLegacyTemplates"
defaultMessage="Show legacy index templates"
/>
</p>
</EuiText>
<EuiSpacer size="m" />
{this.renderForm()}
</EuiConfirmModal>
</EuiOverlayMask>
}
checked={isLegacy}
onChange={(e) => {
setTemplateName('');
setIsLegacy(e.target.checked);
}}
/>
</EuiFormRow>
{error ? (
renderUnableToLoadTemplatesCallout()
) : (
<>
{renderTemplateHasPolicyWarning()}
<EuiFormRow
isInvalid={!!templateError}
error={templateError}
label={
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateLabel"
defaultMessage="Index template"
/>
}
>
<EuiComboBox
isLoading={isLoading}
placeholder={i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateMessage',
{
defaultMessage: 'Select an index template',
}
)}
options={options}
selectedOptions={
templateName
? [
{
label: templateName,
},
]
: []
}
onChange={onComboChange}
singleSelection={{ asPlainText: true }}
isClearable={true}
/>
</EuiFormRow>
</>
)}
{renderAliasFormElement()}
</EuiForm>
);
}
}
};
const addPolicyToTemplate = async () => {
const policyName = policy.name;
if (!templateName) {
setTemplateError(
i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.noTemplateSelectedErrorMessage',
{ defaultMessage: 'You must select an index template.' }
)
);
return;
}
try {
await addLifecyclePolicyToTemplate(
{
policyName,
templateName,
aliasName: aliasName === '' ? undefined : aliasName,
},
isLegacy
);
const message = i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage',
{
defaultMessage: 'Added policy {policyName} to index template {templateName}',
values: { policyName, templateName },
}
);
toasts.addSuccess(message);
onCancel();
} catch (e) {
const title = i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorMessage',
{
defaultMessage: 'Error adding policy "{policyName}" to index template {templateName}',
values: { policyName, templateName },
}
);
showApiError(e, title);
}
};
const title = i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title',
{
defaultMessage: 'Add policy "{name}" to index template',
values: { name: policy.name },
}
);
return (
<EuiOverlayMask>
<EuiConfirmModal
title={title}
onCancel={onCancel}
onConfirm={addPolicyToTemplate}
cancelButtonText={i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.cancelButton',
{
defaultMessage: 'Cancel',
}
)}
confirmButtonText={i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.confirmButton',
{
defaultMessage: 'Add policy',
}
)}
confirmButtonDisabled={isLoading || !!error || !templates}
>
<EuiText>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.explanationText"
defaultMessage="This will apply the lifecycle policy to
all indices which match the index template."
/>{' '}
<LearnMoreLink
docPath="indices-templates.html"
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexTemplatesLink"
defaultMessage="Learn about index templates"
/>
}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
{renderForm()}
</EuiConfirmModal>
</EuiOverlayMask>
);
};

View file

@ -17,10 +17,7 @@ import {
} from '../constants';
import { trackUiMetric } from './ui_metric';
import { sendGet, sendPost, sendDelete, useRequest } from './http';
interface GenericObject {
[key: string]: any;
}
import { IndexSettings } from '../../../../index_management/common/types';
export const useLoadNodes = () => {
return useRequest<ListNodesRouteResponse>({
@ -37,9 +34,14 @@ export const useLoadNodeDetails = (selectedNodeAttrs: string) => {
});
};
export async function loadIndexTemplates() {
return await sendGet(`templates`);
}
export const useLoadIndexTemplates = (legacy: boolean = false) => {
return useRequest<Array<{ name: string; settings: IndexSettings }>>({
path: 'templates',
query: { legacy },
method: 'get',
initialData: [],
});
};
export async function loadPolicies(withIndices: boolean) {
return await sendGet('policies', { withIndices });
@ -89,8 +91,15 @@ export const addLifecyclePolicyToIndex = async (body: {
return response;
};
export const addLifecyclePolicyToTemplate = async (body: GenericObject) => {
const response = await sendPost(`template`, body);
export const addLifecyclePolicyToTemplate = async (
body: {
policyName: string;
templateName: string;
aliasName?: string;
},
legacy: boolean = false
) => {
const response = await sendPost(`template`, body, { legacy });
// Only track successful actions.
trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX_TEMPLATE);
return response;

View file

@ -30,8 +30,8 @@ function getFullPath(path: string): string {
return apiPrefix;
}
export function sendPost(path: string, payload: GenericObject) {
return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload) });
export function sendPost(path: string, payload: GenericObject, query?: GenericObject) {
return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload), query });
}
export function sendGet(path: string, query?: GenericObject): any {

View file

@ -5,45 +5,78 @@
*/
import { merge } from 'lodash';
import { schema } from '@kbn/config-schema';
import { schema, TypeOf } from '@kbn/config-schema';
import { LegacyAPICaller } from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { TemplateFromEs, TemplateSerialized } from '../../../../../index_management/common/types';
import { LegacyTemplateSerialized } from '../../../../../index_management/server';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../../../services';
async function getIndexTemplate(
async function getLegacyIndexTemplate(
callAsCurrentUser: LegacyAPICaller,
templateName: string
): Promise<LegacyTemplateSerialized> {
): Promise<LegacyTemplateSerialized | undefined> {
const response = await callAsCurrentUser('indices.getTemplate', { name: templateName });
return response[templateName];
}
async function getIndexTemplate(
callAsCurrentUser: LegacyAPICaller,
templateName: string
): Promise<TemplateSerialized | undefined> {
const params = {
method: 'GET',
path: `/_index_template/${encodeURIComponent(templateName)}`,
// we allow 404 incase the user shutdown security in-between the check and now
ignore: [404],
};
const { index_templates: templates } = await callAsCurrentUser<{
index_templates: TemplateFromEs[];
}>('transport.request', params);
return templates?.find((template) => template.name === templateName)?.index_template;
}
async function updateIndexTemplate(
callAsCurrentUser: LegacyAPICaller,
isLegacy: boolean,
templateName: string,
policyName: string,
aliasName?: string
): Promise<any> {
// Fetch existing template
const template = await getIndexTemplate(callAsCurrentUser, templateName);
merge(template, {
settings: {
index: {
lifecycle: {
name: policyName,
rollover_alias: aliasName,
},
const settings = {
index: {
lifecycle: {
name: policyName,
rollover_alias: aliasName,
},
},
});
};
const indexTemplate = isLegacy
? await getLegacyIndexTemplate(callAsCurrentUser, templateName)
: await getIndexTemplate(callAsCurrentUser, templateName);
if (!indexTemplate) {
return false;
}
if (isLegacy) {
merge(indexTemplate, { settings });
} else {
merge(indexTemplate, {
template: {
settings,
},
});
}
const pathPrefix = isLegacy ? '/_template/' : '/_index_template/';
const params = {
method: 'PUT',
path: `/_template/${encodeURIComponent(templateName)}`,
path: `${pathPrefix}${encodeURIComponent(templateName)}`,
ignore: [404],
body: template,
body: indexTemplate,
};
return await callAsCurrentUser('transport.request', params);
@ -55,20 +88,35 @@ const bodySchema = schema.object({
aliasName: schema.maybe(schema.string()),
});
const querySchema = schema.object({
legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])),
});
export function registerAddPolicyRoute({ router, license, lib }: RouteDependencies) {
router.post(
{ path: addBasePath('/template'), validate: { body: bodySchema } },
{ path: addBasePath('/template'), validate: { body: bodySchema, query: querySchema } },
license.guardApiRoute(async (context, request, response) => {
const body = request.body as typeof bodySchema.type;
const { templateName, policyName, aliasName } = body;
const isLegacy = (request.query as TypeOf<typeof querySchema>).legacy === 'true';
try {
await updateIndexTemplate(
const updatedTemplate = await updateIndexTemplate(
context.core.elasticsearch.legacy.client.callAsCurrentUser,
isLegacy,
templateName,
policyName,
aliasName
);
if (!updatedTemplate) {
return response.notFound({
body: i18n.translate('xpack.indexLifecycleMgmt.templateNotFoundMessage', {
defaultMessage: `Template {name} not found.`,
values: {
name: templateName,
},
}),
});
}
return response.ok();
} catch (e) {
if (lib.isEsError(e)) {

View file

@ -4,20 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { LegacyAPICaller } from 'src/core/server';
import { LegacyTemplateSerialized } from '../../../../../index_management/server';
import { LegacyAPICaller } from 'kibana/server';
import { schema, TypeOf } from '@kbn/config-schema';
import {
IndexSettings,
LegacyTemplateSerialized,
TemplateFromEs,
} from '../../../../../index_management/common/types';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../../../services';
/**
* We don't want to output system template (whose name starts with a ".") which don't
* have a time base index pattern (with a wildcard in it) as those templates are already
* assigned to a single index.
*
* @param {String} templateName The index template
* @param {Array} indexPatterns Index patterns
*/
function isReservedSystemTemplate(templateName: string, indexPatterns: string[]): boolean {
return (
templateName.startsWith('kibana_index_template') ||
@ -28,9 +24,9 @@ function isReservedSystemTemplate(templateName: string, indexPatterns: string[])
);
}
function filterAndFormatTemplates(templates: {
function filterLegacyTemplates(templates: {
[templateName: string]: LegacyTemplateSerialized;
}): Array<{}> {
}): Array<{ name: string; settings?: IndexSettings }> {
const formattedTemplates = [];
const templateNames = Object.keys(templates);
for (const templateName of templateNames) {
@ -40,11 +36,6 @@ function filterAndFormatTemplates(templates: {
continue;
}
const formattedTemplate = {
index_lifecycle_name:
settings!.index && settings!.index.lifecycle ? settings!.index.lifecycle.name : undefined,
index_patterns,
allocation_rules:
settings!.index && settings!.index.routing ? settings!.index.routing : undefined,
settings,
name: templateName,
};
@ -53,12 +44,30 @@ function filterAndFormatTemplates(templates: {
return formattedTemplates;
}
function filterTemplates(
templates:
| { index_templates: TemplateFromEs[] }
| { [templateName: string]: LegacyTemplateSerialized },
isLegacy: boolean
): Array<{ name: string; settings?: IndexSettings }> {
if (isLegacy) {
return filterLegacyTemplates(templates as { [templateName: string]: LegacyTemplateSerialized });
}
const { index_templates: indexTemplates } = templates as { index_templates: TemplateFromEs[] };
return indexTemplates.map((template: TemplateFromEs) => {
return { name: template.name, settings: template.index_template.template?.settings };
});
}
async function fetchTemplates(
callAsCurrentUser: LegacyAPICaller
): Promise<{ [templateName: string]: LegacyTemplateSerialized }> {
callAsCurrentUser: LegacyAPICaller,
isLegacy: boolean
): Promise<
{ index_templates: TemplateFromEs[] } | { [templateName: string]: LegacyTemplateSerialized }
> {
const params = {
method: 'GET',
path: '/_template',
path: isLegacy ? '/_template' : '/_index_template',
// we allow 404 incase the user shutdown security in-between the check and now
ignore: [404],
};
@ -66,15 +75,21 @@ async function fetchTemplates(
return await callAsCurrentUser('transport.request', params);
}
const querySchema = schema.object({
legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])),
});
export function registerFetchRoute({ router, license, lib }: RouteDependencies) {
router.get(
{ path: addBasePath('/templates'), validate: false },
{ path: addBasePath('/templates'), validate: { query: querySchema } },
license.guardApiRoute(async (context, request, response) => {
const isLegacy = (request.query as TypeOf<typeof querySchema>).legacy === 'true';
try {
const templates = await fetchTemplates(
context.core.elasticsearch.legacy.client.callAsCurrentUser
context.core.elasticsearch.legacy.client.callAsCurrentUser,
isLegacy
);
const okResponse = { body: filterAndFormatTemplates(templates) };
const okResponse = { body: filterTemplates(templates, isLegacy) };
return response.ok(okResponse);
} catch (e) {
if (lib.isEsError(e)) {

View file

@ -7,10 +7,10 @@
import { API_BASE_PATH } from './constants';
export const registerHelpers = ({ supertest }) => {
const loadTemplates = () => supertest.get(`${API_BASE_PATH}/templates`);
const loadTemplates = () => supertest.get(`${API_BASE_PATH}/templates?legacy=true`);
const addPolicyToTemplate = (templateName, policyName, aliasName) =>
supertest.post(`${API_BASE_PATH}/template`).set('kbn-xsrf', 'xxx').send({
supertest.post(`${API_BASE_PATH}/template?legacy=true`).set('kbn-xsrf', 'xxx').send({
templateName,
policyName,
aliasName,