diff --git a/x-pack/index.js b/x-pack/index.js
index 374cef7d26b66..94bfca7424b5a 100644
--- a/x-pack/index.js
+++ b/x-pack/index.js
@@ -21,6 +21,7 @@ import { apm } from './plugins/apm';
import { licenseManagement } from './plugins/license_management';
import { cloud } from './plugins/cloud';
import { indexManagement } from './plugins/index_management';
+import { indexLifecycleManagement } from './plugins/index_lifecycle_management';
import { consoleExtensions } from './plugins/console_extensions';
import { spaces } from './plugins/spaces';
import { notifications } from './plugins/notifications';
@@ -52,6 +53,7 @@ module.exports = function (kibana) {
indexManagement(kibana),
consoleExtensions(kibana),
notifications(kibana),
+ indexLifecycleManagement(kibana),
kueryAutocomplete(kibana),
infra(kibana),
rollup(kibana),
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap
new file mode 100644
index 0000000000000..426e2683370ca
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap
@@ -0,0 +1,975 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ilm banner extension should return extension when any index has lifecycle error 1`] = `
+Object {
+ "filter": Query {
+ "ast": _AST {
+ "_clauses": Array [
+ Object {
+ "field": "ilm.step",
+ "match": "must",
+ "operator": "eq",
+ "type": "field",
+ "value": "ERROR",
+ },
+ ],
+ "_indexedClauses": Object {
+ "field": Object {
+ "ilm.step": Array [
+ Object {
+ "field": "ilm.step",
+ "match": "must",
+ "operator": "eq",
+ "type": "field",
+ "value": "ERROR",
+ },
+ ],
+ },
+ "is": Object {},
+ "term": Array [],
+ },
+ },
+ "syntax": Object {
+ "parse": [Function],
+ "print": [Function],
+ },
+ "text": "ilm.step:ERROR",
+ },
+ "filterLabel": "Show errors",
+ "title": "1
+ index has
+ lifecycle errors",
+ "type": "warning",
+}
+`;
+
+exports[`ilm filter extension should return extension when any index has lifecycle policy 1`] = `
+Array [
+ Object {
+ "field": "ilm.managed",
+ "multiSelect": false,
+ "name": "Lifecycle status",
+ "options": Array [
+ Object {
+ "value": true,
+ "view": "Managed",
+ },
+ Object {
+ "value": false,
+ "view": "Unmanaged",
+ },
+ ],
+ "type": "field_value_selection",
+ },
+ Object {
+ "field": "ilm.phase",
+ "multiSelect": "or",
+ "name": "Lifecycle phase",
+ "options": Array [
+ Object {
+ "value": "hot",
+ "view": "Hot",
+ },
+ Object {
+ "value": "warm",
+ "view": "Warm",
+ },
+ Object {
+ "value": "cold",
+ "view": "Cold",
+ },
+ Object {
+ "value": "delete",
+ "view": "Delete",
+ },
+ ],
+ "type": "field_value_selection",
+ },
+]
+`;
+
+exports[`ilm summary extension should return extension when index has lifecycle error 1`] = `
+
+
+
+
+ Index lifecycle management
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+ Index lifecycle error
+
+
+
+
+
+ illegal_argument_exception
+ :
+ setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
+
+
+
+
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="stackPopover"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="m"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ Lifecycle policy
+
+
+
+
+ -
+
+
+ testy
+
+
+
+
+
+ -
+
+ Next action
+
+
+
+
+ -
+ rollover
+
+
+
+ -
+
+ Failed step
+
+
+
+
+ -
+ check-rollover-ready
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ Current phase
+
+
+
+
+ -
+ hot
+
+
+
+ -
+
+ Next action time
+
+
+
+
+ -
+ 1544187775891
+
+
+
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="phaseExecutionPopover"
+ isOpen={false}
+ key="phaseExecutionPopover"
+ ownFocus={false}
+ panelPaddingSize="m"
+ withTitle={true}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`ilm summary extension should return extension when index has lifecycle policy 1`] = `
+
+
+
+
+ Index lifecycle management
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ Lifecycle policy
+
+
+
+
+ -
+
+
+ testy
+
+
+
+
+
+ -
+
+ Next action
+
+
+
+
+ -
+ complete
+
+
+
+ -
+
+ Failed step
+
+
+
+
+ -
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ Current phase
+
+
+
+
+ -
+ new
+
+
+
+ -
+
+ Next action time
+
+
+
+
+ -
+ 1544187775867
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`remove lifecycle action extension should return extension when all indices have lifecycle policy 1`] = `
+Object {
+ "buttonLabel": "Remove lifecycle policy",
+ "icon": "stopFilled",
+ "indexNames": Array [
+ Array [
+ "testy3",
+ "testy3",
+ ],
+ ],
+ "renderConfirmModal": [Function],
+}
+`;
+
+exports[`retry lifecycle action extension should return extension when all indices have lifecycle errors 1`] = `
+Object {
+ "buttonLabel": "Retry lifecycle step",
+ "icon": "play",
+ "indexNames": Array [
+ Array [
+ "testy3",
+ "testy3",
+ ],
+ ],
+ "requestMethod": [Function],
+ "successMessage": "Called retry lifecycle step for: \\"testy3\\", \\"testy3\\"",
+}
+`;
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap
new file mode 100644
index 0000000000000..fb39b40610072
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap
@@ -0,0 +1,274 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`policy table should change pages when a pagination link is clicked on 1`] = `
+Array [
+ "testy0",
+ "testy1",
+ "testy10",
+ "testy100",
+ "testy101",
+ "testy102",
+ "testy103",
+ "testy104",
+ "testy11",
+ "testy12",
+]
+`;
+
+exports[`policy table should change pages when a pagination link is clicked on 2`] = `
+Array [
+ "testy13",
+ "testy14",
+ "testy15",
+ "testy16",
+ "testy17",
+ "testy18",
+ "testy19",
+ "testy2",
+ "testy20",
+ "testy21",
+]
+`;
+
+exports[`policy table should filter based on content of search input 1`] = `
+Array [
+ "testy0",
+ "testy1",
+ "testy10",
+ "testy100",
+ "testy101",
+ "testy102",
+ "testy103",
+ "testy104",
+ "testy11",
+ "testy12",
+]
+`;
+
+exports[`policy table should show empty state when there are not any policies 1`] = `
+
+`;
+
+exports[`policy table should sort when covered indices header is clicked 1`] = `
+Array [
+ "testy1",
+ "testy3",
+ "testy5",
+ "testy7",
+ "testy9",
+ "testy11",
+ "testy13",
+ "testy15",
+ "testy17",
+ "testy19",
+]
+`;
+
+exports[`policy table should sort when covered indices header is clicked 2`] = `
+Array [
+ "testy104",
+ "testy102",
+ "testy100",
+ "testy98",
+ "testy96",
+ "testy94",
+ "testy92",
+ "testy90",
+ "testy88",
+ "testy86",
+]
+`;
+
+exports[`policy table should sort when modified date header is clicked 1`] = `
+Array [
+ "testy104",
+ "testy103",
+ "testy102",
+ "testy101",
+ "testy100",
+ "testy99",
+ "testy98",
+ "testy97",
+ "testy96",
+ "testy95",
+]
+`;
+
+exports[`policy table should sort when modified date header is clicked 2`] = `
+Array [
+ "testy0",
+ "testy1",
+ "testy2",
+ "testy3",
+ "testy4",
+ "testy5",
+ "testy6",
+ "testy7",
+ "testy8",
+ "testy9",
+]
+`;
+
+exports[`policy table should sort when name header is clicked 1`] = `
+Array [
+ "testy99",
+ "testy98",
+ "testy97",
+ "testy96",
+ "testy95",
+ "testy94",
+ "testy93",
+ "testy92",
+ "testy91",
+ "testy90",
+]
+`;
+
+exports[`policy table should sort when name header is clicked 2`] = `
+Array [
+ "testy0",
+ "testy1",
+ "testy10",
+ "testy100",
+ "testy101",
+ "testy102",
+ "testy103",
+ "testy104",
+ "testy11",
+ "testy12",
+]
+`;
+
+exports[`policy table should sort when version header is clicked 1`] = `
+Array [
+ "testy0",
+ "testy1",
+ "testy2",
+ "testy3",
+ "testy4",
+ "testy5",
+ "testy6",
+ "testy7",
+ "testy8",
+ "testy9",
+]
+`;
+
+exports[`policy table should sort when version header is clicked 2`] = `
+Array [
+ "testy104",
+ "testy103",
+ "testy102",
+ "testy101",
+ "testy100",
+ "testy99",
+ "testy98",
+ "testy97",
+ "testy96",
+ "testy95",
+]
+`;
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js
new file mode 100644
index 0000000000000..c399409408136
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js
@@ -0,0 +1,400 @@
+/*
+ * 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 moment from 'moment-timezone';
+import { Provider } from 'react-redux';
+import { fetchedPolicies, fetchedNodes } from '../../public/store/actions';
+import { indexLifecycleManagementStore } from '../../public/store';
+import { mountWithIntl } from '../../../../test_utils/enzyme_helpers';
+import { EditPolicy } from '../../public/sections/edit_policy';
+// axios has a $http like interface so using it to simulate $http
+import axios from 'axios';
+import { setHttpClient } from '../../public/services/api';
+setHttpClient(axios.create());
+import sinon from 'sinon';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import {
+ positiveNumbersAboveZeroErrorMessage,
+ numberRequiredMessage,
+ positiveNumberRequiredMessage,
+ maximumAgeRequiredMessage,
+ maximumSizeRequiredMessage,
+ policyNameRequiredMessage,
+ policyNameStartsWithUnderscoreErrorMessage,
+ policyNameContainsCommaErrorMessage,
+ policyNameContainsSpaceErrorMessage,
+ policyNameMustBeDifferentErrorMessage,
+ policyNameAlreadyUsedErrorMessage,
+} from '../../public/store/selectors/lifecycle';
+
+let server;
+let store;
+const policy = {
+ phases: {
+ hot: {
+ min_age: '0s',
+ actions: {
+ rollover: {
+ max_size: '1gb',
+ },
+ },
+ },
+ },
+};
+const policies = [];
+for (let i = 0; i < 105; i++) {
+ policies.push({
+ version: i,
+ modified_date: moment()
+ .subtract(i, 'days')
+ .valueOf(),
+ coveredIndices: i % 2 === 0 ? [`index${i}`] : null,
+ name: `testy${i}`,
+ policy: {
+ ...policy
+ },
+ });
+}
+window.scrollTo = jest.fn();
+window.TextEncoder = null;
+let component;
+const activatePhase = (rendered, phase) => {
+ const testSubject = `activatePhaseButton-${phase}`;
+ findTestSubject(rendered, testSubject).simulate('click');
+ rendered.update();
+};
+const expectedErrorMessages = (rendered, expectedErrorMessages) => {
+ const errorMessages = rendered.find('.euiFormErrorText');
+ expect(errorMessages.length).toBe(expectedErrorMessages.length);
+ expectedErrorMessages.forEach(expectedErrorMessage => {
+ let foundErrorMessage;
+ for (let i = 0; i < errorMessages.length; i++) {
+ if (errorMessages.at(i).text() === expectedErrorMessage) {
+ foundErrorMessage = true;
+ }
+ }
+ expect(foundErrorMessage).toBe(true);
+ });
+};
+const noRollover = (rendered) => {
+ findTestSubject(rendered, 'rolloverSwitch').simulate('change', { target: { checked: false } });
+ rendered.update();
+};
+const getNodeAttributeSelect = (rendered, phase) => {
+ return rendered.find(`select#${phase}-selectedNodeAttrs`);
+};
+const setPolicyName = (rendered, policyName) => {
+ const policyNameField = findTestSubject(rendered, 'policyNameField');
+ policyNameField.simulate('change', { target: { value: policyName } });
+ rendered.update();
+};
+const setPhaseAfter = (rendered, phase, after) => {
+ const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`);
+ afterInput.simulate('change', { target: { value: after } });
+ rendered.update();
+};
+const save = rendered => {
+ const saveButton = findTestSubject(rendered, 'savePolicyButton');
+ saveButton.simulate('click');
+ rendered.update();
+};
+describe('edit policy', () => {
+ beforeEach(() => {
+ store = indexLifecycleManagementStore();
+ component = (
+
+
+
+ );
+ store.dispatch(fetchedPolicies(policies));
+ server = sinon.fakeServer.create();
+ server.respondWith('/api/index_lifecycle_management/policies', [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(policies),
+ ]);
+ });
+ describe('top level form', () => {
+ test('should show error when trying to save empty form', () => {
+ const rendered = mountWithIntl(component);
+ save(rendered);
+ expectedErrorMessages(rendered, [
+ policyNameRequiredMessage,
+ maximumSizeRequiredMessage,
+ maximumAgeRequiredMessage,
+ ]);
+ });
+ test('should show error when trying to save policy name with space', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'my policy');
+ save(rendered);
+ expectedErrorMessages(rendered, [policyNameContainsSpaceErrorMessage]);
+ });
+ test('should show error when trying to save policy name that is already used', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'testy0');
+ rendered.update();
+ save(rendered);
+ expectedErrorMessages(rendered, [policyNameAlreadyUsedErrorMessage]);
+ });
+ test('should show error when trying to save as new policy but using the same name', () => {
+ component = (
+
+
+
+ );
+ const rendered = mountWithIntl(component);
+ findTestSubject(rendered, 'saveAsNewSwitch').simulate('change', { target: { checked: true } });
+ rendered.update();
+ setPolicyName(rendered, 'testy0');
+ save(rendered);
+ expectedErrorMessages(rendered, [policyNameMustBeDifferentErrorMessage]);
+ });
+ test('should show error when trying to save policy name with comma', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'my,policy');
+ save(rendered);
+ expectedErrorMessages(rendered, [policyNameContainsCommaErrorMessage]);
+ });
+ test('should show error when trying to save policy name starting with underscore', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, '_mypolicy');
+ save(rendered);
+ expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]);
+ });
+ });
+ describe('warm phase', () => {
+ test('should show number required error when trying to save empty warm phase', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ save(rendered);
+ expectedErrorMessages(rendered, [numberRequiredMessage]);
+ });
+ test('should show positive number required above zero error when trying to save warm phase with 0 for after', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ setPhaseAfter(rendered, 'warm', 0);
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show positive number required error when trying to save warm phase with -1 for after', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ setPhaseAfter(rendered, 'warm', -1);
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
+ });
+ test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ findTestSubject(rendered, 'shrinkSwitch').simulate('change', { target: { checked: true } });
+ rendered.update();
+ setPhaseAfter(rendered, 'warm', 1);
+ const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount');
+ shrinkInput.simulate('change', { target: { value: '0' } });
+ rendered.update();
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ setPhaseAfter(rendered, 'warm', 1);
+ findTestSubject(rendered, 'shrinkSwitch').simulate('change', { target: { checked: true } });
+ rendered.update();
+ const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount');
+ shrinkInput.simulate('change', { target: { value: '-1' } });
+ rendered.update();
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ setPhaseAfter(rendered, 'warm', 1);
+ findTestSubject(rendered, 'forceMergeSwitch').simulate('change', { target: { checked: true } });
+ rendered.update();
+ const shrinkInput = rendered.find('input#warm-selectedForceMergeSegments');
+ shrinkInput.simulate('change', { target: { value: '0' } });
+ rendered.update();
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ setPhaseAfter(rendered, 'warm', 1);
+ findTestSubject(rendered, 'forceMergeSwitch').simulate('change', { target: { checked: true } });
+ rendered.update();
+ const shrinkInput = rendered.find('input#warm-selectedForceMergeSegments');
+ shrinkInput.simulate('change', { target: { value: '-1' } });
+ rendered.update();
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show spinner for node attributes input when loading', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
+ expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
+ });
+ test('should show warning instead of node attributes input when none exist', () => {
+ store.dispatch(fetchedNodes({}));
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy();
+ expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
+ });
+ test('should show node attributes input when attributes exist', () => {
+ store.dispatch(fetchedNodes({ 'attribute:true': [ 'node1' ] }));
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
+ const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm');
+ expect(nodeAttributesSelect.exists()).toBeTruthy();
+ expect(nodeAttributesSelect.find('option').length).toBe(2);
+ });
+ test('should show view node attributes link when attribute selected and show flyout when clicked', () => {
+ store.dispatch(fetchedNodes({ 'attribute:true': [ 'node1' ] }));
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'warm');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
+ const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm');
+ expect(nodeAttributesSelect.exists()).toBeTruthy();
+ expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy();
+ expect(nodeAttributesSelect.find('option').length).toBe(2);
+ nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } });
+ rendered.update();
+ const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton');
+ expect(flyoutButton.exists()).toBeTruthy();
+ flyoutButton.simulate('click');
+ rendered.update();
+ expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
+ });
+ });
+ describe('cold phase', () => {
+ test('should show positive number required error when trying to save cold phase with 0 for after', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'cold');
+ setPhaseAfter(rendered, 'cold', 0);
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show positive number required error when trying to save cold phase with -1 for after', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'cold');
+ setPhaseAfter(rendered, 'cold', -1);
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
+ });
+ test('should show spinner for node attributes input when loading', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'cold');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
+ expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
+ });
+ test('should show warning instead of node attributes input when none exist', () => {
+ store.dispatch(fetchedNodes({}));
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'cold');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy();
+ expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
+ });
+ test('should show node attributes input when attributes exist', () => {
+ store.dispatch(fetchedNodes({ 'attribute:true': [ 'node1' ] }));
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'cold');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
+ const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold');
+ expect(nodeAttributesSelect.exists()).toBeTruthy();
+ expect(nodeAttributesSelect.find('option').length).toBe(2);
+ });
+ test('should show view node attributes link when attribute selected and show flyout when clicked', () => {
+ store.dispatch(fetchedNodes({ 'attribute:true': [ 'node1' ] }));
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'cold');
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
+ expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
+ const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold');
+ expect(nodeAttributesSelect.exists()).toBeTruthy();
+ expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy();
+ expect(nodeAttributesSelect.find('option').length).toBe(2);
+ nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } });
+ rendered.update();
+ const flyoutButton = findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton');
+ expect(flyoutButton.exists()).toBeTruthy();
+ flyoutButton.simulate('click');
+ rendered.update();
+ expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
+ });
+ });
+ describe('delete phase', () => {
+ test('should show positive number required error when trying to save delete phase with 0 for after', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'delete');
+ setPhaseAfter(rendered, 'delete', 0);
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
+ });
+ test('should show positive number required error when trying to save delete phase with -1 for after', () => {
+ const rendered = mountWithIntl(component);
+ noRollover(rendered);
+ setPolicyName(rendered, 'mypolicy');
+ activatePhase(rendered, 'delete');
+ setPhaseAfter(rendered, 'delete', -1);
+ save(rendered);
+ expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js
new file mode 100644
index 0000000000000..bec2a3cf16af4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js
@@ -0,0 +1,172 @@
+/*
+ * 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 moment from 'moment-timezone';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { fetchedPolicies } from '../../public/store/actions';
+import { indexLifecycleManagementStore } from '../../public/store';
+import { mountWithIntl } from '../../../../test_utils/enzyme_helpers';
+import { PolicyTable } from '../../public/sections/policy_table';
+import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test';
+// axios has a $http like interface so using it to simulate $http
+import axios from 'axios';
+import { setHttpClient } from '../../public/services/api';
+setHttpClient(axios.create());
+import sinon from 'sinon';
+let server = null;
+
+let store = null;
+const policies = [];
+for (let i = 0; i < 105; i++) {
+ policies.push({
+ version: i,
+ modified_date: moment().subtract(i, 'days').valueOf(),
+ coveredIndices: i % 2 === 0 ? [`index${i}`] : null,
+ name: `testy${i}`
+ });
+}
+jest.mock('');
+let component = null;
+
+const snapshot = rendered => {
+ expect(rendered).toMatchSnapshot();
+};
+const mountedSnapshot = rendered => {
+ expect(takeMountedSnapshot(rendered)).toMatchSnapshot();
+};
+const names = rendered => {
+ return findTestSubject(rendered, 'policyTablePolicyNameLink');
+};
+const namesText = rendered => {
+ return names(rendered).map(button => button.text());
+};
+
+const testSort = (headerName) => {
+ const rendered = mountWithIntl(component);
+ const nameHeader = findTestSubject(
+ rendered,
+ `policyTableHeaderCell-${headerName}`
+ ).find('button');
+ nameHeader.simulate('click');
+ rendered.update();
+ snapshot(namesText(rendered));
+ nameHeader.simulate('click');
+ rendered.update();
+ snapshot(namesText(rendered));
+};
+const openContextMenu = (buttonIndex) => {
+ const rendered = mountWithIntl(component);
+ const actionsButton = findTestSubject(
+ rendered,
+ 'policyActionsContextMenuButton'
+ );
+ actionsButton.at(buttonIndex).simulate('click');
+ rendered.update();
+ return rendered;
+};
+
+describe('policy table', () => {
+ beforeEach(() => {
+ store = indexLifecycleManagementStore();
+ component = (
+
+
+
+ );
+ store.dispatch(fetchedPolicies(policies));
+ server = sinon.fakeServer.create();
+ server.respondWith('/api/index_lifecycle_management/policies', [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(policies)
+ ]);
+ });
+ test('should show spinner when policies are loading', () => {
+ store = indexLifecycleManagementStore();
+ component = (
+
+
+
+ );
+ const rendered = mountWithIntl(component);
+ expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
+
+ });
+ test('should show empty state when there are not any policies', () => {
+ store.dispatch(fetchedPolicies([]));
+ const rendered = mountWithIntl(component);
+ mountedSnapshot(rendered);
+ });
+ test('should change pages when a pagination link is clicked on', () => {
+ const rendered = mountWithIntl(component);
+ snapshot(namesText(rendered));
+ const pagingButtons = rendered.find('.euiPaginationButton');
+ pagingButtons.at(2).simulate('click');
+ rendered.update();
+ snapshot(namesText(rendered));
+ });
+ test('should show more when per page value is increased', () => {
+ const rendered = mountWithIntl(component);
+ const perPageButton = rendered.find('span[children="Rows per page: 10"]');
+ perPageButton.simulate('click');
+ rendered.update();
+ const fiftyButton = rendered.find('span[children="50 rows"]');
+ fiftyButton.simulate('click');
+ rendered.update();
+ expect(namesText(rendered).length).toBe(50);
+ });
+ test('should filter based on content of search input', () => {
+ const rendered = mountWithIntl(component);
+ const searchInput = rendered.find('.euiFieldSearch').first();
+ searchInput.instance().value = 'testy0';
+ searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 });
+ rendered.update();
+ snapshot(namesText(rendered));
+ });
+ test('should sort when name header is clicked', () => {
+ testSort('name');
+ });
+ test('should sort when version header is clicked', () => {
+ testSort('version');
+ });
+ test('should sort when modified date header is clicked', () => {
+ testSort('modified_date');
+ });
+ test('should sort when covered indices header is clicked', () => {
+ testSort('coveredIndices');
+ });
+ test('should have proper actions in context menu when there are covered indices', () => {
+ const rendered = openContextMenu(0);
+ const buttons = rendered.find('button.euiContextMenuItem');
+ expect(buttons.length).toBe(3);
+ expect(buttons.at(0).text()).toBe('View indices linked to policy');
+ expect(buttons.at(1).text()).toBe('Add policy to index template');
+ expect(buttons.at(2).text()).toBe('Delete policy');
+ expect(buttons.at(2).getDOMNode().disabled).toBeTruthy();
+ });
+ test('should have proper actions in context menu when there are not covered indices', () => {
+ const rendered = openContextMenu(1);
+ const buttons = rendered.find('button.euiContextMenuItem');
+ expect(buttons.length).toBe(2);
+ expect(buttons.at(0).text()).toBe('Add policy to index template');
+ expect(buttons.at(1).text()).toBe('Delete policy');
+ expect(buttons.at(1).getDOMNode().disabled).toBeFalsy();
+ });
+ test('confirmation modal should show when delete button is pressed', () => {
+ const rendered = openContextMenu(1);
+ const deleteButton = rendered.find('button.euiContextMenuItem').at(1);
+ deleteButton.simulate('click');
+ rendered.update();
+ expect(rendered.find('.euiModal--confirmation').exists()).toBeTruthy();
+ });
+ test('confirmation modal should show when add policy to index template button is pressed', () => {
+ const rendered = openContextMenu(1);
+ const deleteButton = rendered.find('button.euiContextMenuItem').at(0);
+ deleteButton.simulate('click');
+ rendered.update();
+ expect(rendered.find('.euiModal--confirmation').exists()).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js
new file mode 100644
index 0000000000000..607bd96694cac
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js
@@ -0,0 +1,224 @@
+/*
+ * 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 { mountWithIntl } from '../../../test_utils/enzyme_helpers';
+import {
+ retryLifecycleActionExtension,
+ removeLifecyclePolicyActionExtension,
+ addLifecyclePolicyActionExtension,
+ ilmBannerExtension,
+ ilmFilterExtension,
+ ilmSummaryExtension,
+} from '../public/extend_index_management';
+const indexWithoutLifecyclePolicy = {
+ health: 'yellow',
+ status: 'open',
+ name: 'noPolicy',
+ uuid: 'BJ-nrZYuSrG-jaofr6SeLg',
+ primary: '1',
+ replica: '1',
+ documents: '1',
+ documents_deleted: '0',
+ size: '3.4kb',
+ primary_size: '3.4kb',
+ aliases: 'none',
+ ilm: {
+ index: 'testy1',
+ managed: false,
+ },
+};
+const indexWithLifecyclePolicy = {
+ health: 'yellow',
+ status: 'open',
+ name: 'testy3',
+ uuid: 'XL11TLa3Tvq298_dMUzLHQ',
+ primary: '1',
+ replica: '1',
+ documents: '2',
+ documents_deleted: '0',
+ size: '6.5kb',
+ primary_size: '6.5kb',
+ aliases: 'none',
+ ilm: {
+ index: 'testy3',
+ managed: true,
+ policy: 'testy',
+ lifecycle_date_millis: 1544020872361,
+ phase: 'new',
+ phase_time_millis: 1544187775867,
+ action: 'complete',
+ action_time_millis: 1544187775867,
+ step: 'complete',
+ step_time_millis: 1544187775867,
+ },
+};
+const indexWithLifecycleError = {
+ health: 'yellow',
+ status: 'open',
+ name: 'testy3',
+ uuid: 'XL11TLa3Tvq298_dMUzLHQ',
+ primary: '1',
+ replica: '1',
+ documents: '2',
+ documents_deleted: '0',
+ size: '6.5kb',
+ primary_size: '6.5kb',
+ aliases: 'none',
+ ilm: {
+ index: 'testy3',
+ managed: true,
+ policy: 'testy',
+ lifecycle_date_millis: 1544020872361,
+ phase: 'hot',
+ phase_time_millis: 1544187775891,
+ action: 'rollover',
+ action_time_millis: 1544187775891,
+ step: 'ERROR',
+ step_time_millis: 1544187776208,
+ failed_step: 'check-rollover-ready',
+ step_info: {
+ type: 'illegal_argument_exception',
+ reason: 'setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined',
+ },
+ phase_execution: {
+ policy: 'testy',
+ phase_definition: { min_age: '0s', actions: { rollover: { max_size: '1gb' } } },
+ version: 1,
+ modified_date_in_millis: 1544031699844,
+ },
+ },
+};
+describe('retry lifecycle action extension', () => {
+ test('should return null when no indices have index lifecycle policy', () => {
+ const extension = retryLifecycleActionExtension([indexWithoutLifecyclePolicy]);
+ expect(extension).toBeNull();
+ });
+ test('should return null when no index has lifecycle errors', () => {
+ const extension = retryLifecycleActionExtension([
+ indexWithLifecyclePolicy,
+ indexWithLifecyclePolicy,
+ ]);
+ expect(extension).toBeNull();
+ });
+ test('should return null when not all indices have lifecycle errors', () => {
+ const extension = retryLifecycleActionExtension([
+ indexWithLifecyclePolicy,
+ indexWithLifecycleError,
+ ]);
+ expect(extension).toBeNull();
+ });
+ test('should return extension when all indices have lifecycle errors', () => {
+ const extension = retryLifecycleActionExtension([
+ indexWithLifecycleError,
+ indexWithLifecycleError,
+ ]);
+ expect(extension).toBeDefined();
+ expect(extension).toMatchSnapshot();
+ });
+});
+describe('remove lifecycle action extension', () => {
+ test('should return null when no indices have index lifecycle policy', () => {
+ const extension = removeLifecyclePolicyActionExtension([indexWithoutLifecyclePolicy]);
+ expect(extension).toBeNull();
+ });
+ test('should return null when some indices have index lifecycle policy', () => {
+ const extension = removeLifecyclePolicyActionExtension([
+ indexWithoutLifecyclePolicy,
+ indexWithLifecyclePolicy
+ ]);
+ expect(extension).toBeNull();
+ });
+ test('should return extension when all indices have lifecycle policy', () => {
+ const extension = removeLifecyclePolicyActionExtension([
+ indexWithLifecycleError,
+ indexWithLifecycleError,
+ ]);
+ expect(extension).toBeDefined();
+ expect(extension).toMatchSnapshot();
+ });
+});
+describe('add lifecycle policy action extension', () => {
+ test('should return null when index has index lifecycle policy', () => {
+ const extension = addLifecyclePolicyActionExtension([indexWithLifecyclePolicy]);
+ expect(extension).toBeNull();
+ });
+ test('should return null when more than one index is passed', () => {
+ const extension = addLifecyclePolicyActionExtension([
+ indexWithoutLifecyclePolicy,
+ indexWithoutLifecyclePolicy,
+ ]);
+ expect(extension).toBeNull();
+ });
+ test('should return extension when one index is passed and it does not have lifecycle policy', () => {
+ const extension = addLifecyclePolicyActionExtension([indexWithoutLifecyclePolicy]);
+ expect(extension.renderConfirmModal).toBeDefined;
+ const component = extension.renderConfirmModal(jest.fn());
+ const rendered = mountWithIntl(component);
+ expect(rendered.exists('.euiModal--confirmation'));
+ });
+});
+describe('ilm banner extension', () => {
+ test('should return null when no index has index lifecycle policy', () => {
+ const extension = ilmBannerExtension([
+ indexWithoutLifecyclePolicy,
+ indexWithoutLifecyclePolicy,
+ ]);
+ expect(extension).toBeNull();
+ });
+ test('should return null no index has lifecycle error', () => {
+ const extension = ilmBannerExtension([
+ indexWithoutLifecyclePolicy,
+ indexWithLifecyclePolicy,
+ ]);
+ expect(extension).toBeNull();
+ });
+ test('should return extension when any index has lifecycle error', () => {
+ const extension = ilmBannerExtension([
+ indexWithoutLifecyclePolicy,
+ indexWithLifecyclePolicy,
+ indexWithLifecycleError,
+ ]);
+ expect(extension).toBeDefined();
+ expect(extension).toMatchSnapshot();
+ });
+});
+describe('ilm summary extension', () => {
+ test('should render null when index has no index lifecycle policy', () => {
+ const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy);
+ const rendered = mountWithIntl(extension);
+ expect(rendered.html()).toBeNull();
+ });
+ test('should return extension when index has lifecycle policy', () => {
+ const extension = ilmSummaryExtension(indexWithLifecyclePolicy);
+ expect(extension).toBeDefined();
+ const rendered = mountWithIntl(extension);
+ expect(rendered).toMatchSnapshot();
+ });
+ test('should return extension when index has lifecycle error', () => {
+ const extension = ilmSummaryExtension(indexWithLifecycleError);
+ expect(extension).toBeDefined();
+ const rendered = mountWithIntl(extension);
+ expect(rendered).toMatchSnapshot();
+ });
+});
+describe('ilm filter extension', () => {
+ test('should return empty array when no indices have index lifecycle policy', () => {
+ const extension = ilmFilterExtension([
+ indexWithoutLifecyclePolicy,
+ indexWithoutLifecyclePolicy,
+ ]);
+ expect(extension.length).toBe(0);
+ });
+ test('should return extension when any index has lifecycle policy', () => {
+ const extension = ilmFilterExtension([
+ indexWithLifecyclePolicy,
+ indexWithoutLifecyclePolicy,
+ indexWithoutLifecyclePolicy,
+ ]);
+ expect(extension).toBeDefined();
+ expect(extension).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/index.js b/x-pack/plugins/index_lifecycle_management/common/constants/index.js
new file mode 100644
index 0000000000000..5525b2b86f4d1
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.js
@@ -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 const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/';
+export const PLUGIN_ID = 'index_lifecycle_management';
diff --git a/x-pack/plugins/index_lifecycle_management/index.js b/x-pack/plugins/index_lifecycle_management/index.js
new file mode 100644
index 0000000000000..cac3aa371411f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/index.js
@@ -0,0 +1,63 @@
+/*
+ * 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 { resolve } from 'path';
+import { registerTemplatesRoutes } from './server/routes/api/templates';
+import { registerNodesRoutes } from './server/routes/api/nodes';
+import { registerPoliciesRoutes } from './server/routes/api/policies';
+import { registerLifecycleRoutes } from './server/routes/api/lifecycle';
+import { registerIndexRoutes } from './server/routes/api/index';
+import { registerLicenseChecker } from './server/lib/register_license_checker';
+import { PLUGIN_ID } from './common/constants';
+import { indexLifecycleDataEnricher } from './index_lifecycle_data';
+export function indexLifecycleManagement(kibana) {
+ return new kibana.Plugin({
+ config: (Joi) => {
+ return Joi.object({
+ enabled: Joi.boolean().default(true),
+ ui: Joi.object({
+ enabled: Joi.boolean().default(true),
+ }).default(),
+ filteredNodeAttributes: Joi.array().items(Joi.string()).default([])
+ }).default();
+ },
+ id: PLUGIN_ID,
+ publicDir: resolve(__dirname, 'public'),
+ configPrefix: 'xpack.ilm',
+ require: ['kibana', 'elasticsearch', 'xpack_main', 'index_management'],
+ uiExports: {
+ styleSheetPaths: `${__dirname}/public/index.scss`,
+ managementSections: ['plugins/index_lifecycle_management'],
+ injectDefaultVars(server) {
+ const config = server.config();
+ return {
+ ilmUiEnabled: config.get('xpack.ilm.ui.enabled')
+ };
+ },
+ },
+ isEnabled(config) {
+ return (
+ config.get('xpack.ilm.enabled') &&
+ config.has('index_management.enabled') &&
+ config.get('index_management.enabled')
+ );
+ },
+ init: function (server) {
+ registerLicenseChecker(server);
+ registerTemplatesRoutes(server);
+ registerNodesRoutes(server);
+ registerPoliciesRoutes(server);
+ registerLifecycleRoutes(server);
+ registerIndexRoutes(server);
+ if (
+ server.plugins.index_management &&
+ server.plugins.index_management.addIndexManagementDataEnricher
+ ) {
+ server.plugins.index_management.addIndexManagementDataEnricher(indexLifecycleDataEnricher);
+ }
+ },
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/index_lifecycle_data.js b/x-pack/plugins/index_lifecycle_management/index_lifecycle_data.js
new file mode 100644
index 0000000000000..57b90204ce224
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/index_lifecycle_data.js
@@ -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.
+ */
+
+export const indexLifecycleDataEnricher = async (indicesList, callWithRequest) => {
+ if (!indicesList || !indicesList.length) {
+ return;
+ }
+ const params = {
+ path: '/*/_ilm/explain',
+ method: 'GET',
+ };
+ const { indices: ilmIndicesData } = await callWithRequest('transport.request', params);
+ return indicesList.map(index => {
+ return {
+ ...index,
+ ilm: { ...(ilmIndicesData[index.name] || {}) },
+ };
+ });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/_index_lifecycle_management.scss b/x-pack/plugins/index_lifecycle_management/public/_index_lifecycle_management.scss
new file mode 100644
index 0000000000000..341ed8a576d4a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/_index_lifecycle_management.scss
@@ -0,0 +1,12 @@
+.policyTable__horizontalScrollContainer {
+ overflow-x: auto;
+ max-width: 100%;
+ height: 100vh;
+}
+.policyTable__horizontalScroll {
+ min-width: 800px;
+ width: 100%;
+}
+.ilmEditPolicyPageContent {
+ max-width: 1200px !important;
+}
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/app.js b/x-pack/plugins/index_lifecycle_management/public/app.js
new file mode 100644
index 0000000000000..e316d11017a14
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/app.js
@@ -0,0 +1,21 @@
+/*
+ * 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 { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
+import { EditPolicy } from './sections/edit_policy';
+import { PolicyTable } from './sections/policy_table';
+import { BASE_PATH } from '../common/constants';
+
+export const App = () => (
+
+
+
+
+
+
+
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js
new file mode 100644
index 0000000000000..5cec2ac4dd73e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js
@@ -0,0 +1,281 @@
+/*
+ * 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, { Component } from 'react';
+import { get } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiLink,
+ EuiSelect,
+ EuiForm,
+ EuiFormRow,
+ EuiOverlayMask,
+ EuiConfirmModal,
+ EuiModal,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiCallOut,
+ EuiModalHeaderTitle,
+} from '@elastic/eui';
+import { BASE_PATH } from '../../../common/constants';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { toastNotifications } from 'ui/notify';
+import { loadPolicies, addLifecyclePolicyToIndex } from '../../services/api';
+import { showApiError } from '../../services/api_errors';
+export class AddLifecyclePolicyConfirmModal extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ policies: [],
+ selectedPolicyName: '',
+ selectedAlias: '',
+ };
+ }
+ addPolicy = async () => {
+ const { indexName, httpClient, closeModal, reloadIndices } = this.props;
+ const { selectedPolicyName, selectedAlias } = this.state;
+ if (!selectedPolicyName) {
+ this.setState({ policyError: i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage',
+ { defaultMessage: 'You must select a policy.' }) });
+ return;
+ }
+ try {
+ const body = {
+ indexName,
+ policyName: selectedPolicyName,
+ alias: selectedAlias,
+ };
+ await addLifecyclePolicyToIndex(body, httpClient);
+ closeModal();
+ toastNotifications.addSuccess(
+ i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.addPolicyToIndexSuccess',
+ {
+ defaultMessage: 'Added policy {policyName} to index {indexName}.',
+ values: {
+ policyName: selectedPolicyName,
+ indexName,
+ },
+ }
+ )
+ );
+ reloadIndices();
+ } catch (err) {
+ showApiError(
+ err,
+ i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.addPolicyToIndexError',
+ {
+ defaultMessage: 'Error adding policy to index',
+ }
+ )
+ );
+ }
+ };
+ renderAliasFormElement = selectedPolicy => {
+ const { selectedAlias } = this.state;
+ const { index } = this.props;
+ const showAliasSelect =
+ selectedPolicy && get(selectedPolicy, 'policy.phases.hot.actions.rollover');
+ if (!showAliasSelect) {
+ return null;
+ }
+ const { aliases } = index;
+ if (aliases === 'none') {
+ return (
+
+ }
+ color="warning"
+ >
+
+
+ );
+ }
+ const aliasOptions = aliases.map(alias => {
+ return {
+ text: alias,
+ value: alias,
+ };
+ });
+ aliasOptions.unshift({
+ text: i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.chooseAliasMessage',
+ {
+ defaultMessage: 'Choose an alias',
+ }
+ ),
+ value: '',
+ });
+ return (
+
+ }
+ >
+ {
+ this.setState({ selectedAlias: e.target.value });
+ }}
+ />
+
+ );
+ };
+ renderForm() {
+ const { policies, selectedPolicyName, policyError } = this.state;
+ const selectedPolicy = selectedPolicyName
+ ? policies.find(policy => policy.name === selectedPolicyName)
+ : null;
+
+ const options = policies.map(({ name }) => {
+ return {
+ value: name,
+ text: name,
+ };
+ });
+ options.unshift({
+ value: '',
+ text: i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.choosePolicyMessage',
+ {
+ defaultMessage: 'Select a lifecycle policy',
+ }
+ ),
+ });
+ return (
+
+
+ }
+ >
+ {
+ this.setState({ policyError: null, selectedPolicyName: e.target.value });
+ }}
+ />
+
+ {this.renderAliasFormElement(selectedPolicy)}
+
+ );
+ }
+ async componentDidMount() {
+ try {
+ const policies = await loadPolicies(false, this.props.httpClient);
+ this.setState({ policies });
+ } catch (err) {
+ showApiError(
+ err,
+ i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.loadPolicyError',
+ {
+ defaultMessage: 'Error loading policy list',
+ }
+ )
+ );
+ this.props.closeModal();
+ }
+ }
+ render() {
+ const { policies } = this.state;
+ const { indexName, closeModal } = this.props;
+ const title = (
+
+ );
+ if (!policies.length) {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+
+ }
+ color="warning"
+ >
+
+
+
+
+
+
+
+
+
+ );
+ }
+ return (
+
+
+ }
+ confirmButtonText={
+
+ }
+ >
+ {this.renderForm()}
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js
new file mode 100644
index 0000000000000..697f94ecdec09
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js
@@ -0,0 +1,207 @@
+/*
+ * 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, { Component, Fragment } from 'react';
+import moment from 'moment-timezone';
+import {
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+ EuiSpacer,
+ EuiTitle,
+ EuiLink,
+ EuiPopover,
+ EuiPopoverTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { getPolicyPath } from '../../services/navigation';
+const getHeaders = () => {
+ return {
+ policy: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', {
+ defaultMessage: 'Lifecycle policy',
+ }),
+ phase: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader', {
+ defaultMessage: 'Current phase',
+ }),
+ action: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.nextActionHeader', {
+ defaultMessage: 'Next action',
+ }),
+ action_time_millis: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.nextActionTimeHeader', {
+ defaultMessage: 'Next action time',
+ }),
+ failed_step: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader', {
+ defaultMessage: 'Failed step',
+ }),
+ };
+};
+export class IndexLifecycleSummary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ showStackPopover: false,
+ showPhaseExecutionPopover: false,
+ };
+ }
+ toggleStackPopover = () => {
+ this.setState({ showStackPopover: !this.state.showStackPopover });
+ }
+ closeStackPopover = () => {
+ this.setState({ showStackPopover: false });
+ }
+ togglePhaseExecutionPopover = () => {
+ this.setState({ showPhaseExecutionPopover: !this.state.showPhaseExecutionPopover });
+ }
+ closePhaseExecutionPopover = () => {
+ this.setState({ showPhaseExecutionPopover: false });
+ }
+ renderStackPopoverButton(ilm) {
+ const button = (
+
+
+
+ );
+ return (
+
+
+
{ilm.step_info.stack_trace}
+
+
+ );
+ }
+ renderPhaseExecutionPopoverButton(ilm) {
+ if (!ilm.phase_execution) {
+ return null;
+ }
+ const button = (
+
+
+
+ );
+ return (
+
+
+
+
+
+
{JSON.stringify(ilm.phase_execution, null, 2)}
+
+
+ );
+ }
+ buildRows() {
+ const { index: { ilm = {} } } = this.props;
+ const headers = getHeaders();
+ const rows = {
+ left: [],
+ right: []
+ };
+ Object.keys(headers).forEach((fieldName, arrayIndex) => {
+ const value = ilm[fieldName];
+ let content;
+ if (fieldName === 'action_time') {
+ content = moment(value).format('YYYY-MM-DD HH:mm:ss');
+ } else if (fieldName === 'policy') {
+ content = ({value});
+ } else {
+ content = value;
+ }
+ content = content || '-';
+ const cell = [
+
+ {headers[fieldName]}
+ ,
+
+ {content}
+
+ ];
+ if (arrayIndex % 2 === 0) {
+ rows.left.push(cell);
+ } else {
+ rows.right.push(cell);
+ }
+ });
+ rows.right.push(this.renderPhaseExecutionPopoverButton(ilm));
+ return rows;
+ }
+
+ render() {
+ const { index: { ilm = {} } } = this.props;
+ if (!ilm.managed) {
+ return null;
+ }
+ const { left, right } = this.buildRows();
+ return (
+
+
+
+
+
+
+ { ilm.step_info && ilm.step_info.type ? (
+
+
+
+ }
+ iconType="cross"
+ >
+ {ilm.step_info.type}: {ilm.step_info.reason}
+
+ {this.renderStackPopoverButton(ilm)}
+
+
+ ) : null}
+
+
+
+
+ {left}
+
+
+
+
+ {right}
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js
new file mode 100644
index 0000000000000..42795a03a7704
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.js
@@ -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, { Component } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiOverlayMask,
+ EuiConfirmModal,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { toastNotifications } from 'ui/notify';
+import { removeLifecycleForIndex } from '../../services/api';
+import { showApiError } from '../../services/api_errors';
+export class RemoveLifecyclePolicyConfirmModal extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ policies: [],
+ selectedPolicyName: null,
+ selectedAlias: null,
+ };
+ }
+ oneIndexSelected = () => {
+ return this.props.indexNames.length === 1;
+ };
+ getEntity = oneIndexSelected => {
+ return oneIndexSelected ? (
+
+ ) : (
+
+ );
+ };
+ removePolicy = async () => {
+ const { indexNames, httpClient, closeModal, reloadIndices } = this.props;
+ const target = this. getTarget();
+ try {
+ await removeLifecycleForIndex(indexNames, httpClient);
+ closeModal();
+ toastNotifications.addSuccess(
+ i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.removeLifecyclePolicyConfirmModal.removePolicySuccess',
+ {
+ defaultMessage: 'Removed lifecycle policy from {target}',
+ values: {
+ target
+ },
+ }
+ )
+ );
+ reloadIndices();
+ } catch (err) {
+ showApiError(
+ err,
+ i18n.translate(
+ 'xpack.indexLifecycleMgmt.indexManagementTable.removeLifecyclePolicyConfirmModal.removePolicyToIndexError',
+ {
+ defaultMessage: 'Error removing policy',
+ }
+ )
+ );
+ }
+ };
+ getTarget() {
+ const { indexNames } = this.props;
+ const oneIndexSelected = this.oneIndexSelected();
+ const entity = this.getEntity(oneIndexSelected);
+ return oneIndexSelected ? (indexNames[0]) : `${indexNames.length} ${entity}`;
+ }
+ render() {
+
+ const { closeModal } = this.props;
+ const target = this. getTarget();
+ return (
+
+
+ }
+ onCancel={closeModal}
+ onConfirm={this.removePolicy}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ >
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js
new file mode 100644
index 0000000000000..2e084dd08ac10
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js
@@ -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 from 'react';
+import { IndexLifecycleSummary } from './components/index_lifecycle_summary';
+import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal';
+import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal';
+import { get, every, any } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import {
+ addSummaryExtension,
+ addBannerExtension,
+ addActionExtension,
+ addFilterExtension,
+} from '../../../index_management/public/index_management_extensions';
+import { retryLifecycleForIndex } from '../services/api';
+import { EuiSearchBar } from '@elastic/eui';
+
+const stepPath = 'ilm.step';
+export const retryLifecycleActionExtension = indices => {
+ const allHaveErrors = every(indices, index => {
+ return index.ilm && index.ilm.failed_step;
+ });
+ if (!allHaveErrors) {
+ return null;
+ }
+ const indexNames = indices.map(({ name }) => name);
+ return {
+ requestMethod: retryLifecycleForIndex,
+ icon: 'play',
+ indexNames: [indexNames],
+ buttonLabel: i18n.translate('xpack.idxMgmt.retryIndexLifecycleActionButtonLabel', {
+ defaultMessage: 'Retry lifecycle step',
+ }),
+ successMessage: i18n.translate(
+ 'xpack.idxMgmt.retryIndexLifecycleAction.retriedLifecycleMessage',
+ {
+ defaultMessage: 'Called retry lifecycle step for: {indexNames}',
+ values: { indexNames: indexNames.map(indexName => `"${indexName}"`).join(', ') },
+ }
+ ),
+ };
+};
+
+export const removeLifecyclePolicyActionExtension = (indices, reloadIndices) => {
+ const allHaveIlm = every(indices, index => {
+ return index.ilm && index.ilm.managed;
+ });
+ if (!allHaveIlm) {
+ return null;
+ }
+ const indexNames = indices.map(({ name }) => name);
+ return {
+ renderConfirmModal: (closeModal, httpClient) => {
+ return (
+
+ );
+ },
+ icon: 'stopFilled',
+ indexNames: [indexNames],
+ buttonLabel: i18n.translate('xpack.idxMgmt.removeIndexLifecycleActionButtonLabel', {
+ defaultMessage: 'Remove lifecycle policy',
+ }),
+ };
+};
+
+export const addLifecyclePolicyActionExtension = (indices, reloadIndices) => {
+ if (indices.length !== 1) {
+ return null;
+ }
+ const index = indices[0];
+ const hasIlm = index.ilm && index.ilm.managed;
+
+ if (hasIlm) {
+ return null;
+ }
+ const indexName = index.name;
+ return {
+ renderConfirmModal: (closeModal, httpClient) => {
+ return (
+
+ );
+ },
+ icon: 'plusInCircle',
+ buttonLabel: i18n.translate('xpack.idxMgmt.addLifecyclePolicyActionButtonLabel', {
+ defaultMessage: 'Add lifecycle policy',
+ }),
+ };
+};
+
+addActionExtension(retryLifecycleActionExtension);
+addActionExtension(removeLifecyclePolicyActionExtension);
+addActionExtension(addLifecyclePolicyActionExtension);
+
+export const ilmBannerExtension = indices => {
+ const { Query } = EuiSearchBar;
+ if (!indices.length) {
+ return null;
+ }
+ const indicesWithLifecycleErrors = indices.filter(index => {
+ return get(index, stepPath) === 'ERROR';
+ });
+ const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length;
+ if (!numIndicesWithLifecycleErrors) {
+ return null;
+ }
+ return {
+ type: 'warning',
+ filter: Query.parse(`${stepPath}:ERROR`),
+ filterLabel: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel', {
+ defaultMessage: 'Show errors',
+ }),
+ title: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.errorMessage', {
+ defaultMessage: `{ numIndicesWithLifecycleErrors, number}
+ {numIndicesWithLifecycleErrors, plural, one {index has} other {indices have} }
+ lifecycle errors`,
+ values: { numIndicesWithLifecycleErrors },
+ }),
+ };
+};
+
+addBannerExtension(ilmBannerExtension);
+
+export const ilmSummaryExtension = index => {
+ return ;
+};
+
+addSummaryExtension(ilmSummaryExtension);
+
+export const ilmFilterExtension = indices => {
+ const hasIlm = any(indices, index => index.ilm && index.ilm.managed);
+ if (!hasIlm) {
+ return [];
+ } else {
+ return [
+ {
+ type: 'field_value_selection',
+ name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel', {
+ defaultMessage: 'Lifecycle status',
+ }),
+ multiSelect: false,
+ field: 'ilm.managed',
+ options: [
+ {
+ value: true,
+ view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel', {
+ defaultMessage: 'Managed',
+ }),
+ },
+ {
+ value: false,
+ view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel', {
+ defaultMessage: 'Unmanaged',
+ }),
+ },
+ ],
+ },
+ {
+ type: 'field_value_selection',
+ field: 'ilm.phase',
+ name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', {
+ defaultMessage: 'Lifecycle phase',
+ }),
+ multiSelect: 'or',
+ options: [
+ {
+ value: 'hot',
+ view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', {
+ defaultMessage: 'Hot',
+ }),
+ },
+ {
+ value: 'warm',
+ view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', {
+ defaultMessage: 'Warm',
+ }),
+ },
+ {
+ value: 'cold',
+ view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', {
+ defaultMessage: 'Cold',
+ }),
+ },
+ {
+ value: 'delete',
+ view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', {
+ defaultMessage: 'Delete',
+ }),
+ },
+ ],
+ },
+ ];
+ }
+};
+
+addFilterExtension(ilmFilterExtension);
diff --git a/x-pack/plugins/index_lifecycle_management/public/index.js b/x-pack/plugins/index_lifecycle_management/public/index.js
new file mode 100644
index 0000000000000..58ba3054db009
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/index.js
@@ -0,0 +1,9 @@
+/*
+ * 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 './register_management_section';
+import './register_routes';
+import './extend_index_management';
diff --git a/x-pack/plugins/index_lifecycle_management/public/index.scss b/x-pack/plugins/index_lifecycle_management/public/index.scss
new file mode 100644
index 0000000000000..f97f8a43731cb
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/index.scss
@@ -0,0 +1,3 @@
+// Import the EUI global scope so we can use EUI constants
+@import 'ui/public/styles/_styling_constants';
+@import 'index_lifecycle_management';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/main.html b/x-pack/plugins/index_lifecycle_management/public/main.html
new file mode 100644
index 0000000000000..d54cba0632cdf
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/main.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/register_management_section.js b/x-pack/plugins/index_lifecycle_management/public/register_management_section.js
new file mode 100644
index 0000000000000..5dd3ea151e7be
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_management_section.js
@@ -0,0 +1,21 @@
+/*
+ * 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 chrome from 'ui/chrome';
+import { management } from 'ui/management';
+import { i18n } from '@kbn/i18n';
+import { BASE_PATH } from '../common/constants';
+const esSection = management.getSection('elasticsearch');
+if (chrome.getInjected('ilmUiEnabled')) {
+ esSection.register('index_lifecycle_policies', {
+ visible: true,
+ display: i18n.translate('xpack.indexLifecycleMgmt.appTitle', {
+ defaultMessage: 'Index Lifecycle Policies',
+ }),
+ order: 2,
+ url: `#${BASE_PATH}policies`,
+ });
+}
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/register_routes.js b/x-pack/plugins/index_lifecycle_management/public/register_routes.js
new file mode 100644
index 0000000000000..643ac68af6131
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_routes.js
@@ -0,0 +1,59 @@
+/*
+ * 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 { render, unmountComponentAtNode } from 'react-dom';
+import { Provider } from 'react-redux';
+import { setHttpClient } from './services/api';
+import chrome from 'ui/chrome';
+import { App } from './app';
+import { BASE_PATH } from '../common/constants';
+import { indexLifecycleManagementStore } from './store';
+import { I18nProvider } from '@kbn/i18n/react';
+import { setUrlService } from './services/navigation';
+
+import routes from 'ui/routes';
+
+import template from './main.html';
+import { manageAngularLifecycle } from './services/manage_angular_lifecycle';
+let elem;
+const renderReact = async (elem) => {
+ render(
+
+
+
+
+ ,
+ elem
+ );
+};
+if (chrome.getInjected('ilmUiEnabled')) {
+ routes.when(`${BASE_PATH}:view?/:action?/:id?`, {
+ template: template,
+ controllerAs: 'indexLifecycleManagement',
+ controller: class IndexLifecycleManagementController {
+ constructor($scope, $route, $http, kbnUrl, $rootScope) {
+ // clean up previously rendered React app if one exists
+ // this happens because of React Router redirects
+ elem && unmountComponentAtNode(elem);
+ setHttpClient($http);
+ setUrlService({
+ change(url) {
+ kbnUrl.change(url);
+ $rootScope.$digest();
+ }
+ });
+ $scope.$$postDigest(() => {
+ elem = document.getElementById('indexLifecycleManagementReactRoot');
+ renderReact(elem);
+ manageAngularLifecycle($scope, $route, elem);
+ });
+ }
+ }
+ });
+}
+
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/components/active_badge.js b/x-pack/plugins/index_lifecycle_management/public/sections/components/active_badge.js
new file mode 100644
index 0000000000000..4a270c9e002f0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/components/active_badge.js
@@ -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.
+ */
+import React from 'react';
+import { EuiBadge } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+export const ActiveBadge = () => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/components/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/components/index.js
new file mode 100644
index 0000000000000..e87d7b34b8f68
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/components/index.js
@@ -0,0 +1,10 @@
+/*
+ * 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 { ActiveBadge } from './active_badge';
+export { LearnMoreLink } from './learn_more_link';
+export { PhaseErrorMessage } from './phase_error_message';
+export { OptionalLabel } from './optional_label';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/components/learn_more_link.js b/x-pack/plugins/index_lifecycle_management/public/sections/components/learn_more_link.js
new file mode 100644
index 0000000000000..38192f6391072
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/components/learn_more_link.js
@@ -0,0 +1,37 @@
+/*
+ * 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 { EuiLink } from '@elastic/eui';
+import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
+
+
+export class LearnMoreLinkUi extends React.PureComponent {
+ render() {
+ const { href, docPath, text } = this.props;
+ let url;
+ if (docPath) {
+ url = `${esBase}${docPath}`;
+ } else {
+ url = href;
+ }
+ const content = text ? text : (
+
+ );
+ return (
+
+ {content}
+
+ );
+
+ }
+}
+export const LearnMoreLink = injectI18n(LearnMoreLinkUi);
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/components/optional_label.js b/x-pack/plugins/index_lifecycle_management/public/sections/components/optional_label.js
new file mode 100644
index 0000000000000..7b43c5fe2e842
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/components/optional_label.js
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+export const OptionalLabel = () => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/components/phase_error_message.js b/x-pack/plugins/index_lifecycle_management/public/sections/components/phase_error_message.js
new file mode 100644
index 0000000000000..4c284494c17fb
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/components/phase_error_message.js
@@ -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.
+ */
+import React from 'react';
+import { EuiBadge } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+export const PhaseErrorMessage = ({ isShowingErrors }) => {
+ return isShowingErrors ? (
+
+
+
+ ) : null;
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.container.js
new file mode 100644
index 0000000000000..bf6a506758829
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.container.js
@@ -0,0 +1,31 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { ColdPhase as PresentationComponent } from './cold_phase';
+import {
+ getPhase,
+ getPhaseData
+} from '../../../../store/selectors';
+import { setPhaseData } from '../../../../store/actions';
+import {
+ PHASE_COLD,
+ PHASE_WARM,
+ PHASE_REPLICA_COUNT
+} from '../../../../store/constants';
+
+export const ColdPhase = connect(
+ (state) => ({
+ phaseData: getPhase(state, PHASE_COLD),
+ warmPhaseReplicaCount: getPhaseData(state, PHASE_WARM, PHASE_REPLICA_COUNT)
+ }),
+ {
+ setPhaseData: (key, value) => setPhaseData(PHASE_COLD, key, value),
+ }
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.js
new file mode 100644
index 0000000000000..6b295f822069b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.js
@@ -0,0 +1,185 @@
+/*
+ * 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, { PureComponent, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiFormRow,
+ EuiFieldNumber,
+ EuiButtonEmpty,
+ EuiDescribedFormGroup,
+ EuiButton,
+} from '@elastic/eui';
+import {
+ PHASE_COLD,
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_ALIAS,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+ PHASE_NODE_ATTRS,
+ PHASE_REPLICA_COUNT,
+} from '../../../../store/constants';
+import { ErrableFormRow } from '../../form_errors';
+import { MinAgeInput } from '../min_age_input';
+import { ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components';
+import { NodeAllocation } from '../node_allocation';
+
+class ColdPhaseUi extends PureComponent {
+ static propTypes = {
+ setPhaseData: PropTypes.func.isRequired,
+ showNodeDetailsFlyout: PropTypes.func.isRequired,
+
+ isShowingErrors: PropTypes.bool.isRequired,
+ errors: PropTypes.object.isRequired,
+ phaseData: PropTypes.shape({
+ [PHASE_ENABLED]: PropTypes.bool.isRequired,
+ [PHASE_ROLLOVER_ALIAS]: PropTypes.string.isRequired,
+ [PHASE_ROLLOVER_MINIMUM_AGE]: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
+ .isRequired,
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: PropTypes.string.isRequired,
+ [PHASE_NODE_ATTRS]: PropTypes.string.isRequired,
+ [PHASE_REPLICA_COUNT]: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ }).isRequired,
+ warmPhaseReplicaCount: PropTypes.number.isRequired,
+ };
+ render() {
+ const {
+ setPhaseData,
+ showNodeDetailsFlyout,
+ phaseData,
+ warmPhaseReplicaCount,
+ errors,
+ isShowingErrors,
+ } = this.props;
+
+ return (
+
+
+
+ {' '}
+ {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null}
+
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+ {phaseData[PHASE_ENABLED] ? (
+ {
+ await setPhaseData(PHASE_ENABLED, false);
+ }}
+ aria-controls="coldPhaseContent"
+ >
+
+
+ ) : (
+ {
+ await setPhaseData(PHASE_ENABLED, true);
+ }}
+ aria-controls="coldPhaseContent"
+ >
+
+
+ )}
+
+ }
+ fullWidth
+ >
+
+ {phaseData[PHASE_ENABLED] ? (
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ errorKey={PHASE_REPLICA_COUNT}
+ isShowingErrors={isShowingErrors}
+ errors={errors}
+ >
+
{
+ await setPhaseData(PHASE_REPLICA_COUNT, e.target.value);
+ }}
+ min={0}
+ />
+
+
+
+
+ setPhaseData(PHASE_REPLICA_COUNT, warmPhaseReplicaCount)}
+ >
+ Set to same number as warm phase
+
+
+
+
+
+ ) : }
+
+
+ );
+ }
+}
+export const ColdPhase = injectI18n(ColdPhaseUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/index.js
new file mode 100644
index 0000000000000..e0d70ceb57726
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/index.js
@@ -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 { ColdPhase } from './cold_phase.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.container.js
new file mode 100644
index 0000000000000..dcb0f9eb63107
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.container.js
@@ -0,0 +1,20 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { DeletePhase as PresentationComponent } from './delete_phase';
+import { getPhase } from '../../../../store/selectors';
+import { setPhaseData } from '../../../../store/actions';
+import { PHASE_DELETE } from '../../../../store/constants';
+
+export const DeletePhase = connect(
+ state => ({
+ phaseData: getPhase(state, PHASE_DELETE)
+ }),
+ {
+ setPhaseData: (key, value) => setPhaseData(PHASE_DELETE, key, value)
+ }
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.js
new file mode 100644
index 0000000000000..698ff73fe5cd3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.js
@@ -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, { PureComponent, Fragment } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import PropTypes from 'prop-types';
+import { MinAgeInput } from '../min_age_input';
+
+import {
+ EuiDescribedFormGroup,
+ EuiButton,
+} from '@elastic/eui';
+import {
+ PHASE_DELETE,
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+} from '../../../../store/constants';
+import { ActiveBadge, PhaseErrorMessage } from '../../../components';
+
+export class DeletePhase extends PureComponent {
+ static propTypes = {
+ setPhaseData: PropTypes.func.isRequired,
+ isShowingErrors: PropTypes.bool.isRequired,
+ errors: PropTypes.object.isRequired,
+ phaseData: PropTypes.shape({
+ [PHASE_ENABLED]: PropTypes.bool.isRequired,
+ [PHASE_ROLLOVER_MINIMUM_AGE]: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string
+ ]).isRequired,
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: PropTypes.string.isRequired
+ }).isRequired
+ };
+
+ render() {
+ const {
+ setPhaseData,
+ phaseData,
+ errors,
+ isShowingErrors,
+ } = this.props;
+
+ return (
+
+
+
+ {' '}
+ {phaseData[PHASE_ENABLED] && !isShowingErrors ? (
+
+ ) : null}
+
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+
+ {phaseData[PHASE_ENABLED] ? (
+ {
+ await setPhaseData(PHASE_ENABLED, false);
+ }}
+ aria-controls="deletePhaseContent"
+ >
+
+
+ ) : (
+ {
+ await setPhaseData(PHASE_ENABLED, true);
+ }}
+ aria-controls="deletePhaseContent"
+ >
+
+
+ )}
+
+ }
+ fullWidth
+ >
+
+ {phaseData[PHASE_ENABLED] ? (
+
+ ) :
}
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/index.js
new file mode 100644
index 0000000000000..5f909ab2c0f79
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/index.js
@@ -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 { DeletePhase } from './delete_phase.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.container.js
new file mode 100644
index 0000000000000..32a7100e3f646
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.container.js
@@ -0,0 +1,23 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { HotPhase as PresentationComponent } from './hot_phase';
+import { getPhase } from '../../../../store/selectors';
+import { setPhaseData } from '../../../../store/actions';
+import { PHASE_HOT } from '../../../../store/constants';
+
+export const HotPhase = connect(
+ state => ({
+ phaseData: getPhase(state, PHASE_HOT)
+ }),
+ {
+ setPhaseData: (key, value) => setPhaseData(PHASE_HOT, key, value)
+ },
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.js
new file mode 100644
index 0000000000000..ef7ac076a8e3b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.js
@@ -0,0 +1,255 @@
+/*
+ * 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, { Fragment, PureComponent } from 'react';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import PropTypes from 'prop-types';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiFieldNumber,
+ EuiSelect,
+ EuiSwitch,
+ EuiFormRow,
+ EuiDescribedFormGroup,
+} from '@elastic/eui';
+import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../../components';
+import {
+ PHASE_HOT,
+ PHASE_ROLLOVER_ALIAS,
+ PHASE_ROLLOVER_MAX_AGE,
+ PHASE_ROLLOVER_MAX_AGE_UNITS,
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+ PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS,
+ PHASE_ROLLOVER_ENABLED,
+ MAX_SIZE_TYPE_DOCUMENT
+} from '../../../../store/constants';
+
+import { ErrableFormRow } from '../../form_errors';
+
+class HotPhaseUi extends PureComponent {
+ static propTypes = {
+ setPhaseData: PropTypes.func.isRequired,
+
+ isShowingErrors: PropTypes.bool.isRequired,
+ errors: PropTypes.object.isRequired,
+ phaseData: PropTypes.shape({
+ [PHASE_ROLLOVER_ALIAS]: PropTypes.string.isRequired,
+ [PHASE_ROLLOVER_MAX_AGE]: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string
+ ]).isRequired,
+ [PHASE_ROLLOVER_MAX_AGE_UNITS]: PropTypes.string.isRequired,
+ [PHASE_ROLLOVER_MAX_SIZE_STORED]: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string
+ ]).isRequired,
+ [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: PropTypes.string.isRequired
+ }).isRequired
+ };
+
+ render() {
+ const {
+ setPhaseData,
+ phaseData,
+ isShowingErrors,
+ errors,
+ intl
+ } = this.props;
+
+ return (
+
+
+
+ {' '}
+ {isShowingErrors ? null : }
+
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+
+ }
+ fullWidth
+ >
+
+
+
+
+
+ }
+ docPath="indices-rollover-index.html"
+ />
+
+
+ }
+ >
+ {
+ await setPhaseData(PHASE_ROLLOVER_ENABLED, e.target.checked);
+ }}
+ label={intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel',
+ defaultMessage: 'Enable rollover'
+ })}
+ />
+
+ {phaseData[PHASE_ROLLOVER_ENABLED] ? (
+
+
+
+
+
+ {
+ await setPhaseData(
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+ e.target.value
+ );
+ }}
+ min={1}
+ />
+
+
+
+
+ {
+ await setPhaseData(
+ PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS,
+ e.target.value
+ );
+ }}
+ options={[
+ { value: 'gb', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel',
+ defaultMessage: 'gigabytes'
+ }) },
+ { value: MAX_SIZE_TYPE_DOCUMENT, text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.hotPhase.documentsLabel',
+ defaultMessage: 'documents'
+ }) }
+ ]}
+ />
+
+
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_ROLLOVER_MAX_AGE, e.target.value);
+ }}
+ min={1}
+ />
+
+
+
+
+ {
+ await setPhaseData(
+ PHASE_ROLLOVER_MAX_AGE_UNITS,
+ e.target.value
+ );
+ }}
+ options={[
+ { value: 'd', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.hotPhase.daysLabel',
+ defaultMessage: 'days'
+ }) },
+ { value: 'h', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.hotPhase.hoursLabel',
+ defaultMessage: 'hours'
+ }) },
+ ]}
+ />
+
+
+
+
+ ) : null}
+
+ );
+ }
+}
+export const HotPhase = injectI18n(HotPhaseUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/index.js
new file mode 100644
index 0000000000000..114e34c3ef4d0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/index.js
@@ -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 { HotPhase } from './hot_phase.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/min_age_input.js
new file mode 100644
index 0000000000000..80e990095ae4c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/min_age_input.js
@@ -0,0 +1,70 @@
+/*
+ * 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 { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
+import {
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+} from '../../../store/constants';
+import { ErrableFormRow } from '../form_errors';
+import { injectI18n } from '@kbn/i18n/react';
+const MinAgeInputUi = props => {
+ const { errors, phaseData, phase, setPhaseData, isShowingErrors, intl } = props;
+ return (
+
+
+
+ {
+ setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE, e.target.value);
+ }}
+ min={1}
+ />
+
+
+
+
+ setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE_UNITS, e.target.value)}
+ options={[
+ {
+ value: 'd',
+ text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.editPolicy.daysLabel',
+ defaultMessage: 'days',
+ }),
+ },
+ {
+ value: 'h',
+ text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.editPolicy.hoursLabel',
+ defaultMessage: 'hours',
+ }),
+ },
+ ]}
+ />
+
+
+
+ );
+};
+export const MinAgeInput = injectI18n(MinAgeInputUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/index.js
new file mode 100644
index 0000000000000..dcf6a0787755d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/index.js
@@ -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 { NodeAllocation } from './node_allocation.container';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/node_allocation.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/node_allocation.container.js
new file mode 100644
index 0000000000000..d309c53eb295c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/node_allocation.container.js
@@ -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 { connect } from 'react-redux';
+import { NodeAllocation as PresentationComponent } from './node_allocation';
+import {
+ getNodeOptions,
+} from '../../../../store/selectors';
+import { fetchNodes } from '../../../../store/actions';
+
+export const NodeAllocation = connect(
+ (state) => ({
+ nodeOptions: getNodeOptions(state),
+ }),
+ {
+ fetchNodes
+ }
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/node_allocation.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/node_allocation.js
new file mode 100644
index 0000000000000..a6b3eaf02ee60
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_allocation/node_allocation.js
@@ -0,0 +1,116 @@
+/*
+ * 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, { Component, Fragment } from 'react';
+import { EuiSelect, EuiButtonEmpty, EuiCallOut, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import { PHASE_NODE_ATTRS } from '../../../../store/constants';
+import { ErrableFormRow } from '../../form_errors';
+import { LearnMoreLink } from '../../../components/learn_more_link';
+const learnMoreLinks = (
+
+
+
+
+ }
+ docPath="shards-allocation.html"
+ />
+
+);
+class NodeAllocationUi extends Component {
+ componentDidMount() {
+ this.props.fetchNodes();
+ }
+ render() {
+ const {
+ phase,
+ setPhaseData,
+ intl,
+ isShowingErrors,
+ phaseData,
+ showNodeDetailsFlyout,
+ nodeOptions,
+ errors
+ } = this.props;
+ if (!nodeOptions) {
+ return (
+
+
+
+
+ );
+ }
+ if (!nodeOptions.length) {
+ return (
+
+
+ }
+ color="warning"
+ >
+
+ {learnMoreLinks}
+
+
+
+
+ );
+ }
+ return (
+
+
+ {
+ await setPhaseData(PHASE_NODE_ATTRS, e.target.value);
+ }}
+ />
+
+
+ {!!phaseData[PHASE_NODE_ATTRS] ? (
+ showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])}
+ >
+
+
+ ) : }
+ {learnMoreLinks}
+
+
+ );
+ }
+}
+export const NodeAllocation = injectI18n(NodeAllocationUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/index.js
new file mode 100644
index 0000000000000..885e965c46c4b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/index.js
@@ -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 { NodeAttrsDetails } from './node_attrs_details.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js
new file mode 100644
index 0000000000000..8887b13740e04
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.container.js
@@ -0,0 +1,17 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { NodeAttrsDetails as PresentationComponent } from './node_attrs_details';
+import { getNodeDetails } from '../../../../store/selectors';
+import { fetchNodeDetails } from '../../../../store/actions';
+
+export const NodeAttrsDetails = connect(
+ (state, ownProps) => ({
+ details: getNodeDetails(state, ownProps.selectedNodeAttrs),
+ }),
+ { fetchNodeDetails }
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js
new file mode 100644
index 0000000000000..66329e448af6c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ EuiFlyoutBody,
+ EuiFlyout,
+ EuiTitle,
+ EuiInMemoryTable,
+ EuiSpacer,
+ EuiPortal,
+} from '@elastic/eui';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+
+
+export class NodeAttrsDetailsUi extends PureComponent {
+ static propTypes = {
+ fetchNodeDetails: PropTypes.func.isRequired,
+ close: PropTypes.func.isRequired,
+
+ details: PropTypes.array,
+ selectedNodeAttrs: PropTypes.string.isRequired,
+ };
+
+ componentWillMount() {
+ this.props.fetchNodeDetails(this.props.selectedNodeAttrs);
+ }
+
+ render() {
+ const { selectedNodeAttrs, details, close, intl } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+export const NodeAttrsDetails = injectI18n(NodeAttrsDetailsUi);
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js
new file mode 100644
index 0000000000000..6e553dfd36efa
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js
@@ -0,0 +1,85 @@
+/*
+ * 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, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { toastNotifications } from 'ui/notify';
+import copy from 'copy-to-clipboard';
+
+import {
+ EuiButton,
+ EuiCodeBlock,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiPortal,
+ EuiTitle,
+} from '@elastic/eui';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+
+
+export class PolicyJsonFlyoutUi extends PureComponent {
+ static propTypes = {
+ close: PropTypes.func.isRequired,
+ lifecycle: PropTypes.object.isRequired,
+ };
+ getEsJson({ phases }) {
+ return JSON.stringify({
+ policy: {
+ phases
+ }
+ }, null, 4);
+ }
+ copyToClipboard(lifecycle) {
+ const { intl } = this.props;
+ copy(this.getEsJson(lifecycle));
+ toastNotifications.add(intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.editPolicy.policyJsonFlyout.copiedToClipboardMessage',
+ defaultMessage: 'JSON copied to clipboard',
+ }));
+ }
+ render() {
+ const { lifecycle, close, policyName } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.getEsJson(lifecycle)}
+
+
+
+
+ this.copyToClipboard(lifecycle)}>
+
+
+
+
+
+ );
+ }
+}
+export const PolicyJsonFlyout = injectI18n(PolicyJsonFlyoutUi);
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/index.js
new file mode 100644
index 0000000000000..7eb5def486c87
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/index.js
@@ -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 { WarmPhase } from './warm_phase.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.container.js
new file mode 100644
index 0000000000000..5b66fcae24ec2
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.container.js
@@ -0,0 +1,26 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { WarmPhase as PresentationComponent } from './warm_phase';
+import {
+ getPhase,
+} from '../../../../store/selectors';
+import { setPhaseData } from '../../../../store/actions';
+import { PHASE_WARM, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../store/constants';
+
+export const WarmPhase = connect(
+ state => ({
+ phaseData: getPhase(state, PHASE_WARM),
+ hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED],
+ }),
+ {
+ setPhaseData: (key, value) => setPhaseData(PHASE_WARM, key, value),
+ }
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.js
new file mode 100644
index 0000000000000..3c6c0449ba391
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.js
@@ -0,0 +1,346 @@
+/*
+ * 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, { Fragment, PureComponent } from 'react';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import PropTypes from 'prop-types';
+import {
+ EuiTextColor,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiFormRow,
+ EuiFieldNumber,
+ EuiSwitch,
+ EuiDescribedFormGroup,
+ EuiButton,
+} from '@elastic/eui';
+import {
+ PHASE_WARM,
+ PHASE_ENABLED,
+ WARM_PHASE_ON_ROLLOVER,
+ PHASE_ROLLOVER_ALIAS,
+ PHASE_FORCE_MERGE_ENABLED,
+ PHASE_FORCE_MERGE_SEGMENTS,
+ PHASE_NODE_ATTRS,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_REPLICA_COUNT,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+ PHASE_SHRINK_ENABLED,
+} from '../../../../store/constants';
+import { NodeAllocation } from '../node_allocation';
+import { ErrableFormRow } from '../../form_errors';
+import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components';
+import { MinAgeInput } from '../min_age_input';
+class WarmPhaseUi extends PureComponent {
+ static propTypes = {
+ setPhaseData: PropTypes.func.isRequired,
+ showNodeDetailsFlyout: PropTypes.func.isRequired,
+
+ isShowingErrors: PropTypes.bool.isRequired,
+ errors: PropTypes.object.isRequired,
+ phaseData: PropTypes.shape({
+ [PHASE_ENABLED]: PropTypes.bool.isRequired,
+ [WARM_PHASE_ON_ROLLOVER]: PropTypes.bool.isRequired,
+ [PHASE_ROLLOVER_ALIAS]: PropTypes.string.isRequired,
+ [PHASE_FORCE_MERGE_ENABLED]: PropTypes.bool.isRequired,
+ [PHASE_FORCE_MERGE_SEGMENTS]: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
+ .isRequired,
+ [PHASE_NODE_ATTRS]: PropTypes.string.isRequired,
+ [PHASE_PRIMARY_SHARD_COUNT]: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
+ .isRequired,
+ [PHASE_REPLICA_COUNT]: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ [PHASE_ROLLOVER_MINIMUM_AGE]: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
+ .isRequired,
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: PropTypes.string.isRequired,
+ }).isRequired,
+ };
+ render() {
+ const {
+ setPhaseData,
+ showNodeDetailsFlyout,
+ phaseData,
+ errors,
+ isShowingErrors,
+ hotPhaseRolloverEnabled,
+ intl,
+ } = this.props;
+ const shrinkLabel = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel',
+ defaultMessage: 'Shrink index',
+ });
+ const moveToWarmPhaseOnRolloverLabel = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel',
+ defaultMessage: 'Move to warm phase on rollover',
+ });
+ const forcemergeLabel = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel',
+ defaultMessage: 'Force merge data',
+ });
+ return (
+
+
+
+
+ {' '}
+ {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null}
+
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+ {phaseData[PHASE_ENABLED] ? (
+ {
+ await setPhaseData(PHASE_ENABLED, false);
+ }}
+ aria-controls="warmPhaseContent"
+ >
+
+
+ ) : (
+ {
+ await setPhaseData(PHASE_ENABLED, true);
+ }}
+ aria-controls="warmPhaseContent"
+ >
+
+
+ )}
+
+ }
+ fullWidth
+ >
+
+
+ {phaseData[PHASE_ENABLED] ? (
+
+ {hotPhaseRolloverEnabled ? (
+
+ {
+ await setPhaseData(WARM_PHASE_ON_ROLLOVER, e.target.checked);
+ }}
+ />
+
+ ) : null}
+ {!phaseData[WARM_PHASE_ON_ROLLOVER] ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+ }
+ errorKey={PHASE_REPLICA_COUNT}
+ isShowingErrors={isShowingErrors}
+ errors={errors}
+ >
+ {
+ await setPhaseData(PHASE_REPLICA_COUNT, e.target.value);
+ }}
+ min={0}
+ />
+
+
+
+
+
+
+ ) : null }
+
+
+
+ {phaseData[PHASE_ENABLED] ? (
+
+
+
+
+ }
+ description={
+
+ {' '}
+
+
+ }
+ fullWidth
+ titleSize="xs"
+ >
+
+ {
+ await setPhaseData(PHASE_SHRINK_ENABLED, e.target.checked);
+ }}
+ label={shrinkLabel}
+ aria-label={shrinkLabel}
+ aria-controls="shrinkContent"
+ />
+
+ {phaseData[PHASE_SHRINK_ENABLED] ? (
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_PRIMARY_SHARD_COUNT, e.target.value);
+ }}
+ min={1}
+ />
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+ }
+ description={
+
+ {' '}
+
+
+ }
+ titleSize="xs"
+ fullWidth
+ >
+ {
+ await setPhaseData(PHASE_FORCE_MERGE_ENABLED, e.target.checked);
+ }}
+ aria-controls="forcemergeContent"
+ />
+
+
+
+ {phaseData[PHASE_FORCE_MERGE_ENABLED] ? (
+
+ {
+ await setPhaseData(PHASE_FORCE_MERGE_SEGMENTS, e.target.value);
+ }}
+ min={1}
+ />
+
+ ) : null}
+
+
+
+ ) : null}
+
+ );
+ }
+}
+export const WarmPhase = injectI18n(WarmPhaseUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.container.js
new file mode 100644
index 0000000000000..5b3a97bb13213
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.container.js
@@ -0,0 +1,51 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { EditPolicy as PresentationComponent } from './edit_policy';
+import {
+ getSaveAsNewPolicy,
+ getSelectedPolicy,
+ validateLifecycle,
+ getLifecycle,
+ getPolicies,
+ isPolicyListLoaded,
+ getIsNewPolicy,
+ getSelectedOriginalPolicyName
+} from '../../store/selectors';
+import {
+ setSelectedPolicy,
+ setSelectedPolicyName,
+ setSaveAsNewPolicy,
+ saveLifecyclePolicy,
+ fetchPolicies,
+} from '../../store/actions';
+import { findFirstError } from '../../services/find_errors';
+
+export const EditPolicy = connect(
+ state => {
+ const errors = validateLifecycle(state);
+ const firstError = findFirstError(errors);
+ return {
+ firstError,
+ errors,
+ selectedPolicy: getSelectedPolicy(state),
+ saveAsNewPolicy: getSaveAsNewPolicy(state),
+ lifecycle: getLifecycle(state),
+ policies: getPolicies(state),
+ isPolicyListLoaded: isPolicyListLoaded(state),
+ isNewPolicy: getIsNewPolicy(state),
+ originalPolicyName: getSelectedOriginalPolicyName(state),
+ };
+ },
+ {
+ setSelectedPolicy,
+ setSelectedPolicyName,
+ setSaveAsNewPolicy,
+ saveLifecyclePolicy,
+ fetchPolicies,
+ }
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js
new file mode 100644
index 0000000000000..d06b687d53788
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js
@@ -0,0 +1,351 @@
+/*
+ * 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, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { toastNotifications } from 'ui/notify';
+import { goToPolicyList } from '../../services/navigation';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiFieldText,
+ EuiPageContent,
+ EuiFormRow,
+ EuiTitle,
+ EuiText,
+ EuiSpacer,
+ EuiSwitch,
+ EuiHorizontalRule,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiDescribedFormGroup,
+} from '@elastic/eui';
+import { HotPhase } from './components/hot_phase';
+import { WarmPhase } from './components/warm_phase';
+import { DeletePhase } from './components/delete_phase';
+import { ColdPhase } from './components/cold_phase';
+import {
+ PHASE_HOT,
+ PHASE_COLD,
+ PHASE_DELETE,
+ PHASE_WARM,
+ STRUCTURE_POLICY_NAME,
+} from '../../store/constants';
+import { findFirstError } from '../../services/find_errors';
+import { NodeAttrsDetails } from './components/node_attrs_details';
+import { PolicyJsonFlyout } from './components/policy_json_flyout';
+import { ErrableFormRow } from './form_errors';
+import { LearnMoreLink } from '../components';
+class EditPolicyUi extends Component {
+ static propTypes = {
+ selectedPolicy: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isShowingErrors: false,
+ isShowingNodeDetailsFlyout: false,
+ selectedNodeAttrsForDetails: undefined,
+ isShowingPolicyJsonFlyout: false,
+ };
+ }
+ selectPolicy = policyName => {
+ const { setSelectedPolicy, policies } = this.props;
+ const selectedPolicy = policies.find(policy => {
+ return policy.name === policyName;
+ });
+ if (selectedPolicy) {
+ setSelectedPolicy(selectedPolicy);
+ }
+ };
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ const {
+ isPolicyListLoaded,
+ fetchPolicies,
+ match: {
+ params: { policyName },
+ } = { params: {} },
+ } = this.props;
+ if (policyName) {
+ const decodedPolicyName = decodeURIComponent(policyName);
+ if (isPolicyListLoaded) {
+ this.selectPolicy(decodedPolicyName);
+ } else {
+ fetchPolicies(true, () => {
+ this.selectPolicy(decodedPolicyName);
+ });
+ }
+ } else {
+ this.props.setSelectedPolicy(null);
+ }
+ }
+ backToPolicyList = () => {
+ this.props.setSelectedPolicy(null);
+ goToPolicyList();
+ };
+ submit = async () => {
+ const { intl } = this.props;
+ this.setState({ isShowingErrors: true });
+ const { saveLifecyclePolicy, lifecycle, saveAsNewPolicy, firstError } = this.props;
+ if (firstError) {
+ toastNotifications.addDanger(
+ intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage',
+ defaultMessage: 'Please fix the errors on this page.',
+ })
+ );
+ const element = document.getElementById(`${firstError}-row`);
+ if (element) {
+ element.scrollIntoView();
+ }
+ } else {
+ const success = await saveLifecyclePolicy(lifecycle, saveAsNewPolicy);
+ if (success) {
+ this.backToPolicyList();
+ }
+ }
+ };
+
+ showNodeDetailsFlyout = selectedNodeAttrsForDetails => {
+ this.setState({ isShowingNodeDetailsFlyout: true, selectedNodeAttrsForDetails });
+ };
+ showPolicyJsonFlyout = () => {
+ this.setState({ isShowingPolicyJsonFlyout: true });
+ };
+ render() {
+ const {
+ intl,
+ selectedPolicy,
+ errors,
+ setSaveAsNewPolicy,
+ saveAsNewPolicy,
+ setSelectedPolicyName,
+ isNewPolicy,
+ lifecycle,
+ originalPolicyName,
+ } = this.props;
+ const selectedPolicyName = selectedPolicy.name;
+ const { isShowingErrors } = this.state;
+
+ return (
+
+
+
+
+
+ {isNewPolicy
+ ? intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage',
+ defaultMessage: 'Create an index lifecycle policy',
+ })
+ : intl.formatMessage(
+ {
+ id: 'xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage',
+ defaultMessage: 'Edit index lifecycle policy {originalPolicyName}',
+ },
+ { originalPolicyName }
+ )}
+
+
+
+
+
+
+
+ {' '}
+
+ }
+ />
+
+
+
+
+ {isNewPolicy ? null : (
+
+
+
+
+
+
+
+ .{' '}
+
+
+
+
+
+
+ {
+ await setSaveAsNewPolicy(e.target.checked);
+ }}
+ label={
+
+
+
+ }
+ />
+
+
+ )}
+ {saveAsNewPolicy || isNewPolicy ? (
+
+
+
+
+
+ }
+ titleSize="s"
+ fullWidth
+ >
+
+ {
+ await setSelectedPolicyName(e.target.value);
+ }}
+ />
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {saveAsNewPolicy ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.isShowingNodeDetailsFlyout ? (
+ this.setState({ isShowingNodeDetailsFlyout: false })}
+ />
+ ) : null}
+ {this.state.isShowingPolicyJsonFlyout ? (
+ this.setState({ isShowingPolicyJsonFlyout: false })}
+ />
+ ) : null}
+
+
+
+
+ );
+ }
+}
+export const EditPolicy = injectI18n(EditPolicyUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/form_errors.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/form_errors.js
new file mode 100644
index 0000000000000..fc3c29c4beb0c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/form_errors.js
@@ -0,0 +1,36 @@
+/*
+ * 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, { cloneElement, Children, Fragment } from 'react';
+import { EuiFormRow } from '@elastic/eui';
+
+export const ErrableFormRow = ({
+ errorKey,
+ isShowingErrors,
+ errors,
+ children,
+ ...rest
+}) => {
+ return (
+ 0
+ }
+ error={errors[errorKey]}
+ {...rest}
+ >
+
+ {Children.map(children, child => cloneElement(child, {
+ isInvalid: isShowingErrors && errors[errorKey].length > 0,
+ }))}
+
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/index.js
new file mode 100644
index 0000000000000..e4154a76289b6
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/index.js
@@ -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 { EditPolicy } from './edit_policy.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/components/no_match/index.js
new file mode 100644
index 0000000000000..2cdbc4a7094a8
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/components/no_match/index.js
@@ -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 { NoMatch } from './no_match';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/components/no_match/no_match.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/components/no_match/no_match.js
new file mode 100644
index 0000000000000..3f8bc17c2b193
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/components/no_match/no_match.js
@@ -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.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const NoMatch = () => (
+
+
+
+);
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/index.js
new file mode 100644
index 0000000000000..63e8cdebd9771
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/no_match/index.js
@@ -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 { NoMatch } from './components/no_match';
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js
new file mode 100644
index 0000000000000..106a9887e833d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js
@@ -0,0 +1,216 @@
+/*
+ * 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, { Component, Fragment } from 'react';
+import { get, find } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import { LearnMoreLink } from '../../../components/learn_more_link';
+import {
+ EuiCallOut,
+ EuiSelect,
+ EuiForm,
+ EuiFormRow,
+ EuiOverlayMask,
+ EuiConfirmModal,
+ EuiFieldText,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../../services/api';
+import { showApiError } from '../../../../services/api_errors';
+export class AddPolicyToTemplateConfirmModalUi extends Component {
+ state = {
+ templates: []
+ }
+ async componentDidMount() {
+ const templates = await loadIndexTemplates();
+ this.setState({ templates });
+ }
+ addPolicyToTemplate = async () => {
+ const { intl, policy, callback, onCancel } = this.props;
+ const { templateName, aliasName } = this.state;
+ const policyName = policy.name;
+ if (!templateName) {
+ this.setState({ templateError: i18n.translate(
+ 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyConfirmModal.noTemplateSelectedErrorMessage',
+ { defaultMessage: 'You must select an index template.' }) });
+ return;
+ }
+ try {
+ await addLifecyclePolicyToTemplate({
+ policyName,
+ templateName,
+ aliasName
+ });
+ const message = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyConfirmModal.successMessage',
+ defaultMessage: 'Added policy {policyName} to index template {templateName}',
+ }, { policyName, templateName });
+ toastNotifications.addSuccess(message);
+ onCancel();
+ } catch (e) {
+ const title = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyConfirmModal.errorMessage',
+ defaultMessage: 'Error adding policy "{policyName}" to index template {templateName}',
+ }, { policyName, templateName });
+ showApiError(e, title);
+ }
+ if (callback) {
+ callback();
+ }
+ };
+ renderTemplateHasPolicyWarning() {
+ const selectedTemplate = this.getSelectedTemplate();
+ const existingPolicyName = get(selectedTemplate, 'settings.index.lifecycle.name');
+ if (!existingPolicyName) {
+ return;
+ }
+ return (
+
+
+ }
+ color="warning"
+ >
+
+
+
+
+ );
+ }
+ 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.addLifecyclePolicyConfirmModal.chooseTemplateMessage',
+ {
+ defaultMessage: 'Select an index template',
+ }
+ ),
+ });
+ return (
+
+ {this.renderTemplateHasPolicyWarning()}
+
+ }
+ >
+ {
+ this.setState({ templateError: null, templateName: e.target.value });
+ }}
+ />
+
+ {this.renderAliasFormElement()}
+
+ );
+ }
+ renderAliasFormElement = () => {
+ const { aliasName } = this.state;
+ const { policy } = this.props;
+ const showAliasTextInput =
+ policy && get(policy, 'policy.phases.hot.actions.rollover');
+ if (!showAliasTextInput) {
+ return null;
+ }
+ return (
+
+ }
+ >
+ {
+ this.setState({ aliasName: e.target.value });
+ }}
+ />
+
+ );
+ };
+ render() {
+ const { intl, policy, onCancel } = this.props;
+ const title = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyConfirmModal.title',
+ defaultMessage: 'Add policy "{name}" to index template',
+ }, { name: policy.name });
+ return (
+
+
+
+
+ {' '}
+ }
+ />
+
+
+
+ {this.renderForm()}
+
+
+ );
+ }
+}
+export const AddPolicyToTemplateConfirmModal = injectI18n(AddPolicyToTemplateConfirmModalUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/confirm_delete.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/confirm_delete.js
new file mode 100644
index 0000000000000..cdaf60d1f829b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/confirm_delete.js
@@ -0,0 +1,74 @@
+/*
+ * 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, { Component } from 'react';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+import { deletePolicy } from '../../../../services/api';
+import { showApiError } from '../../../../services/api_errors';
+export class ConfirmDeleteUi extends Component {
+ deletePolicy = async () => {
+ const { intl, policyToDelete, callback } = this.props;
+ const policyName = policyToDelete.name;
+
+ try {
+ await deletePolicy(policyName);
+ const message = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.confirmDelete.successMessage',
+ defaultMessage: 'Deleted policy {policyName}',
+ }, { policyName });
+ toastNotifications.addSuccess(message);
+ } catch (e) {
+ const title = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.confirmDelete.errorMessage',
+ defaultMessage: 'Error deleting policy {policyName}',
+ }, { policyName });
+ showApiError(e, title);
+ }
+ if (callback) {
+ callback();
+ }
+ };
+ render() {
+ const { intl, policyToDelete, onCancel } = this.props;
+ const title = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.confirmDelete.title',
+ defaultMessage: 'Delete policy "{name}"',
+ }, { name: policyToDelete.name });
+ return (
+
+
+ }
+ confirmButtonText={
+
+ }
+ buttonColor="danger"
+ onClose={onCancel}
+ >
+
+
+
+
+
+ );
+ }
+}
+export const ConfirmDelete = injectI18n(ConfirmDeleteUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/index.js
new file mode 100644
index 0000000000000..1e10c49443cef
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/index.js
@@ -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 { PolicyTable } from './policy_table.container';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.container.js
new file mode 100644
index 0000000000000..0688284606b1f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.container.js
@@ -0,0 +1,56 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { PolicyTable as PresentationComponent } from './policy_table';
+import {
+ fetchPolicies,
+ policyFilterChanged,
+ policyPageChanged,
+ policyPageSizeChanged,
+ policySortChanged,
+} from '../../../../store/actions';
+import {
+ getPolicies,
+ getPageOfPolicies,
+ getPolicyPager,
+ getPolicyFilter,
+ getPolicySort,
+ isPolicyListLoaded,
+} from '../../../../store/selectors';
+
+const mapDispatchToProps = dispatch => {
+ return {
+ policyFilterChanged: filter => {
+ dispatch(policyFilterChanged({ filter }));
+ },
+ policyPageChanged: pageNumber => {
+ dispatch(policyPageChanged({ pageNumber }));
+ },
+ policyPageSizeChanged: pageSize => {
+ dispatch(policyPageSizeChanged({ pageSize }));
+ },
+ policySortChanged: (sortField, isSortAscending) => {
+ dispatch(policySortChanged({ sortField, isSortAscending }));
+ },
+ fetchPolicies: withIndices => {
+ dispatch(fetchPolicies(withIndices));
+ },
+ };
+};
+
+export const PolicyTable = connect(
+ state => ({
+ totalNumberOfPolicies: getPolicies(state).length,
+ policies: getPageOfPolicies(state),
+ pager: getPolicyPager(state),
+ filter: getPolicyFilter(state),
+ sortField: getPolicySort(state).sortField,
+ isSortAscending: getPolicySort(state).isSortAscending,
+ policyListLoaded: isPolicyListLoaded(state),
+ }),
+ mapDispatchToProps
+)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js
new file mode 100644
index 0000000000000..36fc2af7b0040
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js
@@ -0,0 +1,498 @@
+/*
+ * 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, { Component, Fragment } from 'react';
+import moment from 'moment-timezone';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import { BASE_PATH } from '../../../../../common/constants';
+import { NoMatch } from '../no_match';
+import { getPolicyPath } from '../../../../services/navigation';
+import { flattenPanelTree } from '../../../../services/flatten_panel_tree';
+import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
+import {
+ EuiBetaBadge,
+ EuiButton,
+ EuiLink,
+ EuiEmptyPrompt,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiPage,
+ EuiPopover,
+ EuiContextMenu,
+ EuiSpacer,
+ EuiTable,
+ EuiTableBody,
+ EuiTableHeader,
+ EuiTableHeaderCell,
+ EuiTablePagination,
+ EuiTableRow,
+ EuiTableRowCell,
+ EuiTitle,
+ EuiText,
+ EuiPageBody,
+ EuiPageContent,
+} from '@elastic/eui';
+
+import { ConfirmDelete } from './confirm_delete';
+import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal';
+import { getFilteredIndicesUri } from '../../../../../../index_management/public/services/navigation';
+const COLUMNS = {
+ name: {
+ label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', {
+ defaultMessage: 'Name',
+ }),
+ },
+ coveredIndices: {
+ label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.coveredIndicesHeader', {
+ defaultMessage: 'Covered indices',
+ }),
+ width: 120,
+ },
+ version: {
+ label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.versionHeader', {
+ defaultMessage: 'Version',
+ }),
+ width: 120,
+ },
+ modified_date: {
+ label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader', {
+ defaultMessage: 'Modified date',
+ }),
+ width: 200,
+ },
+};
+
+export class PolicyTableUi extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selectedPoliciesMap: {},
+ renderConfirmModal: null,
+ };
+ }
+ componentDidMount() {
+ this.props.fetchPolicies(true);
+ }
+ renderEmpty() {
+ return (
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ actions={this.renderCreatePolicyButton()}
+ />
+ );
+ }
+ renderDeleteConfirmModal = () => {
+ const { policyToDelete } = this.state;
+ if (!policyToDelete) {
+ return null;
+ }
+ return (
+ this.setState({ renderConfirmModal: null, policyToDelete: null })}
+ />
+ );
+ };
+ renderAddPolicyToTemplateConfirmModal = () => {
+ const { policyToAddToTemplate } = this.state;
+ if (!policyToAddToTemplate) {
+ return null;
+ }
+ return (
+ this.setState({ renderConfirmModal: null, policyToAddToTemplate: null })}
+ />
+ );
+ };
+ handleDelete = () => {
+ this.props.fetchPolicies(true);
+ this.setState({ renderDeleteConfirmModal: null, policyToDelete: null });
+ };
+ onSort = column => {
+ const { sortField, isSortAscending, policySortChanged } = this.props;
+ const newIsSortAscending = sortField === column ? !isSortAscending : true;
+ policySortChanged(column, newIsSortAscending);
+ };
+
+ buildHeader() {
+ const { sortField, isSortAscending } = this.props;
+ const headers = Object.entries(COLUMNS).map(([fieldName, { label, width }]) => {
+ const isSorted = sortField === fieldName;
+ return (
+ this.onSort(fieldName)}
+ isSorted={isSorted}
+ isSortAscending={isSortAscending}
+ data-test-subj={`policyTableHeaderCell-${fieldName}`}
+ className={'policyTable__header--' + fieldName}
+ width={width}
+ >
+ {label}
+
+ );
+ });
+ headers.push(
+
+ );
+ return headers;
+ }
+
+ buildRowCell(fieldName, value) {
+ if (fieldName === 'name') {
+ return (
+
+ {value}
+
+ );
+ } else if (fieldName === 'coveredIndices') {
+ return (
+
+ {value ? value.length : '0'}
+
+ );
+ } else if (fieldName === 'modified_date' && value) {
+ return moment(value).format('YYYY-MM-DD HH:mm:ss');
+ }
+ return value;
+ }
+ renderCreatePolicyButton() {
+ return (
+
+
+
+ );
+ }
+ renderConfirmModal() {
+ const { renderConfirmModal } = this.state;
+ if (renderConfirmModal) {
+ return renderConfirmModal();
+ } else {
+ return null;
+ }
+ }
+ buildActionPanelTree(policy) {
+ const { intl } = this.props;
+ const hasCoveredIndices = Boolean(policy.coveredIndices && policy.coveredIndices.length);
+
+ const viewIndicesLabel = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.viewIndicesButtonText',
+ defaultMessage: 'View indices linked to policy',
+ });
+ const addPolicyToTemplateLabel = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText',
+ defaultMessage: 'Add policy to index template',
+ });
+ const deletePolicyLabel = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText',
+ defaultMessage: 'Delete policy',
+ });
+ const deletePolicyTooltip = hasCoveredIndices
+ ? intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip',
+ defaultMessage: 'You cannot delete a policy that is being used by an index',
+ })
+ : null;
+ const items = [];
+ if (hasCoveredIndices) {
+ items.push({
+ name: viewIndicesLabel,
+ icon: 'list',
+ onClick: () => {
+ window.location.hash = getFilteredIndicesUri(`ilm.policy:${policy.name}`);
+ },
+ });
+ }
+ items.push({
+ name: addPolicyToTemplateLabel,
+ icon: 'plusInCircle',
+ onClick: () =>
+ this.setState({
+ renderConfirmModal: this.renderAddPolicyToTemplateConfirmModal,
+ policyToAddToTemplate: policy,
+ }),
+ });
+ items.push({
+ name: deletePolicyLabel,
+ disabled: hasCoveredIndices,
+ icon: 'trash',
+ toolTipContent: deletePolicyTooltip,
+ onClick: () =>
+ this.setState({
+ renderConfirmModal: this.renderDeleteConfirmModal,
+ policyToDelete: policy,
+ }),
+ });
+ const panelTree = {
+ id: 0,
+ title: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle',
+ defaultMessage: 'Policy options',
+ }),
+ items,
+ };
+ return flattenPanelTree(panelTree);
+ }
+ togglePolicyPopover = policy => {
+ if (this.isPolicyPopoverOpen(policy)) {
+ this.closePolicyPopover(policy);
+ } else {
+ this.openPolicyPopover(policy);
+ }
+ };
+ isPolicyPopoverOpen = policy => {
+ return this.state.policyPopover === policy.name;
+ };
+ closePolicyPopover = policy => {
+ if (this.isPolicyPopoverOpen(policy)) {
+ this.setState({ policyPopover: null });
+ }
+ };
+ openPolicyPopover = policy => {
+ this.setState({ policyPopover: policy.name });
+ };
+ buildRowCells(policy) {
+ const { intl } = this.props;
+ const { name } = policy;
+ const cells = Object.entries(COLUMNS).map(([fieldName, { width }]) => {
+ const value = policy[fieldName];
+ return (
+
+ {this.buildRowCell(fieldName, value)}
+
+ );
+ });
+ const button = (
+ this.togglePolicyPopover(policy)}
+ iconType="arrowDown"
+ fill
+ >
+ {intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.actionsButtonText',
+ defaultMessage: 'Actions',
+ })}
+
+ );
+ cells.push(
+
+
+
+ );
+ return cells;
+ }
+
+ buildRows() {
+ const { policies = [] } = this.props;
+ return policies.map(policy => {
+ const { name } = policy;
+ return {this.buildRowCells(policy)};
+ });
+ }
+
+ renderPager() {
+ const { pager, policyPageChanged, policyPageSizeChanged } = this.props;
+ return (
+
+ );
+ }
+
+ onItemSelectionChanged = selectedPolicies => {
+ this.setState({ selectedPolicies });
+ };
+
+ render() {
+ const {
+ totalNumberOfPolicies,
+ policyFilterChanged,
+ filter,
+ intl,
+ policyListLoaded,
+ } = this.props;
+ const { selectedPoliciesMap } = this.state;
+ const numSelected = Object.keys(selectedPoliciesMap).length;
+ let content;
+ let tableContent;
+ if (totalNumberOfPolicies || !policyListLoaded) {
+ if (!policyListLoaded) {
+ tableContent = ;
+ } else if (totalNumberOfPolicies > 0) {
+ tableContent = (
+
+ {this.buildHeader()}
+ {this.buildRows()}
+
+ );
+ } else {
+ tableContent = ;
+ }
+ content = (
+
+
+ {numSelected > 0 ? (
+
+ this.setState({ showDeleteConfirmation: true })}
+ >
+ Delete {numSelected} polic
+ {numSelected > 1 ? 'ies' : 'y'}
+
+
+ ) : null}
+
+ {
+ policyFilterChanged(event.target.value);
+ }}
+ data-test-subj="policyTableFilterInput"
+ placeholder={intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputPlaceholder',
+ defaultMessage: 'Search',
+ })}
+ aria-label="Search policies"
+ />
+
+
+
+ {tableContent}
+
+ );
+ } else {
+ content = this.renderEmpty();
+ }
+
+ return (
+
+
+
+
+ {this.renderConfirmModal()}
+ {totalNumberOfPolicies || !policyListLoaded ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {totalNumberOfPolicies ? (
+ {this.renderCreatePolicyButton()}
+ ) : null}
+
+
+
+
+
+
+
+
+ ) : null}
+
+ {content}
+
+ {totalNumberOfPolicies && totalNumberOfPolicies > 10 ? this.renderPager() : null}
+
+
+
+
+ );
+ }
+}
+
+export const PolicyTable = injectI18n(PolicyTableUi);
diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/index.js
new file mode 100644
index 0000000000000..c4aa32f1f7dc2
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/index.js
@@ -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 { PolicyTable } from './components/policy_table';
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/api.js b/x-pack/plugins/index_lifecycle_management/public/services/api.js
new file mode 100644
index 0000000000000..a82cd3a0a7543
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/api.js
@@ -0,0 +1,78 @@
+/*
+ * 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 chrome from 'ui/chrome';
+
+let httpClient;
+export const setHttpClient = (client) => {
+ httpClient = client;
+};
+const getHttpClient = () => {
+ return httpClient;
+};
+const apiPrefix = chrome.addBasePath('/api/index_lifecycle_management');
+
+export async function loadNodes(httpClient = getHttpClient()) {
+ const response = await httpClient.get(`${apiPrefix}/nodes/list`);
+ return response.data;
+}
+
+export async function loadNodeDetails(selectedNodeAttrs, httpClient = getHttpClient()) {
+ const response = await httpClient.get(`${apiPrefix}/nodes/${selectedNodeAttrs}/details`);
+ return response.data;
+}
+
+export async function loadIndexTemplates(httpClient = getHttpClient()) {
+ const response = await httpClient.get(`${apiPrefix}/templates`);
+ return response.data;
+}
+
+export async function loadIndexTemplate(templateName, httpClient = getHttpClient()) {
+ if (!templateName) {
+ return {};
+ }
+ const response = await httpClient.get(`${apiPrefix}/template/${templateName}`);
+ return response.data;
+}
+
+export async function loadPolicies(withIndices, httpClient = getHttpClient()) {
+ const response = await httpClient.get(`${apiPrefix}/policies${ withIndices ? '?withIndices=true' : ''}`);
+ return response.data;
+}
+
+export async function deletePolicy(policyName, httpClient = getHttpClient()) {
+ const response = await httpClient.delete(`${apiPrefix}/policies/${encodeURIComponent(policyName)}`);
+ return response.data;
+}
+
+export async function saveLifecycle(lifecycle, httpClient = getHttpClient()) {
+ const response = await httpClient.post(`${apiPrefix}/lifecycle`, { lifecycle });
+ return response.data;
+}
+
+
+export async function getAffectedIndices(indexTemplateName, policyName, httpClient = getHttpClient()) {
+ const path = policyName
+ ? `${apiPrefix}/indices/affected/${indexTemplateName}/${encodeURIComponent(policyName)}`
+ : `${apiPrefix}/indices/affected/${indexTemplateName}`;
+ const response = await httpClient.get(path);
+ return response.data;
+}
+export const retryLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => {
+ const response = await httpClient.post(`${apiPrefix}/index/retry`, { indexNames });
+ return response.data;
+};
+export const removeLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => {
+ const response = await httpClient.post(`${apiPrefix}/index/remove`, { indexNames });
+ return response.data;
+};
+export const addLifecyclePolicyToIndex = async (body, httpClient = getHttpClient()) => {
+ const response = await httpClient.post(`${apiPrefix}/index/add`, body);
+ return response.data;
+};
+export const addLifecyclePolicyToTemplate = async (body, httpClient = getHttpClient()) => {
+ const response = await httpClient.post(`${apiPrefix}/template`, body);
+ return response.data;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/api_errors.js b/x-pack/plugins/index_lifecycle_management/public/services/api_errors.js
new file mode 100644
index 0000000000000..bacaf13405898
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/api_errors.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { fatalError, toastNotifications } from 'ui/notify';
+
+function createToastConfig(error, errorTitle) {
+ // Expect an error in the shape provided by Angular's $http service.
+ if (error && error.data) {
+ const { error: errorString, statusCode, message } = error.data;
+ return {
+ title: errorTitle,
+ text: `${statusCode}: ${errorString}. ${message}`,
+ };
+ }
+}
+
+export function showApiWarning(error, errorTitle) {
+ const toastConfig = createToastConfig(error, errorTitle);
+
+ if (toastConfig) {
+ return toastNotifications.addWarning(toastConfig);
+ }
+
+ // This error isn't an HTTP error, so let the fatal error screen tell the user something
+ // unexpected happened.
+ return fatalError(error, errorTitle);
+}
+
+export function showApiError(error, errorTitle) {
+ const toastConfig = createToastConfig(error, errorTitle);
+
+ if (toastConfig) {
+ return toastNotifications.addDanger(toastConfig);
+ }
+
+ // This error isn't an HTTP error, so let the fatal error screen tell the user something
+ // unexpected happened.
+ fatalError(error, errorTitle);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/filter_items.js b/x-pack/plugins/index_lifecycle_management/public/services/filter_items.js
new file mode 100644
index 0000000000000..6d2e3dae57f46
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/filter_items.js
@@ -0,0 +1,17 @@
+/*
+ * 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 filterItems = (fields, filter = '', items = []) => {
+ const lowerFilter = filter.toLowerCase();
+ return items.filter(item => {
+ const actualFields = fields || Object.keys(item);
+ const indexOfMatch = actualFields.findIndex(field => {
+ const normalizedField = String(item[field]).toLowerCase();
+ return normalizedField.includes(lowerFilter);
+ });
+ return indexOfMatch !== -1;
+ });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/services/find_errors.js
new file mode 100644
index 0000000000000..7b77cf7a1301c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/find_errors.js
@@ -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.
+ */
+
+export const findFirstError = (object, topLevel = true) => {
+ let firstError;
+ const keys = topLevel ? [ 'policyName', 'hot', 'warm', 'cold', 'delete'] : Object.keys(object);
+ for (const key of keys) {
+ const value = object[key];
+ if (Array.isArray(value) && value.length > 0) {
+ firstError = key;
+ break;
+ } else if (value) {
+ firstError = findFirstError(value, false);
+ if (firstError) {
+ firstError = `${key}.${firstError}`;
+ break;
+ }
+ }
+ }
+ return firstError;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/flatten_panel_tree.js b/x-pack/plugins/index_lifecycle_management/public/services/flatten_panel_tree.js
new file mode 100644
index 0000000000000..e060e22965cb3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/flatten_panel_tree.js
@@ -0,0 +1,20 @@
+/*
+ * 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 flattenPanelTree = (tree, array = []) => {
+ array.push(tree);
+
+ if (tree.items) {
+ tree.items.forEach(item => {
+ if (item.panel) {
+ flattenPanelTree(item.panel, array);
+ item.panel = item.panel.id;
+ }
+ });
+ }
+
+ return array;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/index.js b/x-pack/plugins/index_lifecycle_management/public/services/index.js
new file mode 100644
index 0000000000000..86f51d8b4a4c1
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/index.js
@@ -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 { filterItems } from './filter_items';
+export { sortTable } from './sort_table';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/manage_angular_lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/services/manage_angular_lifecycle.js
new file mode 100644
index 0000000000000..3813e632a0a73
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/manage_angular_lifecycle.js
@@ -0,0 +1,23 @@
+/*
+ * 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 { unmountComponentAtNode } from 'react-dom';
+
+export const manageAngularLifecycle = ($scope, $route, elem) => {
+ const lastRoute = $route.current;
+
+ const deregister = $scope.$on('$locationChangeSuccess', () => {
+ const currentRoute = $route.current;
+ if (lastRoute.$$route.template === currentRoute.$$route.template) {
+ $route.current = lastRoute;
+ }
+ });
+
+ $scope.$on('$destroy', () => {
+ deregister && deregister();
+ elem && unmountComponentAtNode(elem);
+ });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/navigation.js b/x-pack/plugins/index_lifecycle_management/public/services/navigation.js
new file mode 100644
index 0000000000000..655d9cf6608ff
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/navigation.js
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+let urlService;
+import { BASE_PATH } from '../../common/constants';
+export const setUrlService = (aUrlService) => {
+ urlService = aUrlService;
+};
+export const getUrlService = () => {
+ return urlService;
+};
+
+
+export const goToPolicyList = () => {
+ urlService.change(`${BASE_PATH}policies`);
+};
+
+export const getPolicyPath = (policyName) => {
+ return encodeURI(`#${BASE_PATH}policies/edit/${encodeURIComponent(policyName)}`);
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/services/sort_table.js b/x-pack/plugins/index_lifecycle_management/public/services/sort_table.js
new file mode 100644
index 0000000000000..6c390ecab07dc
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/sort_table.js
@@ -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 { sortBy } from 'lodash';
+
+const stringSort = (fieldName) => (item) => item[fieldName];
+const arraySort = (fieldName) => (item) => (item[fieldName] || []).length;
+
+const sorters = {
+ version: stringSort('version'),
+ name: stringSort('name'),
+ coveredIndices: arraySort('coveredIndices'),
+ modified_date: stringSort('modified_date')
+
+};
+export const sortTable = (array = [], sortField, isSortAscending) => {
+ const sorted = sortBy(array, sorters[sortField]);
+ return isSortAscending
+ ? sorted
+ : sorted.reverse();
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/actions/general.js b/x-pack/plugins/index_lifecycle_management/public/store/actions/general.js
new file mode 100644
index 0000000000000..584488d4c2b42
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/general.js
@@ -0,0 +1,14 @@
+/*
+ * 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 { createAction } from 'redux-actions';
+
+export const setBootstrapEnabled = createAction('SET_BOOTSTRAP_ENABLED');
+export const setIndexName = createAction('SET_INDEX_NAME');
+export const setAliasName = createAction('SET_ALIAS_NAME');
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/store/actions/index.js
new file mode 100644
index 0000000000000..ea539578c885c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/index.js
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './nodes';
+export * from './policies';
+export * from './lifecycle';
+export * from './general';
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/actions/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/store/actions/lifecycle.js
new file mode 100644
index 0000000000000..9dcd1249de71c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/lifecycle.js
@@ -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 { i18n } from '@kbn/i18n';
+import { toastNotifications } from 'ui/notify';
+import { showApiError } from '../../services/api_errors';
+import { saveLifecycle as saveLifecycleApi } from '../../services/api';
+
+
+export const saveLifecyclePolicy = (lifecycle, isNew) => async () => {
+ try {
+ await saveLifecycleApi(lifecycle);
+ }
+ catch (err) {
+ const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage',
+ {
+ defaultMessage: 'Error saving lifecycle policy {lifecycleName}',
+ values: { lifecycleName: lifecycle.name }
+ }
+ );
+ showApiError(err, title);
+ return false;
+ }
+ const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage',
+ {
+ defaultMessage: '{verb} lifecycle policy "{lifecycleName}"',
+ values: { verb: isNew ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createdMessage', {
+ defaultMessage: 'Created',
+ }) : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.updatedMessage', {
+ defaultMessage: 'Updated',
+ }), lifecycleName: lifecycle.name }
+ },
+ );
+ toastNotifications.addSuccess(message);
+ return true;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/store/actions/nodes.js
new file mode 100644
index 0000000000000..a2a526e44298e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/nodes.js
@@ -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 { createAction } from 'redux-actions';
+import { showApiError } from '../../services/api_errors';
+import { loadNodes, loadNodeDetails } from '../../services/api';
+import { SET_SELECTED_NODE_ATTRS } from '../constants';
+
+export const setSelectedNodeAttrs = createAction(SET_SELECTED_NODE_ATTRS);
+export const setSelectedPrimaryShardCount = createAction(
+ 'SET_SELECTED_PRIMARY_SHARED_COUNT'
+);
+export const setSelectedReplicaCount = createAction(
+ 'SET_SELECTED_REPLICA_COUNT'
+);
+export const fetchedNodes = createAction('FETCHED_NODES');
+let fetchingNodes = false;
+export const fetchNodes = () => async dispatch => {
+ try {
+ if (!fetchingNodes) {
+ fetchingNodes = true;
+ const nodes = await loadNodes();
+ dispatch(fetchedNodes(nodes));
+ }
+ } catch (err) {
+ const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeInfoErrorMessage',
+ {
+ defaultMessage: 'Error loading node attribute information',
+ },
+ );
+ showApiError(err, title);
+ } finally {
+ fetchingNodes = false;
+ }
+};
+
+export const fetchedNodeDetails = createAction(
+ 'FETCHED_NODE_DETAILS',
+ (selectedNodeAttrs, details) => ({
+ selectedNodeAttrs,
+ details,
+ })
+);
+export const fetchNodeDetails = selectedNodeAttrs => async dispatch => {
+ let details;
+ try {
+ details = await loadNodeDetails(selectedNodeAttrs);
+ } catch (err) {
+ const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage',
+ {
+ defaultMessage: 'Error loading node attribute details',
+ },
+ );
+ showApiError(err, title);
+ return false;
+ }
+ dispatch(fetchedNodeDetails(selectedNodeAttrs, details));
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/store/actions/policies.js
new file mode 100644
index 0000000000000..fb2384855d92c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/policies.js
@@ -0,0 +1,47 @@
+/*
+ * 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 { showApiError } from '../../services/api_errors';
+import { createAction } from 'redux-actions';
+import { loadPolicies } from '../../services/api';
+import { SET_PHASE_DATA } from '../constants';
+export const fetchedPolicies = createAction('FETCHED_POLICIES');
+export const setSelectedPolicy = createAction('SET_SELECTED_POLICY');
+export const unsetSelectedPolicy = createAction('UNSET_SELECTED_POLICY');
+export const setSelectedPolicyName = createAction('SET_SELECTED_POLICY_NAME');
+export const setSaveAsNewPolicy = createAction('SET_SAVE_AS_NEW_POLICY');
+export const policySortChanged = createAction('POLICY_SORT_CHANGED');
+export const policyPageSizeChanged = createAction('POLICY_PAGE_SIZE_CHANGED');
+export const policyPageChanged = createAction('POLICY_PAGE_CHANGED');
+export const policySortDirectionChanged = createAction('POLICY_SORT_DIRECTION_CHANGED');
+export const policyFilterChanged = createAction('POLICY_FILTER_CHANGED');
+
+export const fetchPolicies = (withIndices, callback) => async dispatch => {
+ let policies;
+ try {
+ policies = await loadPolicies(withIndices);
+ }
+ catch (err) {
+ const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage',
+ {
+ defaultMessage: 'Error loading policies',
+ },
+ );
+ showApiError(err, title);
+ return false;
+ }
+
+ dispatch(fetchedPolicies(policies));
+ if (policies.length === 0) {
+ dispatch(setSelectedPolicy());
+ }
+ callback && callback();
+ return policies;
+};
+
+
+export const setPhaseData = createAction(SET_PHASE_DATA, (phase, key, value) => ({ phase, key, value }));
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/constants.js b/x-pack/plugins/index_lifecycle_management/public/store/constants.js
new file mode 100644
index 0000000000000..a49ebd6ee6be1
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/constants.js
@@ -0,0 +1,94 @@
+/*
+ * 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 SET_PHASE_DATA = 'SET_PHASE_DATA';
+export const SET_SELECTED_NODE_ATTRS = 'SET_SELECTED_NODE_ATTRS';
+export const PHASE_HOT = 'hot';
+export const PHASE_WARM = 'warm';
+export const PHASE_COLD = 'cold';
+export const PHASE_DELETE = 'delete';
+
+export const PHASE_ENABLED = 'phaseEnabled';
+
+export const MAX_SIZE_TYPE_DOCUMENT = 'd';
+
+export const PHASE_ROLLOVER_ENABLED = 'rolloverEnabled';
+export const WARM_PHASE_ON_ROLLOVER = 'warmPhaseOnRollover';
+export const PHASE_ROLLOVER_ALIAS = 'selectedAlias';
+export const PHASE_ROLLOVER_MAX_AGE = 'selectedMaxAge';
+export const PHASE_ROLLOVER_MAX_AGE_UNITS = 'selectedMaxAgeUnits';
+export const PHASE_ROLLOVER_MAX_SIZE_STORED = 'selectedMaxSizeStored';
+export const PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS = 'selectedMaxSizeStoredUnits';
+export const PHASE_ROLLOVER_MINIMUM_AGE = 'selectedMinimumAge';
+export const PHASE_ROLLOVER_MINIMUM_AGE_UNITS = 'selectedMinimumAgeUnits';
+
+export const PHASE_FORCE_MERGE_SEGMENTS = 'selectedForceMergeSegments';
+export const PHASE_FORCE_MERGE_ENABLED = 'forceMergeEnabled';
+
+export const PHASE_SHRINK_ENABLED = 'shrinkEnabled';
+
+export const PHASE_NODE_ATTRS = 'selectedNodeAttrs';
+export const PHASE_PRIMARY_SHARD_COUNT = 'selectedPrimaryShardCount';
+export const PHASE_REPLICA_COUNT = 'selectedReplicaCount';
+
+export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE = [
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_FORCE_MERGE_SEGMENTS,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_REPLICA_COUNT,
+];
+export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS = [
+ ...PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE,
+ PHASE_ROLLOVER_MAX_AGE,
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+];
+
+export const STRUCTURE_INDEX_TEMPLATE = 'indexTemplate';
+export const STRUCTURE_TEMPLATE_SELECTION = 'templateSelection';
+export const STRUCTURE_TEMPLATE_NAME = 'templateName';
+export const STRUCTURE_CONFIGURATION = 'configuration';
+export const STRUCTURE_NODE_ATTRS = 'node_attrs';
+export const STRUCTURE_PRIMARY_NODES = 'primary_nodes';
+export const STRUCTURE_REPLICAS = 'replicas';
+
+export const STRUCTURE_POLICY_CONFIGURATION = 'policyConfiguration';
+
+export const STRUCTURE_REVIEW = 'review';
+export const STRUCTURE_POLICY_NAME = 'policyName';
+export const STRUCTURE_INDEX_NAME = 'indexName';
+export const STRUCTURE_ALIAS_NAME = 'aliasName';
+
+export const ERROR_STRUCTURE = {
+ [PHASE_HOT]: {
+ [PHASE_ROLLOVER_ALIAS]: [],
+ [PHASE_ROLLOVER_MAX_AGE]: [],
+ [PHASE_ROLLOVER_MAX_AGE_UNITS]: [],
+ [PHASE_ROLLOVER_MAX_SIZE_STORED]: [],
+ [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: [],
+ },
+ [PHASE_WARM]: {
+ [PHASE_ROLLOVER_ALIAS]: [],
+ [PHASE_ROLLOVER_MINIMUM_AGE]: [],
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [],
+ [PHASE_NODE_ATTRS]: [],
+ [PHASE_PRIMARY_SHARD_COUNT]: [],
+ [PHASE_REPLICA_COUNT]: [],
+ [PHASE_FORCE_MERGE_SEGMENTS]: [],
+ },
+ [PHASE_COLD]: {
+ [PHASE_ROLLOVER_ALIAS]: [],
+ [PHASE_ROLLOVER_MINIMUM_AGE]: [],
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [],
+ [PHASE_NODE_ATTRS]: [],
+ [PHASE_REPLICA_COUNT]: [],
+ },
+ [PHASE_DELETE]: {
+ [PHASE_ROLLOVER_ALIAS]: [],
+ [PHASE_ROLLOVER_MINIMUM_AGE]: [],
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [],
+ },
+ [STRUCTURE_POLICY_NAME]: [],
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/defaults/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/store/defaults/cold_phase.js
new file mode 100644
index 0000000000000..dcc2c6156f119
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/defaults/cold_phase.js
@@ -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 {
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_NODE_ATTRS,
+ PHASE_REPLICA_COUNT,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+ PHASE_ROLLOVER_ALIAS,
+} from '../constants';
+
+export const defaultColdPhase = {
+ [PHASE_ENABLED]: false,
+ [PHASE_ROLLOVER_ALIAS]: '',
+ [PHASE_ROLLOVER_MINIMUM_AGE]: '',
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd',
+ [PHASE_NODE_ATTRS]: '',
+ [PHASE_REPLICA_COUNT]: ''
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/defaults/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/store/defaults/delete_phase.js
new file mode 100644
index 0000000000000..e5326615e536a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/defaults/delete_phase.js
@@ -0,0 +1,20 @@
+/*
+ * 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 {
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_ENABLED,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+ PHASE_ROLLOVER_ALIAS,
+} from '../constants';
+
+export const defaultDeletePhase = {
+ [PHASE_ENABLED]: false,
+ [PHASE_ROLLOVER_ENABLED]: false,
+ [PHASE_ROLLOVER_ALIAS]: '',
+ [PHASE_ROLLOVER_MINIMUM_AGE]: '',
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd'
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/defaults/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/store/defaults/hot_phase.js
new file mode 100644
index 0000000000000..107a803891f6a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/defaults/hot_phase.js
@@ -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 {
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_ENABLED,
+ PHASE_ROLLOVER_MAX_AGE,
+ PHASE_ROLLOVER_MAX_AGE_UNITS,
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+ PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS,
+ PHASE_ROLLOVER_ALIAS,
+} from '../constants';
+
+export const defaultHotPhase = {
+ [PHASE_ENABLED]: true,
+ [PHASE_ROLLOVER_ENABLED]: true,
+ [PHASE_ROLLOVER_ALIAS]: '',
+ [PHASE_ROLLOVER_MAX_AGE]: '',
+ [PHASE_ROLLOVER_MAX_AGE_UNITS]: 'd',
+ [PHASE_ROLLOVER_MAX_SIZE_STORED]: '',
+ [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: 'gb',
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/defaults/index.js b/x-pack/plugins/index_lifecycle_management/public/store/defaults/index.js
new file mode 100644
index 0000000000000..a92f98fa8e022
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/defaults/index.js
@@ -0,0 +1,10 @@
+/*
+ * 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 { defaultDeletePhase } from './delete_phase';
+export { defaultColdPhase } from './cold_phase';
+export { defaultHotPhase } from './hot_phase';
+export { defaultWarmPhase } from './warm_phase';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/defaults/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/store/defaults/warm_phase.js
new file mode 100644
index 0000000000000..c53c782d410a3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/defaults/warm_phase.js
@@ -0,0 +1,32 @@
+/*
+ * 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 {
+ PHASE_ENABLED,
+ PHASE_FORCE_MERGE_SEGMENTS,
+ PHASE_FORCE_MERGE_ENABLED,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_NODE_ATTRS,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_REPLICA_COUNT,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+ PHASE_ROLLOVER_ALIAS,
+ PHASE_SHRINK_ENABLED,
+ WARM_PHASE_ON_ROLLOVER
+} from '../constants';
+
+export const defaultWarmPhase = {
+ [PHASE_ENABLED]: false,
+ [PHASE_ROLLOVER_ALIAS]: '',
+ [PHASE_FORCE_MERGE_SEGMENTS]: '',
+ [PHASE_FORCE_MERGE_ENABLED]: false,
+ [PHASE_ROLLOVER_MINIMUM_AGE]: '',
+ [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd',
+ [PHASE_NODE_ATTRS]: '',
+ [PHASE_SHRINK_ENABLED]: false,
+ [PHASE_PRIMARY_SHARD_COUNT]: '',
+ [PHASE_REPLICA_COUNT]: '',
+ [WARM_PHASE_ON_ROLLOVER]: false
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/index.js b/x-pack/plugins/index_lifecycle_management/public/store/index.js
new file mode 100644
index 0000000000000..808eb489bf913
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/index.js
@@ -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 { indexLifecycleManagementStore } from './store';
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/reducers/general.js b/x-pack/plugins/index_lifecycle_management/public/store/reducers/general.js
new file mode 100644
index 0000000000000..abb56f5cdae2f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/general.js
@@ -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 { handleActions } from 'redux-actions';
+import { setIndexName, setAliasName, setBootstrapEnabled } from '../actions/general';
+
+const defaultState = {
+ bootstrapEnabled: false,
+ indexName: '',
+ aliasName: '',
+};
+
+export const general = handleActions({
+ [setIndexName](state, { payload: indexName }) {
+ return {
+ ...state,
+ indexName,
+ };
+ },
+ [setAliasName](state, { payload: aliasName }) {
+ return {
+ ...state,
+ aliasName,
+ };
+ },
+ [setBootstrapEnabled](state, { payload: bootstrapEnabled }) {
+ return {
+ ...state,
+ bootstrapEnabled,
+ };
+ }
+}, defaultState);
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/store/reducers/index.js
new file mode 100644
index 0000000000000..d212855db6750
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/index.js
@@ -0,0 +1,19 @@
+/*
+ * 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 { combineReducers } from 'redux';
+import { nodes } from './nodes';
+import { policies } from './policies';
+import { general } from './general';
+
+export const indexLifecycleManagement = combineReducers({
+ nodes,
+ policies,
+ general,
+});
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/store/reducers/nodes.js
new file mode 100644
index 0000000000000..2a4286a72d22d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/nodes.js
@@ -0,0 +1,80 @@
+/*
+ * 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 { handleActions } from 'redux-actions';
+import {
+ fetchedNodes,
+ setSelectedNodeAttrs,
+ setSelectedPrimaryShardCount,
+ setSelectedReplicaCount,
+ fetchedNodeDetails
+} from '../actions/nodes';
+
+const defaultState = {
+ isLoading: false,
+ selectedNodeAttrs: '',
+ selectedPrimaryShardCount: 1,
+ selectedReplicaCount: 1,
+ nodes: undefined,
+ details: {},
+};
+
+export const nodes = handleActions(
+ {
+ [fetchedNodes](state, { payload: nodes }) {
+ return {
+ ...state,
+ isLoading: false,
+ nodes
+ };
+ },
+ [fetchedNodeDetails](state, { payload }) {
+ const { selectedNodeAttrs, details } = payload;
+ return {
+ ...state,
+ details: {
+ ...state.details,
+ [selectedNodeAttrs]: details,
+ }
+ };
+ },
+ [setSelectedNodeAttrs](state, { payload: selectedNodeAttrs }) {
+ return {
+ ...state,
+ selectedNodeAttrs
+ };
+ },
+ [setSelectedPrimaryShardCount](state, { payload }) {
+ let selectedPrimaryShardCount = parseInt(payload);
+ if (isNaN(selectedPrimaryShardCount)) {
+ selectedPrimaryShardCount = '';
+ }
+ return {
+ ...state,
+ selectedPrimaryShardCount
+ };
+ },
+ [setSelectedReplicaCount](state, { payload }) {
+ let selectedReplicaCount;
+ if (payload != null) {
+ selectedReplicaCount = parseInt(payload);
+ if (isNaN(selectedReplicaCount)) {
+ selectedReplicaCount = '';
+ }
+ } else {
+ // default value for Elasticsearch
+ selectedReplicaCount = 1;
+ }
+
+
+ return {
+ ...state,
+ selectedReplicaCount
+ };
+ }
+ },
+ defaultState
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/store/reducers/policies.js
new file mode 100644
index 0000000000000..27b5304fec1fa
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/policies.js
@@ -0,0 +1,176 @@
+/*
+ * 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 { handleActions } from 'redux-actions';
+import {
+ fetchedPolicies,
+ setSelectedPolicy,
+ unsetSelectedPolicy,
+ setSelectedPolicyName,
+ setSaveAsNewPolicy,
+ setPhaseData,
+ policyFilterChanged,
+ policyPageChanged,
+ policyPageSizeChanged,
+ policySortChanged,
+} from '../actions';
+import { policyFromES } from '../selectors';
+import {
+ PHASE_HOT,
+ PHASE_WARM,
+ PHASE_COLD,
+ PHASE_DELETE,
+ PHASE_ATTRIBUTES_THAT_ARE_NUMBERS,
+} from '../constants';
+
+import {
+ defaultColdPhase,
+ defaultDeletePhase,
+ defaultHotPhase,
+ defaultWarmPhase,
+} from '../defaults';
+export const defaultPolicy = {
+ name: '',
+ saveAsNew: true,
+ isNew: true,
+ phases: {
+ [PHASE_HOT]: defaultHotPhase,
+ [PHASE_WARM]: defaultWarmPhase,
+ [PHASE_COLD]: defaultColdPhase,
+ [PHASE_DELETE]: defaultDeletePhase
+ }
+};
+
+const defaultState = {
+ isLoading: false,
+ isLoaded: false,
+ originalPolicyName: undefined,
+ selectedPolicySet: false,
+ selectedPolicy: defaultPolicy,
+ policies: [],
+ sort: {
+ sortField: 'name',
+ isSortAscending: true
+ },
+ pageSize: 10,
+ currentPage: 0,
+ filter: ''
+};
+
+export const policies = handleActions(
+ {
+ [fetchedPolicies](state, { payload: policies }) {
+ return {
+ ...state,
+ isLoading: false,
+ isLoaded: true,
+ policies
+ };
+ },
+ [setSelectedPolicy](state, { payload: selectedPolicy }) {
+ if (!selectedPolicy) {
+ return {
+ ...state,
+ selectedPolicy: defaultPolicy,
+ selectedPolicySet: true,
+ };
+ }
+
+ return {
+ ...state,
+ originalPolicyName: selectedPolicy.name,
+ selectedPolicySet: true,
+ selectedPolicy: {
+ ...defaultPolicy,
+ ...policyFromES(selectedPolicy)
+ }
+ };
+ },
+ [unsetSelectedPolicy]() {
+ return defaultState;
+ },
+ [setSelectedPolicyName](state, { payload: name }) {
+ return {
+ ...state,
+ selectedPolicy: {
+ ...state.selectedPolicy,
+ name
+ }
+ };
+ },
+ [setSaveAsNewPolicy](state, { payload: saveAsNew }) {
+ return {
+ ...state,
+ selectedPolicy: {
+ ...state.selectedPolicy,
+ saveAsNew
+ }
+ };
+ },
+ [setPhaseData](state, { payload }) {
+ const { phase, key } = payload;
+
+ let value = payload.value;
+ if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) {
+ value = parseInt(value);
+ if (isNaN(value)) {
+ value = '';
+ }
+ }
+
+ return {
+ ...state,
+ selectedPolicy: {
+ ...state.selectedPolicy,
+ phases: {
+ ...state.selectedPolicy.phases,
+ [phase]: {
+ ...state.selectedPolicy.phases[phase],
+ [key]: value
+ }
+ }
+ }
+ };
+ },
+ [policyFilterChanged](state, action) {
+ const { filter } = action.payload;
+ return {
+ ...state,
+ filter,
+ currentPage: 0
+ };
+ },
+ [policySortChanged](state, action) {
+ const { sortField, isSortAscending } = action.payload;
+
+ return {
+ ...state,
+ sort: {
+ sortField,
+ isSortAscending,
+ }
+ };
+ },
+ [policyPageChanged](state, action) {
+ const { pageNumber } = action.payload;
+ return {
+ ...state,
+ currentPage: pageNumber,
+ };
+ },
+ [policyPageSizeChanged](state, action) {
+ const { pageSize } = action.payload;
+ return {
+ ...state,
+ pageSize
+ };
+ }
+ },
+ defaultState
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/selectors/general.js b/x-pack/plugins/index_lifecycle_management/public/store/selectors/general.js
new file mode 100644
index 0000000000000..41459d1bbb2c8
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/general.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+
+export const getBootstrapEnabled = state => state.general.bootstrapEnabled;
+export const getIndexName = state => state.general.indexName;
+export const getAliasName = state => state.general.aliasName;
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/store/selectors/index.js
new file mode 100644
index 0000000000000..ea539578c885c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/index.js
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './nodes';
+export * from './policies';
+export * from './lifecycle';
+export * from './general';
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/store/selectors/lifecycle.js
new file mode 100644
index 0000000000000..632048748b07c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/lifecycle.js
@@ -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 { i18n } from '@kbn/i18n';
+import {
+ PHASE_HOT,
+ PHASE_WARM,
+ PHASE_COLD,
+ PHASE_DELETE,
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_ENABLED,
+ PHASE_ROLLOVER_MAX_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+ STRUCTURE_POLICY_NAME,
+ ERROR_STRUCTURE,
+ PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_SHRINK_ENABLED,
+ PHASE_FORCE_MERGE_ENABLED,
+ PHASE_FORCE_MERGE_SEGMENTS,
+ PHASE_REPLICA_COUNT,
+ WARM_PHASE_ON_ROLLOVER
+} from '../constants';
+import {
+ getPhase,
+ getPhases,
+ phaseToES,
+ getSelectedPolicyName,
+ isNumber,
+ getSaveAsNewPolicy,
+ getSelectedOriginalPolicyName,
+ getPolicies
+} from '.';
+export const numberRequiredMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', {
+ defaultMessage: 'A number is required.'
+});
+export const positiveNumberRequiredMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', {
+ defaultMessage: 'Only positive numbers are allowed.'
+});
+export const maximumAgeRequiredMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', {
+ defaultMessage: 'A maximum age is required.'
+});
+export const maximumSizeRequiredMessage =
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', {
+ defaultMessage: 'A maximum index size is required.'
+ });
+export const positiveNumbersAboveZeroErrorMessage =
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', {
+ defaultMessage: 'Only numbers above 0 are allowed.'
+ });
+export const validatePhase = (type, phase, errors) => {
+ const phaseErrors = {};
+
+ if (!phase[PHASE_ENABLED]) {
+ return;
+ }
+
+ for (const numberedAttribute of PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE) {
+ if (phase.hasOwnProperty(numberedAttribute)) {
+ // If WARM_PHASE_ON_ROLLOVER there is no need to validate this
+ if (numberedAttribute === PHASE_ROLLOVER_MINIMUM_AGE && phase[WARM_PHASE_ON_ROLLOVER]) {
+ continue;
+ }
+ // If shrink is disabled, there is no need to validate this
+ if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && !phase[PHASE_SHRINK_ENABLED]) {
+ continue;
+ }
+ // If forcemerge is disabled, there is no need to validate this
+ if (numberedAttribute === PHASE_FORCE_MERGE_SEGMENTS && !phase[PHASE_FORCE_MERGE_ENABLED]) {
+ continue;
+ }
+ // PHASE_REPLICA_COUNT is optional
+ if (numberedAttribute === PHASE_REPLICA_COUNT && !phase[numberedAttribute]) {
+ continue;
+ }
+ if (!isNumber(phase[numberedAttribute])) {
+ phaseErrors[numberedAttribute] = [numberRequiredMessage];
+ }
+ else if (phase[numberedAttribute] < 0) {
+ phaseErrors[numberedAttribute] = [positiveNumberRequiredMessage];
+ }
+ else if ((numberedAttribute === PHASE_ROLLOVER_MINIMUM_AGE ||
+ numberedAttribute === PHASE_PRIMARY_SHARD_COUNT) && phase[numberedAttribute] < 1) {
+ phaseErrors[numberedAttribute] = [positiveNumbersAboveZeroErrorMessage];
+ }
+ }
+ }
+ if (phase[PHASE_ROLLOVER_ENABLED]) {
+ if (
+ !isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) &&
+ !isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED])
+ ) {
+ phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [
+ maximumAgeRequiredMessage
+ ];
+ phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [
+ maximumSizeRequiredMessage
+ ];
+ }
+ }
+ if (phase[PHASE_SHRINK_ENABLED]) {
+ if (!isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) {
+ phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [numberRequiredMessage];
+ }
+ else if (phase[PHASE_PRIMARY_SHARD_COUNT] < 1) {
+ phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [positiveNumbersAboveZeroErrorMessage];
+ }
+ }
+
+ if (phase[PHASE_FORCE_MERGE_ENABLED]) {
+ if (!isNumber(phase[PHASE_FORCE_MERGE_SEGMENTS])) {
+ phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [numberRequiredMessage];
+ }
+ else if (phase[PHASE_FORCE_MERGE_SEGMENTS] < 1) {
+ phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [
+ positiveNumbersAboveZeroErrorMessage
+ ];
+ }
+ }
+ errors[type] = {
+ ...errors[type],
+ ...phaseErrors
+ };
+};
+
+export const policyNameRequiredMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', {
+ defaultMessage: 'A policy name is required.'
+});
+export const policyNameStartsWithUnderscoreErrorMessage =
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', {
+ defaultMessage: 'A policy name cannot start with an underscore.'
+ });
+export const policyNameContainsCommaErrorMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', {
+ defaultMessage: 'A policy name cannot include a comma.'
+});
+export const policyNameContainsSpaceErrorMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', {
+ defaultMessage: 'A policy name cannot include a space.'
+});
+export const policyNameTooLongErrorMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', {
+ defaultMessage: 'A policy name cannot be longer than 255 bytes.'
+});
+export const policyNameMustBeDifferentErrorMessage =
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', {
+ defaultMessage: 'The policy name must be different.'
+ });
+export const policyNameAlreadyUsedErrorMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', {
+ defaultMessage: 'That policy name is already used.'
+});
+export const validateLifecycle = state => {
+ // This method of deep copy does not always work but it should be fine here
+ const errors = JSON.parse(JSON.stringify(ERROR_STRUCTURE));
+ const policyName = getSelectedPolicyName(state);
+ if (!policyName) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameRequiredMessage);
+ } else {
+ if (policyName.startsWith('_')) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameStartsWithUnderscoreErrorMessage);
+ }
+ if (policyName.includes(',')) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameContainsCommaErrorMessage);
+ }
+ if (policyName.includes(' ')) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameContainsSpaceErrorMessage);
+ }
+ if (TextEncoder && new TextEncoder('utf-8').encode(policyName).length > 255) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameTooLongErrorMessage);
+ }
+ }
+
+ if (getSaveAsNewPolicy(state) && getSelectedOriginalPolicyName(state) === getSelectedPolicyName(state)) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameMustBeDifferentErrorMessage);
+ } else {
+ const policyNames = getPolicies(state).map(policy => policy.name);
+ if (policyNames.includes(getSelectedPolicyName(state))) {
+ errors[STRUCTURE_POLICY_NAME].push(policyNameAlreadyUsedErrorMessage);
+ }
+ }
+
+
+ const hotPhase = getPhase(state, PHASE_HOT);
+ const warmPhase = getPhase(state, PHASE_WARM);
+ const coldPhase = getPhase(state, PHASE_COLD);
+ const deletePhase = getPhase(state, PHASE_DELETE);
+
+ validatePhase(PHASE_HOT, hotPhase, errors);
+ validatePhase(PHASE_WARM, warmPhase, errors);
+ validatePhase(PHASE_COLD, coldPhase, errors);
+ validatePhase(PHASE_DELETE, deletePhase, errors);
+ return errors;
+};
+
+export const getLifecycle = state => {
+ const phases = Object.entries(getPhases(state)).reduce(
+ (accum, [phaseName, phase]) => {
+ // Hot is ALWAYS enabled
+ if (phaseName === PHASE_HOT) {
+ phase[PHASE_ENABLED] = true;
+ }
+
+ if (phase[PHASE_ENABLED]) {
+ accum[phaseName] = phaseToES(state, phase);
+
+ // These seem to be constants
+ // TODO: verify this assumption
+ if (phaseName === PHASE_HOT) {
+ accum[phaseName].min_age = '0s';
+ }
+
+ if (phaseName === PHASE_DELETE) {
+ accum[phaseName].actions = {
+ ...accum[phaseName].actions,
+ delete: {}
+ };
+ }
+ }
+ return accum;
+ },
+ {}
+ );
+
+ return {
+ name: getSelectedPolicyName(state),
+ //type, TODO: figure this out (jsut store it and not let the user change it?)
+ phases
+ };
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/store/selectors/nodes.js
new file mode 100644
index 0000000000000..82774559fe275
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/nodes.js
@@ -0,0 +1,46 @@
+/*
+ * 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 { createSelector } from 'reselect';
+
+export const getNodes = state => state.nodes.nodes;
+export const getNodeOptions = createSelector(
+ [state => getNodes(state)],
+ nodes => {
+ if (!nodes) {
+ return null;
+ }
+
+ const options = Object.keys(nodes).map(attrs => ({
+ text: `${attrs} (${nodes[attrs].length})`,
+ value: attrs,
+ }));
+
+ options.sort((a, b) => a.value.localeCompare(b.value));
+ if (options.length) {
+ return [{ text: 'Default allocation (don\'t use attributes)', value: '' }, ...options];
+ } else {
+ return options;
+ }
+ }
+);
+
+export const getSelectedPrimaryShardCount = state =>
+ state.nodes.selectedPrimaryShardCount;
+export const getSelectedReplicaCount = state =>
+ state.nodes.selectedReplicaCount !== undefined ? state.nodes.selectedReplicaCount : 1;
+export const getSelectedNodeAttrs = state => state.nodes.selectedNodeAttrs;
+export const getNodesFromSelectedNodeAttrs = state => {
+ const nodes = getNodes(state)[getSelectedNodeAttrs(state)];
+ if (nodes) {
+ return nodes.length;
+ }
+ return null;
+};
+
+export const getNodeDetails = (state, selectedNodeAttrs) => {
+ return state.nodes.details[selectedNodeAttrs];
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/store/selectors/policies.js
new file mode 100644
index 0000000000000..57065812f4b6f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/policies.js
@@ -0,0 +1,273 @@
+/*
+ * 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 { createSelector } from 'reselect';
+import { Pager } from '@elastic/eui';
+import {
+ defaultColdPhase,
+ defaultDeletePhase,
+ defaultHotPhase,
+ defaultWarmPhase,
+} from '../defaults';
+import {
+ PHASE_HOT,
+ PHASE_WARM,
+ PHASE_COLD,
+ PHASE_DELETE,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+ PHASE_ROLLOVER_ENABLED,
+ PHASE_ROLLOVER_MAX_AGE,
+ PHASE_ROLLOVER_MAX_AGE_UNITS,
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+ PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS,
+ PHASE_NODE_ATTRS,
+ PHASE_FORCE_MERGE_ENABLED,
+ PHASE_FORCE_MERGE_SEGMENTS,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_REPLICA_COUNT,
+ PHASE_ENABLED,
+ PHASE_ATTRIBUTES_THAT_ARE_NUMBERS,
+ MAX_SIZE_TYPE_DOCUMENT,
+ WARM_PHASE_ON_ROLLOVER,
+ PHASE_SHRINK_ENABLED
+} from '../constants';
+import { filterItems, sortTable } from '../../services';
+
+
+export const getPolicies = state => state.policies.policies;
+export const getIsNewPolicy = state => state.policies.selectedPolicy.isNew;
+export const getSelectedPolicy = state => state.policies.selectedPolicy;
+export const getIsSelectedPolicySet = state => state.policies.selectedPolicySet;
+export const getSelectedOriginalPolicyName = state => state.policies.originalPolicyName;
+export const getPolicyFilter = (state) => state.policies.filter;
+export const getPolicySort = (state) => state.policies.sort;
+export const getPolicyCurrentPage = (state) => state.policies.currentPage;
+export const getPolicyPageSize = (state) => state.policies.pageSize;
+export const isPolicyListLoaded = (state) => state.policies.isLoaded;
+
+const getFilteredPolicies = createSelector(
+ getPolicies,
+ getPolicyFilter,
+ (policies, filter) => {
+ return filterItems(['name'], filter, policies);
+ }
+);
+export const getTotalPolicies = createSelector(
+ getFilteredPolicies,
+ (filteredPolicies) => {
+ return filteredPolicies.length;
+ }
+);
+export const getPolicyPager = createSelector(
+ getPolicyCurrentPage,
+ getPolicyPageSize,
+ getTotalPolicies,
+ (currentPage, pageSize, totalPolicies) => {
+ return new Pager(totalPolicies, pageSize, currentPage);
+ }
+);
+export const getPageOfPolicies = createSelector(
+ getFilteredPolicies,
+ getPolicySort,
+ getPolicyPager,
+ (filteredPolicies, sort, pager) => {
+ const sortedPolicies = sortTable(filteredPolicies, sort.sortField, sort.isSortAscending);
+ const { firstItemIndex, lastItemIndex } = pager;
+ const pagedPolicies = sortedPolicies.slice(firstItemIndex, lastItemIndex + 1);
+ return pagedPolicies;
+ }
+);
+export const getSaveAsNewPolicy = state =>
+ state.policies.selectedPolicy.saveAsNew;
+
+export const getSelectedPolicyName = state => {
+ if (!getSaveAsNewPolicy(state)) {
+ return getSelectedOriginalPolicyName(state);
+ }
+ return state.policies.selectedPolicy.name;
+};
+
+export const getPhases = state => state.policies.selectedPolicy.phases;
+export const getPhase = (state, phase) =>
+ getPhases(state)[phase];
+export const getPhaseData = (state, phase, key) => {
+ if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) {
+ return parseInt(getPhase(state, phase)[key]);
+ }
+ return getPhase(state, phase)[key];
+};
+
+export const splitSizeAndUnits = field => {
+ let size;
+ let units;
+
+ const result = /(\d+)(\w+)/.exec(field);
+ if (result) {
+ size = parseInt(result[1]) || 0;
+ units = result[2];
+ }
+
+ return {
+ size,
+ units
+ };
+};
+
+export const isNumber = value => typeof value === 'number';
+
+export const phaseFromES = (phase, phaseName, defaultPolicy) => {
+ const policy = { ...defaultPolicy };
+
+ if (!phase) {
+ return policy;
+ }
+
+ policy[PHASE_ENABLED] = true;
+ policy[PHASE_ROLLOVER_ENABLED] = false;
+
+ if (phase.min_age) {
+ if (phaseName === PHASE_WARM && phase.min_age === '0ms') {
+ policy[WARM_PHASE_ON_ROLLOVER] = true;
+ } else {
+ const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(
+ phase.min_age
+ );
+ policy[PHASE_ROLLOVER_MINIMUM_AGE] = minAge;
+ policy[PHASE_ROLLOVER_MINIMUM_AGE_UNITS] = minAgeUnits;
+ }
+ }
+ if (phaseName === PHASE_WARM) {
+ policy[PHASE_SHRINK_ENABLED] = !!(phase.actions && phase.actions.shrink);
+ }
+ if (phase.actions) {
+ const actions = phase.actions;
+
+ if (actions.rollover) {
+ const rollover = actions.rollover;
+ policy[PHASE_ROLLOVER_ENABLED] = true;
+ if (rollover.max_age) {
+ const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(
+ rollover.max_age
+ );
+ policy[PHASE_ROLLOVER_MAX_AGE] = maxAge;
+ policy[PHASE_ROLLOVER_MAX_AGE_UNITS] = maxAgeUnits;
+ }
+ if (rollover.max_size) {
+ const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(
+ rollover.max_size
+ );
+ policy[PHASE_ROLLOVER_MAX_SIZE_STORED] = maxSize;
+ policy[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS] = maxSizeUnits;
+ }
+ if (rollover.max_docs) {
+ policy[PHASE_ROLLOVER_MAX_SIZE_STORED] = rollover.max_docs;
+ policy[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS] = MAX_SIZE_TYPE_DOCUMENT;
+ }
+ }
+
+ if (actions.allocate) {
+ const allocate = actions.allocate;
+ if (allocate.require) {
+ Object.entries(allocate.require).forEach((entry) => {
+ policy[PHASE_NODE_ATTRS] = entry.join(':');
+ });
+ // checking for null or undefined here
+ if (allocate.number_of_replicas != null) {
+ policy[PHASE_REPLICA_COUNT] = allocate.number_of_replicas;
+ }
+ }
+ }
+
+ if (actions.forcemerge) {
+ const forcemerge = actions.forcemerge;
+ policy[PHASE_FORCE_MERGE_ENABLED] = true;
+ policy[PHASE_FORCE_MERGE_SEGMENTS] = forcemerge.max_num_segments;
+ }
+
+ if (actions.shrink) {
+ policy[PHASE_PRIMARY_SHARD_COUNT] = actions.shrink.number_of_shards;
+ }
+ }
+ return policy;
+};
+
+export const policyFromES = (policy) => {
+ const { name, policy: { phases } } = policy;
+ return {
+ name,
+ phases: {
+ [PHASE_HOT]: phaseFromES(phases[PHASE_HOT], PHASE_HOT, defaultHotPhase),
+ [PHASE_WARM]: phaseFromES(phases[PHASE_WARM], PHASE_WARM, defaultWarmPhase),
+ [PHASE_COLD]: phaseFromES(phases[PHASE_COLD], PHASE_COLD, defaultColdPhase),
+ [PHASE_DELETE]: phaseFromES(phases[PHASE_DELETE], PHASE_DELETE, defaultDeletePhase)
+ },
+ isNew: false,
+ saveAsNew: false
+ };
+};
+
+export const phaseToES = (state, phase) => {
+ const esPhase = {};
+
+ if (!phase[PHASE_ENABLED]) {
+ return esPhase;
+ }
+
+ if (isNumber(phase[PHASE_ROLLOVER_MINIMUM_AGE])) {
+ esPhase.min_age = `${phase[PHASE_ROLLOVER_MINIMUM_AGE]}${phase[PHASE_ROLLOVER_MINIMUM_AGE_UNITS]}`;
+ }
+
+ esPhase.actions = {};
+
+ if (phase[PHASE_ROLLOVER_ENABLED]) {
+ esPhase.actions.rollover = {};
+
+ if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE])) {
+ esPhase.actions.rollover.max_age = `${phase[PHASE_ROLLOVER_MAX_AGE]}${
+ phase[PHASE_ROLLOVER_MAX_AGE_UNITS]
+ }`;
+ }
+ if (isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED])) {
+ if (phase[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS] === MAX_SIZE_TYPE_DOCUMENT) {
+ esPhase.actions.rollover.max_docs = phase[PHASE_ROLLOVER_MAX_SIZE_STORED];
+ } else {
+ esPhase.actions.rollover.max_size = `${phase[PHASE_ROLLOVER_MAX_SIZE_STORED]}${
+ phase[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]
+ }`;
+ }
+ }
+ }
+ if (phase[PHASE_NODE_ATTRS]) {
+ const [ name, value, ] = phase[PHASE_NODE_ATTRS].split(':');
+ esPhase.actions.allocate = {
+ include: {},
+ exclude: {},
+ require: {
+ [name]: value
+ }
+ };
+ }
+ if (isNumber(phase[PHASE_REPLICA_COUNT])) {
+ esPhase.actions.allocate = esPhase.actions.allocate || {};
+ esPhase.actions.allocate.number_of_replicas = phase[PHASE_REPLICA_COUNT];
+ }
+
+ if (phase[PHASE_FORCE_MERGE_ENABLED]) {
+ esPhase.actions.forcemerge = {
+ max_num_segments: phase[PHASE_FORCE_MERGE_SEGMENTS]
+ };
+ }
+
+ if (phase[PHASE_SHRINK_ENABLED] && isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) {
+ esPhase.actions.shrink = {
+ number_of_shards: phase[PHASE_PRIMARY_SHARD_COUNT]
+ };
+ }
+ return esPhase;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/store.js b/x-pack/plugins/index_lifecycle_management/public/store/store.js
new file mode 100644
index 0000000000000..151a7a5bf8b50
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/store.js
@@ -0,0 +1,28 @@
+/*
+ * 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 {
+ createStore,
+ applyMiddleware,
+ compose
+} from 'redux';
+import thunk from 'redux-thunk';
+
+import {
+ indexLifecycleManagement
+} from './reducers/';
+
+
+export const indexLifecycleManagementStore = (initialState = {}) => {
+ const enhancers = [applyMiddleware(thunk)];
+
+ window.__REDUX_DEVTOOLS_EXTENSION__ && enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__());
+ return createStore(
+ indexLifecycleManagement,
+ initialState,
+ compose(...enhancers)
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.js
new file mode 100644
index 0000000000000..b9a77a1a0362b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/call_with_request_factory/call_with_request_factory.js
@@ -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.
+ */
+
+import { once } from 'lodash';
+
+const callWithRequest = once((server) => {
+ const cluster = server.plugins.elasticsearch.getCluster('data');
+ return cluster.callWithRequest;
+});
+
+export const callWithRequestFactory = (server, request) => {
+ return (...args) => {
+ return callWithRequest(server)(request, ...args);
+ };
+};
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.js b/x-pack/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.js
new file mode 100644
index 0000000000000..787814d87dff9
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/call_with_request_factory/index.js
@@ -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 { callWithRequestFactory } from './call_with_request_factory';
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js
new file mode 100644
index 0000000000000..19a7b56759269
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/__tests__/check_license.js
@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from 'expect.js';
+import { set } from 'lodash';
+import { checkLicense } from '../check_license';
+
+describe('check_license', function () {
+
+ let mockLicenseInfo;
+ beforeEach(() => mockLicenseInfo = {});
+
+ describe('license information is undefined', () => {
+ beforeEach(() => mockLicenseInfo = undefined);
+
+ it('should set isAvailable to false', () => {
+ expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
+ });
+
+ it('should set showLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
+ });
+
+ it('should set enableLinks to false', () => {
+ expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false);
+ });
+
+ it('should set a message', () => {
+ expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
+ });
+ });
+
+ describe('license information is not available', () => {
+ beforeEach(() => mockLicenseInfo.isAvailable = () => false);
+
+ it('should set isAvailable to false', () => {
+ expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
+ });
+
+ it('should set showLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
+ });
+
+ it('should set enableLinks to false', () => {
+ expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false);
+ });
+
+ it('should set a message', () => {
+ expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
+ });
+ });
+
+ describe('license information is available', () => {
+ beforeEach(() => {
+ mockLicenseInfo.isAvailable = () => true;
+ set(mockLicenseInfo, 'license.getType', () => 'basic');
+ });
+
+ describe('& license is trial, standard, gold, platinum', () => {
+ beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true));
+
+ describe('& license is active', () => {
+ beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true));
+
+ it('should set isAvailable to true', () => {
+ expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true);
+ });
+
+ it ('should set showLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
+ });
+
+ it ('should set enableLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true);
+ });
+
+ it('should not set a message', () => {
+ expect(checkLicense(mockLicenseInfo).message).to.be(undefined);
+ });
+ });
+
+ describe('& license is expired', () => {
+ beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false));
+
+ it('should set isAvailable to false', () => {
+ expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
+ });
+
+ it ('should set showLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
+ });
+
+ it ('should set enableLinks to false', () => {
+ expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false);
+ });
+
+ it('should set a message', () => {
+ expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
+ });
+ });
+ });
+
+ describe('& license is basic', () => {
+ beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true));
+
+ describe('& license is active', () => {
+ beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true));
+
+ it('should set isAvailable to true', () => {
+ expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true);
+ });
+
+ it ('should set showLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
+ });
+
+ it ('should set enableLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true);
+ });
+
+ it('should not set a message', () => {
+ expect(checkLicense(mockLicenseInfo).message).to.be(undefined);
+ });
+ });
+
+ describe('& license is expired', () => {
+ beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false));
+
+ it('should set isAvailable to false', () => {
+ expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false);
+ });
+
+ it ('should set showLinks to true', () => {
+ expect(checkLicense(mockLicenseInfo).showLinks).to.be(true);
+ });
+
+ it('should set a message', () => {
+ expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined);
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/check_license/check_license.js b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/check_license.js
new file mode 100644
index 0000000000000..4dff66dffa8d1
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/check_license.js
@@ -0,0 +1,67 @@
+/*
+ * 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';
+export function checkLicense(xpackLicenseInfo) {
+ const pluginName = 'Index Management';
+
+ // If, for some reason, we cannot get the license information
+ // from Elasticsearch, assume worst case and disable
+ if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) {
+ return {
+ isAvailable: false,
+ showLinks: true,
+ enableLinks: false,
+ message: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', {
+ defaultMessage: `You cannot use {pluginName} because license information is not available at this time.`,
+ values: { pluginName }
+ })
+ };
+ }
+
+ const VALID_LICENSE_MODES = [
+ 'trial',
+ 'basic',
+ 'standard',
+ 'gold',
+ 'platinum'
+ ];
+
+ const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES);
+ const isLicenseActive = xpackLicenseInfo.license.isActive();
+ const licenseType = xpackLicenseInfo.license.getType();
+
+ // License is not valid
+ if (!isLicenseModeValid) {
+ return {
+ isAvailable: false,
+ showLinks: false,
+ message: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', {
+ defaultMessage: `Your {licenseType} license does not support ${pluginName}. Please upgrade your license.`,
+ values: { licenseType }
+ })
+ };
+ }
+
+ // License is valid but not active
+ if (!isLicenseActive) {
+ return {
+ isAvailable: false,
+ showLinks: true,
+ enableLinks: false,
+ message: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', {
+ defaultMessage: `You cannot use {pluginName} because your {licenseType} license has expired.`,
+ values: { pluginName, licenseType }
+ })
+ };
+ }
+
+ // License is valid and active
+ return {
+ isAvailable: true,
+ showLinks: true,
+ enableLinks: true
+ };
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/check_license/index.js b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/index.js
new file mode 100644
index 0000000000000..f2c070fd44b6e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/index.js
@@ -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 { checkLicense } from './check_license';
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js
new file mode 100644
index 0000000000000..443744ccb0cc8
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_custom_error.js
@@ -0,0 +1,21 @@
+/*
+ * 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 expect from 'expect.js';
+import { wrapCustomError } from '../wrap_custom_error';
+
+describe('wrap_custom_error', () => {
+ describe('#wrapCustomError', () => {
+ it('should return a Boom object', () => {
+ const originalError = new Error('I am an error');
+ const statusCode = 404;
+ const wrappedError = wrapCustomError(originalError, statusCode);
+
+ expect(wrappedError.isBoom).to.be(true);
+ expect(wrappedError.output.statusCode).to.equal(statusCode);
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js
new file mode 100644
index 0000000000000..394c182140000
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_es_error.js
@@ -0,0 +1,39 @@
+/*
+ * 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 expect from 'expect.js';
+import { wrapEsError } from '../wrap_es_error';
+
+describe('wrap_es_error', () => {
+ describe('#wrapEsError', () => {
+
+ let originalError;
+ beforeEach(() => {
+ originalError = new Error('I am an error');
+ originalError.statusCode = 404;
+ });
+
+ it('should return a Boom object', () => {
+ const wrappedError = wrapEsError(originalError);
+
+ expect(wrappedError.isBoom).to.be(true);
+ });
+
+ it('should return the correct Boom object', () => {
+ const wrappedError = wrapEsError(originalError);
+
+ expect(wrappedError.output.statusCode).to.be(originalError.statusCode);
+ expect(wrappedError.output.payload.message).to.be(originalError.message);
+ });
+
+ it('should return the correct Boom object with custom message', () => {
+ const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' });
+
+ expect(wrappedError.output.statusCode).to.be(originalError.statusCode);
+ expect(wrappedError.output.payload.message).to.be('No encontrado!');
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js
new file mode 100644
index 0000000000000..6d6a336417bef
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/__tests__/wrap_unknown_error.js
@@ -0,0 +1,19 @@
+/*
+ * 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 expect from 'expect.js';
+import { wrapUnknownError } from '../wrap_unknown_error';
+
+describe('wrap_unknown_error', () => {
+ describe('#wrapUnknownError', () => {
+ it('should return a Boom object', () => {
+ const originalError = new Error('I am an error');
+ const wrappedError = wrapUnknownError(originalError);
+
+ expect(wrappedError.isBoom).to.be(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/index.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/index.js
new file mode 100644
index 0000000000000..f275f15637091
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/index.js
@@ -0,0 +1,9 @@
+/*
+ * 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 { wrapCustomError } from './wrap_custom_error';
+export { wrapEsError } from './wrap_es_error';
+export { wrapUnknownError } from './wrap_unknown_error';
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.js
new file mode 100644
index 0000000000000..3295113d38ee5
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_custom_error.js
@@ -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.
+ */
+
+import Boom from 'boom';
+
+/**
+ * Wraps a custom error into a Boom error response and returns it
+ *
+ * @param err Object error
+ * @param statusCode Error status code
+ * @return Object Boom error response
+ */
+export function wrapCustomError(err, statusCode) {
+ return Boom.boomify(err, { statusCode });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.js
new file mode 100644
index 0000000000000..2df2e4b802e1a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_es_error.js
@@ -0,0 +1,30 @@
+/*
+ * 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 Boom from 'boom';
+
+/**
+ * Wraps an error thrown by the ES JS client into a Boom error response and returns it
+ *
+ * @param err Object Error thrown by ES JS client
+ * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages
+ * @return Object Boom error response
+ */
+export function wrapEsError(err, statusCodeToMessageMap = {}) {
+
+ const statusCode = err.statusCode;
+
+ // If no custom message if specified for the error's status code, just
+ // wrap the error as a Boom error response and return it
+ if (!statusCodeToMessageMap[statusCode]) {
+ return Boom.boomify(err, { statusCode });
+ }
+
+ // Otherwise, use the custom message to create a Boom error response and
+ // return it
+ const message = statusCodeToMessageMap[statusCode];
+ return new Boom(message, { statusCode });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.js
new file mode 100644
index 0000000000000..4b865880ae20d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/error_wrappers/wrap_unknown_error.js
@@ -0,0 +1,17 @@
+/*
+ * 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 Boom from 'boom';
+
+/**
+ * Wraps an unknown error into a Boom error response and returns it
+ *
+ * @param err Object Unknown error
+ * @return Object Boom error response
+ */
+export function wrapUnknownError(err) {
+ return Boom.boomify(err);
+}
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js
new file mode 100644
index 0000000000000..d50ff9480d3e4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js
@@ -0,0 +1,48 @@
+/*
+ * 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 expect from 'expect.js';
+import { isEsErrorFactory } from '../is_es_error_factory';
+import { set } from 'lodash';
+
+class MockAbstractEsError {}
+
+describe('is_es_error_factory', () => {
+
+ let mockServer;
+ let isEsError;
+
+ beforeEach(() => {
+ const mockEsErrors = {
+ _Abstract: MockAbstractEsError
+ };
+ mockServer = {};
+ set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors }));
+
+ isEsError = isEsErrorFactory(mockServer);
+ });
+
+ describe('#isEsErrorFactory', () => {
+
+ it('should return a function', () => {
+ expect(isEsError).to.be.a(Function);
+ });
+
+ describe('returned function', () => {
+
+ it('should return true if passed-in err is a known esError', () => {
+ const knownEsError = new MockAbstractEsError();
+ expect(isEsError(knownEsError)).to.be(true);
+ });
+
+ it('should return false if passed-in err is not a known esError', () => {
+ const unknownEsError = {};
+ expect(isEsError(unknownEsError)).to.be(false);
+
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/index.js b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/index.js
new file mode 100644
index 0000000000000..441648a8701e0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/index.js
@@ -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 { isEsErrorFactory } from './is_es_error_factory';
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/is_es_error_factory.js b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/is_es_error_factory.js
new file mode 100644
index 0000000000000..80daac5bd496d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error_factory/is_es_error_factory.js
@@ -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.
+ */
+
+import { memoize } from 'lodash';
+
+const esErrorsFactory = memoize((server) => {
+ return server.plugins.elasticsearch.getCluster('admin').errors;
+});
+
+export function isEsErrorFactory(server) {
+ const esErrors = esErrorsFactory(server);
+ return function isEsError(err) {
+ return err instanceof esErrors._Abstract;
+ };
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js
new file mode 100644
index 0000000000000..359b3fb2ce6f4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js
@@ -0,0 +1,69 @@
+/*
+ * 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 expect from 'expect.js';
+import { licensePreRoutingFactory } from '../license_pre_routing_factory';
+
+describe('license_pre_routing_factory', () => {
+ describe('#reportingFeaturePreRoutingFactory', () => {
+ let mockServer;
+ let mockLicenseCheckResults;
+
+ beforeEach(() => {
+ mockServer = {
+ plugins: {
+ xpack_main: {
+ info: {
+ feature: () => ({
+ getLicenseCheckResults: () => mockLicenseCheckResults
+ })
+ }
+ }
+ }
+ };
+ });
+
+ it('only instantiates one instance per server', () => {
+ const firstInstance = licensePreRoutingFactory(mockServer);
+ const secondInstance = licensePreRoutingFactory(mockServer);
+
+ expect(firstInstance).to.be(secondInstance);
+ });
+
+ describe('isAvailable is false', () => {
+ beforeEach(() => {
+ mockLicenseCheckResults = {
+ isAvailable: false
+ };
+ });
+
+ it ('replies with 403', () => {
+ const licensePreRouting = licensePreRoutingFactory(mockServer);
+ const stubRequest = {};
+ expect(() => licensePreRouting(stubRequest)).to.throwException((response) => {
+ expect(response).to.be.an(Error);
+ expect(response.isBoom).to.be(true);
+ expect(response.output.statusCode).to.be(403);
+ });
+ });
+ });
+
+ describe('isAvailable is true', () => {
+ beforeEach(() => {
+ mockLicenseCheckResults = {
+ isAvailable: true
+ };
+ });
+
+ it ('replies with nothing', () => {
+ const licensePreRouting = licensePreRoutingFactory(mockServer);
+ const stubRequest = {};
+ const response = licensePreRouting(stubRequest);
+ expect(response).to.be(null);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.js b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.js
new file mode 100644
index 0000000000000..0743e443955f4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/index.js
@@ -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 { licensePreRoutingFactory } from './license_pre_routing_factory';
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.js
new file mode 100644
index 0000000000000..af2b3acb287c7
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.js
@@ -0,0 +1,28 @@
+/*
+ * 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 { once } from 'lodash';
+import { wrapCustomError } from '../error_wrappers';
+import { PLUGIN_ID } from '../../../common/constants';
+
+export const licensePreRoutingFactory = once((server) => {
+ const xpackMainPlugin = server.plugins.xpack_main;
+
+ // License checking and enable/disable logic
+ function licensePreRouting() {
+ const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN_ID).getLicenseCheckResults();
+ if (!licenseCheckResults.isAvailable) {
+ const error = new Error(licenseCheckResults.message);
+ const statusCode = 403;
+ throw wrapCustomError(error, statusCode);
+ }
+
+ return null;
+ }
+
+ return licensePreRouting;
+});
+
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/register_license_checker/index.js b/x-pack/plugins/index_lifecycle_management/server/lib/register_license_checker/index.js
new file mode 100644
index 0000000000000..7b0f97c38d129
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/register_license_checker/index.js
@@ -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 { registerLicenseChecker } from './register_license_checker';
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.js b/x-pack/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.js
new file mode 100644
index 0000000000000..260fc15c7a83f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/register_license_checker/register_license_checker.js
@@ -0,0 +1,21 @@
+/*
+ * 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 { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status';
+import { checkLicense } from '../check_license';
+import { PLUGIN_ID } from '../../../common/constants';
+
+export function registerLicenseChecker(server) {
+ const xpackMainPlugin = server.plugins.xpack_main;
+ const ilmPlugin = server.plugins.index_lifecycle_management;
+
+ mirrorPluginStatus(xpackMainPlugin, ilmPlugin);
+ xpackMainPlugin.status.once('green', () => {
+ // Register a function that is called whenever the xpack info changes,
+ // to re-compute the license check results for this plugin
+ xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense);
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.js
new file mode 100644
index 0000000000000..82fb2e3b2a372
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/index.js
@@ -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 { registerIndexRoutes } from './register_index_routes';
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.js
new file mode 100644
index 0000000000000..2e64758bade0b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.js
@@ -0,0 +1,54 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+async function addLifecyclePolicy(callWithRequest, indexName, policyName, alias) {
+ const body = {
+ lifecycle: {
+ name: policyName,
+ rollover_alias: alias
+ }
+ };
+
+ const params = {
+ method: 'PUT',
+ path: `/${encodeURIComponent(indexName)}/_settings`,
+ body
+ };
+
+ return callWithRequest('transport.request', params);
+}
+
+export function registerAddPolicyRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/index/add',
+ method: 'POST',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+ const { indexName, policyName, alias } = request.payload;
+ try {
+ const response = await addLifecyclePolicy(callWithRequest, indexName, policyName, alias);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_bootstrap_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_bootstrap_route.js
new file mode 100644
index 0000000000000..ad449101cb4f5
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_bootstrap_route.js
@@ -0,0 +1,50 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
+
+async function bootstrap(callWithRequest, payload) {
+ await callWithRequest('indices.create', {
+ index: payload.indexName,
+ body: {
+ aliases: {
+ [payload.aliasName]: {
+ is_write_alias: true
+ }
+ },
+ }
+ });
+}
+
+export function registerBootstrapRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/indices/bootstrap',
+ method: 'POST',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await bootstrap(callWithRequest, request.payload);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [licensePreRouting]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_get_affected_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_get_affected_route.js
new file mode 100644
index 0000000000000..ed5b6b09db16b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_get_affected_route.js
@@ -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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
+
+async function fetchTemplates(callWithRequest) {
+ const params = {
+ method: 'GET',
+ path: '/_template',
+ // we allow 404 in case there are no templates
+ ignore: [404]
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+
+async function getAffectedIndices(
+ callWithRequest,
+ indexTemplateName,
+ policyName
+) {
+ const templates = await fetchTemplates(callWithRequest);
+
+ if (!templates || Object.keys(templates).length === 0 || templates.status === 404) {
+ return [];
+ }
+
+ const indexPatterns = Object.entries(templates).reduce((accum, [templateName, template]) => {
+ const isMatchingTemplate = templateName === indexTemplateName;
+ const isMatchingPolicy = (
+ policyName &&
+ template.settings &&
+ template.settings.index &&
+ template.settings.index.lifecycle &&
+ template.settings.index.lifecycle.name === policyName
+ );
+ if (isMatchingTemplate || isMatchingPolicy) {
+ accum.push(...template.index_patterns);
+ }
+ return accum;
+ }, []);
+
+ if (indexPatterns.length === 0) {
+ return [];
+ }
+ const indexParams = {
+ method: 'GET',
+ path: `/${indexPatterns.join(',')}`,
+ // we allow 404 in case there are no indices
+ ignore: [404]
+ };
+ const indices = await callWithRequest('transport.request', indexParams);
+
+ if (!indices || indices.status === 404) {
+ return [];
+ }
+
+ return Object.keys(indices);
+}
+
+export function registerGetAffectedRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path:
+ '/api/index_lifecycle_management/indices/affected/{indexTemplateName}',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await getAffectedIndices(
+ callWithRequest,
+ request.params.indexTemplateName,
+ );
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [licensePreRouting]
+ }
+ });
+
+ server.route({
+ path:
+ '/api/index_lifecycle_management/indices/affected/{indexTemplateName}/{policyName}',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await getAffectedIndices(
+ callWithRequest,
+ request.params.indexTemplateName,
+ request.params.policyName
+ );
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [licensePreRouting]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.js
new file mode 100644
index 0000000000000..e5c210f16632e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.js
@@ -0,0 +1,19 @@
+/*
+ * 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 { registerBootstrapRoute } from './register_bootstrap_route';
+import { registerGetAffectedRoute } from './register_get_affected_route';
+import { registerRetryRoute } from './register_retry_route';
+import { registerRemoveRoute } from './register_remove_route';
+import { registerAddPolicyRoute } from './register_add_policy_route';
+
+export function registerIndexRoutes(server) {
+ registerBootstrapRoute(server);
+ registerGetAffectedRoute(server);
+ registerRetryRoute(server);
+ registerRemoveRoute(server);
+ registerAddPolicyRoute(server);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.js
new file mode 100644
index 0000000000000..943c584ea6cce
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.js
@@ -0,0 +1,52 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+async function removeLifecycle(callWithRequest, indexNames) {
+ const responses = [];
+ for (let i = 0; i < indexNames.length; i++) {
+ const indexName = indexNames[i];
+ const params = {
+ method: 'POST',
+ path: `/${encodeURIComponent(indexName)}/_ilm/remove`,
+ ignore: [ 404 ],
+ };
+
+ responses.push(callWithRequest('transport.request', params));
+ }
+ return Promise.all(responses);
+}
+
+export function registerRemoveRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/index/remove',
+ method: 'POST',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await removeLifecycle(callWithRequest, request.payload.indexNames);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.js
new file mode 100644
index 0000000000000..986e4bc794a7a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.js
@@ -0,0 +1,52 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+async function retryLifecycle(callWithRequest, indexNames) {
+ const responses = [];
+ for (let i = 0; i < indexNames.length; i++) {
+ const indexName = indexNames[i];
+ const params = {
+ method: 'POST',
+ path: `/${encodeURIComponent(indexName)}/_ilm/retry`,
+ ignore: [ 404 ],
+ };
+
+ responses.push(callWithRequest('transport.request', params));
+ }
+ return Promise.all(responses);
+}
+
+export function registerRetryRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/index/retry',
+ method: 'POST',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await retryLifecycle(callWithRequest, request.payload.indexNames);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/index.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/index.js
new file mode 100644
index 0000000000000..17f52a723405d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/index.js
@@ -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 { registerLifecycleRoutes } from './register_lifecycle_routes';
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_create_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_create_route.js
new file mode 100644
index 0000000000000..88db36d8c642c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_create_route.js
@@ -0,0 +1,56 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+async function createLifecycle(callWithRequest, lifecycle) {
+ const body = {
+ policy: {
+ phases: lifecycle.phases,
+ }
+ };
+ const params = {
+ method: 'PUT',
+ path: `/_ilm/policy/${encodeURIComponent(lifecycle.name)}`,
+ ignore: [ 404 ],
+ body,
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+
+export function registerCreateRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/lifecycle',
+ method: 'POST',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await createLifecycle(callWithRequest, request.payload.lifecycle);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_lifecycle_routes.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_lifecycle_routes.js
new file mode 100644
index 0000000000000..ba179d14b8112
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_lifecycle_routes.js
@@ -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.
+ */
+
+import { registerCreateRoute } from './register_create_route';
+
+export function registerLifecycleRoutes(server) {
+ registerCreateRoute(server);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/constants.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/constants.js
new file mode 100644
index 0000000000000..2e2b84a76002e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/constants.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const NODE_ATTRS_KEYS_TO_IGNORE = [
+ 'ml.enabled',
+ 'ml.machine_memory',
+ 'ml.max_open_jobs',
+ 'testattr',
+ 'xpack.installed',
+];
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.js
new file mode 100644
index 0000000000000..ef0ac271ae60e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/index.js
@@ -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 { registerNodesRoutes } from './register_nodes_routes';
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.js
new file mode 100644
index 0000000000000..9547e66c73dc3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.js
@@ -0,0 +1,65 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+function findMatchingNodes(stats, nodeAttrs) {
+ return Object.entries(stats.nodes).reduce((accum, [nodeId, stats]) => {
+ const attributes = stats.attributes || {};
+ for (const [key, value] of Object.entries(attributes)) {
+ if (`${key}:${value}` === nodeAttrs) {
+ accum.push({
+ nodeId,
+ stats,
+ });
+ break;
+ }
+ }
+ return accum;
+ }, []);
+}
+
+async function fetchNodeStats(callWithRequest) {
+ const params = {
+ format: 'json'
+ };
+
+ return await callWithRequest('nodes.stats', params);
+}
+
+export function registerDetailsRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/nodes/{nodeAttrs}/details',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const stats = await fetchNodeStats(callWithRequest);
+ const response = findMatchingNodes(stats, request.params.nodeAttrs);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.js
new file mode 100644
index 0000000000000..e5aed9fbd260b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.js
@@ -0,0 +1,67 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+import { NODE_ATTRS_KEYS_TO_IGNORE } from './constants';
+
+function convertStatsIntoList(stats, attributesToBeFiltered) {
+ return Object.entries(stats.nodes).reduce((accum, [nodeId, stats]) => {
+ const attributes = stats.attributes || {};
+ for (const [key, value] of Object.entries(attributes)) {
+ if (!attributesToBeFiltered.includes(key)) {
+ const attributeString = `${key}:${value}`;
+ accum[attributeString] = accum[attributeString] || [];
+ accum[attributeString].push(nodeId);
+ }
+ }
+ return accum;
+ }, {});
+}
+
+async function fetchNodeStats(callWithRequest) {
+ const params = {
+ format: 'json'
+ };
+
+ return await callWithRequest('nodes.stats', params);
+}
+
+export function registerListRoute(server) {
+ const config = server.config();
+ const filteredNodeAttributes = config.get('xpack.ilm.filteredNodeAttributes');
+ const attributesToBeFiltered = [...NODE_ATTRS_KEYS_TO_IGNORE, ...filteredNodeAttributes];
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/nodes/list',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const stats = await fetchNodeStats(callWithRequest);
+ const response = convertStatsIntoList(stats, attributesToBeFiltered);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.js
new file mode 100644
index 0000000000000..341f1d4f1ebf3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_nodes_routes.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerListRoute } from './register_list_route';
+import { registerDetailsRoute } from './register_details_route';
+
+export function registerNodesRoutes(server) {
+ registerListRoute(server);
+ registerDetailsRoute(server);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.js
new file mode 100644
index 0000000000000..7c6103a3389ab
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/index.js
@@ -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 { registerPoliciesRoutes } from './register_policies_routes';
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.js
new file mode 100644
index 0000000000000..7a13ca4062fc1
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.js
@@ -0,0 +1,47 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+async function deletePolicies(policyNames, callWithRequest) {
+ const params = {
+ method: 'DELETE',
+ path: `/_ilm/policy/${encodeURIComponent(policyNames)}`,
+ // we allow 404 since they may have no policies
+ ignore: [ 404 ]
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+
+export function registerDeleteRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/policies/{policyNames}',
+ method: 'DELETE',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+ const { policyNames } = request.params;
+ try {
+ await deletePolicies(policyNames, callWithRequest);
+ return {};
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.js
new file mode 100644
index 0000000000000..df2c0096cddff
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.js
@@ -0,0 +1,84 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+function formatPolicies(policiesMap) {
+ if (policiesMap.status === 404) {
+ return [];
+ }
+ return Object.keys(policiesMap).reduce((accum, lifecycleName) => {
+ const policyEntry = policiesMap[lifecycleName];
+ accum.push({
+ ...policyEntry,
+ name: lifecycleName,
+ });
+ return accum;
+ }, []);
+}
+
+async function fetchPolicies(callWithRequest) {
+ const params = {
+ method: 'GET',
+ path: '/_ilm/policy',
+ // we allow 404 since they may have no policies
+ ignore: [ 404 ]
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+async function addCoveredIndices(policiesMap, callWithRequest) {
+ if (policiesMap.status === 404) {
+ return policiesMap;
+ }
+ const params = {
+ method: 'GET',
+ path: '/*/_ilm/explain',
+ // we allow 404 since they may have no policies
+ ignore: [ 404 ]
+ };
+
+ const policyExplanation = await callWithRequest('transport.request', params);
+ Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]) => {
+ if (policy) {
+ policiesMap[policy].coveredIndices = policiesMap[policy].coveredIndices || [];
+ policiesMap[policy].coveredIndices.push(indexName);
+ }
+ });
+}
+
+export function registerFetchRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/policies',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+ const { withIndices } = request.query;
+ try {
+ const policiesMap = await fetchPolicies(callWithRequest);
+ if (withIndices) {
+ await addCoveredIndices(policiesMap, callWithRequest);
+ }
+ return formatPolicies(policiesMap);
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.js
new file mode 100644
index 0000000000000..40cf2430641b5
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_routes.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerFetchRoute } from './register_fetch_route';
+import { registerDeleteRoute } from './register_delete_route';
+
+export function registerPoliciesRoutes(server) {
+ registerFetchRoute(server);
+ registerDeleteRoute(server);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.js
new file mode 100644
index 0000000000000..dc9a0acaaf09b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/index.js
@@ -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 { registerTemplatesRoutes } from './register_templates_routes';
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.js
new file mode 100644
index 0000000000000..ba6d42e720023
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.js
@@ -0,0 +1,70 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+import { merge } from 'lodash';
+
+async function getIndexTemplate(callWithRequest, templateName) {
+ const response = await callWithRequest('indices.getTemplate', { name: templateName });
+ return response[templateName];
+}
+
+async function updateIndexTemplate(callWithRequest, indexTemplatePatch) {
+ // Fetch existing template
+ const template = await getIndexTemplate(callWithRequest, indexTemplatePatch.templateName);
+ merge(template, {
+ settings: {
+ index: {
+ lifecycle: {
+ name: indexTemplatePatch.policyName,
+ rollover_alias: indexTemplatePatch.aliasName
+ },
+ }
+ }
+ });
+
+ const params = {
+ method: 'PUT',
+ path: `/_template/${indexTemplatePatch.templateName}`,
+ ignore: [ 404 ],
+ body: template,
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+
+export function registerAddPolicyRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/template',
+ method: 'POST',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const response = await updateIndexTemplate(callWithRequest, request.payload);
+ return response;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.js
new file mode 100644
index 0000000000000..c2bdeccc39a52
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.js
@@ -0,0 +1,81 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+import { any } from 'lodash';
+
+function isReservedSystemTemplate(templateName, indexPattens) {
+ return templateName.startsWith('kibana_index_template') ||
+ (
+ templateName.startsWith('.') &&
+ !any(indexPattens, (pattern) => {
+ return pattern.includes('*');
+ })
+ );
+}
+async function filterAndFormatTemplates(templates) {
+ const formattedTemplates = [];
+ const templateNames = Object.keys(templates);
+ for (const templateName of templateNames) {
+ const { settings, index_patterns } = templates[templateName]; // eslint-disable-line camelcase
+ if (isReservedSystemTemplate(templateName, index_patterns)) {
+ 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,
+ };
+ formattedTemplates.push(formattedTemplate);
+ }
+ return formattedTemplates;
+}
+
+async function fetchTemplates(callWithRequest) {
+ const params = {
+ method: 'GET',
+ path: '/_template',
+ // we allow 404 incase the user shutdown security in-between the check and now
+ ignore: [ 404 ]
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+export function registerFetchRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/templates',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+
+ try {
+ const hits = await fetchTemplates(callWithRequest);
+ const templates = await filterAndFormatTemplates(hits);
+ return templates;
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.js
new file mode 100644
index 0000000000000..ad24160fe798f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_get_route.js
@@ -0,0 +1,52 @@
+/*
+ * 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 { callWithRequestFactory } from '../../../lib/call_with_request_factory';
+import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
+import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+
+async function fetchTemplate(callWithRequest, templateName) {
+ const params = {
+ method: 'GET',
+ path: `/_template/${templateName}`,
+ // we allow 404 incase the user shutdown security in-between the check and now
+ ignore: [ 404 ]
+ };
+
+ return await callWithRequest('transport.request', params);
+}
+
+export function registerGetRoute(server) {
+ const isEsError = isEsErrorFactory(server);
+ const licensePreRouting = licensePreRoutingFactory(server);
+
+ server.route({
+ path: '/api/index_lifecycle_management/template/{templateName}',
+ method: 'GET',
+ handler: async (request) => {
+ const callWithRequest = callWithRequestFactory(server, request);
+ const templateName = request.params.templateName;
+
+ try {
+ const template = await fetchTemplate(callWithRequest, templateName);
+ return template[templateName];
+ } catch (err) {
+ if (isEsError(err)) {
+ return wrapEsError(err);
+ }
+
+ return wrapUnknownError(err);
+ }
+ },
+ config: {
+ pre: [ licensePreRouting ]
+ }
+ });
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.js
new file mode 100644
index 0000000000000..3f737d1f70f56
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.js
@@ -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.
+ */
+
+
+
+
+import { registerFetchRoute } from './register_fetch_route';
+import { registerGetRoute } from './register_get_route';
+import { registerAddPolicyRoute } from './register_add_policy_route';
+
+export function registerTemplatesRoutes(server) {
+ registerFetchRoute(server);
+ registerGetRoute(server);
+ registerAddPolicyRoute(server);
+}
diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
index 31c0d6e7e1503..b6cb1947b5d18 100644
--- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js
+++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
@@ -121,7 +121,7 @@ describe('index table', () => {
store = indexManagementStore();
component = (
-
+
@@ -198,8 +198,9 @@ describe('index table', () => {
});
test('should filter based on content of search input', () => {
const rendered = mountWithIntl(component);
- const searchInput = findTestSubject(rendered, 'indexTableFilterInput');
- searchInput.simulate('change', { target: { value: 'testy0' } });
+ const searchInput = rendered.find('.euiFieldSearch').first();
+ searchInput.instance().value = 'testy0';
+ searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 });
rendered.update();
snapshot(namesText(rendered));
});
diff --git a/x-pack/plugins/index_management/index.js b/x-pack/plugins/index_management/index.js
index 4408428364b83..c48eaa6ba15ca 100644
--- a/x-pack/plugins/index_management/index.js
+++ b/x-pack/plugins/index_management/index.js
@@ -11,7 +11,7 @@ import { registerSettingsRoutes } from './server/routes/api/settings';
import { registerStatsRoute } from './server/routes/api/stats';
import { registerLicenseChecker } from './server/lib/register_license_checker';
import { PLUGIN } from './common/constants';
-
+import { addIndexManagementDataEnricher } from "./index_management_data";
export function indexManagement(kibana) {
return new kibana.Plugin({
id: PLUGIN.ID,
@@ -24,6 +24,7 @@ export function indexManagement(kibana) {
]
},
init: function (server) {
+ server.expose('addIndexManagementDataEnricher', addIndexManagementDataEnricher);
registerLicenseChecker(server);
registerIndicesRoutes(server);
registerSettingsRoutes(server);
diff --git a/x-pack/plugins/index_management/index_management_data.js b/x-pack/plugins/index_management/index_management_data.js
new file mode 100644
index 0000000000000..60b2ffba236de
--- /dev/null
+++ b/x-pack/plugins/index_management/index_management_data.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+const indexManagementDataEnrichers = [];
+export const addIndexManagementDataEnricher = (enricher) => {
+ indexManagementDataEnrichers.push(enricher);
+};
+export const getIndexManagementDataEnrichers = () => {
+ return indexManagementDataEnrichers;
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_management/public/app.js b/x-pack/plugins/index_management/public/app.js
index 6791edd562dae..c0b01f63919c7 100644
--- a/x-pack/plugins/index_management/public/app.js
+++ b/x-pack/plugins/index_management/public/app.js
@@ -12,7 +12,8 @@ import { IndexList } from './sections/index_list';
export const App = () => (
-
+
+
);
diff --git a/x-pack/plugins/index_management/public/index.js b/x-pack/plugins/index_management/public/index.js
index ccde49edbdf5d..d52bf02b82f65 100644
--- a/x-pack/plugins/index_management/public/index.js
+++ b/x-pack/plugins/index_management/public/index.js
@@ -6,3 +6,4 @@
import './register_management_section';
import './register_routes';
+import './index_management_extensions';
diff --git a/x-pack/plugins/index_management/public/index_management_extensions.js b/x-pack/plugins/index_management/public/index_management_extensions.js
new file mode 100644
index 0000000000000..61a074e5d0766
--- /dev/null
+++ b/x-pack/plugins/index_management/public/index_management_extensions.js
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+const summaryExtensions = [];
+export const addSummaryExtension = (summaryExtension)=> {
+ summaryExtensions.push(summaryExtension);
+};
+export const getSummaryExtensions = () => {
+ return summaryExtensions;
+};
+const actionExtensions = [];
+export const addActionExtension = (actionExtension)=> {
+ actionExtensions.push(actionExtension);
+};
+export const getActionExtensions = () => {
+ return actionExtensions;
+};
+const bannerExtensions = [];
+export const addBannerExtension = (actionExtension)=> {
+ bannerExtensions.push(actionExtension);
+};
+export const getBannerExtensions = () => {
+ return bannerExtensions;
+};
+const filterExtensions = [];
+export const addFilterExtension = (filterExtension)=> {
+ filterExtensions.push(filterExtension);
+};
+export const getFilterExtensions = () => {
+ return filterExtensions;
+};
+
+
diff --git a/x-pack/plugins/index_management/public/register_management_section.js b/x-pack/plugins/index_management/public/register_management_section.js
index 78dec3515a899..08f0a6b1d486e 100644
--- a/x-pack/plugins/index_management/public/register_management_section.js
+++ b/x-pack/plugins/index_management/public/register_management_section.js
@@ -13,6 +13,6 @@ esSection.register('index_management', {
visible: true,
display: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }),
order: 1,
- url: `#${BASE_PATH}home`
+ url: `#${BASE_PATH}indices`
});
diff --git a/x-pack/plugins/index_management/public/register_routes.js b/x-pack/plugins/index_management/public/register_routes.js
index a9623231e57d9..ca50a351e0a17 100644
--- a/x-pack/plugins/index_management/public/register_routes.js
+++ b/x-pack/plugins/index_management/public/register_routes.js
@@ -11,6 +11,7 @@ import { HashRouter } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { setHttpClient } from './services/api';
+import { setUrlService } from './services/navigation';
import { App } from './app';
import { BASE_PATH } from '../common/constants/base_path';
@@ -35,7 +36,7 @@ const renderReact = async (elem) => {
);
};
-routes.when(`${BASE_PATH}:view?/:id?`, {
+routes.when(`${BASE_PATH}:view?/:action?/:id?`, {
template: template,
k7Breadcrumbs: () => [
MANAGEMENT_BREADCRUMB,
@@ -47,11 +48,16 @@ routes.when(`${BASE_PATH}:view?/:id?`, {
],
controllerAs: 'indexManagement',
controller: class IndexManagementController {
- constructor($scope, $route, $http) {
+ constructor($scope, $route, $http, kbnUrl, $rootScope) {
// NOTE: We depend upon Angular's $http service because it's decorated with interceptors,
// e.g. to check license status per request.
setHttpClient($http);
-
+ setUrlService({
+ change(url) {
+ kbnUrl.change(url);
+ $rootScope.$digest();
+ }
+ });
$scope.$$postDigest(() => {
const elem = document.getElementById('indexManagementReactRoot');
renderReact(elem);
diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/detail_panel/summary/summary.js b/x-pack/plugins/index_management/public/sections/index_list/components/detail_panel/summary/summary.js
index 4813a67b53485..5a2aa973e914c 100644
--- a/x-pack/plugins/index_management/public/sections/index_list/components/detail_panel/summary/summary.js
+++ b/x-pack/plugins/index_management/public/sections/index_list/components/detail_panel/summary/summary.js
@@ -4,50 +4,76 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { healthToColor } from '../../../../../services';
+import { getUrlService } from '../../../../../services/navigation';
+import { FormattedMessage } from '@kbn/i18n/react';
import {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiHealth,
EuiDescriptionList,
+ EuiHorizontalRule,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
+ EuiSpacer,
+ EuiTitle
} from '@elastic/eui';
-
-const HEADERS = {
- health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', {
- defaultMessage: 'Health',
- }),
- status: i18n.translate('xpack.idxMgmt.summary.headers.statusHeader', {
- defaultMessage: 'Status',
- }),
- primary: i18n.translate('xpack.idxMgmt.summary.headers.primaryHeader', {
- defaultMessage: 'Primaries',
- }),
- replica: i18n.translate('xpack.idxMgmt.summary.headers.replicaHeader', {
- defaultMessage: 'Replicas',
- }),
- documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', {
- defaultMessage: 'Docs Count',
- }),
- documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', {
- defaultMessage: 'Docs Deleted',
- }),
- size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', {
- defaultMessage: 'Storage Size',
- }),
- primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', {
- defaultMessage: 'Primary Storage Size',
- }),
- aliases: i18n.translate('xpack.idxMgmt.summary.headers.aliases', {
- defaultMessage: 'Aliases'
- })
+import { getSummaryExtensions } from '../../../../../index_management_extensions';
+const getHeaders = () =>{
+ return {
+ health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', {
+ defaultMessage: 'Health',
+ }),
+ status: i18n.translate('xpack.idxMgmt.summary.headers.statusHeader', {
+ defaultMessage: 'Status',
+ }),
+ primary: i18n.translate('xpack.idxMgmt.summary.headers.primaryHeader', {
+ defaultMessage: 'Primaries',
+ }),
+ replica: i18n.translate('xpack.idxMgmt.summary.headers.replicaHeader', {
+ defaultMessage: 'Replicas',
+ }),
+ documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', {
+ defaultMessage: 'Docs Count',
+ }),
+ documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', {
+ defaultMessage: 'Docs Deleted',
+ }),
+ size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', {
+ defaultMessage: 'Storage Size',
+ }),
+ primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', {
+ defaultMessage: 'Primary Storage Size',
+ }),
+ aliases: i18n.translate('xpack.idxMgmt.summary.headers.aliases', {
+ defaultMessage: 'Aliases'
+ })
+ };
};
export class Summary extends React.PureComponent {
+ getAdditionalContent() {
+ const { index } = this.props;
+ const extensions = getSummaryExtensions();
+ return extensions.map((summaryExtension, i) => {
+ return (
+
+
+ { summaryExtension(index, getUrlService()) }
+
+ );
+ });
+ }
buildRows() {
const { index } = this.props;
- return Object.keys(HEADERS).map(fieldName => {
+ const headers = getHeaders();
+ const rows = {
+ left: [],
+ right: []
+ };
+ Object.keys(headers).forEach((fieldName, arrayIndex) => {
const value = index[fieldName];
let content = value;
if(fieldName === 'health') {
@@ -56,22 +82,51 @@ export class Summary extends React.PureComponent {
if(Array.isArray(content)) {
content = content.join(', ');
}
- return [
+ const cell = [
- {HEADERS[fieldName]}:
+ {headers[fieldName]}
,
{content}
];
+ if (arrayIndex % 2 === 0) {
+ rows.left.push(cell);
+ } else {
+ rows.right.push(cell);
+ }
});
+ return rows;
}
render() {
+ const { left, right } = this.buildRows();
+ const additionalContent = this.getAdditionalContent();
return (
-
- {this.buildRows()}
-
+
+
+
+
+
+
+
+
+
+
+ {left}
+
+
+
+
+ {right}
+
+
+
+ { additionalContent }
+
);
}
}
diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.container.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.container.js
index bbf6a5d4a9cea..babc94deef458 100644
--- a/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.container.js
+++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.container.js
@@ -15,11 +15,14 @@ import {
openIndices,
editIndexSettings,
refreshIndices,
- openDetailPanel
+ openDetailPanel,
+ performExtensionAction,
+ reloadIndices
} from '../../../../store/actions';
import {
- getIndexStatusByIndexName
+ getIndexStatusByIndexName,
+ getIndicesByName
} from '../../../../store/selectors';
const mapStateToProps = (state, ownProps) => {
@@ -29,7 +32,8 @@ const mapStateToProps = (state, ownProps) => {
indexStatusByName[indexName] = getIndexStatusByIndexName(state, indexName);
});
return {
- indexStatusByName
+ indexStatusByName,
+ indices: getIndicesByName(state, indexNames)
};
};
@@ -73,6 +77,12 @@ const mapDispatchToProps = (dispatch, { indexNames }) => {
},
deleteIndices: () => {
dispatch(deleteIndices({ indexNames }));
+ },
+ reloadIndices: () => {
+ dispatch(reloadIndices(indexNames));
+ },
+ performExtensionAction: (requestMethod, successMessage) => {
+ dispatch(performExtensionAction({ requestMethod, successMessage, indexNames }));
}
};
};
diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js
index 7c5339b53f75f..55593040a18ee 100644
--- a/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js
+++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_actions_context_menu/index_actions_context_menu.js
@@ -22,6 +22,8 @@ import {
} from '@elastic/eui';
import { flattenPanelTree } from '../../../../lib/flatten_panel_tree';
import { INDEX_OPEN } from '../../../../../common/constants';
+import { getActionExtensions } from '../../../../index_management_extensions';
+import { getHttpClient } from '../../../../services/api';
class IndexActionsContextMenuUi extends Component {
constructor(props) {
@@ -29,9 +31,15 @@ class IndexActionsContextMenuUi extends Component {
this.state = {
isPopoverOpen: false,
- showDeleteConfirmation: false
+ renderConfirmModal: null,
};
}
+ closeConfirmModal = () => {
+ this.setState({
+ renderConfirmModal: null
+ });
+ this.props.resetSelection && this.props.resetSelection();
+ }
panels() {
const {
closeIndices,
@@ -46,7 +54,10 @@ class IndexActionsContextMenuUi extends Component {
detailPanel,
indexNames,
indexStatusByName,
- intl
+ performExtensionAction,
+ indices,
+ intl,
+ reloadIndices
} = this.props;
const allOpen = all(indexNames, indexName => {
return indexStatusByName[indexName] === INDEX_OPEN;
@@ -118,7 +129,7 @@ class IndexActionsContextMenuUi extends Component {
icon: ,
onClick: () => {
this.closePopover();
- this.openForcemergeSegmentsModal();
+ this.setState({ renderConfirmModal: this.renderForcemergeSegmentsModal });
}
});
items.push({
@@ -171,7 +182,33 @@ class IndexActionsContextMenuUi extends Component {
icon: ,
onClick: () => {
this.closePopover();
- this.openDeleteConfirmationModal();
+ this.setState({ renderConfirmModal: this.renderConfirmDeleteModal });
+ }
+ });
+ getActionExtensions().forEach((actionExtension) => {
+ const actionExtensionDefinition = actionExtension(indices, reloadIndices);
+ if (actionExtensionDefinition) {
+ const { buttonLabel, requestMethod, successMessage, icon, renderConfirmModal } = actionExtensionDefinition;
+ if (requestMethod) {
+ items.push({
+ name: buttonLabel,
+ icon: ,
+ onClick: () => {
+ this.closePopoverAndExecute(async () => {
+ await performExtensionAction(requestMethod, successMessage);
+ });
+ },
+ });
+ } else {
+ items.push({
+ name: buttonLabel,
+ icon: ,
+ onClick: () => {
+ this.closePopover();
+ this.setState({ renderConfirmModal });
+ }
+ });
+ }
}
});
items.forEach(item => {
@@ -197,7 +234,7 @@ class IndexActionsContextMenuUi extends Component {
closePopoverAndExecute = func => {
this.setState({
isPopoverOpen: false,
- showDeleteConfirmation: false
+ renderConfirmModal: false
});
func();
this.props.resetSelection && this.props.resetSelection();
@@ -209,22 +246,6 @@ class IndexActionsContextMenuUi extends Component {
});
};
- closeDeleteConfirmationModal = () => {
- this.setState({ showDeleteConfirmation: false });
- };
-
- openDeleteConfirmationModal = () => {
- this.setState({ showDeleteConfirmation: true });
- };
-
- openForcemergeSegmentsModal = () => {
- this.setState({ showForcemergeSegmentsModal: true });
- };
-
- closeForcemergeSegmentsModal = () => {
- this.setState({ showForcemergeSegmentsModal: false });
- };
-
forcemergeSegmentsError = () => {
const { forcemergeSegments } = this.state;
const { intl } = this.props;
@@ -237,7 +258,7 @@ class IndexActionsContextMenuUi extends Component {
});
}
};
- forcemergeSegmentsModal = () => {
+ renderForcemergeSegmentsModal = () => {
const { forcemergeIndices, indexNames, intl } = this.props;
const helpText = intl.formatMessage({
id: 'xpack.idxMgmt.indexActionsMenu.forceMerge.forceMergeSegmentsHelpText',
@@ -245,10 +266,6 @@ class IndexActionsContextMenuUi extends Component {
});
const oneIndexSelected = this.oneIndexSelected();
const entity = this.getEntity(oneIndexSelected);
- const { showForcemergeSegmentsModal } = this.state;
- if (!showForcemergeSegmentsModal) {
- return null;
- }
return (
{
if (!this.forcemergeSegmentsError()) {
this.closePopoverAndExecute(() => {
@@ -341,14 +358,10 @@ class IndexActionsContextMenuUi extends Component {
);
};
- confirmDeleteModal = () => {
+ renderConfirmDeleteModal = () => {
const oneIndexSelected = this.oneIndexSelected();
const entity = this.getEntity(oneIndexSelected);
const { deleteIndices, indexNames, intl } = this.props;
- const { showDeleteConfirmation } = this.state;
- if (!showDeleteConfirmation) {
- return null;
- }
return (
this.closePopoverAndExecute(deleteIndices)}
cancelButtonText={
intl.formatMessage({
@@ -417,7 +430,18 @@ class IndexActionsContextMenuUi extends Component {
return this.props.indexNames.length === 1;
};
getEntity = oneIndexSelected => {
- return oneIndexSelected ? 'index' : 'indices';
+ const { intl } = this.props;
+ return oneIndexSelected ? (
+ intl.formatMessage({
+ id: 'xpack.idxMgmt.indexActionsMenu.indexMessage',
+ defaultMessage: 'index'
+ })
+ ) : (
+ intl.formatMessage({
+ id: 'xpack.idxMgmt.indexActionsMenu.indicesMessage',
+ defaultMessage: 'indices'
+ })
+ );
};
render() {
const { intl } = this.props;
@@ -449,8 +473,7 @@ class IndexActionsContextMenuUi extends Component {
return (
- {this.confirmDeleteModal()}
- {this.forcemergeSegmentsModal()}
+ {this.state.renderConfirmModal ? this.state.renderConfirmModal(this.closeConfirmModal, getHttpClient()) : null}
);
diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.container.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.container.js
index 274935995dd69..91387f5aba5fa 100644
--- a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.container.js
+++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.container.js
@@ -13,7 +13,8 @@ import {
isDetailPanelOpen,
showSystemIndices,
getSortField,
- isSortAscending
+ isSortAscending,
+ getIndicesAsArray,
} from '../../../../store/selectors';
import {
filterChanged,
@@ -29,6 +30,7 @@ import { IndexTable as PresentationComponent } from './index_table';
const mapStateToProps = (state) => {
return {
+ allIndices: getIndicesAsArray(state),
isDetailPanelOpen: isDetailPanelOpen(state),
detailPanelIndexName: getDetailPanelIndexName(state),
indices: getPageOfIndices(state),
diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js
index df501ef98ff32..ef7649133aa21 100644
--- a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js
+++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Component } from 'react';
-import { i18n } from '@kbn/i18n';
+import React, { Component, Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { Route } from 'react-router-dom';
import { NoMatch } from '../../../no_match';
@@ -14,14 +14,15 @@ import { healthToColor } from '../../../../services';
import '../../../../styles/table.less';
import {
+ EuiCallOut,
EuiHealth,
EuiLink,
EuiCheckbox,
- EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiSpacer,
+ EuiSearchBar,
EuiSwitch,
EuiTable,
EuiTableBody,
@@ -35,10 +36,11 @@ import {
EuiTitle,
EuiText,
EuiPageBody,
- EuiPageContent
+ EuiPageContent,
} from '@elastic/eui';
import { IndexActionsContextMenu } from '../../components';
+import { getBannerExtensions, getFilterExtensions } from '../../../../index_management_extensions';
const HEADERS = {
name: i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', {
@@ -62,9 +64,6 @@ const HEADERS = {
size: i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', {
defaultMessage: 'Storage size',
}),
- primary_size: i18n.translate('xpack.idxMgmt.indexTable.headers.primaryStorageSizeHeader', {
- defaultMessage: 'Primary storage size',
- })
};
export class IndexTableUi extends Component {
@@ -90,17 +89,62 @@ export class IndexTableUi extends Component {
super(props);
this.state = {
- selectedIndicesMap: {}
+ selectedIndicesMap: {},
};
}
-
+ componentDidMount() {
+ const {
+ filterChanged,
+ filterFromURI
+ } = this.props;
+ if (filterFromURI) {
+ const decodedFilter = decodeURIComponent(filterFromURI);
+ filterChanged(EuiSearchBar.Query.parse(decodedFilter));
+ }
+ }
onSort = column => {
const { sortField, isSortAscending, sortChanged } = this.props;
const newIsSortAscending = sortField === column ? !isSortAscending : true;
sortChanged(column, newIsSortAscending);
};
-
+ renderFilterError() {
+ const { intl } = this.props;
+ const { filterError } = this.state;
+ if (!filterError) {
+ return;
+ }
+ return (
+
+
+
+
+ );
+ }
+ onFilterChanged = ({ query, error }) => {
+ if (error) {
+ this.setState({ filterError: error });
+ } else {
+ this.props.filterChanged(query);
+ this.setState({ filterError: null });
+ }
+ }
+ getFilters = () => {
+ const { allIndices } = this.props;
+ return getFilterExtensions().reduce((accum, filterExtension) => {
+ const filtersToAdd = filterExtension(allIndices);
+ return [...accum, ...filtersToAdd];
+ }, []);
+ }
toggleAll = () => {
const allSelected = this.areAllItemsSelected();
if (allSelected) {
@@ -112,7 +156,7 @@ export class IndexTableUi extends Component {
selectedIndicesMap[name] = true;
});
this.setState({
- selectedIndicesMap
+ selectedIndicesMap,
});
};
@@ -125,7 +169,7 @@ export class IndexTableUi extends Component {
newMap[name] = true;
}
return {
- selectedIndicesMap: newMap
+ selectedIndicesMap: newMap,
};
});
};
@@ -136,9 +180,7 @@ export class IndexTableUi extends Component {
areAllItemsSelected = () => {
const { indices } = this.props;
- const indexOfUnselectedItem = indices.findIndex(
- index => !this.isItemSelected(index.name)
- );
+ const indexOfUnselectedItem = indices.findIndex(index => !this.isItemSelected(index.name));
return indexOfUnselectedItem === -1;
};
@@ -196,16 +238,50 @@ export class IndexTableUi extends Component {
);
});
}
+ renderBanners() {
+ const { allIndices = [], filterChanged } = this.props;
+ return getBannerExtensions().map((bannerExtension, i) => {
+ const bannerData = bannerExtension(allIndices);
+ if (!bannerData) {
+ return null;
+ }
+ const {
+ type,
+ title,
+ message,
+ filter,
+ filterLabel,
+ } = bannerData;
+
+ return (
+
+
+
+ {message}
+ {filter ? (
+ filterChanged(filter)}>
+ {filterLabel}
+
+ ) : null}
+
+
+
+
+ );
+ });
+ }
buildRows() {
const { indices = [], detailPanelIndexName } = this.props;
return indices.map(index => {
const { name } = index;
return (
@@ -245,7 +321,6 @@ export class IndexTableUi extends Component {
render() {
const {
- filterChanged,
filter,
showSystemIndices,
showSystemIndicesChanged,
@@ -254,7 +329,6 @@ export class IndexTableUi extends Component {
} = this.props;
const { selectedIndicesMap } = this.state;
const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0;
-
return (
@@ -274,7 +348,7 @@ export class IndexTableUi extends Component {
@@ -284,14 +358,17 @@ export class IndexTableUi extends Component {
id="checkboxShowSystemIndices"
checked={showSystemIndices}
onChange={event => showSystemIndicesChanged(event.target.checked)}
- label={}
+ label={
+
+ }
/>
+ {this.renderBanners()}
{atLeastOneItemSelected ? (
@@ -309,29 +386,28 @@ export class IndexTableUi extends Component {
) : null}
- {
- filterChanged(event.target.value);
- }}
- data-test-subj="indexTableFilterInput"
- placeholder={
- intl.formatMessage({
+
-
+ {this.renderFilterError()}
{indices.length > 0 ? (
diff --git a/x-pack/plugins/index_management/public/services/api.js b/x-pack/plugins/index_management/public/services/api.js
index fdbf800ce180c..ec74e309847a0 100644
--- a/x-pack/plugins/index_management/public/services/api.js
+++ b/x-pack/plugins/index_management/public/services/api.js
@@ -9,6 +9,9 @@ let httpClient;
export const setHttpClient = (client) => {
httpClient = client;
};
+export const getHttpClient = () => {
+ return httpClient;
+};
const apiPrefix = chrome.addBasePath('/api/index_management');
export async function loadIndices() {
diff --git a/x-pack/plugins/index_management/public/services/filter_items.js b/x-pack/plugins/index_management/public/services/filter_items.js
index 6d2e3dae57f46..f523cb6105916 100644
--- a/x-pack/plugins/index_management/public/services/filter_items.js
+++ b/x-pack/plugins/index_management/public/services/filter_items.js
@@ -3,13 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { get } from 'lodash';
export const filterItems = (fields, filter = '', items = []) => {
- const lowerFilter = filter.toLowerCase();
+ const lowerFilter = filter.trim().toLowerCase();
return items.filter(item => {
const actualFields = fields || Object.keys(item);
const indexOfMatch = actualFields.findIndex(field => {
- const normalizedField = String(item[field]).toLowerCase();
+ const normalizedField = String(get(item, field)).toLowerCase();
return normalizedField.includes(lowerFilter);
});
return indexOfMatch !== -1;
diff --git a/x-pack/plugins/index_management/public/services/navigation.js b/x-pack/plugins/index_management/public/services/navigation.js
new file mode 100644
index 0000000000000..1394ecf045c7c
--- /dev/null
+++ b/x-pack/plugins/index_management/public/services/navigation.js
@@ -0,0 +1,16 @@
+/*
+ * 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 { BASE_PATH } from '../../common/constants';
+let urlService;
+export const setUrlService = (aUrlService) => {
+ urlService = aUrlService;
+};
+export const getUrlService = () => {
+ return urlService;
+};
+export const getFilteredIndicesUri = (filter) => {
+ return encodeURI(`#${BASE_PATH}indices/filter/${encodeURIComponent(filter)}`);
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_management/public/store/actions/extension_action.js b/x-pack/plugins/index_management/public/store/actions/extension_action.js
new file mode 100644
index 0000000000000..98ed6170f1b14
--- /dev/null
+++ b/x-pack/plugins/index_management/public/store/actions/extension_action.js
@@ -0,0 +1,20 @@
+/*
+ * 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 { reloadIndices } from '../actions';
+import { toastNotifications } from 'ui/notify';
+import { getHttpClient } from '../../services/api';
+
+export const performExtensionAction = ({ requestMethod, indexNames, successMessage }) => async (dispatch) => {
+ try {
+ await requestMethod(indexNames, getHttpClient());
+ } catch (error) {
+ toastNotifications.addDanger(error.data.message);
+ return;
+ }
+ dispatch(reloadIndices(indexNames));
+ toastNotifications.addSuccess(successMessage);
+};
diff --git a/x-pack/plugins/index_management/public/store/actions/index.js b/x-pack/plugins/index_management/public/store/actions/index.js
index 86c6280e12bbf..a50854015adea 100644
--- a/x-pack/plugins/index_management/public/store/actions/index.js
+++ b/x-pack/plugins/index_management/public/store/actions/index.js
@@ -19,4 +19,5 @@ export * from './table_state';
export * from './edit_index_settings';
export * from './update_index_settings';
export * from './detail_panel';
+export * from './extension_action';
diff --git a/x-pack/plugins/index_management/public/store/selectors/index.js b/x-pack/plugins/index_management/public/store/selectors/index.js
index 19a02908992e2..08e4cd9872157 100644
--- a/x-pack/plugins/index_management/public/store/selectors/index.js
+++ b/x-pack/plugins/index_management/public/store/selectors/index.js
@@ -3,12 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-import { Pager } from '@elastic/eui';
+import { Pager, EuiSearchBar } from '@elastic/eui';
import { createSelector } from 'reselect';
import { indexStatusLabels } from '../../lib/index_status_labels';
-import { filterItems, sortTable } from '../../services';
+import { sortTable } from '../../services';
export const getDetailPanelData = (state) => state.detailPanel.data;
export const getDetailPanelError = (state) => state.detailPanel.error;
@@ -16,6 +15,11 @@ export const getDetailPanelType = (state) => state.detailPanel.panelType;
export const isDetailPanelOpen = (state) => !!getDetailPanelType(state);
export const getDetailPanelIndexName = (state) => state.detailPanel.indexName;
export const getIndices = (state) => state.indices.byId;
+export const getIndicesAsArray = (state) => Object.values(state.indices.byId);
+export const getIndicesByName = (state, indexNames) => {
+ const indices = getIndices(state);
+ return indexNames.map((indexName) => indices[indexName]);
+};
export const getIndexByIndexName = (state, name) => getIndices(state)[name];
export const getFilteredIds = (state) => state.indices.filteredIds;
export const getRowStatuses = (state) => state.rowStatus;
@@ -27,6 +31,7 @@ export const getIndexStatusByIndexName = (state, indexName) => {
const { status } = indices[indexName] || {};
return status;
};
+const defaultFilterFields = ['name', 'uuid'];
const getFilteredIndices = createSelector(
getIndices,
getRowStatuses,
@@ -36,7 +41,8 @@ const getFilteredIndices = createSelector(
const systemFilteredIndexes = tableState.showSystemIndices
? indexArray
: indexArray.filter(index => !(index.name + '').startsWith('.'));
- return filterItems(['name', 'uuid'], tableState.filter, systemFilteredIndexes);
+ const filter = tableState.filter || EuiSearchBar.Query.MATCH_ALL;
+ return EuiSearchBar.Query.execute(filter, systemFilteredIndexes, defaultFilterFields);
}
);
export const getTotalItems = createSelector(
diff --git a/x-pack/plugins/index_management/public/styles/table.less b/x-pack/plugins/index_management/public/styles/table.less
index 6ac08e744951d..10957c9458f3c 100644
--- a/x-pack/plugins/index_management/public/styles/table.less
+++ b/x-pack/plugins/index_management/public/styles/table.less
@@ -12,7 +12,7 @@
.indTable__horizontalScrollContainer {
overflow-x: auto;
max-width: 100%;
- height: 100vh;
+ min-height: 100vh;
}
.indTable__horizontalScroll {
min-width: 800px;
diff --git a/x-pack/plugins/index_management/server/lib/enrich_response.js b/x-pack/plugins/index_management/server/lib/enrich_response.js
new file mode 100644
index 0000000000000..1f502717b9d9a
--- /dev/null
+++ b/x-pack/plugins/index_management/server/lib/enrich_response.js
@@ -0,0 +1,15 @@
+/*
+ * 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 { getIndexManagementDataEnrichers } from '../../index_management_data';
+export const enrichResponse = async (response, callWithRequest) => {
+ const dataEnrichers = getIndexManagementDataEnrichers();
+ for (let i = 0; i < dataEnrichers.length; i++) {
+ const dataEnricher = dataEnrichers[i];
+ response = await dataEnricher(response, callWithRequest);
+ }
+ return response;
+};
\ No newline at end of file
diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.js b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.js
index fefb5fc91a843..7d580fd92e268 100644
--- a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.js
+++ b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.js
@@ -7,8 +7,9 @@
import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
-import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
+import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
import { fetchAliases } from './fetch_aliases';
+import { enrichResponse } from '../../../lib/enrich_response';
function formatHits(hits, aliases) {
return hits.map(hit => {
@@ -49,7 +50,8 @@ export function registerListRoute(server) {
try {
const aliases = await fetchAliases(callWithRequest);
const hits = await fetchIndices(callWithRequest);
- const response = formatHits(hits, aliases);
+ let response = formatHits(hits, aliases);
+ response = await enrichResponse(response, callWithRequest);
return response;
} catch (err) {
if (isEsError(err)) {
diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.js b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.js
index bf2fe6c558867..3b39b2c56ae04 100644
--- a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.js
+++ b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.js
@@ -8,6 +8,7 @@ import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
+import { enrichResponse } from '../../../lib/enrich_response';
import { fetchAliases } from './fetch_aliases';
function getIndexNamesFromPayload(payload) {
@@ -55,7 +56,8 @@ export function registerReloadRoute(server) {
try {
const indices = await fetchIndices(callWithRequest, indexNames);
const aliases = await fetchAliases(callWithRequest);
- const response = formatHits(indices, aliases);
+ let response = formatHits(indices, aliases);
+ response = await enrichResponse(response, callWithRequest);
return response;
} catch (err) {
if (isEsError(err)) {