diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts index 2f7b75dfba57..69d1423f5f8f 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import './mocks'; import { setup as homeSetup } from './home.helpers'; import { setup as repositoryAddSetup } from './repository_add.helpers'; import { setup as repositoryEditSetup } from './repository_edit.helpers'; import { setup as policyAddSetup } from './policy_add.helpers'; import { setup as policyEditSetup } from './policy_edit.helpers'; +import { setup as restoreSnapshotSetup } from './restore_snapshot.helpers'; export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils'; @@ -20,4 +21,5 @@ export const pageHelpers = { repositoryEdit: { setup: repositoryEditSetup }, policyAdd: { setup: policyAddSetup }, policyEdit: { setup: policyEditSetup }, + restoreSnapshot: { setup: restoreSnapshotSetup }, }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/mocks.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/mocks.tsx new file mode 100644 index 000000000000..fc02452e3730 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/mocks.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +/* + * Mocking AutoSizer of the react-virtualized because it does not render children in JS DOM. + * This seems related to not being able to properly discover height and width. + */ +jest.mock('react-virtualized', () => { + const original = jest.requireActual('react-virtualized'); + + return { + ...original, + AutoSizer: ({ children }: { children: any }) => ( +
{children({ height: 500, width: 500 })}
+ ), + }; +}); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts index 131969b997b5..a3ab829ab642 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts @@ -41,6 +41,8 @@ export type PolicyFormTestSubjects = | 'allIndicesToggle' | 'backButton' | 'deselectIndicesLink' + | 'allDataStreamsToggle' + | 'deselectDataStreamLink' | 'expireAfterValueInput' | 'expireAfterUnitSelect' | 'ignoreUnavailableIndicesToggle' @@ -53,4 +55,5 @@ export type PolicyFormTestSubjects = | 'selectIndicesLink' | 'showAdvancedCronLink' | 'snapshotNameInput' + | 'dataStreamBadge' | 'submitButton'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts new file mode 100644 index 000000000000..0cfb6fbc9797 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; +import { RestoreSnapshot } from '../../../public/application/sections/restore_snapshot'; +import { WithAppDependencies } from './setup_environment'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/add_policy'], + componentRoutePath: '/add_policy', + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed( + WithAppDependencies(RestoreSnapshot), + testBedConfig +); + +const setupActions = (testBed: TestBed) => { + const { find } = testBed; + return { + findDataStreamCallout() { + return find('dataStreamWarningCallOut'); + }, + }; +}; + +type Actions = ReturnType; + +export type RestoreSnapshotTestBed = TestBed & { + actions: Actions; +}; + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: setupActions(testBed), + }; +}; + +export type RestoreSnapshotFormTestSubject = + | 'snapshotRestoreStepLogistics' + | 'dataStreamWarningCallOut'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx index c4f4876b8a1c..e3c0ab0be9bd 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx @@ -64,6 +64,14 @@ export const setupEnvironment = () => { }; }; +/** + * Suppress error messages about Worker not being available in JS DOM. + */ +(window as any).Worker = function Worker() { + this.postMessage = () => {}; + this.terminate = () => {}; +}; + export const WithAppDependencies = (Comp: any) => (props: any) => ( diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index a8e6e976bb16..17a745fafcc2 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -3,11 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +// import helpers first, this also sets up the mocks +import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; + import { act } from 'react-dom/test-utils'; import * as fixtures from '../../test/fixtures'; -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; import { PolicyFormTestBed } from './helpers/policy_form.helpers'; import { DEFAULT_POLICY_SCHEDULE } from '../../public/application/constants'; @@ -37,7 +40,10 @@ describe('', () => { describe('on component mount', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [repository] }); - httpRequestsMockHelpers.setLoadIndicesResponse({ indices: ['my_index'] }); + httpRequestsMockHelpers.setLoadIndicesResponse({ + indices: ['my_index'], + dataStreams: ['my_data_stream', 'my_other_data_stream'], + }); testBed = await setup(); await nextTick(); @@ -96,7 +102,7 @@ describe('', () => { actions.clickNextButton(); }); - test('should require at least one index', async () => { + test('should require at least one index if no data streams are provided', async () => { const { find, form, component } = testBed; await act(async () => { @@ -109,7 +115,22 @@ describe('', () => { // Deselect all indices from list find('deselectIndicesLink').simulate('click'); - expect(form.getErrorsMessages()).toEqual(['You must select at least one index.']); + expect(form.getErrorsMessages()).toEqual([ + 'You must select at least one data stream or index.', + ]); + }); + + test('should correctly indicate data streams with a badge', async () => { + const { find, component, form } = testBed; + + await act(async () => { + // Toggle "All indices" switch + form.toggleEuiSwitch('allIndicesToggle', false); + await nextTick(); + }); + component.update(); + + expect(find('dataStreamBadge').length).toBe(2); }); }); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 297741755e88..7eec80890ca8 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -35,7 +35,10 @@ describe('', () => { describe('on component mount', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetPolicyResponse({ policy: POLICY_EDIT }); - httpRequestsMockHelpers.setLoadIndicesResponse({ indices: ['my_index'] }); + httpRequestsMockHelpers.setLoadIndicesResponse({ + indices: ['my_index'], + dataStreams: ['my_data_stream'], + }); httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [{ name: POLICY_EDIT.repository }], }); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts new file mode 100644 index 000000000000..17d714c07429 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.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 { nextTick, pageHelpers, setupEnvironment } from './helpers'; +import { RestoreSnapshotTestBed } from './helpers/restore_snapshot.helpers'; +import * as fixtures from '../../test/fixtures'; + +const { + restoreSnapshot: { setup }, +} = pageHelpers; + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: RestoreSnapshotTestBed; + + afterAll(() => { + server.restore(); + }); + describe('with data streams', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + testBed = await setup(); + await nextTick(); + testBed.component.update(); + }); + + it('shows the data streams warning when the snapshot has data streams', () => { + const { exists } = testBed; + expect(exists('dataStreamWarningCallOut')).toBe(true); + }); + }); + + describe('without data streams', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot({ totalDataStreams: 0 })); + testBed = await setup(); + await nextTick(); + testBed.component.update(); + }); + + it('hides the data streams warning when the snapshot has data streams', () => { + const { exists } = testBed; + expect(exists('dataStreamWarningCallOut')).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/common/lib/index.ts b/x-pack/plugins/snapshot_restore/common/lib/index.ts index 579dae026593..eaec8054a93a 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/index.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/index.ts @@ -16,3 +16,5 @@ export { serializeSnapshotRetention, } from './snapshot_serialization'; export { deserializePolicy, serializePolicy } from './policy_serialization'; +export { csvToArray } from './utils'; +export { isDataStreamBackingIndex } from './is_data_stream_backing_index'; diff --git a/x-pack/plugins/snapshot_restore/common/lib/is_data_stream_backing_index.ts b/x-pack/plugins/snapshot_restore/common/lib/is_data_stream_backing_index.ts new file mode 100644 index 000000000000..3b937670362f --- /dev/null +++ b/x-pack/plugins/snapshot_restore/common/lib/is_data_stream_backing_index.ts @@ -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. + */ + +/** + * @remark + * WARNING! + * + * This is a very hacky way of determining whether an index is a backing index. + * + * We only do this so that we can show users during a snapshot restore workflow + * that an index is part of a data stream. At the moment there is no way for us + * to get this information from the snapshot itself, even though it contains the + * metadata for the data stream that information is fully opaque to us until after + * we have done the snapshot restore. + * + * Issue for tracking this discussion here: https://github.com/elastic/elasticsearch/issues/58890 + */ +export const isDataStreamBackingIndex = (indexName: string) => { + return indexName.startsWith('.ds'); +}; diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index 298fc235fd9c..473a3392deb3 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -97,6 +97,7 @@ describe('deserializeSnapshotDetails', () => { version: 'version', // Indices are sorted. indices: ['index1', 'index2', 'index3'], + dataStreams: [], includeGlobalState: false, // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. indexFailures: [ diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index a636cc1f6326..a85b49430eec 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -17,6 +17,8 @@ import { import { deserializeTime, serializeTime } from './time_serialization'; +import { csvToArray } from './utils'; + export function deserializeSnapshotDetails( repository: string, snapshotDetailsEs: SnapshotDetailsEs, @@ -33,6 +35,7 @@ export function deserializeSnapshotDetails( version_id: versionId, version, indices = [], + data_streams: dataStreams = [], include_global_state: includeGlobalState, state, start_time: startTime, @@ -77,6 +80,7 @@ export function deserializeSnapshotDetails( versionId, version, indices: [...indices].sort(), + dataStreams: [...dataStreams].sort(), includeGlobalState, state, startTime, @@ -127,8 +131,10 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs { const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig; + const indicesArray = csvToArray(indices); + const snapshotConfigEs: SnapshotConfigEs = { - indices, + indices: indicesArray, ignore_unavailable: ignoreUnavailable, include_global_state: includeGlobalState, partial, diff --git a/x-pack/plugins/snapshot_restore/common/lib/utils.ts b/x-pack/plugins/snapshot_restore/common/lib/utils.ts new file mode 100644 index 000000000000..96eb7cb6908d --- /dev/null +++ b/x-pack/plugins/snapshot_restore/common/lib/utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const csvToArray = (indices?: string | string[]): string[] => { + return indices && Array.isArray(indices) + ? indices + : typeof indices === 'string' + ? indices.split(',') + : []; +}; diff --git a/x-pack/plugins/snapshot_restore/common/types/index.ts b/x-pack/plugins/snapshot_restore/common/types/index.ts index d52584ca737a..a12ae904cfee 100644 --- a/x-pack/plugins/snapshot_restore/common/types/index.ts +++ b/x-pack/plugins/snapshot_restore/common/types/index.ts @@ -8,3 +8,4 @@ export * from './repository'; export * from './snapshot'; export * from './restore'; export * from './policy'; +export * from './indices'; diff --git a/x-pack/plugins/snapshot_restore/common/types/indices.ts b/x-pack/plugins/snapshot_restore/common/types/indices.ts new file mode 100644 index 000000000000..5e4f2b5fdc16 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/common/types/indices.ts @@ -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 interface PolicyIndicesResponse { + indices: string[]; + dataStreams: string[]; +} diff --git a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts index a46f5c7921bf..1ff058e15538 100644 --- a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts @@ -30,6 +30,7 @@ export interface SnapshotDetails { versionId: number; version: string; indices: string[]; + dataStreams: string[]; includeGlobalState: boolean; state: string; /** e.g. '2019-04-05T21:56:40.438Z' */ @@ -52,6 +53,7 @@ export interface SnapshotDetailsEs { version_id: number; version: string; indices: string[]; + data_streams?: string[]; include_global_state: boolean; state: string; /** e.g. '2019-04-05T21:56:40.438Z' */ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_indices_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_indices_list.tsx deleted file mode 100644 index 1d8ee726f4cc..000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_indices_list.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * 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, { useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; -interface Props { - indices: string[] | string | undefined; -} - -export const CollapsibleIndicesList: React.FunctionComponent = ({ indices }) => { - const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState(false); - const displayIndices = indices - ? typeof indices === 'string' - ? indices.split(',') - : indices - : undefined; - const hiddenIndicesCount = - displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0; - return ( - <> - {displayIndices ? ( - <> - -
    - {(isShowingFullIndicesList ? displayIndices : [...displayIndices].splice(0, 10)).map( - (index) => ( -
  • - - {index} - -
  • - ) - )} -
