diff --git a/x-pack/index.js b/x-pack/index.js index f20e87de989845..c86338d9569949 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -20,6 +20,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 { notifications } from './plugins/notifications'; @@ -43,5 +44,6 @@ module.exports = function (kibana) { indexManagement(kibana), consoleExtensions(kibana), notifications(kibana), + indexLifecycleManagement(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 00000000000000..14297875d177c1 --- /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_AFTER_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 00000000000000..5eea1d0ead4a47 --- /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 00000000000000..59b61f7b99f989 --- /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 00000000000000..0261f57a93e8c3 --- /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 00000000000000..2824f8e345d50a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/index.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 { 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 { registerIndicesRoutes } from './server/routes/api/indices'; +import { registerLicenseChecker } from './server/lib/register_license_checker'; +import { PLUGIN } from './common/constants'; + +export function indexLifecycleManagement(kibana) { + return new kibana.Plugin({ + id: PLUGIN.ID, + publicDir: resolve(__dirname, 'public'), + require: ['kibana', 'elasticsearch', 'xpack_main'], + uiExports: { + managementSections: [ + 'plugins/index_lifecycle_management', + ] + }, + init: function (server) { + registerLicenseChecker(server); + registerTemplatesRoutes(server); + registerNodesRoutes(server); + registerPoliciesRoutes(server); + registerLifecycleRoutes(server); + registerIndicesRoutes(server); + } + }); +} 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 00000000000000..c771697e6b27ad --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/api/index.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 chrome from 'ui/chrome'; +let httpClient; +export const setHttpClient = (client) => { + httpClient = client; +}; +const apiPrefix = chrome.addBasePath('/api/index_lifecycle_management'); + +export async function loadNodes() { + const response = await httpClient.get(`${apiPrefix}/nodes/list`); + return response.data; +} + +export async function loadNodeDetails(selectedNodeAttrs) { + const response = await httpClient.get(`${apiPrefix}/nodes/${selectedNodeAttrs}/details`); + return response.data; +} + +export async function loadIndexTemplates() { + const response = await httpClient.get(`${apiPrefix}/templates`); + return response.data; +} + +export async function loadIndexTemplate(templateName) { + const response = await httpClient.get(`${apiPrefix}/template/${templateName}`); + return response.data; +} + +export async function loadPolicies() { + const response = await httpClient.get(`${apiPrefix}/policies`); + return response.data; +} + +export async function saveLifecycle(lifecycle, indexTemplatePatch) { + const response = await httpClient.post(`${apiPrefix}/lifecycle`, { lifecycle, indexTemplatePatch }); + return response.data; +} + +export async function bootstrap(indexName, aliasName) { + const response = await httpClient.post(`${apiPrefix}/indices/bootstrap`, { indexName, aliasName }); + return response.data; +} + +export async function getAffectedIndices(indexTemplateName, policyName) { + const path = policyName + ? `${apiPrefix}/indices/affected/${indexTemplateName}/${encodeURIComponent(policyName)}` + : `${apiPrefix}/indices/affected/${indexTemplateName}`; + const response = await httpClient.get(path); + 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 00000000000000..2f5c19a16b684d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/app.js @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + + +import React from 'react'; +import { Landing } from './sections/landing'; + +export const App = () => ( + +); 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 00000000000000..ccde49edbdf5d0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/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. + */ + +import './register_management_section'; +import './register_routes'; 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 00000000000000..392512d93d0c3f --- /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 00000000000000..82c6e6b4d1e394 --- /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 00000000000000..c06835261c7599 --- /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 00000000000000..39d260b05301ea --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/lib/find_errors.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 hasErrors = (object, keysToIgnore = []) => { + let errors = false; + for (const [key, value] of Object.entries(object)) { + if (keysToIgnore.includes(key)) continue; + if (Array.isArray(value) && value.length > 0) { + errors = true; + break; + } else if (value) { + errors = hasErrors(value, keysToIgnore); + if (errors) { + break; + } + } + } + return errors; +}; 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 00000000000000..3813e632a0a738 --- /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 00000000000000..36820f8e159ac5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/main.html @@ -0,0 +1,3 @@ + +
+ diff --git a/x-pack/plugins/index_lifecycle_management/public/register_management_section.js b/x-pack/plugins/index_lifecycle_management/public/register_management_section.js new file mode 100644 index 00000000000000..16c4de6cf32364 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/register_management_section.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 { management } from 'ui/management'; +import { BASE_PATH } from '../common/constants'; + +const esSection = management.getSection('elasticsearch'); +esSection.register('index_lifecycle_management', { + visible: true, + display: 'Index Lifecycle Management', + order: 1, + url: `#${BASE_PATH}home` +}); + 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 00000000000000..c601c63299f316 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/register_routes.js @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 routes from 'ui/routes'; + +import template from './main.html'; +import { manageAngularLifecycle } from './lib/manage_angular_lifecycle'; + +const renderReact = async (elem) => { + render( + + + , + elem + ); +}; + +routes.when(`${BASE_PATH}:view?/:id?`, { + template: template, + controllerAs: 'indexManagement', + controller: class IndexManagementController { + constructor($scope, $route, $http) { + setHttpClient($http); + + $scope.$$postDigest(() => { + const elem = document.getElementById('indexLifecycleManagementReactRoot'); + renderReact(elem); + manageAngularLifecycle($scope, $route, elem); + }); + } + } +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/landing/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/landing/index.js new file mode 100644 index 00000000000000..cc858ed0f20e58 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/landing/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 { Landing } from './landing.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/landing/landing.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/landing/landing.container.js new file mode 100644 index 00000000000000..93e507b00ab7b9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/landing/landing.container.js @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + + +import { connect } from 'react-redux'; +import { Landing as PresentationComponent } from './landing'; +import { fetchIndexTemplates } from '../../store/actions'; +import { + getIndexTemplates, +} from '../../store/selectors'; + +export const Landing = connect( + state => ({ + indexTemplates: getIndexTemplates(state), + }), + { + fetchIndexTemplates + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/landing/landing.js b/x-pack/plugins/index_lifecycle_management/public/sections/landing/landing.js new file mode 100644 index 00000000000000..c9b2366e97e053 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/landing/landing.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 React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Wizard } from '../wizard'; + +export class Landing extends PureComponent { + static propTypes = { + fetchIndexTemplates: PropTypes.func.isRequired, + + indexTemplates: PropTypes.array, + } + + componentWillMount() { + this.props.fetchIndexTemplates(); + } + + render() { + const { indexTemplates } = this.props; + + if (indexTemplates === null) { + // Loading... + return null; + } + + if (indexTemplates.length === 0) { + return ( +

No index templates found.

+ ); + } + + return ( + + ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/configuration.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/configuration.container.js new file mode 100644 index 00000000000000..01ee20f34d5db0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/configuration.container.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Configuration as PresentationComponent } from './configuration'; +import { + getNodeOptions, + getSelectedPrimaryShardCount, + getSelectedReplicaCount, + getSelectedNodeAttrs, + getIsPrimaryShardCountHigherThanSelectedNodeAttrsCount, +} from '../../../../../../store/selectors'; +import { + setSelectedNodeAttrs, + setSelectedPrimaryShardCount, + setSelectedReplicaCount, + fetchNodes +} from '../../../../../../store/actions'; + +export const Configuration = connect( + state => ({ + nodeOptions: getNodeOptions(state), + selectedNodeAttrs: getSelectedNodeAttrs(state), + selectedPrimaryShardCount: getSelectedPrimaryShardCount(state), + selectedReplicaCount: getSelectedReplicaCount(state), + selectedNodeAttrs: getSelectedNodeAttrs(state), + isPrimaryShardCountHigherThanSelectedNodeAttrsCount: getIsPrimaryShardCountHigherThanSelectedNodeAttrsCount(state), + }), + { + setSelectedNodeAttrs, + setSelectedPrimaryShardCount, + setSelectedReplicaCount, + fetchNodes + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/configuration.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/configuration.js new file mode 100644 index 00000000000000..707bf4d9dcad28 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/configuration.js @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 PropTypes from 'prop-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSelect, + EuiFieldNumber, + EuiCallOut, + EuiButtonEmpty, + EuiLink, + EuiDescribedFormGroup, +} from '@elastic/eui'; +import { + STRUCTURE_NODE_ATTRS, + STRUCTURE_PRIMARY_NODES, + STRUCTURE_REPLICAS, +} from '../../../../../../store/constants'; + +import { ErrableFormRow } from '../../../../form_errors'; +import { NodeAttrsDetails } from '../../../node_attrs_details'; + +export class Configuration extends Component { + static propTypes = { + fetchNodes: PropTypes.func.isRequired, + setSelectedNodeAttrs: PropTypes.func.isRequired, + setSelectedPrimaryShardCount: PropTypes.func.isRequired, + setSelectedReplicaCount: PropTypes.func.isRequired, + validate: PropTypes.func.isRequired, + + selectedPrimaryShardCount: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + selectedNodeAttrs: PropTypes.string.isRequired, + nodeOptions: PropTypes.array.isRequired, + selectedReplicaCount: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + isShowingErrors: PropTypes.bool.isRequired, + errors: PropTypes.object.isRequired, + isPrimaryShardCountHigherThanSelectedNodeAttrsCount: + PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + isShowingNodeDetailsFlyout: false, + }; + } + + componentWillMount() { + this.props.fetchNodes(); + } + + render() { + const { + setSelectedNodeAttrs, + setSelectedPrimaryShardCount, + setSelectedReplicaCount, + validate, + + nodeOptions, + selectedPrimaryShardCount, + selectedReplicaCount, + selectedNodeAttrs, + errors, + isShowingErrors, + isPrimaryShardCountHigherThanSelectedNodeAttrsCount, + } = this.props; + + const primaryNodeErrors = isPrimaryShardCountHigherThanSelectedNodeAttrsCount ? ( + + The shard count should be lower than the number of nodes that match the selected attributes. + + ) : null; + + return ( +
+ Configure hot indices} + titleSize="s" + description="A hot index is actively being written to." + fullWidth + > + + this.setState({ isShowingNodeDetailsFlyout: true }) + } + > + View a list of nodes attached to this configuration + + ) : null} + > + { + await setSelectedNodeAttrs(e.target.value); + validate(); + }} + options={nodeOptions} + /> + + + The best way to determine how many shards you need is to benchmark + using realistic data and queries on your hardware.{' '} + + Learn more + +

+ } + /> + + + + + { + await setSelectedPrimaryShardCount(e.target.value); + validate(); + }} + value={selectedPrimaryShardCount} + /> + + + + + { + await setSelectedReplicaCount(e.target.value); + validate(); + }} + value={selectedReplicaCount} + /> + + + + {this.state.isShowingNodeDetailsFlyout ? ( + this.setState({ isShowingNodeDetailsFlyout: false })} + /> + ) : null} + + {primaryNodeErrors} +
+
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/index.js new file mode 100644 index 00000000000000..2a387bd853de8c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/configuration/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 { Configuration } from './configuration.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/index.js new file mode 100644 index 00000000000000..a7c5cfb8f3dab0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/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 { TemplateSelection } from './template_selection.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/template_selection.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/template_selection.container.js new file mode 100644 index 00000000000000..fb11120d37d3ce --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/template_selection.container.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { TemplateSelection as PresentationComponent } from './template_selection'; +import { + getIndexTemplateOptions, + getSelectedIndexTemplateName, + getIndexName, + getAliasName, + getBootstrapEnabled, + getSelectedIndexTemplateIndices, +} from '../../../../../../store/selectors'; +import { + fetchIndexTemplates, + setSelectedIndexTemplate, + setAliasName, + setBootstrapEnabled, + setIndexName +} from '../../../../../../store/actions'; + +export const TemplateSelection = connect( + state => ({ + templateOptions: getIndexTemplateOptions(state), + selectedIndexTemplateName: getSelectedIndexTemplateName(state), + bootstrapEnabled: getBootstrapEnabled(state), + aliasName: getAliasName(state), + indexName: getIndexName(state), + selectedIndexTemplateIndices: getSelectedIndexTemplateIndices(state), + }), + { + fetchIndexTemplates, + setSelectedIndexTemplate, + setBootstrapEnabled, + setIndexName, + setAliasName, + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/template_selection.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/template_selection.js new file mode 100644 index 00000000000000..9614e64414ed56 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/components/template_selection/template_selection.js @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiSelect, + EuiFormRow, + EuiSwitch, + EuiFieldText, + EuiDescribedFormGroup, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; + +import { ErrableFormRow } from '../../../../form_errors'; +import { + STRUCTURE_TEMPLATE_NAME, + STRUCTURE_INDEX_NAME, + STRUCTURE_ALIAS_NAME, +} from '../../../../../../store/constants'; + +export class TemplateSelection extends Component { + static propTypes = { + fetchIndexTemplates: PropTypes.func.isRequired, + setSelectedIndexTemplate: PropTypes.func.isRequired, + validate: PropTypes.func.isRequired, + + selectedIndexTemplateName: PropTypes.string.isRequired, + templateOptions: PropTypes.array.isRequired, + errors: PropTypes.object.isRequired, + isShowingErrors: PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + isIncludingSystemIndices: false, + }; + } + + componentWillMount() { + this.props.fetchIndexTemplates(); + } + + onChangeIncludingSystemIndices = e => { + this.setState({ isIncludingSystemIndices: e.target.checked }); + }; + + render() { + const { + setSelectedIndexTemplate, + validate, + setBootstrapEnabled, + setIndexName, + setAliasName, + + bootstrapEnabled, + selectedIndexTemplateIndices, + indexName, + aliasName, + selectedIndexTemplateName, + errors, + isShowingErrors, + } = this.props; + + const { isIncludingSystemIndices } = this.state; + + const templateOptions = this.props.templateOptions.filter(option => { + if (option.value && option.value.startsWith('.') && !isIncludingSystemIndices) { + return false; + } + return true; + }); + + return ( + Select a template} + fullWidth + titleSize="s" + description={ +

+ An index template defines the settings, mappings, and aliases to apply + when you create an index.{' '} + + Learn more + +

+ } + > + + + + { + await setSelectedIndexTemplate(e.target.value); + validate(); + }} + options={templateOptions} + /> + + {selectedIndexTemplateName && selectedIndexTemplateIndices.length === 0 ? ( + + + setBootstrapEnabled(e.target.checked)} + label={Create an index and alias for this template} + /> + + {bootstrapEnabled ? ( + + + { + await setIndexName(e.target.value); + validate(); + }} + /> + + + { + await setAliasName(e.target.value); + validate(); + }} + /> + + + ) : null} + + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/index.js new file mode 100644 index 00000000000000..20271f5f66ec26 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/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 { IndexTemplate } from './index_template'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/index_template.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/index_template.js new file mode 100644 index 00000000000000..70566e84867dfb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/index_template/index_template.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 React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { toastNotifications } from 'ui/notify'; + +import { TemplateSelection } from './components/template_selection'; +import { Configuration } from './components/configuration'; + +import { + EuiHorizontalRule, + EuiButton, +} from '@elastic/eui'; +import { hasErrors } from '../../../../lib/find_errors'; +import { + STRUCTURE_TEMPLATE_SELECTION, + STRUCTURE_CONFIGURATION, +} from '../../../../store/constants'; + +export class IndexTemplate extends Component { + static propTypes = { + done: PropTypes.func.isRequired, + + errors: PropTypes.object, + }; + + constructor(props) { + super(props); + this.state = { + isShowingErrors: false, + }; + } + + validate = async () => { + await this.props.validate(); + const noErrors = !hasErrors(this.props.errors); + return noErrors; + }; + + submit = async () => { + this.setState({ isShowingErrors: true }); + if (await this.validate()) { + this.props.done(); + } else { + toastNotifications.addDanger('Please fix the errors on the page'); + } + }; + + render() { + const { errors } = this.props; + const { isShowingErrors } = this.state; + + return ( +
+ + + + + + + Continue + +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/node_attrs_details/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/node_attrs_details/index.js new file mode 100644 index 00000000000000..885e965c46c4bf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/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/wizard/components/node_attrs_details/node_attrs_details.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/node_attrs_details/node_attrs_details.container.js new file mode 100644 index 00000000000000..3128a38c2c34fd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/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/wizard/components/node_attrs_details/node_attrs_details.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/node_attrs_details/node_attrs_details.js new file mode 100644 index 00000000000000..7cb2977e706ab8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/node_attrs_details/node_attrs_details.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 React, { Fragment, PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyout, + EuiTitle, + EuiInMemoryTable, + EuiSpacer, + EuiButtonEmpty, + EuiCallOut, + EuiPortal, +} from '@elastic/eui'; + +export class NodeAttrsDetails 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 } = this.props; + + return ( + + + + +

Nodes that contain the attribute: `{selectedNodeAttrs}`

+
+ + {allocationRules ? ( + + + Be aware that this index template has existing allocation rules + which will affect the list of nodes these indices can be allocated to. + + + + ) : null} + +
+ + + Close + + +
+
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/cold_phase/cold_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/cold_phase/cold_phase.container.js new file mode 100644 index 00000000000000..bfeb21f8d32f14 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/cold_phase/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/cold_phase/cold_phase.js new file mode 100644 index 00000000000000..dc55d36b4d067f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/cold_phase/cold_phase.js @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTextColor, + EuiFormRow, + EuiFieldNumber, + EuiSelect, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiBetaBadge, + EuiButton, +} from '@elastic/eui'; +import { + PHASE_ENABLED, + PHASE_ROLLOVER_ALIAS, + PHASE_ROLLOVER_AFTER, + PHASE_ROLLOVER_AFTER_UNITS, + PHASE_NODE_ATTRS, + PHASE_REPLICA_COUNT +} from '../../../../../../store/constants'; +import { ErrableFormRow } from '../../../../form_errors'; + +export class ColdPhase extends PureComponent { + static propTypes = { + setPhaseData: PropTypes.func.isRequired, + validate: 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_AFTER]: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string + ]).isRequired, + [PHASE_ROLLOVER_AFTER_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, + validate, + showNodeDetailsFlyout, + + phaseData, + nodeOptions, + warmPhaseReplicaCount, + errors, + isShowingErrors + } = this.props; + + return ( + + Cold phase{' '} + {phaseData[PHASE_ENABLED] ? ( + + ) : null} +
+ } + titleSize="s" + description={ + +

+ Your index is queried less frequently + and no longer needs to be on the most performant hardware. +

+ {isShowingErrors ? ( + + +

This phase contains errors

+
+
+ ) : null} +
+ } + fullWidth + > + {phaseData[PHASE_ENABLED] ? ( + + +
+ + { + await setPhaseData(PHASE_ENABLED, false); + validate(); + }} + > + Deactive cold phase + +
+ + + + + + { + setPhaseData(PHASE_ROLLOVER_AFTER, e.target.value); + validate(); + }} + /> + + + + + + setPhaseData(PHASE_ROLLOVER_AFTER_UNITS, e.target.value) + } + options={[ + { value: 'd', text: 'days' }, + { value: 'h', text: 'hours' } + ]} + /> + + + + + + + showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])} + > + View a list of nodes attached to this configuration + + ) : null} + > + { + await setPhaseData(PHASE_NODE_ATTRS, e.target.value); + validate(); + }} + /> + + + + + + { + await setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + validate(); + }} + /> + + + + + + setPhaseData(PHASE_REPLICA_COUNT, warmPhaseReplicaCount) + } + > + Set to same as warm phase + + + + +
+ ) : ( +
+ + { + await setPhaseData(PHASE_ENABLED, true); + validate(); + }} + > + Activate cold phase + +
+ )} + + ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/cold_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/cold_phase/index.js new file mode 100644 index 00000000000000..e0d70ceb57726d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/delete_phase/delete_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/delete_phase/delete_phase.container.js new file mode 100644 index 00000000000000..661489d2d9aa97 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/delete_phase/delete_phase.js new file mode 100644 index 00000000000000..8d9e179dc99fa7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/delete_phase/delete_phase.js @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiText, + EuiTextColor, + EuiFormRow, + EuiFieldNumber, + EuiSelect, + EuiDescribedFormGroup, + EuiBetaBadge, + EuiButton, +} from '@elastic/eui'; +import { + PHASE_ENABLED, + PHASE_ROLLOVER_AFTER, + PHASE_ROLLOVER_AFTER_UNITS, +} from '../../../../../../store/constants'; +import { ErrableFormRow } from '../../../../form_errors'; + +export class DeletePhase extends PureComponent { + static propTypes = { + setPhaseData: PropTypes.func.isRequired, + validate: PropTypes.func.isRequired, + + isShowingErrors: PropTypes.bool.isRequired, + errors: PropTypes.object.isRequired, + phaseData: PropTypes.shape({ + [PHASE_ENABLED]: PropTypes.bool.isRequired, + [PHASE_ROLLOVER_AFTER]: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string + ]).isRequired, + [PHASE_ROLLOVER_AFTER_UNITS]: PropTypes.string.isRequired + }).isRequired + }; + + render() { + const { + setPhaseData, + validate, + phaseData, + errors, + isShowingErrors + } = this.props; + + return ( + + Delete phase{' '} + {phaseData[PHASE_ENABLED] ? ( + + ) : null} + + } + titleSize="s" + description={ + +

+ Use this phase to define how long to retain your data. +

+ {isShowingErrors ? ( + + +

This phase contains errors

+
+
+ ) : null} +
+ } + fullWidth + > + {phaseData[PHASE_ENABLED] ? ( + + +
+ + { + await setPhaseData(PHASE_ENABLED, false); + validate(); + }} + > + Deactive cold phase + +
+ + + +

Configuration

+
+ + + + + { + setPhaseData(PHASE_ROLLOVER_AFTER, e.target.value); + validate(); + }} + /> + + + + + + setPhaseData(PHASE_ROLLOVER_AFTER_UNITS, e.target.value) + } + options={[ + { value: 'd', text: 'days' }, + { value: 'h', text: 'hours' } + ]} + /> + + + +
+ ) : ( +
+ + { + await setPhaseData(PHASE_ENABLED, true); + validate(); + }} + > + Activate delete phase + +
+ )} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/delete_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/delete_phase/index.js new file mode 100644 index 00000000000000..5f909ab2c0f793 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/hot_phase/hot_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/hot_phase/hot_phase.container.js new file mode 100644 index 00000000000000..1db810b49c19cd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/hot_phase/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/hot_phase/hot_phase.js new file mode 100644 index 00000000000000..2510e264f93955 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/hot_phase/hot_phase.js @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTextColor, + EuiFieldNumber, + EuiSelect, + EuiSwitch, + EuiLink, + EuiFormRow, + EuiDescribedFormGroup, + EuiBetaBadge, +} from '@elastic/eui'; +import { + 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'; + +export class HotPhase extends PureComponent { + static propTypes = { + setPhaseData: PropTypes.func.isRequired, + validate: 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 { + validate, + setPhaseData, + + phaseData, + isShowingErrors, + errors, + } = this.props; + + return ( + + Hot phase{' '} + + + } + titleSize="s" + description={ + +

+ This phase is required. Your index is being queried and actively written to. + You can optimize this phase for write throughput. +

+ {isShowingErrors ? ( + + +

This phase contains errors

+
+
+ ) : null} +
+ } + fullWidth + > + + If true, rollover the index when it gets too big or too old. The alias switches to the new index.{' '} + + Learn more + +

