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/README.md b/x-pack/plugins/index_lifecycle_management/README.md
new file mode 100644
index 0000000000000..a25136adbf8de
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/README.md
@@ -0,0 +1,72 @@
+# Index lifecyle management
+
+## What is it
+-- TODO --
+
+## UI
+
+The UI currently consists of a single wizard, broken into three steps.
+
+### Step 1
+The first step involves choosing the index template in which the created/selected policy will be applied.
+Then, it lets the user tweak configuration options on this template including shard and replica count as well as allocation rules.
+
+### Step 2
+The second step lets the user choose which policy they want to apply to the selected index template. They can choose a new one or select an existing one. Either way, after selection, they will see configuration options for the policy itself. This includes configuration for the hot, warm, cold, and delete phase.
+
+### Step 3
+The third and last step lets the user name their policy and also see the affected indices and index templates. These indices and index templates are what will be affected once the user saves the work done in the wizard (This includes changes to the index template itself which will change indices created from the template and also changes to a policy that is attached to another index template). The user can also see a visual diff of what will change in the index template. Then, the user clicks the Save button and blamo!
+
+## UI Architecture
+
+The UI is built on React and Redux.
+
+### Redux
+
+The redux store consists of a few top level attributes:
+```
+indexTemplate
+nodes
+policies
+general
+```
+
+The logic behind the store is separate into four main concerns:
+1) reducers/
+2) actions/
+3) selectors/
+4) middleware/
+
+The reducers and actions are pretty standard redux, so no need to discuss much there.
+
+### Selectors
+
+The selectors showcase how we access any stateful data. All access comes through selectors so if there are any changes required to the state tree, we only need to update the reducers and selectors.
+
+#### Middleware
+
+The middleware folder contains specific pieces of state logic we need to handle side effects of certain state changing.
+
+One example is the `auto_enable_phase.js` middleware. By default, the warm, cold and delete phases are disabled. However, the user can expand the section in the UI and edit configuration without needing to enable/disable the phase. Ideally, once the user edits any configuration piece within a phase, we _assume_ they want that phase enabled so this middleware will detect a change in a phase and enable if it is not enabled already.
+
+#### Generic phase data
+
+Each of our four phases have some similar and some unique configuration options. Instead of making each individual phase a specific action for that phase, the code is written more generically to capture any data change within a phase to a single action. Therefore, each phase component's configuration inputs will look similar, like: `setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE_UNITS, e.target.value)`. The top level container for each phase will handle automatically prefixing the `setPhaseData` prop with the right phase: ` setPhaseData: (key, value) => setPhaseData(PHASE_COLD, key, value),`.
+
+To complement this generic logic, there is a list of constants that are used to ensure the right pieces of data are changed. These are contained within `store/constants.js`
+
+### Diff View
+
+The third step of the wizard features a visual diff UI component which is custom to this feature. It is based off Ace/Brace and the custom code used to power is it lives in `lib/diff_ace_addons.js` and `lib/diff_tools.js`. The UI parts are in `sections/wizard/components/review/diff_view.js`. See those individual files for more detailed comments/explanations.
+
+### Validation
+
+Every step in the wizard features validation and will show error states after the user attempts to move to the next step assuming there are errors on the current page.
+
+This works by constantly revalidating the entire wizard state after each state change. This is technically optional as the method to trigger validation is manually called in each UI component that triggers a state change.
+
+It's important to note that the validation logic does not apply to a single step in the wizard, but will always validate the entire state tree. This helps prevent scenarios where a change in a step might invalidate a change in another step and we lose that validation state.
+
+Once a step change is initiated (like clicking Next Step), the current step is marked as able to see errors and will reject the change if there are errors. It will show a toast to the user that there are errors and make each error visible on the relevant UI control.
+
+As a way to consolidate showing these errors, there is a custom UI component called `ErrableFormRow` that wraps a `EuiFormRow` and it's child with the appropriate error states when appropriate.
diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/base_path.js b/x-pack/plugins/index_lifecycle_management/common/constants/base_path.js
new file mode 100644
index 0000000000000..5eea1d0ead4a4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/common/constants/base_path.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 const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/';
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..59b61f7b99f98
--- /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 { PLUGIN } from './plugin';
+export { BASE_PATH } from './base_path';
diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/plugin.js b/x-pack/plugins/index_lifecycle_management/common/constants/plugin.js
new file mode 100644
index 0000000000000..0261f57a93e8c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/common/constants/plugin.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 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..ce483d9218864
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/index.js
@@ -0,0 +1,45 @@
+/*
+ * 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 } from './common/constants';
+import { indexLifecycleDataEnricher } from './index_lifecycle_data';
+export function indexLifecycleManagement(kibana) {
+ return new kibana.Plugin({
+ id: PLUGIN.ID,
+ publicDir: resolve(__dirname, 'public'),
+ require: ['kibana', 'elasticsearch', 'xpack_main', 'index_management'],
+ uiExports: {
+ managementSections: ['plugins/index_lifecycle_management'],
+ injectDefaultVars(server) {
+ const config = server.config();
+ return {
+ indexLifecycleManagementUiEnabled: config.get(`${PLUGIN.ID}.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/api/index.js b/x-pack/plugins/index_lifecycle_management/public/api/index.js
new file mode 100644
index 0000000000000..8db47a8e6380c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/api/index.js
@@ -0,0 +1,66 @@
+/*
+ * 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/${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;
+};
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..d973dbb56cb5d
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/app.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 React from 'react';
+import { HashRouter, Switch, Route } 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/components/active_badge.js b/x-pack/plugins/index_lifecycle_management/public/components/active_badge.js
new file mode 100644
index 0000000000000..4a270c9e002f0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/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/components/index.js b/x-pack/plugins/index_lifecycle_management/public/components/index.js
new file mode 100644
index 0000000000000..9e77b35f5df41
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/components/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 { ActiveBadge } from './active_badge';
+export { LearnMoreLink } from './learn_more_link';
+export { PhaseErrorMessage } from './phase_error_message';
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/components/index_lifecycle_summary.js
new file mode 100644
index 0000000000000..77f87be1da824
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/components/index_lifecycle_summary.js
@@ -0,0 +1,109 @@
+/*
+ * 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 {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+ EuiSpacer,
+ EuiTitle
+} from '@elastic/eui';
+import { i18n, FormattedMessage } from '@kbn/i18n';
+const HEADERS = {
+ policy: i18n.translate('xpack.indexLifecycleMgmt.summary.headers.lifecyclePolicyHeader', {
+ defaultMessage: 'Lifecycle policy',
+ }),
+ phase: i18n.translate('xpack.indexLifecycleMgmt.summary.headers.currentPhaseHeader', {
+ defaultMessage: 'Current phase',
+ }),
+ action: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.nextActionHeader', {
+ defaultMessage: 'Next action',
+ }),
+ action_time: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.nextActionTimeHeader', {
+ defaultMessage: 'Next action time',
+ }),
+ failed_step: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader', {
+ defaultMessage: 'Failed step',
+ }),
+ step_info: i18n.translate('xpack.idxMgmt.indexLifecycleMgmtSummary.headers.errorInfoHeader', {
+ defaultMessage: 'Error info',
+ }),
+};
+export class IndexLifecycleSummary extends Component {
+
+ buildRows() {
+ const { index: { ilm = {} } } = this.props;
+ const rows = {
+ left: [],
+ right: []
+ };
+ Object.keys(HEADERS).forEach((fieldName, arrayIndex) => {
+ const value = ilm[fieldName];
+ let content;
+ if (fieldName === 'step_info') {
+ if (value) {
+ content = `${value.type}: ${value.reason}`;
+ }
+ } else if (fieldName === 'action_time') {
+ content = moment(value).format('YYYY-MM-DD HH:mm:ss');
+ } else {
+ content = value;
+ }
+ content = content || '-';
+ const cell = [
+
+ {HEADERS[fieldName]}:
+ ,
+
+ {content}
+
+ ];
+ if (arrayIndex % 2 === 0) {
+ rows.left.push(cell);
+ } else {
+ rows.right.push(cell);
+ }
+ });
+ return rows;
+ }
+
+ render() {
+ const { index: { ilm = {} } } = this.props;
+ if (!ilm.managed) {
+ return null;
+ }
+ const { left, right } = this.buildRows();
+ return (
+
+
+
+
+
+
+
+
+
+
+ {left}
+
+
+
+
+ {right}
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/components/learn_more_link.js b/x-pack/plugins/index_lifecycle_management/public/components/learn_more_link.js
new file mode 100644
index 0000000000000..25ebeac229729
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/components/learn_more_link.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { 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 } = this.props;
+ let url;
+ if (docPath) {
+ url = `${esBase}${docPath}`;
+ } else {
+ url = href;
+ }
+ return (
+
+
+
+ );
+
+ }
+}
+export const LearnMoreLink = injectI18n(LearnMoreLinkUi);
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/components/phase_error_message.js b/x-pack/plugins/index_lifecycle_management/public/components/phase_error_message.js
new file mode 100644
index 0000000000000..3466f09168b0a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/components/phase_error_message.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 React from 'react';
+import { EuiText, EuiTextColor } 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/index.js b/x-pack/plugins/index_lifecycle_management/public/index.js
new file mode 100644
index 0000000000000..3747d4e89a7b1
--- /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 './register_index_management_extensions';
diff --git a/x-pack/plugins/index_lifecycle_management/public/less/main.less b/x-pack/plugins/index_lifecycle_management/public/less/main.less
new file mode 100644
index 0000000000000..392512d93d0c3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/less/main.less
@@ -0,0 +1,49 @@
+@import (reference) "~ui/styles/variables";
+
+#indexLifecycleManagementReactRoot {
+ background: @globalColorLightestGray;
+ min-height: 100vh;
+}
+
+.euiPageContent.ilmContent {
+ max-width: 1000px;
+ width: 100%;
+}
+
+.ilmHrule {
+ // Less has a bug with calcs
+ width: calc(~"100% + 48px") !important;
+ margin-left: -24px;
+ margin-right: -24px;
+}
+
+.ilmAlias {
+ display: inline-block;
+ background-color: #333;
+ color: white;
+ padding: 4px 8px;
+}
+
+.ilmDiff__nav {
+ padding: 16px;
+ background: #f5f5f5;
+}
+
+.ilmDiff__code {
+
+}
+
+.euiAnimateContentLoad {
+ animation: euiAnimContentLoad $euiAnimSpeedExtraSlow $euiAnimSlightResistance;
+}
+
+@keyframes euiAnimContentLoad {
+ 0% {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/lib/diff_ace_addons.js b/x-pack/plugins/index_lifecycle_management/public/lib/diff_ace_addons.js
new file mode 100644
index 0000000000000..82c6e6b4d1e39
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/lib/diff_ace_addons.js
@@ -0,0 +1,134 @@
+/*
+ * 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 ace from 'brace';
+import { ADDITION_PREFIX, REMOVAL_PREFIX } from './diff_tools';
+
+function findInObject(key, obj, debug) {
+ const objKeys = Object.keys(obj);
+ for (const objKey of objKeys) {
+ if (objKey === key) {
+ return obj[objKey];
+ }
+ if (typeof obj[objKey] === 'object' && !Array.isArray(obj[objKey])) {
+ const item = findInObject(key, obj[objKey], debug);
+ if (item !== false) {
+ return item;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Utilty method that will determine if the current key/value pair
+ * is an addition or removal and return the appropriate ace classes
+ * for styling. This is called after finding a valid key/value match
+ * in our custom JSON diff mode for ace.
+ *
+ * @param {string} key
+ * @param {string} val
+ * @param {object} jsonObject
+ */
+function getDiffClasses(key, val, jsonObject) {
+ let value = val;
+ if (value.endsWith(',')) {
+ value = value.slice(0, -1);
+ }
+ if (value.startsWith('"')) {
+ value = value.slice(1, -1);
+ }
+
+ const additionValue = findInObject(`${ADDITION_PREFIX}${key}`, jsonObject);
+ const removalValue = findInObject(`${REMOVAL_PREFIX}${key}`, jsonObject);
+
+ const isAddition = Array.isArray(additionValue)
+ ? !!additionValue.find(v => v === value)
+ : (additionValue === value || (additionValue && value === '{'));
+ const isRemoval = Array.isArray(removalValue)
+ ? !!removalValue.find(v => v === value)
+ : (removalValue === value || (removalValue && value === '{'));
+
+ let diffClasses = '';
+ if (isAddition) {
+ diffClasses = 'diff_addition ace_variable';
+ } else if (isRemoval) {
+ diffClasses = 'diff_removal ace_variable';
+ } else {
+ diffClasses = 'variable';
+ }
+
+ return diffClasses;
+}
+
+let currentJsonObject;
+const getCurrentJsonObject = () => currentJsonObject;
+export const setCurrentJsonObject = jsonObject => currentJsonObject = jsonObject;
+
+/**
+ * This function will update the ace editor to support a `DiffJsonMode` that will
+ * show a merged object (merged through `diff_tools:mergeAndPreserveDuplicateKeys`)
+ * and highlight additions and removals. The goal of this from a UI perspective is
+ * to help the user see a visual result of merging two javascript objects.
+ *
+ * Read this as a starter: https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode
+ */
+export const addDiffAddonsForAce = () => {
+ const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules')
+ .JsonHighlightRules;
+ class DiffJsonHighlightRules extends JsonHighlightRules {
+ constructor(props) {
+ super(props);
+ this.$rules = new JsonHighlightRules().getRules();
+
+ let currentArrayKey;
+ this.addRules({
+ start: [
+ {
+ token: (key, val) => {
+ return getDiffClasses(key, val, getCurrentJsonObject());
+ },
+ // This is designed to match a key:value pair represented in JSON
+ // like:
+ // "foo": "bar"
+ // Be aware when tweaking this that there are idiosyncracies with
+ // how these work internally in ace.
+ regex: '(?:"([\\w-+]+)"\\s*:\\s*([^\\n\\[]+)$)',
+ },
+ {
+ token: key => {
+ currentArrayKey = key;
+ return 'variable';
+ },
+ next: 'array',
+ regex: '(?:"([\\w-+]+)"\\s*:\\s*\\[$)',
+ },
+ ...this.$rules.start,
+ ],
+ array: [
+ {
+ token: val => {
+ return getDiffClasses(currentArrayKey, val, getCurrentJsonObject());
+ },
+ next: 'start',
+ regex: '\\s*"([^"]+)"\\s*',
+ },
+ ],
+ });
+ }
+ }
+
+ const JsonMode = ace.acequire('ace/mode/json').Mode;
+ class DiffJsonMode extends JsonMode {
+ constructor(props) {
+ super(props);
+ this.HighlightRules = DiffJsonHighlightRules;
+ }
+ }
+
+ ace.define('ace/mode/diff_json', [], () => ({
+ Mode: DiffJsonMode,
+ }));
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/lib/diff_tools.js b/x-pack/plugins/index_lifecycle_management/public/lib/diff_tools.js
new file mode 100644
index 0000000000000..c06835261c759
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/lib/diff_tools.js
@@ -0,0 +1,192 @@
+/*
+ * 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 ADDITION_PREFIX = '$$$';
+export const REMOVAL_PREFIX = '^^^';
+
+/**
+ * Utility method that will properly escape the prefixes to use in a valid
+ * RegExp
+ *
+ * @param {string} prefix
+ */
+const escapePrefix = prefix =>
+ prefix
+ .split('')
+ .map(i => `\\${i}`)
+ .join('');
+const removePrefixRegex = new RegExp(
+ `(${escapePrefix(ADDITION_PREFIX)})|(${escapePrefix(REMOVAL_PREFIX)})`,
+ 'g'
+);
+
+export const isBoolean = value =>
+ JSON.parse(value) === true || JSON.parse(value) === false;
+const isObject = value => typeof value === 'object' && !Array.isArray(value);
+
+/**
+ * Utility method that will determine if the key/value pair provided is different
+ * than the value found using the key in the provided obj.
+ *
+ * @param {object} obj
+ * @param {string} key
+ * @param {object} value
+ */
+const isDifferent = (obj, key, value) => {
+ // If the object does not contain the key, then ignore since it's not a removal or addition
+ if (!obj.hasOwnProperty(key)) {
+ return false;
+ }
+
+ // If we're dealing with an array, we need something better than a simple === comparison
+ if (Array.isArray(value)) {
+ return JSON.stringify(value) !== JSON.stringify(obj[key]);
+ }
+
+ // If the value is an object, do not try and compare as this is called in a recursive function
+ // so safely ignore
+ if (typeof value === 'object') {
+ return false;
+ }
+
+ // We should be dealing with primitives so do a basic comparison
+ return obj[key] !== value;
+};
+
+/**
+ * This utility method is called when an object exists in the target object
+ * but not in the source and we want to mark each part of the object as an
+ * addition
+ *
+ * @param {*} obj
+ */
+const getAdditions = obj => {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ if (isObject(value)) {
+ result[`${ADDITION_PREFIX}${key}`] = getAdditions(value);
+ } else {
+ result[`${ADDITION_PREFIX}${key}`] = value;
+ }
+ }
+ return result;
+};
+
+/**
+ * This method is designed to remove all prefixes from the object previously added
+ * by `mergeAndPreserveDuplicateKeys`
+ *
+ * @param {object} obj
+ */
+export const removePrefixes = obj => {
+ if (typeof obj === 'string') {
+ return obj.replace(removePrefixRegex, '');
+ }
+
+ if (!obj || typeof obj !== 'object') {
+ return obj;
+ }
+
+ return Object.keys(obj).reduce(
+ (newObj, key) => ({
+ ...newObj,
+ [key.replace(removePrefixRegex, '')]: obj[key] && typeof obj[key] === 'object' ?
+ removePrefixes(obj[key]) :
+ obj[key],
+ }), {}
+ );
+};
+
+/**
+ * This function is designed to recursively remove any prefixes added through the
+ * `mergeAndPreserveDuplicateKeys` process.
+ *
+ * @param {string} key
+ * @param {object} value
+ */
+const normalizeChange = (key, value) => {
+ if (typeof value === 'string') {
+ return {
+ key: removePrefixes(key),
+ value: removePrefixes(value)
+ };
+ }
+ return Object.entries(value).reduce((accum, [key, value]) => {
+ if (typeof value === 'string') {
+ return {
+ key: removePrefixes(key),
+ value: removePrefixes(value)
+ };
+ }
+ return normalizeChange(key, value);
+ }, {});
+};
+
+/**
+ * This function is designed to merge two objects together, but instead of
+ * overriding key collisions, it will create two keys for each collision - the key
+ * from the source object will start with the `REMOVAL_PREFIX` and the key from the
+ * target object will start with the `ADDITION_PREFIX`. The resulting object from
+ * this function call will contain the merged object and potentially some
+ * `REMOVAL_PREFIX` and `ADDITION_PREFIX` keys.
+ *
+ * @param {object} source
+ * @param {object} target
+ */
+export const mergeAndPreserveDuplicateKeys = (
+ source,
+ target,
+ result = {},
+ changes = []
+) => {
+ for (const [key, value] of Object.entries(source)) {
+ if (isDifferent(target, key, value)) {
+ result[`${REMOVAL_PREFIX}${key}`] = value;
+ result[`${ADDITION_PREFIX}${key}`] = target[key];
+ changes.push({
+ key,
+ original: removePrefixes(value),
+ updated: removePrefixes(target[key]),
+ });
+ } else if (isObject(value)) {
+ if (target.hasOwnProperty(key)) {
+ const recurseResult = mergeAndPreserveDuplicateKeys(value, target[key]);
+ result[key] = recurseResult.result;
+ changes.push(...recurseResult.changes);
+ } else {
+ result[key] = value;
+ }
+ } else {
+ result[key] = value;
+ }
+ }
+
+ for (const [key, value] of Object.entries(target)) {
+ if (
+ result.hasOwnProperty(key) ||
+ result.hasOwnProperty(`${REMOVAL_PREFIX}${key}`) ||
+ result.hasOwnProperty(`${ADDITION_PREFIX}${key}`)
+ ) {
+ continue;
+ }
+
+ if (isObject(value)) {
+ result[`${ADDITION_PREFIX}${key}`] = getAdditions(value);
+ } else {
+ result[`${ADDITION_PREFIX}${key}`] = value;
+ }
+
+ const normalized = normalizeChange(key, result[`${ADDITION_PREFIX}${key}`]);
+ changes.push({
+ key: normalized.key,
+ updated: normalized.value,
+ });
+ }
+ return {
+ result,
+ changes
+ };
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/lib/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/lib/find_errors.js
new file mode 100644
index 0000000000000..7b77cf7a1301c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/lib/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/lib/manage_angular_lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/lib/manage_angular_lifecycle.js
new file mode 100644
index 0000000000000..3813e632a0a73
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/lib/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/main.html b/x-pack/plugins/index_lifecycle_management/public/main.html
new file mode 100644
index 0000000000000..ca86144cbe934
--- /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_index_management_extensions/index.js b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/index.js
new file mode 100644
index 0000000000000..706894c14eacc
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/index.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.
+ */
+
+import chrome from 'ui/chrome';
+if (chrome.getInjected('indexLifecycleManagementUiEnabled')) {
+ require('./register_index_lifecycle_actions');
+ require('./register_index_lifecycle_banner');
+ require('./register_index_lifecycle_summary');
+}
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_actions.js b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_actions.js
new file mode 100644
index 0000000000000..9e265d82009c8
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_actions.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 { every } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { addActionExtension } from '../../../index_management/public/index_management_extensions';
+import { retryLifecycleForIndex } from '../api';
+addActionExtension((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,
+ indexNames: [indexNames],
+ buttonLabel: i18n.translate('xpack.idxMgmt.retryIndexLifecycleActionButtonLabel', {
+ defaultMessage: 'Retry lifecycle',
+ }),
+ successMessage: i18n.translate('xpack.idxMgmt.retryIndexLifecycleAction.successfullyRetriedLifecycleMessage', {
+ defaultMessage: 'Successfully called retry lifecycle for: [{indexNames}]',
+ values: { indexNames: indexNames.join(', ') }
+ }),
+ };
+});
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_banner.js b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_banner.js
new file mode 100644
index 0000000000000..519c9b78518b4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_banner.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 { get } from 'lodash';
+import { addBannerExtension } from '../../../index_management/public/index_management_extensions';
+const stepPath = 'ilm.step';
+import { i18n } from '@kbn/i18n';
+
+addBannerExtension((indices) =>{
+ 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: `${stepPath}:ERROR`,
+ message: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.errorMessage', {
+ defaultMessage: `{ numIndicesWithLifecycleErrors, number}
+ {numIndicesWithLifecycleErrors, plural, one {index has} other {indices have} }
+ lifecycle errors`,
+ values: { numIndicesWithLifecycleErrors }
+ }),
+ };
+});
\ No newline at end of file
diff --git a/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_summary.js
new file mode 100644
index 0000000000000..e1a74f0dd6991
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_index_management_extensions/register_index_lifecycle_summary.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 React from 'react';
+import { addSummaryExtension } from '../../../index_management/public/index_management_extensions';
+import { IndexLifecycleSummary } from '../components/index_lifecycle_summary';
+addSummaryExtension((index) => {
+ return ;
+});
+
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..ed6ecfd756028
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_management_section.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 { management } from 'ui/management';
+import { BASE_PATH } from '../common/constants';
+import chrome from 'ui/chrome';
+if (chrome.getInjected('indexLifecycleManagementUiEnabled')) {
+ const esSection = management.getSection('elasticsearch');
+ esSection.register('index_lifecycle_management', {
+ visible: true,
+ display: 'Index Lifecycle Management',
+ order: 1,
+ 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..ebb37c9b76a51
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/register_routes.js
@@ -0,0 +1,55 @@
+/*
+ * 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 } from 'react-dom';
+import { Provider } from 'react-redux';
+import { setHttpClient } from './api';
+
+import { App } from './app';
+import { BASE_PATH } from '../common/constants/base_path';
+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 './lib/manage_angular_lifecycle';
+import chrome from 'ui/chrome';
+
+const renderReact = async (elem) => {
+ render(
+
+
+
+
+ ,
+ elem
+ );
+};
+if (chrome.getInjected('indexLifecycleManagementUiEnabled')) {
+ routes.when(`${BASE_PATH}:view?/:action?/:id?`, {
+ template: template,
+ controllerAs: 'indexLifecycleManagement',
+ controller: class IndexLifecycleManagementController {
+ constructor($scope, $route, $http, kbnUrl, $rootScope) {
+ setHttpClient($http);
+ setUrlService({
+ change(url) {
+ kbnUrl.change(url);
+ $rootScope.$digest();
+ }
+ });
+ $scope.$$postDigest(() => {
+ const elem = document.getElementById('indexLifecycleManagementReactRoot');
+ renderReact(elem);
+ manageAngularLifecycle($scope, $route, elem);
+ });
+ }
+ }
+ });
+}
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..d8f2b97e253a3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.container.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+
+import { connect } from 'react-redux';
+import { ColdPhase as PresentationComponent } from './cold_phase';
+import {
+ getNodeOptions,
+ getPhase,
+ getPhaseData
+} from '../../../../store/selectors';
+import { setPhaseData, fetchNodes } from '../../../../store/actions';
+import {
+ PHASE_COLD,
+ PHASE_WARM,
+ PHASE_REPLICA_COUNT
+} from '../../../../store/constants';
+
+export const ColdPhase = connect(
+ (state) => ({
+ phaseData: getPhase(state, PHASE_COLD),
+ nodeOptions: getNodeOptions(state),
+ warmPhaseReplicaCount: getPhaseData(state, PHASE_WARM, PHASE_REPLICA_COUNT)
+ }),
+ {
+ setPhaseData: (key, value) => setPhaseData(PHASE_COLD, key, value),
+ fetchNodes
+ }
+)(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..92973ad3a0e38
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/cold_phase/cold_phase.js
@@ -0,0 +1,254 @@
+/*
+ * 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,
+ EuiSelect,
+ 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 { ActiveBadge, PhaseErrorMessage } from '../../../../components';
+
+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,
+ nodeOptions: PropTypes.array.isRequired
+ };
+
+ componentWillMount() {
+ this.props.fetchNodes();
+ }
+
+ render() {
+ const {
+ setPhaseData,
+ showNodeDetailsFlyout,
+ phaseData,
+ nodeOptions,
+ warmPhaseReplicaCount,
+ errors,
+ isShowingErrors,
+ intl
+ } = this.props;
+
+ return (
+
+
+
+ {' '}
+ {phaseData[PHASE_ENABLED] ? (
+
+ ) : null}
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+
+
+ }
+ fullWidth
+ >
+ {phaseData[PHASE_ENABLED] ? (
+
+
+
+
+ {
+ await setPhaseData(PHASE_ENABLED, false);
+ }}
+ >
+
+
+
+
+
+
+
+
+ {
+ 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.coldPhase.daysLabel',
+ defaultMessage: 'days'
+ }) },
+ { value: 'h', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.coldPhase.hoursLabel',
+ defaultMessage: 'hours'
+ }) },
+ ]}
+ />
+
+
+
+
+
+
+ showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])}
+ >
+
+
+ ) : null}
+ >
+ {
+ await setPhaseData(PHASE_NODE_ATTRS, e.target.value);
+ }}
+ />
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_REPLICA_COUNT, e.target.value);
+ }}
+ min={0}
+ />
+
+
+
+
+
+ setPhaseData(PHASE_REPLICA_COUNT, warmPhaseReplicaCount)
+ }
+ >
+ Set to same as warm phase
+
+
+
+
+
+ ) : (
+
+
+ {
+ await setPhaseData(PHASE_ENABLED, true);
+
+ }}
+ >
+
+
+
+ )}
+
+ );
+ }
+}
+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..d15d794a277bc
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/delete_phase/delete_phase.js
@@ -0,0 +1,178 @@
+/*
+ * 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, injectI18n } from '@kbn/i18n/react';
+import PropTypes from 'prop-types';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiSpacer,
+ EuiFormRow,
+ EuiFieldNumber,
+ EuiSelect,
+ EuiDescribedFormGroup,
+ EuiButton,
+} from '@elastic/eui';
+import {
+ PHASE_DELETE,
+ PHASE_ENABLED,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
+} from '../../../../store/constants';
+import { ErrableFormRow } from '../../form_errors';
+import { ActiveBadge, PhaseErrorMessage } from '../../../../components';
+
+class DeletePhaseUi 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,
+ intl
+ } = this.props;
+
+ return (
+
+
+
+ {' '}
+ {phaseData[PHASE_ENABLED] ? (
+
+ ) : null}
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+
+
+
+ }
+ fullWidth
+ >
+ {phaseData[PHASE_ENABLED] ? (
+
+
+
+
+ {
+ await setPhaseData(PHASE_ENABLED, false);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ 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.deletePhase.daysLabel',
+ defaultMessage: 'days'
+ }) },
+ { value: 'h', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.deletePhase.hoursLabel',
+ defaultMessage: 'hours'
+ }) },
+ ]}
+ />
+
+
+
+
+ ) : (
+
+
+ {
+ await setPhaseData(PHASE_ENABLED, true);
+ }}
+ >
+
+
+
+ )}
+
+ );
+ }
+}
+export const DeletePhase = injectI18n(DeletePhaseUi);
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..19fcb8507b8d5
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/hot_phase/hot_phase.js
@@ -0,0 +1,234 @@
+/*
+ * 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 (
+
+
+
+ {' '}
+
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+
+
+ }
+ fullWidth
+ >
+
+
+ {' '}
+
+
+ }
+ >
+ {
+ 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/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..3128a38c2c34f
--- /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,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 { connect } from 'react-redux';
+import { NodeAttrsDetails as PresentationComponent } from './node_attrs_details';
+import { getNodeDetails, getExistingAllocationRules } from '../../../../store/selectors';
+import { fetchNodeDetails } from '../../../../store/actions';
+
+export const NodeAttrsDetails = connect(
+ (state, ownProps) => ({
+ details: getNodeDetails(state, ownProps.selectedNodeAttrs),
+ allocationRules: getExistingAllocationRules(state),
+ }),
+ { 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..e55a6df0e9013
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js
@@ -0,0 +1,104 @@
+/*
+ * 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 PropTypes from 'prop-types';
+
+import {
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyout,
+ EuiTitle,
+ EuiInMemoryTable,
+ EuiSpacer,
+ EuiButtonEmpty,
+ EuiCallOut,
+ 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,
+ allocationRules: PropTypes.object,
+ };
+
+ componentWillMount() {
+ this.props.fetchNodeDetails(this.props.selectedNodeAttrs);
+ }
+
+ render() {
+ const { selectedNodeAttrs, allocationRules, details, close, intl } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {allocationRules ? (
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+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..cd8ddfd372551
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js
@@ -0,0 +1,66 @@
+/*
+ * 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 {
+ EuiCodeEditor,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyout,
+ EuiTitle,
+ EuiSpacer,
+ EuiButtonEmpty,
+ EuiPortal,
+} 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,
+ };
+ render() {
+ const { lifecycle, close, policyName } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+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..c21cc9e7bfbee
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.container.js
@@ -0,0 +1,29 @@
+/*
+ * 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 {
+ getNodeOptions,
+ getPhase,
+} from '../../../../store/selectors';
+import { setPhaseData, fetchNodes } 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],
+ nodeOptions: getNodeOptions(state)
+ }),
+ {
+ setPhaseData: (key, value) => setPhaseData(PHASE_WARM, key, value),
+ fetchNodes
+ }
+)(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..abc012ca590df
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/components/warm_phase/warm_phase.js
@@ -0,0 +1,387 @@
+/*
+ * 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,
+ EuiTitle,
+ EuiSpacer,
+ EuiFormRow,
+ EuiFieldNumber,
+ EuiSelect,
+ EuiSwitch,
+ EuiButtonEmpty,
+ 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 { ErrableFormRow } from '../../form_errors';
+import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../../../components';
+
+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,
+ nodeOptions: PropTypes.array.isRequired,
+ };
+
+ componentWillMount() {
+ this.props.fetchNodes();
+ }
+
+ render() {
+ const {
+ setPhaseData,
+ showNodeDetailsFlyout,
+ phaseData,
+ nodeOptions,
+ errors,
+ isShowingErrors,
+ hotPhaseRolloverEnabled,
+ intl
+ } = this.props;
+
+ return (
+
+
+
+ {' '}
+ {phaseData[PHASE_ENABLED] ? (
+
+ ) : null}
+
+ }
+ titleSize="s"
+ description={
+
+
+
+
+
+
+ }
+ fullWidth
+ >
+
+ {phaseData[PHASE_ENABLED] ? (
+
+
+
+ {
+ await setPhaseData(PHASE_ENABLED, false);
+ }}
+ >
+
+
+
+
+ {hotPhaseRolloverEnabled ? (
+
+ {
+ await setPhaseData(WARM_PHASE_ON_ROLLOVER, e.target.checked);
+ }}
+ />
+
+ ) : null}
+ {!phaseData[WARM_PHASE_ON_ROLLOVER] ? (
+
+
+
+ {
+ setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE, e.target.value);
+ }}
+ min={1}
+ />
+
+
+
+
+ {
+ await setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE_UNITS, e.target.value);
+ }}
+ options={[
+ { value: 'd', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.warmPhase.daysLabel',
+ defaultMessage: 'days'
+ }) },
+ { value: 'h', text: intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.warmPhase.hoursLabel',
+ defaultMessage: 'hours'
+ }) },
+ ]}
+ />
+
+
+
+ ) : null}
+
+
+
+ showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])}
+ >
+
+
+ ) : null
+ }
+ >
+ {
+ await setPhaseData(PHASE_NODE_ATTRS, e.target.value);
+ }}
+ />
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_REPLICA_COUNT, e.target.value);
+ }}
+ min={0}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_SHRINK_ENABLED, e.target.checked);
+ }}
+ label={intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel',
+ defaultMessage: 'Shrink index'
+ })}
+ />
+ {phaseData[PHASE_SHRINK_ENABLED] ? (
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_PRIMARY_SHARD_COUNT, e.target.value);
+ }}
+ min={1}
+ />
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+ {
+ await setPhaseData(PHASE_FORCE_MERGE_ENABLED, e.target.checked);
+ }}
+ />
+
+
+
+ {phaseData[PHASE_FORCE_MERGE_ENABLED] ? (
+
+ {
+ await setPhaseData(PHASE_FORCE_MERGE_SEGMENTS, e.target.value);
+ }}
+ min={1}
+ />
+
+ ) : null}
+
+ ) : (
+
+
+ {
+ await setPhaseData(PHASE_ENABLED, true);
+ }}
+ >
+
+
+
+
+ )}
+
+
+ );
+ }
+}
+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..d5f85e814f0dc
--- /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,
+ getAffectedIndexTemplates,
+ validateLifecycle,
+ getLifecycle,
+ getPolicies,
+ isPolicyListLoaded,
+ getIsNewPolicy
+} from '../../store/selectors';
+import {
+ setSelectedPolicy,
+ setSelectedPolicyName,
+ setSaveAsNewPolicy,
+ saveLifecyclePolicy,
+ fetchPolicies,
+} from '../../store/actions';
+import { findFirstError } from '../../lib/find_errors';
+
+export const EditPolicy = connect(
+ state => {
+ const errors = validateLifecycle(state);
+ const firstError = findFirstError(errors);
+ return {
+ firstError,
+ errors,
+ selectedPolicy: getSelectedPolicy(state),
+ affectedIndexTemplates: getAffectedIndexTemplates(state),
+ saveAsNewPolicy: getSaveAsNewPolicy(state),
+ lifecycle: getLifecycle(state),
+ policies: getPolicies(state),
+ isPolicyListLoaded: isPolicyListLoaded(state),
+ isNewPolicy: getIsNewPolicy(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..d81f517fce816
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js
@@ -0,0 +1,292 @@
+/*
+ * 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,
+} 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 '../../lib/find_errors';
+import { NodeAttrsDetails } from './components/node_attrs_details';
+import { PolicyJsonFlyout } from './components/policy_json_flyout';
+import { ErrableFormRow } from './form_errors';
+
+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 {
+ match: {
+ params: { policyName },
+ },
+ isPolicyListLoaded,
+ fetchPolicies,
+ } = this.props;
+ if (policyName) {
+ if (isPolicyListLoaded) {
+ this.selectPolicy(policyName);
+ } else {
+ fetchPolicies(true, () => {
+ this.selectPolicy(policyName);
+ });
+ }
+ }
+ }
+ 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 the fix errors on the 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,
+ match: {
+ params: { policyName },
+ },
+ setSaveAsNewPolicy,
+ saveAsNewPolicy,
+ setSelectedPolicyName,
+ isNewPolicy,
+ lifecycle
+ } = 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 {selectedPolicyName}',
+ }, { selectedPolicyName }) }
+
+
+
+
+
+ Configure the phases of your data and when to transition between them.
+
+
+
+ {policyName ? (
+
+
+
+
+
+
+ .{' '}
+
+
+
+
+
+ {policyName ? (
+
+ {
+ await setSaveAsNewPolicy(e.target.checked);
+ }}
+ label={
+
+
+
+ }
+ />
+
+ ) : null}
+
+ ) : null}
+ {saveAsNewPolicy || !policyName ? (
+
+
+ {
+ await setSelectedPolicyName(e.target.value);
+ }}
+ />
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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/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..596ccb6645ea1
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/confirm_delete.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 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 '../../../../api';
+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 message = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.confirmDelete.errorMessage',
+ defaultMessage: 'Error deleting policy}{policyName}',
+ }, { policyName });
+ toastNotifications.addDanger(message);
+ }
+ if (callback) {
+ callback();
+ }
+ };
+ render() {
+ const { intl, policyToDelete, onCancel } = this.props;
+ const title = intl.formatMessage({
+ id: 'xpack.indexLifecycleMgmt.confirmDelete.title',
+ defaultMessage: 'Delete {name}',
+ }, { name: policyToDelete.name });
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
+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..fa53d471286ae
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.container.js
@@ -0,0 +1,45 @@
+/*
+ * 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 { getPageOfPolicies, getPolicyPager, getPolicyFilter, getPolicySort } 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 => ({
+ policies: getPageOfPolicies(state),
+ pager: getPolicyPager(state),
+ filter: getPolicyFilter(state),
+ sortField: getPolicySort(state).sortField,
+ isSortAscending: getPolicySort(state).isSortAscending,
+ }),
+ 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..658a4710d885f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js
@@ -0,0 +1,297 @@
+/*
+ * 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 moment from 'moment-timezone';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
+import { NoMatch } from '../no_match';
+import { BASE_PATH } from '../../../../../common/constants';
+import {
+ EuiButton,
+ EuiLink,
+ EuiButtonIcon,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPage,
+ EuiSpacer,
+ EuiTable,
+ EuiTableBody,
+ EuiTableHeader,
+ EuiTableHeaderCell,
+ EuiTablePagination,
+ EuiTableRow,
+ EuiTableRowCell,
+ EuiTitle,
+ EuiText,
+ EuiPageBody,
+ EuiPageContent
+} from '@elastic/eui';
+
+import { ConfirmDelete } from './confirm_delete';
+const HEADERS = {
+ name: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', {
+ defaultMessage: 'Name',
+ }),
+ coveredIndices: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.coveredIndicesHeader', {
+ defaultMessage: 'Covered Indices',
+ }),
+ version: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.versionHeader', {
+ defaultMessage: 'Version',
+ }),
+ modified_date: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader', {
+ defaultMessage: 'Modified date',
+ }),
+};
+
+export class PolicyTableUi extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selectedPoliciesMap: {},
+ showDeleteConfirmation: false
+ };
+ }
+ componentDidMount() {
+ this.props.fetchPolicies(true);
+ }
+ deleteConfirmation() {
+ const { policyToDelete } = this.state;
+ if (!policyToDelete) {
+ return null;
+ }
+ return (
+ this.setState({ policyToDelete: null })}
+ />
+ );
+ }
+ handleDelete = () => {
+ this.props.fetchPolicies(true);
+ this.setState({ 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(HEADERS).map(([fieldName, label]) => {
+ const isSorted = sortField === fieldName;
+ return (
+ this.onSort(fieldName)}
+ isSorted={isSorted}
+ isSortAscending={isSortAscending}
+ data-test-subj={`policyTableHeaderCell-${fieldName}`}
+ className={'policyTable__header--' + fieldName}
+ >
+ {label}
+
+ );
+ });
+ headers.push(
+
+ );
+ return headers;
+ }
+
+ buildRowCell(fieldName, value) {
+ if (fieldName === 'name') {
+ return (
+
+ {value}
+
+ );
+ } else if (fieldName === 'coveredIndices' && value) {
+ return (
+
+ {value.length}
+
+ );
+ } else if (fieldName === 'modified_date' && value) {
+ return moment(value).format('YYYY-MM-DD HH:mm:ss');
+ }
+ return value;
+ }
+
+ buildRowCells(policy) {
+ const { name } = policy;
+ const cells = Object.keys(HEADERS).map(fieldName => {
+ const value = policy[fieldName];
+ return (
+
+ {this.buildRowCell(fieldName, value)}
+
+ );
+ });
+ cells.push(
+
+ this.setState({ policyToDelete: policy })}
+ iconType="trash"
+ />
+
+ );
+ 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 {
+ policyFilterChanged,
+ filter,
+ policies,
+ intl,
+ } = this.props;
+ const { selectedPoliciesMap } = this.state;
+ const numSelected = Object.keys(selectedPoliciesMap).length;
+
+ return (
+
+
+
+ {this.deleteConfirmation()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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"
+ />
+
+
+
+ Create new policy
+
+
+
+
+
+
+ {policies.length > 0 ? (
+
+
+ {this.buildHeader()}
+
+ {this.buildRows()}
+
+ ) : (
+
+ )}
+
+ {policies.length > 0 ? 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/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/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/navigation.js b/x-pack/plugins/index_lifecycle_management/public/services/navigation.js
new file mode 100644
index 0000000000000..b7365e8f22ffc
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_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.
+ */
+
+let urlService;
+import { BASE_PATH } from '../../common/constants';
+export const setUrlService = (aUrlService) => {
+ urlService = aUrlService;
+};
+
+
+export const goToPolicyList = () => {
+ urlService.change(`${BASE_PATH}policies`);
+};
\ 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..c94bb95a85ce0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/services/sort_table.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 { sortBy } from 'lodash';
+
+const stringSort = (fieldName) => (item) => item[fieldName];
+const arraySort = (fieldName) => (item) => (item[fieldName] || []).length;
+
+const sorters = {
+ name: stringSort('name'),
+ coveredIndices: arraySort('coveredIndices'),
+};
+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..621cbf007d3b2
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/index.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.
+ */
+
+
+
+
+export * from './index_template';
+export * from './nodes';
+export * from './policies';
+export * from './lifecycle';
+export * from './general';
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/actions/index_template.js b/x-pack/plugins/index_lifecycle_management/public/store/actions/index_template.js
new file mode 100644
index 0000000000000..c7bcd3518d036
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/index_template.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+
+import { createAction } from 'redux-actions';
+import { toastNotifications } from 'ui/notify';
+import { loadIndexTemplates, loadIndexTemplate } from '../../api';
+import { getAlias } from '../selectors';
+import {
+ setPhaseData,
+ setIndexName,
+ setAliasName,
+ setSelectedPrimaryShardCount,
+ setSelectedReplicaCount,
+ setSelectedNodeAttrs,
+ setSelectedPolicyName,
+} from '.';
+import {
+ PHASE_HOT,
+ PHASE_ROLLOVER_ALIAS,
+ PHASE_WARM,
+ PHASE_COLD,
+ PHASE_DELETE
+} from '../constants';
+
+export const fetchingIndexTemplates = createAction('FETCHING_INDEX_TEMPLATES');
+export const fetchedIndexTemplates = createAction('FETCHED_INDEX_TEMPLATES');
+export const fetchIndexTemplates = () => async dispatch => {
+ dispatch(fetchingIndexTemplates());
+
+ let templates;
+ try {
+ templates = await loadIndexTemplates();
+ } catch (err) {
+ return toastNotifications.addDanger(err.data.message);
+ }
+
+ dispatch(fetchedIndexTemplates(templates));
+};
+
+export const fetchedIndexTemplate = createAction('FETCHED_INDEX_TEMPLATE');
+export const fetchIndexTemplate = templateName => async (dispatch) => {
+ let template;
+ try {
+ template = await loadIndexTemplate(templateName);
+ } catch (err) {
+ return toastNotifications.addDanger(err.data.message);
+ }
+
+ if (template.settings && template.settings.index) {
+ dispatch(
+ setSelectedPrimaryShardCount(template.settings.index.number_of_shards)
+ );
+ dispatch(
+ setSelectedReplicaCount(template.settings.index.number_of_replicas)
+ );
+ if (
+ template.settings.index.routing &&
+ template.settings.index.routing.allocation &&
+ template.settings.index.routing.allocation.include
+ ) {
+ dispatch(
+ setSelectedNodeAttrs(
+ template.settings.index.routing.allocation.include.sattr_name
+ )
+ );
+ }
+ if (template.settings.index.lifecycle) {
+ dispatch(setSelectedPolicyName(template.settings.index.lifecycle.name));
+ }
+ }
+
+ let indexPattern = template.index_patterns[0];
+ if (indexPattern.endsWith('*')) {
+ indexPattern = indexPattern.slice(0, -1);
+ }
+ dispatch(setIndexName(`${indexPattern}-00001`));
+ dispatch(setAliasName(`${indexPattern}-alias`));
+ dispatch(fetchedIndexTemplate(template));
+};
+
+export const setSelectedIndexTemplateName = createAction(
+ 'SET_SELECTED_INDEX_TEMPLATE_NAME'
+);
+
+export const setSelectedIndexTemplate = name => async (dispatch, getState) => {
+ // Await all of these to ensure they happen before the next round of validation
+ const promises = [
+ dispatch(setSelectedIndexTemplateName(name)),
+ dispatch(fetchIndexTemplate(name))
+ ];
+ const alias = getAlias(getState());
+ if (alias) {
+ promises.push(...[
+ dispatch(setPhaseData(PHASE_HOT, PHASE_ROLLOVER_ALIAS, alias)),
+ dispatch(setPhaseData(PHASE_WARM, PHASE_ROLLOVER_ALIAS, alias)),
+ dispatch(setPhaseData(PHASE_COLD, PHASE_ROLLOVER_ALIAS, alias)),
+ dispatch(setPhaseData(PHASE_DELETE, PHASE_ROLLOVER_ALIAS, alias))
+ ]);
+ }
+ await Promise.all(promises);
+};
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..f03c0018034a5
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/lifecycle.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 { toastNotifications } from 'ui/notify';
+import { saveLifecycle as saveLifecycleApi } from '../../api';
+
+
+export const saveLifecyclePolicy = (lifecycle, isNew) => async () => {
+ try {
+ await saveLifecycleApi(lifecycle);
+ }
+ catch (err) {
+ toastNotifications.addDanger(err.data.message);
+ return false;
+ }
+ const verb = isNew ? 'created' : 'updated';
+ toastNotifications.addSuccess(`Successfully ${verb} lifecycle policy '${lifecycle.name}'`);
+ 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..b806d1df3d380
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/nodes.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 { createAction } from 'redux-actions';
+import { toastNotifications } from 'ui/notify';
+import { loadNodes, loadNodeDetails } from '../../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');
+export const fetchNodes = () => async dispatch => {
+ let nodes;
+ try {
+ nodes = await loadNodes();
+ } catch (err) {
+ return toastNotifications.addDanger(err.data.message);
+ }
+
+ dispatch(fetchedNodes(nodes));
+};
+
+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) {
+ return toastNotifications.addDanger(err.data.message);
+ }
+
+ 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..926b524921481
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/policies.js
@@ -0,0 +1,43 @@
+/*
+ * 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';
+import { toastNotifications } from 'ui/notify';
+import { loadPolicies } from '../../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) {
+ return toastNotifications.addDanger(err.data.message);
+ }
+
+ 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..873e4acd72312
--- /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_MAX_DOC_SIZE = 'selectedMaxDocSize';
+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 = [
+ PHASE_ROLLOVER_MAX_AGE,
+ PHASE_ROLLOVER_MAX_SIZE_STORED,
+ PHASE_ROLLOVER_MAX_DOC_SIZE,
+ PHASE_ROLLOVER_MINIMUM_AGE,
+ PHASE_FORCE_MERGE_SEGMENTS,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_REPLICA_COUNT,
+];
+
+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_ROLLOVER_MAX_DOC_SIZE]: []
+ },
+ [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..d0e45a963505b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/defaults/hot_phase.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 {
+ 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,
+ PHASE_ROLLOVER_MAX_DOC_SIZE,
+} 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',
+ [PHASE_ROLLOVER_MAX_DOC_SIZE]: '',
+};
\ 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..7225d9e0be9f5
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/index.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 { combineReducers } from 'redux';
+import { indexTemplate } from './index_template';
+import { nodes } from './nodes';
+import { policies } from './policies';
+import { general } from './general';
+
+export const indexLifecycleManagement = combineReducers({
+ indexTemplate,
+ nodes,
+ policies,
+ general,
+});
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/reducers/index_template.js b/x-pack/plugins/index_lifecycle_management/public/store/reducers/index_template.js
new file mode 100644
index 0000000000000..19bc7af01954c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/index_template.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 { handleActions } from 'redux-actions';
+import {
+ fetchingIndexTemplates,
+ fetchedIndexTemplates,
+ setSelectedIndexTemplateName,
+ fetchedIndexTemplate
+} from '../actions/index_template';
+
+const defaultState = {
+ isLoading: false,
+ fullSelectedIndexTemplate: null,
+ selectedIndexTemplateName: '',
+ indexTemplates: null,
+};
+
+export const indexTemplate = handleActions(
+ {
+ [fetchingIndexTemplates](state) {
+ return {
+ ...state,
+ isLoading: true
+ };
+ },
+ [fetchedIndexTemplates](state, { payload: indexTemplates }) {
+ return {
+ ...state,
+ isLoading: false,
+ indexTemplates
+ };
+ },
+ [fetchedIndexTemplate](state, { payload: fullSelectedIndexTemplate }) {
+ return {
+ ...state,
+ fullSelectedIndexTemplate,
+ };
+ },
+ [setSelectedIndexTemplateName](state, { payload: selectedIndexTemplateName }) {
+ return {
+ ...state,
+ selectedIndexTemplateName
+ };
+ }
+ },
+ defaultState
+);
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..5e8e01eab26df
--- /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: [],
+ 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..621cbf007d3b2
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/index.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.
+ */
+
+
+
+
+export * from './index_template';
+export * from './nodes';
+export * from './policies';
+export * from './lifecycle';
+export * from './general';
diff --git a/x-pack/plugins/index_lifecycle_management/public/store/selectors/index_template.js b/x-pack/plugins/index_lifecycle_management/public/store/selectors/index_template.js
new file mode 100644
index 0000000000000..0ba81b4595296
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/index_template.js
@@ -0,0 +1,178 @@
+/*
+ * 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 { get, merge, cloneDeep } from 'lodash';
+import {
+ getSaveAsNewPolicy,
+ getSelectedPolicyName,
+ getSelectedPrimaryShardCount,
+ getNodesFromSelectedNodeAttrs,
+ getSelectedReplicaCount,
+ getSelectedNodeAttrs
+} from '.';
+import { getAliasName } from './general';
+
+export const getIndexTemplates = state => { return state.indexTemplate.indexTemplates || []; };
+export const getIndexTemplateOptions = createSelector(
+ [state => getIndexTemplates(state)],
+ templates => {
+ if (!templates) {
+ return [];
+ }
+
+ const options = templates.map(template => ({
+ text: template.name,
+ value: template.name
+ }));
+
+ options.sort((a, b) => a.text.localeCompare(b.text));
+ options.unshift({
+ text: '',
+ value: undefined
+ });
+
+ return options;
+ }
+);
+export const getSelectedIndexTemplateName = state =>
+ state.indexTemplate.selectedIndexTemplateName;
+
+export const getSelectedIndexTemplate = createSelector(
+ [
+ state => getSelectedIndexTemplateName(state),
+ state => getIndexTemplates(state)
+ ],
+ (selectedIndexTemplateName, allTemplates) => {
+ if (!allTemplates) {
+ return null;
+ }
+ return allTemplates.find(
+ template => template.name === selectedIndexTemplateName
+ );
+ }
+);
+
+export const getFullSelectedIndexTemplate = state => state.indexTemplate.fullSelectedIndexTemplate;
+
+export const getAlias = state => {
+ const indexTemplate = getSelectedIndexTemplate(state);
+ return get(indexTemplate, 'settings.indexlifecycle.rollover_alias');
+};
+
+// TODO: add createSelector
+export const getAffectedIndexTemplates = state => {
+ const selectedIndexTemplateName = getSelectedIndexTemplateName(state);
+ const indexTemplates = [selectedIndexTemplateName];
+
+ const selectedPolicyName = getSelectedPolicyName(state);
+ const allTemplates = getIndexTemplates(state);
+ indexTemplates.push(
+ ...allTemplates.reduce((accum, template) => {
+ if (template.index_lifecycle_name === selectedPolicyName && template.name !== selectedIndexTemplateName) {
+ accum.push(template.name);
+ }
+ return accum;
+ }, [])
+ );
+
+ return indexTemplates;
+};
+
+// TODO: add createSelector
+export const getAffectedIndexPatterns = state => {
+ const indexPatterns = [...getSelectedIndexTemplate(state).index_patterns];
+
+ if (!getSaveAsNewPolicy(state)) {
+ const allTemplates = getIndexTemplates(state);
+ const selectedPolicyName = getSelectedPolicyName(state);
+ indexPatterns.push(
+ ...allTemplates.reduce((accum, template) => {
+ if (template.index_lifecycle_name === selectedPolicyName) {
+ accum.push(...template.index_patterns);
+ }
+ return accum;
+ }, [])
+ );
+ }
+
+ return indexPatterns;
+};
+
+export const getSelectedIndexTemplateIndices = state => {
+ const selectedIndexTemplate = getSelectedIndexTemplate(state);
+ if (selectedIndexTemplate) {
+ return selectedIndexTemplate.indices;
+ }
+ return undefined;
+};
+
+export const getExistingAllocationRules = state => {
+ const selectedIndexTemplate = getSelectedIndexTemplate(state);
+ if (selectedIndexTemplate) {
+ return selectedIndexTemplate.allocation_rules;
+ }
+ return undefined;
+};
+
+const hasJSONChanged = (json1, json2) => JSON.stringify(json1) !== JSON.stringify(json2);
+export const getTemplateDiff = state => {
+ const originalFullIndexTemplate = getFullSelectedIndexTemplate(state) || { settings: {} };
+ const attributeNameAndValue = getSelectedNodeAttrs(state);
+ const baseNewFullIndexTemplate = {
+ settings: {
+ index: {
+ number_of_shards: getSelectedPrimaryShardCount(state) + '',
+ number_of_replicas: getSelectedReplicaCount(state) + '',
+ lifecycle: {
+ name: getSelectedPolicyName(state)
+ },
+ }
+ }
+ };
+ if (attributeNameAndValue) {
+ const [ name, value ] = attributeNameAndValue.split[':'];
+ baseNewFullIndexTemplate.routing = {
+ allocation: {
+ include: {
+ [name]: value,
+ }
+ }
+ };
+ }
+ const newFullIndexTemplate = merge(cloneDeep(originalFullIndexTemplate), baseNewFullIndexTemplate);
+
+ return {
+ originalFullIndexTemplate,
+ newFullIndexTemplate,
+ hasChanged: hasJSONChanged(originalFullIndexTemplate, newFullIndexTemplate),
+ };
+};
+
+export const getIsPrimaryShardCountHigherThanSelectedNodeAttrsCount = state => {
+ const primaryShardCount = getSelectedPrimaryShardCount(state);
+ const selectedNodeAttrsCount = getNodesFromSelectedNodeAttrs(state);
+
+ if (selectedNodeAttrsCount === null) {
+ return false;
+ }
+
+ return primaryShardCount > selectedNodeAttrsCount;
+};
+
+export const getIndexTemplatePatch = state => {
+ return {
+ indexTemplate: getSelectedIndexTemplateName(state),
+ primaryShardCount: getSelectedPrimaryShardCount(state),
+ replicaCount: getSelectedReplicaCount(state),
+ lifecycleName: getSelectedPolicyName(state),
+ nodeAttrs: getSelectedNodeAttrs(state),
+ rolloverAlias: getAliasName(state)
+ };
+};
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..9d44cd4ae5b5e
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/lifecycle.js
@@ -0,0 +1,181 @@
+/*
+ * 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_MAX_SIZE_STORED,
+ STRUCTURE_POLICY_NAME,
+ ERROR_STRUCTURE,
+ PHASE_ATTRIBUTES_THAT_ARE_NUMBERS,
+ PHASE_PRIMARY_SHARD_COUNT,
+ PHASE_SHRINK_ENABLED,
+ PHASE_FORCE_MERGE_ENABLED,
+ PHASE_FORCE_MERGE_SEGMENTS
+} from '../constants';
+import {
+ getPhase,
+ getPhases,
+ phaseToES,
+ getSelectedPolicyName,
+ isNumber,
+ getSaveAsNewPolicy,
+ getSelectedOriginalPolicyName,
+ getPolicies
+} from '.';
+const numberRequiredMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', {
+ defaultMessage: 'A number is required'
+});
+const positiveNumberRequiredMessage = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', {
+ defaultMessage: 'Only positive numbers are allowed'
+});
+export const validatePhase = (type, phase, errors) => {
+ const phaseErrors = {};
+
+ if (!phase[PHASE_ENABLED]) {
+ return;
+ }
+
+ if (phase[PHASE_ROLLOVER_ENABLED]) {
+ if (
+ !isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) &&
+ !isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED])
+ ) {
+ phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', {
+ defaultMessage: 'A maximum age is required'
+ })
+ ];
+ phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', {
+ defaultMessage: 'A maximum index size is required'
+ })
+ ];
+ }
+ }
+
+ for (const numberedAttribute of PHASE_ATTRIBUTES_THAT_ARE_NUMBERS) {
+ if (phase.hasOwnProperty(numberedAttribute) && phase[numberedAttribute] !== '') {
+ // If shrink is disabled, there is no need to validate this
+ if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && !phase[PHASE_SHRINK_ENABLED]) {
+ continue;
+ }
+ if (!isNumber(phase[numberedAttribute])) {
+ phaseErrors[numberedAttribute] = [numberRequiredMessage];
+ }
+ else if (phase[numberedAttribute] < 0) {
+ phaseErrors[numberedAttribute] = [positiveNumberRequiredMessage];
+ }
+ else if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && phase[numberedAttribute] < 1) {
+ phaseErrors[numberedAttribute] = [positiveNumberRequiredMessage];
+ }
+ }
+ }
+
+ 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] = [positiveNumberRequiredMessage];
+ }
+ }
+
+ 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] = [
+ i18n.translate('xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError', {
+ defaultMessage: 'Only positive numbers above 0 are allowed'
+ })
+ ];
+ }
+ }
+ errors[type] = {
+ ...errors[type],
+ ...phaseErrors
+ };
+};
+
+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));
+
+ if (!getSelectedPolicyName(state)) {
+ errors[STRUCTURE_POLICY_NAME].push(i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', {
+ defaultMessage: 'A policy name is required'
+ }));
+ }
+
+ if (getSaveAsNewPolicy(state) && getSelectedOriginalPolicyName(state) === getSelectedPolicyName(state)) {
+ errors[STRUCTURE_POLICY_NAME].push(i18n.translate('xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', {
+ defaultMessage: 'The policy name must be different'
+ }));
+ }
+
+ if (getSaveAsNewPolicy(state)) {
+ const policyNames = getPolicies(state).map(policy => policy.name);
+ if (policyNames.includes(getSelectedPolicyName(state))) {
+ errors[STRUCTURE_POLICY_NAME].push(i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', {
+ defaultMessage: 'That policy name is already used'
+ }));
+ }
+ }
+
+ 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..097704a96d475
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/nodes.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 { createSelector } from 'reselect';
+
+export const getNodes = state => state.nodes.nodes;
+export const getNodeOptions = createSelector(
+ [state => getNodes(state)],
+ nodes => {
+ if (!nodes) {
+ return [];
+ }
+
+ const options = Object.keys(nodes).map(attrs => ({
+ text: `${attrs} (${nodes[attrs].length})`,
+ value: attrs,
+ }));
+
+ options.sort((a, b) => a.value.localeCompare(b.value));
+ return [{ text: 'Default allocation (don\'t use attributes)', value: '' }, ...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..be50ad382c87a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/policies.js
@@ -0,0 +1,277 @@
+/*
+ * 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 { getIndexTemplates } from '.';
+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 getAllPolicyNamesFromTemplates = state => {
+ return getIndexTemplates(state).map(template => template.index_lifecycle_name).filter(name => 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: {}, // TODO: this seems to be a constant, confirm?
+ exclude: {}, // TODO: this seems to be a constant, confirm?
+ require: {
+ [name]: value
+ }
+ };
+ if (isNumber(phase[PHASE_REPLICA_COUNT])) {
+ 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..8a5a7d7029b71
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/lib/check_license/check_license.js
@@ -0,0 +1,58 @@
+/*
+ * 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 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: `You cannot use ${pluginName} because license information is not available at this time.`
+ };
+ }
+
+ 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: `Your ${licenseType} license does not support ${pluginName}. Please upgrade your license.`
+ };
+ }
+
+ // License is valid but not active
+ if (!isLicenseActive) {
+ return {
+ isAvailable: false,
+ showLinks: true,
+ enableLinks: false,
+ message: `You cannot use ${pluginName} because your ${licenseType} license has expired.`
+ };
+ }
+
+ // 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..11e01304b6e5c
--- /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 } 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..35bc4b7533605
--- /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 } 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_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..12007e778a989
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_index_routes.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 { registerBootstrapRoute } from './register_bootstrap_route';
+import { registerGetAffectedRoute } from './register_get_affected_route';
+import { registerRetryRoute } from './register_retry_route';
+
+export function registerIndexRoutes(server) {
+ registerBootstrapRoute(server);
+ registerGetAffectedRoute(server);
+ registerRetryRoute(server);
+}
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..97d51022374a6
--- /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: `/${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..8aa93e3540243
--- /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/${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..d6a9dd774e206
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/constants.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.
+ */
+
+export const NODE_ATTRS_KEYS_TO_IGNORE = [
+ 'ml.enabled',
+ 'ml.machine_memory',
+ 'ml.max_open_jobs'
+];
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..40525b45f5566
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.js
@@ -0,0 +1,64 @@
+/*
+ * 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) {
+ return Object.entries(stats.nodes).reduce((accum, [nodeId, stats]) => {
+ const attributes = stats.attributes || {};
+ for (const [key, value] of Object.entries(attributes)) {
+ if (!NODE_ATTRS_KEYS_TO_IGNORE.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 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);
+ 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..1e06bf99f6db4
--- /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/${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_fetch_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.js
new file mode 100644
index 0000000000000..d7fd8c83a2c98
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.js
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+
+import { 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 formatTemplates(templates, callWithRequest) {
+ const formattedTemplates = [];
+ const templateNames = Object.keys(templates);
+ for (const templateName of templateNames) {
+ const { settings, index_patterns } = templates[templateName]; // eslint-disable-line camelcase
+ 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,
+ };
+
+ const { indices } = await fetchIndices(index_patterns, callWithRequest);
+ formattedTemplate.indices = indices ? Object.keys(indices) : [];
+ 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);
+}
+
+async function fetchIndices(indexPatterns, callWithRequest) {
+ const params = {
+ method: 'GET',
+ path: `/${indexPatterns}/_stats`,
+ // 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 formatTemplates(hits, callWithRequest);
+ 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..9750c0157b965
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_templates_routes.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 { registerFetchRoute } from './register_fetch_route';
+import { registerGetRoute } from './register_get_route';
+
+export function registerTemplatesRoutes(server) {
+ registerFetchRoute(server);
+ registerGetRoute(server);
+}
diff --git a/x-pack/plugins/index_management/index.js b/x-pack/plugins/index_management/index.js
index 4408428364b83..6e29b91882d48 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,
@@ -21,9 +21,16 @@ export function indexManagement(kibana) {
styleSheetPaths: `${__dirname}/public/index.scss`,
managementSections: [
'plugins/index_management',
- ]
+ ],
+ injectDefaultVars(server) {
+ const config = server.config();
+ return {
+ indexManagementUiEnabled: config.get(`${PLUGIN.ID}.enabled`)
+ };
+ },
},
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..022ab9b6da5d4
--- /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 = (provider) => {
+ indexManagementDataEnrichers.push(provider);
+};
+export const getIndexManagementDataEnrichers = () => {
+ return indexManagementDataEnrichers;
+};
\ No newline at end of file
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..25ebb5eb0445a
--- /dev/null
+++ b/x-pack/plugins/index_management/public/index_management_extensions.js
@@ -0,0 +1,29 @@
+/*
+ * 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;
+};
+
+
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..3f084da770e1d 100644
--- a/x-pack/plugins/index_management/public/register_management_section.js
+++ b/x-pack/plugins/index_management/public/register_management_section.js
@@ -7,12 +7,15 @@
import { management } from 'ui/management';
import { i18n } from '@kbn/i18n';
import { BASE_PATH } from '../common/constants';
+import chrome from 'ui/chrome';
+if (chrome.getInjected('indexManagementUiEnabled')) {
+ const esSection = management.getSection('elasticsearch');
+ esSection.register('index_management', {
+ visible: true,
+ display: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }),
+ order: 1,
+ url: `#${BASE_PATH}home`
+ });
+}
-const esSection = management.getSection('elasticsearch');
-esSection.register('index_management', {
- visible: true,
- display: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }),
- order: 1,
- url: `#${BASE_PATH}home`
-});
diff --git a/x-pack/plugins/index_management/public/register_routes.js b/x-pack/plugins/index_management/public/register_routes.js
index 02666e81a0b5c..acae726940b04 100644
--- a/x-pack/plugins/index_management/public/register_routes.js
+++ b/x-pack/plugins/index_management/public/register_routes.js
@@ -19,6 +19,7 @@ import routes from 'ui/routes';
import template from './main.html';
import { manageAngularLifecycle } from './lib/manage_angular_lifecycle';
import { indexManagementStore } from './store';
+import chrome from 'ui/chrome';
const renderReact = async (elem) => {
render(
@@ -32,21 +33,22 @@ const renderReact = async (elem) => {
elem
);
};
-
-routes.when(`${BASE_PATH}:view?/:id?`, {
- template: template,
- controllerAs: 'indexManagement',
- controller: class IndexManagementController {
- constructor($scope, $route, $http) {
+if (chrome.getInjected('indexManagementUiEnabled')) {
+ routes.when(`${BASE_PATH}:view?/:id?`, {
+ template: template,
+ controllerAs: 'indexManagement',
+ controller: class IndexManagementController {
+ constructor($scope, $route, $http) {
// NOTE: We depend upon Angular's $http service because it's decorated with interceptors,
// e.g. to check license status per request.
- setHttpClient($http);
+ setHttpClient($http);
- $scope.$$postDigest(() => {
- const elem = document.getElementById('indexManagementReactRoot');
- renderReact(elem);
- manageAngularLifecycle($scope, $route, elem);
- });
+ $scope.$$postDigest(() => {
+ const elem = document.getElementById('indexManagementReactRoot');
+ renderReact(elem);
+ manageAngularLifecycle($scope, $route, 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..9a33de20f9d6d 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,16 +4,21 @@
* 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 {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiHealth,
EuiDescriptionList,
+ EuiHorizontalRule,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
+ EuiSpacer,
+ EuiTitle
} from '@elastic/eui';
-
+import { getSummaryExtensions } from '../../../../../index_management_extensions';
const HEADERS = {
health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', {
defaultMessage: 'Health',
@@ -45,9 +50,25 @@ const HEADERS = {
};
export class Summary extends React.PureComponent {
+ getAdditionalContent() {
+ const { index } = this.props;
+ const extensions = getSummaryExtensions();
+ return extensions.map((summaryExtension) => {
+ return (
+
+
+ { summaryExtension(index) }
+
+ );
+ });
+ }
buildRows() {
const { index } = this.props;
- return Object.keys(HEADERS).map(fieldName => {
+ const rows = {
+ left: [],
+ right: []
+ };
+ Object.keys(HEADERS).forEach((fieldName, arrayIndex) => {
const value = index[fieldName];
let content = value;
if(fieldName === 'health') {
@@ -56,7 +77,7 @@ export class Summary extends React.PureComponent {
if(Array.isArray(content)) {
content = content.join(', ');
}
- return [
+ const cell = [
{HEADERS[fieldName]}:
,
@@ -64,14 +85,36 @@ export class Summary extends React.PureComponent {
{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()}
-
+
+ General
+
+
+
+
+ {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..458c861dbfe05 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,13 @@ import {
openIndices,
editIndexSettings,
refreshIndices,
- openDetailPanel
+ openDetailPanel,
+ performExtensionAction
} from '../../../../store/actions';
import {
- getIndexStatusByIndexName
+ getIndexStatusByIndexName,
+ getIndicesByName
} from '../../../../store/selectors';
const mapStateToProps = (state, ownProps) => {
@@ -29,7 +31,8 @@ const mapStateToProps = (state, ownProps) => {
indexStatusByName[indexName] = getIndexStatusByIndexName(state, indexName);
});
return {
- indexStatusByName
+ indexStatusByName,
+ indices: getIndicesByName(state, indexNames)
};
};
@@ -73,6 +76,9 @@ const mapDispatchToProps = (dispatch, { indexNames }) => {
},
deleteIndices: () => {
dispatch(deleteIndices({ 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..855c6ee7fe302 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,7 @@ import {
} from '@elastic/eui';
import { flattenPanelTree } from '../../../../lib/flatten_panel_tree';
import { INDEX_OPEN } from '../../../../../common/constants';
+import { getActionExtensions } from '../../../../index_management_extensions';
class IndexActionsContextMenuUi extends Component {
constructor(props) {
@@ -46,6 +47,8 @@ class IndexActionsContextMenuUi extends Component {
detailPanel,
indexNames,
indexStatusByName,
+ performExtensionAction,
+ indices,
intl
} = this.props;
const allOpen = all(indexNames, indexName => {
@@ -174,6 +177,22 @@ class IndexActionsContextMenuUi extends Component {
this.openDeleteConfirmationModal();
}
});
+ getActionExtensions().forEach((actionExtension) => {
+ const actionExtensionDefinition = actionExtension(indices);
+ if (actionExtensionDefinition) {
+ const { buttonLabel, requestMethod, successMessage } = actionExtensionDefinition;
+ items.push({
+ name: buttonLabel,
+ icon: ,
+ onClick: () => {
+ this.closePopoverAndExecute(() => performExtensionAction(requestMethod, successMessage));
+ }
+ }
+
+ );
+ }
+ performExtensionAction;
+ });
items.forEach(item => {
item['data-test-subj'] = 'indexTableContextMenuButton';
});
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..4d9c6b5d95862 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,6 +14,8 @@ import { healthToColor } from '../../../../services';
import '../../../../styles/table.less';
import {
+ EuiButtonEmpty,
+ EuiCallOut,
EuiHealth,
EuiLink,
EuiCheckbox,
@@ -35,10 +37,11 @@ import {
EuiTitle,
EuiText,
EuiPageBody,
- EuiPageContent
+ EuiPageContent,
} from '@elastic/eui';
import { IndexActionsContextMenu } from '../../components';
+import { getBannerExtensions } from '../../../../index_management_extensions';
const HEADERS = {
name: i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', {
@@ -62,9 +65,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,7 +90,7 @@ export class IndexTableUi extends Component {
super(props);
this.state = {
- selectedIndicesMap: {}
+ selectedIndicesMap: {},
};
}
@@ -112,7 +112,7 @@ export class IndexTableUi extends Component {
selectedIndicesMap[name] = true;
});
this.setState({
- selectedIndicesMap
+ selectedIndicesMap,
});
};
@@ -125,7 +125,7 @@ export class IndexTableUi extends Component {
newMap[name] = true;
}
return {
- selectedIndicesMap: newMap
+ selectedIndicesMap: newMap,
};
});
};
@@ -136,9 +136,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 +194,43 @@ export class IndexTableUi extends Component {
);
});
}
-
+ renderBanners() {
+ const { indices = [], filterChanged } = this.props;
+ return getBannerExtensions().map(bannerExtension => {
+ const bannerData = bannerExtension(indices);
+ console.log(bannerData);
+ if (!bannerData) {
+ return null;
+ }
+ return (
+
+
+ {bannerData.message} {bannerData.filter ? (
+ {
+ filterChanged(bannerData.filter);
+ }}
+ >
+
+
+ ) : null}
+
+
+
+
+ );
+ });
+ }
buildRows() {
const { indices = [], detailPanelIndexName } = this.props;
return indices.map(index => {
const { name } = index;
return (
@@ -284,14 +309,17 @@ export class IndexTableUi extends Component {
id="checkboxShowSystemIndices"
checked={showSystemIndices}
onChange={event => showSystemIndicesChanged(event.target.checked)}
- label={}
+ label={
+
+ }
/>
+ {this.renderBanners()}
{atLeastOneItemSelected ? (
@@ -316,18 +344,14 @@ export class IndexTableUi extends Component {
filterChanged(event.target.value);
}}
data-test-subj="indexTableFilterInput"
- placeholder={
- intl.formatMessage({
- id: 'xpack.idxMgmt.indexTable.systemIndicesSearchInputPlaceholder',
- defaultMessage: 'Search',
- })
- }
- aria-label={
- intl.formatMessage({
- id: 'xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel',
- defaultMessage: 'Search indices',
- })
- }
+ placeholder={intl.formatMessage({
+ id: 'xpack.idxMgmt.indexTable.systemIndicesSearchInputPlaceholder',
+ defaultMessage: 'Search',
+ })}
+ aria-label={intl.formatMessage({
+ id: 'xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel',
+ defaultMessage: 'Search indices',
+ })}
/>
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..5151f055136c0 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,16 @@
* 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();
+ console.log('N', normalizedField);
+ console.log('N', normalizedField);
return normalizedField.includes(lowerFilter);
});
return indexOfMatch !== -1;
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..fe0d1678c1b35 100644
--- a/x-pack/plugins/index_management/public/store/selectors/index.js
+++ b/x-pack/plugins/index_management/public/store/selectors/index.js
@@ -3,7 +3,6 @@
* 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 { createSelector } from 'reselect';
@@ -16,6 +15,10 @@ 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 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 +30,7 @@ export const getIndexStatusByIndexName = (state, indexName) => {
const { status } = indices[indexName] || {};
return status;
};
+const defaultFilterFields = ['name', 'uuid'];
const getFilteredIndices = createSelector(
getIndices,
getRowStatuses,
@@ -36,7 +40,14 @@ const getFilteredIndices = createSelector(
const systemFilteredIndexes = tableState.showSystemIndices
? indexArray
: indexArray.filter(index => !(index.name + '').startsWith('.'));
- return filterItems(['name', 'uuid'], tableState.filter, systemFilteredIndexes);
+ let filter = tableState.filter;
+ let fields = defaultFilterFields;
+ if (filter.includes(':')) {
+ const splitFilter = filter.split(':');
+ fields = [ splitFilter[0]];
+ filter = splitFilter[1];
+ }
+ return filterItems(fields, filter, systemFilteredIndexes);
}
);
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/routes/api/indices/register_list_route.js b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.js
index cfa93c66f0a7c..add293e76794a 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
@@ -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 { getIndexManagementDataEnrichers } from '../../../../index_management_data';
import { fetchAliases } from './fetch_aliases';
function formatHits(hits, aliases) {
@@ -49,7 +50,12 @@ 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);
+ const dataEnrichers = getIndexManagementDataEnrichers();
+ for (let i = 0; i < dataEnrichers.length; i++) {
+ const dataEnricher = dataEnrichers[i];
+ response = await dataEnricher(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..09b58615a59bb 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 { getIndexManagementDataEnrichers } from '../../../../index_management_data';
import { fetchAliases } from './fetch_aliases';
function getIndexNamesFromPayload(payload) {
@@ -55,7 +56,12 @@ 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);
+ const dataEnrichers = getIndexManagementDataEnrichers();
+ for (let i = 0; i < dataEnrichers.length; i++) {
+ const dataEnricher = dataEnrichers[i];
+ response = await dataEnricher(response, callWithRequest);
+ }
return response;
} catch (err) {
if (isEsError(err)) {