-
- {hiddenIndicesCount ? ( - <> - - - isShowingFullIndicesList - ? setIsShowingFullIndicesList(false) - : setIsShowingFullIndicesList(true) - } - > - {isShowingFullIndicesList ? ( - - ) : ( - - )}{' '} - - - - ) : null} - - ) : ( - - )} - - ); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx new file mode 100644 index 000000000000..ce1bd7c8d6e4 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; + +import { useCollapsibleList } from './use_collapsible_list'; + +interface Props { + dataStreams: string[] | string | undefined; +} + +export const CollapsibleDataStreamsList: React.FunctionComponent = ({ dataStreams }) => { + const { isShowingFullList, setIsShowingFullList, items, hiddenItemsCount } = useCollapsibleList({ + items: dataStreams, + }); + + return items === 'all' ? ( + + ) : ( + <> + +
    + {items.map((dataStream) => ( +
  • + + {dataStream} + +
  • + ))} +
+
+ {hiddenItemsCount ? ( + <> + + + isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true) + } + > + {isShowingFullList ? ( + + ) : ( + + )}{' '} + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx new file mode 100644 index 000000000000..ff676a369694 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; + +import { useCollapsibleList } from './use_collapsible_list'; + +interface Props { + indices: string[] | string | undefined; +} + +export const CollapsibleIndicesList: React.FunctionComponent = ({ indices }) => { + const { hiddenItemsCount, isShowingFullList, items, setIsShowingFullList } = useCollapsibleList({ + items: indices, + }); + return items === 'all' ? ( + + ) : ( + <> + +
    + {items.map((index) => ( +
  • + + {index} + +
  • + ))} +
+
+ {hiddenItemsCount ? ( + <> + + + isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true) + } + > + {isShowingFullList ? ( + + ) : ( + + )}{' '} + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts new file mode 100644 index 000000000000..d58edc983c54 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts @@ -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 { CollapsibleIndicesList } from './collapsible_indices_list'; +export { CollapsibleDataStreamsList } from './collapsible_data_streams_list'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts new file mode 100644 index 000000000000..bdeb801117de --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.test.ts @@ -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 { renderHook } from '@testing-library/react-hooks'; + +import { useCollapsibleList } from './use_collapsible_list'; + +describe('useCollapseList', () => { + it('handles undefined', () => { + const { result } = renderHook(() => useCollapsibleList({ items: undefined })); + expect(result.current.items).toBe('all'); + expect(result.current.hiddenItemsCount).toBe(0); + }); + + it('handles csv', () => { + const { result } = renderHook(() => useCollapsibleList({ items: 'a,b,c' })); + expect(result.current.items).toEqual(['a', 'b', 'c']); + expect(result.current.hiddenItemsCount).toBe(0); + }); + + it('hides items passed a defined maximum (10)', () => { + const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']; + const { result } = renderHook(() => useCollapsibleList({ items })); + expect(result.current.items).toEqual(items.slice(0, -1)); + expect(result.current.hiddenItemsCount).toBe(1); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts new file mode 100644 index 000000000000..275915c5760a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts @@ -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 { useState } from 'react'; +import { csvToArray } from '../../../../common/lib'; + +type ChildItems = string[] | 'all'; + +interface Arg { + items: string[] | string | undefined; +} + +export interface ReturnValue { + items: ChildItems; + hiddenItemsCount: number; + isShowingFullList: boolean; + setIsShowingFullList: (showAll: boolean) => void; +} + +const maximumItemPreviewCount = 10; + +export const useCollapsibleList = ({ items }: Arg): ReturnValue => { + const [isShowingFullList, setIsShowingFullList] = useState(false); + const itemsArray = csvToArray(items); + const displayItems: ChildItems = + items === undefined + ? 'all' + : itemsArray.slice(0, isShowingFullList ? Infinity : maximumItemPreviewCount); + + const hiddenItemsCount = + itemsArray.length > maximumItemPreviewCount ? itemsArray.length - maximumItemPreviewCount : 0; + + return { + items: displayItems, + hiddenItemsCount, + setIsShowingFullList, + isShowingFullList, + }; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/data_stream_badge.tsx b/x-pack/plugins/snapshot_restore/public/application/components/data_stream_badge.tsx new file mode 100644 index 000000000000..e7d3f59bd567 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/data_stream_badge.tsx @@ -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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiBadge } from '@elastic/eui'; + +export const DataStreamBadge: FunctionComponent = () => { + return ( + + {i18n.translate('xpack.snapshotRestore.policyForm.setSettings.dataStreamBadgeContent', { + defaultMessage: 'Data stream', + })} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/index.ts index f5bb89238987..91266aae66e2 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/index.ts @@ -15,7 +15,7 @@ export { SnapshotDeleteProvider } from './snapshot_delete_provider'; export { RestoreSnapshotForm } from './restore_snapshot_form'; export { PolicyExecuteProvider } from './policy_execute_provider'; export { PolicyDeleteProvider } from './policy_delete_provider'; -export { CollapsibleIndicesList } from './collapsible_indices_list'; +export { CollapsibleIndicesList, CollapsibleDataStreamsList } from './collapsible_lists'; export { RetentionSettingsUpdateModalProvider, UpdateRetentionSettings, diff --git a/x-pack/plugins/snapshot_restore/public/application/components/lib/helpers.ts b/x-pack/plugins/snapshot_restore/public/application/components/lib/helpers.ts new file mode 100644 index 000000000000..f21576778a0e --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/lib/helpers.ts @@ -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. + */ + +export const orderDataStreamsAndIndices = ({ + dataStreams, + indices, +}: { + dataStreams: D[]; + indices: D[]; +}) => { + return dataStreams.concat(indices); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/lib/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/lib/index.ts new file mode 100644 index 000000000000..a40695e9a20e --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/lib/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 { orderDataStreamsAndIndices } from './helpers'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx index f9cad7cc4e07..3e1fb9b6500b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx @@ -27,6 +27,7 @@ import { PolicyNavigation } from './navigation'; interface Props { policy: SlmPolicyPayload; + dataStreams: string[]; indices: string[]; currentUrl: string; isEditing?: boolean; @@ -39,6 +40,7 @@ interface Props { export const PolicyForm: React.FunctionComponent = ({ policy: originalPolicy, + dataStreams, indices, currentUrl, isEditing, @@ -71,6 +73,8 @@ export const PolicyForm: React.FunctionComponent = ({ }, }); + const isEditingManagedPolicy = Boolean(isEditing && policy.isManagedPolicy); + // Policy validation state const [validation, setValidation] = useState({ isValid: true, @@ -132,6 +136,7 @@ export const PolicyForm: React.FunctionComponent = ({ = ({ {currentStep === lastStep ? ( savePolicy()} isLoading={isSaving} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts index 8b251de80a8e..a79a6ecb42e4 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/index.ts @@ -10,6 +10,7 @@ import { PolicyValidation } from '../../../services/validation'; export interface StepProps { policy: SlmPolicyPayload; indices: string[]; + dataStreams: string[]; updatePolicy: (updatedSettings: Partial, validationHelperData?: any) => void; isEditing: boolean; currentUrl: string; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx index b2422be3b78c..6b253a3fada0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx @@ -22,7 +22,7 @@ import { import { serializePolicy } from '../../../../../common/lib'; import { useServices } from '../../../app_context'; import { StepProps } from './'; -import { CollapsibleIndicesList } from '../../collapsible_indices_list'; +import { CollapsibleIndicesList } from '../../collapsible_lists'; export const PolicyStepReview: React.FunctionComponent = ({ policy, @@ -148,8 +148,8 @@ export const PolicyStepReview: React.FunctionComponent = ({ @@ -187,8 +187,8 @@ export const PolicyStepReview: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx deleted file mode 100644 index 07a627231230..000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * 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, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescribedFormGroup, - EuiTitle, - EuiFormRow, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiSwitch, - EuiLink, - EuiSelectable, - EuiPanel, - EuiComboBox, - EuiToolTip, -} from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui'; -import { SlmPolicyPayload, SnapshotConfig } from '../../../../../common/types'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; -import { StepProps } from './'; - -export const PolicyStepSettings: React.FunctionComponent = ({ - policy, - indices, - updatePolicy, - errors, -}) => { - const { i18n } = useServices(); - const { config = {}, isManagedPolicy } = policy; - - const updatePolicyConfig = (updatedFields: Partial): void => { - const newConfig = { ...config, ...updatedFields }; - updatePolicy({ - config: newConfig, - }); - }; - - // States for choosing all indices, or a subset, including caching previously chosen subset list - const [isAllIndices, setIsAllIndices] = useState(!Boolean(config.indices)); - const [indicesSelection, setIndicesSelection] = useState([...indices]); - const [indicesOptions, setIndicesOptions] = useState( - indices.map( - (index): EuiSelectableOption => ({ - label: index, - checked: - isAllIndices || - // If indices is a string, we default to custom input mode, so we mark individual indices - // as selected if user goes back to list mode - typeof config.indices === 'string' || - (Array.isArray(config.indices) && config.indices.includes(index)) - ? 'on' - : undefined, - }) - ) - ); - - // State for using selectable indices list or custom patterns - // Users with more than 100 indices will probably want to use an index pattern to select - // them instead, so we'll default to showing them the index pattern input. - const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>( - typeof config.indices === 'string' || - (Array.isArray(config.indices) && config.indices.length > 100) - ? 'custom' - : 'list' - ); - - // State for custom patterns - const [indexPatterns, setIndexPatterns] = useState( - typeof config.indices === 'string' ? config.indices.split(',') : [] - ); - - const renderIndicesField = () => { - const indicesSwitch = ( - - } - checked={isAllIndices} - disabled={isManagedPolicy} - data-test-subj="allIndicesToggle" - onChange={(e) => { - const isChecked = e.target.checked; - setIsAllIndices(isChecked); - if (isChecked) { - updatePolicyConfig({ indices: undefined }); - } else { - updatePolicyConfig({ - indices: - selectIndicesMode === 'custom' - ? indexPatterns.join(',') - : [...(indicesSelection || [])], - }); - } - }} - /> - ); - - return ( - -

- -

- - } - description={ - - } - fullWidth - > - - - {isManagedPolicy ? ( - - -

- } - > - {indicesSwitch} -
- ) : ( - indicesSwitch - )} - {isAllIndices ? null : ( - - - - - - - - { - setSelectIndicesMode('custom'); - updatePolicyConfig({ indices: indexPatterns.join(',') }); - }} - > - - - - - ) : ( - - - - - - { - setSelectIndicesMode('list'); - updatePolicyConfig({ indices: indicesSelection }); - }} - > - - - - - ) - } - helpText={ - selectIndicesMode === 'list' ? ( - 0 ? ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = undefined; - }); - updatePolicyConfig({ indices: [] }); - setIndicesSelection([]); - }} - > - - - ) : ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = 'on'; - }); - updatePolicyConfig({ indices: [...indices] }); - setIndicesSelection([...indices]); - }} - > - - - ), - }} - /> - ) : null - } - isInvalid={Boolean(errors.indices)} - error={errors.indices} - > - {selectIndicesMode === 'list' ? ( - { - const newSelectedIndices: string[] = []; - options.forEach(({ label, checked }) => { - if (checked === 'on') { - newSelectedIndices.push(label); - } - }); - setIndicesOptions(options); - updatePolicyConfig({ indices: newSelectedIndices }); - setIndicesSelection(newSelectedIndices); - }} - searchable - height={300} - > - {(list, search) => ( - - {search} - {list} - - )} - - ) : ( - ({ label: index }))} - placeholder={i18n.translate( - 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', - { - defaultMessage: 'Enter index patterns, i.e. logstash-*', - } - )} - selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))} - onCreateOption={(pattern: string) => { - if (!pattern.trim().length) { - return; - } - const newPatterns = [...indexPatterns, pattern]; - setIndexPatterns(newPatterns); - updatePolicyConfig({ - indices: newPatterns.join(','), - }); - }} - onChange={(patterns: Array<{ label: string }>) => { - const newPatterns = patterns.map(({ label }) => label); - setIndexPatterns(newPatterns); - updatePolicyConfig({ - indices: newPatterns.join(','), - }); - }} - /> - )} - - - )} -
-
-
- ); - }; - - const renderIgnoreUnavailableField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={Boolean(config.ignoreUnavailable)} - onChange={(e) => { - updatePolicyConfig({ - ignoreUnavailable: e.target.checked, - }); - }} - /> - -
- ); - - const renderPartialField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={Boolean(config.partial)} - onChange={(e) => { - updatePolicyConfig({ - partial: e.target.checked, - }); - }} - /> - -
- ); - - const renderIncludeGlobalStateField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={config.includeGlobalState === undefined || config.includeGlobalState} - onChange={(e) => { - updatePolicyConfig({ - includeGlobalState: e.target.checked, - }); - }} - /> - -
- ); - return ( -
- {/* Step title and doc link */} - - - -

- -

-
-
- - - - - - -
- - - {renderIndicesField()} - {renderIgnoreUnavailableField()} - {renderPartialField()} - {renderIncludeGlobalStateField()} -
- ); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts new file mode 100644 index 000000000000..e0d632a58e4e --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx new file mode 100644 index 000000000000..3570c74fb8fd --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + onSelectionChange: (selection: 'all' | 'none') => void; + selectedIndicesAndDataStreams: string[]; + indices: string[]; + dataStreams: string[]; +} + +export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({ + onSelectionChange, + selectedIndicesAndDataStreams, + indices, + dataStreams, +}) => { + if (selectedIndicesAndDataStreams.length === 0) { + return ( + { + onSelectionChange('all'); + }} + > + + + ), + }} + /> + ); + } + + const indicesCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (indices.includes(v) ? acc + 1 : acc), + 0 + ); + const dataStreamsCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc), + 0 + ); + + return ( + { + onSelectionChange('none'); + }} + > + + + ), + }} + /> + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts new file mode 100644 index 000000000000..9bf97af6400b --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { determineListMode } from './helpers'; + +describe('helpers', () => { + describe('determineListMode', () => { + test('list length (> 100)', () => { + expect( + determineListMode({ + indices: Array.from(Array(101).keys()).map(String), + dataStreams: [], + configuredIndices: undefined, + }) + ).toBe('custom'); + + // The length of indices and data streams are cumulative + expect( + determineListMode({ + indices: Array.from(Array(51).keys()).map(String), + dataStreams: Array.from(Array(51).keys()).map(String), + configuredIndices: undefined, + }) + ).toBe('custom'); + + // Other values should result in list mode + expect( + determineListMode({ + indices: [], + dataStreams: [], + configuredIndices: undefined, + }) + ).toBe('list'); + }); + + test('configured indices is a string', () => { + expect( + determineListMode({ + indices: [], + dataStreams: [], + configuredIndices: 'test', + }) + ).toBe('custom'); + }); + + test('configured indices not included in current indices and data streams', () => { + expect( + determineListMode({ + indices: ['a'], + dataStreams: ['b'], + configuredIndices: ['a', 'b', 'c'], + }) + ).toBe('custom'); + }); + + test('configured indices included in current indices and data streams', () => { + expect( + determineListMode({ + indices: ['a'], + dataStreams: ['b'], + configuredIndices: ['a', 'b'], + }) + ).toBe('list'); + }); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx new file mode 100644 index 000000000000..98ad2fe9c548 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { EuiSelectableOption } from '@elastic/eui'; +import { orderDataStreamsAndIndices } from '../../../../../lib'; +import { DataStreamBadge } from '../../../../../data_stream_badge'; + +export const mapSelectionToIndicesOptions = ({ + allSelected, + selection, + dataStreams, + indices, +}: { + allSelected: boolean; + selection: string[]; + dataStreams: string[]; + indices: string[]; +}): EuiSelectableOption[] => { + return orderDataStreamsAndIndices({ + dataStreams: dataStreams.map( + (dataStream): EuiSelectableOption => { + return { + label: dataStream, + append: , + checked: allSelected || selection.includes(dataStream) ? 'on' : undefined, + }; + } + ), + indices: indices.map( + (index): EuiSelectableOption => { + return { + label: index, + checked: allSelected || selection.includes(index) ? 'on' : undefined, + }; + } + ), + }); +}; + +/** + * @remark + * Users with more than 100 indices will probably want to use an index pattern to select + * them instead, so we'll default to showing them the index pattern input. Also show the custom + * list if we have no exact matches in the configured array to some existing index. + */ +export const determineListMode = ({ + configuredIndices, + indices, + dataStreams, +}: { + configuredIndices: string | string[] | undefined; + indices: string[]; + dataStreams: string[]; +}): 'custom' | 'list' => { + const indicesAndDataStreams = indices.concat(dataStreams); + return typeof configuredIndices === 'string' || + indicesAndDataStreams.length > 100 || + (Array.isArray(configuredIndices) && + // If not every past configured index maps to an existing index or data stream + // we also show the custom list + !configuredIndices.every((c) => indicesAndDataStreams.some((i) => i === c))) + ? 'custom' + : 'list'; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts new file mode 100644 index 000000000000..e0d632a58e4e --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx new file mode 100644 index 000000000000..94854905e668 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx @@ -0,0 +1,348 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiComboBox, + EuiDescribedFormGroup, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiPanel, + EuiSelectable, + EuiSelectableOption, + EuiSpacer, + EuiSwitch, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; + +import { SlmPolicyPayload } from '../../../../../../../../common/types'; +import { useServices } from '../../../../../../app_context'; +import { PolicyValidation } from '../../../../../../services/validation'; + +import { orderDataStreamsAndIndices } from '../../../../../lib'; +import { DataStreamBadge } from '../../../../../data_stream_badge'; + +import { mapSelectionToIndicesOptions, determineListMode } from './helpers'; + +import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; + +interface Props { + isManagedPolicy: boolean; + policy: SlmPolicyPayload; + indices: string[]; + dataStreams: string[]; + onUpdate: (arg: { indices?: string[] | string }) => void; + errors: PolicyValidation['errors']; +} + +/** + * In future we may be able to split data streams to its own field, but for now + * they share an array "indices" in the snapshot lifecycle policy config. See + * this github issue for progress: https://github.com/elastic/elasticsearch/issues/58474 + */ +export const IndicesAndDataStreamsField: FunctionComponent = ({ + isManagedPolicy, + dataStreams, + indices, + policy, + onUpdate, + errors, +}) => { + const { i18n } = useServices(); + const { config = {} } = policy; + + const indicesAndDataStreams = indices.concat(dataStreams); + + // We assume all indices if the config has no indices entry or if we receive an empty array + const [isAllIndices, setIsAllIndices] = useState( + !config.indices || (Array.isArray(config.indices) && config.indices.length === 0) + ); + + const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState( + () => + Array.isArray(config.indices) && !isAllIndices + ? indicesAndDataStreams.filter((i) => (config.indices! as string[]).includes(i)) + : [...indicesAndDataStreams] + ); + + // States for choosing all indices, or a subset, including caching previously chosen subset list + const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState< + EuiSelectableOption[] + >(() => + mapSelectionToIndicesOptions({ + selection: indicesAndDataStreamsSelection, + dataStreams, + indices, + allSelected: isAllIndices || typeof config.indices === 'string', + }) + ); + + // State for using selectable indices list or custom patterns + const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(() => + determineListMode({ configuredIndices: config.indices, dataStreams, indices }) + ); + + // State for custom patterns + const [indexPatterns, setIndexPatterns] = useState(() => + typeof config.indices === 'string' + ? (config.indices as string).split(',') + : Array.isArray(config.indices) && config.indices + ? config.indices + : [] + ); + + const indicesSwitch = ( + + } + checked={isAllIndices} + disabled={isManagedPolicy} + data-test-subj="allIndicesToggle" + onChange={(e) => { + const isChecked = e.target.checked; + setIsAllIndices(isChecked); + if (isChecked) { + setIndicesAndDataStreamsSelection(indicesAndDataStreams); + setIndicesAndDataStreamsOptions( + mapSelectionToIndicesOptions({ + allSelected: isAllIndices || typeof config.indices === 'string', + dataStreams, + indices, + selection: indicesAndDataStreamsSelection, + }) + ); + onUpdate({ indices: undefined }); + } else { + onUpdate({ + indices: + selectIndicesMode === 'custom' + ? indexPatterns.join(',') + : [...(indicesAndDataStreamsSelection || [])], + }); + } + }} + /> + ); + + return ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + {isManagedPolicy ? ( + + +

+ } + > + {indicesSwitch} +
+ ) : ( + indicesSwitch + )} + {isAllIndices ? null : ( + + + + + + + + { + setSelectIndicesMode('custom'); + onUpdate({ indices: indexPatterns.join(',') }); + }} + > + + + + + ) : ( + + + + + + { + setSelectIndicesMode('list'); + onUpdate({ indices: indicesAndDataStreamsSelection }); + }} + > + + + + + ) + } + helpText={ + selectIndicesMode === 'list' ? ( + { + if (selection === 'all') { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = 'on'; + }); + onUpdate({ indices: [...indicesAndDataStreams] }); + setIndicesAndDataStreamsSelection([...indicesAndDataStreams]); + } else { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = undefined; + }); + onUpdate({ indices: [] }); + setIndicesAndDataStreamsSelection([]); + } + }} + selectedIndicesAndDataStreams={indicesAndDataStreamsSelection} + indices={indices} + dataStreams={dataStreams} + /> + ) : null + } + isInvalid={Boolean(errors.indices)} + error={errors.indices} + > + {selectIndicesMode === 'list' ? ( + { + const newSelectedIndices: string[] = []; + options.forEach(({ label, checked }) => { + if (checked === 'on') { + newSelectedIndices.push(label); + } + }); + setIndicesAndDataStreamsOptions(options); + onUpdate({ indices: newSelectedIndices }); + setIndicesAndDataStreamsSelection(newSelectedIndices); + }} + searchable + height={300} + > + {(list, search) => ( + + {search} + {list} + + )} + + ) : ( + ({ + label: index, + value: { isDataStream: false }, + })), + dataStreams: dataStreams.map((dataStream) => ({ + label: dataStream, + value: { isDataStream: true }, + })), + })} + renderOption={({ label, value }) => { + if (value?.isDataStream) { + return ( + + {label} + + + + + ); + } + return label; + }} + placeholder={i18n.translate( + 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', + { + defaultMessage: 'Enter index patterns, i.e. logstash-*', + } + )} + selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))} + onCreateOption={(pattern: string) => { + if (!pattern.trim().length) { + return; + } + const newPatterns = [...indexPatterns, pattern]; + setIndexPatterns(newPatterns); + onUpdate({ + indices: newPatterns.join(','), + }); + }} + onChange={(patterns: Array<{ label: string }>) => { + const newPatterns = patterns.map(({ label }) => label); + setIndexPatterns(newPatterns); + onUpdate({ + indices: newPatterns.join(','), + }); + }} + /> + )} + + + )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts new file mode 100644 index 000000000000..24e9b36e7488 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/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 { PolicyStepSettings } from './step_settings'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx new file mode 100644 index 000000000000..9d43c45d17ea --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescribedFormGroup, + EuiTitle, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; + +import { SlmPolicyPayload } from '../../../../../../common/types'; +import { documentationLinksService } from '../../../../services/documentation'; +import { StepProps } from '../'; + +import { IndicesAndDataStreamsField } from './fields'; + +export const PolicyStepSettings: React.FunctionComponent = ({ + policy, + indices, + dataStreams, + updatePolicy, + errors, +}) => { + const { config = {}, isManagedPolicy } = policy; + + const updatePolicyConfig = (updatedFields: Partial): void => { + const newConfig = { ...config, ...updatedFields }; + updatePolicy({ + config: newConfig, + }); + }; + + const renderIgnoreUnavailableField = () => ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={Boolean(config.ignoreUnavailable)} + onChange={(e) => { + updatePolicyConfig({ + ignoreUnavailable: e.target.checked, + }); + }} + /> + +
+ ); + + const renderPartialField = () => ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={Boolean(config.partial)} + onChange={(e) => { + updatePolicyConfig({ + partial: e.target.checked, + }); + }} + /> + +
+ ); + + const renderIncludeGlobalStateField = () => ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={config.includeGlobalState === undefined || config.includeGlobalState} + onChange={(e) => { + updatePolicyConfig({ + includeGlobalState: e.target.checked, + }); + }} + /> + +
+ ); + return ( +
+ {/* Step title and doc link */} + + + +

+ +

+
+
+ + + + + + +
+ + + + + {renderIgnoreUnavailableField()} + {renderPartialField()} + {renderIncludeGlobalStateField()} +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts index 3f3db0ff28ec..182d4ef8f583 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts @@ -14,6 +14,6 @@ export interface StepProps { updateCurrentStep: (step: number) => void; } -export { RestoreSnapshotStepLogistics } from './step_logistics'; +export { RestoreSnapshotStepLogistics } from './step_logistics/step_logistics'; export { RestoreSnapshotStepSettings } from './step_settings'; export { RestoreSnapshotStepReview } from './step_review'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx new file mode 100644 index 000000000000..877dbe896392 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + onSelectionChange: (selection: 'all' | 'none') => void; + selectedIndicesAndDataStreams: string[]; + indices: string[]; + dataStreams: string[]; +} + +export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({ + onSelectionChange, + selectedIndicesAndDataStreams, + indices, + dataStreams, +}) => { + if (selectedIndicesAndDataStreams.length === 0) { + return ( + { + onSelectionChange('all'); + }} + > + + + ), + }} + /> + ); + } + + const indicesCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (indices.includes(v) ? acc + 1 : acc), + 0 + ); + const dataStreamsCount = selectedIndicesAndDataStreams.reduce( + (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc), + 0 + ); + + return ( + { + onSelectionChange('none'); + }} + > + + + ), + }} + /> + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx new file mode 100644 index 000000000000..64fce4dcfac4 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +import { documentationLinksService } from '../../../../services/documentation'; + +const i18nTexts = { + callout: { + title: (count: number) => + i18n.translate('xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.title', { + defaultMessage: + 'This snapshot contains {count, plural, one {a data stream} other {data streams}}', + values: { count }, + }), + body: () => ( + + {i18n.translate( + 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink', + { defaultMessage: 'Learn more' } + )} + + ), + }} + /> + ), + }, +}; + +interface Props { + dataStreamsCount: number; +} + +export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => { + return ( + + {i18nTexts.callout.body()} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts new file mode 100644 index 000000000000..8f4efcf2a91f --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/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 { RestoreSnapshotStepLogistics } from './step_logistics'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx similarity index 69% rename from x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx rename to x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index c80c5a2e4c01..d9fd4cca0d61 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -21,10 +21,22 @@ import { EuiComboBox, } from '@elastic/eui'; import { EuiSelectableOption } from '@elastic/eui'; -import { RestoreSettings } from '../../../../../common/types'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; -import { StepProps } from './'; + +import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib'; +import { RestoreSettings } from '../../../../../../common/types'; + +import { documentationLinksService } from '../../../../services/documentation'; + +import { useServices } from '../../../../app_context'; + +import { orderDataStreamsAndIndices } from '../../../lib'; +import { DataStreamBadge } from '../../../data_stream_badge'; + +import { StepProps } from '../index'; + +import { DataStreamsGlobalStateCallOut } from './data_streams_global_state_call_out'; + +import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({ snapshotDetails, @@ -34,10 +46,30 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = }) => { const { i18n } = useServices(); const { - indices: snapshotIndices, + indices: unfilteredSnapshotIndices, + dataStreams: snapshotDataStreams = [], includeGlobalState: snapshotIncludeGlobalState, } = snapshotDetails; + const snapshotIndices = unfilteredSnapshotIndices.filter( + (index) => !isDataStreamBackingIndex(index) + ); + const snapshotIndicesAndDataStreams = snapshotIndices.concat(snapshotDataStreams); + + const comboBoxOptions = orderDataStreamsAndIndices<{ + label: string; + value: { isDataStream: boolean; name: string }; + }>({ + dataStreams: snapshotDataStreams.map((dataStream) => ({ + label: dataStream, + value: { isDataStream: true, name: dataStream }, + })), + indices: snapshotIndices.map((index) => ({ + label: index, + value: { isDataStream: false, name: index }, + })), + }); + const { indices: restoreIndices, renamePattern, @@ -47,28 +79,50 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } = restoreSettings; // States for choosing all indices, or a subset, including caching previously chosen subset list - const [isAllIndices, setIsAllIndices] = useState(!Boolean(restoreIndices)); - const [indicesOptions, setIndicesOptions] = useState( - snapshotIndices.map( - (index): EuiSelectableOption => ({ - label: index, - checked: - isAllIndices || - // If indices is a string, we default to custom input mode, so we mark individual indices - // as selected if user goes back to list mode - typeof restoreIndices === 'string' || - (Array.isArray(restoreIndices) && restoreIndices.includes(index)) - ? 'on' - : undefined, - }) - ) + const [isAllIndicesAndDataStreams, setIsAllIndicesAndDataStreams] = useState( + !Boolean(restoreIndices) + ); + const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState< + EuiSelectableOption[] + >(() => + orderDataStreamsAndIndices({ + dataStreams: snapshotDataStreams.map( + (dataStream): EuiSelectableOption => ({ + label: dataStream, + append: , + checked: + isAllIndicesAndDataStreams || + // If indices is a string, we default to custom input mode, so we mark individual indices + // as selected if user goes back to list mode + typeof restoreIndices === 'string' || + (Array.isArray(restoreIndices) && restoreIndices.includes(dataStream)) + ? 'on' + : undefined, + }) + ), + indices: snapshotIndices.map( + (index): EuiSelectableOption => ({ + label: index, + checked: + isAllIndicesAndDataStreams || + // If indices is a string, we default to custom input mode, so we mark individual indices + // as selected if user goes back to list mode + typeof restoreIndices === 'string' || + (Array.isArray(restoreIndices) && restoreIndices.includes(index)) + ? 'on' + : undefined, + }) + ), + }) ); // State for using selectable indices list or custom patterns // Users with more than 100 indices will probably want to use an index pattern to select // them instead, so we'll default to showing them the index pattern input. const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>( - typeof restoreIndices === 'string' || snapshotIndices.length > 100 ? 'custom' : 'list' + typeof restoreIndices === 'string' || snapshotIndicesAndDataStreams.length > 100 + ? 'custom' + : 'list' ); // State for custom patterns @@ -83,13 +137,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = // Caching state for togglable settings const [cachedRestoreSettings, setCachedRestoreSettings] = useState({ - indices: [...snapshotIndices], + indices: [...snapshotIndicesAndDataStreams], renamePattern: '', renameReplacement: '', }); return ( -
+
{/* Step title and doc link */} @@ -118,6 +175,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = + + {snapshotDataStreams.length ? ( + <> + + + + ) : undefined} + {/* Indices */} @@ -126,16 +191,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =

} description={ } @@ -146,14 +211,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } - checked={isAllIndices} + checked={isAllIndicesAndDataStreams} onChange={(e) => { const isChecked = e.target.checked; - setIsAllIndices(isChecked); + setIsAllIndicesAndDataStreams(isChecked); if (isChecked) { updateRestoreSettings({ indices: undefined }); } else { @@ -166,7 +231,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } }} /> - {isAllIndices ? null : ( + {isAllIndicesAndDataStreams ? null : ( = @@ -210,8 +275,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = }} > @@ -220,52 +285,35 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } helpText={ selectIndicesMode === 'list' ? ( - 0 ? ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = undefined; - }); - updateRestoreSettings({ indices: [] }); - setCachedRestoreSettings({ - ...cachedRestoreSettings, - indices: [], - }); - }} - > - - - ) : ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: EuiSelectableOption) => { - option.checked = 'on'; - }); - updateRestoreSettings({ indices: [...snapshotIndices] }); - setCachedRestoreSettings({ - ...cachedRestoreSettings, - indices: [...snapshotIndices], - }); - }} - > - - - ), + { + if (selection === 'all') { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = 'on'; + }); + updateRestoreSettings({ + indices: [...snapshotIndicesAndDataStreams], + }); + setCachedRestoreSettings({ + ...cachedRestoreSettings, + indices: [...snapshotIndicesAndDataStreams], + }); + } else { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => { + option.checked = undefined; + }); + updateRestoreSettings({ indices: [] }); + setCachedRestoreSettings({ + ...cachedRestoreSettings, + indices: [], + }); + } }} + selectedIndicesAndDataStreams={csvToArray(restoreIndices)} + indices={snapshotIndices} + dataStreams={snapshotDataStreams} /> ) : null } @@ -275,7 +323,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = {selectIndicesMode === 'list' ? ( { const newSelectedIndices: string[] = []; options.forEach(({ label, checked }) => { @@ -283,7 +331,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = newSelectedIndices.push(label); } }); - setIndicesOptions(options); + setIndicesAndDataStreamsOptions(options); updateRestoreSettings({ indices: [...newSelectedIndices] }); setCachedRestoreSettings({ ...cachedRestoreSettings, @@ -302,7 +350,24 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = ) : ( ({ label: index }))} + options={comboBoxOptions} + renderOption={({ value }) => { + return value?.isDataStream ? ( + + {value.name} + + + + + ) : ( + value?.name + ); + }} placeholder={i18n.translate( 'xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder', { @@ -336,22 +401,22 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = - {/* Rename indices */} + {/* Rename data streams and indices */}

} description={ } fullWidth @@ -361,8 +426,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } checked={isRenamingIndices} @@ -405,7 +470,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = > { setCachedRestoreSettings({ ...cachedRestoreSettings, @@ -431,7 +496,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = > { setCachedRestoreSettings({ ...cachedRestoreSettings, diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx index 27a3717566d9..5dacba506fe1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx @@ -24,7 +24,7 @@ import { import { serializeRestoreSettings } from '../../../../../common/lib'; import { useServices } from '../../../app_context'; import { StepProps } from './'; -import { CollapsibleIndicesList } from '../../collapsible_indices_list'; +import { CollapsibleIndicesList } from '../../collapsible_lists/collapsible_indices_list'; export const RestoreSnapshotStepReview: React.FunctionComponent = ({ restoreSettings, @@ -73,8 +73,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx index 5f3ebf804c5e..b9a2d7e4b7cd 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx @@ -18,6 +18,7 @@ import { EuiSwitch, EuiTitle, EuiLink, + EuiCallOut, } from '@elastic/eui'; import { RestoreSettings } from '../../../../../common/types'; import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants'; @@ -28,10 +29,12 @@ import { StepProps } from './'; export const RestoreSnapshotStepSettings: React.FunctionComponent = ({ restoreSettings, updateRestoreSettings, + snapshotDetails, errors, }) => { const { i18n } = useServices(); const { indexSettings, ignoreIndexSettings } = restoreSettings; + const { dataStreams } = snapshotDetails; // State for index setting toggles const [isUsingIndexSettings, setIsUsingIndexSettings] = useState(Boolean(indexSettings)); @@ -96,6 +99,23 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( + {dataStreams?.length ? ( + <> + + + + + + ) : undefined} {/* Modify index settings */} diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx index 7bcee4f5f662..e69b0fad8014 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx @@ -236,8 +236,8 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx index 287a77493307..1a0c26c85449 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -22,6 +22,7 @@ import { DataPlaceholder, FormattedDateTime, CollapsibleIndicesList, + CollapsibleDataStreamsList, } from '../../../../../components'; import { linkToPolicy } from '../../../../../services/navigation'; import { SnapshotState } from './snapshot_state'; @@ -40,6 +41,7 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { // TODO: Add a tooltip explaining that: a false value means that the cluster global state // is not stored as part of the snapshot. includeGlobalState, + dataStreams, indices, state, startTimeInMillis, @@ -135,6 +137,22 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { + + + + + + + + + + + + diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx index 6d1a432be7f9..90cd26c821c5 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx @@ -25,13 +25,8 @@ export const PolicyAdd: React.FunctionComponent = ({ const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); - const { - error: errorLoadingIndices, - isLoading: isLoadingIndices, - data: { indices } = { - indices: [], - }, - } = useLoadIndices(); + const { error: errorLoadingIndices, isLoading: isLoadingIndices, data } = useLoadIndices(); + const { indices, dataStreams } = data ?? { indices: [], dataStreams: [] }; // Set breadcrumb and page title useEffect(() => { @@ -123,6 +118,7 @@ export const PolicyAdd: React.FunctionComponent = ({ { }; export const useLoadIndices = () => { - return useRequest({ + return useRequest({ path: `${API_BASE_PATH}policies/indices`, method: 'get', }); diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts index 27a565ccb74b..b4d0493098bb 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts @@ -18,6 +18,6 @@ export const sendRequest = (config: SendRequestConfig) => { return _sendRequest(httpService.httpClient, config); }; -export const useRequest = (config: UseRequestConfig) => { - return _useRequest(httpService.httpClient, config); +export const useRequest = (config: UseRequestConfig) => { + return _useRequest(httpService.httpClient, config); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts index 0720994ca766..24960b253323 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts @@ -48,6 +48,7 @@ export const validatePolicy = ( snapshotName: [], schedule: [], repository: [], + dataStreams: [], indices: [], expireAfterValue: [], minCount: [], @@ -106,7 +107,7 @@ export const validatePolicy = ( if (config && Array.isArray(config.indices) && config.indices.length === 0) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredErrorMessage', { - defaultMessage: 'You must select at least one index.', + defaultMessage: 'You must select at least one data stream or index.', }) ); } diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts index 5c1a1fbfab12..93e278e51f09 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts @@ -48,7 +48,7 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida if (Array.isArray(indices) && indices.length === 0) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.restoreValidation.indicesRequiredError', { - defaultMessage: 'You must select at least one index.', + defaultMessage: 'You must select at least one data stream or index.', }) ); } @@ -93,7 +93,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida 'xpack.snapshotRestore.restoreValidation.indexSettingsNotModifiableError', { defaultMessage: 'You can’t modify: {settings}', - // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299 values: { settings: unmodifiableSettings.map((setting: string, index: number) => index === 0 ? `${setting} ` : setting @@ -131,7 +130,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida validation.errors.ignoreIndexSettings.push( i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsNotRemovableError', { defaultMessage: 'You can’t reset: {settings}', - // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299 values: { settings: unremovableSettings.map((setting: string, index: number) => index === 0 ? `${setting} ` : setting diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index eb29b7bad37e..b96d305fa4a8 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -6,6 +6,7 @@ import { addBasePath } from '../helpers'; import { registerPolicyRoutes } from './policy'; import { RouterMock, routeDependencies, RequestMock } from '../../test/helpers'; +import { ResolveIndexResponseFromES } from '../../types'; describe('[Snapshot and Restore API Routes] Policy', () => { const mockEsPolicy = { @@ -324,27 +325,45 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }; it('should arrify and sort index names returned from ES', async () => { - const mockEsResponse = [ - { - index: 'fooIndex', - }, - { - index: 'barIndex', - }, - ]; + const mockEsResponse: ResolveIndexResponseFromES = { + indices: [ + { + name: 'fooIndex', + attributes: ['open'], + }, + { + name: 'barIndex', + attributes: ['open'], + data_stream: 'testDataStream', + }, + ], + aliases: [], + data_streams: [ + { + name: 'testDataStream', + backing_indices: ['barIndex'], + timestamp_field: '@timestamp', + }, + ], + }; router.callAsCurrentUserResponses = [mockEsResponse]; const expectedResponse = { - indices: ['barIndex', 'fooIndex'], + indices: ['fooIndex'], + dataStreams: ['testDataStream'], }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should return empty array if no indices returned from ES', async () => { - const mockEsResponse: any[] = []; + const mockEsResponse: ResolveIndexResponseFromES = { + indices: [], + aliases: [], + data_streams: [], + }; router.callAsCurrentUserResponses = [mockEsResponse]; - const expectedResponse = { indices: [] }; + const expectedResponse = { indices: [], dataStreams: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index 90667eda23b3..b8e701252955 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -5,10 +5,10 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { SlmPolicyEs } from '../../../common/types'; +import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types'; import { deserializePolicy, serializePolicy } from '../../../common/lib'; import { getManagedPolicyNames } from '../../lib'; -import { RouteDependencies } from '../../types'; +import { RouteDependencies, ResolveIndexResponseFromES } from '../../types'; import { addBasePath } from '../helpers'; import { nameParameterSchema, policySchema } from './validate_schemas'; @@ -232,17 +232,26 @@ export function registerPolicyRoutes({ const { callAsCurrentUser } = ctx.snapshotRestore!.client; try { - const indices: Array<{ - index: string; - }> = await callAsCurrentUser('cat.indices', { - format: 'json', - h: 'index', - }); + const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser( + 'transport.request', + { + method: 'GET', + path: `_resolve/index/*`, + query: { + expand_wildcards: 'all,hidden', + }, + } + ); + + const body: PolicyIndicesResponse = { + dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(), + indices: resolvedIndicesResponse.indices + .flatMap((index) => (index.data_stream ? [] : index.name)) + .sort(), + }; return res.ok({ - body: { - indices: indices.map(({ index }) => index).sort(), - }, + body, }); } catch (e) { if (isEsError(e)) { diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index f913299fc399..a7e61d1e7c02 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -15,6 +15,7 @@ const defaultSnapshot = { versionId: undefined, version: undefined, indices: [], + dataStreams: [], includeGlobalState: undefined, state: undefined, startTime: undefined, diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index 7794156eb1b8..8cfcaec1a2cd 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -31,4 +31,20 @@ export interface RouteDependencies { }; } +/** + * An object representing a resolved index, data stream or alias + */ +interface IndexAndAliasFromEs { + name: string; + // per https://github.com/elastic/elasticsearch/pull/57626 + attributes: Array<'open' | 'closed' | 'hidden' | 'frozen'>; + data_stream?: string; +} + +export interface ResolveIndexResponseFromES { + indices: IndexAndAliasFromEs[]; + aliases: IndexAndAliasFromEs[]; + data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; +} + export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts index d6a55579b322..e59f4689d9e3 100644 --- a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts @@ -13,13 +13,23 @@ export const getSnapshot = ({ state = 'SUCCESS', indexFailures = [], totalIndices = getRandomNumber(), -} = {}) => ({ + totalDataStreams = getRandomNumber(), +}: Partial<{ + repository: string; + snapshot: string; + uuid: string; + state: string; + indexFailures: any[]; + totalIndices: number; + totalDataStreams: number; +}> = {}) => ({ repository, snapshot, uuid, versionId: 8000099, version: '8.0.0', indices: new Array(totalIndices).fill('').map(getRandomString), + dataStreams: new Array(totalDataStreams).fill('').map(getRandomString), includeGlobalState: 1, state, startTime: '2019-05-23T06:25:15.896Z', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b5cdad5583e1..4c1572ddfcad 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13387,7 +13387,6 @@ "xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "いいえ", "xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "グローバルステータスを含める", "xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "はい", - "xpack.snapshotRestore.policyDetails.indicesLabel": "インデックス", "xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "「{snapshotName}」が進行中", "xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日付", "xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "ポリシー「{name}」の前回のエラーの詳細", @@ -13495,10 +13494,8 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "いいえ", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバルステータスを含める", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "はい", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "インデックス", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "ポリシー名", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "いいえ", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "部分シャードを許可", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "はい", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "レポジトリ", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "スケジュール", @@ -13507,7 +13504,6 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "スナップショット名", "xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "まとめ", "xpack.snapshotRestore.policyForm.stepReviewTitle": "レビューポリシー", - "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "システムインデックスを含むすべてのインデックス", "xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "すべて選択解除", "xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "スナップショット設定ドキュメント", "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "スナップショットの撮影時に利用不可能なインデックスを無視します。これが設定されていない場合、スナップショット全体がエラーになります。", @@ -13515,19 +13511,15 @@ "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "利用不可能なインデックスを無視", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "スナップショットの一部としてクラスターのグローバルステータスを格納します。", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "グローバルステータスを含める", - "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "バックアップするインデックスです。", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "インデックスパターン", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力", - "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "インデックス", "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "インデックスパターンを使用", - "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "インデックスを選択", "xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "クラウドで管理されたポリシーにはすべてのインデックスが必要です。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "利用不可能なプライマリシャードのインデックスのスナップショットを許可します。これが設定されていない場合、スナップショット全体がエラーになります。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "部分インデックスを許可", "xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "部分インデックスを許可", "xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "グローバルステータスを含める", "xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "すべて選択", - "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}がバックアップされます。{selectOrDeselectAllLink}", "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "インデックスを選択", "xpack.snapshotRestore.policyForm.stepSettingsTitle": "スナップショット設定", "xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "スナップショットライフサイクルポリシーを管理するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {missingPrivileges}。", @@ -13874,31 +13866,22 @@ "xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "インデックス設定", "xpack.snapshotRestore.restoreForm.nextButtonLabel": "次へ", "xpack.snapshotRestore.restoreForm.savingButtonLabel": "復元中...", - "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "システムインデックスを含むすべてのインデックス", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "すべて選択解除", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "スナップショットと復元ドキュメント", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続的な設定も復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "存在しない場合は、新しいインデックスを作成します。閉じていて、スナップショットインデックスと同じ数のシャードがある場合は、既存のインデックスを復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "インデックスパターン", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "インデックス", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "インデックスパターンを使用", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "インデックスを選択", "xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "すべてのシャードのスナップショットがないインデックスを復元できます。", "xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分復元", "xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分復元", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "復元時にインデックス名を変更します。", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "インデックス名の変更", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "インデックス名の変更", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "正規表現を使用", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "取り込みパターン", "xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "置換パターン", "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "すべて選択", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}が復元されます。{selectOrDeselectAllLink}", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "インデックスを選択", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "詳細を復元", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "実行する設定を復元", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON", @@ -13908,7 +13891,6 @@ "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "はい", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修正", - "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "インデックス", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "インデックス設定の修正はありません", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "いいえ", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分復元", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 568397912141..97f10e77dc71 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13392,7 +13392,6 @@ "xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "否", "xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "包括全局状态", "xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "是", - "xpack.snapshotRestore.policyDetails.indicesLabel": "索引", "xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "“{snapshotName}”正在进行中", "xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日期", "xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "策略“{name}”的上次失败详情", @@ -13500,10 +13499,8 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "否", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "包括全局状态", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "是", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "索引", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "策略名称", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "否", - "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "允许部分分片", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "是", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "存储库", "xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "计划", @@ -13512,7 +13509,6 @@ "xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "快照名称", "xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "总结", "xpack.snapshotRestore.policyForm.stepReviewTitle": "复查策略", - "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "所有索引,包括系统索引", "xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "取消全选", "xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "快照设置文档", "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "拍取快照时忽略不可用的索引。否则,整个快照将失败。", @@ -13520,19 +13516,15 @@ "xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "忽略不可用索引", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "将集群的全局状态存储为快照的一部分。", "xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "包括全局状态", - "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "要备份的索引。", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "索引模式", "xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*", - "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "索引", "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "使用索引模式", - "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "选择索引", "xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "云托管的策略需要所有索引。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "允许具有不可用主分片的索引的快照。否则,整个快照将失败。", "xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "允许部分索引", "xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "允许部分索引", "xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "包括全局状态", "xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "全选", - "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "将备份 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}", "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "选择索引", "xpack.snapshotRestore.policyForm.stepSettingsTitle": "快照设置", "xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "要管理快照生命周期策略,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", @@ -13879,31 +13871,22 @@ "xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "索引设置", "xpack.snapshotRestore.restoreForm.nextButtonLabel": "下一步", "xpack.snapshotRestore.restoreForm.savingButtonLabel": "正在还原……", - "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "所有索引,包括系统索引", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "取消全选", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "快照和还原文档", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "如果不存在,则创建新索引。如果现有索引已关闭且与快照索引有相同数目的分片,则还原现有索引。", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "索引模式", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "索引", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "使用索引模式", - "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "选择索引", "xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "允许还原不具有所有分片的快照的索引。", "xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分还原", "xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分还原", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "还原时重命名索引。", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "重命名索引", - "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "重命名索引", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "使用正则表达式", "xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "捕获模式", "xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "替换模式", "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "全选", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "将还原 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}", - "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "选择索引", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "还原详情", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "还原要执行的设置", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON", @@ -13913,7 +13896,6 @@ "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "鏄", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修改", - "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "索引", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "无索引设置修改", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "否", "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分还原",