+ } + > + { + await setPhaseData(PHASE_ROLLOVER_ENABLED, e.target.checked); + validate(); + }} + label="Enable rollover" + /> +
+ {phaseData[PHASE_ROLLOVER_ENABLED] ? ( + + + + + + { + await setPhaseData( + PHASE_ROLLOVER_MAX_SIZE_STORED, + e.target.value + ); + validate(); + }} + /> + + + + + { + await setPhaseData( + PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, + e.target.value + ); + validate(); + }} + options={[ + { value: 'gb', text: 'gigabytes' }, + { value: MAX_SIZE_TYPE_DOCUMENT, text: 'documents' } + ]} + /> + + + + + + + + { + await setPhaseData(PHASE_ROLLOVER_MAX_AGE, e.target.value); + validate(); + }} + /> + + + + + { + await setPhaseData( + PHASE_ROLLOVER_MAX_AGE_UNITS, + e.target.value + ); + validate(); + }} + options={[ + { value: 'd', text: 'days' }, + { value: 'h', text: 'hours' } + ]} + /> + + + + + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/hot_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/hot_phase/index.js new file mode 100644 index 00000000000000..114e34c3ef4d02 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/warm_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/warm_phase/index.js new file mode 100644 index 00000000000000..7eb5def486c871 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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/wizard/components/policy_configuration/components/warm_phase/warm_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/warm_phase/warm_phase.container.js new file mode 100644 index 00000000000000..b224e9b2327694 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/warm_phase/warm_phase.container.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, + getSelectedReplicaCount, + getSelectedPrimaryShardCount +} 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), + hotPhaseReplicaCount: getSelectedReplicaCount(state), + hotPhasePrimaryShardCount: getSelectedPrimaryShardCount(state), + 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/wizard/components/policy_configuration/components/warm_phase/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/warm_phase/warm_phase.js new file mode 100644 index 00000000000000..a31f58ec303527 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/components/warm_phase/warm_phase.js @@ -0,0 +1,403 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiText, + EuiTextColor, + EuiFormRow, + EuiFieldNumber, + EuiSelect, + EuiSwitch, + EuiButtonEmpty, + EuiLink, + EuiDescribedFormGroup, + EuiBetaBadge, + EuiButton, +} from '@elastic/eui'; +import { + PHASE_ENABLED, + PHASE_ROLLOVER_ENABLED, + PHASE_ROLLOVER_ALIAS, + PHASE_FORCE_MERGE_ENABLED, + PHASE_FORCE_MERGE_SEGMENTS, + PHASE_NODE_ATTRS, + PHASE_PRIMARY_SHARD_COUNT, + PHASE_REPLICA_COUNT, + PHASE_ROLLOVER_AFTER, + PHASE_ROLLOVER_AFTER_UNITS, + PHASE_SHRINK_ENABLED, +} from '../../../../../../store/constants'; +import { ErrableFormRow } from '../../../../form_errors'; + +export class WarmPhase extends PureComponent { + static propTypes = { + setPhaseData: PropTypes.func.isRequired, + validate: 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_ENABLED]: 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_AFTER]: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + [PHASE_ROLLOVER_AFTER_UNITS]: PropTypes.string.isRequired, + }).isRequired, + + hotPhaseReplicaCount: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + hotPhasePrimaryShardCount: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + + nodeOptions: PropTypes.array.isRequired, + }; + + componentWillMount() { + this.props.fetchNodes(); + } + + render() { + const { + validate, + setPhaseData, + showNodeDetailsFlyout, + + phaseData, + hotPhaseReplicaCount, + hotPhasePrimaryShardCount, + nodeOptions, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, + } = this.props; + + return ( + + Warm phase{' '} + {phaseData[PHASE_ENABLED] ? ( + + ) : null} + + } + titleSize="s" + description={ + +

+ Your index becomes read-only when it enters the warm phase. + You can optimize this phase for search. +

+ {isShowingErrors ? ( + + +

This phase contains errors

+
+
+ ) : null} +
+ } + fullWidth + > + + + + {phaseData[PHASE_ENABLED] ? ( + + +
+ { + await setPhaseData(PHASE_ENABLED, false); + validate(); + }} + > + Remove warm phase + +
+
+ {hotPhaseRolloverEnabled ? ( + + { + await setPhaseData(PHASE_ROLLOVER_ENABLED, e.target.checked); + validate(); + }} + /> + + ) : null} + {!phaseData[PHASE_ROLLOVER_ENABLED] ? ( + + + + { + setPhaseData(PHASE_ROLLOVER_AFTER, e.target.value); + validate(); + }} + /> + + + + + { + await setPhaseData( + PHASE_ROLLOVER_AFTER_UNITS, + e.target.value + ); + validate(); + }} + options={[ + { value: 'd', text: 'days' }, + { value: 'h', text: 'hours' }, + ]} + /> + + + + ) : null} + + + + + showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS]) + } + > + View node details + + ) : null + } + > + { + await setPhaseData(PHASE_NODE_ATTRS, e.target.value); + validate(); + }} + /> + + + + + + { + await setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + validate(); + }} + /> + + + + + { + await setPhaseData( + PHASE_REPLICA_COUNT, + hotPhaseReplicaCount + ); + validate(); + }} + > + Set to same as hot phase + + + + + + + + +

