diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx index 1530d5c2cc4c8..9a49094d063d3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx @@ -7,16 +7,17 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; +import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { licensingMock } from '../../../../licensing/public/mocks'; import { App } from '../../../public/application/app'; import { TestSubjects } from '../helpers'; -import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; const breadcrumbService = createBreadcrumbsMock(); const AppWithContext = (props: any) => { return ( - + ); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index f9f2233ff02ee..6bb51602df21f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -9,12 +9,15 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; +import { licensingMock } from '../../../../licensing/public/mocks'; + import { EditPolicy } from '../../../public/application/sections/edit_policy'; import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; import { Phases as PolicyPhases } from '../../../common/types'; import { KibanaContextProvider } from '../../../public/shared_imports'; +import { AppServicesContext } from '../../../public/types'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; type Phases = keyof PolicyPhases; @@ -53,10 +56,16 @@ const testBedConfig: TestBedConfig = { const breadcrumbService = createBreadcrumbsMock(); -const MyComponent = (props: any) => { +const MyComponent = ({ appServicesContext, ...rest }: any) => { return ( - - + + ); }; @@ -67,10 +76,10 @@ type SetupReturn = ReturnType; export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async () => { - const testBed = await initTestBed(); +export const setup = async (arg?: { appServicesContext: Partial }) => { + const testBed = await initTestBed(arg); - const { find, component, form } = testBed; + const { find, component, form, exists } = testBed; const createFormToggleAction = (dataTestSubject: string) => async (checked: boolean) => { await act(async () => { @@ -128,12 +137,15 @@ export const setup = async () => { component.update(); }; - const toggleForceMerge = (phase: Phases) => createFormToggleAction(`${phase}-forceMergeSwitch`); - - const setForcemergeSegmentsCount = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedForceMergeSegments`); - - const setBestCompression = (phase: Phases) => createFormToggleAction(`${phase}-bestCompression`); + const createForceMergeActions = (phase: Phases) => { + const toggleSelector = `${phase}-forceMergeSwitch`; + return { + forceMergeFieldExists: () => exists(toggleSelector), + toggleForceMerge: createFormToggleAction(toggleSelector), + setForcemergeSegmentsCount: createFormSetValueAction(`${phase}-selectedForceMergeSegments`), + setBestCompression: createFormToggleAction(`${phase}-bestCompression`), + }; + }; const setIndexPriority = (phase: Phases) => createFormSetValueAction(`${phase}-phaseIndexPriority`); @@ -180,7 +192,35 @@ export const setup = async () => { await createFormSetValueAction('warm-selectedPrimaryShardCount')(value); }; + const shrinkExists = () => exists('shrinkSwitch'); + const setFreeze = createFormToggleAction('freezeSwitch'); + const freezeExists = () => exists('freezeSwitch'); + + const createSearchableSnapshotActions = (phase: Phases) => { + const fieldSelector = `searchableSnapshotField-${phase}`; + const licenseCalloutSelector = `${fieldSelector}.searchableSnapshotDisabledDueToLicense`; + const toggleSelector = `${fieldSelector}.searchableSnapshotToggle`; + + const toggleSearchableSnapshot = createFormToggleAction(toggleSelector); + return { + searchableSnapshotDisabled: () => exists(licenseCalloutSelector), + searchableSnapshotsExists: () => exists(fieldSelector), + findSearchableSnapshotToggle: () => find(toggleSelector), + searchableSnapshotDisabledDueToLicense: () => + exists(`${fieldSelector}.searchableSnapshotDisabledDueToLicense`), + toggleSearchableSnapshot, + setSearchableSnapshot: async (value: string) => { + await toggleSearchableSnapshot(true); + act(() => { + find(`searchableSnapshotField-${phase}.searchableSnapshotCombobox`).simulate('change', [ + { label: value }, + ]); + }); + component.update(); + }, + }; + }; return { ...testBed, @@ -192,10 +232,9 @@ export const setup = async () => { setMaxDocs, setMaxAge, toggleRollover, - toggleForceMerge: toggleForceMerge('hot'), - setForcemergeSegments: setForcemergeSegmentsCount('hot'), - setBestCompression: setBestCompression('hot'), + ...createForceMergeActions('hot'), setIndexPriority: setIndexPriority('hot'), + ...createSearchableSnapshotActions('hot'), }, warm: { enable: enable('warm'), @@ -206,9 +245,8 @@ export const setup = async () => { setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), setReplicas: setReplicas('warm'), setShrink, - toggleForceMerge: toggleForceMerge('warm'), - setForcemergeSegments: setForcemergeSegmentsCount('warm'), - setBestCompression: setBestCompression('warm'), + shrinkExists, + ...createForceMergeActions('warm'), setIndexPriority: setIndexPriority('warm'), }, cold: { @@ -219,7 +257,9 @@ export const setup = async () => { setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), setReplicas: setReplicas('cold'), setFreeze, + freezeExists, setIndexPriority: setIndexPriority('cold'), + ...createSearchableSnapshotActions('cold'), }, delete: { enable: enable('delete'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index a203a434bb21a..12a061f0980dd 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -6,10 +6,11 @@ import { act } from 'react-dom/test-utils'; +import { licensingMock } from '../../../../licensing/public/mocks'; +import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment } from '../helpers/setup_environment'; import { EditPolicyTestBed, setup } from './edit_policy.helpers'; -import { API_BASE_PATH } from '../../../common/constants'; import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, @@ -100,6 +101,11 @@ describe('', () => { describe('serialization', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); httpRequestsMockHelpers.setLoadSnapshotPolicies([]); await act(async () => { @@ -117,7 +123,7 @@ describe('', () => { await actions.hot.setMaxDocs('123'); await actions.hot.setMaxAge('123', 'h'); await actions.hot.toggleForceMerge(true); - await actions.hot.setForcemergeSegments('123'); + await actions.hot.setForcemergeSegmentsCount('123'); await actions.hot.setBestCompression(true); await actions.hot.setIndexPriority('123'); @@ -150,6 +156,19 @@ describe('', () => { `); }); + test('setting searchable snapshot', async () => { + const { actions } = testBed; + + await actions.hot.setSearchableSnapshot('my-repo'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe( + 'my-repo' + ); + }); + test('disabling rollover', async () => { const { actions } = testBed; await actions.hot.toggleRollover(true); @@ -167,6 +186,26 @@ describe('', () => { } `); }); + + test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => { + const { actions } = testBed; + + await actions.warm.enable(true); + await actions.cold.enable(true); + + expect(actions.warm.forceMergeFieldExists()).toBeTruthy(); + expect(actions.warm.shrinkExists()).toBeTruthy(); + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.freezeExists()).toBeTruthy(); + + await actions.hot.setSearchableSnapshot('my-repo'); + + expect(actions.warm.forceMergeFieldExists()).toBeFalsy(); + expect(actions.warm.shrinkExists()).toBeFalsy(); + // searchable snapshot in cold is still visible + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.freezeExists()).toBeFalsy(); + }); }); }); @@ -202,7 +241,6 @@ describe('', () => { "priority": 50, }, }, - "min_age": "0ms", } `); }); @@ -210,14 +248,12 @@ describe('', () => { test('setting all values', async () => { const { actions } = testBed; await actions.warm.enable(true); - await actions.warm.setMinAgeValue('123'); - await actions.warm.setMinAgeUnits('d'); await actions.warm.setDataAllocation('node_attrs'); await actions.warm.setSelectedNodeAttribute('test:123'); await actions.warm.setReplicas('123'); await actions.warm.setShrink('123'); await actions.warm.toggleForceMerge(true); - await actions.warm.setForcemergeSegments('123'); + await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); await actions.warm.setIndexPriority('123'); await actions.savePolicy(); @@ -259,22 +295,23 @@ describe('', () => { "number_of_shards": 123, }, }, - "min_age": "123d", }, }, } `); }); - test('setting warm phase on rollover to "true"', async () => { + test('setting warm phase on rollover to "false"', async () => { const { actions } = testBed; await actions.warm.enable(true); - await actions.warm.warmPhaseOnRollover(true); + await actions.warm.warmPhaseOnRollover(false); + await actions.warm.setMinAgeValue('123'); + await actions.warm.setMinAgeUnits('d'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; const warmPhaseMinAge = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm .min_age; - expect(warmPhaseMinAge).toBe(undefined); + expect(warmPhaseMinAge).toBe('123d'); }); }); @@ -359,7 +396,7 @@ describe('', () => { `); }); - test('setting all values', async () => { + test('setting all values, excluding searchable snapshot', async () => { const { actions } = testBed; await actions.cold.enable(true); @@ -410,6 +447,19 @@ describe('', () => { } `); }); + + // Setting searchable snapshot field disables setting replicas so we test this separately + test('setting searchable snapshot', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.setSearchableSnapshot('my-repo'); + await actions.savePolicy(); + const latestRequest2 = server.requests[server.requests.length - 1]; + const entirePolicy2 = JSON.parse(JSON.parse(latestRequest2.requestBody).body); + expect(entirePolicy2.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'my-repo' + ); + }); }); }); @@ -598,6 +648,7 @@ describe('', () => { `); }); }); + describe('node attr and none', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); @@ -625,4 +676,73 @@ describe('', () => { }); }); }); + + describe('searchable snapshot', () => { + describe('on cloud', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + + test('correctly sets snapshot repository default to "found-snapshots"', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'found-snapshots' + ); + }); + }); + describe('on non-enterprise license', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + test('disable setting searchable snapshots', async () => { + const { actions } = testBed; + + expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + + await actions.cold.enable(true); + + // Still hidden in hot + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); + }); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index c7a493ce80d96..d9bb6702cb166 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -6,7 +6,7 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ListNodesRouteResponse } from '../../../common/types'; +import { ListNodesRouteResponse, ListSnapshotReposResponse } from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -47,9 +47,18 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setListSnapshotRepos = (body: ListSnapshotReposResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/snapshot_repositories`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadPolicies, setLoadSnapshotPolicies, setListNodes, + setListSnapshotRepos, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md new file mode 100644 index 0000000000000..ce1ea7aa396a6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md @@ -0,0 +1,8 @@ +# Deprecated + +This test folder contains useful test coverage, mostly error states for form validation. However, it is +not in keeping with other ES UI maintained plugins. See ../client_integration for the established pattern +of tests. + +The tests here should be migrated to the above pattern and should not be added to. Any new test coverage must +be added to ../client_integration. diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index eb17402a46950..65952e81ae0ff 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -172,6 +172,9 @@ const MyComponent = ({ existingPolicies, policyName, getUrlForApp, + license: { + canUseSearchableSnapshot: () => true, + }, }} > @@ -209,6 +212,7 @@ describe('edit policy', () => { getUrlForApp={jest.fn()} policyName="test" isCloudEnabled={false} + license={{ canUseSearchableSnapshot: () => true }} /> ); @@ -247,6 +251,7 @@ describe('edit policy', () => { existingPolicies={policies} getUrlForApp={jest.fn()} isCloudEnabled={false} + license={{ canUseSearchableSnapshot: () => true }} /> ); const rendered = mountWithIntl(component); @@ -283,6 +288,7 @@ describe('edit policy', () => { existingPolicies={policies} getUrlForApp={jest.fn()} isCloudEnabled={false} + license={{ canUseSearchableSnapshot: () => true }} /> ); @@ -827,6 +833,7 @@ describe('edit policy', () => { existingPolicies={policies} getUrlForApp={jest.fn()} isCloudEnabled={true} + license={{ canUseSearchableSnapshot: () => true }} /> ); ({ http } = editPolicyHelpers.setup()); diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts index 522dc6d82a4e9..7982bdb211ae7 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts @@ -5,18 +5,19 @@ */ import { i18n } from '@kbn/i18n'; -import { LicenseType } from '../../../licensing/common/types'; export { phaseToNodePreferenceMap } from './data_tiers'; -const basicLicense: LicenseType = 'basic'; +import { MIN_PLUGIN_LICENSE, MIN_SEARCHABLE_SNAPSHOT_LICENSE } from './license'; export const PLUGIN = { ID: 'index_lifecycle_management', - minimumLicenseType: basicLicense, + minimumLicenseType: MIN_PLUGIN_LICENSE, TITLE: i18n.translate('xpack.indexLifecycleMgmt.appTitle', { defaultMessage: 'Index Lifecycle Policies', }), }; export const API_BASE_PATH = '/api/index_lifecycle_management'; + +export { MIN_SEARCHABLE_SNAPSHOT_LICENSE, MIN_PLUGIN_LICENSE }; diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/license.ts b/x-pack/plugins/index_lifecycle_management/common/constants/license.ts new file mode 100644 index 0000000000000..ccb0a2a59a315 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/common/constants/license.ts @@ -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 { LicenseType } from '../../../licensing/common/types'; + +export const MIN_PLUGIN_LICENSE: LicenseType = 'basic'; + +export const MIN_SEARCHABLE_SNAPSHOT_LICENSE: LicenseType = 'enterprise'; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index b7ca16ac46dde..c0355daf3c62a 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -18,3 +18,10 @@ export interface ListNodesRouteResponse { */ isUsingDeprecatedDataRoleConfig: boolean; } + +export interface ListSnapshotReposResponse { + /** + * An array of repository names + */ + repositories: string[]; +} diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index dd5fb9e014446..94cc11d0b61a6 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -48,6 +48,15 @@ export interface SerializedActionWithAllocation { migrate?: MigrateAction; } +export interface SearchableSnapshotAction { + snapshot_repository: string; + /** + * We do not configure this value in the UI as it is an advanced setting that will + * not suit the vast majority of cases. + */ + force_merge_index?: boolean; +} + export interface SerializedHotPhase extends SerializedPhase { actions: { rollover?: { @@ -59,6 +68,10 @@ export interface SerializedHotPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + /** + * Only available on enterprise license + */ + searchable_snapshot?: SearchableSnapshotAction; }; } @@ -84,6 +97,10 @@ export interface SerializedColdPhase extends SerializedPhase { priority: number | null; }; migrate?: MigrateAction; + /** + * Only available on enterprise license + */ + searchable_snapshot?: SearchableSnapshotAction; }; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index bb1a4810ba2d2..e44854985c056 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -9,6 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; import { CloudSetup } from '../../../cloud/public'; +import { ILicense } from '../../../licensing/public'; import { KibanaContextProvider } from '../shared_imports'; @@ -23,11 +24,12 @@ export const renderApp = ( navigateToApp: ApplicationStart['navigateToApp'], getUrlForApp: ApplicationStart['getUrlForApp'], breadcrumbService: BreadcrumbService, + license: ILicense, cloud?: CloudSetup ): UnmountCallback => { render( - + JSX.Element) | JSX.Element | JSX.Element[] | undefined; + switchProps?: Omit; }; export const DescribedFormField: FunctionComponent = ({ @@ -20,7 +21,13 @@ export const DescribedFormField: FunctionComponent = ({ }) => { return ( - {children} + {switchProps ? ( + {children} + ) : typeof children === 'function' ? ( + children() + ) : ( + children + )} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/field_loading_error.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/field_loading_error.tsx new file mode 100644 index 0000000000000..de1a6875c29f4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/field_loading_error.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiSpacer, EuiButtonIcon } from '@elastic/eui'; + +interface Props { + title: React.ReactNode; + body: React.ReactNode; + resendRequest: () => void; + 'data-test-subj'?: string; + 'aria-label'?: string; +} + +export const FieldLoadingError: FunctionComponent = (props) => { + const { title, body, resendRequest } = props; + return ( + <> + + + {title} + + + + } + > + {body} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index 326f6ff87dc3b..265996c650024 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -10,5 +10,6 @@ export { LearnMoreLink } from './learn_more_link'; export { OptionalLabel } from './optional_label'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { DescribedFormField } from './described_form_field'; +export { FieldLoadingError } from './field_loading_error'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index b87243bd1a9a1..2f5be3e45cbe7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -9,17 +9,23 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiTextColor, EuiAccordion } from '@elastic/eui'; import { Phases } from '../../../../../../../common/types'; import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../edit_policy_context'; +import { useConfigurationIssues } from '../../../form'; import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../'; -import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared_fields'; +import { + MinAgeInputField, + DataTierAllocationField, + SetPriorityInputField, + SearchableSnapshotField, +} from '../shared_fields'; const i18nTexts = { dataTierAllocation: { @@ -34,16 +40,19 @@ const coldProperty: keyof Phases = 'cold'; const formFieldPaths = { enabled: '_meta.cold.enabled', + searchableSnapshot: 'phases.cold.actions.searchable_snapshot.snapshot_repository', }; export const ColdPhase: FunctionComponent = () => { const { policy } = useEditPolicyContext(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const [formData] = useFormData({ - watch: [formFieldPaths.enabled], + watch: [formFieldPaths.enabled, formFieldPaths.searchableSnapshot], }); const enabled = get(formData, formFieldPaths.enabled); + const showReplicasField = get(formData, formFieldPaths.searchableSnapshot) == null; return (
@@ -91,83 +100,104 @@ export const ColdPhase: FunctionComponent = () => { {enabled && ( <> - {/* Data tier allocation section */} - - - {/* Replicas section */} - - {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} - - } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + + + - - - {/* Freeze section */} - - - - } - description={ - - {' '} - - + { + /* Replicas section */ + showReplicasField && ( + + {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + 'data-test-subj': 'cold-setReplicasSwitch', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean( + policy.phases.cold?.actions?.allocate?.number_of_replicas + ), + }} + fullWidth + > + + + ) } - fullWidth - titleSize="xs" - > - + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + + + )} + {/* Data tier allocation section */} + - - + + )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 629c1388f61fb..d358fdeb25194 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -14,6 +14,8 @@ import { EuiSpacer, EuiDescribedFormGroup, EuiCallOut, + EuiAccordion, + EuiTextColor, } from '@elastic/eui'; import { Phases } from '../../../../../../../common/types'; @@ -28,29 +30,40 @@ import { import { i18nTexts } from '../../../i18n_texts'; -import { ROLLOVER_EMPTY_VALIDATION } from '../../../form'; +import { ROLLOVER_EMPTY_VALIDATION, useConfigurationIssues } from '../../../form'; + +import { useEditPolicyContext } from '../../../edit_policy_context'; import { ROLLOVER_FORM_PATHS } from '../../../constants'; -import { LearnMoreLink, ActiveBadge } from '../../'; +import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../'; -import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared_fields'; +import { + ForcemergeField, + SetPriorityInputField, + SearchableSnapshotField, + useRolloverPath, +} from '../shared_fields'; import { maxSizeStoredUnits, maxAgeUnits } from './constants'; const hotProperty: keyof Phases = 'hot'; export const HotPhase: FunctionComponent = () => { + const { license } = useEditPolicyContext(); const [formData] = useFormData({ watch: useRolloverPath, }); const isRolloverEnabled = get(formData, useRolloverPath); - const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + return ( <>

@@ -62,166 +75,184 @@ export const HotPhase: FunctionComponent = () => {

} - titleSize="s" description={ - -

- -

-
+

+ +

} - fullWidth > - - key="_meta.hot.useRollover" - path="_meta.hot.useRollover" - component={ToggleField} - componentProps={{ - hasEmptyLabelSpace: true, - fullWidth: false, - helpText: ( - <> -

- -

+
+ + + + + {i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', { + defaultMessage: 'Rollover', + })} + + } + description={ + +

+ {' '} } docPath="indices-rollover-index.html" /> - - - ), - euiFieldProps: { - 'data-test-subj': 'rolloverSwitch', - }, - }} - /> - {isRolloverEnabled && ( - <> - - {showEmptyRolloverFieldsError && ( - <> - -

{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
- - - - )} - - - - {(field) => { - const showErrorCallout = field.errors.some( - (e) => e.validationType === ROLLOVER_EMPTY_VALIDATION - ); - if (showErrorCallout !== showEmptyRolloverFieldsError) { - setShowEmptyRolloverFieldsError(showErrorCallout); - } - return ( - - ); - }} - - - - - - - - - - - - - - - - - - - - - - +

+
+ } + fullWidth + > + + key="_meta.hot.useRollover" + path="_meta.hot.useRollover" + component={ToggleField} + componentProps={{ + fullWidth: false, + euiFieldProps: { + 'data-test-subj': 'rolloverSwitch', + }, + }} + /> + {isRolloverEnabled && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
+
+ + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.validationType === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + )} +
+ {license.canUseSearchableSnapshot() && } + {isRolloverEnabled && !isUsingSearchableSnapshotInHotPhase && ( + )} - - {isRolloverEnabled && } - + +
); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/_data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/_data_tier_allocation.scss new file mode 100644 index 0000000000000..8449d5ea53bdf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/_data_tier_allocation.scss @@ -0,0 +1,3 @@ +.ilmDataTierAllocationField { + max-width: $euiFormMaxWidth; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index 73814537ff276..0879b12ed0b28 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -8,7 +8,7 @@ import { get } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiSpacer } from '@elastic/eui'; import { useKibana, useFormData } from '../../../../../../../shared_imports'; @@ -28,6 +28,8 @@ import { CloudDataTierCallout, } from './components'; +import './_data_tier_allocation.scss'; + const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { defaultMessage: 'Data allocation', @@ -114,21 +116,19 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr description={description} fullWidth > - - <> - - - {/* Data tier related warnings and call-to-action notices */} - {renderNotice()} - - +
+ + + {/* Data tier related warnings and call-to-action notices */} + {renderNotice()} +
); }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index b05d49be497cd..fb7f93a42e491 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -20,7 +20,7 @@ interface Props { phase: 'hot' | 'warm'; } -export const Forcemerge: React.FunctionComponent = ({ phase }) => { +export const ForcemergeField: React.FunctionComponent = ({ phase }) => { const { policy } = useEditPolicyContext(); const initialToggleValue = useMemo(() => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 9cf6034a15e35..452abd4c2aeac 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -8,10 +8,12 @@ export { useRolloverPath } from '../../../constants'; export { DataTierAllocationField } from './data_tier_allocation_field'; -export { Forcemerge } from './forcemerge_field'; +export { ForcemergeField } from './forcemerge_field'; -export { SetPriorityInput } from './set_priority_input'; +export { SetPriorityInputField } from './set_priority_input_field'; export { MinAgeInputField } from './min_age_input_field'; export { SnapshotPoliciesField } from './snapshot_policies_field'; + +export { SearchableSnapshotField } from './searchable_snapshot_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/_searchable_snapshot_field.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/_searchable_snapshot_field.scss new file mode 100644 index 0000000000000..04fec443a5290 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/_searchable_snapshot_field.scss @@ -0,0 +1,3 @@ +.ilmSearchableSnapshotField { + max-width: $euiFormMaxWidth; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/index.ts new file mode 100644 index 0000000000000..2e8878004f544 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/index.ts @@ -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 { SearchableSnapshotField } from './searchable_snapshot_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_data_provider.tsx new file mode 100644 index 0000000000000..c940dc88b16c0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_data_provider.tsx @@ -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 { useLoadSnapshotRepositories } from '../../../../../../services/api'; + +interface Props { + children: (arg: ReturnType) => JSX.Element; +} + +export const SearchableSnapshotDataProvider = ({ children }: Props) => { + return children(useLoadSnapshotRepositories()); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx new file mode 100644 index 0000000000000..e5ab5fb6a2c71 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -0,0 +1,338 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiComboBoxOptionOption, + EuiTextColor, + EuiSpacer, + EuiCallOut, + EuiLink, +} from '@elastic/eui'; + +import { + UseField, + ComboBoxField, + useKibana, + fieldValidators, + useFormData, +} from '../../../../../../../shared_imports'; + +import { useEditPolicyContext } from '../../../../edit_policy_context'; +import { useConfigurationIssues } from '../../../../form'; + +import { i18nTexts } from '../../../../i18n_texts'; + +import { FieldLoadingError, DescribedFormField, LearnMoreLink } from '../../../index'; + +import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider'; + +import './_searchable_snapshot_field.scss'; + +const { emptyField } = fieldValidators; + +export interface Props { + phase: 'hot' | 'cold'; +} + +/** + * This repository is provisioned by Elastic Cloud and will always + * exist as a "managed" repository. + */ +const CLOUD_DEFAULT_REPO = 'found-snapshots'; + +export const SearchableSnapshotField: FunctionComponent = ({ phase }) => { + const { + services: { cloud }, + } = useKibana(); + const { getUrlForApp, policy, license } = useEditPolicyContext(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; + + const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); + const isDisabledInColdDueToHotPhase = phase === 'cold' && isUsingSearchableSnapshotInHotPhase; + + const isDisabled = isDisabledDueToLicense || isDisabledInColdDueToHotPhase; + + const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => + Boolean(policy.phases[phase]?.actions?.searchable_snapshot?.snapshot_repository) + ); + + useEffect(() => { + if (isDisabled) { + setIsFieldToggleChecked(false); + } + }, [isDisabled]); + + const [formData] = useFormData({ watch: searchableSnapshotPath }); + const searchableSnapshotRepo = get(formData, searchableSnapshotPath); + + const renderField = () => ( + + {({ error, isLoading, resendRequest, data }) => { + const repos = data?.repositories ?? []; + + let calloutContent: React.ReactNode | undefined; + + if (!isLoading) { + if (error) { + calloutContent = ( + + } + body={ + + } + /> + ); + } else if (repos.length === 0) { + calloutContent = ( + + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.createSearchableSnapshotLink', + { + defaultMessage: 'Create a snapshot repository', + } + )} + + ), + }} + /> + + ); + } else if (searchableSnapshotRepo && !repos.includes(searchableSnapshotRepo)) { + calloutContent = ( + + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.createSnapshotRepositoryLink', + { + defaultMessage: 'create a new snapshot repository', + } + )} + + ), + }} + /> + + ); + } + } + + return ( +
+ + config={{ + defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, + label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, + validations: [ + { + validator: emptyField( + i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired + ), + }, + ], + }} + path={searchableSnapshotPath} + > + {(field) => { + const singleSelectionArray: [selectedSnapshot?: string] = field.value + ? [field.value] + : []; + + return ( + ({ label: repo, value: repo })), + singleSelection: { asPlainText: true }, + isLoading, + noSuggestions: !!(error || repos.length === 0), + onCreateOption: (newOption: string) => { + field.setValue(newOption); + }, + onChange: (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + field.setValue(options[0].label); + } else { + field.setValue(''); + } + }, + }} + /> + ); + }} + + {calloutContent && ( + <> + + {calloutContent} + + )} +
+ ); + }} +
+ ); + + const renderInfoCallout = (): JSX.Element | undefined => { + let infoCallout: JSX.Element | undefined; + + if (phase === 'hot' && isUsingSearchableSnapshotInHotPhase) { + infoCallout = ( + + ); + } else if (isDisabledDueToLicense) { + infoCallout = ( + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody', + { + defaultMessage: 'To create a searchable snapshot an enterprise license is required.', + } + )} + + ); + } else if (isDisabledInColdDueToHotPhase) { + infoCallout = ( + + ); + } + + return infoCallout ? ( + <> + + {infoCallout} + + + ) : undefined; + }; + + return ( + + {i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { + defaultMessage: 'Searchable snapshot', + })} + + } + description={ + <> + + , + }} + /> + + {renderInfoCallout()} + + } + fullWidth + > + {isDisabled ?
: renderField} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx similarity index 93% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx index 700a020577a43..e5ec1d116ec6f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx @@ -12,13 +12,13 @@ import { Phases } from '../../../../../../../common/types'; import { UseField, NumericField } from '../../../../../../shared_imports'; -import { LearnMoreLink } from '../../'; +import { LearnMoreLink } from '../..'; interface Props { phase: keyof Phases & string; } -export const SetPriorityInput: FunctionComponent = ({ phase }) => { +export const SetPriorityInputField: FunctionComponent = ({ phase }) => { return ( { @@ -46,40 +42,28 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => { let calloutContent; if (error) { calloutContent = ( - <> - - - - - - + + )} + title={ + + } + body={ - - + } + /> ); } else if (data.length === 0) { calloutContent = ( @@ -87,7 +71,6 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => { { { const { policy } = useEditPolicyContext(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const [formData] = useFormData({ watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], }); @@ -74,7 +81,7 @@ export const WarmPhase: FunctionComponent = () => { } titleSize="s" description={ - + <>

{ }, }} /> - + } fullWidth > - + <> {enabled && ( - + <> {hotPhaseRolloverEnabled && ( { )} - + )} - + {enabled && ( - - {/* Data tier allocation section */} - - + @@ -168,58 +178,64 @@ export const WarmPhase: FunctionComponent = () => { }} /> - - - - } - description={ - - {' '} - - - } - titleSize="xs" - switchProps={{ - 'aria-controls': 'shrinkContent', - 'data-test-subj': 'shrinkSwitch', - label: i18nTexts.shrinkLabel, - 'aria-label': i18nTexts.shrinkLabel, - initialValue: Boolean(policy.phases.warm?.actions?.shrink), - }} - fullWidth - > -

- - - - + - - - -
- - - + + } + description={ + + {' '} + + + } + titleSize="xs" + switchProps={{ + 'aria-controls': 'shrinkContent', + 'data-test-subj': 'shrinkSwitch', + label: i18nTexts.shrinkLabel, + 'aria-label': i18nTexts.shrinkLabel, + initialValue: Boolean(policy.phases.warm?.actions?.shrink), + }} + fullWidth + > +
+ + + + + + + +
+ + )} - -
+ {!isUsingSearchableSnapshotInHotPhase && } + {/* Data tier allocation section */} + + + )}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx index d188a172d746b..fb5e636902780 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx @@ -8,17 +8,24 @@ import React, { FunctionComponent, useState } from 'react'; import { EuiSpacer, EuiSwitch, EuiSwitchProps } from '@elastic/eui'; export interface Props extends Omit { - initialValue: boolean; + children: (() => JSX.Element) | JSX.Element | JSX.Element[] | undefined; + checked?: boolean; + initialValue?: boolean; onChange?: (nextValue: boolean) => void; } export const ToggleableField: FunctionComponent = ({ initialValue, + checked, onChange, children, ...restProps }) => { - const [isContentVisible, setIsContentVisible] = useState(initialValue); + const [uncontrolledIsContentVisible, setUncontrolledIsContentVisible] = useState( + initialValue ?? false + ); + + const isContentVisible = Boolean(checked ?? uncontrolledIsContentVisible); return ( <> @@ -27,14 +34,14 @@ export const ToggleableField: FunctionComponent = ({ checked={isContentVisible} onChange={(e) => { const nextValue = e.target.checked; - setIsContentVisible(nextValue); + setUncontrolledIsContentVisible(nextValue); if (onChange) { onChange(nextValue); } }} /> - {isContentVisible ? children : null} + {isContentVisible ? (typeof children === 'function' ? children() : children) : null} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index 4c0cc2c8957e1..b65e161685985 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -9,6 +9,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; import { useKibana, attemptToURIDecode } from '../../../shared_imports'; import { useLoadPoliciesList } from '../../services/api'; @@ -40,7 +41,7 @@ export const EditPolicy: React.FunctionComponent { const { - services: { breadcrumbService }, + services: { breadcrumbService, license }, } = useKibana(); const { error, isLoading, data: policies, resendRequest } = useLoadPoliciesList(false); @@ -100,6 +101,9 @@ export const EditPolicy: React.FunctionComponent license.hasAtLeast(MIN_SEARCHABLE_SNAPSHOT_LICENSE), + }, }} > diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 1e462dcb680f2..97e4c3ddf4a87 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -30,7 +30,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { useForm, Form, UseField, TextField, useFormData } from '../../../shared_imports'; +import { useForm, UseField, TextField, useFormData } from '../../../shared_imports'; import { toasts } from '../../services/notification'; @@ -45,7 +45,7 @@ import { WarmPhase, } from './components'; -import { schema, deserializer, createSerializer, createPolicyNameValidations } from './form'; +import { schema, deserializer, createSerializer, createPolicyNameValidations, Form } from './form'; import { useEditPolicyContext } from './edit_policy_context'; import { FormInternal } from './types'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx index da5f940b1b6c8..f7b9b1af1ee3a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx @@ -14,6 +14,9 @@ export interface EditPolicyContextValue { policy: SerializedPolicy; existingPolicies: PolicyFromES[]; getUrlForApp: ApplicationStart['getUrlForApp']; + license: { + canUseSearchableSnapshot: () => boolean; + }; policyName?: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx new file mode 100644 index 0000000000000..2b3411e394a90 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +import { Form as LibForm, FormHook } from '../../../../../shared_imports'; + +import { ConfigurationIssuesProvider } from '../configuration_issues_context'; + +interface Props { + form: FormHook; +} + +export const Form: FunctionComponent = ({ form, children }) => ( + + {children} + +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts new file mode 100644 index 0000000000000..15d8d4ed272e5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts @@ -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 { Form } from './form'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx new file mode 100644 index 0000000000000..c31eb5bdaa329 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -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 { get } from 'lodash'; +import React, { FunctionComponent, createContext, useContext } from 'react'; +import { useFormData } from '../../../../shared_imports'; + +export interface ConfigurationIssues { + isUsingForceMergeInHotPhase: boolean; + /** + * If this value is true, phases after hot cannot set shrink, forcemerge, freeze, or + * searchable_snapshot actions. + * + * See https://github.com/elastic/elasticsearch/blob/master/docs/reference/ilm/actions/ilm-searchable-snapshot.asciidoc. + */ + isUsingSearchableSnapshotInHotPhase: boolean; +} + +const ConfigurationIssuesContext = createContext(null as any); + +const pathToHotPhaseSearchableSnapshot = + 'phases.hot.actions.searchable_snapshot.snapshot_repository'; + +const pathToHotForceMerge = 'phases.hot.actions.forcemerge.max_num_segments'; + +export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { + const [formData] = useFormData({ + watch: [pathToHotPhaseSearchableSnapshot, pathToHotForceMerge], + }); + return ( + + {children} + + ); +}; + +export const useConfigurationIssues = () => { + const ctx = useContext(ConfigurationIssuesContext); + if (!ctx) + throw new Error('Cannot use configuration issues outside of configuration issues context'); + + return ctx; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index df5d6e2f80c15..04d4fbef9939e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -26,7 +26,7 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { }, warm: { enabled: Boolean(warm), - warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), + warmPhaseOnRollover: warm === undefined ? true : Boolean(warm.min_age === '0ms'), bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', dataTierAllocationType: determineDataTierAllocationType(warm?.actions), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index edff72dccc6dd..bafe6c15d9dca 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -76,6 +76,10 @@ const originalPolicy: SerializedPolicy = { set_priority: { priority: 12, }, + searchable_snapshot: { + snapshot_repository: 'my repo!', + force_merge_index: false, + }, }, }, delete: { @@ -209,6 +213,16 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.min_age).toBeUndefined(); }); + it('removes snapshot_repository when it is unset', () => { + delete formInternal.phases.hot!.actions.searchable_snapshot; + delete formInternal.phases.cold!.actions.searchable_snapshot; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.searchable_snapshot).toBeUndefined(); + expect(result.phases.cold!.actions.searchable_snapshot).toBeUndefined(); + }); + it('correctly serializes a minimal policy', () => { policy = cloneDeep(originalMinimalPolicy); const formInternalPolicy = cloneDeep(originalMinimalPolicy); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 82fa478832582..66fe498cbac87 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -11,3 +11,10 @@ export { createSerializer } from './serializer'; export { schema } from './schema'; export * from './validations'; + +export { Form } from './components'; + +export { + ConfigurationIssuesProvider, + useConfigurationIssues, +} from './configuration_issues_context'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 0ad2d923117f4..cedf1cdb4d9fe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -287,6 +287,14 @@ export const schema: FormSchema = { serializer: serializers.stringToNumber, }, }, + searchable_snapshot: { + snapshot_repository: { + label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, + validations: [ + { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, + ], + }, + }, }, }, delete: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index c543fef05733a..2071d1be523b6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -68,6 +68,10 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (!updatedPolicy.phases.hot!.actions?.set_priority) { delete hotPhaseActions.set_priority; } + + if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) { + delete hotPhaseActions.searchable_snapshot; + } } /** @@ -137,6 +141,10 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (!updatedPolicy.phases.cold?.actions?.set_priority) { delete coldPhase.actions.set_priority; } + + if (!updatedPolicy.phases.cold?.actions?.searchable_snapshot) { + delete coldPhase.actions.searchable_snapshot; + } } else { delete draft.phases.cold; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index ccd5d3a568fe3..f787f2661aa5c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -8,6 +8,23 @@ import { i18n } from '@kbn/i18n'; export const i18nTexts = { editPolicy: { + searchableSnapshotInHotPhase: { + searchableSnapshotDisallowed: { + calloutTitle: i18n.translate( + 'xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutTitle', + { + defaultMessage: 'Searchable snapshot disabled', + } + ), + calloutBody: i18n.translate( + 'xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutBody', + { + defaultMessage: + 'To use searchable snapshot in this phase you must disable searchable snapshot in the hot phase.', + } + ), + }, + }, forceMergeEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', { defaultMessage: 'Force merge data', }), @@ -46,6 +63,12 @@ export const i18nTexts = { defaultMessage: 'Select a node attribute', } ), + searchableSnapshotsFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel', + { + defaultMessage: 'Searchable snapshot repository', + } + ), errors: { numberRequired: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage', @@ -134,6 +157,12 @@ export const i18nTexts = { defaultMessage: 'A policy name cannot be longer than 255 bytes.', } ), + searchableSnapshotRepoRequired: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError', + { + defaultMessage: 'A snapshot repository name is required.', + } + ), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index f63c62e1fc529..8f1a4d733887f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -6,7 +6,12 @@ import { METRIC_TYPE } from '@kbn/analytics'; -import { PolicyFromES, SerializedPolicy, ListNodesRouteResponse } from '../../../common/types'; +import { + PolicyFromES, + SerializedPolicy, + ListNodesRouteResponse, + ListSnapshotReposResponse, +} from '../../../common/types'; import { UIM_POLICY_DELETE, @@ -112,3 +117,10 @@ export const useLoadSnapshotPolicies = () => { initialData: [], }); }; + +export const useLoadSnapshotRepositories = () => { + return useRequest({ + path: `snapshot_repositories`, + method: 'get', + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index deef5cfe6ef2c..e0b4ac6d848b6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; -import { CoreSetup, PluginInitializerContext } from 'src/core/public'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { PLUGIN } from '../common/constants'; import { init as initHttp } from './application/services/http'; @@ -14,15 +14,16 @@ import { init as initUiMetric } from './application/services/ui_metric'; import { init as initNotification } from './application/services/notification'; import { BreadcrumbService } from './application/services/breadcrumbs'; import { addAllExtensions } from './extend_index_management'; -import { ClientConfigType, SetupDependencies } from './types'; +import { ClientConfigType, SetupDependencies, StartDependencies } from './types'; import { registerUrlGenerator } from './url_generator'; -export class IndexLifecycleManagementPlugin { +export class IndexLifecycleManagementPlugin + implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private breadcrumbService = new BreadcrumbService(); - public setup(coreSetup: CoreSetup, plugins: SetupDependencies) { + public setup(coreSetup: CoreSetup, plugins: SetupDependencies) { const { ui: { enabled: isIndexLifecycleManagementUiEnabled }, } = this.initializerContext.config.get(); @@ -47,7 +48,7 @@ export class IndexLifecycleManagementPlugin { title: PLUGIN.TITLE, order: 2, mount: async ({ element, history, setBreadcrumbs }) => { - const [coreStart] = await getStartServices(); + const [coreStart, { licensing }] = await getStartServices(); const { chrome: { docTitle }, i18n: { Context: I18nContext }, @@ -55,6 +56,8 @@ export class IndexLifecycleManagementPlugin { application: { navigateToApp, getUrlForApp }, } = coreStart; + const license = await licensing.license$.pipe(first()).toPromise(); + docTitle.change(PLUGIN.TITLE); this.breadcrumbService.setup(setBreadcrumbs); @@ -72,6 +75,7 @@ export class IndexLifecycleManagementPlugin { navigateToApp, getUrlForApp, this.breadcrumbService, + license, cloud ); diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index a5844af0bf6dd..4cb5d95239408 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -11,6 +11,7 @@ export { useForm, useFormData, Form, + FormHook, UseField, FieldConfig, OnFormUpdateArg, diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 1ce43957b1444..9107dcc9f2e9a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -8,18 +8,23 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; -import { CloudSetup } from '../../cloud/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; +import { CloudSetup } from '../../cloud/public'; +import { LicensingPluginStart, ILicense } from '../../licensing/public'; + import { BreadcrumbService } from './application/services/breadcrumbs'; export interface SetupDependencies { usageCollection?: UsageCollectionSetup; management: ManagementSetup; - cloud?: CloudSetup; indexManagement?: IndexManagementPluginSetup; - home?: HomePublicPluginSetup; share: SharePluginSetup; + cloud?: CloudSetup; + home?: HomePublicPluginSetup; +} +export interface StartDependencies { + licensing: LicensingPluginStart; } export interface ClientConfigType { @@ -30,5 +35,6 @@ export interface ClientConfigType { export interface AppServicesContext { breadcrumbService: BreadcrumbService; + license: ILicense; cloud?: CloudSetup; } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/index.ts new file mode 100644 index 0000000000000..d61b30a4e0ebe --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerFetchRoute } from './register_fetch_route'; +import { RouteDependencies } from '../../../types'; + +export const registerSnapshotRepositoriesRoutes = (deps: RouteDependencies) => { + registerFetchRoute(deps); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/register_fetch_route.ts new file mode 100644 index 0000000000000..f3097f1f39ec9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/register_fetch_route.ts @@ -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 { i18n } from '@kbn/i18n'; + +import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; +import { ListSnapshotReposResponse } from '../../../../common/types'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; +import { handleEsError } from '../../../shared_imports'; + +export const registerFetchRoute = ({ router, license }: RouteDependencies) => { + router.get( + { path: addBasePath('/snapshot_repositories'), validate: false }, + async (ctx, request, response) => { + if (!license.isCurrentLicenseAtLeast(MIN_SEARCHABLE_SNAPSHOT_LICENSE)) { + return response.forbidden({ + body: i18n.translate('xpack.indexLifecycleMgmt.searchSnapshotlicenseCheckErrorMessage', { + defaultMessage: + 'Use of searchable snapshots requires at least an enterprise level license.', + }), + }); + } + + try { + const esResult = await ctx.core.elasticsearch.client.asCurrentUser.snapshot.getRepository({ + repository: '*', + }); + const repos: ListSnapshotReposResponse = { + repositories: Object.keys(esResult.body), + }; + return response.ok({ body: repos }); + } catch (e) { + // If ES responds with 404 when looking up all snapshots we return an empty array + if (e?.statusCode === 404) { + const repos: ListSnapshotReposResponse = { + repositories: [], + }; + return response.ok({ body: repos }); + } + return handleEsError({ error: e, response }); + } + } + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts index f7390debbe177..6c450ea0d3c71 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts @@ -11,6 +11,7 @@ import { registerNodesRoutes } from './api/nodes'; import { registerPoliciesRoutes } from './api/policies'; import { registerTemplatesRoutes } from './api/templates'; import { registerSnapshotPoliciesRoutes } from './api/snapshot_policies'; +import { registerSnapshotRepositoriesRoutes } from './api/snapshot_repositories'; export function registerApiRoutes(dependencies: RouteDependencies) { registerIndexRoutes(dependencies); @@ -18,4 +19,5 @@ export function registerApiRoutes(dependencies: RouteDependencies) { registerPoliciesRoutes(dependencies); registerTemplatesRoutes(dependencies); registerSnapshotPoliciesRoutes(dependencies); + registerSnapshotRepositoriesRoutes(dependencies); } diff --git a/x-pack/plugins/index_lifecycle_management/server/services/license.ts b/x-pack/plugins/index_lifecycle_management/server/services/license.ts index 2d863e283d440..e7e05f480a21f 100644 --- a/x-pack/plugins/index_lifecycle_management/server/services/license.ts +++ b/x-pack/plugins/index_lifecycle_management/server/services/license.ts @@ -12,7 +12,7 @@ import { } from 'kibana/server'; import { LicensingPluginSetup } from '../../../licensing/server'; -import { LicenseType } from '../../../licensing/common/types'; +import { LicenseType, ILicense } from '../shared_imports'; export interface LicenseStatus { isValid: boolean; @@ -26,6 +26,7 @@ interface SetupSettings { } export class License { + private currentLicense: ILicense | undefined; private licenseStatus: LicenseStatus = { isValid: false, message: 'Invalid License', @@ -36,6 +37,7 @@ export class License { { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } ) { licensing.license$.subscribe((license) => { + this.currentLicense = license; const { state, message } = license.check(pluginId, minimumLicenseType); const hasRequiredLicense = state === 'valid'; @@ -76,6 +78,13 @@ export class License { }; } + isCurrentLicenseAtLeast(type: LicenseType): boolean { + if (!this.currentLicense) { + return false; + } + return this.currentLicense.hasAtLeast(type); + } + getStatus() { return this.licenseStatus; } diff --git a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts index 068cddcee4c86..18740d91a179c 100644 --- a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts @@ -5,3 +5,4 @@ */ export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { ILicense, LicenseType } from '../../licensing/common/types';