Shrink

+
+ + + Shrink the index into a new index with fewer primary shards.{' '} + + Learn more + + + + + + + { + await setPhaseData(PHASE_SHRINK_ENABLED, e.target.checked); + validate(); + }} + label="Shrink index" + /> + + + + {phaseData[PHASE_SHRINK_ENABLED] ? ( + + + + + { + await setPhaseData( + PHASE_PRIMARY_SHARD_COUNT, + e.target.value + ); + validate(); + }} + /> + + + + + { + await setPhaseData( + PHASE_PRIMARY_SHARD_COUNT, + hotPhasePrimaryShardCount + ); + validate(); + }} + > + Set to same as hot phase + + + + + + + + ) : null} + + +

Force merge

+
+ + + Reduce the number of segments in your shard by merging smaller + files and clearing deleted ones.{' '} + + Learn more + + + + + + + { + await setPhaseData(PHASE_FORCE_MERGE_ENABLED, e.target.checked); + validate(); + }} + /> + + + + {phaseData[PHASE_FORCE_MERGE_ENABLED] ? ( + + { + await setPhaseData( + PHASE_FORCE_MERGE_SEGMENTS, + e.target.value + ); + validate(); + }} + /> + + ) : null} +
+ ) : ( + +
+ { + await setPhaseData(PHASE_ENABLED, true); + validate(); + }} + > + Activate warm phase + +
+
+ )} +
+
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/index.js new file mode 100644 index 00000000000000..d8a885b0b32f3d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/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 { PolicyConfiguration } from './policy_configuration.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/policy_configuration.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/policy_configuration.container.js new file mode 100644 index 00000000000000..29da1a719d9b56 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/policy_configuration.container.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 { connect } from 'react-redux'; +import { PolicyConfiguration as PresentationComponent } from './policy_configuration'; +import { + getSelectedPolicyName, + getAffectedIndexTemplates, + getSelectedIndexTemplateName, + getBootstrapEnabled, + getIndexName, + getAliasName, + getSaveAsNewPolicy, + getSelectedOriginalPolicyName, + getIsSelectedPolicySet +} from '../../../../store/selectors'; +import { + setBootstrapEnabled, + setIndexName, + setAliasName, + setSelectedPolicyName, + setSaveAsNewPolicy +} from '../../../../store/actions'; + +export const PolicyConfiguration = connect( + state => ({ + isSelectedPolicySet: getIsSelectedPolicySet(state), + selectedPolicyName: getSelectedPolicyName(state), + selectedIndexTemplateName: getSelectedIndexTemplateName(state), + affectedIndexTemplates: getAffectedIndexTemplates(state), + bootstrapEnabled: getBootstrapEnabled(state), + indexName: getIndexName(state), + aliasName: getAliasName(state), + saveAsNewPolicy: getSaveAsNewPolicy(state), + originalPolicyName: getSelectedOriginalPolicyName(state) + }), + { + setBootstrapEnabled, + setIndexName, + setAliasName, + setSelectedPolicyName, + setSaveAsNewPolicy + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/policy_configuration.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/policy_configuration.js new file mode 100644 index 00000000000000..a2ed77bd87f3b8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_configuration/policy_configuration.js @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 PropTypes from 'prop-types'; +import { toastNotifications } from 'ui/notify'; + +import { + EuiTitle, + EuiText, + EuiSpacer, + 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, +} from '../../../../store/constants'; +import { hasErrors } from '../../../../lib/find_errors'; +import { NodeAttrsDetails } from '../node_attrs_details'; +import { PolicySelection } from '../policy_selection/policy_selection.container'; + +export class PolicyConfiguration extends Component { + static propTypes = { + setSelectedPolicyName: PropTypes.func.isRequired, + setSaveAsNewPolicy: PropTypes.func.isRequired, + setIndexName: PropTypes.func.isRequired, + setAliasName: PropTypes.func.isRequired, + setBootstrapEnabled: PropTypes.func.isRequired, + done: PropTypes.func.isRequired, + back: PropTypes.func.isRequired, + validate: PropTypes.func.isRequired, + + affectedIndexTemplates: PropTypes.array.isRequired, + selectedIndexTemplateName: PropTypes.string.isRequired, + selectedPolicyName: PropTypes.string.isRequired, + saveAsNewPolicy: PropTypes.bool.isRequired, + errors: PropTypes.object.isRequired, + bootstrapEnabled: PropTypes.bool.isRequired, + indexName: PropTypes.string.isRequired, + aliasName: PropTypes.string.isRequired, + originalPolicyName: PropTypes.string, + }; + + constructor(props) { + super(props); + this.state = { + isShowingErrors: false, + isShowingNodeDetailsFlyout: false, + selectedNodeAttrsForDetails: undefined, + }; + } + + validate = async () => { + await this.props.validate(); + const noErrors = !hasErrors(this.props.errors); + return noErrors; + }; + + submit = async () => { + this.setState({ isShowingErrors: true }); + if (await this.validate()) { + this.props.done(); + } else { + toastNotifications.addDanger('Please the fix errors on the page'); + } + }; + + showNodeDetailsFlyout = selectedNodeAttrsForDetails => { + this.setState({ isShowingNodeDetailsFlyout: true, selectedNodeAttrsForDetails }); + } + + render() { + const { + back, + + selectedPolicyName, + isSelectedPolicySet, + errors, + } = this.props; + + const { isShowingErrors } = this.state; + + if (!isSelectedPolicySet) { + return ( + + ); + } + + return ( +
+ + + +

+ {!selectedPolicyName ? 'Create a policy' : `Edit policy ${selectedPolicyName}`} +

+
+ + +

Configure the phases of your data and when to transition between them.

+
+ + + + + + + + + + + + Back + +    + + Continue + + + {this.state.isShowingNodeDetailsFlyout ? ( + this.setState({ isShowingNodeDetailsFlyout: false })} + /> + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/index.js new file mode 100644 index 00000000000000..447bc384c368d4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/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 { PolicySelection } from './policy_selection.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/policy_selection.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/policy_selection.container.js new file mode 100644 index 00000000000000..618cd3a128bbcb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/policy_selection.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 { PolicySelection as PresentationComponent } from './policy_selection'; +import { + getPolicies, getSelectedPolicyName, +} from '../../../../store/selectors'; +import { + fetchPolicies, + setSelectedPolicy, +} from '../../../../store/actions'; + +export const PolicySelection = connect( + state => ({ + policies: getPolicies(state), + selectedPolicyName: getSelectedPolicyName(state), + }), + { + fetchPolicies, + setSelectedPolicy, + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/policy_selection.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/policy_selection.js new file mode 100644 index 00000000000000..97ab62ddb4c82e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/policy_selection/policy_selection.js @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiDescribedFormGroup, + EuiFormRow, + EuiSelect, +} from '@elastic/eui'; + +export class PolicySelection extends Component { + static propTypes = { + fetchPolicies: PropTypes.func.isRequired, + setSelectedPolicy: PropTypes.func.isRequired, + + selectedPolicyName: PropTypes.string.isRequired, + policies: PropTypes.array.isRequired + }; + + componentWillMount() { + this.props.fetchPolicies(); + } + + selectPolicy(policyName) { + const policy = this.props.policies.find(policy => policy.name === policyName); + this.props.setSelectedPolicy(policy); + } + + render() { + const { policies, selectedPolicyName } = this.props; + + const options = policies.map(item => ({ value: item.name, text: item.name })); + options.unshift({ + value: '', + text: '-- New Policy --' + }); + + return ( + Select or create a policy} + titleSize="s" + description="An index lifecycle policy is a + blueprint for transitioning your data over time. + You can create a new policy or edit an existing + policy and save it with a new name." + fullWidth + > + + + + { + await this.selectPolicy(e.target.value); + }} + /> + + + + + this.selectPolicy()} + > + Create new policy + + + + + + ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/diff_view.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/diff_view.js new file mode 100644 index 00000000000000..a89f7e2412c596 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/diff_view.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 React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiCodeEditor, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiToolTip, + EuiCode, +} from '@elastic/eui'; +import ace from 'brace'; +import 'brace/mode/json'; +import { + mergeAndPreserveDuplicateKeys, + removePrefixes, +} from '../../../../lib/diff_tools'; +import { addDiffAddonsForAce, setCurrentJsonObject } from '../../../../lib/diff_ace_addons'; + +export class DiffView extends PureComponent { + static propTypes = { + templateDiff: PropTypes.shape({ + originalFullIndexTemplate: PropTypes.object.isRequired, + newFullIndexTemplate: PropTypes.object.isRequired, + }).isRequired, + }; + + scrollToKey = (key, value) => { + const editorDom = this.aceEditor.aceEditor.refEditor; + const editor = ace.edit(editorDom); + const escapedValue = value.replace(/\^/g, '\\^'); + const range = editor.find(new RegExp(`"${key}"\\s*:\\s*"*(${escapedValue})"*`), { regex: true }); + if (!range) { + return; + } + editor.gotoLine(range.start.row + 1, range.start.column); + } + + render() { + const { + templateDiff: { originalFullIndexTemplate, newFullIndexTemplate }, + } = this.props; + + const { result: mergedJson, changes } = mergeAndPreserveDuplicateKeys( + originalFullIndexTemplate, + newFullIndexTemplate + ); + + // Strip the ^ and $ characters + const mergedJsonAsString = removePrefixes( + JSON.stringify(mergedJson, null, 2) + ); + + setCurrentJsonObject(mergedJson); + addDiffAddonsForAce(); + + return ( + + +
    + {changes.map(({ key, original, updated }) => ( +
  • + + Changing the value of {key} from {JSON.stringify(original)} + to {JSON.stringify(updated)} + + ) : ( + Setting a value of {JSON.stringify(updated)} for {key} + )} + > + this.scrollToKey(key, updated)}> + {key} + + +
  • + ))} +
+
+ + (this.aceEditor = aceEditor)} + mode="diff_json" + theme="github" + width="100%" + value={mergedJsonAsString} + setOptions={{ + useWorker: false, + readOnly: true, + }} + editorProps={{ + $blockScrolling: Infinity, + }} + /> + +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/index.js new file mode 100644 index 00000000000000..defebab0131ea8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/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 { Review } from './review.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.container.js new file mode 100644 index 00000000000000..dbc2fcae7f535f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.container.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Review as PresentationComponent } from './review'; +import { + getSelectedIndexTemplateName, + getAffectedIndexTemplates, + getTemplateDiff, + getLifecycle, + getSelectedPolicyName, + getSaveAsNewPolicy, + getSelectedOriginalPolicyName, + getAliasName, + getBootstrapEnabled, + getIndexName, + getPolicies, +} from '../../../../store/selectors'; +import { + setSelectedPolicyName, + setSaveAsNewPolicy, + setAliasName, + setIndexName, + setBootstrapEnabled, +} from '../../../../store/actions'; + +export const Review = connect( + state => ({ + selectedIndexTemplateName: getSelectedIndexTemplateName(state), + affectedIndexTemplates: getAffectedIndexTemplates(state), + policies: getPolicies(state), + templateDiff: getTemplateDiff(state), + lifecycle: getLifecycle(state), + bootstrapEnabled: getBootstrapEnabled(state), + aliasName: getAliasName(state), + selectedPolicyName: getSelectedPolicyName(state), + saveAsNewPolicy: getSaveAsNewPolicy(state), + originalPolicyName: getSelectedOriginalPolicyName(state), + + /* start might go away */ + indexName: getIndexName(state), + /* end might go away */ + }), + { + setSelectedPolicyName, + setSaveAsNewPolicy, + setBootstrapEnabled, + setIndexName, + setAliasName, + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.js new file mode 100644 index 00000000000000..4119c6ef1a0eee --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.js @@ -0,0 +1,279 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, Component } from 'react'; +import PropTypes from 'prop-types'; +import { toastNotifications } from 'ui/notify'; + +import './review.less'; + +import 'brace/theme/github'; +import 'brace/mode/json'; +import 'brace/snippets/json'; +import 'brace/ext/language_tools'; + +import { + EuiTitle, + EuiCode, + EuiCallOut, + EuiSpacer, + EuiHorizontalRule, + EuiButton, + EuiFlexItem, + EuiText, + EuiButtonEmpty, + EuiFormRow, + EuiSwitch, + EuiFieldText, + EuiLoadingSpinner, + EuiFlexGroup, +} from '@elastic/eui'; +import { getAffectedIndices } from '../../../../api'; +import { DiffView } from './diff_view'; +import { ErrableFormRow } from '../../form_errors'; +import { STRUCTURE_POLICY_NAME } from '../../../../store/constants'; +import { hasErrors } from '../../../../lib/find_errors'; + +export class Review extends Component { + static propTypes = { + setSelectedPolicyName: PropTypes.func.isRequired, + setSaveAsNewPolicy: PropTypes.func.isRequired, + done: PropTypes.func.isRequired, + back: PropTypes.func.isRequired, + + selectedIndexTemplateName: PropTypes.string.isRequired, + affectedIndexTemplates: PropTypes.array.isRequired, + templateDiff: PropTypes.object.isRequired, + lifecycle: PropTypes.object.isRequired, + selectedPolicyName: PropTypes.string.isRequired, + saveAsNewPolicy: PropTypes.bool.isRequired, + originalPolicyName: PropTypes.string, + bootstrapEnabled: PropTypes.bool.isRequired, + aliasName: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + selectedTab: 1, + affectedIndices: [], + isLoadingAffectedIndices: false, + }; + + this.currentFetchTimeout = null; + } + + fetchAffectedIndices = () => { + if (this.currentFetchTimeout) { + clearTimeout(this.currentFetchTimeout); + } + + this.setState({ isLoadingAffectedIndices: true }); + this.currentFetchTimeout = setTimeout(async () => { + const affectedIndices = await getAffectedIndices( + this.props.selectedIndexTemplateName, + this.props.selectedPolicyName + ); + this.setState({ affectedIndices, isLoadingAffectedIndices: false }); + }, 1000); + } + + async componentWillMount() { + this.fetchAffectedIndices(); + } + + async componentWillReceiveProps(nextProps) { + if (nextProps.selectedPolicyName !== this.props.selectedPolicyName) { + this.fetchAffectedIndices(); + } + } + + validate = async () => { + await this.props.validate(); + const noErrors = !hasErrors(this.props.errors); + return noErrors; + }; + + submit = async () => { + this.setState({ isShowingErrors: true }); + if (await this.validate()) { + this.props.done(); + } else { + toastNotifications.addDanger('Please fix the errors on the page'); + } + }; + + render() { + const { + done, + back, + setSelectedPolicyName, + setSaveAsNewPolicy, + validate, + + errors, + selectedPolicyName, + saveAsNewPolicy, + originalPolicyName, + selectedIndexTemplateName, + affectedIndexTemplates, + templateDiff, + lifecycle, + bootstrapEnabled, + aliasName, + policies, + } = this.props; + + const { affectedIndices, isLoadingAffectedIndices, isShowingErrors } = this.state; + + const showSaveChangedMessage = (originalPolicyName && !saveAsNewPolicy) + || (saveAsNewPolicy && !!policies.find(policy => policy.name === selectedPolicyName)); + + return ( +
+ + +

Review your policy changes

+
+ +

When you save a policy, your changes go into effect immediately.

+
+ + + + +

{`${affectedIndexTemplates.length} Index ${affectedIndexTemplates.length === 1 ? 'template' : 'templates'}`}

+
    + {affectedIndexTemplates.map(template => ( +
  • {template}
  • + ))} +
+

{`${affectedIndices.length} ${affectedIndices.length === 1 ? 'Index' : 'Indices' }`}

+ { isLoadingAffectedIndices ? ( + + ) : ( +
    + {affectedIndices.map(index =>
  • {index}
  • )} +
+ ) } +
+ + {bootstrapEnabled ? ( + + + +

You decided to bootstrap a new index. Point to this new alias going forward.

+

{aliasName} is your new alias

+
+
+ ) : null} + + + {templateDiff.hasChanged ? ( + + +

+ {selectedIndexTemplateName} template changes +

+
+ + + +
+ ) : null} + + + + {originalPolicyName ? ( + + { showSaveChangedMessage ? ( + + +

Save changes to {selectedPolicyName} policy?

+
+ +

+ You are editing an existing policy. Any changes you make + will also change index templates that this policy is attached to. Alternately, you can save + these changes in a new policy and only change the template you + selected. +

+
+ +
+ ) : null } + + { + await setSaveAsNewPolicy(e.target.checked); + validate(); + }} + label={ + + Save this as a new policy + + } + /> + +
+ ) : null} + {saveAsNewPolicy ? ( + + +

Save your work

+
+ + + { + await setSelectedPolicyName(e.target.value); + validate(); + }} + /> + +
+ ) : null} +
+ + + + + Back + + + + + done(lifecycle)} + > + Looks good, save changes + + + +    +
+ ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.less b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.less new file mode 100644 index 00000000000000..e826e78a7bf7e0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/components/review/review.less @@ -0,0 +1,7 @@ +.ace_diff_addition { + background-color: #e6ffed; +} + +.ace_diff_removal { + background-color: #ffeef0; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/form_errors.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/form_errors.js new file mode 100644 index 00000000000000..fc3c29c4beb0c5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/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/wizard/index.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/index.js new file mode 100644 index 00000000000000..3a6f61a7c04927 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/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 { Wizard } from './wizard.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/wizard.container.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/wizard.container.js new file mode 100644 index 00000000000000..2b76624165a782 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/wizard.container.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 '../../less/main.less'; +import { Wizard as PresentationComponent } from './wizard'; +import { saveLifecycle } from '../../store/actions'; +import { + getIndexTemplatePatch, + getBootstrapEnabled, + getIndexName, + getAliasName, + validateLifecycle, +} from '../../store/selectors'; + +export const Wizard = connect( + state => ({ + indexTemplatePatch: getIndexTemplatePatch(state), + bootstrapEnabled: getBootstrapEnabled(state), + indexName: getIndexName(state), + aliasName: getAliasName(state), + validateLifecycle: () => validateLifecycle(state), + }), + { + saveLifecycle + } +)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/sections/wizard/wizard.js b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/wizard.js new file mode 100644 index 00000000000000..ab92bc815bd2f0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/sections/wizard/wizard.js @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 PropTypes from 'prop-types'; +import { toastNotifications } from 'ui/notify'; +import { IndexTemplate } from './components/index_template'; +import { PolicyConfiguration } from './components/policy_configuration'; +import { Review } from './components/review'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiSpacer, + EuiStepsHorizontal, +} from '@elastic/eui'; +import { bootstrap } from '../../api'; +import { + STRUCTURE_INDEX_TEMPLATE, + STRUCTURE_POLICY_CONFIGURATION, + STRUCTURE_REVIEW, +} from '../../store/constants'; + +export class Wizard extends Component { + static propTypes = { + saveLifecycle: PropTypes.func.isRequired, + validateLifecycle: PropTypes.func.isRequired, + + indexTemplatePatch: PropTypes.object.isRequired, + bootstrapEnabled: PropTypes.bool.isRequired, + indexName: PropTypes.string.isRequired, + aliasName: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + selectedStep: 1, + errors: this.getErrors(), + }; + } + + onSelectedStepChanged = selectedStep => { + this.setState({ + selectedStep, + }); + }; + + getErrors = () => { + return this.props.validateLifecycle(); + }; + + validate = () => { + const errors = this.getErrors(); + this.setState({ errors }); + }; + + addLifecycle = async lifecycle => { + await this.props.saveLifecycle(lifecycle, this.props.indexTemplatePatch); + await this.bootstrap(); + // this.onSelectedStepChanged(5); + }; + + bootstrap = async () => { + const { indexName, aliasName, bootstrapEnabled } = this.props; + if (!bootstrapEnabled) { + return; + } + + const response = await bootstrap(indexName, aliasName); + if (response && response.acknowledged) { + toastNotifications.addSuccess( + 'Successfully bootstrapped an index and alias' + ); + // Bounce back to management + // this.onSelectedStepChanged(1); + // TODO: also clear state? + } else { + toastNotifications.addDanger('Unable to bootstrap an index and alias'); + } + }; + + renderContent() { + const { selectedStep, errors } = this.state; + + switch (selectedStep) { + case 1: + return ( + this.onSelectedStepChanged(2)} + /> + ); + case 2: + return ( + this.onSelectedStepChanged(3)} + back={() => this.onSelectedStepChanged(1)} + /> + ); + case 3: + return ( + this.onSelectedStepChanged(2)} + /> + ); + } + } + + render() { + const steps = [ + { + title: 'Select a template', + isSelected: this.state.selectedStep === 1, + isComplete: this.state.selectedStep > 1, + onClick: () => this.onSelectedStepChanged(1), + }, + { + title: 'Configure a policy', + isSelected: this.state.selectedStep === 2, + isComplete: this.state.selectedStep > 2, + disabled: this.state.selectedStep < 2, + onClick: () => this.onSelectedStepChanged(2), + }, + { + title: 'Review and save', + isSelected: this.state.selectedStep === 3, + isComplete: this.state.selectedStep > 3, + disabled: this.state.selectedStep < 3, + onClick: () => this.onSelectedStepChanged(3), + }, + ]; + + return ( + + + + +

Index lifecycle management

+
+ + + + + {this.renderContent()} +
+
+
+ ); + } +} 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 00000000000000..584488d4c2b420 --- /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 00000000000000..621cbf007d3b22 --- /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 00000000000000..c7bcd3518d0369 --- /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 00000000000000..676894d0790932 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/lifecycle.js @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { saveLifecycle as saveLifecycleApi } from '../../api'; + + +export const savedLifecycle = createAction('SAVED_LIFECYCLE'); +export const saveLifecycle = (lifecycle, indexTemplatePatch) => async dispatch => { + let saved; + try { + saved = await saveLifecycleApi(lifecycle, indexTemplatePatch); + } + catch (err) { + return toastNotifications.addDanger(err.data.message); + } + + toastNotifications.addSuccess(`Successfully created lifecycle policy '${lifecycle.name}'`); + + dispatch(savedLifecycle(saved)); +}; 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 00000000000000..e357b6d6587f32 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/nodes.js @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { toastNotifications } from 'ui/notify'; +import { loadNodes, loadNodeDetails } from '../../api'; + +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 00000000000000..7d420d6e43b201 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/actions/policies.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 { createAction } from 'redux-actions'; +import { toastNotifications } from 'ui/notify'; +import { loadPolicies } from '../../api'; + +export const fetchedPolicies = createAction('FETCHED_POLICIES'); +export const fetchPolicies = () => async dispatch => { + let policies; + try { + policies = await loadPolicies(); + } + catch (err) { + return toastNotifications.addDanger(err.data.message); + } + + dispatch(fetchedPolicies(policies)); + return policies; +}; + +export const setSelectedPolicy = createAction('SET_SELECTED_POLICY'); +export const setSelectedPolicyName = createAction('SET_SELECTED_POLICY_NAME'); +export const setSaveAsNewPolicy = createAction('SET_SAVE_AS_NEW_POLICY'); + +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 00000000000000..04c59709bd426c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/constants.js @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 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 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_AFTER = 'selectedAfter'; +export const PHASE_ROLLOVER_AFTER_UNITS = 'selectedAfterUnits'; + +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_AFTER, + 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 = { + [STRUCTURE_INDEX_TEMPLATE]: { + [STRUCTURE_TEMPLATE_SELECTION]: { + [STRUCTURE_TEMPLATE_NAME]: [], + [STRUCTURE_INDEX_NAME]: [], + [STRUCTURE_ALIAS_NAME]: [] + }, + [STRUCTURE_CONFIGURATION]: { + [STRUCTURE_NODE_ATTRS]: [], + [STRUCTURE_PRIMARY_NODES]: [], + [STRUCTURE_REPLICAS]: [] + } + }, + [STRUCTURE_POLICY_CONFIGURATION]: { + [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_AFTER]: [], + [PHASE_ROLLOVER_AFTER_UNITS]: [], + [PHASE_NODE_ATTRS]: [], + [PHASE_PRIMARY_SHARD_COUNT]: [], + [PHASE_REPLICA_COUNT]: [], + [PHASE_FORCE_MERGE_SEGMENTS]: [], + }, + [PHASE_COLD]: { + [PHASE_ROLLOVER_ALIAS]: [], + [PHASE_ROLLOVER_AFTER]: [], + [PHASE_ROLLOVER_AFTER_UNITS]: [], + [PHASE_NODE_ATTRS]: [], + [PHASE_REPLICA_COUNT]: [], + }, + [PHASE_DELETE]: { + [PHASE_ROLLOVER_ALIAS]: [], + [PHASE_ROLLOVER_AFTER]: [], + [PHASE_ROLLOVER_AFTER_UNITS]: [], + }, + }, + [STRUCTURE_REVIEW]: { + [STRUCTURE_POLICY_NAME]: [], + } +}; 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 00000000000000..808eb489bf9137 --- /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/middleware/auto_enable_phase.js b/x-pack/plugins/index_lifecycle_management/public/store/middleware/auto_enable_phase.js new file mode 100644 index 00000000000000..738be1fd7a56d5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/middleware/auto_enable_phase.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 { + setPhaseData +} from '../actions'; +import { getPhaseData } from '../selectors'; +import { PHASE_ENABLED } from '../constants'; + +const setsPerPhase = {}; + +export const autoEnablePhase = store => next => action => { + const state = store.getState(); + + if (action.type === setPhaseData().type) { + const { phase } = action.payload; + setsPerPhase[phase] = setsPerPhase[phase] || 0; + setsPerPhase[phase]++; + + if (setsPerPhase[phase] === 1 && !getPhaseData(state, phase, PHASE_ENABLED)) { + store.dispatch(setPhaseData(phase, PHASE_ENABLED, true)); + } + } + + return next(action); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/store/middleware/auto_set_node_attrs.js b/x-pack/plugins/index_lifecycle_management/public/store/middleware/auto_set_node_attrs.js new file mode 100644 index 00000000000000..5f0a43c31abdc1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/middleware/auto_set_node_attrs.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 { + setPhaseData, setSelectedNodeAttrs +} from '../actions'; +import { getPhaseData } from '../selectors'; +import { PHASE_WARM, PHASE_NODE_ATTRS, PHASE_COLD } from '../constants'; + +export const autoSetNodeAttrs = store => next => action => { + const state = store.getState(); + + if (action.type === setSelectedNodeAttrs().type) { + const warmPhaseAttrs = getPhaseData(state, PHASE_WARM, PHASE_NODE_ATTRS); + if (!warmPhaseAttrs) { + store.dispatch(setPhaseData(PHASE_WARM, PHASE_NODE_ATTRS, action.payload)); + } + } + else if (action.type === setPhaseData().type) { + const { phase, key, value } = action.payload; + + if (phase === PHASE_WARM && key === PHASE_NODE_ATTRS) { + const coldPhaseAttrs = getPhaseData(state, PHASE_COLD, PHASE_NODE_ATTRS); + if (!coldPhaseAttrs) { + store.dispatch(setPhaseData(PHASE_COLD, PHASE_NODE_ATTRS, value)); + } + } + } + + return next(action); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/store/middleware/index.js b/x-pack/plugins/index_lifecycle_management/public/store/middleware/index.js new file mode 100644 index 00000000000000..fcb0a960db4e26 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/middleware/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 { autoEnablePhase } from './auto_enable_phase'; +export { setSelectedPolicyFromSelectedTemplate } from './set_selected_policy_from_selected_template'; +export { autoSetNodeAttrs } from './auto_set_node_attrs'; diff --git a/x-pack/plugins/index_lifecycle_management/public/store/middleware/set_selected_policy_from_selected_template.js b/x-pack/plugins/index_lifecycle_management/public/store/middleware/set_selected_policy_from_selected_template.js new file mode 100644 index 00000000000000..a87f9c3496d646 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/middleware/set_selected_policy_from_selected_template.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 { + fetchedIndexTemplate, + fetchPolicies, + setSelectedPolicy, + setPhaseData +} from '../actions'; +import { getSelectedNodeAttrs, getPhaseData } from '../selectors'; +import { PHASE_WARM, PHASE_NODE_ATTRS, PHASE_COLD } from '../constants'; + +export const setSelectedPolicyFromSelectedTemplate = store => next => async action => { + if (action.type === fetchedIndexTemplate().type) { + const template = action.payload; + if (template.settings.index && template.settings.index.lifecycle) { + const policies = await fetchPolicies()(store.dispatch); + const selectedPolicy = policies.find(policy => policy.name === template.settings.index.lifecycle.name); + if (selectedPolicy) { + store.dispatch(setSelectedPolicy(selectedPolicy)); + + // We also want to update node attrs for future phases if they do not exist + const state = store.getState(); + const hotNodeAttrs = getSelectedNodeAttrs(state); + const warmNodeAttrs = getPhaseData(state, PHASE_WARM, PHASE_NODE_ATTRS); + const coldNodeAttrs = getPhaseData(state, PHASE_COLD, PHASE_NODE_ATTRS); + + if (hotNodeAttrs && !warmNodeAttrs) { + store.dispatch(setPhaseData(PHASE_WARM, PHASE_NODE_ATTRS, hotNodeAttrs)); + } + if ((hotNodeAttrs || warmNodeAttrs) && !coldNodeAttrs) { + store.dispatch(setPhaseData(PHASE_COLD, PHASE_NODE_ATTRS, warmNodeAttrs || hotNodeAttrs)); + } + } + } + } + + return next(action); +}; 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 00000000000000..abb56f5cdae2fa --- /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 00000000000000..7225d9e0be9f56 --- /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 00000000000000..19bc7af01954c0 --- /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 00000000000000..a5a372ed17a8ad --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/nodes.js @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 = parseInt(payload); + if (isNaN(selectedReplicaCount)) { + selectedReplicaCount = ''; + } + + 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 00000000000000..982b20381dc50f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/reducers/policies.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 { handleActions } from 'redux-actions'; +import { + fetchedPolicies, + setSelectedPolicy, + setSelectedPolicyName, + setSaveAsNewPolicy, + setPhaseData +} from '../actions'; +import { policyFromES } from '../selectors'; +import { + PHASE_HOT, + PHASE_WARM, + PHASE_COLD, + PHASE_DELETE, + PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, + PHASE_ENABLED, + PHASE_ROLLOVER_ENABLED, + PHASE_FORCE_MERGE_SEGMENTS, + PHASE_FORCE_MERGE_ENABLED, + PHASE_ROLLOVER_AFTER, + PHASE_NODE_ATTRS, + PHASE_PRIMARY_SHARD_COUNT, + PHASE_REPLICA_COUNT, + PHASE_ROLLOVER_AFTER_UNITS, + 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, + PHASE_SHRINK_ENABLED +} from '../constants'; + +export const defaultWarmPhase = { + [PHASE_ENABLED]: false, + [PHASE_ROLLOVER_ENABLED]: false, + [PHASE_ROLLOVER_ALIAS]: '', + [PHASE_FORCE_MERGE_SEGMENTS]: '', + [PHASE_FORCE_MERGE_ENABLED]: false, + [PHASE_ROLLOVER_AFTER]: '', + [PHASE_ROLLOVER_AFTER_UNITS]: 's', + [PHASE_NODE_ATTRS]: '', + [PHASE_SHRINK_ENABLED]: true, + [PHASE_PRIMARY_SHARD_COUNT]: '', + [PHASE_REPLICA_COUNT]: '' +}; + +export const defaultHotPhase = { + [PHASE_ENABLED]: true, + [PHASE_ROLLOVER_ENABLED]: true, + [PHASE_ROLLOVER_ALIAS]: '', + [PHASE_ROLLOVER_MAX_AGE]: '', + [PHASE_ROLLOVER_MAX_AGE_UNITS]: 's', + [PHASE_ROLLOVER_MAX_SIZE_STORED]: '', + [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: 'gb', + [PHASE_ROLLOVER_MAX_DOC_SIZE]: '', +}; + +export const defaultColdPhase = { + [PHASE_ENABLED]: false, + [PHASE_ROLLOVER_ENABLED]: false, + [PHASE_ROLLOVER_ALIAS]: '', + [PHASE_ROLLOVER_AFTER]: '', + [PHASE_ROLLOVER_AFTER_UNITS]: 's', + [PHASE_NODE_ATTRS]: '', + [PHASE_REPLICA_COUNT]: '' +}; + +export const defaultDeletePhase = { + [PHASE_ENABLED]: false, + [PHASE_ROLLOVER_ENABLED]: false, + [PHASE_ROLLOVER_ALIAS]: '', + [PHASE_ROLLOVER_AFTER]: '', + [PHASE_ROLLOVER_AFTER_UNITS]: 's' +}; + +export const defaultPolicy = { + name: '', + saveAsNew: true, + phases: { + [PHASE_HOT]: defaultHotPhase, + [PHASE_WARM]: defaultWarmPhase, + [PHASE_COLD]: defaultColdPhase, + [PHASE_DELETE]: defaultDeletePhase + } +}; + +const defaultState = { + isLoading: false, + originalPolicyName: undefined, + selectedPolicySet: false, + selectedPolicy: defaultPolicy, + policies: [] +}; + +export const policies = handleActions( + { + [fetchedPolicies](state, { payload: policies }) { + return { + ...state, + isLoading: false, + policies + }; + }, + [setSelectedPolicy](state, { payload: selectedPolicy }) { + if (!selectedPolicy) { + return { + ...state, + selectedPolicy: defaultPolicy, + selectedPolicySet: true, + }; + } + + return { + ...state, + originalPolicyName: selectedPolicy.name, + selectedPolicySet: true, + selectedPolicy: { + ...defaultPolicy, + ...policyFromES(selectedPolicy) + } + }; + }, + [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 + } + } + } + }; + } + }, + 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 00000000000000..41459d1bbb2c8a --- /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 00000000000000..621cbf007d3b22 --- /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 00000000000000..9992c7491d3682 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/index_template.js @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { merge, cloneDeep } from 'lodash'; +import { + getSaveAsNewPolicy, + getSelectedPolicyName, + getSelectedPrimaryShardCount, + getNodesFromSelectedNodeAttrs, + getSelectedReplicaCount, + getSelectedNodeAttrs +} from '.'; + +export const getIndexTemplates = state => 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) => { + return allTemplates.find( + template => template.name === selectedIndexTemplateName + ); + } +); + +export const getFullSelectedIndexTemplate = state => state.indexTemplate.fullSelectedIndexTemplate; + +export const getAlias = state => { + const template = getSelectedIndexTemplate(state); + if (template && template.settings) { + return template.settings.indexlifecycle.rollover_alias; + } + return undefined; +}; + +// 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 newFullIndexTemplate = merge(cloneDeep(originalFullIndexTemplate), { + settings: { + index: { + number_of_shards: '' + getSelectedPrimaryShardCount(state), + number_of_replicas: '' + getSelectedReplicaCount(state), + lifecycle: { + name: getSelectedPolicyName(state) + }, + routing: { + allocation: { + include: { + sattr_name: getSelectedNodeAttrs(state), + } + } + } + } + } + }); + + 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) + }; +}; 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 00000000000000..49a074fc4b90ab --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/lifecycle.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 { + PHASE_HOT, + PHASE_WARM, + PHASE_COLD, + PHASE_DELETE, + PHASE_ENABLED, + PHASE_ROLLOVER_ENABLED, + PHASE_ROLLOVER_MAX_AGE, + PHASE_ROLLOVER_MAX_SIZE_STORED, + STRUCTURE_INDEX_TEMPLATE, + STRUCTURE_CONFIGURATION, + STRUCTURE_PRIMARY_NODES, + STRUCTURE_REPLICAS, + STRUCTURE_TEMPLATE_SELECTION, + STRUCTURE_TEMPLATE_NAME, + STRUCTURE_POLICY_NAME, + STRUCTURE_POLICY_CONFIGURATION, + STRUCTURE_INDEX_NAME, + STRUCTURE_ALIAS_NAME, + ERROR_STRUCTURE, + PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, + PHASE_PRIMARY_SHARD_COUNT, + PHASE_SHRINK_ENABLED, + STRUCTURE_REVIEW, + PHASE_FORCE_MERGE_ENABLED, + PHASE_FORCE_MERGE_SEGMENTS +} from '../constants'; +import { + getPhase, + getPhases, + phaseToES, + getSelectedPolicyName, + getSelectedIndexTemplateName, + isNumber, + getSelectedPrimaryShardCount, + getSelectedReplicaCount, + getSaveAsNewPolicy, + getSelectedOriginalPolicyName, + getBootstrapEnabled, + getIndexName, + getAliasName, +} from '.'; + +export const validatePhase = (type, phase) => { + const errors = {}; + + if (!phase[PHASE_ENABLED]) { + return errors; + } + + if (phase[PHASE_ROLLOVER_ENABLED]) { + if ( + !isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && + !isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) + ) { + errors[PHASE_ROLLOVER_MAX_AGE] = [ + 'A maximum age is required' + ]; + errors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [ + '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])) { + errors[numberedAttribute] = ['A number is required']; + } + else if (phase[numberedAttribute] < 0) { + errors[numberedAttribute] = ['Only positive numbers are allowed']; + } + else if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && phase[numberedAttribute] < 1) { + errors[numberedAttribute] = ['Only positive numbers are allowed']; + } + } + } + + if (phase[PHASE_SHRINK_ENABLED]) { + if (!isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { + errors[PHASE_PRIMARY_SHARD_COUNT] = ['A number is required.']; + } + else if (phase[PHASE_PRIMARY_SHARD_COUNT] < 1) { + errors[PHASE_PRIMARY_SHARD_COUNT] = ['Only positive numbers above 0 are allowed.']; + } + } + + if (phase[PHASE_FORCE_MERGE_ENABLED]) { + if (!isNumber(phase[PHASE_FORCE_MERGE_SEGMENTS])) { + errors[PHASE_FORCE_MERGE_SEGMENTS] = ['A number is required.']; + } + else if (phase[PHASE_FORCE_MERGE_SEGMENTS] < 1) { + errors[PHASE_FORCE_MERGE_SEGMENTS] = ['Only positive numbers above 0 are allowed.']; + } + } + + return errors; +}; + +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 (!getSelectedIndexTemplateName(state)) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_TEMPLATE_SELECTION][ + STRUCTURE_TEMPLATE_NAME + ].push('An index template is required'); + } + + if (getBootstrapEnabled(state) && !getIndexName(state)) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_TEMPLATE_SELECTION][STRUCTURE_INDEX_NAME].push('An index name is required'); + } + + if (getBootstrapEnabled(state) && !getAliasName(state)) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_TEMPLATE_SELECTION][STRUCTURE_ALIAS_NAME].push('An alias name is required'); + } + + if (!isNumber(getSelectedPrimaryShardCount(state))) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_CONFIGURATION][ + STRUCTURE_PRIMARY_NODES + ].push('A value is required'); + } + else if (getSelectedPrimaryShardCount(state) < 1) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_CONFIGURATION][ + STRUCTURE_PRIMARY_NODES + ].push('Only positive numbers are allowed'); + } + + if (!isNumber(getSelectedReplicaCount(state))) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_CONFIGURATION][ + STRUCTURE_REPLICAS + ].push('A value is required'); + } + else if (getSelectedReplicaCount(state) < 0) { + errors[STRUCTURE_INDEX_TEMPLATE][STRUCTURE_CONFIGURATION][ + STRUCTURE_REPLICAS + ].push('Only positive numbers are allowed'); + } + + if (!getSelectedPolicyName(state)) { + errors[STRUCTURE_REVIEW][STRUCTURE_POLICY_NAME].push('A policy name is required'); + } + + if (getSaveAsNewPolicy(state) && getSelectedOriginalPolicyName(state) === getSelectedPolicyName(state)) { + errors[STRUCTURE_REVIEW][STRUCTURE_POLICY_NAME].push('The policy name must be different'); + } + + // if (getSaveAsNewPolicy(state)) { + // const policyNames = getAllPolicyNamesFromTemplates(state); + // if (policyNames.includes(getSelectedPolicyName(state))) { + // errors[STRUCTURE_POLICY_CONFIGURATION][STRUCTURE_POLICY_NAME].push('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); + + errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_HOT] = { + ...errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_HOT], + ...validatePhase(PHASE_HOT, hotPhase) + }; + errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_WARM] = { + ...errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_WARM], + ...validatePhase(PHASE_WARM, warmPhase) + }; + errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_COLD] = { + ...errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_COLD], + ...validatePhase(PHASE_COLD, coldPhase) + }; + errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_DELETE] = { + ...errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_DELETE], + ...validatePhase(PHASE_DELETE, deletePhase) + }; + + if (warmPhase[PHASE_SHRINK_ENABLED]) { + if (isNumber(warmPhase[PHASE_PRIMARY_SHARD_COUNT]) && warmPhase[PHASE_PRIMARY_SHARD_COUNT] > 0) { + if (getSelectedPrimaryShardCount(state) % warmPhase[PHASE_PRIMARY_SHARD_COUNT] !== 0) { + errors[STRUCTURE_POLICY_CONFIGURATION][PHASE_WARM][PHASE_PRIMARY_SHARD_COUNT].push( + 'The shard count needs to be a divisor of the hot phase shard count.' + ); + } + } + } + + 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].after = '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 00000000000000..4a65cf969bdb56 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/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 { 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)); + options.unshift({ + text: '-- Do not reallocate my indices --', + value: undefined, + }); + + return options; + } +); + +export const getSelectedPrimaryShardCount = state => + state.nodes.selectedPrimaryShardCount; +export const getSelectedReplicaCount = state => + state.nodes.selectedReplicaCount; +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 00000000000000..cf2f859fffbca2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/selectors/policies.js @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + defaultHotPhase, + defaultWarmPhase, + defaultColdPhase, + defaultDeletePhase +} from '../reducers/policies'; +import { + PHASE_HOT, + PHASE_WARM, + PHASE_COLD, + PHASE_DELETE, + PHASE_ROLLOVER_AFTER, + PHASE_ROLLOVER_AFTER_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 +} from '../constants'; +import { getIndexTemplates } from '.'; + +export const getPolicies = state => state.policies.policies; +export const getSelectedPolicy = state => state.policies.selectedPolicy; +export const getIsSelectedPolicySet = state => state.policies.selectedPolicySet; +export const getSelectedOriginalPolicyName = state => state.policies.originalPolicyName; + +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.after) { + const { size: after, units: afterUnits } = splitSizeAndUnits( + phase.after + ); + // If the after is set to 0s, it effectively means we are moving + // to the warm phase after rollover from the hot phase + if (phaseName === PHASE_WARM && after === 0) { + policy[PHASE_ROLLOVER_ENABLED] = true; + } else { + policy[PHASE_ROLLOVER_AFTER] = after; + policy[PHASE_ROLLOVER_AFTER_UNITS] = afterUnits; + } + } + + 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) { + policy[PHASE_NODE_ATTRS] = allocate.require._name; + } + } + + 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; + } + + if (actions.replicas) { + const replicas = actions.replicas; + policy[PHASE_REPLICA_COUNT] = replicas.number_of_replicas; + } + } + + return policy; +}; + +export const policyFromES = ({ name, type, phases }) => { + return { + name, + type, + 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) + } + }; +}; + +export const phaseToES = (state, phase) => { + const esPhase = {}; + + if (!phase[PHASE_ENABLED]) { + return esPhase; + } + + if (isNumber(phase[PHASE_ROLLOVER_AFTER])) { + esPhase.after = `${phase[PHASE_ROLLOVER_AFTER]}${phase[PHASE_ROLLOVER_AFTER_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] + }`; + } else 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]) { + esPhase.actions.allocate = { + include: {}, // TODO: this seems to be a constant, confirm? + exclude: {}, // TODO: this seems to be a constant, confirm? + require: { + _name: phase[PHASE_NODE_ATTRS] + } + }; + } + + if (phase[PHASE_FORCE_MERGE_ENABLED]) { + esPhase.actions.forcemerge = { + max_num_segments: phase[PHASE_FORCE_MERGE_SEGMENTS] + }; + } + + if (isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { + esPhase.actions.shrink = { + number_of_shards: phase[PHASE_PRIMARY_SHARD_COUNT] + }; + } + + if (isNumber(phase[PHASE_REPLICA_COUNT])) { + esPhase.actions.replicas = { + number_of_replicas: phase[PHASE_REPLICA_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 00000000000000..2e3b5c219dad86 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/store/store.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 { + createStore, + applyMiddleware, + compose +} from 'redux'; +import thunk from 'redux-thunk'; + +import { + indexLifecycleManagement +} from './reducers/'; +import { + autoEnablePhase, + setSelectedPolicyFromSelectedTemplate, + autoSetNodeAttrs +} from './middleware'; + +export const indexLifecycleManagementStore = (initialState = {}) => { + const enhancers = [applyMiddleware(thunk, autoEnablePhase, setSelectedPolicyFromSelectedTemplate, autoSetNodeAttrs)]; + + 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 00000000000000..b9a77a1a0362bb --- /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 00000000000000..787814d87dff94 --- /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 00000000000000..19a7b567592694 --- /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 00000000000000..8a5a7d7029b710 --- /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 00000000000000..f2c070fd44b6e6 --- /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 00000000000000..443744ccb0cc8b --- /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 00000000000000..394c1821400000 --- /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 00000000000000..6d6a336417beff --- /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 00000000000000..f275f156370912 --- /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 00000000000000..890a366ac65c1e --- /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.wrap(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 00000000000000..6ac4d50c7e0fef --- /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.wrap(err, err.statusCode); + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return Boom.create(statusCode, message); +} 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 00000000000000..b0cdced7adbefe --- /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.wrap(err); +} 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 00000000000000..d50ff9480d3e4c --- /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 00000000000000..441648a8701e08 --- /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 00000000000000..80daac5bd496dc --- /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 00000000000000..b72f8cc7697310 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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', (done) => { + const licensePreRouting = licensePreRoutingFactory(mockServer); + const stubRequest = {}; + licensePreRouting(stubRequest, (response) => { + expect(response).to.be.an(Error); + expect(response.isBoom).to.be(true); + expect(response.output.statusCode).to.be(403); + done(); + }); + }); + }); + + describe('isAvailable is true', () => { + beforeEach(() => { + mockLicenseCheckResults = { + isAvailable: true + }; + }); + + it ('replies with nothing', (done) => { + const licensePreRouting = licensePreRoutingFactory(mockServer); + const stubRequest = {}; + licensePreRouting(stubRequest, (response) => { + expect(response).to.be(undefined); + done(); + }); + }); + }); + }); +}); 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 00000000000000..0743e443955f45 --- /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 00000000000000..b3720ab265393b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/lib/license_pre_routing_factory/license_pre_routing_factory.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 { 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(request, reply) { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + if (!licenseCheckResults.isAvailable) { + const error = new Error(licenseCheckResults.message); + const statusCode = 403; + const wrappedError = wrapCustomError(error, statusCode); + reply(wrappedError); + } else { + reply(); + } + } + + 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 00000000000000..7b0f97c38d1292 --- /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 00000000000000..35bc4b75336059 --- /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/indices/index.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/index.js new file mode 100644 index 00000000000000..915fb695bb4683 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/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 { registerIndicesRoutes } from './register_indices_routes'; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_bootstrap_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_bootstrap_route.js new file mode 100644 index 00000000000000..ad7128903b99ce --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_bootstrap_route.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, + aliases: { + [payload.aliasName]: {} + }, + settings: { + 'index.lifecycle.rollover_alias': payload.aliasName, + } + }); +} + +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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const response = await bootstrap(callWithRequest, request.payload); + reply(response); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(wrapUnknownError(err)); + } + }, + config: { + pre: [licensePreRouting] + } + }); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_get_affected_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_get_affected_route.js new file mode 100644 index 00000000000000..fc7f5f2d2d1319 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_get_affected_route.js @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 incase the user shutdown security in-between the check and now + 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) { + return []; + } + + const indexPatterns = Object.entries(templates).reduce((accum, [templateName, template]) => { + if (templateName === indexTemplateName) { + accum.push(...template.index_patterns); + } else if ( + template.settings && + template.settings.index && + template.settings.index.lifecycle && + (policyName && template.settings.index.lifecycle.name === policyName) + ) { + accum.push(...template.index_patterns); + } + return accum; + }, []); + + if (!indexPatterns || indexPatterns.length === 0) { + return []; + } + + const indices = await callWithRequest('indices.get', { + index: indexPatterns + }); + + if (!indices) { + 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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const response = await getAffectedIndices( + callWithRequest, + request.params.indexTemplateName, + ); + reply(response); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(wrapUnknownError(err)); + } + }, + config: { + pre: [licensePreRouting] + } + }); + + server.route({ + path: + '/api/index_lifecycle_management/indices/affected/{indexTemplateName}/{policyName}', + method: 'GET', + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const response = await getAffectedIndices( + callWithRequest, + request.params.indexTemplateName, + request.params.policyName + ); + reply(response); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(wrapUnknownError(err)); + } + }, + config: { + pre: [licensePreRouting] + } + }); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_indices_routes.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_indices_routes.js new file mode 100644 index 00000000000000..1f94eb13d125e0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/indices/register_indices_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 { registerBootstrapRoute } from './register_bootstrap_route'; +import { registerGetAffectedRoute } from './register_get_affected_route'; + +export function registerIndicesRoutes(server) { + registerBootstrapRoute(server); + registerGetAffectedRoute(server); +} 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 00000000000000..17f52a723405dd --- /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 00000000000000..84e2637ab5a4c5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/lifecycle/register_create_route.js @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + + +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; +import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; +import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; +import { merge } from 'lodash'; + +async function createLifecycle(callWithRequest, lifecycle) { + const body = { + policy: { + phases: lifecycle.phases, + } + }; + const params = { + method: 'PUT', + path: `/_xpack/index_lifecycle/${lifecycle.name}`, + ignore: [ 404 ], + body, + }; + + return await callWithRequest('transport.request', params); +} + +async function getIndexTemplate(callWithRequest, indexTemplate) { + const response = await callWithRequest('indices.getTemplate', { name: indexTemplate }); + return response[indexTemplate]; +} + +async function updateIndexTemplate(callWithRequest, indexTemplatePatch) { + // Fetch existing template + const template = await getIndexTemplate(callWithRequest, indexTemplatePatch.indexTemplate); + merge(template, { + settings: { + index: { + number_of_shards: indexTemplatePatch.primaryShardCount, + number_of_replicas: indexTemplatePatch.replicaCount, + lifecycle: { + name: indexTemplatePatch.lifecycleName, + }, + routing: { + allocation: { + include: { + sattr_name: indexTemplatePatch.nodeAttrs, + } + } + } + } + } + }); + + const params = { + method: 'PUT', + path: `/_template/${indexTemplatePatch.indexTemplate}`, + ignore: [ 404 ], + body: template, + }; + + 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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const response = await createLifecycle(callWithRequest, request.payload.lifecycle); + const response2 = await updateIndexTemplate(callWithRequest, request.payload.indexTemplatePatch); + reply([response, response2]); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(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 00000000000000..ba179d14b81120 --- /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 00000000000000..d6a9dd774e206b --- /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 00000000000000..ef0ac271ae60ec --- /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 00000000000000..9ab30dd0f2f7e6 --- /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 formatStats(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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const stats = await fetchNodeStats(callWithRequest); + const response = formatStats(stats, request.params.nodeAttrs); + reply(response); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(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 00000000000000..7b826196654125 --- /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 formatStats(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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const stats = await fetchNodeStats(callWithRequest); + const response = formatStats(stats); + reply(response); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(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 00000000000000..341f1d4f1ebf32 --- /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 00000000000000..7c6103a3389ab2 --- /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_fetch_route.js b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.js new file mode 100644 index 00000000000000..6ec6745e80b219 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.js @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 formatHits(hits) { + return Object.keys(hits).reduce((accum, lifecycleName) => { + const hit = hits[lifecycleName]; + accum.push({ + ...hit, + name: lifecycleName, + }); + return accum; + }, []); +} + +async function fetchPolicies(callWithRequest) { + const params = { + method: 'GET', + path: '/_xpack/index_lifecycle', + // 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/policies', + method: 'GET', + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const hits = await fetchPolicies(callWithRequest); + const response = formatHits(hits); + reply(response); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(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 00000000000000..676121eadccb18 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_policies_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 { registerFetchRoute } from './register_fetch_route'; + +export function registerPoliciesRoutes(server) { + registerFetchRoute(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 00000000000000..dc9a0acaaf09bd --- /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 00000000000000..7bc4922478be08 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.js @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, + 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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const hits = await fetchTemplates(callWithRequest); + const templates = formatTemplates(hits, callWithRequest); + reply(templates); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(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 00000000000000..690960e953c8c1 --- /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, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + const templateName = request.params.templateName; + + try { + const template = await fetchTemplate(callWithRequest, templateName); + reply(template[templateName]); + } catch (err) { + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(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 00000000000000..9750c0157b9656 --- /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/public/styles/table.less b/x-pack/plugins/index_management/public/styles/table.less index bd2b7a65c443e6..2f365744e8da0c 100644 --- a/x-pack/plugins/index_management/public/styles/table.less +++ b/x-pack/plugins/index_management/public/styles/table.less @@ -8,7 +8,7 @@ .indexTableHorizontalScrollContainer { overflow-x: auto; max-width: 100%; - height: 100vh; + min-height: 100vh; } .indexTableHorizontalScroll { min-width: 800px; diff --git a/yarn.lock b/yarn.lock index a463ab08320d6a..9f12ffe9adc154 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10816,9 +10816,6 @@ regex-cache@^0.4.2: regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" regexpu-core@^1.0.0: version "1.0.0"